Skip to main content

Overview

The Analytics API provides access to link performance data, including view counts, geographic information, and user agent details. All analytics are collected automatically when users click shortened links.

Collections

sptfy.in uses these PocketBase collections for analytics:

analytics

Detailed click-level data (user agent, country, timestamp)

viewList

Aggregated link data with view counts (utm_view field)

Analytics Record Structure

Each click on a shortened link creates a record in the analytics collection (unless from a bot).

Analytics Fields

id
string
PocketBase record ID
author
string
Link record ID (reference to random_short collection)
url_id
string
Link record ID (duplicate of author field)
utm_userAgent
string
Full user agent string of the visitorExample:
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
utm_country
string
Two-letter country code from Cloudflare (CF-IPCountry header)Example: US, GB, DE, JP
rawData
string
Copy of utm_country (for backwards compatibility)
created
string
ISO 8601 timestamp when the click occurredExample: 2024-01-15T10:30:00.000Z

Accessing Analytics Data

Analytics are accessed through PocketBase’s standard API endpoints. You need authentication to access analytics for your own links. Fetch all analytics records for a specific link.
curl -X GET "https://pb.sptfy.in/api/collections/analytics/records?filter=url_id='RECORD_ID'&sort=-created" \
  -H "Authorization: Bearer YOUR_AUTH_TOKEN"

Query Parameters

filter
string
PocketBase filter expression (e.g., url_id='abc123')
sort
string
Sort order (prefix with - for descending, e.g., -created)
perPage
number
default:"30"
Number of records per page
page
number
default:"1"
Page number

Response

{
  "page": 1,
  "perPage": 30,
  "totalPages": 2,
  "totalItems": 42,
  "items": [
    {
      "id": "analytics_record_id",
      "author": "link_record_id",
      "url_id": "link_record_id",
      "utm_userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)...",
      "utm_country": "US",
      "rawData": "US",
      "created": "2024-01-15T10:30:00.000Z",
      "updated": "2024-01-15T10:30:00.000Z"
    }
  ]
}

View Count

Each link in the viewList collection has a utm_view field that tracks total clicks.
curl -X GET "https://pb.sptfy.in/api/collections/viewList/records?filter=id_url='abc123'" \
  -H "Authorization: Bearer YOUR_AUTH_TOKEN"

Response

{
  "items": [
    {
      "id": "link_record_id",
      "id_url": "abc123",
      "from": "https://open.spotify.com/track/...",
      "utm_view": 42,
      "subdomain": "sptfy.in",
      "created": "2024-01-10T08:00:00.000Z"
    }
  ]
}
Fetch recently created links across the platform.
curl -X GET "https://pb.sptfy.in/api/collections/viewList/records?sort=-created&perPage=10&page=1"

Query Parameters

sort
string
default:"-created"
Sort by creation date (descending)
fields
string
default:"id_url,from,created,subdomain"
Comma-separated list of fields to return
perPage
number
default:"10"
Number of items per page
page
number
default:"1"
Page number
Fetch links sorted by view count (most popular first).
curl -X GET "https://pb.sptfy.in/api/collections/viewList/records?sort=-utm_view&filter=(utm_view>0)&perPage=10&page=1"

Query Parameters

sort
string
default:"-utm_view"
Sort by view count (descending)
filter
string
default:"(utm_view>0)"
Only include links with at least 1 view
fields
string
Fields to return: id_url, from, created, subdomain, utm_view
perPage
number
default:"10"
Number of items per page

Response

{
  "items": [
    {
      "id_url": "popular",
      "from": "https://open.spotify.com/track/...",
      "created": "2024-01-01T00:00:00.000Z",
      "subdomain": "sptfy.in",
      "utm_view": 1337
    },
    {
      "id_url": "viral",
      "from": "https://open.spotify.com/playlist/...",
      "created": "2024-01-05T12:00:00.000Z",
      "subdomain": "sptfy.in",
      "utm_view": 892
    }
  ]
}

Analytics Aggregation

Analyze clicks by country, user agent, or time period.

Group by Country

const records = await pb.collection('analytics').getFullList({
  filter: pb.filter('url_id = {:linkId}', { linkId: 'your_link_id' })
});

// Count by country
const byCountry = records.reduce((acc, record) => {
  const country = record.utm_country || 'Unknown';
  acc[country] = (acc[country] || 0) + 1;
  return acc;
}, {});

console.log(byCountry);
// { "US": 42, "GB": 18, "DE": 12, ... }

Time-based Analysis

// Get clicks in the last 24 hours
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();

const recentClicks = await pb.collection('analytics').getFullList({
  filter: pb.filter('url_id = {:linkId} && created >= {:date}', {
    linkId: 'your_link_id',
    date: oneDayAgo
  }),
  sort: '-created'
});

console.log(`Clicks in last 24h: ${recentClicks.length}`);

