@layers/client 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +362 -111
  2. package/package.json +2 -2
package/README.md CHANGED
@@ -1,64 +1,289 @@
1
- # @layers/client
1
+ # Layers Web SDK
2
2
 
3
- Web browser SDK for Layers Analytics. Thin wrapper over the Rust/WASM core (`@layers/core-wasm`) that adds browser-specific features: localStorage persistence, network detection, page lifecycle flush via `sendBeacon`, and React integration.
3
+ `@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.
4
4
 
5
- ## Install
5
+ ## Requirements
6
+
7
+ - Modern browser with ES module support
8
+ - React 18+ (for React hooks, optional)
9
+
10
+ ## Installation
6
11
 
7
12
  ```bash
8
13
  npm install @layers/client
9
14
  # or
15
+ yarn add @layers/client
16
+ # or
10
17
  pnpm add @layers/client
11
18
  ```
12
19
 
13
20
  ## Quick Start
14
21
 
15
- ```ts
22
+ ```typescript
16
23
  import { LayersClient } from '@layers/client';
17
24
 
18
25
  const layers = new LayersClient({
19
- apiKey: 'your-api-key',
20
26
  appId: 'your-app-id',
21
27
  environment: 'production'
22
28
  });
23
-
24
29
  await layers.init();
25
30
 
26
31
  // Track events
27
- await layers.track('button_click', { button: 'signup' });
28
-
29
- // Track screen views
30
- await layers.screen('home_page');
32
+ layers.track('button_click', { button_name: 'signup' });
31
33
 
32
- // Identify a user
33
- layers.setAppUserId('user-123');
34
+ // Track page views
35
+ layers.screen('Home');
34
36
 
35
- // Shut down gracefully
36
- await layers.shutdownAsync();
37
+ // Identify users
38
+ layers.setAppUserId('user_123');
37
39
  ```
38
40
 
39
41
  ## Configuration
40
42
 
41
- All fields for `LayersConfig`:
43
+ ### LayersConfig
44
+
45
+ ```typescript
46
+ interface LayersConfig {
47
+ appId: string;
48
+ environment: 'development' | 'staging' | 'production';
49
+ appUserId?: string;
50
+ enableDebug?: boolean;
51
+ baseUrl?: string;
52
+ flushIntervalMs?: number;
53
+ flushThreshold?: number;
54
+ maxQueueSize?: number;
55
+ }
56
+ ```
57
+
58
+ | Option | Type | Default | Description |
59
+ | ----------------- | ------------- | ------------------------- | ------------------------------------------------ |
60
+ | `appId` | `string` | _required_ | Your Layers application identifier. |
61
+ | `environment` | `Environment` | _required_ | `'development'`, `'staging'`, or `'production'`. |
62
+ | `appUserId` | `string` | `undefined` | Optional user ID to set at construction time. |
63
+ | `enableDebug` | `boolean` | `false` | Enable verbose console logging. |
64
+ | `baseUrl` | `string` | `"https://in.layers.com"` | Custom ingest API endpoint. |
65
+ | `flushIntervalMs` | `number` | `30000` | Automatic flush interval in milliseconds. |
66
+ | `flushThreshold` | `number` | `10` | Queue size that triggers an automatic flush. |
67
+ | `maxQueueSize` | `number` | `1000` | Maximum events in the queue before dropping. |
68
+
69
+ ```typescript
70
+ const layers = new LayersClient({
71
+ appId: 'your-app-id',
72
+ environment: 'development',
73
+ enableDebug: true,
74
+ flushIntervalMs: 15000,
75
+ flushThreshold: 5,
76
+ maxQueueSize: 5000
77
+ });
78
+ ```
79
+
80
+ ## Core API
42
81
 
43
- | Field | Type | Default | Description |
44
- | ----------------- | -------------------------------------------- | ----------------------- | ------------------------------------------ |
45
- | `apiKey` | `string` | _required_ | API key from the Layers dashboard |
46
- | `appId` | `string` | _required_ | Application identifier |
47
- | `environment` | `'development' \| 'staging' \| 'production'` | _required_ | Deployment environment |
48
- | `appUserId` | `string` | `undefined` | Pre-set user ID at init time |
49
- | `enableDebug` | `boolean` | `false` | Verbose console logging for all SDK calls |
50
- | `baseUrl` | `string` | `https://in.layers.com` | Override the ingest endpoint |
51
- | `flushIntervalMs` | `number` | `30000` | Periodic flush interval (ms) |
52
- | `flushThreshold` | `number` | `10` | Queue depth that triggers auto-flush |
53
- | `maxQueueSize` | `number` | `1000` | Max events in queue before dropping oldest |
82
+ ### Constructor & Initialization
54
83
 
