Skip to content

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:

  1. 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.
  2. Third-party device-mode pixels@segment/analytics-consent-tools still 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.

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 analytics capability gates silo’s own collection: a denied event has its Segment.io source turned off (the gateway POST is skipped) while device-mode pixels — gated separately by consent-tools — still fire.
  • mode is per-writeKey, published in the CDN settings JSON by the control plane and tunable without a redeploy — see Remote Settings.

The gate resolves each capability independently. Precedence (highest first):

  1. GPC (navigator.globalPrivacyControl) — when on, denies every silo capability and cannot be overridden by a CMP opt-in.
  2. Live CMP category value — the current value for the mapped category, once the CMP has set one.
  3. mode default — 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.

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.

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.

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" }

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.

analytics.ts
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);
}
analytics.ts
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",
},
});
analytics.ts
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",
},
});