@tracelog/lib 2.9.0-rc.108.6 → 2.10.0-rc.113.17

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 CHANGED
@@ -1,15 +1,15 @@
1
1
  # TraceLog
2
2
 
3
- Lightweight web analytics library for tracking user behavior. Works standalone or with optional backend integrations.
3
+ Lightweight web analytics library for tracking user behavior. Works standalone or with the TraceLog SaaS backend.
4
4
 
5
5
  ## Features
6
6
 
7
- - **Zero-config** - Auto-captures clicks, scrolls, page views, sessions, and performance metrics
8
- - **Standalone** - No backend required; optional integrations (TraceLog SaaS, custom API)
9
- - **Privacy-first** - PII sanitization, client-side sampling, `data-tlog-ignore` attribute
10
- - **Cross-tab sessions** - BroadcastChannel sync with localStorage recovery
11
- - **Event-driven** - Subscribe via `on()`/`off()` for real-time events
12
- - **Lightweight** - Single dependency (`web-vitals`), ~62KB gzipped
7
+ - **Zero-config** Auto-captures clicks, scrolls, page views, sessions, performance metrics, and JavaScript errors
8
+ - **Standalone-first** Works without any backend; opt in to TraceLog SaaS by passing a `projectId`
9
+ - **Privacy-first** PII sanitization, client-side sampling, `data-tlog-ignore` attribute, sensitive URL param stripping
10
+ - **Cross-tab sessions** BroadcastChannel sync with localStorage + sessionStorage recovery
11
+ - **Event-driven** Subscribe via `on()` / `off()` for real-time consumption
12
+ - **Lightweight** Single dependency (`web-vitals`), ~62KB gzipped
13
13
 
14
14
  ## Live Demo
15
15
 
@@ -20,6 +20,7 @@ Lightweight web analytics library for tracking user behavior. Works standalone o
20
20
  ## Installation
21
21
 
22
22
  ### NPM (Recommended)
23
+
23
24
  ```bash
24
25
  npm install @tracelog/lib
25
26
  ```
