The tracker is a single 30 KB JavaScript file delivered from /p/:siteKey/k.js. The file is byte-identical for every site; the siteKey is encoded in the URL so the tracker can derive it from document.currentScript.src. This lets the same file work behind a first-party CNAME proxy without rebuilds.
Navigate to Web Analytics > Sites > + New Site (or call POST /api/analytics/sites directly). You'll provide:
| Field | Notes |
|---|---|
name |
Human-readable label. |
domain |
The primary website domain (e.g. customer.example). Used as the default origin allowlist entry. |
allowedOrigins (optional) |
Additional origins permitted to POST to /collect/:siteKey. Wildcard * is allowed. |
respectDnt (default true) |
When true, events tagged with consent.dnt: true or gpc: true are filtered server-side. |
analyticsRetentionDays (optional) |
Per-site override of the tenant default (25 months). null = unlimited. |
The system generates a 16-character hex siteKey and registers it in the global AnalyticsSiteKeyIndex. This index lets the unauthenticated /collect endpoint resolve a siteKey to its tenant + database name without a session.
The Sites page shows a ready-to-paste snippet. Substitute your siteKey:
<!-- KarmaFlow tracker -->
<script async src="https://omni.karmaflow.ai/p/YOUR_SITE_KEY/k.js"></script>
<script>
window.karma = window.karma || function(){(window.karma.q=window.karma.q||[]).push(arguments)};
</script>
Paste this into the <head> of every page. Order doesn't matter — the queue stub captures any calls that happen before the tracker finishes downloading; once it loads, it drains the queue and starts sending events.
The tracker:
DOMContentLoaded, or immediately if the script loads after that.siteKey and endpoint from document.currentScript.src.page_view on init and on every pushState / popstate (SPA-safe).Three ways, fastest first:
Web Analytics > Realtime). Reads from a per-tenant Mongo capped collection — events should appear within ~2 s of being fired.window.karmaDebug = true before the snippet loads to enable log lines like enqueued page_view, flushing 5 events via batch-full.POST /collect/:siteKey returning 202 Accepted with {"success":true,"accepted":N}.If you see traffic in Realtime but not in Funnels / Cohorts the next day, see Fleet Dashboard & Troubleshooting — the most common cause is BigQuery insert errors landing in events_dlq.
The snippet has three optional config knobs. Pass them via a script-tag data-* attribute or by calling karma('init', {...}) before the first event:
<script async
src="https://omni.karmaflow.ai/p/YOUR_SITE_KEY/k.js"
data-karma-respect-dnt="true"
data-karma-debug="false"></script>
| Option | Default | Behavior |
|---|---|---|
respectDnt |
true |
Skips enqueueing any event when navigator.doNotTrack === '1' or navigator.globalPrivacyControl === true. Both client- and server-side enforced. |
debug |
false |
Verbose console.log of every queue / flush / tag-eval step. |
endpointBase |
snippet URL origin | Override only when running behind a CNAME proxy that the tracker can't auto-detect. |
To bypass ad-blockers, route the tracker through a subdomain you control (e.g. analytics.customer.example → omni.karmaflow.ai). See First-Party CNAME Proxy for the full DNS verification flow. With a CNAME live, the snippet src becomes:
<script async src="https://analytics.customer.example/p/YOUR_SITE_KEY/k.js"></script>
The tracker auto-detects the new origin and routes its POST /collect/:siteKey calls to https://analytics.customer.example/collect/... — no code changes on the customer site.
When you POST /api/analytics/sites:
Site document is created in the tenant Mongo DB.siteKey ↔ tenantId row is inserted into the global AnalyticsSiteKeyIndex so /collect can resolve siteKey without a session.Main workspace is created for Tag Manager work./p/:siteKey/container.json returns a valid empty container immediately).realtimeBufferModel, eventRollupHourlyModel, eventRollupDailyModel).The BigQuery events, events_dlq, and identity_map tables are created once per Cloud Run instance on first event ingest via ensureEventsTable(). They are NOT per-tenant — every tenant writes to the same tables, isolated by mandatory WHERE tenant_id = ? clauses in the query layer.
Remove the snippet from the customer site. To stop the siteKey from being usable even if the snippet is still present somewhere (e.g. cached HTML), set the Site's status to paused — /collect/:siteKey will then return 202 { accepted: 0, filtered: N } without writing anywhere. Archiving the site (status: archived) preserves history but rejects new events with 404 unknown_site_key.