Web SDK
The Active Reach Web SDK tracks user behavior, identifies visitors, manages push notification subscriptions, renders in-app messages, and sends e-commerce events — all from the browser.
Current version: 1.14.1 — published through the Phase 5.6 release-readiness gate (the same drift + smoke contract the mobile SDKs go through).
Brand canon. The customer-facing package is
@active-reach/web-sdkbut the class you import is stillAegis. This is intentional and matches the rest of the family: “Aegis” is the internal source-tree name; “Active Reach” is the customer-facing identifier on registries (npm, CocoaPods, Maven, pub.dev) and customer-visible runtime strings (console prefixes, User-Agent headers, OS log subsystems).
Installation
Three ways to install, depending on your stack.
CDN snippet (fastest)
Drop this before </body> on every page. For SPAs, add once in your entry file.
<!-- latest -->
<script src="https://cdn.activereach.ai/sdk/aegis.min.js"></script>
<!-- or pin to a version (recommended for production) -->
<!-- <script src="https://cdn.activereach.ai/sdk/v1.14.1/aegis.min.js"></script> -->
<script>
Aegis.init('YOUR_WRITE_KEY', {
workspace_id: 'YOUR_WORKSPACE_ID',
api_host: 'https://api.active-reach.ai'
});
</script>The CDN is fronted by Cloudflare. Latest-tagged assets have a 24-hour cache lifetime; pinned v{version} assets are immutable for one year. The publish workflow uploads three artefacts each release: aegis.min.js, snippet.min.js, aegis-sw.js (service worker for web push).
npm (recommended for React, Next.js, Vue)
npm install @active-reach/web-sdkimport { Aegis } from '@active-reach/web-sdk';
await Aegis.init('YOUR_WRITE_KEY', {
workspace_id: 'YOUR_WORKSPACE_ID',
});React hook
npm install @active-reach/web-sdkimport { AegisProvider, useAegis } from '@active-reach/web-sdk/react';
function App() {
return (
<AegisProvider writeKey="YOUR_WRITE_KEY" config={{ workspace_id: 'YOUR_WORKSPACE_ID' }}>
<YourApp />
</AegisProvider>
);
}
function TrackButton() {
const aegis = useAegis();
return (
<button onClick={() => aegis.track('button_clicked', { label: 'signup' })}>
Sign up
</button>
);
}Core tracking methods
| Method | Description |
|---|---|
track(event, properties?) | Track a custom event (e.g., order_completed, page_scrolled) |
identify(userId, traits?) | Identify a user and set traits (name, email, plan, etc.) |
page(name?, properties?) | Track a page view — called automatically on load + SPA navigation if auto_page_view is enabled |
group(groupId, traits?) | Associate the user with a company or account |
alias(newUserId) | Merge an anonymous session with an identified user |
reset() | Clear identity and start a fresh anonymous session |
flush() | Manually flush queued events — call before window.unload |
track
Aegis.track('order_completed', {
order_id: 'ORD-123',
amount: 4999,
currency: 'INR',
products: ['SKU-A', 'SKU-B'],
});identify
Aegis.identify('user_456', {
name: 'Priya Sharma',
email: '[email protected]',
plan: 'pro',
created_at: '2026-01-15',
});Call identify after login or signup. Traits merge — you don’t need to send every trait every time.
page
Aegis.page('Product Detail', {
product_id: 'SKU-A',
category: 'Electronics',
});If auto_page_view is enabled (default), page() fires automatically on every navigation. Call it manually only if you want to add custom properties.
group
Aegis.group('company_789', {
name: 'Acme Corp',
industry: 'Retail',
employees: 50,
});alias
// After a user signs up, merge their anonymous browsing history
Aegis.alias('user_456');reset
// On logout — clears user ID, traits, and session
Aegis.reset();E-commerce tracking
The SDK includes purpose-built e-commerce methods that emit standardized events:
import { EcommerceTracker } from '@active-reach/web-sdk';
EcommerceTracker.trackProductView({ id: 'SKU-A', name: 'Widget', price: 999 });
EcommerceTracker.trackAddToCart({ id: 'SKU-A', name: 'Widget', price: 999, quantity: 1 });
EcommerceTracker.trackCheckoutStarted({ cart_id: 'CART-1', total: 999 });
EcommerceTracker.trackOrderCompleted({ order_id: 'ORD-1', total: 999, currency: 'INR' });
EcommerceTracker.trackSearch({ query: 'blue widget' });Push notifications
import { AegisWebPush } from '@active-reach/web-sdk/push';
await AegisWebPush.init({ vapidPublicKey: 'YOUR_VAPID_KEY' });
const subscription = await AegisWebPush.subscribe();
AegisWebPush.onPushReceived((notification) => {
console.log('Push received:', notification);
});
AegisWebPush.onPushClicked((notification) => {
console.log('Push clicked:', notification);
});Requires a service worker — see Push setup guide.
In-app messaging
In-app campaigns are fetched and rendered automatically once the SDK is initialized with inapp: { enabled: true }. No manual code needed for basic display.
For programmatic control, use the AegisMessageRuntime facade — it owns the in-app and widget managers as readonly props (the manager classes are intentionally not barrel-exported; consumers reach them through the runtime):
import { AegisMessageRuntime } from '@active-reach/web-sdk';
const runtime = new AegisMessageRuntime({ /* config */ });
await runtime.initialize();
runtime.inApp.show('campaign_123'); // Show a specific campaign
runtime.inApp.dismiss(); // Dismiss the current message
runtime.notifyConversion('checkout'); // Silence armed campaigns + journey
// -queued cart-recovery touches (G1 seam)New methods added on the underlying managers are reachable as runtime.<area>.<method> without any additional wiring.
Workspace-aware runtime (1.14.0)
On multi-outlet storefronts the SDK needs to know which outlet the customer is on so events stamp the right workspace_id. 1.14.0 introduces a resolution cascade so a single SDK install can disambiguate without per-outlet boilerplate:
config.workspace_id → ?ws=<id> → URL path slug → setWorkspace() → sessionStorage → undefinedThe URL-path slug step matches the first segment of the path against the org’s workspaceCodes allowlist (delivered by the bootstrap call). On a customer-facing URL like slug.actii.me/south/rewards the SDK auto-detects south and stamps every event with that workspace’s UUID without any operator config.
Three new runtime methods support host-driven control:
aegis.setWorkspace('south'); // pin runtime workspace + persist in sessionStorage
aegis.clearWorkspace(); // clear the runtime + sessionStorage step
aegis.ingestWorkspaceCodes(['south', 'ghatkopar', 'bandra']);
// seed allowlist when bootstrap is bypassedThe bootstrap response now carries workspaceCodes: string[] for the org so the SDK can validate path-segment matches client-side and skip non-workspace prefixes like /products or /collections.
In-app and widget plumbing. AegisInAppManager and AegisWidgetManager expose a getWorkspaceId config callback. The host SDK wires () => aegis.getEffectiveWorkspaceId() so impressions, clicks, dismisses, and widget interactions (spin/scratch plays, feedback submissions) carry the same workspace context the main analytics stream uses. Without it, in-app engagement events arrive at event-ingress unscoped and fall back to the org’s primary workspace.
Gateway-side normalization. The platform resolves workspace slugs to UUIDs server-side and the canonical event always stamps the UUID. The SDK can therefore send either a slug or a UUID — both forms work, and resolutions are cached so the lookup adds negligible latency.
User namespace and per-channel opt-in (1.13.0)
The aegis.user namespace is the WebEngage-/CleverTap-parity entry point for PII writes and per-channel marketing consent. Every method delegates to aegis.identify() under the hood with SDK-side validation:
aegis.user.login(userId, traits?);
aegis.user.logout();
aegis.user.setEmail(email); // RFC 5322 simple validation
aegis.user.setPhone(e164); // E.164 validation
aegis.user.setHashedEmail(sha256hex);
aegis.user.setOptIn(channel, granted); // channel: 'email' | 'sms' | 'push'
// | 'webpush' | 'whatsapp' | 'rcs' | 'inapp'setX calls before login() accumulate in an in-memory buffer and flush as one consolidated identify event when login() fires — avoids the anonymous-id-as-userId footgun.
Consent UI mirror requirement. Any cookie-consent or marketing-opt-in UI in your storefront must mirror its state to aegis.user.setOptIn(channel, granted) for every supported channel. Writing only to localStorage keeps the consent state invisible to segmenters, journey channel pickers, and the DPDP audit trail.
Client-side governance — TraitGovernor / NameGovernor
The aegis.user namespace runs every trait write through a TraitGovernor before the payload reaches the network. It mirrors the same ingestion guards the cell-plane backend enforces at the trait-write chokepoint — so developers see problems in the browser console the first time they happen, not in the Validation tab after a production batch.
What the governor does on every aegis.user.setX(...) / identify() / group() call:
- Normalizes keys — camelCase / PascalCase / dashed / space-separated keys are rewritten to
snake_case.firstNamebecomesfirst_nameautomatically. - Drops reserved-prefix keys —
system.,user.,loyalty.,cart.,bill.,$,_, … belong to the platform. Writes against them are dropped with areserved_prefixverdict. - Truncates long strings; rejects huge ones — soft cap truncates, hard cap rejects (DoS protection).
- Parses date-keyed fields — values on
*_at,*_on,created,expires,dob, … keys are auto-parsed from ISO-8601 / epoch strings to epoch-ms.
Drops surface as console.warn lines rate-limited to first 3 per (workspace_id, verdict) per session. The backend remains the authoritative chokepoint; the SDK path is advisory — designed to catch the issue early, not enforce harder than the backend.
For the full verdict matrix and the cross-SDK drift contract, see Event governance → TraitGovernor.
NameGovernor is the event-name counterpart — covered in § Event governance below.
Plugin registry
Aegis ships a typed plugin registry that any third-party integration can register against. The registry replaces the earlier ad-hoc hook subscription with a formal Plugin interface — plugins receive lifecycle hooks (init, beforeEventCapture, afterEventCapture, beforeBatchSend, afterBatchSend, onError, destroy) and can suppress (return null from beforeEventCapture) or transform events on their way to the gateway.
import { Aegis, type Plugin } from '@active-reach/web-sdk';
const myPlugin: Plugin = {
name: 'my-analytics-bridge',
version: '1.0.0',
beforeEventCapture(event) {
// Mutate or filter — return null to suppress.
return event;
},
};
Aegis.use(myPlugin);Use the registry instead of monkey-patching Aegis.track. The same shape is consumed by every first-party integration, including the Meta Pixel plugin below.
Meta Pixel plugin
The Web SDK includes a first-party Meta Pixel bridge for ad-attribution parity. The SDK manages the _fbp and _fbc browser cookies (the same first-party match keys Meta uses for EMQ — Event Match Quality) and exposes aegis.lastEventId(eventName) so a tenant’s own Meta Pixel can pass the same event_id for client-side dedup with the server-side Conversions API.
| Capability | Where it lives |
|---|---|
_fbp cookie write + read | libs/web-sdk/src/utils/meta-cookies.ts — 90-day TTL, fb.1.<creation_ms>.<random> format |
_fbc cookie write + read | Captured from ?fbclid=<id> on landing, persisted same TTL |
aegis.lastEventId(name) | Returns the messageId of the most recent track(<name>) — null if the event hasn’t fired since init |
| Consent gating | Honors marketing consent — when denied, _fbp / _fbc are neither generated nor stamped onto events |
Mobile parity ships as MetaPixelCompanion on iOS / Android / React Native / Flutter (Phase 4.5). The mobile companion is in-memory only; cookie equivalents are persisted in storage and stamped onto outgoing event metadata so the cell-plane Meta CAPI service has matching keys. The cross-SDK wire contract is pinned by libs/mobile-sdk/tests/drift/meta-pixel-companion-contract.json.
See E-commerce integrations → Meta Pixel for the operator-facing setup.
SPA navigation — aegis.screen()
Use aegis.screen('storefront.cart') for SPA-logical surface tracking (cart drawer, checkout step, in-app modal step) distinct from URL-bound page(). The gateway maps screen events to canonical event engagement.screen_view.
aegis.screen('storefront.cart', { cart_id: 'CART-1' });Don’t ALSO call page() for SPA transitions — the canonical mapping is one-shot per surface change.
E-commerce — back-in-stock signup
productWaitlisted is the SDK-side counterpart to the cashier-portal <BackInStockButton> on PDP. The cell-side stock_event_handler_worker fans out to waitlisted contacts when a restock fires (older signups expire per the platform retention window):
aegis.ecommerce.productWaitlisted({
productId: 'SKU-A',
name: 'Widget',
email: '[email protected]', // or rely on identified user
});There’s no default catalog.back_in_stock journey template — operators wire their own using the standard journey builder.
Cart abandonment is server-side
Don’t roll your own JS timer for cart abandonment. The platform scans abandoned carts via Celery beat over ClickHouse (storefront_cart_abandonment_scanner) — same pattern as CleverTap / WebEngage / Klaviyo. JS timers miss tab-close, drain battery, and leak timing logic into the browser. The same pattern applies to search-, browse-, and checkout-abandonment.
Event governance
The Web SDK ships with client-side event-name cap enforcement (introduced in SDK 1.4.0 ). When you bootstrap the SDK, the server returns a compact Bloom filter of your already-registered event names plus a counter of how many new names you can still add. The SDK uses this to drop novel event names locally once you hit your plan cap, so a bug like track('view_' + id) doesn’t amplify into a thousand gateway round-trips.
Integration (two lines)
import { bootstrap } from '@active-reach/web-sdk';
const result = await bootstrap(apiHost, { writeKey, currentOrigin: window.location.origin });
await aegis.init(writeKey, { /* ... */ });
aegis.ingestGovernanceHint(result.eventGovernance); // ← this lineSkip the ingestGovernanceHint call and the SDK silently fails open — the gateway remains the authoritative cap, you just don’t get the local-drop optimization.
What the SDK does
- Known name → sends immediately (Bloom hit)
- Novel name, within headroom → sends; increments a per-instance counter; remembers the name so calling it again doesn’t double-charge
- Novel name, over cap, grace inactive → drops locally; logs one
console.warnper distinct dropped name; queues a coalescedaegis.client.name_governor_droppedmeta event for the next flush so ops dashboards see the drop pattern - Grace window active (see Event governance concept → Grace period) → SDK fails open — gateway is still accepting novel names, so the SDK shouldn’t drop
The hint shape
export interface EventGovernanceHint {
bloom_algo: 'mmh3_x86_32_km';
seed_a: 0;
seed_b: 1;
k: number; // hash count (typically 7)
m: number; // bitarray size (power of 2, typically 1024)
bloom_b64: string; // base64 of the bitarray, LSB-first within each byte
remaining_new_names: number | null; // null = Enterprise / unlimited
grace_active?: boolean;
ttl_seconds: number; // SDK re-bootstraps if older than this
}The bloom_algo is versioned — if the server ships a new algo and your SDK doesn’t recognize it, the SDK silently disables governance and falls back to gateway enforcement. Safe by default.
Advanced — inspecting governor state
For debugging:
import { NameGovernor } from '@active-reach/web-sdk';
// Rarely needed — the governor is instantiated internally by Aegis.
// Exposed for advanced use (custom bloom construction, test fixtures).Full API reference for governance → Reference page
Configuration reference
Key config options passed to Aegis.init():
| Option | Type | Default | Description |
|---|---|---|---|
workspace_id | string | Required | Your workspace ID |
api_host | string | Auto-detected | API endpoint (region-based) |
auto_page_view | boolean | true | Automatically track page views on navigation |
batch_size | number | 50 | Events per batch before sending |
batch_interval | number | 5000 | Milliseconds between batch sends |
enable_offline_mode | boolean | true | Cache events when offline |
session_timeout | number | 1800000 | Session timeout in ms (30 min) |
cookie_domain | string | Current domain | Cookie domain for cross-domain tracking |
wait_for_consent | boolean | false | Defer event sending until consent granted |
enable_consent_mode | boolean | false | Enable GDPR/CCPA consent mode |
respect_dnt | boolean | true | Honor browser Do Not Track signal |
rate_limit_per_second | number | 100 | Client-side event rate limit |
debug | boolean | false | Enable console debug logging |
Full config reference with all 40+ options → SDK Config Reference (coming soon)
What’s next
- Web SDK Reference — full method signatures + config options
- Set up push notifications — FCM, APNs, or managed VAPID
- Configure in-app messaging — modals, banners, surveys
- Consent and privacy — GDPR, CCPA, OneTrust, Cookiebot integration,
setOptInmirror - Example apps — HTML/CDN, React, and Next.js samples
Geofencing is a mobile-only capability (iOS CoreLocation / Android Play Services) — there is no Web SDK equivalent. Use the React Native or Flutter geofencing modules in a mobile app.