@@ -40,6 +41,7 @@ const { sessionId } = await tracelog.init({
40
41
  ```
41
42
 
42
43
  ### CDN (Script Tag)
44
+
43
45
  ```html
44
46
  <script src="https://cdn.jsdelivr.net/npm/@tracelog/lib@latest/dist/browser/tracelog.js"></script>
45
47
  <script>
@@ -48,6 +50,7 @@ const { sessionId } = await tracelog.init({
48
50
  ```
49
51
 
50
52
  ### CDN (ES Module)
53
+
51
54
  ```html
52
55
  <script type="module">
53
56
  import { tracelog } from 'https://cdn.jsdelivr.net/npm/@tracelog/lib@latest/dist/browser/tracelog.esm.js';
@@ -55,70 +58,52 @@ const { sessionId } = await tracelog.init({
55
58
  </script>
56
59
  ```
57
60
 
61
+ ---
62
+
58
63
  ## Quick Start
59
64
 
60
65
  ### Initialization Order
61
66
 
62
- **Important:** Set up listeners and transformers **before** calling `init()` to capture all events from the start:
67
+ Set up listeners **before** calling `init()` so they receive the initial `SESSION_START` and `PAGE_VIEW` events that fire during init.
63
68
 
64
69
  ```typescript
65
- // 1. Obtain user consent FIRST (your responsibility)
66
- // See "User Consent Management" section below for implementation details
67
- const hasConsent = await getUserConsent(); // Your consent management system
68
-
69
- if (!hasConsent) {
70
- console.log('User declined tracking');
71
- return;
72
- }
70
+ // 1. Obtain user consent first (your responsibility — see "User Consent" below)
71
+ const hasConsent = await getUserConsent();
72
+ if (!hasConsent) return;
73
73
 
74
- // 2. Register event listeners SECOND (before init)
74
+ // 2. Register listeners (before init)
75
75
  tracelog.on('event', (event) => {
76
76
  console.log(event.type, event);
77
77
  });
78
78
 
79
- // 3. Configure transformers THIRD (if using custom backend)
80
- tracelog.setTransformer('beforeSend', (event) => {
81
- // Transform events before they're queued
82
- return { ...event, custom_metadata: { app: 'v1' } };
83
- });
84
-
85
- // 4. Configure custom headers FOURTH (if using custom backend with auth)
86
- tracelog.setCustomHeaders(() => ({
87
- 'Authorization': `Bearer ${getAuthToken()}`
88
- }));
79
+ // 3. Identify the user (optional can be called before or after init)
80
+ tracelog.identify('cust_123', { name: 'Maria Garcia', plan: 'pro' });
89
81
 
90
- // 5. Initialize FIFTH (starts tracking immediately)
91
- await tracelog.init({
82
+ // 4. Initialize (starts tracking immediately)
83
+ const { sessionId } = await tracelog.init({
92
84
  integrations: {
93
- custom: { collectApiUrl: 'https://api.example.com' }
85
+ tracelog: { projectId: 'your-project-id' }
94
86
  }
95
87
  });
96
88
 
97
- // 6. Identify user (after init, optional)
98
- tracelog.identify('cust_123', { name: 'Maria Garcia', plan: 'pro' });
99
-
100
- // 7. Track custom events (after init)
101
- tracelog.event('button_clicked', {
102
- buttonId: 'signup-cta',
103
- source: 'homepage'
104
- });
89
+ // 5. Track custom events at any point after init
90
+ tracelog.event('button_clicked', { buttonId: 'signup-cta', source: 'homepage' });
105
91
 
106
- // 8. On logout: reset identity
92
+ // 6. On logout: reset identity (clears identity, regenerates UUID, opens a new session)
107
93
  await tracelog.resetIdentity();
108
94
 
109
- // 9. Cleanup (on consent revoke or app unmount)
95
+ // 7. Cleanup on consent revoke or app unmount
110
96
  tracelog.destroy();
111
97
  ```
112
98
 
113
- **Why this order?** You must obtain user consent before initializing. Events like `SESSION_START` and `PAGE_VIEW` fire during initialization. Registering listeners, transformers, and custom headers before init ensures you capture, transform, and send these initial events with proper authentication.
99
+ **Auto-captured events** (no code required):
114
100
 
115
- **That's it!** TraceLog now automatically tracks:
116
- - Page views & navigation (including SPA routes)
101
+ - Page views & navigation (including SPA route changes)
117
102
  - Click interactions
118
103
  - Scroll behavior
119
104
  - User sessions
120
105
  - Web Vitals (LCP, INP, CLS, FCP, TTFB)
121
- - JavaScript errors
106
+ - JavaScript errors & unhandled promise rejections
122
107
 
123
108
  ---
124
109
 
@@ -126,51 +111,40 @@ tracelog.destroy();
126
111
 
127
112
  | Method | Description |
128
113
  |--------|-------------|
129
- | `init(config?)` | Initialize tracking (see [Configuration](#configuration)) |
130
- | `event(name, metadata?)` | Track custom events |
131
- | `updateGlobalMetadata(metadata)` | Replace all global metadata |
132
- | `mergeGlobalMetadata(metadata)` | Merge with existing global metadata |
133
- | `on(event, callback)` | Subscribe to events (`'event'` or `'queue'`) |
134
- | `off(event, callback)` | Unsubscribe from events |
135
- | `setTransformer(hook, fn)` | Transform events before sending (see [Transformers](#transformers)) |
136
- | `removeTransformer(hook)` | Remove a previously set transformer |
137
- | `setCustomHeaders(provider)` | Add custom HTTP headers to requests (see [Custom Headers](#custom-headers)) |
138
- | `removeCustomHeaders()` | Remove custom headers provider |
139
- | `identify(userId, traits?)` | Associate visitor with a known user identity |
140
- | `resetIdentity()` | Clear identity, regenerate UUID, start new session |
141
- | `isInitialized()` | Check initialization status |
142
- | `getSessionId()` | Get current session ID (or null) |
143
- | `setQaMode(enabled)` | Enable/disable QA mode (console logging) |
144
- | `destroy()` | Stop tracking and cleanup |
114
+ | `init(config?)` | Initialize tracking. Returns `Promise<{ sessionId }>`. See [Configuration](#configuration). |
115
+ | `event(name, metadata?, options?)` | Track a custom event. `options.critical: true` drains the queue via `sendBeacon` right after tracking, so the batch (the critical event + anything already queued) survives an imminent navigation. Subject to `sendBeacon`'s 64KB cap — oversized batches are persisted to `localStorage` and recovered on next `init()` via their idempotency token; the backend deduplicates by `event.id`. |
116
+ | `on(event, callback)` | Subscribe to events (`'event'` or `'queue'`). Local consumption — independent of backend sends. |
117
+ | `off(event, callback)` | Unsubscribe. Must pass the exact callback reference used in `on()`. |
118
+ | `identify(userId, traits?)` | Associate the current visitor with a known user identity. `traits` accepts a `Record<string, string>`; non-string values are dropped silently. |
119
+ | `resetIdentity()` | Flush pending events under the old identity, clear identity, regenerate the visitor UUID, and start a new session. Use for logout flows. |
120
+ | `isInitialized()` | `true` after a successful `init()`, `false` otherwise (including during teardown). |
121
+ | `getSessionId()` | Current session ID, or `null` if not initialized. |
122
+ | `getUserId()` | Current visitor UUID, or `null` if not initialized. |
123
+ | `destroy()` | Stop tracking, drain pending events via `sendBeacon`, and release all resources. |
145
124
 
146
125
  **→ [Complete API Reference](./API_REFERENCE.md)**
147
126
 
148
127
  ---
149
128
 
150
- ## User Consent Management
129
+ ## User Consent
151
130
 
152
- TraceLog requires you to obtain user consent **before** calling `init()`. The library does not include a built-in consent management system.
131
+ TraceLog does not ship a consent manager. You are responsible for obtaining consent **before** calling `init()`.
153
132
 
154
133
  ```typescript
155
- // Your responsibility: Obtain consent before initialization
156
134
  const userConsent = await showCookieBanner(); // Your consent solution
157
135
 
158
136
  if (userConsent.analytics) {
159
- // Initialize only after consent
160
137
  await tracelog.init({
161
- integrations: {
162
- tracelog: { projectId: 'your-project-id' }
163
- }
138
+ integrations: { tracelog: { projectId: 'your-project-id' } }
164
139
  });
165
140
  } else {
166
- // User rejected - don't initialize
167
- console.log('Analytics consent denied');
141
+ // User rejected don't initialize
168
142
  }
169
143
 
170
- // If user revokes consent later
144
+ // If consent is revoked later
171
145
  function handleConsentRevoke() {
172
- tracelog.destroy(); // Stop tracking immediately
173
- localStorage.clear(); // Clear stored session data
146
+ tracelog.destroy();
147
+ localStorage.clear();
174
148
  }
175
149
  ```
176
150
 
@@ -178,136 +152,76 @@ function handleConsentRevoke() {
178
152
 
179
153
  ## Configuration
180
154
 
181
- All configuration is **optional**. TraceLog works out-of-the-box with sensible defaults.
155
+ All configuration is optional. TraceLog works out-of-the-box with sensible defaults.
182
156
 
183
157
  ```typescript
184
158
  await tracelog.init({
185
159
  // Session
186
- sessionTimeout: 900000, // 15 min (default)
160
+ sessionTimeout: 900000, // 15 min (default)
187
161
 
188
162
  // Privacy
189
- samplingRate: 1.0, // 100% (default)
190
- sensitiveQueryParams: ['token'], // Add to defaults
191
-
192
- // Integrations (pick one, multiple, or none)
193
- integrations: {
194
- tracelog: { projectId: 'your-id' }, // TraceLog SaaS
195
- custom: { collectApiUrl: 'https://api.com' }, // Custom backend
196
-
197
- // Multi-integration: Send to multiple backends simultaneously
198
- // tracelog: { projectId: 'proj-123' }, // Analytics dashboard
199
- // custom: { collectApiUrl: 'https://warehouse.com' } // Data warehouse
200
- // Events sent to BOTH independently with separate error handling
201
- },
202
-
203
- // Web Vitals filtering
204
- webVitalsMode: 'needs-improvement', // 'all' | 'needs-improvement' | 'poor'
205
-
206
- // Viewport tracking (element visibility)
207
- viewport: {
208
- elements: [{ selector: '.cta', id: 'hero-cta' }],
209
- threshold: 0.5, // 50% visible
210
- minDwellTime: 1000 // 1 second
211
- }
212
- });
213
- ```
214
-
215
- **→ [Full Configuration Guide](./API_REFERENCE.md#configuration)**
216
-
217
- ---
218
-
219
- ## Automatic Event Types
220
-
221
- TraceLog captures these events automatically (no code required):
163
+ samplingRate: 1.0, // 100% (default)
164
+ errorSampling: 1.0, // 100% (default)
165
+ sensitiveQueryParams: ['token'], // Added to the 15-param default deny-list
222
166
 
223
- | Event Type | What It Tracks |
224
- |-------------------|---------------------------------------------|
225
- | `page_view` | Navigation, SPA route changes |
226
- | `click` | User interactions with elements |
227
- | `session_start` | New session creation |
228
- | `scroll` | Scroll depth, velocity, engagement |
229
- | `web_vitals` | Core Web Vitals (LCP, INP, CLS, FCP, TTFB) |
230
- | `error` | JavaScript errors, promise rejections |
231
- | `viewport_visible`| Element visibility (requires `viewport` config) |
167
+ // Throttles
168
+ pageViewThrottleMs: 1000, // Min interval between page_view events
169
+ clickThrottleMs: 300, // Min interval between click events per element
170
+ maxSameEventPerMinute: 60, // Per-name custom-event rate cap
232
171
 
233
- **Filtering Events:**
172
+ // Batch flush
173
+ sendIntervalMs: 10000, // Default batch interval
174
+ flushOnSpaNavigation: false, // Opt-in: flush after pushState / replaceState / popstate / hashchange
175
+ flushOnPageHidden: true, // Flush when document.hidden becomes true (mobile Safari coverage)
234
176
 
235
- You can filter specific events before they're sent to your backend using the `beforeSend` transformer. This gives you complete control over what data is transmitted.
177
+ // Web Vitals
178
+ webVitalsMode: 'needs-improvement', // 'all' | 'needs-improvement' | 'poor'
179
+ webVitalsThresholds: { LCP: 2500 }, // Optional per-metric overrides
236
180
 
237
- ```typescript
238
- // Filter out high-volume events (scroll, web_vitals)
239
- tracelog.setTransformer('beforeSend', (event) => {
240
- // Skip scroll and web vitals events
241
- if (['scroll', 'web_vitals'].includes(event.type)) {
242
- return null; // Returning null excludes the event from being sent
243
- }
244
- return event; // Send all other events normally
245
- });
181
+ // Global metadata appended to every event
182
+ globalMetadata: {
183
+ env: 'production',
184
+ version: '1.2.0',
185
+ appName: 'MyApp'
186
+ },
246
187
 
247
- await tracelog.init({
188
+ // Integration (omit for standalone mode)
248
189
  integrations: {
249
- custom: { collectApiUrl: 'https://api.example.com' }
250
- }
251
- });
252
- ```
253
-
254
- **Advanced Filtering:**
255
-
256
- ```typescript
257
- // Conditional filtering based on custom logic
258
- tracelog.setTransformer('beforeSend', (event) => {
259
- // Only send errors in production
260
- if (event.type === 'error' && process.env.NODE_ENV !== 'production') {
261
- return null;
262
- }
263
-
264
- // Only send 10% of scroll events (sampling)
265
- if (event.type === 'scroll' && Math.random() > 0.1) {
266
- return null;
190
+ tracelog: {
191
+ projectId: 'your-project-id',
192
+ shopify: false // Optional: enable Shopify cart attribute linking
193
+ }
267
194
  }
268
-
269
- return event;
270
195
  });
271
196
  ```
272
197
 
273
- **Multi-Integration Behavior:**
274
-
275
- ```typescript
276
- // Transformers ONLY apply to custom backends
277
- // TraceLog SaaS always receives all events unmodified
278
- tracelog.setTransformer('beforeSend', (event) => {
279
- if (['scroll', 'web_vitals'].includes(event.type)) {
280
- return null; // Filtered from custom backend only
281
- }
282
- return event;
283
- });
198
+ **→ [Full Configuration Reference](./API_REFERENCE.md#configuration)**
284
199
 
285
- await tracelog.init({
286
- integrations: {
287
- tracelog: { projectId: 'proj-123' }, // Gets ALL events (unfiltered)
288
- custom: { collectApiUrl: 'https://warehouse.com' } // Gets filtered events
289
- }
290
- });
291
- ```
200
+ ---
292
201
 
293
- **Important:** Transformers (`beforeSend`, `beforeBatch`) only apply to **custom backend integrations**. TraceLog SaaS always receives all events unmodified to maintain schema integrity and ensure complete analytics. This behavior is the same as the removed `disabledEvents` configuration.
202
+ ## Automatic Event Types
294
203
 
295
- **Use Cases:**
296
- - Reduce bandwidth and backend costs for custom backends
297
- - Already using Sentry/Datadog for errors (filter out `error` events from custom backend)
298
- - Data warehouse doesn't need scroll/vitals granularity
299
- - Minimize custom backend data volume for privacy compliance
300
- - Custom sampling logic per event type
204
+ | Event Type | What It Tracks |
205
+ |-----------------|-------------------------------------------------|
206
+ | `page_view` | Initial load, SPA route changes, hash changes |
207
+ | `click` | User interactions with elements |
208
+ | `session_start` | New session creation (server infers session end)|
209
+ | `scroll` | Depth and direction per scrollable container |
210
+ | `web_vitals` | Core Web Vitals (LCP, INP, CLS, FCP, TTFB) |
211
+ | `error` | JavaScript errors, unhandled promise rejections |
301
212
 
302
- **Note:** Filtered events are still captured locally. Use `tracelog.on('event')` to access all events client-side, even those excluded from backend transmission.
213
+ **Custom events:**
303
214
 
304
- **Custom Events:**
305
215
  ```typescript
306
216
  tracelog.event('purchase_completed', {
307
217
  orderId: 'ord-123',
308
218
  total: 99.99,
309
219
  currency: 'USD'
310
220
  });
221
+
222
+ // Right before a navigation — guarantee delivery via sendBeacon
223
+ tracelog.event('purchase_completed', { orderId: 'ord-123' }, { critical: true });
224
+ window.location.href = '/thanks';
311
225
  ```
312
226
 
313
227
  **→ [Event Types Reference](./API_REFERENCE.md#event-types)**
@@ -316,441 +230,37 @@ tracelog.event('purchase_completed', {
316
230
 
317
231
  ## Global Metadata
318
232
 
319
- Global metadata is automatically attached to **every event** sent to your backend, making it ideal for user context, environment info, or app-wide properties.
320
-
321
- ### Setting Initial Metadata
322
-
323
- Configure global metadata during initialization:
233
+ Set at `init()` time and attached to every event the library sends. Replace it by destroying and re-initializing TraceLog with a new config (typical at login / logout).
324
234
 
325
235
  ```typescript
326
236
  await tracelog.init({
327
237
  globalMetadata: {
328
238
  env: 'production',
329
239
  version: '1.2.0',
330
- appName: 'MyApp'
331
- }
332
- });
333
- ```
334
-
335
- ### Updating Metadata at Runtime
336
-
337
- **Replace all metadata** (previous keys removed):
338
-
339
- ```typescript
340
- // User login: Replace with user context
341
- tracelog.updateGlobalMetadata({
342
- userId: 'user-456',
343
- plan: 'premium',
344
- cohort: 'beta-testers'
345
- });
346
-
347
- // User logout: Clear all metadata
348
- tracelog.updateGlobalMetadata({});
349
- ```
350
-
351
- **Merge with existing metadata** (preserves other keys):
352
-
353
- ```typescript
354
- // Add user ID while preserving env and version
355
- tracelog.mergeGlobalMetadata({ userId: 'user-123' });
356
-
357
- // Update version while preserving others
358
- tracelog.mergeGlobalMetadata({ version: '1.3.0' });
359
-
360
- // Add feature flags
361
- tracelog.mergeGlobalMetadata({
362
- feature_new_ui: true,
363
- feature_dark_mode: false
364
- });
365
- ```
366
-
367
- ### Use Cases
368
-
369
- **User Authentication:**
370
- ```typescript
371
- // Login
372
- tracelog.mergeGlobalMetadata({
373
- userId: user.id,
374
- email: user.email,
375
- plan: user.subscription.plan
376
- });
377
-
378
- // Logout
379
- tracelog.updateGlobalMetadata({});
380
- ```
381
-
382
- **A/B Testing:**
383
- ```typescript
384
- tracelog.mergeGlobalMetadata({
385
- experiment_checkout: 'variant-b',
386
- experiment_pricing: 'control'
387
- });
388
- ```
389
-
390
- **Environment Context:**
391
- ```typescript
392
- tracelog.mergeGlobalMetadata({
393
- build: process.env.BUILD_NUMBER,
394
- region: user.location.region,
395
- language: navigator.language
396
- });
397
- ```
398
-
399
- ### Validation Rules
400
-
401
- - **Allowed Types**: Primitives (string, number, boolean), string arrays, nested objects (up to 10 levels)
402
- - **NOT Allowed**: Functions, symbols, undefined, circular references
403
- - **Limits**: Max 100 keys, 48KB serialized size, 500 items per array, 1000 chars per string
404
-
405
- **→ [Metadata API Reference](./API_REFERENCE.md#global-metadata)**
406
-
407
- ---
408
-
409
- ## Transformers
410
-
411
- Transform events dynamically at runtime before they're sent to integrations. Useful for adding custom logic, enrichment, or filtering.
412
-
413
- **Important**: Transformers are **integration-specific** to protect TraceLog SaaS schema integrity:
414
-
415
- | Integration | `beforeSend` | `beforeBatch` | Notes |
416
- |-------------|--------------|---------------|-------|
417
- | **Standalone (no backend)** | ✅ Applied | ⚠️ Not supported | Only local event emission; `beforeBatch` requires backend |
418
- | **TraceLog SaaS (only)** | ❌ Silently ignored | ❌ Silently ignored | Schema protection |
419
- | **Custom Backend (only)** | ✅ Applied | ✅ Applied | Full control |
420
- | **Multi-Integration** | ⚠️ Custom only | ⚠️ Custom only | SaaS gets original events, custom gets transformed |
421
-
422
- **Multi-Integration Behavior:**
423
- - When using both TraceLog SaaS + Custom backend simultaneously
424
- - SaaS receives **original events** (transformers not applied)
425
- - Custom backend receives **transformed events**
426
- - Independent error handling and retry per integration
427
-
428
- **Event Listeners and Transformers:**
429
-
430
- Event listeners (`tracelog.on('event', ...)`) receive **original events**, not transformed events. Transformers only affect data sent to backends.
431
-
432
- ```typescript
433
- tracelog.setTransformer('beforeSend', (data) => {
434
- if ('type' in data) {
435
- return { ...data, enrichedField: 'value' };
436
- }
437
- return data;
438
- });
439
-
440
- tracelog.on('event', (ev) => {
441
- console.log(ev.enrichedField); // undefined - listeners receive original events
442
- });
443
- ```
444
-
445
- **Workaround for GTM/Third-Party Relay:**
446
-
447
- If you need to forward enriched events to GTM or other systems, apply the transformation in your listener:
448
-
449
- ```typescript
450
- // Define enrichment function once
451
- const enrichEvent = (event) => ({
452
- ...event,
453
- appVersion: '1.0.0',
454
- environment: 'production'
455
- });
456
-
457
- // Use in transformer (for backend)
458
- tracelog.setTransformer('beforeSend', (data) => {
459
- if ('type' in data) {
460
- return enrichEvent(data);
461
- }
462
- return data;
463
- });
464
-
465
- // Use in listener (for GTM relay)
466
- tracelog.on('event', (event) => {
467
- const enrichedEvent = enrichEvent(event);
468
- window.dataLayer?.push({ event: 'tracelog_event', ...enrichedEvent });
469
- });
470
- ```
471
-
472
- ### Available Hooks
473
-
474
- #### `beforeSend` - Per-Event Transformation
475
-
476
- Transform individual events **before** deduplication, sampling, and queueing.
477
-
478
- **Timing (depends on integration mode):**
479
- - **Standalone mode (no backend)**: Runs in `EventManager.buildEventPayload()` before dedup/sampling/queueing
480
- - **Custom-only mode**: Runs in `EventManager.buildEventPayload()` before dedup/sampling/queueing
481
- - **Multi-integration mode (SaaS + Custom)**: Runs in `SenderManager` per-integration (SaaS skipped, Custom applied)
482
- - **TraceLog SaaS-only mode**: Silently ignored (not applied)
483
-
484
- ```typescript
485
- import { tracelog } from '@tracelog/lib';
486
- import type { EventData, EventsQueue } from '@tracelog/lib';
487
-
488
- // Add custom metadata to all events
489
- tracelog.setTransformer('beforeSend', (data: EventData | EventsQueue) => {
490
- if ('type' in data) {
491
- return {
492
- ...data,
493
- custom_event: {
494
- ...data.custom_event,
495
- metadata: {
496
- ...data.custom_event?.metadata,
497
- environment: 'production',
498
- version: '1.0.0'
499
- }
500
- }
501
- };
502
- }
503
- return data;
504
- });
505
-
506
- // Filter out sensitive events
507
- tracelog.setTransformer('beforeSend', (data) => {
508
- if ('type' in data && data.custom_event?.name === 'internal_event') {
509
- return null; // Event will be dropped
510
- }
511
- return data;
512
- });
513
- ```
514
-
515
- #### `beforeBatch` - Batch Transformation
516
-
517
- Transform the entire batch before sending to backend. Runs once per batch (every 10s or 50 events).
518
-
519
- ```typescript
520
- // Add batch-level metadata
521
- tracelog.setTransformer('beforeBatch', (data) => {
522
- if ('events' in data) {
523
- return {
524
- ...data,
525
- global_metadata: {
526
- ...data.global_metadata,
527
- batchSize: data.events.length,
528
- batchTimestamp: Date.now()
529
- }
530
- };
531
- }
532
- return data;
533
- });
534
-
535
- // Filter batch based on conditions
536
- tracelog.setTransformer('beforeBatch', (data) => {
537
- if ('events' in data && data.events.length < 5) {
538
- return null; // Don't send small batches
539
- }
540
- return data;
541
- });
542
- ```
543
-
544
- ### Removing Transformers
545
-
546
- ```typescript
547
- // Remove specific transformer
548
- tracelog.removeTransformer('beforeSend');
549
- tracelog.removeTransformer('beforeBatch');
550
- ```
551
-
552
- ### Error Handling & Validation
553
-
554
- Transformers are designed to be resilient and flexible:
555
-
556
- **Input Validation:**
557
- - **Function type check**: `setTransformer()` throws error if `fn` is not a function
558
- - **Example**: `tracelog.setTransformer('beforeSend', null)` → Throws `Error: [TraceLog] Transformer must be a function, received: object`
559
-
560
- **Error Handling:**
561
- - **Exceptions**: Caught and logged, original event/batch used
562
- - **Invalid return**: Logged warning, original event/batch used
563
- - **`null` return**: Event/batch filtered out (intended behavior)
564
-
565
- **Validation:**
566
- - **Minimal checks only**: `beforeSend` requires `'type'` field, `beforeBatch` requires `'events'` array
567
- - **Custom schemas supported**: All other fields optional for maximum flexibility with custom backends
568
- - **Use case**: Transform data to match your backend's schema (e.g., data warehouses, custom APIs)
569
-
570
- ```typescript
571
- // Safe transformer - errors won't break tracking
572
- tracelog.setTransformer('beforeSend', (data) => {
573
- try {
574
- // Complex transformation logic
575
- return transformData(data);
576
- } catch (error) {
577
- console.error('Transformer error:', error);
578
- return data; // Fallback to original
579
- }
580
- });
581
-
582
- // Custom schema example - completely reshape for your backend
583
- tracelog.setTransformer('beforeSend', (data) => {
584
- if ('type' in data) {
585
- return {
586
- type: 'analytics_event',
587
- eventName: data.custom_event?.name,
588
- timestamp: Date.now(),
589
- // Your custom fields - TraceLog won't reject this!
590
- customField1: 'value',
591
- customField2: 123
592
- };
240
+ plan: user?.plan ?? 'anonymous'
593
241
  }
594
- return data;
595
242
  });
596
243
  ```
597
244
 
598
- ### Use Cases
599
-
600
- **Data Enrichment:**
601
- ```typescript
602
- tracelog.setTransformer('beforeSend', (data) => {
603
- if ('type' in data) {
604
- return {
605
- ...data,
606
- custom_event: {
607
- ...data.custom_event,
608
- metadata: {
609
- ...data.custom_event?.metadata,
610
- userId: getCurrentUserId(),
611
- sessionContext: getSessionContext()
612
- }
613
- }
614
- };
615
- }
616
- return data;
617
- });
618
- ```
245
+ **Validation rules:**
619
246
 
620
- **Event Filtering:**
621
- ```typescript
622
- // Filter out bot traffic
623
- tracelog.setTransformer('beforeBatch', (data) => {
624
- if ('events' in data) {
625
- const filteredEvents = data.events.filter(
626
- event => !isBotUserAgent(navigator.userAgent)
627
- );
628
- return { ...data, events: filteredEvents };
629
- }
630
- return data;
631
- });
632
- ```
247
+ - **Allowed types**: primitives (string, number, boolean), string arrays, nested objects (up to 10 levels)
248
+ - **Disallowed**: functions, symbols, `undefined`, circular references
249
+ - **Limits**: max 100 keys, 48 KB serialized size, 500 items per array, 1000 chars per string
633
250
 
634
- **PII Sanitization (Custom Backend):**
635
- ```typescript
636
- // Additional sanitization for custom backend
637
- tracelog.setTransformer('beforeSend', (data) => {
638
- if ('type' in data && data.custom_event?.metadata) {
639
- const sanitized = { ...data.custom_event.metadata };
640
- delete sanitized.email;
641
- delete sanitized.phone;
642
- return {
643
- ...data,
644
- custom_event: {
645
- ...data.custom_event,
646
- metadata: sanitized
647
- }
648
- };
649
- }
650
- return data;
651
- });
652
- ```
653
-
654
- ---
655
-
656
- ## Custom Headers
657
-
658
- Add custom HTTP headers to requests sent to custom backends. Useful for authentication, tenant identification, or API versioning.
659
-
660
- **Important**: Custom headers **only apply to custom backend integrations**. TraceLog SaaS always receives requests without custom headers.
661
-
662
- ### Static Headers (Config)
663
-
664
- Set fixed headers in configuration:
665
-
666
- ```typescript
667
- await tracelog.init({
668
- integrations: {
669
- custom: {
670
- collectApiUrl: 'https://api.example.com/collect',
671
- headers: {
672
- 'X-Tenant-Id': 'tenant-123',
673
- 'X-Brand': 'my-brand',
674
- 'X-API-Version': '2.0'
675
- }
676
- }
677
- }
678
- });
679
- ```
680
-
681
- ### Dynamic Headers (Provider)
682
-
683
- Set headers dynamically at runtime (e.g., auth tokens that expire):
684
-
685
- ```typescript
686
- // Set before or after init
687
- tracelog.setCustomHeaders(() => ({
688
- 'Authorization': `Bearer ${getAuthToken()}`,
689
- 'X-Request-ID': crypto.randomUUID()
690
- }));
691
-
692
- await tracelog.init({
693
- integrations: {
694
- custom: { collectApiUrl: 'https://api.example.com/collect' }
695
- }
696
- });
697
- ```
698
-
699
- ### Static + Dynamic Headers
700
-
701
- Combine both approaches. Dynamic headers override static on key collision:
702
-
703
- ```typescript
704
- await tracelog.init({
705
- integrations: {
706
- custom: {
707
- collectApiUrl: 'https://api.example.com/collect',
708
- headers: {
709
- 'X-Brand': 'static-brand', // Static
710
- 'X-Tenant-Id': 'tenant-123' // Static
711
- }
712
- }
713
- }
714
- });
715
-
716
- // Dynamic provider overrides 'X-Brand'
717
- tracelog.setCustomHeaders(() => ({
718
- 'X-Brand': 'dynamic-brand', // Overrides static
719
- 'Authorization': 'Bearer token' // New header
720
- }));
721
-
722
- // Result: { 'X-Tenant-Id': 'tenant-123', 'X-Brand': 'dynamic-brand', 'Authorization': 'Bearer token' }
723
- ```
724
-
725
- ### Removing Headers
726
-
727
- ```typescript
728
- // Remove dynamic provider (static headers from config remain)
729
- tracelog.removeCustomHeaders();
730
- ```
731
-
732
- ### sendBeacon Limitation
733
-
734
- ⚠️ Custom headers are **NOT applied** to `sendBeacon()` requests (page unload). The browser API doesn't support custom headers. For scenarios requiring headers on all requests:
735
- - Ensure async sends complete before page unload
736
- - Use short-lived tokens that don't require refresh per request
737
-
738
- **→ [Custom Headers API Reference](./API_REFERENCE.md#setcustomheadersprovider-customheadersprovider-void)**
251
+ **→ [Metadata Reference](./API_REFERENCE.md#globalmetadata)**
739
252
 
740
253
  ---
741
254
 
742
255
  ## Integration Modes
743
256
 
744
- TraceLog supports multiple integration modes. Choose what fits your needs:
745
-
746
- ### 1. Standalone (No Backend)
257
+ ### 1. Standalone (no backend)
747
258
 
748
- **Default mode when no integrations configured.** Events captured and emitted locally without network requests.
259
+ Default when no `integrations` are configured. Events are captured, queued, and emitted locally no network requests.
749
260
 
750
261
  ```typescript
751
262
  await tracelog.init();
752
263
 
753
- // Consume events locally
754
264
  tracelog.on('event', (event) => {
755
265
  myAnalytics.track(event);
756
266
  });
@@ -760,143 +270,76 @@ tracelog.on('queue', (batch) => {
760
270
  });
761
271
  ```
762
272
 
763
- **Behavior:**
764
- - ✅ Events captured and queued normally
765
- - ✅ `beforeSend` transformer applied (per-event transformation)
766
- - ⚠️ `beforeBatch` transformer **NOT supported** (no SenderManager created)
767
- - ✅ Events emitted to local listeners every 10 seconds or 50 events
768
- - ❌ **NO network requests made** (no backends configured)
769
- - ✅ Perfect for custom analytics pipelines, testing, or privacy-focused implementations
273
+ Perfect for custom analytics pipelines, testing, or privacy-focused implementations where you want to ship events to your own destination via a listener.
770
274
 
771
275
  ### 2. TraceLog SaaS
772
- ```typescript
773
- await tracelog.init({
774
- integrations: {
775
- tracelog: { projectId: 'your-project-id' }
776
- }
777
- });
778
- ```
779
276
 
780
- ### 3. Custom Backend
781
277
  ```typescript
782
278
  await tracelog.init({
783
279
  integrations: {
784
- custom: {
785
- collectApiUrl: 'https://api.example.com/collect',
786
- allowHttp: false, // Only true for local testing
787
- fetchCredentials: 'include' // Cookie policy: 'include' | 'same-origin' | 'omit'
788
- }
280
+ tracelog: { projectId: 'your-project-id' }
789
281
  }
790
282
  });
791
283
  ```
792
284
 
793
- ### 4. Multi-Integration (TraceLog SaaS + Custom Backend)
794
- ```typescript
795
- await tracelog.init({
796
- integrations: {
797
- tracelog: { projectId: 'your-project-id' }, // Analytics dashboard
798
- custom: { collectApiUrl: 'https://warehouse.com' } // Data warehouse
799
- }
800
- });
801
-
802
- // Events sent to BOTH endpoints independently
803
- // - Independent error handling per integration
804
- // - Independent retry/persistence per integration
805
- // - Parallel sending (non-blocking)
806
- ```
285
+ **Domain requirement.** The SaaS endpoint is derived from the host page's domain (`https://{projectId}.{rootDomain}/collect`), so `init()` rejects when called from `localhost` or a raw IP address. For local development, omit `integrations.tracelog` to run in standalone mode, or test against a staging domain mapped via `/etc/hosts`.
807
286
 
808
- **→ [Integration Setup Guide](./API_REFERENCE.md#integration-configuration)**
287
+ **→ [Integration Reference](./API_REFERENCE.md#integration-configuration)**
809
288
 
810
289
  ---
811
290
 
812
291
  ## Error Handling & Reliability
813
292
 
814
- TraceLog implements intelligent error handling with automatic retries for transient failures:
293
+ ### Automatic retry strategy
815
294
 
816
- ### Automatic Retry Strategy
295
+ **Transient errors** (5xx, timeouts, network failures):
817
296
 
818
- **Transient Errors** (5xx, timeouts, network failures):
819
- - **Up to 2 retry attempts** per integration (3 total attempts)
820
- - **Exponential backoff with jitter**: 200-300ms, 400-500ms
821
- - **Independent retries** per integration (SaaS and Custom retry separately)
822
- - **Persistence after exhaustion**: Events saved to localStorage for next-page recovery
297
+ - Up to 2 retry attempts (3 total)
298
+ - Exponential backoff with jitter (200–300 ms, 400–500 ms)
299
+ - Persisted to `localStorage` after exhaustion for next-page recovery
823
300
 
824
- **Rate Limit** (429):
825
- - **No in-session retries** — arms a 60-second cooldown instead
826
- - **Cooldown is mirrored to localStorage** and shared across tabs/windows on the same origin (prevents every fresh page load from hammering the server's 429 window)
827
- - **Events persisted immediately** to localStorage; retried once the cooldown elapses
828
- - TraceLog SaaS deduplicates retries server-side; custom backends should implement idempotency
301
+ **Rate limit (429):**
829
302
 
830
- **Permanent Errors** (4xx except 408, 429):
831
- - **No retries** (immediate failure)
832
- - **Events discarded** (not persisted)
833
- - **Exception**: 408 Request Timeout is treated as transient
303
+ - No in-session retries arms a 60-second cooldown instead
304
+ - Cooldown is mirrored to `localStorage` and shared across tabs/windows on the same origin (prevents every fresh page load from hammering the server during its 429 window)
305
+ - Events are persisted immediately and retried once the cooldown elapses
306
+ - The backend deduplicates retries via the batch idempotency token
834
307
 
835
- ```typescript
836
- // Multi-backend example with automatic retries
837
- await tracelog.init({
838
- integrations: {
839
- tracelog: { projectId: 'project-id' },
840
- custom: { collectApiUrl: 'https://api.example.com/collect' }
841
- }
842
- });
843
-
844
- // If tracelog SaaS returns 500:
845
- // - Retries 2 times with backoff (200-300ms, 400-500ms)
846
- // - If all fail → persists to localStorage for next page
308
+ **Permanent errors** (4xx except 408, 429):
847
309
 
848
- // If custom backend succeeds:
849
- // - Events removed from queue (optimistic removal)
850
- // - Failed integration recovered on next page load
851
- ```
310
+ - No retries events are discarded
311
+ - 408 Request Timeout is treated as transient
852
312
 
853
- ### Error Classification
313
+ ### Error classification
854
314
 
855
- | Status Code | Type | Retries | Persistence |
856
- |-------------|------|---------|-------------|
857
- | **2xx** | Success | None | Cleared |
858
- | **4xx** (except 408, 429) | Permanent | ❌ None | ❌ Discarded |
859
- | **408** Request Timeout | Transient | ✅ Up to 2 | ✅ After exhaustion |
860
- | **429** Too Many Requests | Rate Limited (60s cooldown, shared across tabs) | ❌ None | ✅ Immediate |
861
- | **5xx** | Transient | ✅ Up to 2 | ✅ After exhaustion |
862
- | **Network Error** | Transient | ✅ Up to 2 | ✅ After exhaustion |
863
- | **Timeout** | Transient | ✅ Up to 2 | ✅ After exhaustion |
315
+ | Status | Type | Retries | Persistence |
316
+ |-----------------------|-------------|------------------|-------------------|
317
+ | **2xx** | Success | None | Cleared |
318
+ | **4xx** (except 408/429) | Permanent | ❌ None | ❌ Discarded |
319
+ | **408** | Transient | ✅ Up to 2 | ✅ After exhaustion |
320
+ | **429** | Rate Limit | None (60s cooldown, shared across tabs) | ✅ Immediate |
321
+ | **5xx** | Transient | ✅ Up to 2 | ✅ After exhaustion |
322
+ | **Network error** | Transient | ✅ Up to 2 | ✅ After exhaustion |
323
+ | **Timeout** | Transient | ✅ Up to 2 | ✅ After exhaustion |
864
324
 
865
- ### Optimistic Queue Management
325
+ ### Recovery on page load
866
326
 
867
- **Multi-Integration Behavior:**
868
- - Events removed from queue if **AT LEAST ONE** integration succeeds
869
- - Failed integrations persist independently for next-page recovery
870
- - Successful integration doesn't retry (performance optimization)
327
+ Failed events are recovered automatically on the next `init()`:
871
328
 
872
- **Example Scenario:**
873
329
  ```typescript
874
- // SaaS succeeds immediatelyno retry needed
875
- // Custom fails with 503 retries 2 times persists for recovery
876
- // Events removed from queue (SaaS succeeded)
877
- // Next page load → only Custom integration recovers persisted events
330
+ // Page 1: events fail to send (5xx after retries) persisted with idempotency token
331
+ // Page 2: init() recovers and resends; backend deduplicates by idempotency token
878
332
  ```
879
333
 
880
- ### Recovery on Page Load
334
+ Multi-tab protection: a 1-second window prevents two tabs from re-sending the same persisted batch simultaneously.
881
335
 
882
- Failed events automatically recovered on next `init()`:
883
-
884
- ```typescript
885
- // Page 1: Events fail to send (5xx error after retries)
886
- // → Persisted to localStorage per-integration
887
-
888
- // Page 2: User navigates to new page
889
- await tracelog.init({ /* same config */ });
890
- // ✅ Automatically recovers and resends persisted events
891
- // ✅ Independent recovery per integration
892
- // ✅ Multi-tab protection (1s window prevents duplicates)
893
- ```
336
+ **Circuit breaker.** After `MAX_CONSECUTIVE_NETWORK_FAILURES` consecutive network-level failures (DNS, connection refused), the sender opens its circuit and skips further requests until `CIRCUIT_BREAKER_COOLDOWN_MS` elapses. A single probe request is then allowed (half-open state) before fully closing.
894
337
 
895
338
  **→ [Full Error Handling Reference](./API_REFERENCE.md#error-handling)**
896
339
 
897
- ### Session Continuity (External Redirects)
340
+ ### Session continuity across external redirects
898
341
 
899
- TraceLog automatically preserves sessions across external redirects (payment processors, OAuth flows, etc.) with zero developer action. Session data is mirrored to `sessionStorage` alongside `localStorage`, so when a user returns from an external site and `localStorage` is empty, the session is recovered from `sessionStorage` transparently.
342
+ TraceLog preserves sessions across external redirects (payment processors, OAuth flows, etc.) with zero developer action. Session data is mirrored to `sessionStorage` alongside `localStorage`, so when a user returns from an external site and `localStorage` is empty, the session is recovered from `sessionStorage` transparently.
900
343
 
901
344
  ```typescript
902
345
  // No special handling needed before redirect
@@ -908,7 +351,7 @@ tracelog.event('purchase', { orderId: '12345', amount: 99.99 });
908
351
  // Same session as before the redirect
909
352
  ```
910
353
 
911
- - Automatic: no API calls or developer action required
354
+ - Automatic no API calls or developer action required
912
355
  - `sessionStorage` mirror survives same-tab navigation (cleared on tab close)
913
356
  - Session timeout still applies (expired sessions are not recovered)
914
357
 
@@ -916,26 +359,26 @@ tracelog.event('purchase', { orderId: '12345', amount: 99.99 });
916
359
 
917
360
  ## Privacy & Security
918
361
 
919
- TraceLog is **privacy-first** by design:
362
+ TraceLog is privacy-first by design:
920
363
 
921
- - ✅ **PII Sanitization** - Auto-redacts emails, phones, credit cards, API keys
922
- - ✅ **Input Protection** - Never captures `<input>`, `<textarea>`, `<select>` values
923
- - ✅ **URL Filtering** - Removes sensitive query params (15 defaults + custom)
924
- - ✅ **Element Exclusion** - Use `data-tlog-ignore` to exclude sensitive areas
925
- - ✅ **Client-Side Controls** - All sampling and validation happens in browser
364
+ - ✅ **PII sanitization** auto-redacts emails, phones, credit cards, IBANs, API keys, bearer tokens, and connection-string passwords from click text and error messages
365
+ - ✅ **Input protection** never captures values from `<input>`, `<textarea>`, `<select>`
366
+ - ✅ **URL filtering** removes 15 default sensitive query params (token, password, auth, secret, api_key, …) plus any you add via `sensitiveQueryParams`
367
+ - ✅ **Element exclusion** use `data-tlog-ignore` on any container to exclude its contents from click tracking
368
+ - ✅ **Client-side controls** sampling, dedup, and validation all happen in the browser
926
369
 
927
- **Example:**
928
370
  ```html
929
- <!-- Exclude sensitive forms -->
371
+ <!-- Exclude sensitive forms entirely -->
930
372
  <div data-tlog-ignore>
931
373
  <input type="password" name="password">
932
374
  <input type="text" name="credit_card">
933
375
  </div>
934
376
  ```
935
377
 
936
- **Your Responsibilities:**
937
- - Get user consent before calling `init()` (GDPR/CCPA)
938
- - Sanitize custom event metadata (avoid PII)
378
+ **Your responsibilities:**
379
+
380
+ - Obtain user consent before calling `init()` (GDPR / CCPA / LOPD)
381
+ - Avoid PII in custom event metadata (TraceLog only sanitizes element text and error messages)
939
382
  - Call `destroy()` on consent revoke
940
383
 
941
384
  **→ [Complete Security Guide](./SECURITY.md)**
@@ -944,30 +387,20 @@ TraceLog is **privacy-first** by design:
944
387
 
945
388
  ## QA Mode
946
389
 
947
- Enable QA mode for debugging and development:
390
+ QA mode logs custom events to the browser console so you can verify tracking implementation without inspecting the network tab.
948
391
 
949
- ### URL Activation
950
- ```bash
951
- # Enable
952
- ?tlog_mode=qa
392
+ ### URL activation
953
393
 
954
- # Disable
955
- ?tlog_mode=qa_off
956
- ```
957
-
958
- ### Programmatic API
959
- ```typescript
960
- tracelog.setQaMode(true); // Enable
961
- tracelog.setQaMode(false); // Disable
394
+ ```text
395
+ ?tlog_mode=qa # Enable (persists in sessionStorage for the tab)
396
+ ?tlog_mode=qa_off # Disable
962
397
  ```
963
398
 
964
- **Features:**
965
- - Custom events logged to browser console
966
- - Strict validation (throws errors instead of warnings)
967
- - Session state visible in console
968
- - Persistent across page reloads (sessionStorage)
399
+ **Effects in QA mode:**
969
400
 
970
- **→ [QA Mode Documentation](./API_REFERENCE.md#setqamodeenabled-boolean-void)**
401
+ - Custom events logged to console with their name and metadata
402
+ - Strict validation: invalid custom-event payloads throw instead of being silently dropped
403
+ - Persists across navigations within the same tab (cleared on tab close)
971
404
 
972
405
  ---
973
406
 
@@ -978,7 +411,7 @@ tracelog.setQaMode(false); // Disable
978
411
  - Safari 12+
979
412
  - Edge 79+
980
413
 
981
- **SSR/SSG Compatible:** Safe to import in Angular Universal, Next.js, Nuxt, SvelteKit (no-ops in Node.js).
414
+ **SSR/SSG compatible** safe to import in Angular Universal, Next.js, Nuxt, SvelteKit. All methods no-op in Node.js.
982
415
 
983
416
  ---
984
417
 
@@ -986,9 +419,9 @@ tracelog.setQaMode(false); // Disable
986
419
 
987
420
  ```bash
988
421
  npm install # Install dependencies
989
- npm run build:all # Build ESM + CJS + Browser bundles
422
+ npm run build:all # Build ESM + CJS + browser bundles
990
423
  npm run check # Lint + format validation
991
- npm run test # Run all tests
424
+ npm test # Run all tests
992
425
  npm run test:coverage # Generate coverage report
993
426
  ```
994
427
 
@@ -1000,10 +433,10 @@ npm run test:coverage # Generate coverage report
1000
433
 
1001
434
  | Document | Description |
1002
435
  |----------|-------------|
1003
- | **[API Reference](./API_REFERENCE.md)** | Complete API documentation with all methods, config options, and event types |
1004
- | **[Best Practices](./BEST_PRACTICES.md)** | Patterns, anti-patterns, and optimization tips |
1005
- | **[Security Guide](./SECURITY.md)** | Privacy, GDPR compliance, and security best practices |
1006
- | **[Changelog](./CHANGELOG.md)** | Release history and migration guides |
436
+ | **[API Reference](./API_REFERENCE.md)** | Complete API documentation: methods, config options, event types |
437
+ | **[Best Practices](./BEST_PRACTICES.md)** | Patterns, anti-patterns, optimization tips |
438
+ | **[Security Guide](./SECURITY.md)** | Privacy, GDPR compliance, security checklist |
439
+ | **[Changelog](./CHANGELOG.md)** | Release history |
1007
440
  | **[Handlers](./src/handlers/README.md)** | Event capture implementation details |
1008
441
  | **[Managers](./src/managers/README.md)** | Core component architecture |
1009
442