> ## Documentation Index
> Fetch the complete documentation index at: https://docs.traceten.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Browser SDK

> The client-side SDK: every method the Traceten snippet exposes on window.traceten: pageviews, custom conversion events with revenue, identify, consent, and declarative click tracking.

## What this lets you do

Record custom conversion events (signups, purchases, demo bookings) with revenue amounts, link visitors to your own user IDs, and control tracking from your own consent banner, using the `window.traceten` global the snippet installs.

## Before you start

* The snippet is installed and verified. See the [install overview](/install/overview) and [verify installation](/install/verify).
* Your site key (`data-site` attribute) matches the site in your dashboard.

## Script tag attributes

The snippet reads exactly two attributes off its own `<script>` tag. Both are rendered for you on the install page.

| Attribute            | Required | What it does                                                                                                                                                                                                                    |
| -------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `data-site`          | Yes      | Your site key, `ttid_` followed by 22 characters. Case-sensitive. Nothing is recorded without it.                                                                                                                               |
| `data-cookie-domain` | No       | The domain to scope identity cookies to, so one visitor stays one visitor across your subdomains. Off by default. Set only once you confirm a value under **Sites → Settings → Cookies**. Omitted means cookies stay host-only. |

Anything else you add to the tag is ignored. The `data-traceten` attributes further down this page go on your own page elements, not on the script tag.

## The `window.traceten` global

The snippet writes exactly one global: `window.traceten`. It is a callable function with five attached methods:

