@snovasys/usage-analytics-sdk 1.0.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.
package/README.md ADDED
@@ -0,0 +1,638 @@
1
+ # @snovasys/usage-analytics-sdk
2
+
3
+ Lightweight, extensible usage analytics SDK for browser and Node. Emit events from your app without coupling to a specific analytics provider; plug in Microsoft Clarity, Google Analytics, or your own API via **listeners**. One API, multiple backends.
4
+
5
+ ## Features
6
+
7
+ - **Single API** – `init`, `track`, `identify`. No provider-specific code in your app.
8
+ - **Multiple listeners** – Send the same events to Clarity, GA, and your backend at once.
9
+ - **Pre-init safe** – Call `track` or `identify` before `init()`; choose to **queue** (replay after init) or **drop**.
10
+ - **Enriched events** – Every event gets `appId`, `environment`, `timestamp`, `sessionId`, `userId` (if identified), and SDK version.
11
+ - **Error isolation** – One failing listener does not break others; optional `onError` callback.
12
+ - **SSR / Node safe** – No `window` access until needed; in Node/SSR, pre-init events are dropped.
13
+ - **Zero/minimal deps** – Core has no runtime dependencies; listeners are optional entry points.
14
+ - **Auto-configuration** – Pass `configUrl` or `getConfig` at init; the SDK fetches config and creates built-in listeners (Clarity, GA, custom API) **without** you passing listener factories.
15
+ - **Switch listeners without touching package or products** – In simple terms: the **package** stays unchanged, **products** stay unchanged. Only the **pointing** (Clarity vs GA vs custom API, etc.) changes—and that comes from **config** (a URL or API you control). Switch from Clarity to GA by updating that config; no package republish, no product redeploy.
16
+ - **Direct backend communication** – Built-in listeners send events **from the client** directly to Clarity, GA, and your API. No proxy hop; minimal bandwidth.
17
+
18
+ ---
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ npm install @snovasys/usage-analytics-sdk
24
+ ```
25
+
26
+ Optional listeners (install only what you use):
27
+
28
+ ```bash
29
+ # No extra install for noop and custom-api – they live in this package
30
+ # Import from '@snovasys/usage-analytics-sdk/noop' or '@snovasys/usage-analytics-sdk/custom-api'
31
+ ```
32
+
33
+ ---
34
+
35
+ ## Quick start
36
+
37
+ Minimal setup: init with an app id, then track.
38
+
39
+ ```ts
40
+ import { init, track } from '@snovasys/usage-analytics-sdk';
41
+
42
+ await init({ appId: 'my-app' });
43
+ track({ name: 'feature_used', properties: { feature: 'export' } });
44
+ ```
45
+
46
+ With a listener (e.g. send events to your API):
47
+
48
+ ```ts
49
+ import { init, track } from '@snovasys/usage-analytics-sdk';
50
+ import { createCustomApiListener } from '@snovasys/usage-analytics-sdk/custom-api';
51
+
52
+ await init({
53
+ appId: 'my-app',
54
+ environment: 'production',
55
+ listeners: [
56
+ {
57
+ id: 'api',
58
+ listener: createCustomApiListener({
59
+ endpoint: 'https://api.example.com/analytics/events',
60
+ apiKey: 'optional-key',
61
+ }),
62
+ config: {},
63
+ },
64
+ ],
65
+ });
66
+ track({ name: 'page_view', properties: { path: '/dashboard' } });
67
+ ```
68
+
69
+ **Auto-config (no listener code in your app):** Pass **appId** and **environment** from your environment files; the config API returns **listener details only**. See [Auto-configuration and direct backend communication](#auto-configuration-and-direct-backend-communication) and [docs/configuration.md](docs/configuration.md).
70
+
71
+ ```ts
72
+ import { init, track } from '@snovasys/usage-analytics-sdk';
73
+
74
+ await init({
75
+ appId: import.meta.env.VITE_APP_ID, // or process.env.REACT_APP_ID, etc.
76
+ environment: import.meta.env.VITE_ENV ?? 'production',
77
+ configUrl: 'https://config.example.com/analytics/listeners.json',
78
+ });
79
+ track({ name: 'feature_used', properties: { feature: 'export' } });
80
+ ```
81
+
82
+ Events go **directly** from the client to Clarity, GA, and your API; no proxy.
83
+
84
+ ---
85
+
86
+ ## Script tag (index.html) – no API calls
87
+
88
+ 1. **Add one script** in your **index page**, loaded from a CDN (or your own host).
89
+ 2. **Put config in the script tag** via the `data-config` attribute – that config decides **which listeners** (Clarity, GA, custom API, etc.) receive events.
90
+ 3. **Add track calls** across your app; all events go to the listeners configured in that script tag.
91
+
92
+ **No other API calls** – the script reads config from the tag and initializes; no fetch, no backend.
93
+
94
+ ### 1. Add the script tag in index.html
95
+
96
+ Load the script from a CDN (industry standard: unpkg or jsDelivr) and pass config in the **script tag** using `data-config` (JSON):
97
+
98
+ ```html
99
+ <!DOCTYPE html>
100
+ <html>
101
+ <head>...</head>
102
+ <body>
103
+ <script
104
+ src="https://unpkg.com/@snovasys/usage-analytics-sdk/dist/usage-analytics.min.js"
105
+ data-config='{"appId":"my-app","environment":"production","listeners":[{"id":"clarity","config":{"projectId":"your-clarity-project-id"}},{"id":"ga","config":{"measurementId":"G-XXXXXXXX"}}]}'
106
+ ></script>
107
+ </body>
108
+ </html>
109
+ ```
110
+
111
+ **Using unpkg or jsDelivr (free CDNs)**
112
+
113
+ Both [unpkg](https://unpkg.com) and [jsDelivr](https://www.jsdelivr.com) are **free**, no signup required. They serve files from the npm registry.
114
+
115
+ | CDN | Script URL |
116
+ |-----|------------|
117
+ | **unpkg** | `https://unpkg.com/@snovasys/usage-analytics-sdk@1/dist/usage-analytics.min.js` |
118
+ | **jsDelivr** | `https://cdn.jsdelivr.net/npm/@snovasys/usage-analytics-sdk@1/dist/usage-analytics.min.js` |
119
+
120
+ - Replace `@1` with your desired version (e.g. `@1.0.0`) to pin it in production.
121
+ - Omit the version (e.g. `.../usage-analytics-sdk/dist/...`) to get the latest release, but pinning is recommended for stability.
122
+
123
+ **Example with unpkg:**
124
+
125
+ ```html
126
+ <script
127
+ src="https://unpkg.com/@snovasys/usage-analytics-sdk@1/dist/usage-analytics.min.js"
128
+ data-config='{"appId":"my-app","listeners":[{"id":"clarity","config":{"projectId":"abc123"}}]}'
129
+ ></script>
130
+ ```
131
+
132
+ **Example with jsDelivr:**
133
+
134
+ ```html
135
+ <script
136
+ src="https://cdn.jsdelivr.net/npm/@snovasys/usage-analytics-sdk@1/dist/usage-analytics.min.js"
137
+ data-config='{"appId":"my-app","listeners":[{"id":"clarity","config":{"projectId":"abc123"}}]}'
138
+ ></script>
139
+ ```
140
+
141
+ You can also copy `dist/usage-analytics.min.js` from the package into your app and use a relative path instead of a CDN.
142
+
143
+ **Maintainers:** jsDelivr serves from npm. To make the package available on jsDelivr, publish to npm first; see [PUBLISH.md](PUBLISH.md).
144
+
145
+ ### 2. Add track calls across your code
146
+
147
+ The script exposes `UsageAnalytics` on `window`. Call `track` (and optionally `identify`) anywhere in your app:
148
+
149
+ ```javascript
150
+ UsageAnalytics.track({ name: 'page_view', properties: { path: window.location.pathname } });
151
+ UsageAnalytics.track({ name: 'feature_used', properties: { feature: 'export' } });
152
+ UsageAnalytics.identify('user-123', { email: 'user@example.com' });
153
+ ```
154
+
155
+ **Which listeners receive these events** is determined **only** by the `data-config` in the script tag you added in index.html. Change Clarity vs GA vs custom API by editing that config and redeploying the page.
156
+
157
+ ### Fallback: config in a separate script
158
+
159
+ If you prefer not to put JSON in the script tag, set `window.__ANALYTICS_CONFIG__` in an inline script **before** the SDK script; the script will use it when `data-config` is not present.
160
+
161
+ ---
162
+
163
+ ## How to use – full feature guide
164
+
165
+ ### 1. `init(options)` – initialize once
166
+
167
+ Call `init()` early in your app (e.g. in `main.ts` or root layout). It is **idempotent**: calling it again re-applies config. It returns a **Promise**; you can `await init(...)` to know when the SDK is ready.
168
+
169
+ **Required**
170
+
171
+ - **`appId`** (string) – Your application identifier. Used in every event so you can filter by app. Set from environment variables (e.g. `VITE_APP_ID`, `REACT_APP_ID`) when using remote config.
172
+
173
+ **Optional**
174
+
175
+ | Option | Type | Default | Description |
176
+ |--------|------|---------|-------------|
177
+ | `environment` | string | `'production'` | e.g. `production`, `staging`, `dev`. Added to every event. |
178
+ | `listeners` | ListenerEntry[] | `[]` | List of listener instances and their config. See [Listeners](#listeners) below. |
179
+ | `preInitBehavior` | `'queue'` \| `'drop'` | `'drop'` | What to do with `track`/`identify` calls **before** `init()`: **queue** (replay after init) or **drop** (ignore). |
180
+ | `queueMaxSize` | number | 100 | When `preInitBehavior === 'queue'`, max events to keep. When full, oldest are dropped (FIFO). |
181
+ | `queueTtlMs` | number | 5000 | When queueing, events older than this (ms) at flush time are dropped. |
182
+ | `onError` | `(error, context?) => void` | — | Called when a listener throws (e.g. in `init` or `track`). Use for logging or monitoring. |
183
+ | `sessionId` | string | (generated) | If you don’t set it, the SDK generates a UUID and optionally persists it in `sessionStorage` keyed by `appId`. |
184
+
185
+ **Auto-config options** (when using remote config): `configUrl` (URL to fetch config), `getConfig` (async function returning config), `listenerFactories` (map id → factory to add/override built-in listeners), `fetch` (custom fetch for `configUrl`). See [Auto-configuration](#auto-configuration-and-direct-backend-communication) and [docs/configuration.md](docs/configuration.md).
186
+
187
+ **Example – full config**
188
+
189
+ ```ts
190
+ await init({
191
+ appId: 'my-app',
192
+ environment: import.meta.env.PROD ? 'production' : 'development',
193
+ preInitBehavior: 'queue',
194
+ queueMaxSize: 50,
195
+ queueTtlMs: 3000,
196
+ onError: (err, ctx) => console.error('Analytics listener error', ctx?.listenerId, err),
197
+ listeners: [
198
+ { id: 'api', listener: createCustomApiListener({ endpoint: '...' }), config: {} },
199
+ ],
200
+ });
201
+ ```
202
+
203
+ ---
204
+
205
+ ### 2. `track(event)` – emit an event
206
+
207
+ Fire-and-forget. Safe to call before `init()` (then queued or dropped per `preInitBehavior`).
208
+
209
+ **Event shape**
210
+
211
+ - **`name`** (string, required) – Event name, e.g. `page_view`, `feature_used`, `button_click`.
212
+ - **`properties`** (object, optional) – Extra data (JSON-serializable). e.g. `{ path: '/dashboard', role: 'admin' }`.
213
+ - **`timestamp`** (string, optional) – ISO 8601; if omitted, the SDK sets it to now.
214
+
215
+ **Example**
216
+
217
+ ```ts
218
+ track({ name: 'page_view', properties: { path: '/dashboard', title: 'Dashboard' } });
219
+ track({ name: 'feature_used', properties: { feature: 'export', format: 'csv', rows: 100 } });
220
+ track({ name: 'signup_completed' });
221
+ ```
222
+
223
+ Every event is **enriched** before being sent to listeners: `timestamp`, `eventId`, `appId`, `environment`, `sdkVersion`, `sessionId`, and (if you called `identify`) `userId` are added automatically.
224
+
225
+ ---
226
+
227
+ ### 3. `identify(userId, traits?)` – set user identity
228
+
229
+ Call when the user logs in or you know their id. The SDK stores it and adds **`userId`** to every subsequent `track` envelope. Listeners that support identity (e.g. your API) can implement `identify` and receive this call too.
230
+
231
+ - **`userId`** (string) – Your stable user id.
232
+ - **`traits`** (object, optional) – e.g. `{ email, name, plan }`.
233
+
234
+ **Example**
235
+
236
+ ```ts
237
+ identify('user-123', { email: 'user@example.com', plan: 'pro' });
238
+ track({ name: 'settings_updated' }); // envelope will include userId: 'user-123'
239
+ ```
240
+
241
+ Same as `track`, **identify** is safe before `init()` (queued or dropped). On flush, all queued `identify` calls are replayed **before** queued `track` calls.
242
+
243
+ ---
244
+
245
+ ### 4. `flush()` – wait for batched sends
246
+
247
+ Some listeners (e.g. custom API) batch events and send periodically. Call **`flush()`** when you need to wait for current batches to be sent (e.g. before page unload or in tests).
248
+
249
+ ```ts
250
+ window.addEventListener('beforeunload', () => {
251
+ flush(); // best-effort; may not complete if page unloads immediately
252
+ });
253
+ ```
254
+
255
+ Returns a **Promise**; await it if you need to block until listeners have flushed.
256
+
257
+ ---
258
+
259
+ ### 5. `reset()` – clear state and teardown listeners
260
+
261
+ Clears the pre-init queue, clears stored `userId`, and calls **`teardown()`** on each listener (if implemented). Use in **tests** (between cases) or on **logout** so the next user does not inherit the previous identity.
262
+
263
+ ```ts
264
+ await reset();
265
+ // SDK is no longer initialized; track/identify will queue or drop again until init() is called
266
+ ```
267
+
268
+ ---
269
+
270
+ ### Auto-configuration and direct backend communication
271
+
272
+ When you pass **`configUrl`** or **`getConfig`** at init, the SDK **fetches listener configuration** and **creates listeners automatically** from its built-in registry (noop, clarity, ga, custom-api). **appId** and **environment** are always taken from init options (e.g. from your environment files); only listener details come from the API.
273
+
274
+ - **Config API** – Your backend returns JSON with **listener details only**: `{ listeners: [ { id, enabled?, config } ] }`. **appId** and **environment** are not read from the API; pass them to `init()` from your env. See [docs/configuration.md](docs/configuration.md) for the full contract.
275
+ - **Built-in listeners** – The SDK creates and registers the corresponding listeners (Clarity, GA, custom API) from the config. No proxy: each listener sends events **directly** from the client to its backend (Clarity, Google, your API). Bandwidth overhead is minimal; config is fetched once at init.
276
+ - **Manual override** – Use **`listenerFactories`** to add custom listener ids or override built-in ones when using remote config.
277
+
278
+ Example: **appId** and **environment** from env files; config API returns only listeners. Products call `init({ appId: import.meta.env.VITE_APP_ID, environment: import.meta.env.VITE_ENV, configUrl: 'https://...' })` and get Clarity, GA, and custom API auto-configured. Events go client → Clarity, client → GA, client → your API.
279
+
280
+ ---
281
+
282
+ ### Pre-init behavior (queue vs drop)
283
+
284
+ Calls to **`track`** and **`identify`** before **`init()`** are handled according to **`preInitBehavior`**:
285
+
286
+ | Mode | Behavior |
287
+ |------|----------|
288
+ | **`drop`** (default) | Events before `init()` are ignored. |
289
+ | **`queue`** | Events are stored (up to `queueMaxSize`, events older than `queueTtlMs` at flush are dropped). After `init()` completes, queued **identify** calls are replayed first (in order), then queued **track** calls (in order). |
290
+
291
+ **When to use queue**
292
+
293
+ - You load the SDK early and call `track`/`identify` before your init code runs (e.g. in a shared layout).
294
+ - You want early events (e.g. first click) to be sent once the SDK is initialized.
295
+
296
+ **When to use drop**
297
+
298
+ - You always call `init()` before any `track`/`identify` (e.g. in `main.ts`).
299
+ - You want to avoid any memory or replay logic (e.g. strict SSR).
300
+
301
+ **SSR / Node:** When `typeof window === 'undefined'`, the SDK **always drops** pre-init events (no queue), regardless of `preInitBehavior`. Call `init()` only in the browser or omit analytics in SSR bundles.
302
+
303
+ ---
304
+
305
+ ### Listeners
306
+
307
+ Listeners are **adapters** that receive the same enriched event envelope and send it to a backend (Clarity, GA, your API). You pass **instances** at `init()`; the core does not import provider code.
308
+
309
+ **No-op listener** – Use when analytics is disabled or in tests.
310
+
311
+ ```ts
312
+ import { createNoopListener } from '@snovasys/usage-analytics-sdk/noop';
313
+
314
+ listeners: [{ id: 'noop', listener: createNoopListener(), config: {} }]
315
+ ```
316
+
317
+ **Custom API listener** – POST events to your endpoint. Optional batching.
318
+
319
+ ```ts
320
+ import { createCustomApiListener } from '@snovasys/usage-analytics-sdk/custom-api';
321
+
322
+ listeners: [
323
+ {
324
+ id: 'api',
325
+ listener: createCustomApiListener({
326
+ endpoint: 'https://api.example.com/analytics/events',
327
+ apiKey: 'optional-bearer-token',
328
+ batchSize: 10, // send after 10 events (default 1)
329
+ flushIntervalMs: 5000, // or after 5s (default 5000)
330
+ }),
331
+ config: {},
332
+ },
333
+ ]
334
+ ```
335
+
336
+ **Clarity and GA listeners** – Built-in listeners (script injection; no npm deps). Use via auto-config (id `clarity` / `ga` in remote config) or import for manual wiring:
337
+
338
+ ```ts
339
+ import { createClarityListener } from '@snovasys/usage-analytics-sdk/clarity';
340
+ import { createGAListener } from '@snovasys/usage-analytics-sdk/ga';
341
+
342
+ listeners: [
343
+ { id: 'clarity', listener: createClarityListener({ projectId: 'your-project-id' }), config: {} },
344
+ { id: 'ga', listener: createGAListener({ measurementId: 'G-XXXXXXXX' }), config: {} },
345
+ ]
346
+ ```
347
+
348
+ **Multiple listeners** – Same events go to all.
349
+
350
+ ```ts
351
+ listeners: [
352
+ { id: 'api', listener: createCustomApiListener({ endpoint: '...' }), config: {} },
353
+ { id: 'noop', listener: createNoopListener(), config: {} },
354
+ // Add Clarity/GA adapters the same way
355
+ ],
356
+ ```
357
+
358
+ **Listener entry shape**
359
+
360
+ - **`id`** (string, optional) – For logging and `onError(context.listenerId)`.
361
+ - **`enabled`** (boolean, default `true`) – If `false`, the listener is not registered.
362
+ - **`listener`** – Object implementing `init(config)`, `track(envelope)`, and optionally `identify`, `flush`, `teardown`.
363
+ - **`config`** – Passed to `listener.init(config)` (opaque to the SDK).
364
+
365
+ ---
366
+
367
+ ### How an admin decides whether Clarity (or any backend) receives events
368
+
369
+ The SDK does **not** decide which backends receive events. The **app** (or the config an admin controls) decides at **init** by which listeners it passes and whether each is **enabled**.
370
+
371
+ **Two ways an admin can control this:**
372
+
373
+ 1. **Include or exclude the listener** – Only add the Clarity listener to `listeners` when the admin wants Clarity to receive events (e.g. from env or remote config).
374
+ 2. **Use the `enabled` flag** – Add the Clarity listener to the config but set **`enabled: false`** when the admin wants Clarity off. The SDK will not register that listener, so Clarity will not receive any events.
375
+
376
+ **Who controls the config?**
377
+
378
+ The object passed to `init(options)` is built by your app. The admin controls it indirectly by controlling **how** that object is built, for example:
379
+
380
+ - **Environment variables** – e.g. `VITE_ANALYTICS_CLARITY_ENABLED=true` and `VITE_ANALYTICS_CLARITY_PROJECT_ID=xxx`. The app reads these and only adds (or enables) the Clarity listener when the admin has set them.
381
+ - **Remote config / feature flags** – Admin toggles “Clarity” in a dashboard; the app fetches config and builds the `listeners` array accordingly before calling `init()`.
382
+ - **Admin API** – Backend returns which analytics backends are enabled for the tenant or app; the app builds `listeners` from that and calls `init()`.
383
+
384
+ **Example – env-driven: Clarity only when enabled**
385
+
386
+ ```ts
387
+ import { init, track } from '@snovasys/usage-analytics-sdk';
388
+ import { createClarityListener } from '@snovasys/usage-analytics-sdk-clarity'; // or your Clarity adapter
389
+
390
+ const clarityProjectId = import.meta.env.VITE_CLARITY_PROJECT_ID;
391
+ const clarityEnabled = import.meta.env.VITE_CLARITY_ENABLED === 'true';
392
+
393
+ const listeners = [];
394
+ if (clarityEnabled && clarityProjectId) {
395
+ listeners.push({
396
+ id: 'clarity',
397
+ enabled: true,
398
+ listener: createClarityListener({ projectId: clarityProjectId }),
399
+ config: { projectId: clarityProjectId },
400
+ });
401
+ }
402
+
403
+ await init({
404
+ appId: 'my-app',
405
+ listeners,
406
+ });
407
+ ```
408
+
409
+ **Example – admin toggle: Clarity enabled/disabled via `enabled`**
410
+
411
+ ```ts
412
+ // analyticsConfig comes from your backend or feature-flag service (admin-controlled)
413
+ const analyticsConfig = await fetchAnalyticsConfig(); // { clarityEnabled: true, clarityProjectId: '...' }
414
+
415
+ const listeners = [
416
+ {
417
+ id: 'clarity',
418
+ enabled: analyticsConfig.clarityEnabled,
419
+ listener: createClarityListener({ projectId: analyticsConfig.clarityProjectId }),
420
+ config: { projectId: analyticsConfig.clarityProjectId },
421
+ },
422
+ ];
423
+
424
+ await init({ appId: 'my-app', listeners });
425
+ ```
426
+
427
+ **Summary**
428
+
429
+ | Admin wants Clarity to receive events | What to do |
430
+ |---------------------------------------|------------|
431
+ | Yes | Include Clarity listener in `listeners` with `enabled: true` (or omit `enabled`; default is true). |
432
+ | No | Either omit the Clarity listener from `listeners`, or include it with **`enabled: false`**. |
433
+
434
+ The SDK never talks to Clarity by itself; it only forwards events to the listeners you register at init. So **whether Clarity receives events is entirely decided by the config you pass into `init()`** (which you can drive from env, feature flags, or admin API).
435
+
436
+ ---
437
+
438
+ ### Multi-product setup: switch listeners for all products without a release
439
+
440
+ **Problem:** You have many products (e.g. 100), each with its own frontend. As a company admin you want to turn Clarity (or another listener) on or off for **all** products, or change which backends receive events, **without** releasing every product or editing each app’s code.
441
+
442
+ If each product decides listeners at **build time** (e.g. env vars) or from **product-specific** config, then changing listeners globally means changing config in every product and doing a release for each. That does not scale.
443
+
444
+ **Solution: central runtime config.** All products get their analytics config from **one admin-controlled API** (or feature-flag service) **at runtime**, before calling `init()`. The company admin changes that central config; every product picks up the new config on the next load (or after cache TTL). No per-product release needed.
445
+
446
+ **Pattern:**
447
+
448
+ 1. **Central config API** – A single backend (e.g. platform BFF, config service, or feature-flag service) that the company admin updates. It returns which listeners are enabled and their settings (e.g. Clarity project id, custom API endpoint, etc.), optionally per product/tenant or globally.
449
+ 2. **Same bootstrap in every product** – Each product’s app, at startup (e.g. in `main.ts` or root layout):
450
+ - Calls the central API (e.g. `GET /api/platform/analytics-config` or your feature-flag SDK).
451
+ - Builds the `listeners` array from the response (which listeners are enabled, their config).
452
+ - Calls `init({ appId, environment, listeners, ... })` with that config.
453
+
454
+ So the **source of truth** for “which listeners are on” is the central API, not env vars or code in each product. Admin changes the central config; all products follow it on next load.
455
+
456
+ **Example – central config API response (admin-controlled):**
457
+
458
+ ```json
459
+ {
460
+ "listeners": [
461
+ { "id": "clarity", "enabled": true, "config": { "projectId": "abc123" } },
462
+ { "id": "custom-api", "enabled": true, "config": { "endpoint": "https://api.example.com/events", "apiKey": "..." } }
463
+ ]
464
+ }
465
+ ```
466
+
467
+ **Example – product bootstrap (same in every product):**
468
+
469
+ ```ts
470
+ // In every product's main.ts or analytics bootstrap – same code, config comes from central API
471
+ import { init, track } from '@snovasys/usage-analytics-sdk';
472
+ import { createClarityListener } from '@snovasys/usage-analytics-sdk-clarity';
473
+ import { createCustomApiListener } from '@snovasys/usage-analytics-sdk/custom-api';
474
+
475
+ async function bootstrapAnalytics() {
476
+ const centralConfig = await fetch('https://platform.example.com/api/analytics-config').then(r => r.json());
477
+ // centralConfig.listeners: [{ id, enabled, config }, ...]
478
+
479
+ const listeners = centralConfig.listeners
480
+ .filter((entry: { enabled: boolean }) => entry.enabled)
481
+ .map((entry: { id: string; config: unknown }) => {
482
+ if (entry.id === 'clarity') {
483
+ return { id: 'clarity', listener: createClarityListener(entry.config as { projectId: string }), config: entry.config };
484
+ }
485
+ if (entry.id === 'custom-api') {
486
+ return { id: 'custom-api', listener: createCustomApiListener(entry.config as { endpoint: string; apiKey?: string }), config: entry.config };
487
+ }
488
+ return null;
489
+ })
490
+ .filter(Boolean);
491
+
492
+ await init({
493
+ appId: 'my-product-id', // or from centralConfig
494
+ environment: import.meta.env.MODE,
495
+ listeners,
496
+ });
497
+ }
498
+
499
+ await bootstrapAnalytics();
500
+ track({ name: 'app_loaded' });
501
+ ```
502
+
503
+ **Optional:** You can cache the config (e.g. in memory or sessionStorage with a TTL) so you don’t hit the API on every load; when the admin changes the config, products still pick it up after cache expiry or next deploy of the central API.
504
+
505
+ **Summary:**
506
+
507
+ | Approach | Admin changes listeners for all products |
508
+ |----------|------------------------------------------|
509
+ | Build-time / env per product | Needs a release (or env change + release) per product. |
510
+ | **Central runtime config API** | Admin updates central API; all products get new config on next load. **No product release needed.** |
511
+
512
+ The SDK does not fetch config itself; it only accepts the config you pass to `init()`. So you keep one central API (or feature-flag service) as the source of truth, and every product uses the same pattern: fetch config → build listeners → init().
513
+
514
+ ---
515
+
516
+ ### Error handling
517
+
518
+ - **Config validation** – Invalid `init` options (e.g. missing `appId`) throw; the SDK does not partially init.
519
+ - **Listener init** – If `listener.init(config)` throws, the SDK calls **`onError(error, { listenerId })`** and does **not** register that listener. Other listeners are still registered.
520
+ - **Listener track/identify** – The SDK wraps each call in try/catch. If a listener throws, **`onError`** is called and the loop continues to the next listener. One failing listener does not break others or your app.
521
+
522
+ **Example**
523
+
524
+ ```ts
525
+ onError: (error, ctx) => {
526
+ logger.error('Analytics listener failed', { listenerId: ctx?.listenerId, error });
527
+ }
528
+ ```
529
+
530
+ ---
531
+
532
+ ### Session and user identity
533
+
534
+ - **Session id** – If you do not pass **`sessionId`** in `init()`, the SDK generates a UUID and, in the browser, can persist it in `sessionStorage` (keyed by `appId`) so it survives reloads. In Node/SSR there is no storage; a new id is used per process.
535
+ - **User id** – Set via **`identify(userId, traits?)`**. Stored in memory and added to every subsequent event envelope until **`reset()`** (e.g. logout).
536
+
537
+ ---
538
+
539
+ ## Usage examples
540
+
541
+ **Minimal (no listeners – events are dropped after enrich)**
542
+
543
+ ```ts
544
+ import { init, track } from '@snovasys/usage-analytics-sdk';
545
+ await init({ appId: 'my-app' });
546
+ track({ name: 'app_loaded' });
547
+ ```
548
+
549
+ **With custom API and user identity**
550
+
551
+ ```ts
552
+ import { init, track, identify } from '@snovasys/usage-analytics-sdk';
553
+ import { createCustomApiListener } from '@snovasys/usage-analytics-sdk/custom-api';
554
+
555
+ await init({
556
+ appId: 'my-app',
557
+ environment: 'production',
558
+ listeners: [
559
+ {
560
+ id: 'api',
561
+ listener: createCustomApiListener({
562
+ endpoint: 'https://api.example.com/events',
563
+ apiKey: process.env.ANALYTICS_API_KEY,
564
+ batchSize: 5,
565
+ }),
566
+ config: {},
567
+ },
568
+ ],
569
+ });
570
+
571
+ identify(user.id, { email: user.email });
572
+ track({ name: 'dashboard_viewed', properties: { tab: 'overview' } });
573
+ ```
574
+
575
+ **Queue mode – track before init**
576
+
577
+ ```ts
578
+ // Script runs early; init() runs later (e.g. after config load)
579
+ track({ name: 'early_click', properties: { target: 'nav' } });
580
+ identify('anonymous-session');
581
+
582
+ await init({
583
+ appId: 'my-app',
584
+ preInitBehavior: 'queue',
585
+ queueMaxSize: 100,
586
+ queueTtlMs: 5000,
587
+ listeners: [/* ... */],
588
+ });
589
+ // Queued identify then track are replayed to listeners
590
+ ```
591
+
592
+ **With onError**
593
+
594
+ ```ts
595
+ await init({
596
+ appId: 'my-app',
597
+ onError: (error, ctx) => {
598
+ if (ctx?.listenerId) {
599
+ reportToMonitoring({ listenerId: ctx.listenerId, error: error.message });
600
+ }
601
+ },
602
+ listeners: [/* ... */],
603
+ });
604
+ ```
605
+
606
+ **Logout – reset identity**
607
+
608
+ ```ts
609
+ async function logout() {
610
+ await reset();
611
+ // Redirect or clear auth state
612
+ }
613
+ ```
614
+
615
+ ---
616
+
617
+ ## Entry points
618
+
619
+ | Import | Contents |
620
+ |--------|----------|
621
+ | `@snovasys/usage-analytics-sdk` | Core: `init`, `track`, `identify`, `flush`, `reset`, `isInitialized`, and types. |
622
+ | `@snovasys/usage-analytics-sdk/noop` | `createNoopListener()`, `noopListener`, and types. |
623
+ | `@snovasys/usage-analytics-sdk/custom-api` | `createCustomApiListener(config)`, `CustomApiListenerConfig`, and types. |
624
+
625
+ Only import the entry points you use so bundlers can tree-shake the rest.
626
+
627
+ ---
628
+
629
+ ## API reference and listener authoring
630
+
631
+ - **[API reference](docs/api.md)** – All methods and types.
632
+ - **[Listener authoring](docs/listener-authoring.md)** – Implement your own listener (e.g. Clarity, GA) and ship it as a separate package.
633
+
634
+ ---
635
+
636
+ ## License
637
+
638
+ MIT