@adfinia/sdk-web
v1.5.0
Published
Official Adfinia SDK for web — first-party event + identify ingestion.
Maintainers
Readme
@adfinia/sdk-web
Adfinia Web SDK — event + identify ingest for browser apps. Around 19.5 KB minified for the IIFE bundle (~6 KB gzipped); tree-shakes further in modern bundlers.
- Tiny: ESM, CJS, and
<script>-friendly IIFE builds. - Reliable: events buffer to
localStorageand survive page reloads, crashes, and offline windows. Last-mile delivery usesnavigator.sendBeaconon tab close so the final batch lands even when the page is unloading. - Consent-aware: opt-out by default until your consent callback says yes.
- Privacy-first context: browser context (page, viewport, locale, referrer, user-agent) is opt-in via
autoContext: true. Default OFF. - Typed: full TypeScript types exported.
Install
npm install @adfinia/sdk-web
# or
pnpm add @adfinia/sdk-web
# or
yarn add @adfinia/sdk-webOr drop the IIFE bundle in via <script>:
<script src="https://cdn.adfinia.com/sdk-web/1.1.0/adfinia.iife.js"></script>
<script>
Adfinia.init({ writeKey: 'pk_live_...' })
Adfinia.track('Page Viewed')
</script>Quickstart
import { Adfinia } from '@adfinia/sdk-web'
Adfinia.init({
writeKey: 'pk_live_your_public_key_here',
consent: () => window.__cookieBanner?.allowsAnalytics === true,
})
Adfinia.identify('cust_42', {
email: '[email protected]',
first_name: 'Ahmed',
language: 'en-AE',
country: 'AE',
city: 'Dubai',
source: 'sdk_web',
utm_source: 'google',
utm_campaign: 'ramadan_2026',
})
Adfinia.track('Order Completed', { order_id: 'o_123', total: 49.99 })
Adfinia.page('Pricing')identify() traits
The full IdentifyTraits interface (added in 1.1.0, mirrors the api
IdentifyTraits contract) accepts the following optional fields:
| Field | Type | Notes |
|-------|------|-------|
| email | string | Resolves to an email alias on the identity graph. |
| phone | string | E.164. Resolves to a phone alias. |
| device_id | string | Resolves to a device_id alias. |
| external_id | string | Tenant-side CRM / external system ID. |
| first_name / last_name | string | |
| language | string | BCP 47 — e.g. en-AE, ar-AE, hi-IN. |
| timezone | string | IANA — e.g. Asia/Dubai. |
| country | string | ISO 3166-1 alpha-2 — e.g. AE. |
| city | string | Free text. |
| whatsapp | string | E.164. Separate channel from phone. |
| gender | 'male' \| 'female' \| 'non_binary' \| 'prefer_not_to_say' | |
| date_of_birth | string | ISO 8601 date — YYYY-MM-DD. |
| source | IdentifySource | Closed enum — see src/types.ts. Web defaults to sdk_web when callers omit it server-side. |
| utm_source / utm_medium / utm_campaign / utm_term / utm_content | string | First-touch lands on contact creation; last-touch on every Identify carrying any UTM key. |
| extra | Record<string, string> | Open-ended bag for tenant-specific custom fields. |
Unset fields are omitted from the JSON body (never null / empty
string); the server treats them as "leave existing value alone".
Note on imports. The named-import form above is the recommended shape and matches the SDK integration guide. The default-import form
import Adfinia from '@adfinia/sdk-web'is the legacy alias kept for backwards-compat with consumers who started against1.0.0(which only shipped the default export). Both forms resolve to the same singleton.For advanced cases that need their own client instance (multi-tenant SSR, isolated test contexts), import the underlying class:
import { AdfiniaClient } from '@adfinia/sdk-web'.
API reference
| Method | Notes |
|--------|-------|
| Adfinia.init(config) | One-shot. Subsequent calls are ignored. |
| Adfinia.identify(customerId, traits?) | Customer-id form. |
| Adfinia.identify({ customerId, externalId?, anonymousId?, traits?, context? }) | Object form. externalId is a tenant-owned stable id (e.g. wallet hash). context is merged on top of auto-context. |
| Adfinia.track(event, properties?, { context?, externalId? }?) | Event name + properties + per-call context / external id. |
| Adfinia.page(name?, properties?, { context?, externalId? }?) | Page view. Auto-captures URL/title/referrer if no args. |
| Adfinia.screen(name?, properties?, { context?, externalId? }?) | Parity hook for mobile SDKs; identical to page() on web. |
| Adfinia.setConsent(channels, status) | Write-only consent. channels is a single channel string or an array; status is 'opted_in' or 'opted_out'. Channels are open strings (not an enum) - the backend owns the valid-channel registry. Emits one consent_updated event with channels always an array. No read method by design. |
| Adfinia.optIn(channels) | Shorthand for setConsent(channels, 'opted_in'). |
| Adfinia.optOut(channels) | Shorthand for setConsent(channels, 'opted_out'). Safe from a GTM tag (window.Adfinia.optOut(['email','sms'])); never throws. |
| Adfinia.alias(newId, previousId?) | Deprecated (no-op). There is no server-side alias handler; this method does nothing and will be removed in the next major version. Anonymous sessions are promoted to a known customer automatically by identify() (the identify event carries the live anonymous_id). |
| Adfinia.reset() | Logout — mints a new anonymous_id and clears external_id. |
| Adfinia.flush() | Promise — drains the queue and resolves when the in-flight batch settles. Use before a critical navigation. |
| Adfinia.registerWebPush({ vapidPublicKey, ... }) | Promise — registers a web-push subscription (service worker + permission + PushManager). See Web push. |
| Adfinia.notifications.list({ status?, cursor?, limit? }) | Promise — a page of in-app notifications: { data, nextCursor, hasMore }. See In-app inbox. |
| Adfinia.notifications.markRead(id) | Promise<boolean> — mark one notification read. |
| Adfinia.notifications.markAllRead() | Promise<boolean> — mark all read for the current contact. |
| Adfinia.notifications.subscribe(handler, options?) | Live SSE subscription. Returns { unsubscribe() }. Replays unread on connect. |
| Adfinia.notifications.trackOpened(n) / trackClicked(n) | Emit notification_opened / notification_clicked track events. |
AdfiniaConfig
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| writeKey | string | — | Required. Tenant write-only public key (pk_live_… / pk_test_…). |
| host | string | https://api.adfinia.com | Override for self-hosted ingress. |
| debug | boolean | false | Log SDK internals to console.debug. |
| consent | () => boolean | undefined | Consent gate. Returning false drops events silently. |
| autoContext | boolean | false | Opt in to automatic browser-context enrichment (page_path, page_url, referrer, user_agent, locale, timezone, viewport, screen_resolution) plus first-touch acquisition (UTM tags, ad click IDs, landing page). Off by default — privacy-first. |
| autoPage | boolean | true | Auto-fire page() on load + SPA route change (pushState/replaceState/popstate). Set false to wire page views by hand. |
| flushAt | number | 50 | Flush immediately once N events are buffered. |
| flushIntervalMs | number | 5000 | Otherwise, flush every N ms. |
| maxQueueSize | number | 1000 | Oldest events drop when this fills up. |
Browser context: autoContext
By default the SDK ships only library identifiers + the message envelope. To enrich every event with browser context, opt in:
Adfinia.init({
writeKey: 'pk_live_…',
autoContext: true, // collect page_path, page_url, referrer, user_agent, locale, timezone, viewport, screen_resolution
})Per-call context wins on key collision:
Adfinia.track('Order Completed', { total: 49.99 }, {
context: { experiment_variant: 'checkout_v3' },
})The auto-context keys map 1:1 to the server's context map[string]string contract. Caller-supplied context is layered on last, so anything you pass in { context } always overrides what the SDK auto-collects.
External identity (external_id)
Pass a tenant-owned stable identifier — a wallet hash, a CRM key, anything you control — as externalId. It's persisted client-side and emitted on the wire as external_id. The server resolves identity in the order customer_id > external_id > anonymous_id.
// At wallet connect / login:
Adfinia.identify({ externalId: walletAddress, traits: { country: 'AE' } })
// Or per call, before you have a full identify:
Adfinia.track('wallet_connected', { is_new_wallet: true }, { externalId: walletAddress })Once set (via identify or a per-call option), external_id rides every subsequent event automatically until reset().
Acquisition (first-touch attribution)
When autoContext: true, the SDK reads acquisition signals from the first session URL and persists them — later events keep the original attribution even after the query string is gone:
- UTM tags:
utm_source / utm_medium / utm_campaign / utm_term / utm_content→campaign.utm_* - Ad click IDs:
gclid / fbclid / ttclid / sc / msclkid→campaign.* - Landing page →
page.landing
No code beyond autoContext: true — land the user with ?utm_source=google&gclid=… and the attribution sticks for the whole session.
Auto page tracking (SPAs)
By default (autoPage: true) the SDK fires page() on the initial load and on every SPA route change — it hooks history.pushState, history.replaceState, and popstate, de-duped by path+search so you never get a double-fire. Hash-only changes don't count as a new page.
Adfinia.init({ writeKey: 'pk_live_…' }) // page views are automatic
// Wiring them by hand instead? Turn it off:
Adfinia.init({ writeKey: 'pk_live_…', autoPage: false })
router.afterEach(() => Adfinia.page())Web push
Adfinia.registerWebPush(config) runs the full browser opt-in: register a service worker, request Notification permission, PushManager.subscribe with your tenant VAPID public key, and POST the subscription to Adfinia. It also emits notification_permission_prompted/granted/denied track events.
const result = await Adfinia.registerWebPush({
vapidPublicKey: 'BEl62iUY…', // tenant VAPID public key (base64url P-256)
serviceWorkerUrl: '/adfinia-sw.js', // default; host this at your web root
})
if (result.ok) {
console.log('subscribed', result.endpoint)
} else {
console.log('not subscribed —', result.reason) // 'permission_denied' | 'unsupported' | …
}Service worker hosting. The service worker must be served from your web root (a SW only controls pages at or below its own URL). Copy it from the package after install:
cp node_modules/@adfinia/sdk-web/dist/adfinia-sw.js ./public/adfinia-sw.jsThe worker shows the notification and emits push_received / push_clicked back to Adfinia. If you serve it from a sub-path, set the Service-Worker-Allowed response header and pass scope to registerWebPush.
VAPID key. Get your tenant VAPID public key from the Adfinia console (Settings → Channels → Push). You can pass it as
vapidPublicKey, or setfetchVapidFromConfig: trueto have the SDK pull it from/sdk/config(requires server support for that field).iOS. Web push works on Android + desktop Chrome/Firefox/Edge today. iOS Safari 16.4+ supports web push only for home-screen-added PWAs — lead with email/SMS there.
In-app inbox
Adfinia.notifications is a framework-agnostic data client for the in-app notification inbox. It ships no UI — you render the bell, list, and toasts; the SDK gives you typed notifications, a live stream, and the open/click events.
// 1. Page through the inbox (cursor-paginated).
const page = await Adfinia.notifications.list({ status: 'unread', limit: 20 })
renderList(page.data)
if (page.hasMore) {
const next = await Adfinia.notifications.list({ cursor: page.nextCursor! })
}
// 2. Subscribe to live updates. On connect the handler is called once per
// currently-unread notification (replay), then once per new one (SSE).
const sub = Adfinia.notifications.subscribe((n) => {
showToast(n) // { id, title, body, severity, deep_link?, ... }
})
// 3. Mark read + record engagement when the user interacts.
async function onOpen(n) {
Adfinia.notifications.trackOpened(n) // -> notification_opened
await Adfinia.notifications.markRead(n.id)
}
function onClick(n) {
Adfinia.notifications.trackClicked(n) // -> notification_clicked
if (n.deep_link) router.push(n.deep_link)
}
await Adfinia.notifications.markAllRead()
// 4. Tear down (e.g. on logout / unmount).
sub.unsubscribe()The InboxNotification shape is { id, contact_id, title, body, severity('info'|'success'|'warning'|'error'), dismissable, deep_link?, data?, read, created_at, read_at?, expires_at? }.
Identity. The inbox is keyed on the current contact. The SDK resolves contact_id from the active identity in the server's order — customer_id > external_id > anonymous_id. Call identify(...) first, or pass { contactId } explicitly to any method.
Live stream vs push. subscribe() opens an EventSource that runs in the page — it's the foreground channel for a live inbox while the tab is open. It's complementary to web push: the service worker delivers background system notifications when the tab is closed. Use both. Where EventSource is unavailable, subscribe() still replays unread on connect; only the live push part is skipped.
Auth.
EventSourcecan't set anAuthorizationheader, so the write key + SDK version ride the stream URL as query params — the gateway accepts both forms (same as thesendBeaconunload path).
Last-mile delivery on unload
On tab close / hide, the SDK drains the queue via navigator.sendBeacon (with a fetch({ keepalive: true }) fallback). This survives the unloading page so the final batch reliably reaches the server. The auth + SDK-version travel as query params on the beacon URL because sendBeacon can't set custom headers — the gateway accepts both forms.
For navigations you control, prefer the explicit await Adfinia.flush():
Adfinia.track('CTA Clicked', { cta: 'upgrade' })
await Adfinia.flush()
router.push('/upgrade')Consent integration
The SDK ships with a consent gate. Pass a consent callback that returns the user's current opt-in state — it runs on every track / identify / page / screen / alias call. If it returns false, the SDK drops the call silently. The callback can flip from false to true mid-session without re-initialisation.
For UAE PDPL, India DPDP, EU GDPR: pair the gate with Adfinia.reset() when the user revokes — that clears any client-side identifier you'd otherwise still hold.
Full consent-architecture write-up: docs.adfinia.com/user-guide/consent.
Bundle output
| Format | File | Raw size | Use when |
|--------|------|----------|----------|
| ESM | dist/index.js | ~36 KB | Bundler-driven apps (Next.js, Vite, Webpack). Tree-shakes to ~10 KB gzipped. |
| CJS | dist/index.cjs | ~36 KB | Node.js + legacy bundlers. |
| IIFE | dist/adfinia.iife.js | ~19.5 KB | Direct <script> include, CDN drop-in, Google Tag Manager. |
| SW | dist/adfinia-sw.js | ~3.5 KB | Web-push service worker. Host at your web root (not bundled). |
| Types | dist/index.d.ts | ~16 KB | TypeScript autocomplete + type-checking. |
Sizes are pre-gzip; the IIFE bundle gzips to roughly 6 KB on the wire. The IIFE bundle exposes window.Adfinia and self-bootstraps — no import needed.
Looking for the full integration guide?
docs.adfinia.com/user-guide/sdk-integration#web — covers CSP, Next.js App Router patterns, SPA routing, consent banners, e-commerce conversion tracking, and self-hosted ingest configuration.
Browser support
| Browser | Version | |---------|---------| | Chrome / Edge | last 2 majors | | Firefox | last 2 majors | | Safari | iOS 14+, macOS 14+ | | Node.js | 18+ (for server-side use) |
The SDK uses fetch, crypto.getRandomValues, and localStorage. All are polyfill-free on the supported set. In SSR contexts it gracefully degrades — localStorage falls back to in-memory.
Issues + contributing
- Bugs and feature requests: github.com/Adfinia/sdk-web/issues
- Contributing guide: CONTRIBUTING.md
- Email: [email protected]
License
MIT — see LICENSE.
