Subrite Signal tracking SDK
npm install @subrite/signal-sdkwindow.subriteSignal when the bundle loads.init(tenantId, apiHost?, debug?) once per page. apiHost is optional and defaults to https://signal-api.subrite.no.identify(userId, profileFields?).pageview, track, and convert queue events, batch them, and POST to ${apiHost}/tracking/events.reset() clears the cached user + queue after logout. Until a new identify() runs, nothing is sent.
┌─────────────────────────────────────────────────────────────┐
│ Your Website / App │
│ ┌───────────────────────────────────────────────────┐ │
│ │ window.subriteSignal.init('tenant') │ │
│ └───────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────▼───────────────────────────┐ │
│ │ Event Queue (memory) │ │
│ │ • pageview / track / convert │ │
│ └───────────────────────┬───────────────────────────┘ │
│ │ auto-flush every 3s / 100 events │
└──────────────────────────┼──────────────────────────────────┘
│ batched POST ↓
┌────────────────────────────┐ ┌──────────────────────┐
│ Subrite Signal API │ ───▶ │ PostgreSQL storage │
│ POST /tracking/events │ │ per-tenant isolation│
└────────────────────────────┘ └──────────────────────┘
`$3
- Authenticated-only analytics – events require identify(); unauthenticated payloads never leave the browser.
- Session ID – crypto.randomUUID() (with fallback) per page load.
- Retry + offline support – events queue while offline and retry (up to 3x) with exponential backoff.
- OIDC session hand-off – NextAuth (or your auth layer) exposes session.user. The SDK uses those stable IDs (e.g. sub, member_id, subscription_id).
- Privacy – never send PII (emails, names). Use tenant IDs, subscription IDs, or hashed identifiers that your auth flow already provides.---
2. Embedding the hosted script
Include the bundle that Subrite publishes at
http://signal.subrite.no/sdk/subrite-signal.js:`html
`The script exposes
window.subriteSignal in browsers as soon as it loads. Simply check for the global before calling init (no SignalSDKReady event is emitted today):`html
`3. Using the SDK APIs
After
init() you can:-
identify(userId, properties?) – unlock tracking with an authenticated ID.
- pageview(properties?), track(eventName, properties?), and convert(...) – send batched events; identify() must precede them.
- reset() – call on logout to clear identity + queue.
- flushNow() and getDistinctId() – helpers for QA or manual flushing.The exported singleton works the same across React/Vue components or classic
---
5. SDK Lifecycle & Internals
$3
- Singleton state
`typescript
private queue: any[] = [];
private retryQueue: any[] = [];
private userId: string | null = null;
private sessionId = generateId();
private flushInterval = 3000;
private maxBatchSize = 100;
private maxRetries = 3;
`-
init
- Stores tenant + host + debug flag.
- Starts the flush timer.
- Registers event listeners: beforeunload, visibilitychange, online/offline.
- Does nothing else until a user authenticates.-
identify(userId, properties?)
- Requires the SDK to be initialised.
- Validates the ID, stores it, and optionally logs metadata.
- Does not enqueue any network event—it only unlocks tracking for authenticated sessions.-
pageview, track, convert
- Abort with an error if userId is missing (protects against anonymous tracking).
- Append the event to the queue with tenant_id, user_id, session_id, current URL, referrer, timestamp.
- convert adds conversion metadata (conversion_type, value).- Queue + flush
`typescript
enqueue(event) {
this.queue.push(event);
if (this.queue.length >= this.maxBatchSize) {
this.flush();
}
} startFlushTimer() {
this.flushTimer = setInterval(() => {
if (this.queue.length > 0) {
this.flush();
}
}, this.flushInterval);
}
async flush(force = false) {
if (!this.config || this.flushing || this.queue.length === 0) return;
if (!this.isOnline && !force) return;
...
}
`
- Batches events (maxBatchSize).
- POSTs to ${apiHost}/tracking/events.
- On failures, increments retryCount, re-queues with exponential backoff, drops after the configured maximum.-
reset()
`typescript
reset() {
this.userId = null;
this.sessionId = generateId();
this.queue = [];
this.retryQueue = [];
}
`---
8. Tracking API (Method Reference)
| Method | Purpose | Requires identify? | Notes |
| ------ | ------- | ------------------ | ----- |
|
init(tenantId, apiHost?, debug?) | Configure SDK and start timers | No | apiHost optional, defaults to https://signal-api.subrite.no. Call exactly once per page |
| identify(userId, properties?) | Unlock tracking for this user | Yes | Caches ID locally; no network event |
| pageview(properties?) | Record a view/route change | Yes | Captures URL + referrer automatically |
| track(eventName, properties?) | Custom events (CTA, scroll, etc.) | Yes | event_type: 'custom' |
| convert(eventName, conversionType, value?, meta?) | High-value conversions | Yes | Adds conversion_type and optional value |
| reset() | Clear identity & queue | No | Use on logout |
| flushNow() | Force immediate POST | Yes | Useful for QA |
| getDistinctId() | Inspect current user ID | N/A | Returns cached userId or null |Reminder: All user identifiers must originate from authenticated context—never invent anonymous IDs client-side.
---
9. Practical Examples
$3
`tsx
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import { useSession } from 'next-auth/react';export function RouteAnalytics() {
const router = useRouter();
const { data: session, status } = useSession();
useEffect(() => {
if (status !== 'authenticated' || !session?.user) return;
const client = window.subriteSignal;
if (!client?.getDistinctId?.()) return; // ensure identify ran
const trackPage = (url: string) =>
client.pageview({
section: deriveSection(url),
member_tier: session.user.subscription_plan,
});
trackPage(router.asPath);
router.events.on('routeChangeComplete', trackPage);
return () => router.events.off('routeChangeComplete', trackPage);
}, [router, session, status]);
return null;
}
`$3
`typescript
document.querySelector('#buy-now')?.addEventListener('click', () => {
const client = window.subriteSignal;
client?.track('cta_clicked', {
cta_type: 'purchase_button',
plan_id: 'premium',
plan_name: 'Premium Plan',
price: 59.99,
});
});
`$3
`typescript
window.subriteSignal?.convert('subscription_purchase', 'subscription', 59.99, {
plan_id: 'premium',
billing_cycle: 'monthly',
payment_method: 'stripe',
conversion_funnel: 'landing_to_checkout',
});
`$3
`typescript
document.querySelector('#logout')?.addEventListener('click', () => {
const client = window.subriteSignal;
client?.track('user_logout', { location: 'header_menu' });
client?.reset();
});
``