autotel-web 1.11.0 → 1.11.2

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
@@ -85,6 +85,120 @@ app.get('/api/users', async (req, res) => {
85
85
 
86
86
  Open your observability platform (Honeycomb, Datadog, Jaeger, etc.) and see the complete trace from browser → backend → database!
87
87
 
88
+ ## Browser Span Export (lean mode)
89
+
90
+ By default, lean mode only injects `traceparent` headers — no spans are exported from the browser. This means trace UIs like Jaeger may show "missing parent span" because the browser's spanId doesn't exist in the collector.
91
+
92
+ To fix this, set the `endpoint` option. autotel-web will export a lightweight span via `navigator.sendBeacon` for each fetch, so the browser span appears in your collector as the trace root:
93
+
94
+ ```typescript
95
+ init({
96
+ service: 'my-frontend',
97
+ endpoint: '', // same-origin — requires /v1/traces proxy (see below)
98
+ })
99
+ ```
100
+
101
+ ### Collector Proxy
102
+
103
+ Browsers can't send directly to most collectors (CORS). Add a simple proxy route to your API:
104
+
105
+ **Hono:**
106
+
107
+ ```typescript
108
+ app.post('/v1/traces', async (c) => {
109
+ const body = await c.req.arrayBuffer()
110
+ await fetch(`${process.env.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/traces`, {
111
+ method: 'POST',
112
+ headers: { 'Content-Type': 'application/json' },
113
+ body,
114
+ })
115
+ return c.json({ ok: true })
116
+ })
117
+ ```
118
+
119
+ **Express:**
120
+
121
+ ```typescript
122
+ app.post('/v1/traces', express.raw({ type: 'application/json' }), async (req, res) => {
123
+ await fetch(`${process.env.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/traces`, {
124
+ method: 'POST',
125
+ headers: { 'Content-Type': 'application/json' },
126
+ body: req.body,
127
+ })
128
+ res.json({ ok: true })
129
+ })
130
+ ```
131
+
132
+ If using Vite in dev, proxy `/v1/traces` to your API:
133
+
134
+ ```typescript
135
+ // vite.config.ts
136
+ server: {
137
+ proxy: {
138
+ '/v1/traces': { target: 'http://localhost:8787', changeOrigin: true },
139
+ },
140
+ },
141
+ ```
142
+
143
+ ### PostHog Reverse Proxy
144
+
145
+ If you use PostHog for analytics, route events through the same API to bypass ad blockers (typically increases event capture by 10-30%):
146
+
147
+ **Hono:**
148
+
149
+ ```typescript
150
+ const POSTHOG_HOST = process.env.POSTHOG_HOST || 'https://eu.i.posthog.com'
151
+
152
+ app.post('/ingest/*', async (c) => {
153
+ const path = c.req.path.replace('/ingest', '')
154
+ const body = await c.req.arrayBuffer()
155
+ const resp = await fetch(`${POSTHOG_HOST}${path}`, {
156
+ method: 'POST',
157
+ headers: { 'Content-Type': c.req.header('content-type') || 'application/json' },
158
+ body,
159
+ })
160
+ return new Response(resp.body, { status: resp.status, headers: resp.headers })
161
+ })
162
+
163
+ app.get('/ingest/decide*', async (c) => {
164
+ const url = new URL(c.req.url)
165
+ const resp = await fetch(`${POSTHOG_HOST}/decide${url.search}`)
166
+ return new Response(resp.body, { status: resp.status, headers: resp.headers })
167
+ })
168
+ ```
169
+
170
+ **Browser (posthog-js):**
171
+
172
+ ```typescript
173
+ import posthog from 'posthog-js'
174
+
175
+ posthog.init('phc_your_key', {
176
+ api_host: '/ingest', // same-origin proxy
177
+ ui_host: 'https://eu.i.posthog.com', // keep toolbar working
178
+ })
179
+ ```
180
+
181
+ **Vite proxy (dev):**
182
+
183
+ ```typescript
184
+ proxy: {
185
+ '/ingest': { target: 'http://localhost:8787', changeOrigin: true },
186
+ }
187
+ ```
188
+
189
+ See [PostHog reverse proxy docs](https://posthog.com/docs/advanced/proxy) for production setups (managed proxy, Cloudflare Workers, etc.).
190
+
191
+ The resulting trace tree:
192
+
193
+ ```
194
+ browser /api/transfer (CLIENT, root) ← autotel-web
195
+ └─ POST /api/transfer (SERVER) ← autotel / autotel-hono
196
+ └─ sendMoney (INTERNAL) ← your app
197
+ ├─ validate
198
+ ├─ fetchRate
199
+ └─ ...
200
+ ```
201
+
88
202
  ## Full mode (real spans)
89
203
 
90
204
  When you need real browser spans, network timing events, and optional export from the client, use full mode. Same install: `autotel-web`. No Zone.js.
@@ -354,6 +468,14 @@ interface AutotelWebConfig {
354
468
  /** Enable debug logging (default: false) */
355
469
  debug?: boolean
356
470
 
471
+ /**
472
+ * OTLP endpoint for exporting browser spans.
473
+ * When set, a real span is sent via sendBeacon for each fetch,
474
+ * so the traceparent spanId exists in the collector.
475
+ * Use '' for same-origin (requires /v1/traces proxy).
476
+ */
477
+ endpoint?: string
478
+
357
479
  /** Privacy controls for traceparent header injection */
358
480
  privacy?: PrivacyConfig
359
481
  }
@@ -1059,6 +1181,26 @@ app.use(cors({
1059
1181
 
1060
1182
  3. For custom frameworks, manually extract context (see "Backend Integration" above)
1061
1183
 
1184
+ ### Browser spans not appearing in collector
1185
+
1186
+ If you've set `endpoint` but spans don't appear:
1187
+
1188
+ 1. Check the proxy is working: `curl -X POST http://localhost:8787/v1/traces -H 'Content-Type: application/json' -d '{}'`
1189
+ 2. Verify Vite proxy config includes `/v1/traces`
1190
+ 3. Enable `debug: true` — you should see `[autotel-web] flushSpans: sending N span(s)` in the console
1191
+
1192
+ ### Vite dev server caching stale autotel-web
1193
+
1194
+ When using `file:` linked autotel packages in development, Vite caches pre-bundled dependencies in `node_modules/.vite/`. After rebuilding autotel-web, clear this cache:
1195
+
1196
+ ```bash
1197
+ rm -rf node_modules/.vite
1198
+ # or for monorepos
1199
+ rm -rf apps/web/node_modules/.vite
1200
+ ```
1201
+
1202
+ Then restart the Vite dev server. Without this, the browser may run an old version of autotel-web even after rebuilding.
1203
+
1062
1204
  ### TypeScript errors
1063
1205
 
1064
1206
  Ensure you're using TypeScript 5.0+ and have `@types/node` installed:
package/dist/index.d.ts CHANGED
@@ -31,6 +31,13 @@ interface AutotelWebConfig {
31
31
  * @default true
32
32
  */
33
33
  instrumentXHR?: boolean;
34
+ /**
35
+ * OTLP endpoint for exporting browser spans.
36
+ * When set, browser spans are sent via sendBeacon so the traceparent
37
+ * spanId exists as a real span in the collector.
38
+ * Use '' (empty string) for same-origin (requires /v1/traces proxy).
39
+ */
40
+ endpoint?: string;
34
41
  /**
35
42
  * Privacy controls for traceparent header injection
36
43
  *
@@ -92,6 +99,8 @@ interface AutotelWebConfig {
92
99
  */
93
100
  declare function init(userConfig: AutotelWebConfig): void;
94
101
 
102
+ declare function flushSpans(): void;
103
+
95
104
  /**
96
105
  * Minimal W3C Trace Context implementation for browser
97
106
  *
@@ -150,4 +159,4 @@ declare function parseTraceparent(traceparent: string): {
150
159
  flags: string;
151
160
  } | null;
152
161
 
153
- export { type AutotelWebConfig, PrivacyConfig, createTraceparent, generateSpanId, generateTraceId, init, parseTraceparent };
162
+ export { type AutotelWebConfig, PrivacyConfig, createTraceparent, flushSpans, generateSpanId, generateTraceId, init, parseTraceparent };
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
- import { createTraceparent } from './chunk-L3S7LTIH.js';
1
+ import { createTraceparent, __publicField, parseTraceparent } from './chunk-L3S7LTIH.js';
2
2
  export { createTraceparent, extractContext, generateSpanId, generateTraceId, getActiveContext, getTraceparent, parseTraceparent, trace } from './chunk-L3S7LTIH.js';
3
3
 
4
4
  // src/privacy.ts
5
5
  var PrivacyManager = class {
6
6
  constructor(config2) {
7
- this.config = config2;
7
+ __publicField(this, "config", config2);
8
8
  }
9
9
  /**
10
10
  * Check if traceparent header should be injected for a given URL
@@ -128,6 +128,73 @@ function getDenialReason(privacyManager2, url) {
128
128
  return null;
129
129
  }
130
130
 
131
+ // src/span-exporter.ts
132
+ var debug = false;
133
+ var serviceName = "browser";
134
+ var exportEndpoint;
135
+ var pendingSpans = [];
136
+ var flushTimer;
137
+ var rawFetch;
138
+ function setRawFetch(fn) {
139
+ rawFetch = fn;
140
+ }
141
+ function configureExporter(service, endpoint, enableDebug = false) {
142
+ debug = enableDebug;
143
+ serviceName = service;
144
+ exportEndpoint = endpoint.replace(/\/$/, "");
145
+ if (!exportEndpoint.endsWith("/v1/traces")) {
146
+ exportEndpoint += "/v1/traces";
147
+ }
148
+ if (!flushTimer) {
149
+ flushTimer = setInterval(flushSpans, 2e3);
150
+ }
151
+ }
152
+ function recordSpan(traceId, spanId, name, startMs, endMs, attrs) {
153
+ if (!exportEndpoint) return;
154
+ if (debug) console.log(`[autotel-web] recordSpan: ${name} (${traceId.slice(0, 8)}\u2026)`);
155
+ const attributes = attrs ? Object.entries(attrs).map(([key, value]) => ({
156
+ key,
157
+ value: typeof value === "number" ? { intValue: String(value) } : { stringValue: value }
158
+ })) : void 0;
159
+ pendingSpans.push({
160
+ traceId,
161
+ spanId,
162
+ name,
163
+ kind: 3,
164
+ // CLIENT
165
+ startTimeUnixNano: String(Math.round(startMs * 1e6)),
166
+ endTimeUnixNano: String(Math.round(endMs * 1e6)),
167
+ attributes
168
+ });
169
+ flushSpans();
170
+ }
171
+ function flushSpans() {
172
+ if (!exportEndpoint || pendingSpans.length === 0) return;
173
+ if (debug) console.log(`[autotel-web] flushSpans: sending ${pendingSpans.length} span(s) to ${exportEndpoint}`);
174
+ const spans = pendingSpans;
175
+ pendingSpans = [];
176
+ const payload = JSON.stringify({
177
+ resourceSpans: [{
178
+ resource: { attributes: [{ key: "service.name", value: { stringValue: serviceName } }] },
179
+ scopeSpans: [{ scope: { name: "autotel-web" }, spans }]
180
+ }]
181
+ });
182
+ const blob = new Blob([payload], { type: "application/json" });
183
+ const sent = typeof navigator?.sendBeacon === "function" && navigator.sendBeacon(exportEndpoint, blob);
184
+ if (!sent && rawFetch) {
185
+ rawFetch(exportEndpoint, {
186
+ method: "POST",
187
+ headers: { "Content-Type": "application/json" },
188
+ body: payload,
189
+ keepalive: true
190
+ }).catch(() => {
191
+ });
192
+ }
193
+ }
194
+ function isConfigured() {
195
+ return exportEndpoint !== void 0;
196
+ }
197
+
131
198
  // src/init.ts
132
199
  var isInitialized = false;
133
200
  var config;
@@ -150,12 +217,21 @@ function init(userConfig) {
150
217
  if (config.privacy) {
151
218
  privacyManager = new PrivacyManager(config.privacy);
152
219
  }
220
+ if (config.endpoint !== void 0) {
221
+ setRawFetch(window.fetch.bind(window));
222
+ configureExporter(config.service, config.endpoint, config.debug);
223
+ }
153
224
  if (config.instrumentFetch !== false) {
154
225
  patchFetch();
155
226
  }
156
227
  if (config.instrumentXHR !== false) {
157
228
  patchXMLHttpRequest();
158
229
  }
230
+ if (config.endpoint !== void 0) {
231
+ window.addEventListener("visibilitychange", () => {
232
+ if (document.visibilityState === "hidden") flushSpans();
233
+ });
234
+ }
159
235
  isInitialized = true;
160
236
  if (config.debug) {
161
237
  console.log("[autotel-web] Initialized successfully", {
@@ -177,6 +253,7 @@ function patchFetch() {
177
253
  window.fetch = function(input, init2) {
178
254
  const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
179
255
  const headers = new Headers(init2?.headers);
256
+ let injectedTraceparent;
180
257
  if (!headers.has("traceparent")) {
181
258
  if (privacyManager && !privacyManager.shouldInjectTraceparent(url)) {
182
259
  if (config?.debug) {
@@ -188,17 +265,58 @@ function patchFetch() {
188
265
  );
189
266
  }
190
267
  } else {
191
- headers.set("traceparent", createTraceparent());
268
+ injectedTraceparent = createTraceparent();
269
+ headers.set("traceparent", injectedTraceparent);
192
270
  if (config?.debug) {
193
271
  console.log(
194
272
  "[autotel-web] Injected traceparent on fetch:",
195
273
  url,
196
- headers.get("traceparent")
274
+ injectedTraceparent
197
275
  );
198
276
  }
199
277
  }
200
278
  }
201
- return originalFetch(input, { ...init2, headers });
279
+ const method = init2?.method ?? (input instanceof Request ? input.method : void 0) ?? "GET";
280
+ const startTime = performance.timeOrigin + performance.now();
281
+ const fetchPromise = originalFetch(input, { ...init2, headers });
282
+ if (injectedTraceparent && isConfigured()) {
283
+ fetchPromise.then(
284
+ (response) => {
285
+ const endTime = performance.timeOrigin + performance.now();
286
+ const parsed = parseTraceparent(injectedTraceparent);
287
+ if (parsed) {
288
+ let pathname;
289
+ try {
290
+ pathname = new URL(url, window.location.origin).pathname;
291
+ } catch {
292
+ pathname = url;
293
+ }
294
+ recordSpan(parsed.traceId, parsed.spanId, `browser ${pathname}`, startTime, endTime, {
295
+ "http.method": method,
296
+ "http.url": url,
297
+ "http.status_code": response.status
298
+ });
299
+ }
300
+ },
301
+ () => {
302
+ const endTime = performance.timeOrigin + performance.now();
303
+ const parsed = parseTraceparent(injectedTraceparent);
304
+ if (parsed) {
305
+ let pathname;
306
+ try {
307
+ pathname = new URL(url, window.location.origin).pathname;
308
+ } catch {
309
+ pathname = url;
310
+ }
311
+ recordSpan(parsed.traceId, parsed.spanId, `browser ${pathname}`, startTime, endTime, {
312
+ "http.method": method,
313
+ "http.url": url
314
+ });
315
+ }
316
+ }
317
+ );
318
+ }
319
+ return fetchPromise;
202
320
  };
203
321
  }
204
322
  function patchXMLHttpRequest() {
@@ -303,6 +421,6 @@ function validateConfig(userConfig) {
303
421
  }
304
422
  }
305
423
 
306
- export { init };
424
+ export { flushSpans, init };
307
425
  //# sourceMappingURL=index.js.map
308
426
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/privacy.ts","../src/init.ts"],"names":["config","privacyManager","init"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Privacy controls for autotel-web\n *\n * Provides origin filtering and privacy signal respecting (DNT, GPC)\n * to ensure compliance with GDPR, CCPA, and user privacy preferences.\n */\n\nexport interface PrivacyConfig {\n /**\n * Only inject traceparent headers on requests to these origins (whitelist)\n *\n * If specified, traceparent will ONLY be injected on matching origins.\n * Origins are matched using substring matching (e.g., \"example.com\" matches \"https://api.example.com\").\n *\n * @example\n * ```typescript\n * {\n * allowedOrigins: ['api.myapp.com', 'myapp.com']\n * }\n * ```\n */\n allowedOrigins?: string[];\n\n /**\n * Never inject traceparent headers on requests to these origins (blacklist)\n *\n * Takes precedence over allowedOrigins.\n * Origins are matched using substring matching.\n *\n * @example\n * ```typescript\n * {\n * blockedOrigins: ['analytics.google.com', 'facebook.com']\n * }\n * ```\n */\n blockedOrigins?: string[];\n\n /**\n * Respect the Do Not Track (DNT) browser setting\n *\n * If true and user has DNT enabled, no traceparent headers will be injected.\n *\n * @default false\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Navigator/doNotTrack\n */\n respectDoNotTrack?: boolean;\n\n /**\n * Respect the Global Privacy Control (GPC) browser signal\n *\n * If true and user has GPC enabled, no traceparent headers will be injected.\n *\n * @default false\n * @see https://globalprivacycontrol.org/\n */\n respectGPC?: boolean;\n}\n\n/**\n * Manages privacy controls for traceparent header injection\n *\n * Checks user privacy preferences (DNT, GPC) and origin filtering rules\n * to determine if traceparent headers should be injected on a given request.\n */\nexport class PrivacyManager {\n constructor(private readonly config: PrivacyConfig) {}\n\n /**\n * Check if traceparent header should be injected for a given URL\n *\n * Decision order:\n * 1. Check Do Not Track (if enabled)\n * 2. Check Global Privacy Control (if enabled)\n * 3. Check blockedOrigins (explicit deny)\n * 4. Check allowedOrigins (explicit allow, if configured)\n * 5. Default: allow\n *\n * @param url - Full URL or relative path of the request\n * @returns true if traceparent should be injected, false otherwise\n */\n shouldInjectTraceparent(url: string): boolean {\n // Check Do Not Track\n if (this.config.respectDoNotTrack && this.isDoNotTrackEnabled()) {\n return false;\n }\n\n // Check Global Privacy Control\n if (this.config.respectGPC && this.isGPCEnabled()) {\n return false;\n }\n\n // Get the origin of the target URL\n const targetOrigin = this.extractOrigin(url);\n\n // Check blocklist first (explicit deny takes precedence)\n if (\n this.config.blockedOrigins &&\n this.matchesAnyOrigin(targetOrigin, this.config.blockedOrigins)\n ) {\n return false;\n }\n\n // If allowlist exists, only allow those origins\n if (this.config.allowedOrigins && this.config.allowedOrigins.length > 0) {\n return this.matchesAnyOrigin(targetOrigin, this.config.allowedOrigins);\n }\n\n // Default: allow (backward compatible behavior)\n return true;\n }\n\n /**\n * Check if Do Not Track is enabled in the browser\n */\n private isDoNotTrackEnabled(): boolean {\n if (typeof navigator === 'undefined') return false;\n\n // DNT header can be \"1\" (enabled), \"0\" (disabled), or null (not set)\n return navigator.doNotTrack === '1';\n }\n\n /**\n * Check if Global Privacy Control is enabled in the browser\n */\n private isGPCEnabled(): boolean {\n if (typeof navigator === 'undefined') return false;\n\n // GPC is a newer spec, not all browsers support it yet\n // TypeScript doesn't have types for this yet, so we cast\n const nav = navigator as Navigator & { globalPrivacyControl?: boolean };\n return nav.globalPrivacyControl === true;\n }\n\n /**\n * Extract origin from a URL (handles both absolute and relative URLs)\n *\n * @param url - Full URL or relative path\n * @returns Origin string (e.g., \"https://api.example.com\")\n */\n private extractOrigin(url: string): string {\n try {\n // Handle absolute URLs\n if (url.startsWith('http://') || url.startsWith('https://')) {\n return new URL(url).origin;\n }\n\n // Handle relative URLs - use current window location\n if (typeof window !== 'undefined') {\n return new URL(url, window.location.href).origin;\n }\n\n // Fallback for SSR or unknown cases\n return '';\n } catch {\n // Invalid URL - return empty string\n return '';\n }\n }\n\n /**\n * Check if a target origin matches any of the configured origins\n *\n * Uses substring matching for flexibility (e.g., \"example.com\" matches \"https://api.example.com\")\n *\n * @param targetOrigin - Origin to check\n * @param configuredOrigins - List of allowed or blocked origins\n * @returns true if any origin matches\n */\n private matchesAnyOrigin(\n targetOrigin: string,\n configuredOrigins: string[]\n ): boolean {\n return configuredOrigins.some((configuredOrigin) => {\n // Normalize both strings to lowercase for case-insensitive matching\n const normalizedTarget = targetOrigin.toLowerCase();\n const normalizedConfigured = configuredOrigin.toLowerCase();\n\n // Check if target origin contains the configured origin\n // This allows \"example.com\" to match \"https://api.example.com\"\n return normalizedTarget.includes(normalizedConfigured);\n });\n }\n}\n\n/**\n * Get reason why traceparent injection was denied (for debugging)\n *\n * Returns a human-readable reason if injection would be blocked,\n * or null if injection would be allowed.\n *\n * @param privacyManager - Configured PrivacyManager instance\n * @param url - URL to check\n * @returns Denial reason or null if allowed\n *\n * @example\n * ```typescript\n * const manager = new PrivacyManager({ respectDoNotTrack: true })\n * const reason = getDenialReason(manager, 'https://api.example.com')\n * if (reason) {\n * console.log('Traceparent blocked:', reason)\n * }\n * ```\n */\nexport function getDenialReason(\n privacyManager: PrivacyManager,\n url: string\n): string | null {\n // This is a helper for debugging - it re-checks the conditions\n // to provide a user-friendly reason string\n const config = (privacyManager as any).config as PrivacyConfig;\n\n // Check DNT\n if (config.respectDoNotTrack && typeof navigator !== 'undefined') {\n if (navigator.doNotTrack === '1') {\n return 'Do Not Track is enabled';\n }\n }\n\n // Check GPC\n if (config.respectGPC && typeof navigator !== 'undefined') {\n const nav = navigator as Navigator & { globalPrivacyControl?: boolean };\n if (nav.globalPrivacyControl === true) {\n return 'Global Privacy Control is enabled';\n }\n }\n\n // Extract origin\n let targetOrigin = '';\n try {\n if (url.startsWith('http://') || url.startsWith('https://')) {\n targetOrigin = new URL(url).origin;\n } else if (typeof window !== 'undefined') {\n targetOrigin = new URL(url, window.location.href).origin;\n }\n } catch {\n return 'Invalid URL';\n }\n\n // Check blocklist\n if (config.blockedOrigins) {\n const blocked = config.blockedOrigins.some((origin) =>\n targetOrigin.toLowerCase().includes(origin.toLowerCase())\n );\n if (blocked) {\n return `Origin ${targetOrigin} is in blockedOrigins list`;\n }\n }\n\n // Check allowlist\n if (config.allowedOrigins && config.allowedOrigins.length > 0) {\n const allowed = config.allowedOrigins.some((origin) =>\n targetOrigin.toLowerCase().includes(origin.toLowerCase())\n );\n if (!allowed) {\n return `Origin ${targetOrigin} is not in allowedOrigins list`;\n }\n }\n\n return null;\n}\n","/**\n * Minimal browser SDK initialization\n *\n * Patches fetch() and XMLHttpRequest to automatically inject W3C traceparent headers.\n * NO OpenTelemetry dependencies - just native browser APIs.\n *\n * Bundle size: ~2-5KB gzipped\n */\n\nimport { createTraceparent } from './traceparent';\nimport { PrivacyManager, PrivacyConfig, getDenialReason } from './privacy';\n\nexport interface AutotelWebConfig {\n /**\n * Service name for the browser application\n * Used only for logging/debugging - not sent in headers\n */\n service: string;\n\n /**\n * Enable debug logging to console\n * @default false\n */\n debug?: boolean;\n\n /**\n * Enable automatic traceparent injection on fetch calls\n * @default true\n */\n instrumentFetch?: boolean;\n\n /**\n * Enable automatic traceparent injection on XMLHttpRequest\n * @default true\n */\n instrumentXHR?: boolean;\n\n /**\n * Privacy controls for traceparent header injection\n *\n * Configure origin filtering and privacy signal respecting (DNT, GPC)\n * to ensure compliance with GDPR, CCPA, and user privacy preferences.\n *\n * @example Basic origin filtering\n * ```typescript\n * {\n * privacy: {\n * allowedOrigins: ['api.myapp.com'], // Only inject on API calls\n * respectDoNotTrack: true // Respect user's DNT setting\n * }\n * }\n * ```\n *\n * @example Block third-party analytics\n * ```typescript\n * {\n * privacy: {\n * blockedOrigins: ['analytics.google.com', 'facebook.com']\n * }\n * }\n * ```\n */\n privacy?: PrivacyConfig;\n}\n\nlet isInitialized = false;\nlet config: AutotelWebConfig | undefined;\nlet privacyManager: PrivacyManager | undefined;\nlet originalFetch: typeof window.fetch | undefined;\nlet originalXHROpen: typeof XMLHttpRequest.prototype.open | undefined;\nlet originalXHRSetRequestHeader: typeof XMLHttpRequest.prototype.setRequestHeader | undefined;\n\n/**\n * Initialize autotel-web\n *\n * Patches fetch() and XMLHttpRequest to auto-inject traceparent headers.\n *\n * **SSR-safe:** Safe to call in SSR environments (checks for window).\n * **Call once:** Subsequent calls are ignored.\n *\n * @example\n * ```typescript\n * import { init } from 'autotel-web'\n *\n * init({ service: 'my-frontend-app' })\n *\n * // Now all fetch/XHR calls include traceparent headers!\n * fetch('/api/users') // <-- traceparent header automatically injected\n * ```\n *\n * @example With React (client-only)\n * ```typescript\n * import { useEffect } from 'react'\n * import { init } from 'autotel-web'\n *\n * function App() {\n * useEffect(() => {\n * init({ service: 'my-spa' })\n * }, [])\n *\n * return <div>...</div>\n * }\n * ```\n */\nexport function init(userConfig: AutotelWebConfig): void {\n // SSR-safe: do nothing on the server\n if (typeof window === 'undefined') {\n return;\n }\n\n if (isInitialized) {\n if (userConfig.debug) {\n console.warn('[autotel-web] Already initialized. Skipping.');\n }\n return;\n }\n\n // Validate configuration\n validateConfig(userConfig);\n\n config = userConfig;\n\n // Initialize privacy manager if privacy config provided\n if (config.privacy) {\n privacyManager = new PrivacyManager(config.privacy);\n }\n\n // Patch fetch\n if (config.instrumentFetch !== false) {\n patchFetch();\n }\n\n // Patch XHR\n if (config.instrumentXHR !== false) {\n patchXMLHttpRequest();\n }\n\n isInitialized = true;\n\n if (config.debug) {\n console.log('[autotel-web] Initialized successfully', {\n service: config.service,\n instrumentFetch: config.instrumentFetch !== false,\n instrumentXHR: config.instrumentXHR !== false,\n privacyEnabled: !!config.privacy,\n privacyConfig: config.privacy\n ? {\n allowedOrigins: config.privacy.allowedOrigins?.length ?? 0,\n blockedOrigins: config.privacy.blockedOrigins?.length ?? 0,\n respectDoNotTrack: config.privacy.respectDoNotTrack ?? false,\n respectGPC: config.privacy.respectGPC ?? false,\n }\n : null,\n });\n }\n}\n\n/**\n * Patch fetch() to auto-inject traceparent headers\n */\nfunction patchFetch(): void {\n // Always get the current window.fetch as the original\n // This allows tests to set up mocks before calling init()\n originalFetch = window.fetch.bind(window);\n\n window.fetch = function (\n input: RequestInfo | URL,\n init?: RequestInit\n ): Promise<Response> {\n // Get URL string for logging and privacy checks\n const url =\n typeof input === 'string'\n ? input\n : input instanceof URL\n ? input.toString()\n : input.url;\n\n // Create headers object\n const headers = new Headers(init?.headers);\n\n // Only inject if traceparent doesn't already exist\n if (!headers.has('traceparent')) {\n // Check privacy controls\n if (privacyManager && !privacyManager.shouldInjectTraceparent(url)) {\n if (config?.debug) {\n const reason = getDenialReason(privacyManager, url);\n console.log(\n '[autotel-web] Skipped traceparent on fetch (privacy):',\n url,\n reason\n );\n }\n } else {\n // Inject traceparent header\n headers.set('traceparent', createTraceparent());\n\n if (config?.debug) {\n console.log(\n '[autotel-web] Injected traceparent on fetch:',\n url,\n headers.get('traceparent')\n );\n }\n }\n }\n\n // Call original fetch with updated headers\n // originalFetch is always defined here because patchFetch() sets it before patching\n return originalFetch!(input, { ...init, headers });\n };\n}\n\n/**\n * Patch XMLHttpRequest to auto-inject traceparent headers\n */\nfunction patchXMLHttpRequest(): void {\n // Always get the current prototypes as the originals\n // This allows tests to set up mocks before calling init()\n originalXHROpen = XMLHttpRequest.prototype.open;\n originalXHRSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;\n\n // Track which XHR instances have traceparent set\n const xhrHasTraceparent = new WeakSet<XMLHttpRequest>();\n\n // Patch setRequestHeader to track manual traceparent headers\n XMLHttpRequest.prototype.setRequestHeader = function (\n name: string,\n value: string\n ): void {\n if (name.toLowerCase() === 'traceparent') {\n xhrHasTraceparent.add(this);\n }\n // originalXHRSetRequestHeader is always defined here because patchXMLHttpRequest() sets it before patching\n return originalXHRSetRequestHeader!.call(this, name, value);\n };\n\n // Patch open to inject traceparent after headers are ready\n XMLHttpRequest.prototype.open = function (\n method: string,\n url: string | URL,\n async: boolean = true,\n username?: string | null,\n password?: string | null\n ): void {\n // Call original open\n // originalXHROpen is always defined here because patchXMLHttpRequest() sets it before patching\n const result = originalXHROpen!.call(this, method, url, async, username, password);\n\n // Convert URL to string for logging and privacy checks\n const urlStr = typeof url === 'string' ? url : url.toString();\n\n // Listen for readyState change to inject header at the right time\n const xhr = this;\n const originalOnReadyStateChange = xhr.onreadystatechange;\n\n xhr.onreadystatechange = function (event: Event) {\n // OPENED state (1) - headers can now be set\n if (xhr.readyState === XMLHttpRequest.OPENED) {\n // Only inject if not already set\n if (!xhrHasTraceparent.has(xhr)) {\n // Check privacy controls\n if (privacyManager && !privacyManager.shouldInjectTraceparent(urlStr)) {\n if (config?.debug) {\n const reason = getDenialReason(privacyManager, urlStr);\n console.log(\n '[autotel-web] Skipped traceparent on XHR (privacy):',\n urlStr,\n reason\n );\n }\n } else {\n // Inject traceparent header\n try {\n const traceparent = createTraceparent();\n // originalXHRSetRequestHeader is always defined here because patchXMLHttpRequest() sets it before patching\n originalXHRSetRequestHeader!.call(xhr, 'traceparent', traceparent);\n\n if (config?.debug) {\n console.log(\n '[autotel-web] Injected traceparent on XHR:',\n urlStr,\n traceparent\n );\n }\n } catch (error) {\n // Silently ignore if setRequestHeader fails\n if (config?.debug) {\n console.warn(\n '[autotel-web] Failed to inject traceparent on XHR:',\n error\n );\n }\n }\n }\n }\n }\n\n // Call original handler if it exists\n if (originalOnReadyStateChange) {\n return originalOnReadyStateChange.call(xhr, event);\n }\n };\n\n return result;\n };\n}\n\n/**\n * Validate configuration at initialization time\n * Catches common misconfigurations early\n */\nfunction validateConfig(userConfig: AutotelWebConfig): void {\n // Validate service name\n if (!userConfig.service || typeof userConfig.service !== 'string') {\n throw new Error('[autotel-web] service name is required and must be a string');\n }\n\n if (userConfig.service.length === 0) {\n throw new Error('[autotel-web] service name cannot be empty');\n }\n\n if (userConfig.service.length > 255) {\n console.warn(\n '[autotel-web] service name is very long (> 255 chars). Consider using a shorter name.'\n );\n }\n\n // Validate privacy config if provided\n if (userConfig.privacy) {\n const { allowedOrigins, blockedOrigins } = userConfig.privacy;\n\n // Warn if both allowlist and blocklist are empty\n if (\n (!allowedOrigins || allowedOrigins.length === 0) &&\n (!blockedOrigins || blockedOrigins.length === 0) &&\n !userConfig.privacy.respectDoNotTrack &&\n !userConfig.privacy.respectGPC\n ) {\n console.warn(\n '[autotel-web] privacy config provided but all options are empty/disabled. This has no effect.'\n );\n }\n\n // Warn about overlapping origins\n if (allowedOrigins && blockedOrigins) {\n const overlap = allowedOrigins.filter((allowed) =>\n blockedOrigins.some((blocked) =>\n allowed.toLowerCase().includes(blocked.toLowerCase())\n )\n );\n if (overlap.length > 0) {\n console.warn(\n '[autotel-web] Some allowedOrigins match blockedOrigins. Blocklist takes precedence:',\n overlap\n );\n }\n }\n\n // Validate origin format (warn if looks invalid)\n const allOrigins = [\n ...(allowedOrigins ?? []),\n ...(blockedOrigins ?? []),\n ];\n allOrigins.forEach((origin) => {\n if (origin.includes('://')) {\n console.warn(\n `[autotel-web] Origin \"${origin}\" includes protocol (://) - this is usually not needed. Just use the domain name.`\n );\n }\n });\n }\n}\n\n/**\n * Reset initialization state (for testing)\n * @internal\n */\nexport function resetForTesting(): void {\n isInitialized = false;\n config = undefined;\n privacyManager = undefined;\n\n // Restore original fetch/XHR if they were patched\n // Then clear the stored originals so next test can set up fresh mocks\n if (typeof window !== 'undefined') {\n if (originalFetch) {\n window.fetch = originalFetch;\n originalFetch = undefined;\n }\n if (originalXHROpen) {\n XMLHttpRequest.prototype.open = originalXHROpen;\n originalXHROpen = undefined;\n }\n if (originalXHRSetRequestHeader) {\n XMLHttpRequest.prototype.setRequestHeader = originalXHRSetRequestHeader;\n originalXHRSetRequestHeader = undefined;\n }\n }\n}\n\n/**\n * Get current configuration\n * @internal\n */\nexport function getConfig(): AutotelWebConfig | undefined {\n return config;\n}\n\n/**\n * Get current privacy manager\n * @internal\n */\nexport function getPrivacyManager(): PrivacyManager | undefined {\n return privacyManager;\n}\n"]}
1
+ {"version":3,"sources":["../src/privacy.ts","../src/span-exporter.ts","../src/init.ts"],"names":["config","privacyManager","init"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Privacy controls for autotel-web\n *\n * Provides origin filtering and privacy signal respecting (DNT, GPC)\n * to ensure compliance with GDPR, CCPA, and user privacy preferences.\n */\n\nexport interface PrivacyConfig {\n /**\n * Only inject traceparent headers on requests to these origins (whitelist)\n *\n * If specified, traceparent will ONLY be injected on matching origins.\n * Origins are matched using substring matching (e.g., \"example.com\" matches \"https://api.example.com\").\n *\n * @example\n * ```typescript\n * {\n * allowedOrigins: ['api.myapp.com', 'myapp.com']\n * }\n * ```\n */\n allowedOrigins?: string[];\n\n /**\n * Never inject traceparent headers on requests to these origins (blacklist)\n *\n * Takes precedence over allowedOrigins.\n * Origins are matched using substring matching.\n *\n * @example\n * ```typescript\n * {\n * blockedOrigins: ['analytics.google.com', 'facebook.com']\n * }\n * ```\n */\n blockedOrigins?: string[];\n\n /**\n * Respect the Do Not Track (DNT) browser setting\n *\n * If true and user has DNT enabled, no traceparent headers will be injected.\n *\n * @default false\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Navigator/doNotTrack\n */\n respectDoNotTrack?: boolean;\n\n /**\n * Respect the Global Privacy Control (GPC) browser signal\n *\n * If true and user has GPC enabled, no traceparent headers will be injected.\n *\n * @default false\n * @see https://globalprivacycontrol.org/\n */\n respectGPC?: boolean;\n}\n\n/**\n * Manages privacy controls for traceparent header injection\n *\n * Checks user privacy preferences (DNT, GPC) and origin filtering rules\n * to determine if traceparent headers should be injected on a given request.\n */\nexport class PrivacyManager {\n constructor(private readonly config: PrivacyConfig) {}\n\n /**\n * Check if traceparent header should be injected for a given URL\n *\n * Decision order:\n * 1. Check Do Not Track (if enabled)\n * 2. Check Global Privacy Control (if enabled)\n * 3. Check blockedOrigins (explicit deny)\n * 4. Check allowedOrigins (explicit allow, if configured)\n * 5. Default: allow\n *\n * @param url - Full URL or relative path of the request\n * @returns true if traceparent should be injected, false otherwise\n */\n shouldInjectTraceparent(url: string): boolean {\n // Check Do Not Track\n if (this.config.respectDoNotTrack && this.isDoNotTrackEnabled()) {\n return false;\n }\n\n // Check Global Privacy Control\n if (this.config.respectGPC && this.isGPCEnabled()) {\n return false;\n }\n\n // Get the origin of the target URL\n const targetOrigin = this.extractOrigin(url);\n\n // Check blocklist first (explicit deny takes precedence)\n if (\n this.config.blockedOrigins &&\n this.matchesAnyOrigin(targetOrigin, this.config.blockedOrigins)\n ) {\n return false;\n }\n\n // If allowlist exists, only allow those origins\n if (this.config.allowedOrigins && this.config.allowedOrigins.length > 0) {\n return this.matchesAnyOrigin(targetOrigin, this.config.allowedOrigins);\n }\n\n // Default: allow (backward compatible behavior)\n return true;\n }\n\n /**\n * Check if Do Not Track is enabled in the browser\n */\n private isDoNotTrackEnabled(): boolean {\n if (typeof navigator === 'undefined') return false;\n\n // DNT header can be \"1\" (enabled), \"0\" (disabled), or null (not set)\n return navigator.doNotTrack === '1';\n }\n\n /**\n * Check if Global Privacy Control is enabled in the browser\n */\n private isGPCEnabled(): boolean {\n if (typeof navigator === 'undefined') return false;\n\n // GPC is a newer spec, not all browsers support it yet\n // TypeScript doesn't have types for this yet, so we cast\n const nav = navigator as Navigator & { globalPrivacyControl?: boolean };\n return nav.globalPrivacyControl === true;\n }\n\n /**\n * Extract origin from a URL (handles both absolute and relative URLs)\n *\n * @param url - Full URL or relative path\n * @returns Origin string (e.g., \"https://api.example.com\")\n */\n private extractOrigin(url: string): string {\n try {\n // Handle absolute URLs\n if (url.startsWith('http://') || url.startsWith('https://')) {\n return new URL(url).origin;\n }\n\n // Handle relative URLs - use current window location\n if (typeof window !== 'undefined') {\n return new URL(url, window.location.href).origin;\n }\n\n // Fallback for SSR or unknown cases\n return '';\n } catch {\n // Invalid URL - return empty string\n return '';\n }\n }\n\n /**\n * Check if a target origin matches any of the configured origins\n *\n * Uses substring matching for flexibility (e.g., \"example.com\" matches \"https://api.example.com\")\n *\n * @param targetOrigin - Origin to check\n * @param configuredOrigins - List of allowed or blocked origins\n * @returns true if any origin matches\n */\n private matchesAnyOrigin(\n targetOrigin: string,\n configuredOrigins: string[]\n ): boolean {\n return configuredOrigins.some((configuredOrigin) => {\n // Normalize both strings to lowercase for case-insensitive matching\n const normalizedTarget = targetOrigin.toLowerCase();\n const normalizedConfigured = configuredOrigin.toLowerCase();\n\n // Check if target origin contains the configured origin\n // This allows \"example.com\" to match \"https://api.example.com\"\n return normalizedTarget.includes(normalizedConfigured);\n });\n }\n}\n\n/**\n * Get reason why traceparent injection was denied (for debugging)\n *\n * Returns a human-readable reason if injection would be blocked,\n * or null if injection would be allowed.\n *\n * @param privacyManager - Configured PrivacyManager instance\n * @param url - URL to check\n * @returns Denial reason or null if allowed\n *\n * @example\n * ```typescript\n * const manager = new PrivacyManager({ respectDoNotTrack: true })\n * const reason = getDenialReason(manager, 'https://api.example.com')\n * if (reason) {\n * console.log('Traceparent blocked:', reason)\n * }\n * ```\n */\nexport function getDenialReason(\n privacyManager: PrivacyManager,\n url: string\n): string | null {\n // This is a helper for debugging - it re-checks the conditions\n // to provide a user-friendly reason string\n const config = (privacyManager as any).config as PrivacyConfig;\n\n // Check DNT\n if (config.respectDoNotTrack && typeof navigator !== 'undefined') {\n if (navigator.doNotTrack === '1') {\n return 'Do Not Track is enabled';\n }\n }\n\n // Check GPC\n if (config.respectGPC && typeof navigator !== 'undefined') {\n const nav = navigator as Navigator & { globalPrivacyControl?: boolean };\n if (nav.globalPrivacyControl === true) {\n return 'Global Privacy Control is enabled';\n }\n }\n\n // Extract origin\n let targetOrigin = '';\n try {\n if (url.startsWith('http://') || url.startsWith('https://')) {\n targetOrigin = new URL(url).origin;\n } else if (typeof window !== 'undefined') {\n targetOrigin = new URL(url, window.location.href).origin;\n }\n } catch {\n return 'Invalid URL';\n }\n\n // Check blocklist\n if (config.blockedOrigins) {\n const blocked = config.blockedOrigins.some((origin) =>\n targetOrigin.toLowerCase().includes(origin.toLowerCase())\n );\n if (blocked) {\n return `Origin ${targetOrigin} is in blockedOrigins list`;\n }\n }\n\n // Check allowlist\n if (config.allowedOrigins && config.allowedOrigins.length > 0) {\n const allowed = config.allowedOrigins.some((origin) =>\n targetOrigin.toLowerCase().includes(origin.toLowerCase())\n );\n if (!allowed) {\n return `Origin ${targetOrigin} is not in allowedOrigins list`;\n }\n }\n\n return null;\n}\n","/**\n * Lightweight browser span exporter via sendBeacon/fetch.\n * Sends OTLP/JSON spans so the browser's traceparent spanId\n * exists as a real span in the collector.\n */\n\nlet debug = false;\nlet serviceName = 'browser';\nlet exportEndpoint: string | undefined;\nlet pendingSpans: unknown[] = [];\nlet flushTimer: ReturnType<typeof setTimeout> | undefined;\nlet rawFetch: typeof globalThis.fetch | undefined;\n\n/**\n * Provide the unpatched fetch so the exporter bypasses instrumentation.\n * Must be called before init() patches window.fetch.\n */\nexport function setRawFetch(fn: typeof globalThis.fetch): void {\n rawFetch = fn;\n}\n\nexport function configureExporter(service: string, endpoint: string, enableDebug = false): void {\n debug = enableDebug;\n serviceName = service;\n exportEndpoint = endpoint.replace(/\\/$/, '');\n if (!exportEndpoint.endsWith('/v1/traces')) {\n exportEndpoint += '/v1/traces';\n }\n if (!flushTimer) {\n flushTimer = setInterval(flushSpans, 2000);\n }\n}\n\nexport function recordSpan(\n traceId: string,\n spanId: string,\n name: string,\n startMs: number,\n endMs: number,\n attrs?: Record<string, string | number>,\n): void {\n if (!exportEndpoint) return;\n if (debug) console.log(`[autotel-web] recordSpan: ${name} (${traceId.slice(0, 8)}…)`);\n const attributes = attrs\n ? Object.entries(attrs).map(([key, value]) => ({\n key,\n value: typeof value === 'number' ? { intValue: String(value) } : { stringValue: value },\n }))\n : undefined;\n pendingSpans.push({\n traceId,\n spanId,\n name,\n kind: 3, // CLIENT\n startTimeUnixNano: String(Math.round(startMs * 1_000_000)),\n endTimeUnixNano: String(Math.round(endMs * 1_000_000)),\n attributes,\n });\n // Flush immediately — browser spans are infrequent\n flushSpans();\n}\n\nexport function flushSpans(): void {\n if (!exportEndpoint || pendingSpans.length === 0) return;\n if (debug) console.log(`[autotel-web] flushSpans: sending ${pendingSpans.length} span(s) to ${exportEndpoint}`);\n const spans = pendingSpans;\n pendingSpans = [];\n const payload = JSON.stringify({\n resourceSpans: [{\n resource: { attributes: [{ key: 'service.name', value: { stringValue: serviceName } }] },\n scopeSpans: [{ scope: { name: 'autotel-web' }, spans }],\n }],\n });\n const blob = new Blob([payload], { type: 'application/json' });\n const sent = typeof navigator?.sendBeacon === 'function' && navigator.sendBeacon(exportEndpoint, blob);\n if (!sent && rawFetch) {\n rawFetch(exportEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: payload,\n keepalive: true,\n }).catch(() => {});\n }\n}\n\nexport function isConfigured(): boolean {\n return exportEndpoint !== undefined;\n}\n\nexport function resetForTesting(): void {\n exportEndpoint = undefined;\n pendingSpans = [];\n if (flushTimer) { clearInterval(flushTimer); flushTimer = undefined; }\n}\n","/**\n * Minimal browser SDK initialization\n *\n * Patches fetch() and XMLHttpRequest to automatically inject W3C traceparent headers.\n * NO OpenTelemetry dependencies - just native browser APIs.\n *\n * Bundle size: ~2-5KB gzipped\n */\n\nimport { createTraceparent, parseTraceparent } from './traceparent';\nimport { PrivacyManager, PrivacyConfig, getDenialReason } from './privacy';\nimport { configureExporter, setRawFetch, recordSpan, flushSpans, isConfigured, resetForTesting as resetExporter } from './span-exporter';\n\nexport interface AutotelWebConfig {\n /**\n * Service name for the browser application\n * Used only for logging/debugging - not sent in headers\n */\n service: string;\n\n /**\n * Enable debug logging to console\n * @default false\n */\n debug?: boolean;\n\n /**\n * Enable automatic traceparent injection on fetch calls\n * @default true\n */\n instrumentFetch?: boolean;\n\n /**\n * Enable automatic traceparent injection on XMLHttpRequest\n * @default true\n */\n instrumentXHR?: boolean;\n\n /**\n * OTLP endpoint for exporting browser spans.\n * When set, browser spans are sent via sendBeacon so the traceparent\n * spanId exists as a real span in the collector.\n * Use '' (empty string) for same-origin (requires /v1/traces proxy).\n */\n endpoint?: string;\n\n /**\n * Privacy controls for traceparent header injection\n *\n * Configure origin filtering and privacy signal respecting (DNT, GPC)\n * to ensure compliance with GDPR, CCPA, and user privacy preferences.\n *\n * @example Basic origin filtering\n * ```typescript\n * {\n * privacy: {\n * allowedOrigins: ['api.myapp.com'], // Only inject on API calls\n * respectDoNotTrack: true // Respect user's DNT setting\n * }\n * }\n * ```\n *\n * @example Block third-party analytics\n * ```typescript\n * {\n * privacy: {\n * blockedOrigins: ['analytics.google.com', 'facebook.com']\n * }\n * }\n * ```\n */\n privacy?: PrivacyConfig;\n}\n\nlet isInitialized = false;\nlet config: AutotelWebConfig | undefined;\nlet privacyManager: PrivacyManager | undefined;\nlet originalFetch: typeof window.fetch | undefined;\nlet originalXHROpen: typeof XMLHttpRequest.prototype.open | undefined;\nlet originalXHRSetRequestHeader: typeof XMLHttpRequest.prototype.setRequestHeader | undefined;\n\n/**\n * Initialize autotel-web\n *\n * Patches fetch() and XMLHttpRequest to auto-inject traceparent headers.\n *\n * **SSR-safe:** Safe to call in SSR environments (checks for window).\n * **Call once:** Subsequent calls are ignored.\n *\n * @example\n * ```typescript\n * import { init } from 'autotel-web'\n *\n * init({ service: 'my-frontend-app' })\n *\n * // Now all fetch/XHR calls include traceparent headers!\n * fetch('/api/users') // <-- traceparent header automatically injected\n * ```\n *\n * @example With React (client-only)\n * ```typescript\n * import { useEffect } from 'react'\n * import { init } from 'autotel-web'\n *\n * function App() {\n * useEffect(() => {\n * init({ service: 'my-spa' })\n * }, [])\n *\n * return <div>...</div>\n * }\n * ```\n */\nexport function init(userConfig: AutotelWebConfig): void {\n // SSR-safe: do nothing on the server\n if (typeof window === 'undefined') {\n return;\n }\n\n if (isInitialized) {\n if (userConfig.debug) {\n console.warn('[autotel-web] Already initialized. Skipping.');\n }\n return;\n }\n\n // Validate configuration\n validateConfig(userConfig);\n\n config = userConfig;\n\n // Initialize privacy manager if privacy config provided\n if (config.privacy) {\n privacyManager = new PrivacyManager(config.privacy);\n }\n\n // Capture unpatched fetch for the exporter before we patch it\n if (config.endpoint !== undefined) {\n setRawFetch(window.fetch.bind(window));\n configureExporter(config.service, config.endpoint, config.debug);\n }\n\n // Patch fetch\n if (config.instrumentFetch !== false) {\n patchFetch();\n }\n\n // Patch XHR\n if (config.instrumentXHR !== false) {\n patchXMLHttpRequest();\n }\n\n if (config.endpoint !== undefined) {\n window.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'hidden') flushSpans();\n });\n }\n\n isInitialized = true;\n\n if (config.debug) {\n console.log('[autotel-web] Initialized successfully', {\n service: config.service,\n instrumentFetch: config.instrumentFetch !== false,\n instrumentXHR: config.instrumentXHR !== false,\n privacyEnabled: !!config.privacy,\n privacyConfig: config.privacy\n ? {\n allowedOrigins: config.privacy.allowedOrigins?.length ?? 0,\n blockedOrigins: config.privacy.blockedOrigins?.length ?? 0,\n respectDoNotTrack: config.privacy.respectDoNotTrack ?? false,\n respectGPC: config.privacy.respectGPC ?? false,\n }\n : null,\n });\n }\n}\n\n/**\n * Patch fetch() to auto-inject traceparent headers\n */\nfunction patchFetch(): void {\n // Always get the current window.fetch as the original\n // This allows tests to set up mocks before calling init()\n originalFetch = window.fetch.bind(window);\n\n window.fetch = function (\n input: RequestInfo | URL,\n init?: RequestInit\n ): Promise<Response> {\n // Get URL string for logging and privacy checks\n const url =\n typeof input === 'string'\n ? input\n : input instanceof URL\n ? input.toString()\n : input.url;\n\n // Create headers object\n const headers = new Headers(init?.headers);\n\n // Only inject if traceparent doesn't already exist\n let injectedTraceparent: string | undefined;\n if (!headers.has('traceparent')) {\n // Check privacy controls\n if (privacyManager && !privacyManager.shouldInjectTraceparent(url)) {\n if (config?.debug) {\n const reason = getDenialReason(privacyManager, url);\n console.log(\n '[autotel-web] Skipped traceparent on fetch (privacy):',\n url,\n reason\n );\n }\n } else {\n injectedTraceparent = createTraceparent();\n headers.set('traceparent', injectedTraceparent);\n\n if (config?.debug) {\n console.log(\n '[autotel-web] Injected traceparent on fetch:',\n url,\n injectedTraceparent\n );\n }\n }\n }\n\n // Resolve HTTP method: prefer init override, then Request.method, then default GET\n const method = init?.method\n ?? (input instanceof Request ? input.method : undefined)\n ?? 'GET';\n\n // Call original fetch with updated headers\n const startTime = performance.timeOrigin + performance.now();\n const fetchPromise = originalFetch!(input, { ...init, headers });\n\n // Export browser span if exporter is configured\n if (injectedTraceparent && isConfigured()) {\n fetchPromise.then(\n (response) => {\n const endTime = performance.timeOrigin + performance.now();\n const parsed = parseTraceparent(injectedTraceparent!);\n if (parsed) {\n let pathname: string;\n try { pathname = new URL(url, window.location.origin).pathname; } catch { pathname = url; }\n recordSpan(parsed.traceId, parsed.spanId, `browser ${pathname}`, startTime, endTime, {\n 'http.method': method,\n 'http.url': url,\n 'http.status_code': response.status,\n });\n }\n },\n () => {\n const endTime = performance.timeOrigin + performance.now();\n const parsed = parseTraceparent(injectedTraceparent!);\n if (parsed) {\n let pathname: string;\n try { pathname = new URL(url, window.location.origin).pathname; } catch { pathname = url; }\n recordSpan(parsed.traceId, parsed.spanId, `browser ${pathname}`, startTime, endTime, {\n 'http.method': method,\n 'http.url': url,\n });\n }\n },\n );\n }\n\n return fetchPromise;\n };\n}\n\n/**\n * Patch XMLHttpRequest to auto-inject traceparent headers\n */\nfunction patchXMLHttpRequest(): void {\n // Always get the current prototypes as the originals\n // This allows tests to set up mocks before calling init()\n originalXHROpen = XMLHttpRequest.prototype.open;\n originalXHRSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;\n\n // Track which XHR instances have traceparent set\n const xhrHasTraceparent = new WeakSet<XMLHttpRequest>();\n\n // Patch setRequestHeader to track manual traceparent headers\n XMLHttpRequest.prototype.setRequestHeader = function (\n name: string,\n value: string\n ): void {\n if (name.toLowerCase() === 'traceparent') {\n xhrHasTraceparent.add(this);\n }\n // originalXHRSetRequestHeader is always defined here because patchXMLHttpRequest() sets it before patching\n return originalXHRSetRequestHeader!.call(this, name, value);\n };\n\n // Patch open to inject traceparent after headers are ready\n XMLHttpRequest.prototype.open = function (\n method: string,\n url: string | URL,\n async: boolean = true,\n username?: string | null,\n password?: string | null\n ): void {\n // Call original open\n // originalXHROpen is always defined here because patchXMLHttpRequest() sets it before patching\n const result = originalXHROpen!.call(this, method, url, async, username, password);\n\n // Convert URL to string for logging and privacy checks\n const urlStr = typeof url === 'string' ? url : url.toString();\n\n // Listen for readyState change to inject header at the right time\n const xhr = this;\n const originalOnReadyStateChange = xhr.onreadystatechange;\n\n xhr.onreadystatechange = function (event: Event) {\n // OPENED state (1) - headers can now be set\n if (xhr.readyState === XMLHttpRequest.OPENED) {\n // Only inject if not already set\n if (!xhrHasTraceparent.has(xhr)) {\n // Check privacy controls\n if (privacyManager && !privacyManager.shouldInjectTraceparent(urlStr)) {\n if (config?.debug) {\n const reason = getDenialReason(privacyManager, urlStr);\n console.log(\n '[autotel-web] Skipped traceparent on XHR (privacy):',\n urlStr,\n reason\n );\n }\n } else {\n // Inject traceparent header\n try {\n const traceparent = createTraceparent();\n // originalXHRSetRequestHeader is always defined here because patchXMLHttpRequest() sets it before patching\n originalXHRSetRequestHeader!.call(xhr, 'traceparent', traceparent);\n\n if (config?.debug) {\n console.log(\n '[autotel-web] Injected traceparent on XHR:',\n urlStr,\n traceparent\n );\n }\n } catch (error) {\n // Silently ignore if setRequestHeader fails\n if (config?.debug) {\n console.warn(\n '[autotel-web] Failed to inject traceparent on XHR:',\n error\n );\n }\n }\n }\n }\n }\n\n // Call original handler if it exists\n if (originalOnReadyStateChange) {\n return originalOnReadyStateChange.call(xhr, event);\n }\n };\n\n return result;\n };\n}\n\n/**\n * Validate configuration at initialization time\n * Catches common misconfigurations early\n */\nfunction validateConfig(userConfig: AutotelWebConfig): void {\n // Validate service name\n if (!userConfig.service || typeof userConfig.service !== 'string') {\n throw new Error('[autotel-web] service name is required and must be a string');\n }\n\n if (userConfig.service.length === 0) {\n throw new Error('[autotel-web] service name cannot be empty');\n }\n\n if (userConfig.service.length > 255) {\n console.warn(\n '[autotel-web] service name is very long (> 255 chars). Consider using a shorter name.'\n );\n }\n\n // Validate privacy config if provided\n if (userConfig.privacy) {\n const { allowedOrigins, blockedOrigins } = userConfig.privacy;\n\n // Warn if both allowlist and blocklist are empty\n if (\n (!allowedOrigins || allowedOrigins.length === 0) &&\n (!blockedOrigins || blockedOrigins.length === 0) &&\n !userConfig.privacy.respectDoNotTrack &&\n !userConfig.privacy.respectGPC\n ) {\n console.warn(\n '[autotel-web] privacy config provided but all options are empty/disabled. This has no effect.'\n );\n }\n\n // Warn about overlapping origins\n if (allowedOrigins && blockedOrigins) {\n const overlap = allowedOrigins.filter((allowed) =>\n blockedOrigins.some((blocked) =>\n allowed.toLowerCase().includes(blocked.toLowerCase())\n )\n );\n if (overlap.length > 0) {\n console.warn(\n '[autotel-web] Some allowedOrigins match blockedOrigins. Blocklist takes precedence:',\n overlap\n );\n }\n }\n\n // Validate origin format (warn if looks invalid)\n const allOrigins = [\n ...(allowedOrigins ?? []),\n ...(blockedOrigins ?? []),\n ];\n allOrigins.forEach((origin) => {\n if (origin.includes('://')) {\n console.warn(\n `[autotel-web] Origin \"${origin}\" includes protocol (://) - this is usually not needed. Just use the domain name.`\n );\n }\n });\n }\n}\n\n/**\n * Reset initialization state (for testing)\n * @internal\n */\nexport function resetForTesting(): void {\n isInitialized = false;\n config = undefined;\n privacyManager = undefined;\n resetExporter();\n\n // Restore original fetch/XHR if they were patched\n // Then clear the stored originals so next test can set up fresh mocks\n if (typeof window !== 'undefined') {\n if (originalFetch) {\n window.fetch = originalFetch;\n originalFetch = undefined;\n }\n if (originalXHROpen) {\n XMLHttpRequest.prototype.open = originalXHROpen;\n originalXHROpen = undefined;\n }\n if (originalXHRSetRequestHeader) {\n XMLHttpRequest.prototype.setRequestHeader = originalXHRSetRequestHeader;\n originalXHRSetRequestHeader = undefined;\n }\n }\n}\n\n/**\n * Get current configuration\n * @internal\n */\nexport function getConfig(): AutotelWebConfig | undefined {\n return config;\n}\n\n/**\n * Get current privacy manager\n * @internal\n */\nexport function getPrivacyManager(): PrivacyManager | undefined {\n return privacyManager;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autotel-web",
3
- "version": "1.11.0",
3
+ "version": "1.11.2",
4
4
  "description": "Ultra-lightweight browser SDK for distributed tracing - propagates W3C traceparent headers to backends using Autotel (~2-5KB gzipped)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -43,25 +43,25 @@
43
43
  "author": "Jag Reehal <jag@jagreehal.com> (https://jagreehal.com)",
44
44
  "license": "MIT",
45
45
  "dependencies": {
46
- "@opentelemetry/api": "^1.9.0",
47
- "@opentelemetry/core": "^2.6.0",
48
- "@opentelemetry/exporter-trace-otlp-http": "^0.213.0",
49
- "@opentelemetry/instrumentation": "^0.213.0",
50
- "@opentelemetry/instrumentation-document-load": "^0.58.0",
51
- "@opentelemetry/instrumentation-fetch": "^0.213.0",
52
- "@opentelemetry/instrumentation-xml-http-request": "^0.213.0",
53
- "@opentelemetry/resources": "^2.6.0",
54
- "@opentelemetry/sdk-trace-base": "^2.6.0",
55
- "@opentelemetry/sdk-trace-web": "^2.6.0",
56
- "web-vitals": "^5.1.0"
46
+ "@opentelemetry/api": "^1.9.1",
47
+ "@opentelemetry/core": "^2.7.0",
48
+ "@opentelemetry/exporter-trace-otlp-http": "^0.215.0",
49
+ "@opentelemetry/instrumentation": "^0.215.0",
50
+ "@opentelemetry/instrumentation-document-load": "^0.60.0",
51
+ "@opentelemetry/instrumentation-fetch": "^0.215.0",
52
+ "@opentelemetry/instrumentation-xml-http-request": "^0.215.0",
53
+ "@opentelemetry/resources": "^2.7.0",
54
+ "@opentelemetry/sdk-trace-base": "^2.7.0",
55
+ "@opentelemetry/sdk-trace-web": "^2.7.0",
56
+ "web-vitals": "^5.2.0"
57
57
  },
58
58
  "devDependencies": {
59
- "@types/node": "^25.3.5",
59
+ "@types/node": "^25.6.0",
60
60
  "rimraf": "^6.1.3",
61
61
  "tsup": "^8.5.1",
62
- "typescript": "^5.9.3",
63
- "vitest": "^4.0.18",
64
- "vitest-mock-extended": "^3.1.0"
62
+ "typescript": "^6.0.3",
63
+ "vitest": "^4.1.5",
64
+ "vitest-mock-extended": "^4.0.0"
65
65
  },
66
66
  "repository": {
67
67
  "type": "git",
package/src/index.ts CHANGED
@@ -49,6 +49,9 @@
49
49
  // Core initialization
50
50
  export { init, type AutotelWebConfig } from './init';
51
51
 
52
+ // Span exporter
53
+ export { flushSpans } from './span-exporter';
54
+
52
55
  // Privacy types (re-exported from init.ts which imports from privacy.ts)
53
56
  export type { PrivacyConfig } from './privacy';
54
57
 
package/src/init.ts CHANGED
@@ -7,8 +7,9 @@
7
7
  * Bundle size: ~2-5KB gzipped
8
8
  */
9
9
 
10
- import { createTraceparent } from './traceparent';
10
+ import { createTraceparent, parseTraceparent } from './traceparent';
11
11
  import { PrivacyManager, PrivacyConfig, getDenialReason } from './privacy';
12
+ import { configureExporter, setRawFetch, recordSpan, flushSpans, isConfigured, resetForTesting as resetExporter } from './span-exporter';
12
13
 
13
14
  export interface AutotelWebConfig {
14
15
  /**
@@ -35,6 +36,14 @@ export interface AutotelWebConfig {
35
36
  */
36
37
  instrumentXHR?: boolean;
37
38
 
39
+ /**
40
+ * OTLP endpoint for exporting browser spans.
41
+ * When set, browser spans are sent via sendBeacon so the traceparent
42
+ * spanId exists as a real span in the collector.
43
+ * Use '' (empty string) for same-origin (requires /v1/traces proxy).
44
+ */
45
+ endpoint?: string;
46
+
38
47
  /**
39
48
  * Privacy controls for traceparent header injection
40
49
  *
@@ -125,6 +134,12 @@ export function init(userConfig: AutotelWebConfig): void {
125
134
  privacyManager = new PrivacyManager(config.privacy);
126
135
  }
127
136
 
137
+ // Capture unpatched fetch for the exporter before we patch it
138
+ if (config.endpoint !== undefined) {
139
+ setRawFetch(window.fetch.bind(window));
140
+ configureExporter(config.service, config.endpoint, config.debug);
141
+ }
142
+
128
143
  // Patch fetch
129
144
  if (config.instrumentFetch !== false) {
130
145
  patchFetch();
@@ -135,6 +150,12 @@ export function init(userConfig: AutotelWebConfig): void {
135
150
  patchXMLHttpRequest();
136
151
  }
137
152
 
153
+ if (config.endpoint !== undefined) {
154
+ window.addEventListener('visibilitychange', () => {
155
+ if (document.visibilityState === 'hidden') flushSpans();
156
+ });
157
+ }
158
+
138
159
  isInitialized = true;
139
160
 
140
161
  if (config.debug) {
@@ -179,6 +200,7 @@ function patchFetch(): void {
179
200
  const headers = new Headers(init?.headers);
180
201
 
181
202
  // Only inject if traceparent doesn't already exist
203
+ let injectedTraceparent: string | undefined;
182
204
  if (!headers.has('traceparent')) {
183
205
  // Check privacy controls
184
206
  if (privacyManager && !privacyManager.shouldInjectTraceparent(url)) {
@@ -191,22 +213,60 @@ function patchFetch(): void {
191
213
  );
192
214
  }
193
215
  } else {
194
- // Inject traceparent header
195
- headers.set('traceparent', createTraceparent());
216
+ injectedTraceparent = createTraceparent();
217
+ headers.set('traceparent', injectedTraceparent);
196
218
 
197
219
  if (config?.debug) {
198
220
  console.log(
199
221
  '[autotel-web] Injected traceparent on fetch:',
200
222
  url,
201
- headers.get('traceparent')
223
+ injectedTraceparent
202
224
  );
203
225
  }
204
226
  }
205
227
  }
206
228
 
229
+ // Resolve HTTP method: prefer init override, then Request.method, then default GET
230
+ const method = init?.method
231
+ ?? (input instanceof Request ? input.method : undefined)
232
+ ?? 'GET';
233
+
207
234
  // Call original fetch with updated headers
208
- // originalFetch is always defined here because patchFetch() sets it before patching
209
- return originalFetch!(input, { ...init, headers });
235
+ const startTime = performance.timeOrigin + performance.now();
236
+ const fetchPromise = originalFetch!(input, { ...init, headers });
237
+
238
+ // Export browser span if exporter is configured
239
+ if (injectedTraceparent && isConfigured()) {
240
+ fetchPromise.then(
241
+ (response) => {
242
+ const endTime = performance.timeOrigin + performance.now();
243
+ const parsed = parseTraceparent(injectedTraceparent!);
244
+ if (parsed) {
245
+ let pathname: string;
246
+ try { pathname = new URL(url, window.location.origin).pathname; } catch { pathname = url; }
247
+ recordSpan(parsed.traceId, parsed.spanId, `browser ${pathname}`, startTime, endTime, {
248
+ 'http.method': method,
249
+ 'http.url': url,
250
+ 'http.status_code': response.status,
251
+ });
252
+ }
253
+ },
254
+ () => {
255
+ const endTime = performance.timeOrigin + performance.now();
256
+ const parsed = parseTraceparent(injectedTraceparent!);
257
+ if (parsed) {
258
+ let pathname: string;
259
+ try { pathname = new URL(url, window.location.origin).pathname; } catch { pathname = url; }
260
+ recordSpan(parsed.traceId, parsed.spanId, `browser ${pathname}`, startTime, endTime, {
261
+ 'http.method': method,
262
+ 'http.url': url,
263
+ });
264
+ }
265
+ },
266
+ );
267
+ }
268
+
269
+ return fetchPromise;
210
270
  };
211
271
  }
212
272
 
@@ -379,6 +439,7 @@ export function resetForTesting(): void {
379
439
  isInitialized = false;
380
440
  config = undefined;
381
441
  privacyManager = undefined;
442
+ resetExporter();
382
443
 
383
444
  // Restore original fetch/XHR if they were patched
384
445
  // Then clear the stored originals so next test can set up fresh mocks
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Lightweight browser span exporter via sendBeacon/fetch.
3
+ * Sends OTLP/JSON spans so the browser's traceparent spanId
4
+ * exists as a real span in the collector.
5
+ */
6
+
7
+ let debug = false;
8
+ let serviceName = 'browser';
9
+ let exportEndpoint: string | undefined;
10
+ let pendingSpans: unknown[] = [];
11
+ let flushTimer: ReturnType<typeof setTimeout> | undefined;
12
+ let rawFetch: typeof globalThis.fetch | undefined;
13
+
14
+ /**
15
+ * Provide the unpatched fetch so the exporter bypasses instrumentation.
16
+ * Must be called before init() patches window.fetch.
17
+ */
18
+ export function setRawFetch(fn: typeof globalThis.fetch): void {
19
+ rawFetch = fn;
20
+ }
21
+
22
+ export function configureExporter(service: string, endpoint: string, enableDebug = false): void {
23
+ debug = enableDebug;
24
+ serviceName = service;
25
+ exportEndpoint = endpoint.replace(/\/$/, '');
26
+ if (!exportEndpoint.endsWith('/v1/traces')) {
27
+ exportEndpoint += '/v1/traces';
28
+ }
29
+ if (!flushTimer) {
30
+ flushTimer = setInterval(flushSpans, 2000);
31
+ }
32
+ }
33
+
34
+ export function recordSpan(
35
+ traceId: string,
36
+ spanId: string,
37
+ name: string,
38
+ startMs: number,
39
+ endMs: number,
40
+ attrs?: Record<string, string | number>,
41
+ ): void {
42
+ if (!exportEndpoint) return;
43
+ if (debug) console.log(`[autotel-web] recordSpan: ${name} (${traceId.slice(0, 8)}…)`);
44
+ const attributes = attrs
45
+ ? Object.entries(attrs).map(([key, value]) => ({
46
+ key,
47
+ value: typeof value === 'number' ? { intValue: String(value) } : { stringValue: value },
48
+ }))
49
+ : undefined;
50
+ pendingSpans.push({
51
+ traceId,
52
+ spanId,
53
+ name,
54
+ kind: 3, // CLIENT
55
+ startTimeUnixNano: String(Math.round(startMs * 1_000_000)),
56
+ endTimeUnixNano: String(Math.round(endMs * 1_000_000)),
57
+ attributes,
58
+ });
59
+ // Flush immediately — browser spans are infrequent
60
+ flushSpans();
61
+ }
62
+
63
+ export function flushSpans(): void {
64
+ if (!exportEndpoint || pendingSpans.length === 0) return;
65
+ if (debug) console.log(`[autotel-web] flushSpans: sending ${pendingSpans.length} span(s) to ${exportEndpoint}`);
66
+ const spans = pendingSpans;
67
+ pendingSpans = [];
68
+ const payload = JSON.stringify({
69
+ resourceSpans: [{
70
+ resource: { attributes: [{ key: 'service.name', value: { stringValue: serviceName } }] },
71
+ scopeSpans: [{ scope: { name: 'autotel-web' }, spans }],
72
+ }],
73
+ });
74
+ const blob = new Blob([payload], { type: 'application/json' });
75
+ const sent = typeof navigator?.sendBeacon === 'function' && navigator.sendBeacon(exportEndpoint, blob);
76
+ if (!sent && rawFetch) {
77
+ rawFetch(exportEndpoint, {
78
+ method: 'POST',
79
+ headers: { 'Content-Type': 'application/json' },
80
+ body: payload,
81
+ keepalive: true,
82
+ }).catch(() => {});
83
+ }
84
+ }
85
+
86
+ export function isConfigured(): boolean {
87
+ return exportEndpoint !== undefined;
88
+ }
89
+
90
+ export function resetForTesting(): void {
91
+ exportEndpoint = undefined;
92
+ pendingSpans = [];
93
+ if (flushTimer) { clearInterval(flushTimer); flushTimer = undefined; }
94
+ }