autocap
v1.5.0
Published
Privacy-compliant event tracking library for exit point analysis
Maintainers
Readme
AutoCap
Privacy-compliant auto-capture event tracking library focused on exit point analysis. Framework-agnostic and built for global browser compatibility.
Features
- ✅ Exit Point Detection - Track where users leave
- ✅ High-Granularity Input Tracking - Track input events without storing values (PII-safe)
- ✅ Click/Tap Tracking - Comprehensive click event capture with link URL and text content
- ✅ Session Management - Unique session IDs, timing, duration
- ✅ SPA Navigation - Track page loads in single-page applications
- ✅ Dynamic Elements - Works with dynamically created DOM elements via event delegation
- ✅ Privacy-First - No PII collection, complies with global privacy regulations (GDPR, PIPL, CCPA)
- ✅ Analytics-Optimized - Flattened event structure with normalized URLs for efficient dashboard queries
- ✅ Global Browser Compatible - Works across modern browsers including Chrome, Safari, Firefox, Edge, Opera, Samsung Internet, WeChat, UC, QQ, 360, Baidu
- ✅ Lean - < 5KB gzipped bundle size
Installation
npm install autocap
# or
yarn add autocapQuick Start
Next.js (App Router)
// app/layout.tsx or app/tracking-provider.tsx
'use client'
import { useEffect } from 'react'
import { Tracker } from 'autocap'
export function TrackingProvider({ children }) {
useEffect(() => {
const tracker = new Tracker({
endpoint: '/api/track', // Your API endpoint
trackClicks: true,
trackInputs: true,
sanitizeUrls: true
})
tracker.start()
return () => {
tracker.stop()
}
}, [])
return <>{children}</>
}Next.js (Pages Router)
// pages/_app.tsx
import { useEffect } from 'react'
import { Tracker } from 'autocap'
export default function App({ Component, pageProps }) {
useEffect(() => {
const tracker = new Tracker({
endpoint: '/api/track',
trackClicks: true,
trackInputs: true
})
tracker.start()
return () => tracker.stop()
}, [])
return <Component {...pageProps} />
}Vanilla JavaScript
<script src="/autocap.umd.js"></script>
<script>
const tracker = new AutoCap.Tracker({
endpoint: 'https://your-api.com/track',
trackClicks: true,
trackInputs: true
})
tracker.start()
</script>Configuration
interface TrackerConfig {
// Session
sessionStorage?: 'sessionStorage' | 'memory' | 'none'
sessionTimeout?: number // milliseconds
// Events
trackClicks?: boolean
trackInputs?: boolean
// Throttling
inputThrottle?: number // milliseconds
// Privacy
sanitizeUrls?: boolean
excludeQueryParams?: boolean
// Data
maxEventsInMemory?: number
endpoint?: string // Server endpoint
// Compatibility
enablePolyfills?: boolean
// Analytics metadata (optional, added to all events)
environment?: string // e.g., 'dev' | 'staging' | 'prod'
appVersion?: string // semver string
traceId?: string // for backend correlation
requestId?: string // for backend correlation
samplingRate?: number // if sampling is implemented
}Test Harness
A test harness (index.html) is included in the npm package to help you test and debug AutoCap. The test harness provides:
- Interactive testing of click, input, and page navigation events
- Real-time event log display
- Configuration options
- Event export functionality
- SPA navigation simulation
Development Usage
For local development in this repository:
Build the library:
npm run buildStart a local server in the root directory:
npx serveOpen
index.htmlin your browser (the server will typically show the URL, usuallyhttp://localhost:3000)
Note: Using a local server (like npx serve) is recommended over opening the file directly (file://) because some features like pushState work better with HTTP.
Serving with Docker
The project includes a Dockerfile that uses nginx to serve the test harness. Here's how to use it:
Build the Docker image:
docker build -t autocap .Run the container:
docker run -d -p 8080:80 --name autocap-server autocapThen access it at http://localhost:8080/.
Server-Side Implementation
AutoCap sends events to your configured endpoint. Here's how to handle them:
Request Format
- Method:
POST - Content-Type:
application/json(for all events, including exit events viasendBeacon) - Body: Single
EventPayloadobject as JSON string
Events are sent individually and immediately as they occur.
Next.js Route Handler Example
// app/api/track/route.ts (App Router)
import { NextRequest, NextResponse } from 'next/server'
export async function POST(request: NextRequest) {
try {
// All events are sent as application/json
const event = await request.json()
// Log to stdout (will appear in your server logs)
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
event: event
}))
// Optional: Add your own processing here
// - Store in database
// - Send to analytics service
// - Filter/transform data
return NextResponse.json({ success: true }, { status: 200 })
} catch (error) {
console.error('Error processing tracking event:', error)
return NextResponse.json(
{ error: 'Invalid request' },
{ status: 400 }
)
}
}// pages/api/track.ts (Pages Router)
import type { NextApiRequest, NextApiResponse } from 'next'
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' })
}
try {
// All events are sent as application/json
const event = req.body
// Log to stdout
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
event: event
}))
res.status(200).json({ success: true })
} catch (error) {
console.error('Error processing tracking event:', error)
res.status(400).json({ error: 'Invalid request' })
}
}Important Notes
- Exit Events: Use
navigator.sendBeacon()withapplication/jsoncontent-type for reliable delivery during page unload. - Individual Events: Each event is sent in a separate request (not batched).
- Silent Failures: The client silently fails if requests fail to avoid breaking user experience.
- Keepalive: Regular events use
fetchwithkeepalive: truefor reliability. - Session Start Events: A
session_startevent is automatically emitted on the first page load of each session with full browser metadata. Subsequent events include minimal browser fields to reduce payload size.
Event Payload Structure
Events are structured for analytics dashboards with flattened dimensions and normalized URLs for efficient querying.
interface EventPayload {
// Event identification
event_id: string // UUID, auto-generated
event_type: 'click' | 'input' | 'page_load' | 'page_exit' | 'session_start'
// Session
session_id: string
session_start_ts: number // milliseconds
ts: number // event timestamp in milliseconds
ts_iso: string // ISO 8601 string for human readability
elapsed_ms: number // milliseconds since session start
// Location (normalized for low cardinality)
url_host: string // e.g., "localhost:3000"
url_path: string // e.g., "/products"
url?: string // optional raw URL (not for grouping)
referrer_host: string // empty if no referrer
referrer_path?: string // optional
prev_url_path?: string // for SPA navigation
page_title: string
page_id?: string // stable page identity derived from path (e.g., "products" from "/products")
// Element dimensions (promoted from data for click/input events)
element_tag?: string // tagName for click/input elements
element_id?: string // id for click/input elements
element_class?: string // className for click/input elements
element_name?: string // name attribute for click/input elements
element_text_len?: number // textLength for click elements
// Input-specific dimensions
input_type?: string // for input events
field_name?: string // for input events
input_length?: number // for input events
// Click event fields (flattened)
click_x?: number // click X coordinate
click_y?: number // click Y coordinate
link_url?: string // URL of clicked link (sanitized)
link_text?: string // Text content of clicked link (limited to 200 chars)
button_text?: string // Text content of clicked button (limited to 200 chars)
// Page event fields (flattened)
load_time?: number // page load time in milliseconds
// Exit event fields (flattened)
last_interaction?: string // type of last interaction before exit
time_on_page?: number // milliseconds spent on page
exit_element_tag?: string // tagName of exit element
exit_element_id?: string // id of exit element
exit_element_class?: string // className of exit element
// Browser metadata (minimal on regular events, full on session_start)
ua_hash?: string // hash of userAgent for joins
ua_full?: string // full userAgent string (only on session_start event)
viewport_w?: number
viewport_h?: number
timezone?: string
language?: string // browser language (only on session_start event)
ua_browser?: string // parsed browser name (optional)
ua_os?: string // parsed OS (optional)
ua_device?: string // parsed device (optional)
// Analytics metadata from config (optional)
environment?: string
app_version?: string
trace_id?: string
request_id?: string
sampling_rate?: number
}Key Design Decisions
Fully Flattened Structure: All event fields are at the top level (no nested data object). This includes element dimensions, click coordinates, page load times, and exit metadata. This structure enables easier GROUP BY queries in SQL/SLS-style aggregations without nested field access.
Normalized URLs: URLs are split into url_host and url_path to reduce cardinality. Full URLs are high-cardinality and expensive to group by in dashboards.
Session-Level Browser Metadata: Full browser metadata (ua_full, language, viewport_w/h, timezone) is only sent once per session in a session_start event. Regular events include minimal fields (ua_hash, viewport_w/h, timezone) to reduce payload size and storage costs.
Stable Page Identity: page_id is derived from URL path (e.g., /products/123 → products) to provide a stable identifier that doesn't change with A/B tests or dynamic content.
Example Click Event
{
"event_id": "3f2c8a1b-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
"event_type": "click",
"session_id": "789d453b-9e91-4628-98d1-6663fccadbb3",
"session_start_ts": 1766084562000,
"ts": 1766084562976,
"ts_iso": "2025-12-18T10:29:22.976Z",
"elapsed_ms": 976,
"url_host": "localhost:3000",
"url_path": "/products",
"referrer_host": "",
"page_title": "AutoCap Test - /products",
"page_id": "products",
"element_tag": "button",
"element_id": "add-to-cart",
"element_class": "btn btn-primary",
"element_text_len": 12,
"click_x": 450,
"click_y": 320,
"ua_hash": "xxh3:a1b2c3d4",
"viewport_w": 1707,
"viewport_h": 620,
"timezone": "America/Los_Angeles"
}Example Input Event
{
"event_id": "7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d",
"event_type": "input",
"session_id": "789d453b-9e91-4628-98d1-6663fccadbb3",
"session_start_ts": 1766084562000,
"ts": 1766084563450,
"ts_iso": "2025-12-18T10:29:23.450Z",
"elapsed_ms": 1450,
"url_host": "localhost:3000",
"url_path": "/contact",
"referrer_host": "",
"page_title": "Contact Us",
"page_id": "contact",
"element_tag": "input",
"element_id": "email",
"element_class": "form-control",
"element_name": "email",
"input_type": "email",
"field_name": "email",
"input_length": 24,
"ua_hash": "xxh3:a1b2c3d4",
"viewport_w": 1707,
"viewport_h": 620,
"timezone": "America/Los_Angeles"
}Example Session Start Event
{
"event_id": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"event_type": "session_start",
"session_id": "789d453b-9e91-4628-98d1-6663fccadbb3",
"session_start_ts": 1766084562000,
"ts": 1766084562000,
"ts_iso": "2025-12-18T10:29:22.000Z",
"elapsed_ms": 0,
"url_host": "localhost:3000",
"url_path": "/",
"referrer_host": "",
"page_title": "AutoCap Test",
"page_id": "home",
"ua_full": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)...",
"viewport_w": 1707,
"viewport_h": 620,
"language": "en-US",
"timezone": "America/Los_Angeles"
}Privacy & Compliance
AutoCap is designed to comply with global privacy regulations (GDPR, PIPL, CCPA):
- ✅ No Input Values - Never stores input field values, only character count
- ✅ No PII in Values - Only collects metadata (length, type, field names) - never actual input values
- ✅ Metadata Only - Only collects non-identifying metadata
- ✅ Data Localization - Configure endpoint to store data in your preferred region
Server-Side Logging Utilities
AutoCap exports utility functions that you can use in your server-side code to maintain consistent event formatting between client and server logs. This is useful for:
- Augmenting client events with server-side data (e.g., IP hashing)
- Creating server-side events (e.g., API calls, errors)
- Maintaining consistent URL normalization and hashing across your logging pipeline
Available Utilities
import {
// URL utilities
parseUrl,
parseReferrer,
derivePageId,
sanitizeUrl,
// Hashing utilities
hashString, // Fast non-cryptographic hash (djb2)
hashStringSecure, // Secure SHA-256 hash (async)
// Other
generateUUID
} from 'autocap';Usage Examples
URL Normalization
import { parseUrl, parseReferrer, derivePageId } from 'autocap';
// Parse URL into host and path
const url = parseUrl('https://example.com/products/123?foo=bar');
// → { host: 'example.com', path: '/products/123', raw: 'https://...' }
// Parse referrer
const ref = parseReferrer('https://google.com/search');
// → { host: 'google.com', path: '/search' }
// Derive stable page ID
const pageId = derivePageId('/products/123/details');
// → 'products'Hashing PII
import { hashString, hashStringSecure } from 'autocap';
// Fast non-cryptographic hash (synchronous) - for user agents, non-sensitive data
const uaHash = hashString(req.headers['user-agent']);
// → 'xxh3:1a2b3c4d'
// Secure SHA-256 hash for PII (async) - for IPs, emails, etc.
const emailHash = await hashStringSecure('[email protected]');
// → 'sha256:a1b2c3d4e5f6...'
// Hash IP address (extract first IP from x-forwarded-for)
const ipHeader = req.headers['x-forwarded-for'];
const firstIp = ipHeader?.split(',')[0]?.trim();
const ipHash = await hashStringSecure(firstIp || 'unknown');
// → 'sha256:7f8e9d0c1b2a3f4e'Complete Server Log Entry
import {
parseUrl,
parseReferrer,
derivePageId,
hashString,
hashStringSecure,
generateUUID
} from 'autocap';
export async function createServerLogEntry(req: Request) {
const url = new URL(req.url);
const urlData = parseUrl(req.url);
const referrerData = parseReferrer(req.headers.get('referer'));
// Extract first IP from x-forwarded-for
const ipHeader = req.headers.get('x-forwarded-for');
const firstIp = ipHeader?.split(',')[0]?.trim();
return {
event_id: generateUUID(),
event_type: 'server_request',
ts: Date.now(),
ts_iso: new Date().toISOString(),
url_host: urlData.host,
url_path: urlData.path,
referrer_host: referrerData.host,
referrer_path: referrerData.path,
page_id: derivePageId(urlData.path),
ua_hash: hashString(req.headers.get('user-agent') || ''),
ip_hash: await hashStringSecure(firstIp || 'unknown'),
};
}Hash Function Details
hashString(input: string): string
- Algorithm: djb2 variant (non-cryptographic)
- Synchronous: Yes
- Use Case: User agents, non-sensitive identifiers
- Output:
'xxh3:1a2b3c4d'or empty string
hashStringSecure(input: string): Promise<string>
- Algorithm: SHA-256 (cryptographic)
- Synchronous: No (uses Web Crypto API)
- Use Case: PII like IPs, emails, phone numbers
- Output:
'sha256:7f8e9d0c...'(first 16 hex chars) or'unknown' - Fallback: Uses
hashString()if crypto unavailable
Browser Compatibility
Written to be compatible with:
- ✅ Chrome 90+
- ✅ Safari 14+
- ✅ Firefox 88+
- ✅ Edge 90+
- ✅ Opera 76+
- ✅ Samsung Internet 14+
- ✅ WeChat In-App Browser
- ✅ UC Browser 12+
- ✅ QQ Browser
- ✅ 360 Secure Browser
- ✅ Baidu Browser
Note: The library uses feature detection and graceful degradation. If fetch and/or navigator.sendBeacon are unavailable, events will not be sent to your endpoint. All operations are wrapped in try-catch blocks to prevent errors from breaking your application.
License
MIT