55
- ## React Integration
84
+ ```typescript
85
+ const layers = new LayersClient(config: LayersConfig);
86
+ await layers.init();
87
+ ```
88
+
89
+ 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.
90
+
91
+ You can call `track()` and `screen()` before `init()` completes -- events are queued.
56
92
 
57
- The SDK includes first-class React bindings via `@layers/client/react`.
93
+ ### Event Tracking
94
+
95
+ ```typescript
96
+ track(eventName: string, properties?: EventProperties): void
97
+ ```
98
+
99
+ Events are batched and flushed automatically. Attribution properties (UTM, click IDs, referrer) from the current session are automatically merged into every event.
100
+
101
+ ```typescript
102
+ layers.track('purchase_completed', {
103
+ product_id: 'sku_123',
104
+ price: 9.99,
105
+ currency: 'USD'
106
+ });
107
+ ```
108
+
109
+ ### Screen/Page Tracking
110
+
111
+ ```typescript
112
+ screen(screenName: string, properties?: EventProperties): void
113
+ ```
114
+
115
+ Attribution properties are automatically merged.
116
+
117
+ ```typescript
118
+ layers.screen('ProductDetail', { product_id: 'sku_123' });
119
+ ```
120
+
121
+ ### User Identity
122
+
123
+ ```typescript
124
+ // Set or clear the app user ID
125
+ setAppUserId(appUserId: string | undefined): void
126
+
127
+ // Get the current user ID
128
+ getAppUserId(): string | undefined
129
+
130
+ // Deprecated aliases
131
+ setUserId(userId: string): void
132
+ getUserId(): string | undefined
133
+ ```
58
134
 
