Browser SDK
Drop-in analytics for any browser page. Captures page views, clicks, forms, page leave, rage clicks, and unhandled errors out of the box, and exposes explicit track, identify, and page calls through the same transport.
- Runtime: evergreen browsers. No IE.
- Module formats: ESM (npm) + IIFE (CDN).
- Types: shipped in
dist/types. - Zero runtime dependencies.
Install
Bundled apps (recommended)
npm install @getfluxly/browser
import { initGFlux } from "@getfluxly/browser";
const gflux = initGFlux({
apiKey: "gflux_pub_live_xxx",
apiHost: "https://api.getfluxly.com",
});
gflux?.page();
Side-effect boot
For when you want the SDK to read window.__GFLUX__ and boot itself without an explicit initGFlux call.
window.__GFLUX__ = {
apiKey: "gflux_pub_live_xxx",
apiHost: "https://api.getfluxly.com",
};
import "@getfluxly/browser/auto";
Script tag
Always pin a specific version so a silent update cannot change behavior. The snippet below works as-is. For extra hardening against a CDN compromise, add an SRI integrity hash: copy the real value from the GitHub release notes for the version you pin, or generate it yourself (below).
<script>
window.__GFLUX__ = {
apiKey: "gflux_pub_live_xxx",
apiHost: "https://api.getfluxly.com"
};
</script>
<script
src="https://cdn.jsdelivr.net/npm/@getfluxly/browser@0.7.2/dist/gflux.iife.js">
</script>
To generate the SRI hash for any version:
curl -sL https://cdn.jsdelivr.net/npm/@getfluxly/browser@0.7.2/dist/gflux.iife.js \
| openssl dgst -sha384 -binary | openssl base64 -A
Then add it to the script tag with crossorigin:
integrity="sha384-<hash from above>"
crossorigin="anonymous"
Script tag without inline config (strict CSP)
Use data attributes when a strict CSP blocks inline scripts entirely.
<script
src="https://cdn.jsdelivr.net/npm/@getfluxly/browser@0.7.2/dist/gflux.iife.js"
data-project="gflux_pub_live_xxx"
data-host="https://api.getfluxly.com">
</script>
Pre-boot queue
The /auto and CDN builds install window.gflux immediately. Calls made before DOMContentLoaded are queued and replayed in order after the real client boots.
Queued and replayed:
gflux.track(...)gflux.identify(...)gflux.page(...)
Control calls take priority over old queued work:
gflux.optOut()clears earlier queued tracking calls and queues the opt-out.gflux.reset()clears earlier queued tracking calls and queues the reset.gflux.destroy()clears earlier queued tracking calls and queues destroy.
Track an event
gflux.track("checkout_completed", {
order_id: "ord_456",
total_usd: 149.99,
});
track never throws. Errors go to the configured logger and are reflected in gflux.__debug() drop counters.
Identify a user
gflux.identify("user_42", {
email: "jane@example.com",
plan: "pro",
});
identify stitches earlier anonymous activity to the known user. Events buffered before identify are replayed with the new external_id and replayed: true when consent allows replay.
In implicit consent mode, identify also grants consent. In explicit consent mode, identify stores the user id, but buffered events wait until optIn().
Call reset() on logout or account switch before identifying another user.
identify never throws. Errors follow the same logger and drop-counter path as track.
Consent
With the default cookieless mode, there is no client-side buffer or storage while a visitor is anonymous: pre-consent history is stitched onto the profile server-side via the gfh_ id once the visitor calls identify(). The buffering and replay behavior described below applies when you opt out with cookieless: false (or use explicit consent mode without cookieless).
gflux.optIn(); // user accepted tracking; flush buffered events
gflux.optOut(); // persist opt-out; drop buffer; block future events
The SDK resolves every event through this priority order:
- Explicit opt-out blocks.
- GPC blocks when
gpcModeis"honor". Forregion: "us-ca"the SDK pins the effective mode to"honor"regardless of config; a localgpcMode: "ignore"cannot turn off GPC for California visitors. - Explicit opt-in tracks.
- Explicit consent mode buffers until opt-in.
- Honored DNT buffers until opt-in or identify.
- Otherwise track.
Buffered events stay in memory only. They are not written to storage. When replayed, they are sent with replayed: true.
When remote config is enabled and consent is still auto, the SDK buffers early events until /v1/sdk-config returns or times out. This keeps the first page view and queued calls on the same consent decision as later events.
Autocapture
Autocapture events are fired automatically. All fire through the same transport as track.
| Event | Trigger | Main properties |
|---|---|---|
page_view | Initial load and SPA navigation | url, path, title, referrer, referrer_domain, utm |
autocapture_click | Clicks on links, buttons, [role="button"], inputs, or [data-gf] | tag, text, href, id, classes, selector, path, data_gf |
autocapture_form | Form submit | form_id, form_name, action, method, field_count, path |
page_leave | visibilitychange hidden, beforeunload, and SPA navigation | path, time_on_page_ms |
rage_click | 3+ clicks within 1 second on the same interactive element | tag, text, href, id, classes, selector, path, click_count, window_ms |
js_error | Uncaught browser error | source, message, name, filename, lineno, colno, stack, path |
unhandled_rejection | Unhandled promise rejection | message, name, stack, path |
Disable individual capture types:
initGFlux({
apiKey: "gflux_pub_live_xxx",
apiHost: "https://api.getfluxly.com",
autocapture: {
pageviews: true,
clicks: true,
forms: false,
pageLeave: false,
rageClicks: false,
errors: true,
},
});
Disable all autocapture:
initGFlux({
apiKey: "gflux_pub_live_xxx",
apiHost: "https://api.getfluxly.com",
autocapture: false,
});
URL sanitization
Every captured url and referrer (on page_view, session_start, and first-touch attribution) is sanitized before it leaves the browser:
- The
#fragmentis dropped. - The value of any of these query parameters is replaced with
[redacted]:access_token,refresh_token,provider_token,id_token,token_hash,token,code,email.
This protects OAuth and magic-link redirects, which commonly place access and refresh tokens in the landing page URL. It is the default and does not need to be turned on.
If your app is a hash-based router and the route lives in the fragment, keep it with captureUrlFragment. Sensitive query params are still redacted either way.
initGFlux({
apiKey: "gflux_pub_live_xxx",
apiHost: "https://api.getfluxly.com",
captureUrlFragment: true,
});
Cross-subdomain tracking
By default (cookieless mode) no anonymous id is stored in the browser at all. When you opt out with cookieless: false, or after a visitor calls identify(), the persisted id is scoped to one host by default. Set cookieDomain to share it across subdomains of the same eTLD+1, so one person browsing www.example.com, docs.example.com, and app.example.com resolves to one profile.
initGFlux({
apiKey: "gflux_pub_live_xxx",
apiHost: "https://api.getfluxly.com",
cookieDomain: ".example.com",
});
On a script tag, use data-cookie-domain instead:
<script
src="https://cdn.jsdelivr.net/npm/@getfluxly/browser@0.7.2/dist/gflux.iife.js"
data-project="gflux_pub_live_xxx"
data-host="https://api.getfluxly.com"
data-cookie-domain=".example.com">
</script>
beforeSend
Inspect, amend, or drop any event before it is queued. Runs after the built-in URL sanitization above. Return the event to keep it, or null to drop it. A throwing hook is caught and the original event is kept, so a bug in your hook can never disable tracking.
initGFlux({
apiKey: "gflux_pub_live_xxx",
apiHost: "https://api.getfluxly.com",
beforeSend: (event) => {
if (event.event === "page_view" && event.properties.path === "/internal") {
return null; // drop internal-only pages
}
return event;
},
});
DOM controls
Ignore a subtree
Either attribute stops autocapture and declarative capture inside the node.
<div data-gf-ignore>
<button>Not captured</button>
</div>
<form data-gflux-ignore>
<!-- not captured -->
</form>
Declarative events
Attach a custom event directly in HTML without writing JavaScript.
<button
data-gflux-event="checkout_started"
data-gflux-prop-plan="pro"
data-gflux-prop-items="2">
Checkout
</button>
Supported aliases:
data-gflux-eventordata-gf-eventdata-gflux-prop-*ordata-gf-prop-*data-gf-ignoreordata-gflux-ignore
Property values are parsed as booleans, numbers, null, JSON objects, JSON arrays, or strings. Form field values are not captured.
Configuration
| Key | Type | Default | Notes |
|---|---|---|---|
apiKey | string | required | Browser publishable key (gflux_pub_...). |
apiHost | string | current origin | Use https://api.getfluxly.com for hosted GetFluxly. Validated with new URL() at boot. |
autocapture | false | object | all on | Per-type toggles or false for all off. |
sampling | number | 100 | Percentage of anonymous events to keep. Identified events are never sampled. |
dntMode | "honor" | "ignore" | "auto" | "auto" | "auto" resolves from remote config, then falls back to "ignore". |
gpcMode | "honor" | "ignore" | "honor" | GPC should stay honored in production. Overridden to "honor" whenever region resolves to "us-ca". |
consentMode | "implicit" | "explicit" | "auto" | "auto" | "auto" uses explicit consent for EU visitors. |
region | "auto" | "eu" | "us-ca" | "us" | "other" | "auto" | Usually fetched from /v1/sdk-config. "us-ca" is treated as CCPA scope and pins gpcMode to "honor" regardless of config. |
remoteConfig | boolean | string | true | Fetch project runtime config at boot. |
bufferMaxEvents | number | 200 | Max in-memory buffered events (FIFO eviction). |
maskedSelectors | string[] | [] | Text under matching selectors is masked during capture. Selectors are validated at boot; invalid ones are dropped. |
consentBanner | object | default copy | Built-in explicit-consent banner copy. Supports a nonce field for strict CSP (see below). |
maxRetries | number | 3 | Transport retries for transient failures (408, 425, 429, 5xx, network errors). 4xx client errors are not retried. |
logger | Logger | boolean | console | Pass false to silence, or supply a custom logger. |
flushQueueSize | number | 10 | Flush when the queue reaches this size. |
flushIntervalMs | number | 3000 | Background flush interval in milliseconds. |
cookieless | boolean | true | Cookieless is the default: while anonymous the SDK stores nothing and omits anonymous_id, and the server derives a daily-rotating gfh_ hash id at ingest. Set false for the classic persisted gf_anon_id. Also settable via data-cookieless on a script tag or remote config. |
cookieDomain | string | unset | Share the anonymous visitor id across subdomains of the same eTLD+1. See Cross-subdomain tracking. |
captureUrlFragment | boolean | false | Keep the URL #fragment on captured url/referrer. See URL sanitization. |
beforeSend | (event) => event | null | unset | Inspect, amend, or drop any event before it is queued. See beforeSend. |
Custom logger
initGFlux({
apiKey: "gflux_pub_live_xxx",
apiHost: "https://api.getfluxly.com",
logger: {
warn: (msg, ctx) => myLogger.warn({ msg, ...ctx }),
},
});
Pass logger: false to silence all SDK output.
CSP nonce for the consent banner
The built-in consent banner injects a <style> tag, which requires 'unsafe-inline' in style-src by default. Pass a nonce to avoid that:
initGFlux({
apiKey: "gflux_pub_live_xxx",
apiHost: "https://api.getfluxly.com",
consentBanner: {
nonce: "REQUEST_CSP_NONCE",
},
});
Or set a meta tag once per page; the SDK reads it if no explicit nonce is passed:
<meta name="gflux-csp-nonce" content="REQUEST_CSP_NONCE">
Storage
Under the default cookieless mode, none of the keys below are written while a visitor is anonymous. They appear only after identify() is called (or when cookieless: false).
| Key | Purpose |
|---|---|
gf_anon_id | Anonymous visitor id, set once identified (localStorage primary, cookie fallback with Secure on HTTPS). |
gf_external_id | Last identified user id. |
gf_traits | Last identify traits. |
gf_consent | Explicit opt-in. |
gf_optout | Explicit opt-out. |
Lifecycle
gflux.reset()
Clear the anonymous id, identified user, traits, and consent state. Call this on logout or before identifying a different user.
gflux.reset();
gflux.destroy()
Tear down listeners and flush in-flight requests. Returns a promise that resolves once the transport has drained.
await gflux.destroy();
Await this when you need delivery guarantees for in-flight events before tearing down the host page (for example, before navigating in a single-page test harness).
gflux.__debug()
Return the current buffer size, dropped-event counters, resolver config, and current consent decision. Use when investigating why an event did not arrive. Drop counts surface sampling, autocapture toggles, and consent rejections.
gflux.__debug();
// {
// bufferSize: 3,
// bufferDropped: 0,
// dropCounts: { sampled: 12 },
// resolverConfig: { dntMode, gpcMode, consentMode, region },
// decision: { decision: "track" }
// }
Errors
track and identify never throw. Errors are:
- Logged via the configured
logger(defaults toconsole.warn). - Counted in
gflux.__debug().dropCountsunder the relevant reason.
This means a mis-typed event name or a network failure will never crash your application.
Bundle size
The IIFE bundle gzips to ~9 KB. Tree-shaken ESM imports are smaller.