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 +142 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.js +124 -6
- package/dist/index.js.map +1 -1
- package/package.json +16 -16
- package/src/index.ts +3 -0
- package/src/init.ts +67 -6
- package/src/span-exporter.ts +94 -0
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
|
|
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
|
-
|
|
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
|
-
|
|
274
|
+
injectedTraceparent
|
|
197
275
|
);
|
|
198
276
|
}
|
|
199
277
|
}
|
|
200
278
|
}
|
|
201
|
-
|
|
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":";;;;AAiEO,IAAM,iBAAN,MAAqB;AAAA,EAC1B,YAA6BA,OAAAA,EAAuB;AAAvB,IAAA,aAAA,CAAA,IAAA,EAAA,QAAA,EAAAA,OAAAA,CAAAA;AAAA,EAAwB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAerD,wBAAwB,GAAA,EAAsB;AAE5C,IAAA,IAAI,IAAA,CAAK,MAAA,CAAO,iBAAA,IAAqB,IAAA,CAAK,qBAAoB,EAAG;AAC/D,MAAA,OAAO,KAAA;AAAA,IACT;AAGA,IAAA,IAAI,IAAA,CAAK,MAAA,CAAO,UAAA,IAAc,IAAA,CAAK,cAAa,EAAG;AACjD,MAAA,OAAO,KAAA;AAAA,IACT;AAGA,IAAA,MAAM,YAAA,GAAe,IAAA,CAAK,aAAA,CAAc,GAAG,CAAA;AAG3C,IAAA,IACE,IAAA,CAAK,OAAO,cAAA,IACZ,IAAA,CAAK,iBAAiB,YAAA,EAAc,IAAA,CAAK,MAAA,CAAO,cAAc,CAAA,EAC9D;AACA,MAAA,OAAO,KAAA;AAAA,IACT;AAGA,IAAA,IAAI,KAAK,MAAA,CAAO,cAAA,IAAkB,KAAK,MAAA,CAAO,cAAA,CAAe,SAAS,CAAA,EAAG;AACvE,MAAA,OAAO,IAAA,CAAK,gBAAA,CAAiB,YAAA,EAAc,IAAA,CAAK,OAAO,cAAc,CAAA;AAAA,IACvE;AAGA,IAAA,OAAO,IAAA;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKQ,mBAAA,GAA+B;AACrC,IAAA,IAAI,OAAO,SAAA,KAAc,WAAA,EAAa,OAAO,KAAA;AAG7C,IAAA,OAAO,UAAU,UAAA,KAAe,GAAA;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA,EAKQ,YAAA,GAAwB;AAC9B,IAAA,IAAI,OAAO,SAAA,KAAc,WAAA,EAAa,OAAO,KAAA;AAI7C,IAAA,MAAM,GAAA,GAAM,SAAA;AACZ,IAAA,OAAO,IAAI,oBAAA,KAAyB,IAAA;AAAA,EACtC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,cAAc,GAAA,EAAqB;AACzC,IAAA,IAAI;AAEF,MAAA,IAAI,IAAI,UAAA,CAAW,SAAS,KAAK,GAAA,CAAI,UAAA,CAAW,UAAU,CAAA,EAAG;AAC3D,QAAA,OAAO,IAAI,GAAA,CAAI,GAAG,CAAA,CAAE,MAAA;AAAA,MACtB;AAGA,MAAA,IAAI,OAAO,WAAW,WAAA,EAAa;AACjC,QAAA,OAAO,IAAI,GAAA,CAAI,GAAA,EAAK,MAAA,CAAO,QAAA,CAAS,IAAI,CAAA,CAAE,MAAA;AAAA,MAC5C;AAGA,MAAA,OAAO,EAAA;AAAA,IACT,CAAA,CAAA,MAAQ;AAEN,MAAA,OAAO,EAAA;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWQ,gBAAA,CACN,cACA,iBAAA,EACS;AACT,IAAA,OAAO,iBAAA,CAAkB,IAAA,CAAK,CAAC,gBAAA,KAAqB;AAElD,MAAA,MAAM,gBAAA,GAAmB,aAAa,WAAA,EAAY;AAClD,MAAA,MAAM,oBAAA,GAAuB,iBAAiB,WAAA,EAAY;AAI1D,MAAA,OAAO,gBAAA,CAAiB,SAAS,oBAAoB,CAAA;AAAA,IACvD,CAAC,CAAA;AAAA,EACH;AACF,CAAA;AAqBO,SAAS,eAAA,CACdC,iBACA,GAAA,EACe;AAGf,EAAA,MAAMD,UAAUC,eAAAA,CAAuB,MAAA;AAGvC,EAAA,IAAID,OAAAA,CAAO,iBAAA,IAAqB,OAAO,SAAA,KAAc,WAAA,EAAa;AAChE,IAAA,IAAI,SAAA,CAAU,eAAe,GAAA,EAAK;AAChC,MAAA,OAAO,yBAAA;AAAA,IACT;AAAA,EACF;AAGA,EAAA,IAAIA,OAAAA,CAAO,UAAA,IAAc,OAAO,SAAA,KAAc,WAAA,EAAa;AACzD,IAAA,MAAM,GAAA,GAAM,SAAA;AACZ,IAAA,IAAI,GAAA,CAAI,yBAAyB,IAAA,EAAM;AACrC,MAAA,OAAO,mCAAA;AAAA,IACT;AAAA,EACF;AAGA,EAAA,IAAI,YAAA,GAAe,EAAA;AACnB,EAAA,IAAI;AACF,IAAA,IAAI,IAAI,UAAA,CAAW,SAAS,KAAK,GAAA,CAAI,UAAA,CAAW,UAAU,CAAA,EAAG;AAC3D,MAAA,YAAA,GAAe,IAAI,GAAA,CAAI,GAAG,CAAA,CAAE,MAAA;AAAA,IAC9B,CAAA,MAAA,IAAW,OAAO,MAAA,KAAW,WAAA,EAAa;AACxC,MAAA,YAAA,GAAe,IAAI,GAAA,CAAI,GAAA,EAAK,MAAA,CAAO,QAAA,CAAS,IAAI,CAAA,CAAE,MAAA;AAAA,IACpD;AAAA,EACF,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,aAAA;AAAA,EACT;AAGA,EAAA,IAAIA,QAAO,cAAA,EAAgB;AACzB,IAAA,MAAM,OAAA,GAAUA,QAAO,cAAA,CAAe,IAAA;AAAA,MAAK,CAAC,WAC1C,YAAA,CAAa,WAAA,GAAc,QAAA,CAAS,MAAA,CAAO,aAAa;AAAA,KAC1D;AACA,IAAA,IAAI,OAAA,EAAS;AACX,MAAA,OAAO,UAAU,YAAY,CAAA,0BAAA,CAAA;AAAA,IAC/B;AAAA,EACF;AAGA,EAAA,IAAIA,OAAAA,CAAO,cAAA,IAAkBA,OAAAA,CAAO,cAAA,CAAe,SAAS,CAAA,EAAG;AAC7D,IAAA,MAAM,OAAA,GAAUA,QAAO,cAAA,CAAe,IAAA;AAAA,MAAK,CAAC,WAC1C,YAAA,CAAa,WAAA,GAAc,QAAA,CAAS,MAAA,CAAO,aAAa;AAAA,KAC1D;AACA,IAAA,IAAI,CAAC,OAAA,EAAS;AACZ,MAAA,OAAO,UAAU,YAAY,CAAA,8BAAA,CAAA;AAAA,IAC/B;AAAA,EACF;AAEA,EAAA,OAAO,IAAA;AACT;;;AC9PA,IAAI,KAAA,GAAQ,KAAA;AACZ,IAAI,WAAA,GAAc,SAAA;AAClB,IAAI,cAAA;AACJ,IAAI,eAA0B,EAAC;AAC/B,IAAI,UAAA;AACJ,IAAI,QAAA;AAMG,SAAS,YAAY,EAAA,EAAmC;AAC7D,EAAA,QAAA,GAAW,EAAA;AACb;AAEO,SAAS,iBAAA,CAAkB,OAAA,EAAiB,QAAA,EAAkB,WAAA,GAAc,KAAA,EAAa;AAC9F,EAAA,KAAA,GAAQ,WAAA;AACR,EAAA,WAAA,GAAc,OAAA;AACd,EAAA,cAAA,GAAiB,QAAA,CAAS,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAA;AAC3C,EAAA,IAAI,CAAC,cAAA,CAAe,QAAA,CAAS,YAAY,CAAA,EAAG;AAC1C,IAAA,cAAA,IAAkB,YAAA;AAAA,EACpB;AACA,EAAA,IAAI,CAAC,UAAA,EAAY;AACf,IAAA,UAAA,GAAa,WAAA,CAAY,YAAY,GAAI,CAAA;AAAA,EAC3C;AACF;AAEO,SAAS,WACd,OAAA,EACA,MAAA,EACA,IAAA,EACA,OAAA,EACA,OACA,KAAA,EACM;AACN,EAAA,IAAI,CAAC,cAAA,EAAgB;AACrB,EAAA,IAAI,KAAA,EAAO,OAAA,CAAQ,GAAA,CAAI,CAAA,0BAAA,EAA6B,IAAI,CAAA,EAAA,EAAK,OAAA,CAAQ,KAAA,CAAM,CAAA,EAAG,CAAC,CAAC,CAAA,OAAA,CAAI,CAAA;AACpF,EAAA,MAAM,UAAA,GAAa,KAAA,GACf,MAAA,CAAO,OAAA,CAAQ,KAAK,CAAA,CAAE,GAAA,CAAI,CAAC,CAAC,GAAA,EAAK,KAAK,CAAA,MAAO;AAAA,IAC3C,GAAA;AAAA,IACA,KAAA,EAAO,OAAO,KAAA,KAAU,QAAA,GAAW,EAAE,QAAA,EAAU,MAAA,CAAO,KAAK,CAAA,EAAE,GAAI,EAAE,WAAA,EAAa,KAAA;AAAM,IACtF,CAAA,GACF,MAAA;AACJ,EAAA,YAAA,CAAa,IAAA,CAAK;AAAA,IAChB,OAAA;AAAA,IACA,MAAA;AAAA,IACA,IAAA;AAAA,IACA,IAAA,EAAM,CAAA;AAAA;AAAA,IACN,mBAAmB,MAAA,CAAO,IAAA,CAAK,KAAA,CAAM,OAAA,GAAU,GAAS,CAAC,CAAA;AAAA,IACzD,iBAAiB,MAAA,CAAO,IAAA,CAAK,KAAA,CAAM,KAAA,GAAQ,GAAS,CAAC,CAAA;AAAA,IACrD;AAAA,GACD,CAAA;AAED,EAAA,UAAA,EAAW;AACb;AAEO,SAAS,UAAA,GAAmB;AACjC,EAAA,IAAI,CAAC,cAAA,IAAkB,YAAA,CAAa,MAAA,KAAW,CAAA,EAAG;AAClD,EAAA,IAAI,KAAA,UAAe,GAAA,CAAI,CAAA,kCAAA,EAAqC,aAAa,MAAM,CAAA,YAAA,EAAe,cAAc,CAAA,CAAE,CAAA;AAC9G,EAAA,MAAM,KAAA,GAAQ,YAAA;AACd,EAAA,YAAA,GAAe,EAAC;AAChB,EAAA,MAAM,OAAA,GAAU,KAAK,SAAA,CAAU;AAAA,IAC7B,eAAe,CAAC;AAAA,MACd,QAAA,EAAU,EAAE,UAAA,EAAY,CAAC,EAAE,GAAA,EAAK,cAAA,EAAgB,KAAA,EAAO,EAAE,WAAA,EAAa,WAAA,EAAY,EAAG,CAAA,EAAE;AAAA,MACvF,UAAA,EAAY,CAAC,EAAE,KAAA,EAAO,EAAE,IAAA,EAAM,aAAA,EAAc,EAAG,KAAA,EAAO;AAAA,KACvD;AAAA,GACF,CAAA;AACD,EAAA,MAAM,IAAA,GAAO,IAAI,IAAA,CAAK,CAAC,OAAO,CAAA,EAAG,EAAE,IAAA,EAAM,kBAAA,EAAoB,CAAA;AAC7D,EAAA,MAAM,IAAA,GAAO,OAAO,SAAA,EAAW,UAAA,KAAe,cAAc,SAAA,CAAU,UAAA,CAAW,gBAAgB,IAAI,CAAA;AACrG,EAAA,IAAI,CAAC,QAAQ,QAAA,EAAU;AACrB,IAAA,QAAA,CAAS,cAAA,EAAgB;AAAA,MACvB,MAAA,EAAQ,MAAA;AAAA,MACR,OAAA,EAAS,EAAE,cAAA,EAAgB,kBAAA,EAAmB;AAAA,MAC9C,IAAA,EAAM,OAAA;AAAA,MACN,SAAA,EAAW;AAAA,KACZ,CAAA,CAAE,KAAA,CAAM,MAAM;AAAA,IAAC,CAAC,CAAA;AAAA,EACnB;AACF;AAEO,SAAS,YAAA,GAAwB;AACtC,EAAA,OAAO,cAAA,KAAmB,MAAA;AAC5B;;;ACbA,IAAI,aAAA,GAAgB,KAAA;AACpB,IAAI,MAAA;AACJ,IAAI,cAAA;AACJ,IAAI,aAAA;AACJ,IAAI,eAAA;AACJ,IAAI,2BAAA;AAkCG,SAAS,KAAK,UAAA,EAAoC;AAEvD,EAAA,IAAI,OAAO,WAAW,WAAA,EAAa;AACjC,IAAA;AAAA,EACF;AAEA,EAAA,IAAI,aAAA,EAAe;AACjB,IAAA,IAAI,WAAW,KAAA,EAAO;AACpB,MAAA,OAAA,CAAQ,KAAK,8CAA8C,CAAA;AAAA,IAC7D;AACA,IAAA;AAAA,EACF;AAGA,EAAA,cAAA,CAAe,UAAU,CAAA;AAEzB,EAAA,MAAA,GAAS,UAAA;AAGT,EAAA,IAAI,OAAO,OAAA,EAAS;AAClB,IAAA,cAAA,GAAiB,IAAI,cAAA,CAAe,MAAA,CAAO,OAAO,CAAA;AAAA,EACpD;AAGA,EAAA,IAAI,MAAA,CAAO,aAAa,MAAA,EAAW;AACjC,IAAA,WAAA,CAAY,MAAA,CAAO,KAAA,CAAM,IAAA,CAAK,MAAM,CAAC,CAAA;AACrC,IAAA,iBAAA,CAAkB,MAAA,CAAO,OAAA,EAAS,MAAA,CAAO,QAAA,EAAU,OAAO,KAAK,CAAA;AAAA,EACjE;AAGA,EAAA,IAAI,MAAA,CAAO,oBAAoB,KAAA,EAAO;AACpC,IAAA,UAAA,EAAW;AAAA,EACb;AAGA,EAAA,IAAI,MAAA,CAAO,kBAAkB,KAAA,EAAO;AAClC,IAAA,mBAAA,EAAoB;AAAA,EACtB;AAEA,EAAA,IAAI,MAAA,CAAO,aAAa,MAAA,EAAW;AACjC,IAAA,MAAA,CAAO,gBAAA,CAAiB,oBAAoB,MAAM;AAChD,MAAA,IAAI,QAAA,CAAS,eAAA,KAAoB,QAAA,EAAU,UAAA,EAAW;AAAA,IACxD,CAAC,CAAA;AAAA,EACH;AAEA,EAAA,aAAA,GAAgB,IAAA;AAEhB,EAAA,IAAI,OAAO,KAAA,EAAO;AAChB,IAAA,OAAA,CAAQ,IAAI,wCAAA,EAA0C;AAAA,MACpD,SAAS,MAAA,CAAO,OAAA;AAAA,MAChB,eAAA,EAAiB,OAAO,eAAA,KAAoB,KAAA;AAAA,MAC5C,aAAA,EAAe,OAAO,aAAA,KAAkB,KAAA;AAAA,MACxC,cAAA,EAAgB,CAAC,CAAC,MAAA,CAAO,OAAA;AAAA,MACzB,aAAA,EAAe,OAAO,OAAA,GAClB;AAAA,QACE,cAAA,EAAgB,MAAA,CAAO,OAAA,CAAQ,cAAA,EAAgB,MAAA,IAAU,CAAA;AAAA,QACzD,cAAA,EAAgB,MAAA,CAAO,OAAA,CAAQ,cAAA,EAAgB,MAAA,IAAU,CAAA;AAAA,QACzD,iBAAA,EAAmB,MAAA,CAAO,OAAA,CAAQ,iBAAA,IAAqB,KAAA;AAAA,QACvD,UAAA,EAAY,MAAA,CAAO,OAAA,CAAQ,UAAA,IAAc;AAAA,OAC3C,GACA;AAAA,KACL,CAAA;AAAA,EACH;AACF;AAKA,SAAS,UAAA,GAAmB;AAG1B,EAAA,aAAA,GAAgB,MAAA,CAAO,KAAA,CAAM,IAAA,CAAK,MAAM,CAAA;AAExC,EAAA,MAAA,CAAO,KAAA,GAAQ,SACb,KAAA,EACAE,KAAAA,EACmB;AAEnB,IAAA,MAAM,GAAA,GACJ,OAAO,KAAA,KAAU,QAAA,GACb,KAAA,GACA,iBAAiB,GAAA,GACf,KAAA,CAAM,QAAA,EAAS,GACf,KAAA,CAAM,GAAA;AAGd,IAAA,MAAM,OAAA,GAAU,IAAI,OAAA,CAAQA,KAAAA,EAAM,OAAO,CAAA;AAGzC,IAAA,IAAI,mBAAA;AACJ,IAAA,IAAI,CAAC,OAAA,CAAQ,GAAA,CAAI,aAAa,CAAA,EAAG;AAE/B,MAAA,IAAI,cAAA,IAAkB,CAAC,cAAA,CAAe,uBAAA,CAAwB,GAAG,CAAA,EAAG;AAClE,QAAA,IAAI,QAAQ,KAAA,EAAO;AACjB,UAAA,MAAM,MAAA,GAAS,eAAA,CAAgB,cAAA,EAAgB,GAAG,CAAA;AAClD,UAAA,OAAA,CAAQ,GAAA;AAAA,YACN,uDAAA;AAAA,YACA,GAAA;AAAA,YACA;AAAA,WACF;AAAA,QACF;AAAA,MACF,CAAA,MAAO;AACL,QAAA,mBAAA,GAAsB,iBAAA,EAAkB;AACxC,QAAA,OAAA,CAAQ,GAAA,CAAI,eAAe,mBAAmB,CAAA;AAE9C,QAAA,IAAI,QAAQ,KAAA,EAAO;AACjB,UAAA,OAAA,CAAQ,GAAA;AAAA,YACN,8CAAA;AAAA,YACA,GAAA;AAAA,YACA;AAAA,WACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAGA,IAAA,MAAM,SAASA,KAAAA,EAAM,MAAA,KACf,iBAAiB,OAAA,GAAU,KAAA,CAAM,SAAS,MAAA,CAAA,IAC3C,KAAA;AAGL,IAAA,MAAM,SAAA,GAAY,WAAA,CAAY,UAAA,GAAa,WAAA,CAAY,GAAA,EAAI;AAC3D,IAAA,MAAM,eAAe,aAAA,CAAe,KAAA,EAAO,EAAE,GAAGA,KAAAA,EAAM,SAAS,CAAA;AAG/D,IAAA,IAAI,mBAAA,IAAuB,cAAa,EAAG;AACzC,MAAA,YAAA,CAAa,IAAA;AAAA,QACX,CAAC,QAAA,KAAa;AACZ,UAAA,MAAM,OAAA,GAAU,WAAA,CAAY,UAAA,GAAa,WAAA,CAAY,GAAA,EAAI;AACzD,UAAA,MAAM,MAAA,GAAS,iBAAiB,mBAAoB,CAAA;AACpD,UAAA,IAAI,MAAA,EAAQ;AACV,YAAA,IAAI,QAAA;AACJ,YAAA,IAAI;AAAE,cAAA,QAAA,GAAW,IAAI,GAAA,CAAI,GAAA,EAAK,MAAA,CAAO,QAAA,CAAS,MAAM,CAAA,CAAE,QAAA;AAAA,YAAU,CAAA,CAAA,MAAQ;AAAE,cAAA,QAAA,GAAW,GAAA;AAAA,YAAK;AAC1F,YAAA,UAAA,CAAW,MAAA,CAAO,SAAS,MAAA,CAAO,MAAA,EAAQ,WAAW,QAAQ,CAAA,CAAA,EAAI,WAAW,OAAA,EAAS;AAAA,cACnF,aAAA,EAAe,MAAA;AAAA,cACf,UAAA,EAAY,GAAA;AAAA,cACZ,oBAAoB,QAAA,CAAS;AAAA,aAC9B,CAAA;AAAA,UACH;AAAA,QACF,CAAA;AAAA,QACA,MAAM;AACJ,UAAA,MAAM,OAAA,GAAU,WAAA,CAAY,UAAA,GAAa,WAAA,CAAY,GAAA,EAAI;AACzD,UAAA,MAAM,MAAA,GAAS,iBAAiB,mBAAoB,CAAA;AACpD,UAAA,IAAI,MAAA,EAAQ;AACV,YAAA,IAAI,QAAA;AACJ,YAAA,IAAI;AAAE,cAAA,QAAA,GAAW,IAAI,GAAA,CAAI,GAAA,EAAK,MAAA,CAAO,QAAA,CAAS,MAAM,CAAA,CAAE,QAAA;AAAA,YAAU,CAAA,CAAA,MAAQ;AAAE,cAAA,QAAA,GAAW,GAAA;AAAA,YAAK;AAC1F,YAAA,UAAA,CAAW,MAAA,CAAO,SAAS,MAAA,CAAO,MAAA,EAAQ,WAAW,QAAQ,CAAA,CAAA,EAAI,WAAW,OAAA,EAAS;AAAA,cACnF,aAAA,EAAe,MAAA;AAAA,cACf,UAAA,EAAY;AAAA,aACb,CAAA;AAAA,UACH;AAAA,QACF;AAAA,OACF;AAAA,IACF;AAEA,IAAA,OAAO,YAAA;AAAA,EACT,CAAA;AACF;AAKA,SAAS,mBAAA,GAA4B;AAGnC,EAAA,eAAA,GAAkB,eAAe,SAAA,CAAU,IAAA;AAC3C,EAAA,2BAAA,GAA8B,eAAe,SAAA,CAAU,gBAAA;AAGvD,EAAA,MAAM,iBAAA,uBAAwB,OAAA,EAAwB;AAGtD,EAAA,cAAA,CAAe,SAAA,CAAU,gBAAA,GAAmB,SAC1C,IAAA,EACA,KAAA,EACM;AACN,IAAA,IAAI,IAAA,CAAK,WAAA,EAAY,KAAM,aAAA,EAAe;AACxC,MAAA,iBAAA,CAAkB,IAAI,IAAI,CAAA;AAAA,IAC5B;AAEA,IAAA,OAAO,2BAAA,CAA6B,IAAA,CAAK,IAAA,EAAM,IAAA,EAAM,KAAK,CAAA;AAAA,EAC5D,CAAA;AAGA,EAAA,cAAA,CAAe,SAAA,CAAU,OAAO,SAC9B,MAAA,EACA,KACA,KAAA,GAAiB,IAAA,EACjB,UACA,QAAA,EACM;AAGN,IAAA,MAAM,MAAA,GAAS,gBAAiB,IAAA,CAAK,IAAA,EAAM,QAAQ,GAAA,EAAK,KAAA,EAAO,UAAU,QAAQ,CAAA;AAGjF,IAAA,MAAM,SAAS,OAAO,GAAA,KAAQ,QAAA,GAAW,GAAA,GAAM,IAAI,QAAA,EAAS;AAG5D,IAAA,MAAM,GAAA,GAAM,IAAA;AACZ,IAAA,MAAM,6BAA6B,GAAA,CAAI,kBAAA;AAEvC,IAAA,GAAA,CAAI,kBAAA,GAAqB,SAAU,KAAA,EAAc;AAE/C,MAAA,IAAI,GAAA,CAAI,UAAA,KAAe,cAAA,CAAe,MAAA,EAAQ;AAE5C,QAAA,IAAI,CAAC,iBAAA,CAAkB,GAAA,CAAI,GAAG,CAAA,EAAG;AAE/B,UAAA,IAAI,cAAA,IAAkB,CAAC,cAAA,CAAe,uBAAA,CAAwB,MAAM,CAAA,EAAG;AACrE,YAAA,IAAI,QAAQ,KAAA,EAAO;AACjB,cAAA,MAAM,MAAA,GAAS,eAAA,CAAgB,cAAA,EAAgB,MAAM,CAAA;AACrD,cAAA,OAAA,CAAQ,GAAA;AAAA,gBACN,qDAAA;AAAA,gBACA,MAAA;AAAA,gBACA;AAAA,eACF;AAAA,YACF;AAAA,UACF,CAAA,MAAO;AAEL,YAAA,IAAI;AACF,cAAA,MAAM,cAAc,iBAAA,EAAkB;AAEtC,cAAA,2BAAA,CAA6B,IAAA,CAAK,GAAA,EAAK,aAAA,EAAe,WAAW,CAAA;AAEjE,cAAA,IAAI,QAAQ,KAAA,EAAO;AACjB,gBAAA,OAAA,CAAQ,GAAA;AAAA,kBACN,4CAAA;AAAA,kBACA,MAAA;AAAA,kBACA;AAAA,iBACF;AAAA,cACF;AAAA,YACF,SAAS,KAAA,EAAO;AAEd,cAAA,IAAI,QAAQ,KAAA,EAAO;AACjB,gBAAA,OAAA,CAAQ,IAAA;AAAA,kBACN,oDAAA;AAAA,kBACA;AAAA,iBACF;AAAA,cACF;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAGA,MAAA,IAAI,0BAAA,EAA4B;AAC9B,QAAA,OAAO,0BAAA,CAA2B,IAAA,CAAK,GAAA,EAAK,KAAK,CAAA;AAAA,MACnD;AAAA,IACF,CAAA;AAEA,IAAA,OAAO,MAAA;AAAA,EACT,CAAA;AACF;AAMA,SAAS,eAAe,UAAA,EAAoC;AAE1D,EAAA,IAAI,CAAC,UAAA,CAAW,OAAA,IAAW,OAAO,UAAA,CAAW,YAAY,QAAA,EAAU;AACjE,IAAA,MAAM,IAAI,MAAM,6DAA6D,CAAA;AAAA,EAC/E;AAEA,EAAA,IAAI,UAAA,CAAW,OAAA,CAAQ,MAAA,KAAW,CAAA,EAAG;AACnC,IAAA,MAAM,IAAI,MAAM,4CAA4C,CAAA;AAAA,EAC9D;AAEA,EAAA,IAAI,UAAA,CAAW,OAAA,CAAQ,MAAA,GAAS,GAAA,EAAK;AACnC,IAAA,OAAA,CAAQ,IAAA;AAAA,MACN;AAAA,KACF;AAAA,EACF;AAGA,EAAA,IAAI,WAAW,OAAA,EAAS;AACtB,IAAA,MAAM,EAAE,cAAA,EAAgB,cAAA,EAAe,GAAI,UAAA,CAAW,OAAA;AAGtD,IAAA,IAAA,CACG,CAAC,cAAA,IAAkB,cAAA,CAAe,MAAA,KAAW,CAAA,MAC7C,CAAC,cAAA,IAAkB,cAAA,CAAe,MAAA,KAAW,CAAA,CAAA,IAC9C,CAAC,UAAA,CAAW,OAAA,CAAQ,qBACpB,CAAC,UAAA,CAAW,QAAQ,UAAA,EACpB;AACA,MAAA,OAAA,CAAQ,IAAA;AAAA,QACN;AAAA,OACF;AAAA,IACF;AAGA,IAAA,IAAI,kBAAkB,cAAA,EAAgB;AACpC,MAAA,MAAM,UAAU,cAAA,CAAe,MAAA;AAAA,QAAO,CAAC,YACrC,cAAA,CAAe,IAAA;AAAA,UAAK,CAAC,YACnB,OAAA,CAAQ,WAAA,GAAc,QAAA,CAAS,OAAA,CAAQ,aAAa;AAAA;AACtD,OACF;AACA,MAAA,IAAI,OAAA,CAAQ,SAAS,CAAA,EAAG;AACtB,QAAA,OAAA,CAAQ,IAAA;AAAA,UACN,qFAAA;AAAA,UACA;AAAA,SACF;AAAA,MACF;AAAA,IACF;AAGA,IAAA,MAAM,UAAA,GAAa;AAAA,MACjB,GAAI,kBAAkB,EAAC;AAAA,MACvB,GAAI,kBAAkB;AAAC,KACzB;AACA,IAAA,UAAA,CAAW,OAAA,CAAQ,CAAC,MAAA,KAAW;AAC7B,MAAA,IAAI,MAAA,CAAO,QAAA,CAAS,KAAK,CAAA,EAAG;AAC1B,QAAA,OAAA,CAAQ,IAAA;AAAA,UACN,yBAAyB,MAAM,CAAA,iFAAA;AAAA,SACjC;AAAA,MACF;AAAA,IACF,CAAC,CAAA;AAAA,EACH;AACF","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.
|
|
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.
|
|
47
|
-
"@opentelemetry/core": "^2.
|
|
48
|
-
"@opentelemetry/exporter-trace-otlp-http": "^0.
|
|
49
|
-
"@opentelemetry/instrumentation": "^0.
|
|
50
|
-
"@opentelemetry/instrumentation-document-load": "^0.
|
|
51
|
-
"@opentelemetry/instrumentation-fetch": "^0.
|
|
52
|
-
"@opentelemetry/instrumentation-xml-http-request": "^0.
|
|
53
|
-
"@opentelemetry/resources": "^2.
|
|
54
|
-
"@opentelemetry/sdk-trace-base": "^2.
|
|
55
|
-
"@opentelemetry/sdk-trace-web": "^2.
|
|
56
|
-
"web-vitals": "^5.
|
|
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.
|
|
59
|
+
"@types/node": "^25.6.0",
|
|
60
60
|
"rimraf": "^6.1.3",
|
|
61
61
|
"tsup": "^8.5.1",
|
|
62
|
-
"typescript": "^
|
|
63
|
-
"vitest": "^4.
|
|
64
|
-
"vitest-mock-extended": "^
|
|
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
|
-
|
|
195
|
-
headers.set('traceparent',
|
|
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
|
-
|
|
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
|
-
|
|
209
|
-
|
|
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
|
+
}
|