59
- ### Provider
135
+ ```typescript
136
+ // After login
137
+ layers.setAppUserId('user_123');
60
138
 
61
- Wrap your app in `<LayersProvider>` to initialize the SDK and make it available to hooks:
139
+ // On logout
140
+ layers.setAppUserId(undefined);
141
+ ```
142
+
143
+ ### User Properties
144
+
145
+ ```typescript
146
+ setUserProperties(properties: UserProperties): void
147
+ ```
148
+
149
+ ```typescript
150
+ layers.setUserProperties({
151
+ email: 'user@example.com',
152
+ plan: 'premium',
153
+ signup_date: '2024-01-15'
154
+ });
155
+ ```
156
+
157
+ ### Consent Management
158
+
159
+ ```typescript
160
+ setConsent(consent: ConsentState): void
161
+ getConsentState(): ConsentState
162
+ ```
163
+
164
+ ```typescript
165
+ interface ConsentState {
166
+ analytics?: boolean;
167
+ advertising?: boolean;
168
+ }
169
+ ```
170
+
171
+ ```typescript
172
+ // User accepts analytics only
173
+ layers.setConsent({ analytics: true, advertising: false });
174
+
175
+ // Read current state
176
+ const consent = layers.getConsentState();
177
+ ```
178
+
179
+ ### Session Management
180
+
181
+ ```typescript
182
+ // Get the current session ID
183
+ getSessionId(): string
184
+
185
+ // End the current session and start a new one
186
+ startNewSession(): void
187
+ ```
188
+
189
+ ### Device Info
190
+
191
+ ```typescript
192
+ setDeviceInfo(deviceInfo: DeviceContext): void
193
+ ```
194
+
195
+ Override auto-detected device context fields.
196
+
197
+ ### Flush & Shutdown
198
+
199
+ ```typescript
200
+ // Flush all queued events to the server
201
+ async flush(): Promise<void>
202
+
203
+ // Shut down immediately (no flush, events persisted to localStorage)
204
+ shutdown(): void
205
+
206
+ // Shut down with a final flush (with timeout)
207
+ async shutdownAsync(timeoutMs?: number): Promise<void> // default: 3000ms
208
+ ```
209
+
210
+ ### Error Handling
211
+
212
+ ```typescript
213
+ on(event: 'error', listener: (error: Error) => void): this
214
+ off(event: 'error', listener: (error: Error) => void): this
215
+ ```
216
+
217
+ ```typescript
218
+ layers.on('error', (error) => {
219
+ console.error('Layers error:', error.message);
220
+ });
221
+ ```
222
+
223
+ ## Attribution
224
+
225
+ The SDK automatically captures and persists attribution signals from the current page URL and referrer.
226
+
227
+ ### Captured Signals
228
+
229
+ **Click IDs** (highest priority -- overwrites existing attribution):
230
+
231
+ - `fbclid` (Meta/Facebook)
232
+ - `gclid`, `gbraid`, `wbraid` (Google)
233
+ - `ttclid` (TikTok)
234
+ - `msclkid` (Microsoft/Bing)
235
+ - `rclid` (Reddit)
236
+
237
+ **UTM Parameters**:
238
+
239
+ - `utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, `utm_term`
240
+
241
+ **Referrer**:
242
+
243
+ - `document.referrer`
244
+
245
+ ### How It Works
246
+
247
+ 1. On `init()`, the SDK reads the current URL's query parameters and `document.referrer`.
248
+ 2. Attribution data is persisted in `localStorage` with a 30-day TTL.
249
+ 3. Click IDs take priority -- a new click ID overwrites the entire stored record.
250
+ 4. UTM parameters overwrite on fresh campaign visits, but preserve existing click IDs.
251
+ 5. Attribution properties are automatically prefixed with `$attribution_` and merged into every `track()` and `screen()` call.
252
+
253
+ ### Manually Reading Attribution
254
+
255
+ ```typescript
256
+ import { getAttribution, getAttributionProperties } from '@layers/client';
257
+
258
+ // Get the full stored attribution data (or null if expired/missing)
259
+ const attribution = getAttribution();
260
+
261
+ // Get a flat property bag for merging into events
262
+ const props = getAttributionProperties();
263
+ // { '$attribution_utm_source': 'google', '$attribution_click_id_param': 'gclid', ... }
264
+ ```
265
+
266
+ ### AttributionData Type
267
+
268
+ ```typescript
269
+ interface AttributionData {
270
+ click_id_param?: string;
271
+ click_id_value?: string;
272
+ utm_source?: string;
273
+ utm_medium?: string;
274
+ utm_campaign?: string;
275
+ utm_content?: string;
276
+ utm_term?: string;
277
+ referrer_url?: string;
278
+ captured_at: number;
279
+ }
280
+ ```
281
+
282
+ ## React Integration
283
+
284
+ The SDK includes React hooks for idiomatic usage. Import from `@layers/client/react`.
285
+
286
+ ### LayersProvider
62
287
 
63
288
  ```tsx
64
289
  import { LayersProvider } from '@layers/client/react';
