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/LICENSE +21 -0
- package/README.md +921 -0
- package/dist/index.d.ts +310 -0
- package/dist/index.js +391 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -0
- package/src/functional.ts +197 -0
- package/src/index.ts +70 -0
- package/src/init.privacy.test.ts +179 -0
- package/src/init.ts +415 -0
- package/src/privacy.test.ts +404 -0
- package/src/privacy.ts +261 -0
- package/src/traceparent.test.ts +49 -0
- package/src/traceparent.ts +105 -0
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)
|