User Agent Parsing

// Simple mobile detection
const records = await pb.collection('analytics').getFullList({
  filter: pb.filter('url_id = {:linkId}', { linkId: 'your_link_id' })
});

const mobile = records.filter(r => 
  /Mobile|Android|iPhone|iPad/i.test(r.utm_userAgent)
).length;

const desktop = records.length - mobile;

console.log(`Mobile: ${mobile}, Desktop: ${desktop}`);

Bot Detection

Clicks from bots and crawlers are not recorded in analytics. The following patterns are detected:
  • WhatsApp (link preview)
  • Telegram (link preview)
  • Twitter/X (card preview)
  • Facebook (external hit)
  • Googlebot
  • Google Safety
  • Google Firebase
  • Bing (slurp)
  • Generic bot/crawler/spider patterns
  • axios
  • okhttp
  • node-fetch
  • python-requests
  • curl (if “curl/” in user agent)
  • dub.co (link checker)
  • Unknown user agents
  • iframe embeds
Bot detection logic is in /home/daytona/workspace/source/src/routes/[slug]/+page.server.js:7-34

Real-time Analytics

PocketBase supports real-time subscriptions to collections. You can subscribe to analytics updates for live dashboards.
import { getPocketBase } from '$lib/pocketbase';

const pb = getPocketBase();

// Subscribe to new analytics records for a specific link
pb.collection('analytics').subscribe('*', function (e) {
  if (e.record.url_id === 'your_link_id') {
    console.log('New click:', e.record);
    // Update your UI with new data
  }
});

// Unsubscribe when done
pb.collection('analytics').unsubscribe();

Subscription Events

action
string
Event type: create, update, or delete
record
object
The analytics record that was created/updated/deleted

Rate Limits

PocketBase doesn’t enforce strict rate limits by default, but you should:
  • Avoid polling analytics more than once per minute
  • Use real-time subscriptions instead of repeated queries
  • Cache aggregated results on your end
  • Respect server resources (shared PocketBase instance)

Privacy Considerations

sptfy.in collects minimal analytics data:
  • Stored: User agent, country code, timestamp
  • NOT stored: IP addresses, cookies, personal identifiers
  • Bot filtering: Automated requests are excluded from analytics
All analytics data is tied to link records. When a link is deleted, associated analytics records should also be removed (via PocketBase cascade delete rules).

Example: Complete Analytics Dashboard

Here’s a complete example fetching and analyzing all available metrics:
import PocketBase from 'pocketbase';

const pb = new PocketBase('https://pb.sptfy.in');
await pb.collection('users').authWithOAuth2({ provider: 'github' });

async function getLinkAnalytics(linkId) {
  // Get link info with view count
  const link = await pb.collection('random_short').getOne(linkId);
  
  // Get all analytics records
  const analytics = await pb.collection('analytics').getFullList({
    filter: pb.filter('url_id = {:linkId}', { linkId }),
    sort: '-created'
  });
  
  // Calculate metrics
  const totalViews = link.utm_view;
  const uniqueCountries = new Set(analytics.map(a => a.utm_country)).size;
  
  const byCountry = analytics.reduce((acc, a) => {
    const country = a.utm_country || 'Unknown';
    acc[country] = (acc[country] || 0) + 1;
    return acc;
  }, {});
  
  const mobile = analytics.filter(a => 
    /Mobile|Android|iPhone|iPad/i.test(a.utm_userAgent)
  ).length;
  
  const last24h = analytics.filter(a => {
    const clickTime = new Date(a.created).getTime();
    const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000;
    return clickTime >= oneDayAgo;
  }).length;
  
  return {
    link: {
      slug: link.id_url,
      destination: link.from,
      created: link.created
    },
    metrics: {
      totalViews,
      uniqueCountries,
      mobileClicks: mobile,
      desktopClicks: analytics.length - mobile,
      clicksLast24h: last24h,
      topCountries: Object.entries(byCountry)
        .sort((a, b) => b[1] - a[1])
        .slice(0, 5)
    },
    recentClicks: analytics.slice(0, 10)
  };
}

const stats = await getLinkAnalytics('your_link_id');
console.log(stats);
Output:
{
  "link": {
    "slug": "mytrack",
    "destination": "https://open.spotify.com/track/...",
    "created": "2024-01-10T08:00:00.000Z"
  },
  "metrics": {
    "totalViews": 42,
    "uniqueCountries": 8,
    "mobileClicks": 28,
    "desktopClicks": 14,
    "clicksLast24h": 5,
    "topCountries": [
      ["US", 18],
      ["GB", 9],
      ["DE", 6],
      ["FR", 4],
      ["JP", 3]
    ]
  },
  "recentClicks": [
    { "created": "2024-01-15T10:30:00.000Z", "utm_country": "US", ... }
  ]
}

Build docs developers (and LLMs) love