| 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
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.
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.
<!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>// 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>
)
}<!-- 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>Once loaded, use the sa() function to track events:
// Automatic (default behavior)
// Page views are tracked automatically on load and navigation
// Manual tracking
sa('trackPageView', '/custom-path', 'Custom Page Title')// 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)// Associate events with a user ID
sa('identify', 'user-123')// Set properties that persist for the session
sa('setProperty', 'plan', 'enterprise')
sa('setProperty', 'cohort', '2024-Q1')// Enable debug logging
sa('debug', true)
// Disable debug logging
sa('debug', false)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"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' }
// ...Track engagement time:
generateFullTrackingScript({
trackTimeOnPage: [30, 60, 120, 300],
})
// Events fired after 30s, 60s, 2min, 5min on pageTrack clicks to external sites:
generateFullTrackingScript({
trackOutboundLinks: true,
})
// Events: { name: 'outbound*click', properties: { url, text, hostname } }Track navigation in single-page apps:
generateFullTrackingScript({
trackHashChanges: true, // For hash-based routing
autoPageView: true, // Track history.pushState
})Respect the browser's DNT setting:
generateFullTrackingScript({
honorDnt: true, // Default: true
})Don't track certain pages:
generateFullTrackingScript({
excludePaths: [
'/admin/*',
'/api/*',
'/internal',
],
})Remove query parameters from tracked URLs:
generateFullTrackingScript({
excludeQueryParams: true,
})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)- API Endpoints - Set up the server-side API
- Dashboard Components - Display analytics data
- Privacy Features - Learn about privacy options
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>