Skip to Content
StreamsBrowser SDK

Browser SDK

The Zeotap browser SDK is a lightweight JavaScript library (~10 KB gzipped) that captures events from your website and sends them to the Zeotap ingest API. Its API mirrors analytics.js so you can drop it in alongside or replace an existing tracker with minimal changes.

Install

Script Tag

Add this snippet to your website’s <head> to start sending events:

<script>
  !function(){var s=window.zeotap=window.zeotap||[];
  if(!Array.isArray(s)||s.invoked)return;s.invoked=!0;
  s.methods=["load","track","identify","page","screen","group","alias",
  "setConsent","setUserIdentities","unsetUserIdentities","reset","flush"];
  s.factory=function(m){return function(){
  var a=Array.prototype.slice.call(arguments);a.unshift(m);
  s.push(a);return s}};for(var i=0;i<s.methods.length;i++){
  var m=s.methods[i];s[m]=s.factory(m)}
  var q=s.load;s.load=function(k,opts){q(k,opts);
  var e=document.createElement("script");e.type="text/javascript";
  e.async=!0;e.src="https://content.zeotap.com/composable-sdk/v1/zeotap.min.js";
  var n=document.getElementsByTagName("script")[0];
  n.parentNode.insertBefore(e,n)};
  s.load("YOUR_WRITE_KEY",{apiHost:"https://events.zeotap.com"});
  }();
</script>

Replace YOUR_WRITE_KEY with the key from Streams > Event Sources in your workspace. The snippet registers a method queue so any calls made before the main bundle finishes loading are replayed once it is ready. A page view is automatically recorded on initialization (autoPage defaults to true).

You can also copy a pre-filled snippet from Streams > Event Sources in the UI.

API

load(writeKey, options?)

Initialize the SDK. Must be called before any tracking methods. Subsequent calls are no-ops.

OptionTypeDefaultDescription
apiHoststring—Ingest API host (required). Accepts an absolute URL or a relative path such as "/cdp" when proxying through your own domain — see First-Party Domain Setup
flushAtnumber10Flush the queue after this many events accumulate
flushIntervalnumber5000Flush the queue every N milliseconds
autoPagebooleantrueAutomatically call page() after load()
debugbooleanfalseEnable verbose console logging
firstPartyCookiesbooleanfalsePersist the anonymous ID as a server-set, HttpOnly first-party cookie — survives Safari ITP’s 7-day cap. Requires apiHost to point at a proxy on your own domain (setup guide)
credentials"same-origin" | "include""same-origin"Cookie mode for event requests. Set "include" only when the first-party proxy runs on a separate subdomain
storage"auto" | "cookie" | "localStorage""auto"Where identifiers persist. "auto": first-party cookie on the registrable domain with a localStorage mirror, degrading to localStorage-only when cookies are unusable
cookieDomainstringcomputedCookie domain override (e.g. ".example.com"). Default: the computed registrable domain
cookieExpiryDaysnumber365Identifier cookie expiry in days
cookieSecurebooleantrue on httpsCookie Secure flag
cookieSameSite"Lax" | "Strict" | "None""Lax"Cookie SameSite attribute. "None" forces Secure
nativeAppIdstring—Snowflake Native App install id; only needed for write keys created inside a Native App install
consentobject—Consent behavior (opt-in gating, defaults, buffering) — see Consent management
sessionobject—Session tracking. { enabled?: boolean, timeout?: number } — enabled defaults to true, timeout is the inactivity window in milliseconds (default 1800000, i.e. 30 minutes, clamped to between 5 seconds and 30 minutes). See Sessions

Sessions

The SDK groups a visitor’s activity into sessions (visits) and attaches a context.session block to every event:

FieldMeaning
idThe session id, stable until it rolls over
isNewtrue only on the event that started the session
sequence1-based, monotonic within the session — lets you reorder late arrivals
ordinalWhich session this is for this visitor
startedAtISO-8601 session start
eventCount / pageCountActivity so far in this session
durationMsWall-clock time since the session started
activeMsForeground engaged time — excludes any stretch the tab spent hidden
entryUrl / lastUrlFirst and most recent page of the session
referrerdocument.referrer as it was at session start
lastNameName of the most recent page() or screen()
timeoutMsThe inactivity window in force
platformThe kind of client — always web from this SDK

A new session starts on the first event after timeout milliseconds of inactivity, after 24 hours regardless of activity, or on reset() — a logout ends the visit. The session record lives in a first-party session cookie on your registrable domain with a sessionStorage mirror, so a visit is shared across tabs and ends when the browser closes.

session_end

When a session goes quiet, Zeotap emits a session_end track event on your source, carrying the session’s full rollup in properties: sessionId, endReason (inactivity or max_duration), startedAt, endedAt, durationMs, activeMs, eventCount, pageCount, entryUrl, exitUrl, referrer, lastName, platform, deviceType. It behaves like any other event — forward it to a destination, match it in an audience, or trigger a journey on it.

deviceType (desktop, mobile, tablet, bot) is worked out from the browser’s user agent and is there for reporting — “how many of these visits came from a phone”. It never changes how long a session lasts: a phone browser is still a browser, and gets the same window a laptop does.