@@ -67,7 +292,6 @@ function App() {
67
292
  return (
68
293
  <LayersProvider
69
294
  config={{
70
- apiKey: 'your-api-key',
71
295
  appId: 'your-app-id',
72
296
  environment: 'production'
73
297
  }}
@@ -78,73 +302,87 @@ function App() {
78
302
  }
79
303
  ```
80
304
 
81
- The provider creates a `LayersClient` on mount, calls `init()`, and shuts it down on unmount.
305
+ Initializes the client on mount and shuts it down on unmount.
82
306
 
83
- ### Hooks
307
+ ### useLayers
84
308
 
85
- All hooks must be used inside a `<LayersProvider>`.
86
-
87
- #### `useTrack()`
309
+ ```typescript
310
+ function useLayers(): LayersClient;
311
+ ```
88
312
 
89
- Returns a stable, memoized function for tracking events:
313
+ Returns the `LayersClient` instance. Throws if used outside a `<LayersProvider>`.
90
314
 
91
315
  ```tsx
92
- import { useTrack } from '@layers/client/react';
316
+ function MyComponent() {
317
+ const layers = useLayers();
318
+ layers.track('component_viewed');
319
+ }
320
+ ```
321
+
322
+ ### useTrack
323
+
324
+ ```typescript
325
+ function useTrack(): (eventName: string, properties?: EventProperties) => void;
326
+ ```
93
327
 
328
+ Returns a stable, memoized track function.
329
+
330
+ ```tsx
94
331
  function SignupButton() {
95
332
  const track = useTrack();
96
333
 
97
- return <button onClick={() => track('signup_click', { source: 'hero' })}>Sign up</button>;
334
+ return <button onClick={() => track('signup_click', { source: 'hero' })}>Sign Up</button>;
98
335
  }
99
336
  ```
100
337
 
101
- #### `useScreen()`
338
+ ### useScreen
102
339
 
103
- Returns a stable, memoized function for tracking screen views:
340
+ ```typescript
341
+ function useScreen(): (screenName: string, properties?: EventProperties) => void;
342
+ ```
104
343
 
105
- ```tsx
106
- import { useScreen } from '@layers/client/react';
107
- import { useEffect } from 'react';
344
+ Returns a stable, memoized screen tracking function.
108
345
 
346
+ ```tsx
109
347
  function Dashboard() {
110
348
  const screen = useScreen();
111
349
  useEffect(() => {
112
- screen('dashboard');
350
+ screen('Dashboard');
113
351
  }, [screen]);
114
352
 
115
- return <div>...</div>;
353
+ return <div>Dashboard</div>;
116
354
  }
117
355
  ```
118
356
 
119
- #### `useIdentify()`
357
+ ### useIdentify
120
358
 
121
- Returns a function to set (or clear) the app user ID:
359
+ ```typescript
360
+ function useIdentify(): (userId: string | undefined) => void;
361
+ ```
122
362
 
123
- ```tsx
124
- import { useIdentify } from '@layers/client/react';
363
+ Returns a stable, memoized identify function. Pass `undefined` to clear.
125
364
 
365
+ ```tsx
126
366
  function LoginForm() {
127
367
  const identify = useIdentify();
128
368
 
129
- const onLogin = (userId: string) => {
130
- identify(userId);
131
- };
132
-
133
- const onLogout = () => {
134
- identify(undefined);
135
- };
369
+ const onLogin = (userId: string) => identify(userId);
370
+ const onLogout = () => identify(undefined);
136
371
  }
137
372
  ```
138
373
 
139
- #### `useConsent()`
374
+ ### useConsent
140
375
 
141
- Returns functions to get and set consent state:
376
+ ```typescript
377
+ function useConsent(): {
378
+ setConsent: (consent: ConsentState) => void;
379
+ getConsentState: () => ConsentState;
380
+ };
381
+ ```
142
382
 
143
383
  ```tsx
144
- import { useConsent } from '@layers/client/react';
145
-
146
384
  function ConsentBanner() {
147
- const { setConsent, getConsentState } = useConsent();
385
+ const { setConsent } = useConsent();
148
386
 
149
387
  return (
150
388
  <button onClick={() => setConsent({ analytics: true, advertising: false })}>
@@ -154,71 +392,84 @@ function ConsentBanner() {
154
392
  }
155
393
  ```
156
394
 
157
- #### `useLayers()`
395
+ ## Automatic Behaviors
158
396
 
159
- Returns the raw `LayersClient` instance for advanced usage:
160
-
161
- ```tsx
162
- import { useLayers } from '@layers/client/react';
397
+ - **Attribution capture**: UTM parameters, click IDs, and referrer are captured and persisted on init.
398
+ - **Attribution enrichment**: Stored attribution is merged into every `track()` and `screen()` call.
399
+ - **Online/offline detection**: Events are flushed when the browser reconnects.
400
+ - **Visibility change flush**: Events are flushed via `navigator.sendBeacon` when the page is hidden (tab switch, minimize).
401
+ - **Before unload flush**: Events are persisted to `localStorage` on page close.
402
+ - **Periodic flush**: Events are flushed on a timer (configurable).
403
+ - **Remote config**: Server configuration is fetched during init.
404
+ - **Device context**: OS, browser, locale, screen size, and timezone are detected automatically.
405
+ - **Event persistence**: Events are persisted in `localStorage` and rehydrated on page load.
163
406
 
164
- function Advanced() {
165
- const layers = useLayers();
166
- // layers.setUserProperties({ plan: 'pro' });
167
- }
168
- ```
407
+ ## SPA (Single Page App) Usage
169
408
 
170
- ## Error Handling
409
+ For single-page apps with client-side routing, call `screen()` on route changes:
171
410
 
172
- Register error listeners to catch errors from `track()`, `screen()`, and `flush()` that would otherwise be silently dropped:
411
+ ```typescript
412
+ // React Router
413
+ import { useLocation } from 'react-router-dom';
173
414
 
174
- ```ts
175
- const layers = new LayersClient({ ... });
415
+ function RouteTracker() {
416
+ const location = useLocation();
417
+ const screen = useScreen();
176
418
 
177
- layers.on('error', (err) => {
178
- console.error('Layers SDK error:', err.message);
179
- // Send to your error reporting service
180
- });
419
+ useEffect(() => {
420
+ screen(location.pathname, { search: location.search });
421
+ }, [location, screen]);
181
422
 
182
- // Remove a specific listener
183
- layers.off('error', myListener);
423
+ return null;
424
+ }
184
425
  ```
185
426
 
186
- When `enableDebug` is `true` and no error listeners are registered, errors are logged to `console.warn`.
427
+ ```typescript
428
+ 'use client';
187
429
 
188
- ## Consent Management
430
+ import { usePathname } from 'next/navigation';
189
431
 
190
- Control what data the SDK collects:
432
+ function PageTracker() {
433
+ const pathname = usePathname();
434
+ const screen = useScreen();
191
435
 
192
- ```ts
193
- // Allow analytics, deny advertising
194
- layers.setConsent({ analytics: true, advertising: false });
436
+ useEffect(() => {
437
+ screen(pathname);
438
+ }, [pathname, screen]);
195
439
 
196
- // Check current state
197
- const consent = layers.getConsentState();
440
+ return null;
441
+ }
198
442
  ```
199
443
 
200
- When `analytics` is `false`, `track()` and `screen()` calls are silently dropped.
201
-
202
- ## Page Lifecycle (sendBeacon)
203
-
204
- The SDK automatically flushes events when the page becomes hidden using `navigator.sendBeacon()` for reliable delivery. If `sendBeacon` fails, events are persisted to localStorage and retried on the next page load.
205
-
206
- On `beforeunload`, events are synchronously written to localStorage as a last resort.
207
-
208
- ## Debug Mode
209
-
210
- Enable `enableDebug: true` to see detailed logs for every SDK operation:
211
-
212
- ```
213
- [Layers] track("button_click", 2 properties)
214
- [Layers] screen("home_page", 0 properties)
215
- [Layers] setAppUserId("user-123")
444
+ ## TypeScript Types
445
+
446
+ ```typescript
447
+ import type {
448
+ AttributionData,
449
+ BaseEvent,
450
+ ConsentState,
451
+ DeviceContext,
452
+ Environment,
453
+ ErrorListener,
454
+ EventProperties,
455
+ EventsBatchPayload,
456
+ LayersConfig,
457
+ RemoteConfigResponse,
458
+ UserProperties
459
+ } from '@layers/client';
216
460
  ```
217
461
 
218
- ## Server-Side Rendering
462
+ ## Wire Protocol Types
219
463
 
220
- The SDK safely handles SSR environments where `window`, `document`, and `navigator` are unavailable. Device detection and lifecycle listeners are skipped in SSR. For server-side tracking, use [@layers/node](../node) instead.
464
+ The package also exports TypeScript interfaces for the Layers wire protocol, useful for building custom integrations:
221
465
 
222
- ## License
223
-
224
- MIT
466
+ ```typescript
467
+ import type {
468
+ BaseEvent,
469
+ ConsentPayload,
470
+ EventsBatchPayload,
471
+ RemoteConfigResponse,
472
+ SKANPostbackPayload,
473
+ UserPropertiesPayload
474
+ } from '@layers/client';
475
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@layers/client",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -33,7 +33,7 @@
33
33
  "dist"
34
34
  ],
35
35
  "dependencies": {
36
- "@layers/core-wasm": "1.1.0"
36
+ "@layers/core-wasm": "1.2.0"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "react": ">=18"