Skip to content

Latest commit

 

History

History
405 lines (298 loc) · 9.48 KB

File metadata and controls

405 lines (298 loc) · 9.48 KB
title Tracking Script
description Add analytics tracking to your website

})

// Outputs a loader that fetches /sites/{siteId}/tracker.js


### Inline Script

Embed the full script inline:

```typescript
import { generateInlineTrackingScript } from '@stacksjs/ts-analytics'

const script = generateInlineTrackingScript({
  siteId: 'my-site',
  apiEndpoint: 'https://analytics.example.com/api/analytics',
})

// Complete script wrapped in <script> tags

Cross-Domain Installs (CORS)

The tracking script and error SDK work when your site (https://app.example.com) posts to an analytics API on a different origin (https://analytics.example.com). Because beacons are sent as JSON — and the error SDK adds an X-Analytics-Token header — browsers send a CORS preflight (OPTIONS) before each cross-origin request.

The API handles this out of the box: every collect endpoint (/collect, /t, /p, /errors/collect, /issues/report) answers preflights with Access-Control-Allow-Origin: *, allows the Content-Type and X-Analytics-Token headers, and caches the preflight for 24 hours. No configuration is needed — but if you put a proxy or CDN in front of the API, make sure it forwards OPTIONS requests and does not strip Access-Control-* response headers.

To confirm, from your site's origin run:

curl -i -X OPTIONS https://your-analytics-host/collect \
  -H 'Origin: https://app.example.com' \
  -H 'Access-Control-Request-Method: POST' \
  -H 'Access-Control-Request-Headers: content-type'

A 204 with Access-Control-Allow-Origin: * means cross-domain installs will work.

Ad-Blocker Resilience (First-Party Proxy)

Some blocklists drop requests to third-party analytics hosts or to paths like /collect. Two layers of defense:

1. Stealth endpoint. Add ?stealth=true to the script URL and beacons post to the short /t path instead of /collect:

<script defer src="https://your-analytics-host/api/sites/YOUR_SITE_ID/script.js?stealth=true"></script>

2. First-party proxy (strongest). Serve both the script and the beacon path from your own domain, so there is no third-party request to block. Forward two routes to the analytics host:

Path on your domain Proxies to
/stats.js https://your-analytics-host/api/sites/YOUR_SITE_ID/script.js?stealth=true&api=https://yourdomain.com
/t https://your-analytics-host/t

The api= parameter bakes your domain into the script as the beacon target. If you use error tracking, also forward /errors/collect.

Then install with a same-origin tag:

<script defer src="https://yourdomain.com/stats.js"></script>

nginx

location = /stats.js {
  proxy_pass https://your-analytics-host/api/sites/YOUR_SITE_ID/script.js?stealth=true&api=https://yourdomain.com;
}
location = /t {
  proxy_pass https://your-analytics-host/t;
}

Cloudflare Worker

export default {
  async fetch(req) {
    const url = new URL(req.url)
    const upstream = 'https://your-analytics-host'
    if (url.pathname === '/stats.js')
      return fetch(`${upstream}/api/sites/YOUR_SITE_ID/script.js?stealth=true&api=${url.origin}`)
    if (url.pathname === '/t')
      return fetch(`${upstream}/t`, req)
    return fetch(req)
  },
}

Netlify (_redirects)

/stats.js  https://your-analytics-host/api/sites/YOUR_SITE_ID/script.js?stealth=true&api=https://yourdomain.com  200
/t         https://your-analytics-host/t                                                                        200

Make sure the proxy forwards OPTIONS requests and the client IP (X-Forwarded-For), or visitor geolocation and unique counting will see the proxy's address instead.

Adding to Your Website

Static HTML

<!DOCTYPE html>
<html>
<head>
  <script>
  (function(w,d,s,a){
    w.sa=w.sa||function(){(w.sa.q=w.sa.q||[]).push(arguments)};
    var f=d.getElementsByTagName(s)[0],j=d.createElement(s);
    j.async=1;j.src='https://analytics.example.com/api/analytics/sites/my-site/tracker.js';
    f.parentNode.insertBefore(j,f);
  })(window,document,'script');
  sa('init','my-site');
  </script>
</head>
<body>
  <!-- Your content -->
</body>
</html>

React/Next.js

// components/Analytics.tsx
'use client'

import { useEffect } from 'react'

export function Analytics() {
  useEffect(() => {
    // @ts-ignore
    window.sa = window.sa || function() { (window.sa.q = window.sa.q || []).push(arguments) }

    const script = document.createElement('script')
    script.src = 'https://analytics.example.com/api/analytics/sites/my-site/tracker.js'
    script.async = true
    document.head.appendChild(script)

    // @ts-ignore
    window.sa('init', 'my-site')
  }, [])

  return null
}

// app/layout.tsx
import { Analytics } from '@/components/Analytics'

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <Analytics />
      </body>
    </html>
  )
}

Vue/Nuxt

<!-- components/Analytics.vue -->
<script setup>
import { onMounted } from 'vue'

onMounted(() => {
  window.sa = window.sa || function() { (window.sa.q = window.sa.q || []).push(arguments) }

  const script = document.createElement('script')
  script.src = 'https://analytics.example.com/api/analytics/sites/my-site/tracker.js'
  script.async = true
  document.head.appendChild(script)

  window.sa('init', 'my-site')
})
</script>

<template>
  <div />
</template>

Client-Side API

Once loaded, use the sa() function to track events:

Track Page Views

// Automatic (default behavior)
// Page views are tracked automatically on load and navigation

// Manual tracking
sa('trackPageView', '/custom-path', 'Custom Page Title')

Track Custom Events

// Basic event
sa('event', 'button*click')

// Event with properties
sa('event', 'download', { file: 'guide.pdf', size: '2.4MB' })

// Event with category and value
sa('event', 'purchase', { product: 'pro-plan' }, 'conversion', 99.99)

Identify Users

// Associate events with a user ID
sa('identify', 'user-123')

Set Properties

// Set properties that persist for the session
sa('setProperty', 'plan', 'enterprise')
sa('setProperty', 'cohort', '2024-Q1')

Debug Mode

// Enable debug logging
sa('debug', true)

// Disable debug logging
sa('debug', false)

Data Attributes

Track elements automatically with data attributes:

<!-- Basic tracking -->
<button data-sa-track="signup*click">Sign Up</button>

<!-- With category -->
<button
  data-sa-track="download"
  data-sa-track-category="engagement"
>
  Download PDF
</button>

<!-- With value -->
<button
  data-sa-track="purchase"
  data-sa-track-category="conversion"
  data-sa-track-value="99"
>
  Buy Now
</button>

Custom prefix:

generateFullTrackingScript({
  dataAttributePrefix: 'analytics',
  // ...
})

// Then use: data-analytics-track="event*name"

Tracking Features

Scroll Depth

Track how far users scroll:

generateFullTrackingScript({
  trackScrollDepth: [25, 50, 75, 100],
})

// Events fired:
// { name: 'scroll*depth', properties: { depth: 25 }, category: 'engagement' }
// { name: 'scroll*depth', properties: { depth: 50 }, category: 'engagement' }
// ...

Time on Page

Track engagement time:

generateFullTrackingScript({
  trackTimeOnPage: [30, 60, 120, 300],
})

// Events fired after 30s, 60s, 2min, 5min on page

Outbound Links

Track clicks to external sites:

generateFullTrackingScript({
  trackOutboundLinks: true,
})

// Events: { name: 'outbound*click', properties: { url, text, hostname } }

SPA Navigation

Track navigation in single-page apps:

generateFullTrackingScript({
  trackHashChanges: true, // For hash-based routing
  autoPageView: true,     // Track history.pushState
})

Privacy Features

Do Not Track

Respect the browser's DNT setting:

generateFullTrackingScript({
  honorDnt: true, // Default: true
})

Exclude Paths

Don't track certain pages:

generateFullTrackingScript({
  excludePaths: [
    '/admin/*',
    '/api/*',
    '/internal',
  ],
})

Strip Query Params

Remove query parameters from tracked URLs:

generateFullTrackingScript({
  excludeQueryParams: true,
})

Server-Side Rendering

For SSR, generate the script at build time:

// build-tracking.ts
import { writeFileSync } from 'fs'
import { generateFullTrackingScript } from '@stacksjs/ts-analytics'

const script = generateFullTrackingScript({
  siteId: process.env.SITE*ID!,
  apiEndpoint: process.env.ANALYTICS*ENDPOINT!,
  minify: true,
})

writeFileSync('public/tracker.js', script)

Next Steps

Versioning & caching

The script embeds its build version (#179): read it from any live page via window.__tsa, and every beacon carries it as v, so the dashboard's ingest counters show exactly which tracker builds your visitors are running.

/script.js is served with a 1-hour cache plus an ETag and stale-while-revalidate — browsers pick up new tracker builds on their next background revalidation. To force an immediate refresh for all visitors, cache-bust with a query string:

<script defer data-site="YOUR_APP_ID" src="https://your-api.example.com/script.js?v=2"></script>