| Call                                     | What it does                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `traceten("pageview")`                   | Records a pageview. Call it after client-side navigations in a single-page app.                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `traceten.track(eventName, properties?)` | Records a custom conversion event, which is also how you record a [goal](/goals/overview).                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `traceten.identify(arg)`                 | Links the current visitor to your user ID or email.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `traceten.resolveConsent(state)`         | Resolves consent from a custom banner: `"granted"` or `"denied"`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `traceten.getVisitorId()`                | Returns the current visitor cookie's value, or `null` if it hasn't resolved yet (before the first pageview, or while consent is pending) or the visitor has opted out. A pure read that never mints or writes a cookie. Use this instead of reading the cookie by name in your own code: the name changes if cross-subdomain cookies are toggled, but this always returns the current value. See [passing the visitor ID to your payment provider](/integrations/stripe#guest-checkout-pass-the-visitor-id-via-client_reference_id). |
| `traceten.getSessionId()`                | Returns the current session cookie's value, or `null` if it hasn't resolved yet. Unlike `getVisitorId()`, this is **not** `null` for an opted-out visitor: the session cookie is written regardless of opt-out status. A pure read, same caveats as `getVisitorId()` otherwise.                                                                                                                                                                                                                                                      |

Every method is wrapped so it never throws into your page. Invalid input is silently dropped, not rejected with an error. The troubleshooting section below lists the reasons an event can be dropped.

### Calling before the snippet loads

The snippet loads with `async`, so code that runs early may execute before `window.traceten` exists. Install this stub before the snippet tag and calls made in the gap are queued and replayed once the snippet initializes:

```html theme={null}
<script>
  window.traceten = window.traceten || {
    q: [],
    track: function () {
      this.q.push(arguments);
    },
  };
</script>
<script
  async
  src="https://cdn.traceten.com/tt.min.js"
  data-site="ttid_7Rb4TrC1dTbnD8w3s1TS12"
  data-cookie-domain="example.com"
></script>
```

Queued `identify` calls use the shape `["identify", arg]`; anything else in the queue is replayed as a `track` call.

## Pageviews in single-page apps

The snippet records a pageview once, when it first loads. It does not automatically detect client-side route changes. After each navigation in a single-page app, call the function form:

```javascript theme={null}
// After each client-side route change
if (window.traceten) {
  window.traceten("pageview");
}
```

This closes the measurement window for the page you are leaving and reports it, then starts a fresh window for the new page (scroll depth, time on page, active time, exit signals) and sends a new pageview event. Both events go out in a single request, so per-page metrics stay accurate across SPA navigations without doubling the number of requests you send.

Framework examples are in the [Next.js guide](/install/nextjs#single-page-application-navigation) and [troubleshooting](/install/troubleshooting#single-page-app-navigation-not-tracked).

## `traceten.track(eventName, properties?)`

Records a custom conversion event. Use it for the moments that matter to revenue: completed signups, purchases, booked demos, started trials.

```javascript theme={null}
traceten.track("demo_booked", {
  plan: "pro",
  source_page: "/pricing",
});
```

### Event name rules

The event name must match the pattern `^[a-z][a-z0-9_]*$`:

| Rule                 | Detail                                         |
| -------------------- | ---------------------------------------------- |
| Length               | 1 to 64 characters                             |
| First character      | A lowercase letter (not a digit or underscore) |
| Remaining characters | Lowercase letters, digits, underscores         |
| On violation         | The event is silently dropped                  |

Valid: `signup`, `demo_booked`, `trial_started_v2`. Invalid: `Signup`, `demo-booked`, `2fa_enabled`, `_internal`.

### Property rules

`properties` is an optional flat object. The snippet sanitizes it before sending:

| Rule                 | Detail                                                                                                     |
| -------------------- | ---------------------------------------------------------------------------------------------------------- |
| Maximum keys         | 10 (excess keys are silently dropped)                                                                      |
| Key length           | Keys longer than 40 characters are silently dropped                                                        |
| String values        | Truncated to 200 characters                                                                                |
| PII redaction        | Emails, phone numbers, card numbers, and SSN-like patterns in string values are replaced with `[redacted]` |
| Numbers and booleans | Passed through unchanged                                                                                   |

The reserved keys `value_cents` and `currency` (below) count toward the 10-key limit.

### Recording revenue: `value_cents` and `currency`

Two property keys are reserved. `value_cents` (a number, in the smallest currency unit) and `currency` (a string) are lifted out of `properties` and sent as top-level fields, which is how the dashboard attributes revenue to AI traffic:

```javascript theme={null}
traceten.track("subscription_purchased", {
  value_cents: 4900, // $49.00
  currency: "usd",
  plan: "pro",
  billing_interval: "monthly",
});
```

* `value_cents` must be a number. It is rounded to the nearest integer. `4900` means \$49.00 in USD.
* `currency` must be a string, for example `"usd"` or `"eur"`. Case does not matter: the code is uppercased at ingestion and stored as `USD`. See [Currencies](/integrations/currencies) for the list Traceten can attribute revenue in.
* If either has the wrong type it is treated as a regular property and does not attribute revenue. `currency` must also be exactly 3 characters: the ingestion endpoint rejects any other length, so a value like `"dollars"` stays an ordinary property rather than being promoted and losing the whole event.

### When a `track` call is dropped

A `track` call sends nothing when any of the following is true. Note that an
explicit `track()` call is treated as more intentional than a
[declarative goal](#declarative-goals-data-traceten-goal-and-data-traceten-scroll):
it is kept while a recognised consent tool is still deciding, whereas the HTML
triggers are not installed until that decision arrives.

* The visitor denied consent, or your site uses `consentDefault: "pending"` and consent is still unresolved (see the [consent guide](/install/consent)).
* The visitor has Do Not Track enabled or the `_traceten_optout` cookie set.
* The event name fails validation.

Consent behavior while a recognised CMP (TCF, OneTrust, Cookiebot) is still deciding: up to 10 calls are buffered in memory (no cookie is written and nothing is sent yet) and replayed once consent resolves to granted or no CMP is detected. Calls beyond the 10th are dropped silently. This is the same buffering `identify()` uses below, and it is why "kept" above means "not dropped," not "sent immediately."

## `traceten.identify(arg)`

Links the current visitor to your own user ID or email, so conversions that happen later (for example, a Stripe subscription) can be attributed back to the AI source that first brought the visitor.

Three call shapes:

```javascript theme={null}
// Your internal user ID
traceten.identify("usr_8f3ka92");

// Email only
traceten.identify({ email: "alice@example.com" });

// Both
traceten.identify({ id: "usr_8f3ka92", email: "alice@example.com" });
```

Validation:

| Field   | Rules                                                                           |
| ------- | ------------------------------------------------------------------------------- |
| `id`    | Non-empty string, at most 128 characters                                        |
| `email` | Lowercased and trimmed by the snippet; must contain `@`; at most 254 characters |

Fields that fail validation are dropped. If neither field survives, the call is a no-op.

What happens on a successful call:

1. The snippet POSTs to `https://ingest.traceten.com/v1/identify`. The email never enters Traceten analytics storage in plaintext; the server returns a one-way hash.
2. The snippet rewrites the `_traceten_vid` visitor cookie to that server-computed hash, so future sessions by the same identified user connect to the same visitor record.

Consent behavior: if consent is denied, the call is dropped. If consent is still pending, up to 10 `identify` calls are buffered in memory and sent once consent is granted.

## `traceten.resolveConsent(state)`

For sites that run their own consent banner instead of a supported CMP. Call it when the user makes a choice:

```javascript theme={null}
// User accepted
traceten.resolveConsent("granted");

// User declined
traceten.resolveConsent("denied");
```

* Only `"granted"` and `"denied"` are accepted. Any other value is a no-op.
* `"denied"` while consent is pending clears all queued events and buffered `identify()` and `track()` calls, and removes tracking cookies.
* `"granted"` after an earlier denial starts a fresh consented session.

To hold all tracking (including pageviews) until the user answers your banner, set `consentDefault: "pending"` in a pre-load stub. The full flow is in the [consent guide](/install/consent).

## Declarative click tracking: `data-traceten`

To track clicks on a specific element without writing JavaScript, add a `data-traceten` attribute with a label:

```html theme={null}
<a href="/signup" data-traceten="pricing_cta">Start free trial</a>
```

Each click sends a `click` event carrying the label, the element's `id` (if any), and the link URL.

Behavior details:

* Clicks on children of the labeled element count. The snippet walks up to 6 ancestor levels to find the nearest `data-traceten` attribute.
* Labels are clamped to 64 characters and PII-redacted like property values.
* Repeat clicks on the same element within 1 second are deduplicated.
* The link URL is included only for same-origin links, with sensitive query parameters stripped.
* Consent and opt-out are checked on every click; blocked visitors send nothing.

## Declarative goals: `data-traceten-goal` and `data-traceten-scroll`

Two more attributes fire a [goal](/goals/overview), which is a custom conversion event under the name the dashboard counts it by.

```html theme={null}
<!-- fires the `signup` goal on click -->
<button data-traceten-goal="signup" data-traceten-goal-plan-type="pro">Start free trial</button>

<!-- fires the `scroll_to_pricing` goal when the section enters the viewport -->
<section data-traceten-scroll="scroll_to_pricing">…</section>
```

Both send through the same endpoint as `traceten.track()`, with the same opt-out gate and the same property sanitizer.

* Goal names must match `^[a-z][a-z0-9_]*$` and may not be one of the [reserved billing names](/goals/overview#reserved-names). An invalid or reserved name is a silent no-op.
* `data-traceten-goal-*` attributes become properties, kebab-case converted to snake\_case, on either trigger. Both the key and the value are stored and read back: see [what happens to property keys and values](/goals/overview#what-happens-to-property-keys-and-values-exactly).
* Click goals reuse the `data-traceten` listener: children count, and repeat clicks within 1 second are deduplicated.
* Scroll goals fire **at most once per element, per page load**, not once per URL. A route change arms whatever has not fired yet, including the sections the new route just rendered, so a client-side navigation and a fresh load of the same URL record the same thing. They need `IntersectionObserver`, and do nothing where it is absent.

**They do not share `track()`'s behaviour while consent is unresolved.** Both listeners are installed only once a consent decision has been made, so a click or a scroll before your CMP answers records nothing, while a `traceten.track()` call in that same window is kept. The window is short but it covers the moment a visitor lands and clicks a hero button. If a goal has to survive it, call `traceten.track()` from your own handler.

`data-traceten` and `data-traceten-goal` are independent. An element may carry both, and each fires its own event, in either nesting order.

**The two goal triggers have different lifetimes across a back/forward restore.** The click listener is removed when the page is hidden (`pagehide`), which is long-standing behaviour of `data-traceten` click tracking that goal clicks inherit; the scroll observer is not. So on a page restored from the browser's back/forward cache, `data-traceten-scroll` still fires and `data-traceten-goal` no longer does, until the next full load. If a click goal has to survive that, call `traceten.track()` from your own handler.

Full detail: [Goals](/goals/overview) and [scroll tracking](/goals/scroll-tracking).

## Cross-subdomain tracking: `data-cookie-domain`

Without this attribute, Traceten's identity cookies are **host-only**: a cookie set on `www.example.com` is not sent to `checkout.example.com`. A visitor who lands on one subdomain and converts on another presents no cookie on the second host, is counted as brand new, and their conversion is never attributed to the visit that produced it.

This is off by default; creating a site does not turn it on. The dashboard does compute a suggestion (your registrable domain, derived from the domain you entered) and pre-fills it into **Sites → Settings → Cookies**, unconfirmed. Once you confirm it there, the value renders on every snippet the install page hands out:

```html theme={null}
<script
  async
  src="https://cdn.traceten.com/tt.min.js"
  data-site="ttid_7Rb4TrC1dTbnD8w3s1TS12"
  data-cookie-domain="example.com"
></script>
```

`_traceten_vid` and `_traceten_sid` are then written with `Domain=example.com` and shared across every subdomain, so one visitor stays one visitor from landing page to checkout.

Behavior details:

* Use the **apex only**: `example.com`, not `.example.com` or `www.example.com`. A leading dot is stripped.
* The value is ignored unless the current host is the apex itself or a subdomain of it. A mismatch falls back to host-only cookies rather than failing, so a staging host on a different domain degrades safely.
* If the browser rejects the broadened cookie, the snippet retries host-only so tracking still works on that page.
* Set it on **every** page that loads the snippet. A page that omits it writes a host-only cookie that the other subdomains cannot read.
* Only identity and cart cookies are broadened. The opt-out cookie is broadened too, deliberately, so opting out on one subdomain opts the visitor out across all of them.
* Some domains have no apex a browser will accept a `Domain` for, such as `localhost` or a bare IP. The attribute is omitted for those and cookies stay host-only.

To change the value or remove it, open **Sites → Settings → Cookies**. Clearing it scopes cookies to each exact host. Because the cookies are readable on every subdomain under the apex, clear it if you do not control every subdomain under your domain.

<Note>
  This only helps across subdomains of one registrable domain. It cannot span genuinely different
  domains: a browser will never send an `example.com` cookie to `example-checkout.com`. For hosted
  checkouts see below.
</Note>

### Hosted checkouts on another domain

Cookies cannot cross to a different registrable domain, so Traceten bridges the two most common cases without configuration:

| Checkout                                         | How the visitor carries across                                                                                                                  |
| ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| Stripe (`buy.stripe.com`, `checkout.stripe.com`) | The snippet appends `client_reference_id=<visitor_id>` to outbound Stripe links. A `client_reference_id` you set yourself is never overwritten. |
| Shopify (`*.myshopify.com`)                      | The cart/checkout token is captured and matched to the order.                                                                                   |

For any other checkout on a different domain, call [`traceten.identify()`](#identify) once the visitor authenticates so the pre-login visit and the post-login conversion resolve to one person.

## Endpoints the snippet calls

For CSP allowlists and network debugging, these are the requests a correctly installed snippet makes:

| Endpoint                                     | Method | Carries                                                                     |
| -------------------------------------------- | ------ | --------------------------------------------------------------------------- |
| `https://ingest.traceten.com/v1/events`      | POST   | Pageviews, `click` events, exit/behavioral signals                          |
| `https://ingest.traceten.com/v1/conversions` | POST   | Custom events from `traceten.track()`                                       |
| `https://ingest.traceten.com/v1/identify`    | POST   | `traceten.identify()` calls (needs a readable response, so it uses `fetch`) |

Events and conversions are sent with `navigator.sendBeacon`, falling back to `fetch` with `keepalive: true`, so they survive page unloads. Your `Content-Security-Policy` needs `connect-src https://ingest.traceten.com` (see [CSP troubleshooting](/install/troubleshooting#content-security-policy-csp-blocking-the-snippet)).

## Verify it worked

1. Open DevTools, go to the **Network** tab, and filter by `ingest.traceten.com`.
2. Trigger your event (for example, run `traceten.track("test_event")` in the console).
3. You should see a POST to `/v1/conversions`. Inspect the request payload to confirm `event_name` and `properties` are what you expect.
4. The event appears in the dashboard under **Events** within a minute.

## Troubleshooting

**No request appears when I call `traceten.track()`.**
Check the event name against the rules above (lowercase start, no hyphens, no capitals). Then check for an ad blocker, Do Not Track, or a denied consent state. All of these drop the event silently by design.

**`value_cents` shows up inside `properties` instead of attributing revenue.**
It was not a number (for example, you passed `"4900"` as a string). Pass a numeric value in cents.

**A property is missing from the received event.**
You passed more than 10 keys, or the key is longer than 40 characters. Both are dropped silently.

**A property value shows `[redacted]`.**
The value matched a PII pattern (email, phone number, card number). This is intentional: do not send PII in event properties. Use `traceten.identify()` for emails.

**`identify` did not change anything.**
The `id` exceeded 128 characters, or the email had no `@` or exceeded 254 characters, or consent is denied. If consent is pending, the call is buffered and sent after the user grants consent.

## Next

* [Server-side SDKs](/sdks/overview) for delivery that survives ad-blockers
* [Consent and CMP integration](/install/consent)
* [Verify installation](/install/verify)
* [Stripe revenue attribution](/integrations/stripe)
