autotel-web 1.1.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,921 @@
1
+ # autotel-web
2
+
3
+ Ultra-lightweight browser SDK for distributed tracing (**1.6KB gzipped**)
4
+
5
+ **Purpose:** Enable distributed tracing between browser and backend applications. The browser propagates W3C `traceparent` headers, and your backend (using [Autotel](../autotel)) automatically continues the trace.
6
+
7
+ **Core Philosophy:** The backend does all the real tracing — timing, spans, errors, export — while the browser only propagates the trace context via headers.
8
+
9
+ **No OpenTelemetry dependencies. No exporters. No collectors. No CORS. Just header injection.**
10
+
11
+ ```
12
+ ┌─────────┐ traceparent ┌─────────┐ spans ┌───────────┐
13
+ │ Browser │ -----------> │ Backend │ -------> │ Collector │
14
+ │ 1.6KB │ header │ (OTel) │ export │ (Datadog) │
15
+ └─────────┘ └─────────┘ └───────────┘
16
+ ```
17
+
18
+ ## Features
19
+
20
+ ✅ **Tiny bundle** - **1.6KB gzipped** (33x smaller than full OTel browser SDK)
21
+ ✅ **Zero dependencies** - No `@opentelemetry/*` packages needed
22
+ ✅ **W3C trace propagation** - Automatic `traceparent` header injection on fetch/XHR
23
+ ✅ **SSR-safe** - Works with Next.js, Remix, and other SSR frameworks
24
+ ✅ **Framework-agnostic** - Works with React, Vue, Svelte, Angular, vanilla JS
25
+ ✅ **No real spans** - Browser just propagates context, backend does real tracing
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ npm install autotel-web
31
+ # or
32
+ pnpm add autotel-web
33
+ # or
34
+ yarn add autotel-web
35
+ ```
36
+
37
+ **Important:** You do NOT need to install any `@opentelemetry/*` packages. This package has **zero dependencies**.
38
+
39
+ ## Quick Start
40
+
41
+ ### 1. Initialize in Browser
42
+
43
+ ```typescript
44
+ import { init } from 'autotel-web'
45
+
46
+ // Call once, client-side only
47
+ init({ service: 'my-frontend-app' })
48
+
49
+ // That's it! All fetch/XHR calls now include traceparent headers
50
+ fetch('/api/users') // <-- traceparent header automatically injected!
51
+ ```
52
+
53
+ ### 2. Backend Receives Trace
54
+
55
+ Your backend using Autotel automatically extracts the `traceparent` header and continues the trace:
56
+
57
+ ```typescript
58
+ // Backend (Express + Autotel)
59
+ import { init, trace } from 'autotel'
60
+
61
+ init({
62
+ service: 'my-api',
63
+ endpoint: 'http://localhost:4318' // Your OTel collector
64
+ })
65
+
66
+ app.get('/api/users', async (req, res) => {
67
+ // Autotel automatically extracts traceparent from req.headers
68
+ // and creates a child span
69
+ const users = await trace(async () => {
70
+ return db.users.findAll()
71
+ })()
72
+
73
+ res.json(users)
74
+ })
75
+ ```
76
+
77
+ ### 3. View Distributed Trace
78
+
79
+ Open your observability platform (Honeycomb, Datadog, Jaeger, etc.) and see the complete trace from browser → backend → database!
80
+
81
+ ## Framework Integration
82
+
83
+ ### React (Client-Only)
84
+
85
+ ```typescript
86
+ // src/App.tsx
87
+ import { useEffect } from 'react'
88
+ import { init } from 'autotel-web'
89
+
90
+ function App() {
91
+ useEffect(() => {
92
+ init({ service: 'my-react-app' })
93
+ }, [])
94
+
95
+ return <div>Your app</div>
96
+ }
97
+ ```
98
+
99
+ ### Next.js App Router (SSR-Safe)
100
+
101
+ ```typescript
102
+ // app/telemetry-init.tsx (Client Component)
103
+ 'use client'
104
+
105
+ import { useEffect } from 'react'
106
+ import { init } from 'autotel-web'
107
+
108
+ export function TelemetryInit() {
109
+ useEffect(() => {
110
+ init({ service: 'my-nextjs-app' })
111
+ }, [])
112
+
113
+ return null
114
+ }
115
+
116
+ // app/layout.tsx
117
+ import { TelemetryInit } from './telemetry-init'
118
+
119
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
120
+ return (
121
+ <html lang="en">
122
+ <body>
123
+ <TelemetryInit />
124
+ {children}
125
+ </body>
126
+ </html>
127
+ )
128
+ }
129
+ ```
130
+
131
+ ### Next.js Pages Router
132
+
133
+ ```typescript
134
+ // pages/_app.tsx
135
+ import { useEffect } from 'react'
136
+ import { init } from 'autotel-web'
137
+ import type { AppProps } from 'next/app'
138
+
139
+ export default function App({ Component, pageProps }: AppProps) {
140
+ useEffect(() => {
141
+ init({ service: 'my-nextjs-app' })
142
+ }, [])
143
+
144
+ return <Component {...pageProps} />
145
+ }
146
+ ```
147
+
148
+ ### Remix
149
+
150
+ ```typescript
151
+ // app/entry.client.tsx
152
+ import { RemixBrowser } from '@remix-run/react'
153
+ import { startTransition, StrictMode } from 'react'
154
+ import { hydrateRoot } from 'react-dom/client'
155
+ import { init } from 'autotel-web'
156
+
157
+ // Initialize before hydration
158
+ init({ service: 'my-remix-app' })
159
+
160
+ startTransition(() => {
161
+ hydrateRoot(
162
+ document,
163
+ <StrictMode>
164
+ <RemixBrowser />
165
+ </StrictMode>
166
+ )
167
+ })
168
+ ```
169
+
170
+ ### Vue
171
+
172
+ ```typescript
173
+ // src/main.ts
174
+ import { createApp } from 'vue'
175
+ import { init } from 'autotel-web'
176
+ import App from './App.vue'
177
+
178
+ init({ service: 'my-vue-app' })
179
+
180
+ createApp(App).mount('#app')
181
+ ```
182
+
183
+ ### Vanilla JavaScript
184
+
185
+ ```html
186
+ <!-- index.html -->
187
+ <script type="module">
188
+ import { init } from 'autotel-web'
189
+
190
+ init({ service: 'my-vanilla-app' })
191
+
192
+ // Now all fetch calls include traceparent headers
193
+ fetch('/api/data')
194
+ .then(res => res.json())
195
+ .then(data => console.log(data))
196
+ </script>
197
+ ```
198
+
199
+ ## W3C Trace Context Propagation
200
+
201
+ autotel-web **implements the W3C Trace Context format directly**, without pulling in the OpenTelemetry propagator. It generates and injects `traceparent` headers on all outgoing HTTP requests using native browser APIs (`crypto.getRandomValues()`).
202
+
203
+ ### Header Format
204
+
205
+ ```
206
+ traceparent: 00-{trace-id}-{span-id}-{trace-flags}
207
+ ```
208
+
209
+ **Example:**
210
+ ```
211
+ traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
212
+ ```
213
+
214
+ - `00` - Version
215
+ - `4bf92f3577b34da6a3ce929d0e0e4736` - Trace ID (128-bit hex)
216
+ - `00f067aa0ba902b7` - Span ID (64-bit hex)
217
+ - `01` - Trace flags (sampled=1)
218
+
219
+ ### Verification
220
+
221
+ You can verify the header is being sent using browser DevTools:
222
+
223
+ 1. Open DevTools → Network tab
224
+ 2. Make a fetch/XHR request
225
+ 3. Check Request Headers
226
+ 4. Look for `traceparent` header
227
+
228
+ ## Backend Integration
229
+
230
+ ### Automatic Extraction (Express)
231
+
232
+ Autotel automatically extracts `traceparent` from incoming requests:
233
+
234
+ ```typescript
235
+ import express from 'express'
236
+ import { init, trace } from 'autotel'
237
+
238
+ init({
239
+ service: 'my-api',
240
+ endpoint: 'http://localhost:4318'
241
+ })
242
+
243
+ const app = express()
244
+
245
+ app.get('/api/users/:id', async (req, res) => {
246
+ // Parent context is automatically extracted from req.headers.traceparent
247
+ const user = await trace(async () => {
248
+ return db.users.findById(req.params.id)
249
+ })()
250
+
251
+ res.json(user)
252
+ })
253
+ ```
254
+
255
+ ### Manual Extraction (Next.js API Routes)
256
+
257
+ For frameworks where automatic extraction doesn't work, use `extractTraceContext`:
258
+
259
+ ```typescript
260
+ // app/api/users/route.ts (Next.js App Router)
261
+ import { init } from 'autotel'
262
+ import { context, trace as otelTrace } from '@opentelemetry/api'
263
+ import { W3CTraceContextPropagator } from '@opentelemetry/core'
264
+
265
+ init({ service: 'my-api', endpoint: 'http://localhost:4318' })
266
+
267
+ const propagator = new W3CTraceContextPropagator()
268
+
269
+ export async function GET(request: Request) {
270
+ // Extract parent context from headers
271
+ const parentContext = propagator.extract(
272
+ context.active(),
273
+ request.headers,
274
+ {
275
+ get: (headers, key) => headers.get(key) ?? undefined,
276
+ keys: (headers) => Array.from(headers.keys()),
277
+ }
278
+ )
279
+
280
+ // Run in extracted context
281
+ return context.with(parentContext, async () => {
282
+ const tracer = otelTrace.getTracer('my-api')
283
+
284
+ return tracer.startActiveSpan('fetchUsers', async (span) => {
285
+ try {
286
+ const users = await db.users.findAll()
287
+ span.end()
288
+ return Response.json(users)
289
+ } catch (error) {
290
+ span.recordException(error)
291
+ span.end()
292
+ throw error
293
+ }
294
+ })
295
+ })
296
+ }
297
+ ```
298
+
299
+ ## API Reference
300
+
301
+ ### `init(config)`
302
+
303
+ Initialize the browser SDK. Call once, client-side only.
304
+
305
+ ```typescript
306
+ interface AutotelWebConfig {
307
+ /** Service name for the browser application */
308
+ service: string
309
+
310
+ /** Enable fetch instrumentation (default: true) */
311
+ instrumentFetch?: boolean
312
+
313
+ /** Enable XMLHttpRequest instrumentation (default: true) */
314
+ instrumentXHR?: boolean
315
+
316
+ /** Enable debug logging (default: false) */
317
+ debug?: boolean
318
+
319
+ /** Privacy controls for traceparent header injection */
320
+ privacy?: PrivacyConfig
321
+ }
322
+
323
+ interface PrivacyConfig {
324
+ /** Only inject traceparent on these origins (whitelist) */
325
+ allowedOrigins?: string[]
326
+
327
+ /** Never inject traceparent on these origins (blacklist) */
328
+ blockedOrigins?: string[]
329
+
330
+ /** Respect Do Not Track browser setting */
331
+ respectDoNotTrack?: boolean
332
+
333
+ /** Respect Global Privacy Control signal */
334
+ respectGPC?: boolean
335
+ }
336
+ ```
337
+
338
+ **Example:**
339
+
340
+ ```typescript
341
+ init({
342
+ service: 'my-spa',
343
+ debug: false,
344
+ privacy: {
345
+ allowedOrigins: ['api.myapp.com'],
346
+ respectDoNotTrack: true
347
+ }
348
+ })
349
+ ```
350
+
351
+ ### `trace(fn)` and `trace(ctx => fn)`
352
+
353
+ Wrap functions with automatic tracing.
354
+
355
+ **Direct Pattern (no context access):**
356
+
357
+ ```typescript
358
+ import { trace } from 'autotel-web'
359
+
360
+ export const fetchUser = trace(async (id: string) => {
361
+ const response = await fetch(`/api/users/${id}`)
362
+ return response.json()
363
+ })
364
+
365
+ // Usage
366
+ const user = await fetchUser('123')
367
+ ```
368
+
369
+ **Factory Pattern (with context access):**
370
+
371
+ ```typescript
372
+ export const fetchUser = trace(ctx => async (id: string) => {
373
+ ctx.setAttribute('user.id', id)
374
+
375
+ const response = await fetch(`/api/users/${id}`)
376
+ const user = await response.json()
377
+
378
+ ctx.setAttribute('user.email', user.email)
379
+ return user
380
+ })
381
+
382
+ // Usage
383
+ const user = await fetchUser('123')
384
+ ```
385
+
386
+ ### `span(name, fn)`
387
+
388
+ Create a manual span for a block of code:
389
+
390
+ ```typescript
391
+ import { span } from 'autotel-web'
392
+
393
+ const result = await span('processData', async (ctx) => {
394
+ ctx.setAttribute('data.size', data.length)
395
+ return await processData(data)
396
+ })
397
+ ```
398
+
399
+ ### `getActiveContext()`
400
+
401
+ Get the current active trace context:
402
+
403
+ ```typescript
404
+ import { getActiveContext } from 'autotel-web'
405
+
406
+ const ctx = getActiveContext()
407
+ if (ctx) {
408
+ console.log('Trace ID:', ctx.traceId)
409
+ console.log('Span ID:', ctx.spanId)
410
+ }
411
+ ```
412
+
413
+ ## Privacy Controls
414
+
415
+ autotel-web includes built-in privacy controls to ensure compliance with GDPR, CCPA, and other privacy regulations. Control which origins receive `traceparent` headers and respect user privacy preferences.
416
+
417
+ ### Privacy Configuration
418
+
419
+ ```typescript
420
+ interface PrivacyConfig {
421
+ /** Only inject traceparent on these origins (whitelist) */
422
+ allowedOrigins?: string[]
423
+
424
+ /** Never inject traceparent on these origins (blacklist) */
425
+ blockedOrigins?: string[]
426
+
427
+ /** Respect Do Not Track browser setting */
428
+ respectDoNotTrack?: boolean
429
+
430
+ /** Respect Global Privacy Control signal */
431
+ respectGPC?: boolean
432
+ }
433
+ ```
434
+
435
+ ### Example: Restrict to First-Party APIs
436
+
437
+ Only inject `traceparent` on your own API endpoints:
438
+
439
+ ```typescript
440
+ init({
441
+ service: 'my-app',
442
+ privacy: {
443
+ allowedOrigins: ['api.myapp.com', 'myapp.com']
444
+ }
445
+ })
446
+
447
+ // ✅ Injects traceparent
448
+ fetch('https://api.myapp.com/users')
449
+
450
+ // ❌ Does NOT inject traceparent (not in allowlist)
451
+ fetch('https://external-api.com/data')
452
+ ```
453
+
454
+ ### Example: Block Third-Party Analytics
455
+
456
+ Block `traceparent` injection on analytics and tracking domains:
457
+
458
+ ```typescript
459
+ init({
460
+ service: 'my-app',
461
+ privacy: {
462
+ blockedOrigins: [
463
+ 'analytics.google.com',
464
+ 'facebook.com',
465
+ 'mixpanel.com',
466
+ 'segment.io'
467
+ ]
468
+ }
469
+ })
470
+
471
+ // ✅ Injects traceparent (not blocked)
472
+ fetch('https://api.myapp.com/users')
473
+
474
+ // ❌ Does NOT inject traceparent (blocked)
475
+ fetch('https://analytics.google.com/collect')
476
+ ```
477
+
478
+ ### Example: Respect User Privacy Signals
479
+
480
+ Respect Do Not Track (DNT) and Global Privacy Control (GPC):
481
+
482
+ ```typescript
483
+ init({
484
+ service: 'my-app',
485
+ privacy: {
486
+ respectDoNotTrack: true, // Disable tracing if user has DNT enabled
487
+ respectGPC: true // Disable tracing if user has GPC enabled
488
+ }
489
+ })
490
+
491
+ // If user has DNT or GPC enabled:
492
+ // ❌ NO traceparent headers injected on ANY requests
493
+ ```
494
+
495
+ ### Example: Combined Privacy Rules
496
+
497
+ Combine multiple privacy controls for fine-grained control:
498
+
499
+ ```typescript
500
+ init({
501
+ service: 'my-app',
502
+ privacy: {
503
+ // Only inject on these origins
504
+ allowedOrigins: ['myapp.com', 'api.myapp.com'],
505
+
506
+ // BUT never inject on these (even if in allowlist)
507
+ blockedOrigins: ['analytics.myapp.com'],
508
+
509
+ // AND respect user's privacy preferences
510
+ respectDoNotTrack: true,
511
+ respectGPC: true
512
+ }
513
+ })
514
+ ```
515
+
516
+ ### Decision Priority
517
+
518
+ Privacy checks follow this order:
519
+
520
+ 1. **Do Not Track** - If enabled and `respectDoNotTrack: true`, block ALL injection
521
+ 2. **Global Privacy Control** - If enabled and `respectGPC: true`, block ALL injection
522
+ 3. **Blocklist** - If origin matches `blockedOrigins`, block injection
523
+ 4. **Allowlist** - If `allowedOrigins` is set, ONLY allow those origins
524
+ 5. **Default** - Allow injection (backward compatible)
525
+
526
+ ### Origin Matching
527
+
528
+ Origins are matched using **substring matching** for flexibility:
529
+
530
+ ```typescript
531
+ init({
532
+ privacy: {
533
+ allowedOrigins: ['myapp.com']
534
+ }
535
+ })
536
+
537
+ // ✅ Matches (contains "myapp.com")
538
+ fetch('https://myapp.com/api')
539
+ fetch('https://api.myapp.com/users')
540
+ fetch('https://admin.myapp.com/dashboard')
541
+
542
+ // ❌ Does NOT match
543
+ fetch('https://otherapp.com/api')
544
+ ```
545
+
546
+ **Case-insensitive:** Origins are normalized to lowercase before matching.
547
+
548
+ ### GDPR & CCPA Compliance
549
+
550
+ When handling EU or California users, consider these configurations:
551
+
552
+ **Strict Compliance (Recommended):**
553
+ ```typescript
554
+ init({
555
+ service: 'my-app',
556
+ privacy: {
557
+ allowedOrigins: ['myapp.com'], // First-party only
558
+ respectDoNotTrack: true, // Honor DNT
559
+ respectGPC: true // Honor GPC
560
+ }
561
+ })
562
+ ```
563
+
564
+ **Balanced Approach:**
565
+ ```typescript
566
+ init({
567
+ service: 'my-app',
568
+ privacy: {
569
+ blockedOrigins: [
570
+ 'analytics.google.com',
571
+ 'facebook.com',
572
+ 'doubleclick.net'
573
+ ],
574
+ respectGPC: true // Respect explicit privacy request
575
+ }
576
+ })
577
+ ```
578
+
579
+ ### Debug Logging
580
+
581
+ Enable debug logging to see privacy decisions:
582
+
583
+ ```typescript
584
+ init({
585
+ service: 'my-app',
586
+ debug: true, // <-- Enable debug logging
587
+ privacy: {
588
+ blockedOrigins: ['analytics.google.com']
589
+ }
590
+ })
591
+
592
+ // Console output:
593
+ // [autotel-web] Initialized successfully { service: 'my-app', privacyEnabled: true, ... }
594
+ // [autotel-web] Skipped traceparent on fetch (privacy): https://analytics.google.com/collect Origin is in blockedOrigins list
595
+ // [autotel-web] Injected traceparent on fetch: https://api.myapp.com/users 00-4bf92f...
596
+ ```
597
+
598
+ ### Troubleshooting Privacy Issues
599
+
600
+ **Headers not being injected when expected:**
601
+
602
+ 1. Check if DNT or GPC is enabled in your browser
603
+ 2. Verify origin is in `allowedOrigins` (if configured)
604
+ 3. Verify origin is NOT in `blockedOrigins`
605
+ 4. Enable debug logging to see decision reasons
606
+
607
+ **Headers still being injected when blocked:**
608
+
609
+ 1. Ensure privacy config is passed correctly to `init()`
610
+ 2. Check that `init()` was only called once (subsequent calls are ignored)
611
+ 3. Verify origin matching is correct (case-insensitive substring matching)
612
+
613
+ **Testing Privacy Controls:**
614
+
615
+ ```typescript
616
+ // For unit tests, you can access the privacy manager
617
+ import { getPrivacyManager } from 'autotel-web'
618
+
619
+ const manager = getPrivacyManager()
620
+ if (manager) {
621
+ const shouldInject = manager.shouldInjectTraceparent('https://api.myapp.com')
622
+ console.log('Should inject:', shouldInject)
623
+ }
624
+ ```
625
+
626
+ **Checking Browser Privacy Settings:**
627
+
628
+ ```javascript
629
+ // Check if Do Not Track is enabled
630
+ console.log('DNT:', navigator.doNotTrack) // '1' = enabled, '0' = disabled
631
+
632
+ // Check if Global Privacy Control is enabled
633
+ console.log('GPC:', navigator.globalPrivacyControl) // true/false/undefined
634
+ ```
635
+
636
+ ### Advanced: Custom Privacy Logic
637
+
638
+ For advanced use cases, you can import and use the `PrivacyManager` directly:
639
+
640
+ ```typescript
641
+ import { PrivacyManager } from 'autotel-web/privacy'
642
+
643
+ const manager = new PrivacyManager({
644
+ allowedOrigins: ['myapp.com'],
645
+ respectDoNotTrack: true
646
+ })
647
+
648
+ // Check if injection should happen for a specific URL
649
+ const shouldInject = manager.shouldInjectTraceparent('https://api.myapp.com/users')
650
+ console.log('Should inject:', shouldInject)
651
+
652
+ // Get denial reason (for debugging)
653
+ import { getDenialReason } from 'autotel-web/privacy'
654
+ const reason = getDenialReason(manager, 'https://blocked.com/api')
655
+ console.log('Denial reason:', reason)
656
+ // Output: "Origin https://blocked.com is not in allowedOrigins list"
657
+ ```
658
+
659
+ ## Using with Other SDKs
660
+
661
+ ### Sentry
662
+
663
+ autotel-web and Sentry can coexist. Both will instrument fetch/XHR.
664
+
665
+ **Recommendation:** Initialize Sentry first, then autotel-web.
666
+
667
+ ```typescript
668
+ import * as Sentry from '@sentry/browser'
669
+ import { init } from 'autotel-web'
670
+
671
+ // 1. Initialize Sentry first
672
+ Sentry.init({
673
+ dsn: 'YOUR_SENTRY_DSN',
674
+ tracesSampleRate: 1.0,
675
+ })
676
+
677
+ // 2. Then initialize autotel-web
678
+ init({ service: 'my-app' })
679
+ ```
680
+
681
+ Sentry's instrumentation typically preserves existing `traceparent` headers, so both should work together.
682
+
683
+ ### Datadog RUM
684
+
685
+ Similar to Sentry, initialize Datadog RUM first:
686
+
687
+ ```typescript
688
+ import { datadogRum } from '@datadog/browser-rum'
689
+ import { init } from 'autotel-web'
690
+
691
+ // 1. Initialize Datadog RUM first
692
+ datadogRum.init({
693
+ applicationId: 'YOUR_APP_ID',
694
+ clientToken: 'YOUR_CLIENT_TOKEN',
695
+ site: 'datadoghq.com',
696
+ service: 'my-app',
697
+ sessionSampleRate: 100,
698
+ sessionReplaySampleRate: 100,
699
+ trackUserInteractions: true,
700
+ trackResources: true,
701
+ trackLongTasks: true,
702
+ })
703
+
704
+ // 2. Then initialize autotel-web
705
+ init({ service: 'my-app' })
706
+ ```
707
+
708
+ ### Conflicts
709
+
710
+ If you experience conflicts (e.g., duplicate instrumentation or missing headers):
711
+
712
+ **Option 1:** Choose one SDK for distributed tracing
713
+ - For full RUM (errors, session replay, performance): Use vendor SDK only
714
+ - For distributed tracing only: Use autotel-web only
715
+
716
+ **Option 2:** Disable fetch/XHR instrumentation in autotel-web:
717
+
718
+ ```typescript
719
+ init({
720
+ service: 'my-app',
721
+ instrumentFetch: false,
722
+ instrumentXHR: false
723
+ })
724
+ ```
725
+
726
+ Then manually inject `traceparent` headers:
727
+
728
+ ```typescript
729
+ import { getActiveContext } from 'autotel-web'
730
+
731
+ const ctx = getActiveContext()
732
+ if (ctx) {
733
+ fetch('/api/data', {
734
+ headers: {
735
+ traceparent: `00-${ctx.traceId}-${ctx.spanId}-01`
736
+ }
737
+ })
738
+ }
739
+ ```
740
+
741
+ ## SSR Safety
742
+
743
+ autotel-web is **SSR-safe** by design. All browser APIs (WebTracerProvider, ZoneContextManager) are accessed inside `init()`, not at module load time.
744
+
745
+ ### Safe: ✅
746
+
747
+ ```typescript
748
+ // ✅ Safe: init() called in useEffect (client-side only)
749
+ useEffect(() => {
750
+ init({ service: 'my-app' })
751
+ }, [])
752
+
753
+ // ✅ Safe: init() called in entry.client.tsx (Remix)
754
+ init({ service: 'my-app' })
755
+
756
+ // ✅ Safe: init() called in 'use client' component (Next.js)
757
+ 'use client'
758
+ init({ service: 'my-app' })
759
+ ```
760
+
761
+ ### Unsafe: ❌
762
+
763
+ ```typescript
764
+ // ❌ Unsafe: init() at module top-level
765
+ import { init } from 'autotel-web'
766
+ init({ service: 'my-app' }) // This runs during SSR!
767
+ export default function MyComponent() { ... }
768
+ ```
769
+
770
+ ## Bundle Size
771
+
772
+ - **Unminified:** 5.05KB
773
+ - **Gzipped:** **1.6KB** 🎉
774
+ - **Brotli:** ~1.4KB (typical)
775
+
776
+ **Zero dependencies.** No `@opentelemetry/*` packages. Just pure JavaScript using native `crypto.getRandomValues()`.
777
+
778
+ ## Architecture: Header-Only Approach
779
+
780
+ autotel-web takes a **minimalist approach** to browser tracing:
781
+
782
+ ### What it DOES:
783
+ ✅ Generates W3C `traceparent` headers (`00-{traceId}-{spanId}-01`)
784
+ ✅ Automatically injects headers on fetch/XHR calls
785
+ ✅ Provides a nice DX with `trace()` wrappers
786
+
787
+ ### What it DOESN'T do:
788
+ ❌ Create real spans in the browser
789
+ ❌ Measure timing/duration
790
+ ❌ Export to collectors
791
+ ❌ Use OpenTelemetry SDKs
792
+
793
+ ### Why?
794
+
795
+ The browser's job is **trace propagation only**. Your backend (using Autotel) receives the `traceparent` header and creates the real spans with timing, errors, and full context.
796
+
797
+ This approach:
798
+ - Keeps bundle size tiny (1.6KB vs 55KB for full OTel)
799
+ - Avoids CORS issues (no exporter endpoints)
800
+ - Eliminates Zone.js conflicts (Angular, etc.)
801
+ - Simplifies maintenance (no OTel version updates)
802
+
803
+ The backend does all the real work, which is where you want detailed telemetry anyway!
804
+
805
+ ## Why Not Use OpenTelemetry in the Browser?
806
+
807
+ The official OpenTelemetry browser SDK (`@opentelemetry/sdk-trace-web`) is a **full-featured tracing implementation** with:
808
+ - Real span creation and lifecycle management
809
+ - Context propagation via Zone.js (~15KB)
810
+ - Span processors and exporters
811
+ - Automatic instrumentations
812
+ - **Result: ~55KB gzipped**
813
+
814
+ ### When to Use Full OTel Browser SDK
815
+
816
+ ✅ You need to **export spans directly from the browser** to a collector
817
+ ✅ You need **client-side performance timing** (Core Web Vitals, resource timing)
818
+ ✅ You're building a **monitoring/observability product** that requires browser-side analysis
819
+ ✅ You need **detailed client-side error tracking** with full span context
820
+
821
+ ### When to Use autotel-web (This Package)
822
+
823
+ ✅ You only need **trace correlation** between frontend and backend
824
+ ✅ Your backend **already exports to a collector** (OTLP, Datadog, etc.)
825
+ ✅ You want **minimal bundle size impact** (~1.6KB vs ~55KB)
826
+ ✅ You want to **avoid Zone.js** (conflicts with Angular, adds complexity)
827
+ ✅ You prefer **zero dependencies** and simpler maintenance
828
+
829
+ **Bottom Line:** If your backend already does tracing, you don't need full OpenTelemetry in the browser. Just propagate the trace context with autotel-web.
830
+
831
+ ## Performance Impact
832
+
833
+ autotel-web has **effectively zero performance overhead**:
834
+
835
+ ✅ **No promise wrapping** - Your async code runs unchanged
836
+ ✅ **No timer patching** - setTimeout/setInterval work normally
837
+ ✅ **No Zone.js** - No global async context tracking
838
+ ✅ **No span objects** - No memory allocation for browser spans
839
+ ✅ **Header-only** - Just adds one HTTP header per request
840
+
841
+ **What it does:**
842
+ - Patches `window.fetch` and `XMLHttpRequest.prototype.open` at initialization
843
+ - Generates a 32-byte header value using `crypto.getRandomValues()`
844
+ - Adds the header to outgoing requests
845
+
846
+ **Benchmark:**
847
+ - Header generation: ~0.01ms
848
+ - Network overhead: +45 bytes per request (traceparent header)
849
+ - Memory: ~2KB for the SDK code
850
+
851
+ **Real-world impact:** Imperceptible. The network request itself takes orders of magnitude longer than the header injection.
852
+
853
+ ## Examples
854
+
855
+ See the `apps/` directory at the repository root for complete working examples:
856
+
857
+ - **example-web-vanilla** - Simple HTML + script tag example showing traceparent header injection
858
+
859
+ More examples coming soon:
860
+ - React + Vite - Client-side React app
861
+ - Next.js - App Router with SSR
862
+ - Remix - Full-stack Remix app
863
+ - Vue - Vue 3 application
864
+
865
+ ## Troubleshooting
866
+
867
+ ### Headers not appearing
868
+
869
+ 1. Check that `init()` was called:
870
+ ```typescript
871
+ init({ service: 'my-app', debug: true }) // Enable debug logging
872
+ ```
873
+
874
+ 2. Verify in DevTools:
875
+ - Open Network tab
876
+ - Click on a request
877
+ - Check "Request Headers" for `traceparent`
878
+
879
+ 3. Ensure fetch/XHR instrumentation is enabled:
880
+ ```typescript
881
+ init({
882
+ service: 'my-app',
883
+ instrumentFetch: true, // default: true
884
+ instrumentXHR: true, // default: true
885
+ })
886
+ ```
887
+
888
+ ### Backend not receiving context
889
+
890
+ 1. Check that backend is using Autotel or OpenTelemetry
891
+ 2. Verify CORS headers allow `traceparent`:
892
+ ```javascript
893
+ // Express CORS config
894
+ app.use(cors({
895
+ exposedHeaders: ['traceparent', 'tracestate']
896
+ }))
897
+ ```
898
+
899
+ 3. For custom frameworks, manually extract context (see "Backend Integration" above)
900
+
901
+ ### TypeScript errors
902
+
903
+ Ensure you're using TypeScript 5.0+ and have `@types/node` installed:
904
+
905
+ ```bash
906
+ pnpm add -D typescript@^5.0.0 @types/node
907
+ ```
908
+
909
+ ## License
910
+
911
+ MIT © Jag Reehal
912
+
913
+ ## Related Packages
914
+
915
+ - [autotel](../autotel) - Node.js OpenTelemetry SDK
916
+ - [autotel-edge](../autotel-edge) - Edge runtime SDK (Cloudflare Workers, Vercel Edge)
917
+ - [autotel-subscribers](../autotel-subscribers) - Event subscribers (PostHog, Mixpanel, etc.)
918
+
919
+ ---
920
+
921
+ **Questions?** Open an issue at [github.com/jagreehal/autotel](https://github.com/jagreehal/autotel/issues)