Consent
Consent is decided in the browser. Your CMP is the single source: pass
consent.getCategories (and consent.onConsentChanged for live updates) and it
drives two things at once:
- Silo’s own collection — a client-side gate over everything silo sends itself: core events to the gateway, time-on-page beacons, and ubid. Denied → silo sends nothing for that capability.
- Third-party device-mode pixels —
@segment/analytics-consent-toolsstill wraps analytics-next to gate (and stamp) device-mode destination pixels.
No consent stamping on silo’s own events, no backend enforcement — the gate decides client-side and silo simply doesn’t send when denied.
How it works
Section titled “How it works” your CMP (getCategories / onConsentChanged) ← single source │ ┌───────┴────────┐ ▼ ▼ silo consent gate @segment/analytics-consent-tools (client-side) (client-side) ├─ core events └─ third-party device-mode pixels │ → silo gateway ├─ time-on-page beacons └─ ubid- The decision happens entirely in the browser. Denied → silo never sends. There is no consent flag on silo’s own events and the backend never reads consent — events are stored as opaque JSON.
- The
analyticscapability gates silo’s own collection: a denied event has itsSegment.iosource turned off (the gateway POST is skipped) while device-mode pixels — gated separately by consent-tools — still fire. modeis per-writeKey, published in the CDN settings JSON by the control plane and tunable without a redeploy — see Remote Settings.
The silo gate
Section titled “The silo gate”The gate resolves each capability independently. Precedence (highest first):
- GPC (
navigator.globalPrivacyControl) — when on, denies every silo capability and cannot be overridden by a CMP opt-in. - Live CMP category value — the current value for the mapped category, once the CMP has set one.
modedefault — the per-writeKey do-nothing default when the visitor has expressed no choice (see opt-in vs opt-out).
A denied capability means silo emits nothing for it: the core source middleware
turns off Segment.io for the event, the time-on-page plugin holds its beacons,
and ubid is neither computed nor stamped.
opt-in vs opt-out
Section titled “opt-in vs opt-out”The per-writeKey mode sets the default applied before the visitor has chosen:
opt-out(default) — silo collects by default (US / no consent regime). The visitor must deny to stop collection.opt-in— silo collects nothing until consent (GDPR / ePrivacy / Quebec Law 25).
mode lives in the CDN settings (silo.consent.mode) so it’s tunable per
writeKey without a redeploy — see Remote Settings.
consent.offlineFallbackMode in code config is the fallback used whenever the
remote silo.consent.mode is absent — the CDN is unreachable, or its settings
load but don’t set mode; set it to "opt-in" on EU/Quebec properties so a CDN
outage fails safe. Setting "opt-out" is a no-op (it’s already the built-in
fallback).
ubid (universal browser id) is gated by a consent category, resolved through the same precedence above. Denied → no fingerprint is computed, persisted, or stamped.
What sets it apart from analytics is its default regime: ubid is opt-in,
independent of the property’s consent.mode. A browser fingerprint is profiling
tech, so it collects nothing until the visitor consents — even on an opt-out
property. To collect ubid by default (US / no-consent-regime properties), set
silo.ubid.mode: "opt-out" for that writeKey in
Remote Settings — the only knob that flips it.
Identity durability on Safari / iOS
Section titled “Identity durability on Safari / iOS”anonymousId lives in script-writable storage (localStorage + a client-side
cookie), so it falls under WebKit’s 7-day cap: on Safari — and every iOS
browser, since they all run on WebKit — a visitor who returns after more than 7
days without interacting with the site has that storage wiped and is assigned a
new anonymousId. The counter resets on each first-party interaction, so
active visitors keep their id; the loss only hits genuine >7-day gaps. Chrome and
Firefox on desktop (and Android Chrome) don’t apply this blanket first-party cap,
so they’re largely unaffected.
Mapping capabilities to your categories
Section titled “Mapping capabilities to your categories”Silo’s capabilities are analytics and ubid. By default each looks up a CMP
category of the same name. Use consent.map when your CMP uses different
names (OneTrust group labels, TCF purposes, etc.):
map: { analytics: "statistics", ubid: "deviceStorage" }Runtime changes need onConsentChanged
Section titled “Runtime changes need onConsentChanged”getCategories seeds the gate once at load. To pick up a mid-session consent
change, register consent.onConsentChanged: it’s fanned out to both the
silo gate and consent-tools, so one CMP subscription re-resolves first-party
collection and re-stamps device-mode pixels together. Omit it and the gate uses
seed-time consent until the next full page load.
Custom React banner
Section titled “Custom React banner”import { AnalyticsBrowser } from "@adpharm/silo-analytics";
let consent: Record<string, boolean> | undefined;let notifyConsentChanged: | ((categories: Record<string, boolean>) => void) | undefined;
export const analytics = AnalyticsBrowser.load({ writeKey: "your-write-key", env: "production", consent: { // Single consent source for both consent-tools and silo's gate. Returns the // banner's current per-category choice; pre-choice it returns all-false, so // an opt-in writeKey collects nothing until the visitor accepts. getCategories: () => consent ?? { analytics: false, ubid: false }, // The provided notify callback re-resolves silo's gate AND re-stamps // consent-tools' device-mode pixels. Required for the banner's accept/deny // to take effect without a reload. onConsentChanged: (notify) => { notifyConsentChanged = notify; }, // Capability names match the banner's category names here, so no `map` // needed. Add one if they differ, e.g. `{ ubid: "analytics" }`. // EU/Quebec property → fail CLOSED on a CDN outage: collect nothing if the // remote `silo.consent.mode` can't be fetched. (Remote value overrides this // when reachable.) offlineFallbackMode: "opt-in", },});
// On accept, update the categories and notify — fans out to silo + consent-tools.export function onAccept() { consent = { analytics: true, ubid: true }; notifyConsentChanged?.(consent);}OneTrust
Section titled “OneTrust”import { AnalyticsBrowser } from "@adpharm/silo-analytics";
// OneTrust group ids → category names. The same names drive consent-tools'// device-mode pixel gating AND silo's first-party gate (via `map` below).function getOneTrustCategories(): Record<string, boolean> { return { statistics: window.OnetrustActiveGroups?.includes("C0002") ?? false, advertising: window.OnetrustActiveGroups?.includes("C0004") ?? false, };}
export const analytics = AnalyticsBrowser.load({ writeKey: "your-write-key", env: "production", consent: { getCategories: getOneTrustCategories, // Bridge OneTrust's change event. Fanned out to BOTH consent-tools // (re-stamps device-mode pixels) and silo's gate (re-resolves first-party // collection live) — required for mid-page consent changes to take effect. onConsentChanged: (notify) => { window.OneTrust?.OnConsentChanged(() => notify(getOneTrustCategories())); }, // Map each silo capability to this site's CMP category name. Both silo // capabilities ride the OneTrust "statistics" (C0002) group here. map: { analytics: "statistics", ubid: "statistics" }, // US property → fail OPEN on a CDN outage: collect by default if the remote // `silo.consent.mode` can't be fetched. `"opt-out"` is also the built-in // fallback, so this line documents intent rather than changing behavior — // a remote `silo.consent.mode` overrides it whenever reachable. offlineFallbackMode: "opt-out", },});TCF / IAB
Section titled “TCF / IAB”import { AnalyticsBrowser } from "@adpharm/silo-analytics";
// TCF purposes → category names. Purpose 8 ("Measure content performance") maps// to analytics; purpose 1 ("Store and/or access information on a device") gates// the device-stable ubid. The same names drive consent-tools AND silo's gate.function getTcfCategories(): Record<string, boolean> { if (typeof window.__tcfapi !== "function") return { measurement: false, deviceStorage: false }; let result: Record<string, boolean> = { measurement: false, deviceStorage: false, }; window.__tcfapi("getTCData", 2, (data, success) => { if (success && data.cmpStatus === "loaded") { result = { measurement: data.purpose.consents[8] === true, deviceStorage: data.purpose.consents[1] === true, }; } }); return result;}
export const analytics = AnalyticsBrowser.load({ writeKey: "your-write-key", env: "production", consent: { getCategories: getTcfCategories, // Bridge TCF user actions. Fanned out to BOTH consent-tools (device-mode // re-stamp) and silo's gate (re-resolves first-party collection live) — // required for runtime consent changes to take effect. onConsentChanged: (notify) => { window.__tcfapi?.("addEventListener", 2, (data, success) => { if (success && data.eventStatus === "useractioncomplete") { notify(getTcfCategories()); } }); }, // Map silo capabilities to the TCF-purpose category names above. map: { analytics: "measurement", ubid: "deviceStorage" }, // GDPR/ePrivacy property → fail CLOSED on a CDN outage: collect nothing if // the remote `silo.consent.mode` can't be fetched. GPC still denies // everything regardless. (Remote value overrides this when reachable.) offlineFallbackMode: "opt-in", },});