This event is produced server-side, and it has to be: your page is not running when the visit ends, so the only reliable signal that a session is over is that events stopped arriving. Its timestamp is the session’s last event — when the visitor actually stopped — not the moment we noticed, and durationMs excludes the window we waited before deciding the visit was over.

eventCount is what Zeotap received, which can be lower than what your page sent: events dropped for consent, blocked by a contract, or discarded from a full offline queue never arrive. That is deliberate — the number matches your event data.

track(event, properties?)

Record a named user action.

zeotap.track("Order Completed", { orderId: "order-456", revenue: 99.99, currency: "USD" });

identify(userId?, traits?)

Associate the current user with traits. The userId is persisted alongside the anonymous ID (see Identity).

zeotap.identify("user-123", { email: "user@example.com", plan: "pro" });

page(category?, name?, properties?)

Record a page view. Called automatically on load() unless autoPage: false.

zeotap.page("Docs", "Getting Started");

screen(category?, name?, properties?)

Record a screen view (mobile-hybrid apps).

zeotap.screen("Onboarding", "Welcome");

group(groupId, traits?)

Associate the current user with a group (organization, team, workspace).

zeotap.group("org-789", { name: "Acme Inc", plan: "enterprise" });

alias(newId, previousId?)

Alias a new user ID to a previous anonymous or identified ID. Useful at sign-up time.

zeotap.alias("user-123");

setConsent(consent)

Set the user’s consent state. All subsequent events carry context.consent with the current state. Pass exactly one of the three forms below — they are mutually exclusive.

TCF v2 consent string (EU/GDPR) — the server automatically derives consent categories from the TCF purpose consent bits:

zeotap.setConsent({ tcfString: "CPXxRfAPXxRfAAfKAB..." });

US Privacy string (CCPA) — the server derives advertising/marketing opt-out status:

zeotap.setConsent({ usPrivacy: "1YNN" });

Pre-parsed categories — use when your CMP already provides resolved boolean values:

zeotap.setConsent({ analytics: true, marketing: false, advertising: false, functional: true, });

Each call replaces the previous consent state entirely. Forwarding rules use these values to gate delivery based on required consent categories, and every event lands in the warehouse with structured consent columns (consent_string, us_privacy, consent_categories).

reset()

Clear the current user identity (user ID, anonymous ID, queue, consent state). Call this on logout so subsequent events are attributed to a new anonymous session.

zeotap.reset();

flush()

Immediately send any queued events. The SDK flushes automatically when flushAt events accumulate or flushInterval elapses, so you usually don’t need to call this — it’s useful before intentionally terminating a session (e.g., right before window.location changes).

How it Works

Identity

  • Anonymous ID — generated on first load() (UUID v4) and persisted in a first-party cookie on the registrable domain (shared across subdomains) with a localStorage mirror, degrading to localStorage-only where cookies are unusable (configurable via storage). Survives across sessions and is only regenerated on reset().
  • User ID — set via identify() and persisted the same way. Every subsequent event carries both IDs when available.

Storage written by JavaScript is subject to browser protections — Safari ITP deletes it after 7 days without a visit. To keep the anonymous ID stable long-term on all browsers, enable firstPartyCookies with a proxy on your own domain: see First-Party Domain Setup.

Queue and flush

Events are buffered in an in-memory FIFO queue. The queue is bounded (500 events by default) — when full, the oldest event is dropped to prevent unbounded memory growth on clients that go offline for long periods. The queue is flushed when one of:

  • flushAt events accumulate (default 10)
  • flushInterval milliseconds pass (default 5 seconds)
  • The page is hidden (pagehide / visibilitychange) — flushed via navigator.sendBeacon so events are delivered even as the user navigates away
  • flush() is called explicitly

Page visibility handling

The SDK uses pagehide and visibilitychange === "hidden" (not beforeunload) so page navigation is reliably captured on mobile Safari and does not block the browser’s Back-Forward Cache. This is the modern, BFCache-friendly approach.

Transport and retry

Each flush sends a POST to the configured apiHost at /v1/batch with an X-Write-Key header. The sendBeacon fallback for page-hide flushes uses a ?writeKey= query parameter instead, because sendBeacon cannot set custom headers. Failed requests are retried up to 3 times with exponential backoff. After exhaustion, the events are dropped and a debug log is emitted if debug: true.

Payload Format

Events follow the Segment spec:

{
  "type": "track",
  "messageId": "uuid-v4",
  "userId": "user-123",
  "anonymousId": "uuid-v4",
  "event": "Order Completed",
  "properties": { "orderId": "456", "revenue": 99.99 },
  "context": {
    "library": { "name": "@zeotap/browser-sdk", "version": "0.1.0" },
    "page": {
      "url": "https://example.com/checkout",
      "title": "Checkout",
      "referrer": "https://example.com/cart",
      "path": "/checkout",
      "search": ""
    },
    "userAgent": "Mozilla/5.0 ...",
    "locale": "en-US",
    "consent": { "tcfString": "CPXx..." }
  },
  "timestamp": "2026-04-10T12:34:56.789Z",
  "sentAt": "2026-04-10T12:34:56.789Z"
}

See Sending events for the full Segment-compatible schema.

Next Steps

Last updated on