@layers/client
v3.3.3
Published
Layers Analytics Web SDK — thin wrapper over Rust core via WASM
Readme
Layers Web SDK
@layers/client is the Layers analytics SDK for web browsers. It provides event tracking, screen tracking, user identification, automatic attribution capture (UTM parameters, click IDs, referrer), consent management, and lifecycle-aware event flushing with sendBeacon support.
Requirements
- Modern browser with ES module support
- React 18+ (for React hooks, optional)
Installation
npm install @layers/client
# or
yarn add @layers/client
# or
pnpm add @layers/clientVite
Under vite dev, Vite's dependency pre-bundler can intercept a binary asset the
SDK loads at runtime and hand back the dev server's HTML fallback page instead.
The SDK detects the bad response and continues on its portable code path, so
tracking, batching and delivery all keep working -- with debug: true you will
see [Layers] WASM initialization failed in the console.
To keep the dev server matching your production build, exclude the SDK's runtime dependency from pre-bundling:
// vite.config.js
export default {
optimizeDeps: {
exclude: ['@layers/core-wasm']
}
};vite build output is unaffected -- this applies to the dev server only. A
production build loads the accelerated core in the ordinary single-entry app
shape, including entry modules that await layers.init() at the top level; CI
builds exactly that app and asserts the core activates.
Whenever the accelerated core cannot load, in any build, the SDK logs one
[Layers] WASM core unavailable warning naming the underlying reason and
continues on its portable code path, with tracking, batching and delivery
intact.
Script tag (CDN)
No build step needed. Drop in the tag and call layers.init():
<script src="https://cdn.layers.com/sdk/v1/layers.min.js"></script>
<script>
layers.init({ appId: 'your-app-id', environment: 'production' });
layers.track('page_view', { page: '/home' });
</script>Calls made before init() resolves are buffered and replayed, so you can track
from the first line.
The script loads one supporting asset — layers_core_bg.wasm — from its own
directory on the CDN. Nothing to configure: the tag knows where it was loaded
from and asks for the file next to it, on both /sdk/v1/ and the pinned
/sdk/v1/<version>/ path.
If you mirror the SDK onto your own host, copy both files and keep them in the same directory. To put the asset somewhere else, name it explicitly:
<script src="https://your-cdn.example.com/layers.min.js"></script>
<script>
layers.init({
appId: 'your-app-id',
environment: 'production',
wasmUrl: 'https://your-cdn.example.com/assets/layers_core_bg.wasm'
});
</script>The asset is served cross-origin, so your Content-Security-Policy needs
connect-src to allow the host it comes from. If it cannot be fetched the SDK
logs one warning and keeps tracking on its portable code path.
Quick Start
import { LayersClient } from '@layers/client';
const layers = new LayersClient({
appId: 'your-app-id',
environment: 'production'
});
await layers.init();
// Track events
layers.track('button_click', { button_name: 'signup' });
// Page views are captured automatically (see autocapture.pageViews).
// Name a view explicitly when the URL alone does not describe it:
layers.screen('Home');
// Identify users
layers.setAppUserId('user_123');Configuration
LayersConfig
interface LayersConfig {
appId: string;
environment: 'development' | 'staging' | 'production';
appUserId?: string;
enableDebug?: boolean;
baseUrl?: string;
flushIntervalMs?: number;
flushThreshold?: number;
maxQueueSize?: number;
wasmUrl?: string;
}| Option | Type | Default | Description |
| ----------------- | ------------- | ------------------------- | ------------------------------------------------ |
| appId | string | required | Your Layers application identifier. |
| environment | Environment | required | 'development', 'staging', or 'production'. |
| appUserId | string | undefined | Optional user ID to set at construction time. |
| enableDebug | boolean | false | Enable verbose console logging. |
| baseUrl | string | "https://in.layers.com" | Custom ingest API endpoint. |
| flushIntervalMs | number | 30000 | Automatic flush interval in milliseconds. |
| flushThreshold | number | 10 | Queue size that triggers an automatic flush. |
| maxQueueSize | number | 1000 | Maximum events in the queue before dropping. |
| wasmUrl | string | resolved automatically | Explicit URL for the SDK's supporting asset. |
const layers = new LayersClient({
appId: 'your-app-id',
environment: 'development',
enableDebug: true,
flushIntervalMs: 15000,
flushThreshold: 5,
maxQueueSize: 5000
});Core API
Constructor & Initialization
const layers = new LayersClient(config: LayersConfig);
await layers.init();The constructor creates the SDK instance with localStorage-backed persistence. Calling init() detects device info, attaches lifecycle listeners (online/offline, visibility change, beforeunload), captures attribution signals, and fetches remote config.
You can call track() and screen() before init() completes -- events are queued.
Event Tracking
track(eventName: string, properties?: EventProperties): voidEvents are batched and flushed automatically. Attribution properties (UTM, click IDs, referrer) from the current session are automatically merged into every event.
layers.track('purchase_completed', {
product_id: 'sku_123',
price: 9.99,
currency: 'USD'
});Screen/Page Tracking
screen(screenName: string, properties?: EventProperties): voidAttribution properties are automatically merged.
layers.screen('ProductDetail', { product_id: 'sku_123' });User Identity
// Set or clear the app user ID
setAppUserId(appUserId: string | undefined): void
// Get the current user ID
getAppUserId(): string | undefined
// Deprecated aliases
setUserId(userId: string): void
getUserId(): string | undefined// After login
layers.setAppUserId('user_123');
// On logout
layers.setAppUserId(undefined);User Properties
setUserProperties(properties: UserProperties): voidlayers.setUserProperties({
email: '[email protected]',
plan: 'premium',
signup_date: '2024-01-15'
});Consent Management
setConsent(consent: ConsentState): void
getConsentState(): ConsentStateinterface ConsentState {
analytics?: boolean;
advertising?: boolean;
}// User accepts analytics only
layers.setConsent({ analytics: true, advertising: false });
// Read current state
const consent = layers.getConsentState();Session Management
// Get the current session ID
getSessionId(): string
// End the current session and start a new one
startNewSession(): voidDevice Info
setDeviceInfo(deviceInfo: DeviceContext): voidOverride auto-detected device context fields.
Flush & Shutdown
// Flush all queued events to the server
async flush(): Promise<void>
// Shut down immediately (no flush, events persisted to localStorage)
shutdown(): void
// Shut down with a final flush (with timeout)
async shutdownAsync(timeoutMs?: number): Promise<void> // default: 3000msError Handling
on(event: 'error', listener: (error: Error) => void): this
off(event: 'error', listener: (error: Error) => void): thislayers.on('error', (error) => {
console.error('Layers error:', error.message);
});Attribution
The SDK automatically captures and persists attribution signals from the current page URL and referrer.
Captured Signals
Click IDs (highest priority -- overwrites existing attribution):
fbclid(Meta/Facebook)gclid,gbraid,wbraid(Google)ttclid(TikTok)msclkid(Microsoft/Bing)rclid(Reddit)
UTM Parameters:
utm_source,utm_medium,utm_campaign,utm_content,utm_term
Referrer:
document.referrer
How It Works
- On
init(), the SDK reads the current URL's query parameters anddocument.referrer. - Attribution data is persisted in
localStoragewith a 30-day TTL. - Click IDs take priority -- a new click ID overwrites the entire stored record.
- UTM parameters overwrite on fresh campaign visits, but preserve existing click IDs.
- Attribution properties are automatically prefixed with
$attribution_and merged into everytrack()andscreen()call.
Manually Reading Attribution
import { getAttribution, getAttributionProperties } from '@layers/client';
// Get the full stored attribution data (or null if expired/missing)
const attribution = getAttribution();
// Get a flat property bag for merging into events
const props = getAttributionProperties();
// { '$attribution_utm_source': 'google', '$attribution_click_id_param': 'gclid', ... }AttributionData Type
interface AttributionData {
click_id_param?: string;
click_id_value?: string;
utm_source?: string;
utm_medium?: string;
utm_campaign?: string;
utm_content?: string;
utm_term?: string;
referrer_url?: string;
captured_at: number;
}React Integration
The SDK includes React hooks for idiomatic usage. Import from @layers/client/react.
LayersProvider
import { LayersProvider } from '@layers/client/react';
function App() {
return (
<LayersProvider
config={{
appId: 'your-app-id',
environment: 'production'
}}
>
<MyApp />
</LayersProvider>
);
}Initializes the client on mount and shuts it down on unmount.
useLayers
function useLayers(): LayersClient;Returns the LayersClient instance. Throws if used outside a <LayersProvider>.
function MyComponent() {
const layers = useLayers();
layers.track('component_viewed');
}useTrack
function useTrack(): (eventName: string, properties?: EventProperties) => void;Returns a stable, memoized track function.
function SignupButton() {
const track = useTrack();
return <button onClick={() => track('signup_click', { source: 'hero' })}>Sign Up</button>;
}useScreen
function useScreen(): (screenName: string, properties?: EventProperties) => void;Returns a stable, memoized screen tracking function.
function Dashboard() {
const screen = useScreen();
useEffect(() => {
screen('Dashboard');
}, [screen]);
return <div>Dashboard</div>;
}useIdentify
function useIdentify(): (userId: string | undefined) => void;Returns a stable, memoized identify function. Pass undefined to clear.
function LoginForm() {
const identify = useIdentify();
const onLogin = (userId: string) => identify(userId);
const onLogout = () => identify(undefined);
}useConsent
function useConsent(): {
setConsent: (consent: ConsentState) => void;
getConsentState: () => ConsentState;
};function ConsentBanner() {
const { setConsent } = useConsent();
return (
<button onClick={() => setConsent({ analytics: true, advertising: false })}>
Accept Analytics Only
</button>
);
}Automatic Behaviors
- Attribution capture: UTM parameters, click IDs, and referrer are captured and persisted on init.
- Attribution enrichment: Stored attribution is merged into every
track()andscreen()call. - Online/offline detection: Events are flushed when the browser reconnects.
- Visibility change flush: Events are flushed via
navigator.sendBeaconwhen the page is hidden (tab switch, minimize). - Before unload flush: Events are persisted to
localStorageon page close. - Periodic flush: Events are flushed on a timer (configurable).
- Remote config: Server configuration is fetched during init.
- Device context: OS, browser, locale, screen size, and timezone are detected automatically.
- Event persistence: Events are persisted in
localStorageand rehydrated on page load.
SPA (Single Page App) Usage
For single-page apps with client-side routing, call screen() on route changes:
// React Router
import { useLocation } from 'react-router-dom';
function RouteTracker() {
const location = useLocation();
const screen = useScreen();
useEffect(() => {
screen(location.pathname, { search: location.search });
}, [location, screen]);
return null;
}'use client';
import { usePathname } from 'next/navigation';
function PageTracker() {
const pathname = usePathname();
const screen = useScreen();
useEffect(() => {
screen(pathname);
}, [pathname, screen]);
return null;
}TypeScript Types
import type {
AttributionData,
BaseEvent,
ConsentState,
DeviceContext,
Environment,
ErrorListener,
EventProperties,
EventsBatchPayload,
LayersConfig,
RemoteConfigResponse,
UserProperties
} from '@layers/client';Event Payload Types
The package also exports TypeScript interfaces for the event and request payloads the SDK sends, useful for building custom integrations:
import type {
BaseEvent,
ConsentPayload,
EventsBatchPayload,
RemoteConfigResponse,
SKANPostbackPayload,
UserPropertiesPayload
} from '@layers/client';