Tag-loader (drop-in script)
Run Fabric Experiments on a marketing site or via Google Tag Manager without touching application code.
The tag-loader is a single <script> build of the web SDK. It auto-creates an
anonymous subject id, fetches the signed manifest, and applies declarative DOM
ops from each running experiment — governed delivery without application code.
The local static-site path is continuously smoke-tested by
pnpm run smoke:new-project:browser: it creates a new project, publishes a
static manifest + tag-loader, opens a tiny site in headless Chrome, verifies a
DOM operation, and asserts an fx_exposure event reaches window.dataLayer.
Install — Fabric-hosted (recommended for marketing sites)
The fastest install is to load the bundle directly from the Fabric-operated CDN — no build pipeline, no upload step. This is the same model Google Analytics uses: paste the snippet, ship.
<style id="fx-anti-flicker">
html[data-fx-loading] body { visibility: hidden; }
</style>
<script>
document.documentElement.setAttribute('data-fx-loading', '');
window.__FX__ = {
manifestUrl: 'https://manifest.experiments.fabric.pro/<orgId>/manifest',
beaconEndpoint: 'https://ingest.experiments.fabric.pro/<orgId>/v1/exposure',
beaconFlushIntervalMs: 5000,
antiFlickerStyleId: 'fx-anti-flicker',
};
</script>
<script
src="https://cdn.fabric.pro/v/0/experiments.tag.global.js"
async
></script>URL scheme:
| Path | Behavior |
|---|---|
/v/0.2.1/experiments.tag.global.js | Pinned exact version. Immutable, 1-year browser cache. |
/v/0/experiments.tag.global.js | Major-pinned. Auto-updates within 0.x releases. |
/v/latest/experiments.tag.global.js | Always newest. Use for prototyping; pin a major in production. |
Subresource Integrity (SRI) hashes for every published version are
exposed at https://cdn.fabric.pro/index.json:
<script
src="https://cdn.fabric.pro/v/0.2.1/experiments.tag.global.js"
integrity="sha384-…" {/* paste from index.json */}
crossorigin="anonymous"
async
></script>Install — self-hosted (full control)
If you'd rather not load JavaScript from a third-party origin (CSP-strict shops, regulated industries, sites with strict supply-chain reviews), host the bundle yourself. Two ways:
Via the CLI — fx publish --static ./dist-fx writes the signed manifest,
JWKS, revocation record, and tag-loader bundle into a directory you upload
to your CDN. When deploying beneath a CDN path prefix, set
manifestJwksUrl and manifestRevocationsUrl to the exported files. The
self-host page provides a complete example.
Via npm — install @fabricorg/experiments-web and copy the file from
node_modules/@fabricorg/experiments-web/dist/experiments.tag.global.js
into your asset pipeline.
Either way the snippet becomes:
<script src="https://cdn.example.com/experiments.tag.global.js" async></script>How it works
window.__FX__ is the only global the loader expects. Set it before the
loader script tag so the bootstrap reads it on first execution.
The loader exposes window.fx — a Promise<ClientApi> you can await from
later scripts:
<script>
window.fx?.then(client => {
if (client.treatment('homepage-cta') === 'treatment') {
console.log('user is in treatment');
}
});
</script>What's auto-wired
- Anonymous subject id — cookie
_fx_sid(365d, samesite=lax). Override with__FX__.cookieNameor pass__FX__.subjectIddirectly for logged-in users. - DOM ops — variants with
domOps(replaceText, setAttr, addClass, injectCSS, etc.) apply on assignment. - Triggers — when a manifest experiment declares a
trigger(urlMatch, waitForSelector, event), activation defers until the trigger fires. - Exposures — pushed to
window.dataLayerso GTM tags can listen for thefx_exposureevent. Set__FX__.noDefaultAdapter = trueto suppress. - Warehouse telemetry — exposures are batched to
beaconEndpoint; calls toclient.track()are sent to the sibling/conversionendpoint. The edge acknowledges only after Cloudflare Queues accepts the batch, then writes an idempotent NDJSON object to R2 with retries and dead-letter handling. - Anti-flicker — the optional guard style is removed on success, failure, or a two-second safety timeout, so a manifest outage cannot leave the page hidden.
- QA inspector — add
?fxdebug=1to load the separate, read-only inspector asset. It shows manifest version, experiment state, eligibility, and assignment without increasing the normal loader bundle. - Preview — when
?fxpreview=<expId>:<variant>&fxtoken=<jwt>is in the URL, the SDK verifies the short-lived token with public JWKS from/.well-known/jwks/<orgId>and forces the variant in this browser only. Preview assignments are deliberately not exposure-tracked.
Configuration reference
| Field | Type | Default | Notes |
|---|---|---|---|
manifestUrl | string | required | Origin must be CORS-friendly |
manifestJwksUrl | string | derived from manifestUrl | Public JWKS used to verify signed manifests; set explicitly for a CDN path prefix |
manifestRevocationsUrl | string | derived from manifestUrl | Revoked manifest-version record; set explicitly for a CDN path prefix |
subjectId | string | auto | Override the anonymous id |
cookieName | string | _fx_sid | Anonymous-id cookie name |
cookiePrefix | string | 'fx.' | Sticky-bucket key prefix |
previewJwksUrl | string | derived from manifestUrl | Optional public JWKS URL for preview token verification |
beaconEndpoint | string | — | URL for batched exposure POSTs via sendBeacon |
beaconFlushIntervalMs | number | — | Periodic exposure/conversion flush; pagehide flushing is always enabled |
antiFlickerStyleId | string | — | ID of the temporary guard style removed after bootstrap |
inspector | boolean | false | Load the read-only QA inspector without a query parameter |
inspectorUrl | string | beside tag-loader | Override the inspector asset URL |
noDefaultAdapter | boolean | false | Suppress the default dataLayer.push |
onExposure | function | — | Custom exposure handler |
onRecipeFailure | function | — | Fires only when variant code throws — additive to onExposure |
onActivationFailure | function | — | Fires when an activation trigger rejects, throws, or times out |
excluded | bool | function | — | Truthy → SDK is no-op (bots, opt-outs, consent) |
maxErrorStackLength | number | 1000 | Cap on failure.stack chars sent to handlers |
Realistic install (with adapters)
<script>
window.__FX__ = {
manifestUrl: 'https://manifest.example.com/acme/manifest',
cookiePrefix: 'acme.fx.',
excluded: () => /bot|crawler/i.test(navigator.userAgent),
onRecipeFailure: (rec) => {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'fx_recipe_failure',
fx_experiment_id: rec.experimentId,
fx_variant_key: rec.variantKey,
fx_failure_message: rec.failure?.message,
});
},
};
</script>
<script src="https://cdn.example.com/experiments.tag.global.js" async></script>Recommended HTTP headers
Preview tokens are bearer credentials in URLs. They are signed by Studio/API and verified with public keys, so no preview secret belongs in customer HTML. Still set:
Referrer-Policy: no-referrer…on pages that may receive preview links so the token isn't leaked via the
Referer header to third-party origins.
See also
- Migrating from Mojito — same install pattern, no app code.
- Web adapters — Snowplow, GA, GTM.
- Web SDK — the underlying programmatic client.