@virtu3d/event-manager 0.2.7

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Virtu3D
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,624 @@
1
+ # @fountaneengineering/event-manager
2
+
3
+ [![Version](https://img.shields.io/badge/version-0.2.6-blue.svg)](https://gitlab.com/fountaneEngineering/consutling-projects/virtu/event-manager)
4
+ [![License](https://img.shields.io/badge/license-UNLICENSED-red.svg)](LICENSE)
5
+
6
+ Lightweight event router for analytics and observability with browser support and distributed tracing.
7
+
8
+ ## Latest Release: v0.2.6
9
+
10
+ ### What's New
11
+ - **Fixed**: Mixpanel/PostHog connectors now lazy-loaded - no Node.js dependencies bundled in browser builds
12
+ - **Fixed**: Console.log statements removed from production trace interceptors
13
+ - **Fixed**: Peer dependencies updated to require OTEL SDK v2.x (prevents silent span loss)
14
+ - **Fixed**: Circular self-dependency removed from package.json
15
+ - **Improved**: Resource fallback for older OTEL SDK versions
16
+
17
+ ## Installation
18
+
19
+ ### 1. Configure npm registry
20
+
21
+ Create or update `.npmrc` in your project root:
22
+
23
+ ```
24
+ @fountaneengineering:registry=https://gitlab.com/api/v4/projects/77867432/packages/npm/
25
+ //gitlab.com/api/v4/projects/77867432/packages/npm/:_authToken=${GITLAB_TOKEN}
26
+ ```
27
+
28
+ ### 2. Install the package
29
+
30
+ ```bash
31
+ npm install @fountaneengineering/event-manager
32
+ ```
33
+
34
+ ### 3. Install peer dependencies (as needed)
35
+
36
+ **Browser environments:**
37
+ ```bash
38
+ # For OpenTelemetry distributed tracing (browser)
39
+ npm install @opentelemetry/api@^1.9.0 \
40
+ @opentelemetry/sdk-trace-web@^2.0.0 \
41
+ @opentelemetry/sdk-trace-base@^2.0.0 \
42
+ @opentelemetry/resources@^2.0.0 \
43
+ @opentelemetry/context-zone@^2.0.0 \
44
+ @opentelemetry/exporter-trace-otlp-http@^0.200.0 \
45
+ @opentelemetry/semantic-conventions@^1.0.0
46
+ ```
47
+
48
+ **Node.js environments:**
49
+ ```bash
50
+ # For OpenTelemetry distributed tracing (Node.js)
51
+ npm install @opentelemetry/api@^1.9.0 \
52
+ @opentelemetry/sdk-node@^0.200.0
53
+
54
+ # For Mixpanel analytics (Node.js only - lazy loaded)
55
+ npm install mixpanel@^0.18.0
56
+
57
+ # For PostHog analytics (Node.js only - lazy loaded)
58
+ npm install posthog-node@^3.0.0
59
+ ```
60
+
61
+ > **Note**: Mixpanel and PostHog connectors are **lazy-loaded** and only work in Node.js environments. They will not be bundled in browser builds.
62
+
63
+ ## Quick Start
64
+
65
+ ```typescript
66
+ import { TelemetryRouter, installFetchInterceptor } from '@fountaneengineering/event-manager/browser';
67
+
68
+ // Initialize telemetry
69
+ const telemetry = new TelemetryRouter({
70
+ debug: true,
71
+ providers: [
72
+ {
73
+ name: 'console',
74
+ type: 'console',
75
+ config: { pretty: true },
76
+ },
77
+ {
78
+ name: 'otel',
79
+ type: 'otel-browser',
80
+ config: {
81
+ serviceName: 'my-app',
82
+ collectorUrl: 'http://localhost:4318/v1/traces',
83
+ },
84
+ },
85
+ ],
86
+ });
87
+
88
+ // Install fetch interceptor for automatic trace propagation
89
+ installFetchInterceptor({
90
+ propagateToUrls: [/api\.example\.com/, /localhost:3000/],
91
+ });
92
+
93
+ // Track events
94
+ telemetry.track('button_click', { button_id: 'login' });
95
+
96
+ // Identify users
97
+ telemetry.identify('user-123', { email: 'user@example.com', plan: 'pro' });
98
+
99
+ // Track page views
100
+ telemetry.page('Dashboard', { section: 'analytics' });
101
+ ```
102
+
103
+ ## Features
104
+
105
+ - **Multi-provider routing** - Send events to multiple analytics/observability providers simultaneously
106
+ - **Browser-optimized** - Lightweight bundle with tree-shaking support
107
+ - **Distributed tracing** - W3C Trace Context propagation across services
108
+ - **Fetch interceptor** - Automatic trace header injection for fetch/XHR/Axios
109
+ - **Type-safe** - Full TypeScript support with comprehensive types
110
+ - **Plugin system** - Extend functionality with custom plugins
111
+ - **Zero required dependencies** - All providers are optional peer dependencies
112
+
113
+ ## Examples
114
+
115
+ ### Analytics Example
116
+
117
+ Track user behavior and product analytics with Mixpanel or PostHog:
118
+
119
+ ```typescript
120
+ import { TelemetryRouter } from '@fountaneengineering/event-manager/browser';
121
+
122
+ const analytics = new TelemetryRouter({
123
+ providers: [
124
+ {
125
+ name: 'mixpanel',
126
+ type: 'mixpanel',
127
+ config: { token: 'your-mixpanel-token' },
128
+ },
129
+ {
130
+ name: 'posthog',
131
+ type: 'posthog',
132
+ config: {
133
+ apiKey: 'your-posthog-key',
134
+ host: 'https://app.posthog.com',
135
+ },
136
+ },
137
+ ],
138
+ });
139
+
140
+ // Identify user on login
141
+ analytics.identify('user-123', {
142
+ email: 'user@example.com',
143
+ plan: 'premium',
144
+ signupDate: '2024-01-15',
145
+ });
146
+
147
+ // Track feature usage
148
+ analytics.track('feature_used', {
149
+ feature: 'export_report',
150
+ format: 'pdf',
151
+ itemCount: 50,
152
+ });
153
+
154
+ // Track conversions
155
+ analytics.track('purchase_completed', {
156
+ orderId: 'ORD-456',
157
+ amount: 99.99,
158
+ currency: 'USD',
159
+ items: ['product-a', 'product-b'],
160
+ });
161
+
162
+ // Track page views
163
+ analytics.page('Pricing', {
164
+ referrer: 'homepage',
165
+ variant: 'annual-discount',
166
+ });
167
+ ```
168
+
169
+ ### Observability Example
170
+
171
+ Distributed tracing with OpenTelemetry for debugging and performance monitoring:
172
+
173
+ ```typescript
174
+ import {
175
+ TelemetryRouter,
176
+ installFetchInterceptor,
177
+ startSpan,
178
+ getTraceHeaders,
179
+ } from '@fountaneengineering/event-manager/browser';
180
+
181
+ // Initialize with OTEL collector
182
+ const observability = new TelemetryRouter({
183
+ providers: [
184
+ {
185
+ name: 'otel',
186
+ type: 'otel-browser',
187
+ config: {
188
+ serviceName: 'frontend-app',
189
+ collectorUrl: 'https://otel-collector.example.com/v1/traces',
190
+ resourceAttributes: {
191
+ 'deployment.environment': 'production',
192
+ 'service.version': '1.2.3',
193
+ },
194
+ },
195
+ },
196
+ ],
197
+ });
198
+
199
+ // Auto-inject trace headers into API calls
200
+ installFetchInterceptor({
201
+ propagateToUrls: [/api\.example\.com/, /backend\.example\.com/],
202
+ });
203
+
204
+ // Create custom spans for complex operations
205
+ async function checkout(cartId: string) {
206
+ const span = await startSpan('checkout-flow', 'checkout-service');
207
+
208
+ try {
209
+ span.setAttribute('cart.id', cartId);
210
+
211
+ // Validate cart
212
+ const cart = await fetch(`/api/cart/${cartId}`);
213
+ span.addEvent('cart_validated');
214
+
215
+ // Process payment
216
+ const payment = await processPayment(cart);
217
+ span.setAttribute('payment.id', payment.id);
218
+ span.addEvent('payment_processed');
219
+
220
+ // Create order
221
+ const order = await createOrder(cart, payment);
222
+ span.setAttribute('order.id', order.id);
223
+
224
+ span.setStatus({ code: 1 }); // OK
225
+ return order;
226
+ } catch (error) {
227
+ span.setStatus({ code: 2, message: error.message }); // ERROR
228
+ span.recordException(error);
229
+ throw error;
230
+ } finally {
231
+ span.end();
232
+ }
233
+ }
234
+
235
+ // Track errors with trace context
236
+ window.onerror = (message, source, line, col, error) => {
237
+ observability.track('error_occurred', {
238
+ message,
239
+ source,
240
+ line,
241
+ column: col,
242
+ stack: error?.stack,
243
+ });
244
+ };
245
+ ```
246
+
247
+ ### Metrics Example
248
+
249
+ Track business and performance metrics:
250
+
251
+ ```typescript
252
+ import { TelemetryRouter } from '@fountaneengineering/event-manager/browser';
253
+
254
+ const metrics = new TelemetryRouter({
255
+ providers: [
256
+ {
257
+ name: 'console',
258
+ type: 'console',
259
+ config: { pretty: true },
260
+ },
261
+ {
262
+ name: 'otel',
263
+ type: 'otel-browser',
264
+ config: {
265
+ serviceName: 'metrics-app',
266
+ collectorUrl: 'https://otel-collector.example.com/v1/traces',
267
+ },
268
+ },
269
+ ],
270
+ });
271
+
272
+ // Performance metrics
273
+ metrics.track('page_load_time', {
274
+ metric: 'timing',
275
+ value: performance.timing.loadEventEnd - performance.timing.navigationStart,
276
+ unit: 'ms',
277
+ page: window.location.pathname,
278
+ });
279
+
280
+ // Core Web Vitals
281
+ new PerformanceObserver((list) => {
282
+ for (const entry of list.getEntries()) {
283
+ metrics.track('web_vital', {
284
+ metric: entry.name,
285
+ value: entry.value,
286
+ rating: entry.rating,
287
+ });
288
+ }
289
+ }).observe({ type: 'largest-contentful-paint', buffered: true });
290
+
291
+ // Business metrics
292
+ metrics.track('revenue', {
293
+ metric: 'counter',
294
+ value: 149.99,
295
+ currency: 'USD',
296
+ source: 'subscription',
297
+ });
298
+
299
+ metrics.track('active_users', {
300
+ metric: 'gauge',
301
+ value: 1,
302
+ segment: 'premium',
303
+ });
304
+
305
+ // API performance
306
+ async function trackedFetch(url: string) {
307
+ const start = performance.now();
308
+ try {
309
+ const response = await fetch(url);
310
+ metrics.track('api_request', {
311
+ metric: 'histogram',
312
+ url,
313
+ duration: performance.now() - start,
314
+ status: response.status,
315
+ success: response.ok,
316
+ });
317
+ return response;
318
+ } catch (error) {
319
+ metrics.track('api_request', {
320
+ metric: 'histogram',
321
+ url,
322
+ duration: performance.now() - start,
323
+ success: false,
324
+ error: error.message,
325
+ });
326
+ throw error;
327
+ }
328
+ }
329
+ ```
330
+
331
+ ## Entry Points
332
+
333
+ The package provides three entry points:
334
+
335
+ | Entry Point | Use Case |
336
+ |-------------|----------|
337
+ | `@fountaneengineering/event-manager` | Node.js / Server-side |
338
+ | `@fountaneengineering/event-manager/browser` | Browser / Frontend apps |
339
+ | `@fountaneengineering/event-manager/context` | Context utilities only |
340
+
341
+ ## Connectors
342
+
343
+ ### Console Connector
344
+
345
+ Logs events to the console. Useful for development and debugging.
346
+
347
+ ```typescript
348
+ import { TelemetryRouter } from '@fountaneengineering/event-manager/browser';
349
+
350
+ const telemetry = new TelemetryRouter({
351
+ providers: [
352
+ {
353
+ name: 'console',
354
+ type: 'console',
355
+ config: {
356
+ pretty: true, // Pretty print JSON
357
+ logLevel: 'debug', // 'debug' | 'info' | 'warn' | 'error'
358
+ },
359
+ },
360
+ ],
361
+ });
362
+ ```
363
+
364
+ ### Browser OpenTelemetry Connector
365
+
366
+ Sends traces to an OpenTelemetry collector.
367
+
368
+ ```typescript
369
+ import { TelemetryRouter } from '@fountaneengineering/event-manager/browser';
370
+
371
+ const telemetry = new TelemetryRouter({
372
+ providers: [
373
+ {
374
+ name: 'otel',
375
+ type: 'otel-browser',
376
+ config: {
377
+ serviceName: 'my-frontend',
378
+ collectorUrl: 'https://otel-collector.example.com/v1/traces',
379
+ resourceAttributes: {
380
+ 'deployment.environment': 'production',
381
+ },
382
+ },
383
+ },
384
+ ],
385
+ });
386
+ ```
387
+
388
+ ### Mixpanel Connector
389
+
390
+ ```typescript
391
+ import { TelemetryRouter } from '@fountaneengineering/event-manager';
392
+
393
+ const telemetry = new TelemetryRouter({
394
+ providers: [
395
+ {
396
+ name: 'mixpanel',
397
+ type: 'mixpanel',
398
+ config: {
399
+ token: 'your-mixpanel-token',
400
+ },
401
+ },
402
+ ],
403
+ });
404
+ ```
405
+
406
+ ### PostHog Connector
407
+
408
+ ```typescript
409
+ import { TelemetryRouter } from '@fountaneengineering/event-manager';
410
+
411
+ const telemetry = new TelemetryRouter({
412
+ providers: [
413
+ {
414
+ name: 'posthog',
415
+ type: 'posthog',
416
+ config: {
417
+ apiKey: 'your-posthog-key',
418
+ host: 'https://app.posthog.com', // or your self-hosted instance
419
+ },
420
+ },
421
+ ],
422
+ });
423
+ ```
424
+
425
+ ## Fetch Interceptor
426
+
427
+ Automatically inject trace headers into outgoing HTTP requests.
428
+
429
+ ```typescript
430
+ import { installFetchInterceptor, uninstallFetchInterceptor } from '@fountaneengineering/event-manager/browser';
431
+
432
+ // Install interceptor
433
+ installFetchInterceptor({
434
+ propagateToUrls: [
435
+ /api\.example\.com/,
436
+ /localhost:3000/,
437
+ 'https://specific-url.com/api',
438
+ ],
439
+ excludeUrls: [/analytics\.google\.com/],
440
+ });
441
+
442
+ // All fetch calls to matching URLs now include trace headers
443
+ fetch('https://api.example.com/users'); // traceparent header automatically added
444
+
445
+ // Uninstall when done
446
+ uninstallFetchInterceptor();
447
+ ```
448
+
449
+ ### XHR Interceptor
450
+
451
+ ```typescript
452
+ import { installXHRInterceptor } from '@fountaneengineering/event-manager/browser';
453
+
454
+ installXHRInterceptor({
455
+ propagateToUrls: [/api\.example\.com/],
456
+ });
457
+ ```
458
+
459
+ ## React Integration
460
+
461
+ Create a custom hook:
462
+
463
+ ```typescript
464
+ // useTelemetry.ts
465
+ import { useEffect, useRef } from 'react';
466
+ import { TelemetryRouter, installFetchInterceptor } from '@fountaneengineering/event-manager/browser';
467
+
468
+ let telemetryInstance: TelemetryRouter | null = null;
469
+
470
+ function getTelemetry(): TelemetryRouter {
471
+ if (!telemetryInstance) {
472
+ telemetryInstance = new TelemetryRouter({
473
+ debug: process.env.NODE_ENV === 'development',
474
+ providers: [
475
+ {
476
+ name: 'otel',
477
+ type: 'otel-browser',
478
+ config: {
479
+ serviceName: 'react-app',
480
+ collectorUrl: process.env.REACT_APP_OTEL_URL,
481
+ },
482
+ },
483
+ ],
484
+ });
485
+
486
+ installFetchInterceptor({
487
+ propagateToUrls: [process.env.REACT_APP_API_URL!],
488
+ });
489
+ }
490
+ return telemetryInstance;
491
+ }
492
+
493
+ export function useTelemetry() {
494
+ const telemetry = getTelemetry();
495
+
496
+ return {
497
+ track: (event: string, properties?: Record<string, unknown>) =>
498
+ telemetry.track(event, properties),
499
+ identify: (userId: string, traits?: Record<string, unknown>) =>
500
+ telemetry.identify(userId, traits),
501
+ page: (name: string, properties?: Record<string, unknown>) =>
502
+ telemetry.page(name, properties),
503
+ };
504
+ }
505
+ ```
506
+
507
+ Usage:
508
+
509
+ ```tsx
510
+ function LoginButton() {
511
+ const { track, identify } = useTelemetry();
512
+
513
+ const handleLogin = async () => {
514
+ const user = await login();
515
+ identify(user.id, { email: user.email });
516
+ track('user_login', { method: 'oauth' });
517
+ };
518
+
519
+ return <button onClick={handleLogin}>Login</button>;
520
+ }
521
+ ```
522
+
523
+ ## Axios Integration
524
+
525
+ For Axios, use the trace interceptor:
526
+
527
+ ```typescript
528
+ import axios from 'axios';
529
+ import { createAxiosTraceInterceptor } from '@fountaneengineering/event-manager/browser';
530
+
531
+ const api = axios.create({
532
+ baseURL: 'https://api.example.com',
533
+ });
534
+
535
+ // Add trace interceptor
536
+ api.interceptors.request.use(
537
+ createAxiosTraceInterceptor({
538
+ propagateToUrls: [/api\.example\.com/],
539
+ })
540
+ );
541
+ ```
542
+
543
+ ### Manual Header Injection
544
+
545
+ If you need manual control:
546
+
547
+ ```typescript
548
+ import { getTraceHeaders } from '@fountaneengineering/event-manager/browser';
549
+
550
+ async function fetchData() {
551
+ const headers = await getTraceHeaders();
552
+
553
+ const response = await fetch('/api/data', {
554
+ headers: {
555
+ 'Content-Type': 'application/json',
556
+ ...headers, // { traceparent: '00-...', tracestate: '...' }
557
+ },
558
+ });
559
+
560
+ return response.json();
561
+ }
562
+ ```
563
+
564
+ ## Custom Spans
565
+
566
+ Create custom spans for detailed tracing:
567
+
568
+ ```typescript
569
+ import { startSpan } from '@fountaneengineering/event-manager/browser';
570
+
571
+ async function processOrder(orderId: string) {
572
+ const span = await startSpan('process-order', 'order-service');
573
+
574
+ try {
575
+ span.setAttribute('order.id', orderId);
576
+
577
+ const result = await submitOrder(orderId);
578
+
579
+ span.setStatus({ code: 1 }); // SpanStatusCode.OK
580
+ return result;
581
+ } catch (error) {
582
+ span.setStatus({ code: 2, message: error.message }); // SpanStatusCode.ERROR
583
+ throw error;
584
+ } finally {
585
+ span.end();
586
+ }
587
+ }
588
+ ```
589
+
590
+ ## API Reference
591
+
592
+ ### TelemetryRouter
593
+
594
+ | Method | Description |
595
+ |--------|-------------|
596
+ | `track(event, payload?)` | Track a custom event |
597
+ | `identify(userId, traits?)` | Identify a user with optional traits |
598
+ | `page(name?, properties?)` | Track a page view |
599
+ | `registerConnector(connector)` | Register a connector instance |
600
+ | `unregisterConnector(name)` | Remove a connector |
601
+ | `addPlugin(plugin)` | Add a router plugin |
602
+ | `getConnectors()` | Get registered connector names |
603
+ | `shutdown()` | Gracefully shutdown all connectors |
604
+
605
+ ### Context Utilities
606
+
607
+ | Function | Description |
608
+ |----------|-------------|
609
+ | `getTraceHeaders()` | Get current trace headers (async) |
610
+ | `getTraceHeadersSync()` | Get trace headers synchronously |
611
+ | `extractTraceContext(headers)` | Extract trace context from headers |
612
+ | `withTraceContext(headers, fn)` | Run function within trace context |
613
+ | `startSpan(name, serviceName?)` | Create a new span |
614
+ | `generateTraceId()` | Generate a random trace ID |
615
+ | `generateSpanId()` | Generate a random span ID |
616
+
617
+ ### Interceptors
618
+
619
+ | Function | Description |
620
+ |----------|-------------|
621
+ | `installFetchInterceptor(config)` | Install global fetch interceptor |
622
+ | `uninstallFetchInterceptor()` | Remove fetch interceptor |
623
+ | `installXHRInterceptor(config)` | Install XHR interceptor |
624
+ | `createAxiosTraceInterceptor(config)` | Create Axios request interceptor |