---
title: "SDK Changelog"
description: "Release notes for the Noxtica browser collector SDK: what changed in each version, who is affected, what to do, and which @noxtica/sdk version pairs with which collector release."
canonical_url: "https://noxtica.com/docs/changelog/"
markdown_url: "https://noxtica.com/docs/changelog/index.md"
last_updated: 2026-10-24
published: 2026-10-24
section: "browser-sdk"
section_title: "Browser SDK"
site: "Noxtica"
language: "en"
source: markdown
---

# SDK Changelog

Release notes for the browser collector SDK, the core and extensions scripts served from your collector host. Each entry says what changed, who is affected and what to do. The newest release is first, and the version number is the one the collector reports: `window.noxtica.v` on a page that runs 3.7.0 or later.

Only one release needs a code change on your side: **3.8.0**, tagged **Breaking** below. Every other release is additive or a fix, so a page that adopts none of the new surface keeps working. Releases before 3.5.2 are not listed. Install steps, consent modes, events and the command queue are described on [Install and Browser Runtime](https://noxtica.com/docs/browser-runtime/).

The stable collector URLs serve the latest build. A hashed build you pinned with Subresource Integrity keeps serving for the last few releases, so to take a release, copy the new pinned tag from Backoffice → Integrations & API → Script & Consent → Snippets. The version number names a release, not the exact bytes: when a fix ships without a version bump, the hashed file name and its integrity value identify the build.

## A client-side fingerprintId is a hint

The core (minimal) and extensions (full) runtimes legitimately mint different ids for one browser. A `fingerprintId` that arrives in `noxtica:collected` or `noxtica:cache-hit` is therefore a hint for your page, not an identity. The only authoritative id is the one returned to your backend by `POST /v1/protected-actions/verify`. Release 3.8.1 fixes a race so that one page load produces one page check and one submit; before it, the race could produce two `noxtica:collected` events with different ids. For the server-side step see [Backend Integration](https://noxtica.com/docs/backend-integration/).

## Compatibility with @noxtica/sdk

`@noxtica/sdk` is the typed wrapper around the collector script. It does not contain the collector: the script always comes from your collector host, so the pair that runs on a page is one collector release (this page) and one wrapper release. The [Developers page](https://noxtica.com/developers/) states what is currently published.

| Collector release | `@noxtica/sdk` | Notes |
| --- | --- | --- |
| 3.8.1 | 0.3.0 (targets this release) | 0.3.0 needs a collector core of 3.8.0 or newer. |
| 3.8.0 | 0.3.0 (minimum supported core) | `loadExtensions()` resolves a result object (Breaking). |
| 3.7.x, 3.6.x, 3.5.2 | 0.2.x | 0.2.x types `loadExtensions()` as a boolean and passes the loader's value through. On a 3.8.0 or newer core it still resolves the object at run time, which is always truthy: use `.ok`, or move to 0.3.0. |

Against a core older than 3.8.0, 0.3.0 turns the boolean into the 3.8.0 result shape: `{ ok: true, loaded: [] }` for `true` and `{ ok: false, reason: 'unknown', code: 'legacy_loader' }` for `false`. The session members (`@noxtica/sdk/session`, from 3.7.0) throw on a core older than 3.7.0, as `track()` does on a build that predates it. The wrapper has its own release notes for typings and framework helpers; this page covers the collector.

## SDK 3.8.1 (2026-10-24)

Fixes and consent-handling changes; nothing is removed or renamed.

**Fixed**

- **One page check is one submit.** With consent delivered at `noxtica:ready`, `loadExtensions()` called at the same point and a token response slower than the extensions script, a page check could run twice: two submits, two runtimes and two `noxtica:collected` events with different `fingerprintId` values. A page-check call that is still waiting for the token now joins the runtime when it attached in the meantime. One page load produces one submit.
  - Who: pages that grant consent and call `loadExtensions()` early, with `data-auto-check-once` or `checkOnce()`; most visible on cold devices where the token round trip is slower than loading the extensions script.
  - Do: nothing. Remove any de-duplication you added around `noxtica:collected`, and take the authoritative id from your server-side verify call.
- **No second skipped event.** Replaying an already-settled, skipped page check no longer dispatches a second `noxtica:collected` with `skipped: 'consent_required'`. Only a real result is announced.
- **One token per page session across the core to extensions hand-off.** On the documented install (the core with auto-loaded extensions) the extensions runtime could mint its own token a few seconds after the core's. A payload the core had collected, sealed under the first token, was then submitted under the second token and the server answered `400 Unable to process request`. The core now hands its token to the runtime, which adopts it when it has more than 30 seconds of life left; later rotations mint normally.
  - Who: pages on the documented install that call `collect()` then `submit()` (or `checkOnce()`) with the extensions loaded, on a domain with KHAN armed or a guarded RUM policy.
  - Do: nothing. No configuration or server change. Re-copy a pinned tag to get the build.

**Changed**

- **The pre-init command queue is coalesced.** When the core replays `window.noxtica.q`, every `setConsent` or `consent` update folds into one update (a later key wins) applied at the position of the first one, so you get one `noxtica:consent-changed`. A queued `loadExtensions` is dropped when the folded state denies all four categories: it answers `noxtica:extensions-failed` with `{ ok: false, reason: 'consent_missing', code: 'consent_withdrawn' }`. Invalid entries and `consent gcm` run unchanged. A later live `noxtica('loadExtensions')` is an explicit call and still loads.
  - Who: pages that queue several consent updates before the core loads, or queue `loadExtensions` ahead of a consent banner.
  - Do: expect one `noxtica:consent-changed` for the whole queue. Prefer deciding consent in a `noxtica:ready` handler over queuing it.
- **The signed consent mode is announced, and `getConsent()` says whether it is known.** `noxtica:consent-changed` now also fires when the mode signed for your domain differs from the page's declaration at token time (for example an untagged page on a domain set to API mode: `not_required` becomes `api`). `getConsent()` and the event `detail` carry `modeResolved`, which is `false` until the token applied the signed mode. Auto-loading extensions (`data-extensions="auto"`) fails closed: before the mode is known it needs an explicit grant, and it never runs when every category is denied. A manual `loadExtensions()` is unchanged.
  - Who: pages that rely on auto-load without declaring a consent mode on the tag, and code that compares `getConsent().mode` with the tag's declaration.
  - Do: declare the mode on the tag (`data-consent-mode`) or grant consent explicitly, and gate your own logic on `modeResolved` rather than on `mode` alone.
- **The device cookie is minimal.** `nox_fp_<siteKey>` now carries only the state schema version, the device id and the time of the last submit, which is what recovery of the device id and its cache lifetime need. The last result (risk score, level, confidence, flags) stays in `localStorage` and IndexedDB. A cache hit recovered from the cookie alone returns `cachedResult: null`.
  - Who: sites that describe cookie contents in a cookie notice, and code that reads `cachedResult` after the cookie was the only surviving storage.
  - Do: update the notice wording, and treat `cachedResult: null` as a prompt to check again.

## SDK 3.8.0 (2026-10-12), Breaking

`loadExtensions()` resolves a result object instead of a boolean. An object is always truthy, so `if (await client.loadExtensions())` and `=== true` no longer test success.

**Changed**

- **`loadExtensions()` never rejects and resolves a result.** This covers `client.loadExtensions(options)`, the static `NoxticaCollector.loadExtensions()` and `noxtica('loadExtensions', options)`. Success is `{ ok: true, loaded: string[] }`, where `loaded` is the extensions script URL the core injected (`[]` on the combined bundle, or when a tag you added yourself attached the runtime); later calls resolve the same shape without a second fetch. Failure is `{ ok: false, reason, code, url?, status?, error? }`: `reason` is a closed set, `code` is the finer machine-readable cause, `url` is the attempted file, `status` the HTTP status when one was seen and `error` human text that you should not parse.
- **The extensions events carry the result.** `noxtica:extensions-loaded` and `noxtica:extensions-failed` now have `detail` of `{ message: 'attached' | 'not-loaded', ...result }`. In 3.6.0 and 3.7.0 the detail was that string; read it from `detail.message`. Only a real load attempt dispatches `noxtica:extensions-failed`.
- **`loadExtensions({ requireConsent: true })`** refuses to fetch in a gated mode (API or cookie) while no category is granted and answers `{ ok: false, reason: 'consent_missing', code: 'consent_required' }`. The plain call needs no consent, as before.

| `reason` | Meaning |
| --- | --- |
| `consent_missing` | Only with `requireConsent: true`, or when a queued call was dropped because every category is denied (3.8.1). Nothing is fetched. |
| `csp_blocked` | An enforced Content-Security-Policy violation blocked the script URL. `code` is the effective directive (`script-src-elem` or `script-src`). |
| `integrity_mismatch` | The browser rejected the pinned file and its hash differs from the pin (`code: sha384_mismatch`). |
| `network` | The file could not be fetched (`code: http_<status>` or `fetch_failed`). |
| `timeout` | No load or error event within 20 seconds (`code: script_timeout`). The next call retries. |
| `already_loaded` | A script tag for the URL is already in the page but nothing registered (`code: script_present`). Nothing is injected. |
| `unknown` | Everything else, for example a build mismatch or a hand-off error. |

The reason is best effort: the browser's error event carries no cause, so a failed script costs one extra diagnostic request for the same file. A Content-Security-Policy whose `connect-src` also blocks that request reads as `network`.

- Who: code that tests the return value, or that reads the `detail` of the extensions events as a string.
- Do: test `result.ok`, log `result.reason` and `result.code` when it is `false`, and read the old string from `detail.message`. With `@noxtica/sdk`, move to 0.3.0 for the matching types.

## SDK 3.7.0 (2026-10-09)

Additive. A page that uses none of it behaves as 3.6.0, except that the first core or combined bundle publishes `window.noxtica.v` (creating the command-queue stub when the page defined none) and `getConsent()` and `noxtica:consent-changed` details carry `privacySignal`.

**Added**

- **Session API.** `setTag` (replaces a key's values), hashed `identify` (the raw id is never stored or sent), `event`, `getMetadata`, `upgrade`, and a byte-capped pre-roll for conditional recording. Queue commands: `set`, `identify`, `event`, `metadata`, `upgrade`.
- **Session lifecycle.** `pause()`, `resume()`, `stop()` and `start()` on the client and as queue commands. All are idempotent.
- **Google Consent Mode v2 bridge.** `data-consent-mode="gcm"`, `createClient({ consentMode: 'gcm' })`, `client.enableGoogleConsentMode()` or `noxtica('consent', 'gcm')` make the collector follow `analytics_storage`.
- **GPC and DNT on the tracker and console lanes.** With the default signed policy (`honor`), a visitor who sends Global Privacy Control or Do Not Track gets neither the tracker nor console-log capture. `getConsent().privacySignal` reports `'gpc'`, `'dnt'` or `null`, as a local flag that is never sent. The `documented_override` policy runs both lanes.
- **Duplicate-tag guard (`NX001`).** The first core or combined bundle on a page claims it and sets `window.noxtica.v`. A second one logs `NX001` and is refused, so no second client is created.

- Who: everyone gets `window.noxtica.v` and `privacySignal`; the rest is opt-in. Pages with two collector tags (a hand-pasted tag plus a tag-manager copy) now see the `NX001` warning and only the first one runs.
- Do: remove the extra tag if you see `NX001`. If your tracker lane must also run for visitors who send GPC or DNT, review the domain policy before you upgrade.

## SDK 3.6.0 (2026-10-08)

Additive. A page that uses none of it behaves as 3.5.2; the visible differences are that `window.noxtica` is installed as a function by auto-init and `noxtica:ready` is dispatched.

**Added**

- **`noxtica:ready`** on `document` (`detail: { client }`), dispatched once per auto-init tag after the queue ran and before the automatic page check starts. A page without `data-auto-init` gets neither the event nor the queue.
- **The `window.noxtica` command queue** (the gtag pattern): define `window.noxtica = window.noxtica || function () { (window.noxtica.q = window.noxtica.q || []).push(arguments); }` before the core loads. The core replays the queue in order against the first client it creates, then makes `window.noxtica(...)` live. Commands: `setConsent`, `loadExtensions`, `protect`, `checkOnce`. Unknown commands log a warning, and failures go to `noxtica:error` instead of throwing.
- **`noxtica:consent-changed`** (`detail` is the `getConsent()` snapshot), dispatched only when a consent category really changes.
- **`noxtica:extensions-loaded` and `noxtica:extensions-failed`**, dispatched by the core (never by the combined or extensions bundles). The detail was the string `'attached'` or `'not-loaded'`; since 3.8.0 it is the result object.
- **Same-page re-collect is evented.** In API or cookie mode, a page check skipped for consent (`noxtica:collected` with `skipped: 'consent_required'`) runs again with its original options when `deviceId` is granted later on the same page, and announces `noxtica:collected` (and `noxtica:cache-hit` when the cache answers it).

- Who: pages that want to react to readiness, consent or extension loading without polling, and consent banners that load after the collector.
- Do: adopt `noxtica:ready` and the queue when convenient.

## SDK 3.5.2 (2026-10-05)

Additive; completes 3.5.1 of the same day.

**Changed**

- **The legacy URLs serve the core.** `/collector/noxtica-collector.js` (3.5.1) and `/collector/noxtica.js` (3.5.2) return the same bytes, content type, CORS headers and cache class as `/collector/noxtica-collector-core.js`. A core evaluated from a legacy URL defaults `extensions` to `'auto'`: the extensions load immediately in consent mode `not_required`, and on the first category grant in API or cookie mode. An explicit `extensions` option or `data-extensions` value still wins (any value other than `auto` opts out), and the core URL itself keeps the manual default.
- **Pinned combined builds keep working.** The combined bundle is still built, and its hashed file keeps serving the combined bytes for existing integrity pins. `asset-manifest.json` adds `servedAs` and `deprecated` entries for the legacy and combined files.

- Who: pages that still load a legacy URL. Before the extensions attach, extension-lane client methods (`track`, tracker and replay setters) run the core's reduced lane and are not replayed after the hand-off; `maskTextFn` and `maskInputFn` apply only once the extensions runtime owns the client.
- Do: nothing is required. Move to the core tag plus extensions when convenient, as described on [Install and Browser Runtime](https://noxtica.com/docs/browser-runtime/).
