Script reference
lytics.js is the CanaryLytics tracking snippet: a zero-dependency script (~2.5 KB min+gzip, budget-enforced in CI) that tracks pageviews, SPA navigations, and link clicks automatically, with no key on the page. It is SSR-safe: it bails out immediately if window/document aren’t available and does nothing until it runs in a real browser.
<script src="https://cdn.analytics.canarycoders.es/lytics.js" data-domain="myapp.com" defer></script>Attributes
Section titled “Attributes”| Attribute | Default | Description |
|---|---|---|
data-domain |
location.hostname |
Domain reported to /ingest. Must match one of the project’s trusted domains or events are silently dropped. |
data-api |
https://api.analytics.canarycoders.es |
API origin override. Point this at a local or self-hosted instance. |
data-hash |
absent (off) | Opt-in flag (presence-only, no value needed). Includes location.hash in tracked paths and adds a hashchange pageview listener. See SPAs & client-side routing. |
Programmatic config fallback
Section titled “Programmatic config fallback”When the script is bundled into your own JS instead of loaded via a literal <script src> tag, document.currentScript is null and there is no element to read data-* attributes from. In that case, the same three settings fall back to a pre-init global. Set it before the tracker script runs:
<script> window.lytics = { domain: "myapp.com", api: "https://api.analytics.canarycoders.es", hash: true };</script><script src="https://cdn.analytics.canarycoders.es/lytics.js" defer></script>Precedence runs data attribute, then pre-init global, then built-in default. A data-domain attribute always wins over window.lytics.domain when both are present.
What automatic tracking covers
Section titled “What automatic tracking covers”The initial pageview fires once on load, with path = pathname + search (+ hash only when data-hash is set).
For SPA navigations the tracker patches history.pushState and replaceState and listens for popstate, so client-side route changes fire pageviews too. Consecutive calls to the same path are deduped.
Plain left-clicks on same-origin <a> elements fire a link_click event carrying the target path. Modified clicks, #/mailto:/tel:/javascript: hrefs and external links are left untracked.
Referrers are split by origin: a same-origin referrer contributes its path, a cross-origin one contributes only its hostname, never the path or query, so search terms never leave the browser. Both feed traffic-source classification and are then handled per the privacy stance.
Transport
Section titled “Transport”lytics.js sends events via fetch with keepalive: true, credentials: "omit", and a text/plain body. That combination keeps it a CORS “simple request” (no preflight) and survives page unloads. credentials: "omit" matters: /ingest answers with a wildcard Access-Control-Allow-Origin: *, and browsers reject credentialed cross-origin requests against a wildcard ACAO. navigator.sendBeacon is used only as a fallback if fetch throws.
Wire format
Section titled “Wire format”Every event is a POST /ingest with Content-Type: text/plain and a JSON body:
{ "domain": "myapp.com", "sessionId": "<UUIDv4>", "eventType": "page_view | link_click | custom", "path": "/pricing", "locale": "en-US", "referrerHost": "t.co", "referrerPath": "/old-page", "targetPath": "/next-page", "eventName": "signup", "props": { "plan": "pro" }}locale, referrerHost (cross-origin hostname only), and referrerPath (same-origin SPA navigations) are optional; targetPath appears only on link_click; eventName and props (flat string/number/boolean values, ≤ 2 KB) only on custom. The response is always 204, because invalid or untrusted events are dropped silently by design (an error would be an oracle for attackers probing other projects). Debug your own setup with GET /v1/validate instead.
Session id
Section titled “Session id”Each browser tab gets a random UUID (crypto.randomUUID(), with an RFC 4122 v4 fallback) stored in sessionStorage["cly_sid"]. It is tab-scoped: not shared across tabs, and not persisted once a tab closes. There is no cookie and no cross-session identifier. If sessionStorage or crypto throw (for example in a locked-down embed context), the tracker simply doesn’t send that event rather than falling back to something less private.