autotel-web 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +921 -0
- package/dist/index.d.ts +310 -0
- package/dist/index.js +391 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -0
- package/src/functional.ts +197 -0
- package/src/index.ts +70 -0
- package/src/init.privacy.test.ts +179 -0
- package/src/init.ts +415 -0
- package/src/privacy.test.ts +404 -0
- package/src/privacy.ts +261 -0
- package/src/traceparent.test.ts +49 -0
- package/src/traceparent.ts +105 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Privacy controls for autotel-web
|
|
3
|
+
*
|
|
4
|
+
* Provides origin filtering and privacy signal respecting (DNT, GPC)
|
|
5
|
+
* to ensure compliance with GDPR, CCPA, and user privacy preferences.
|
|
6
|
+
*/
|
|
7
|
+
interface PrivacyConfig {
|
|
8
|
+
/**
|
|
9
|
+
* Only inject traceparent headers on requests to these origins (whitelist)
|
|
10
|
+
*
|
|
11
|
+
* If specified, traceparent will ONLY be injected on matching origins.
|
|
12
|
+
* Origins are matched using substring matching (e.g., "example.com" matches "https://api.example.com").
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* {
|
|
17
|
+
* allowedOrigins: ['api.myapp.com', 'myapp.com']
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
allowedOrigins?: string[];
|
|
22
|
+
/**
|
|
23
|
+
* Never inject traceparent headers on requests to these origins (blacklist)
|
|
24
|
+
*
|
|
25
|
+
* Takes precedence over allowedOrigins.
|
|
26
|
+
* Origins are matched using substring matching.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* {
|
|
31
|
+
* blockedOrigins: ['analytics.google.com', 'facebook.com']
|
|
32
|
+
* }
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
blockedOrigins?: string[];
|
|
36
|
+
/**
|
|
37
|
+
* Respect the Do Not Track (DNT) browser setting
|
|
38
|
+
*
|
|
39
|
+
* If true and user has DNT enabled, no traceparent headers will be injected.
|
|
40
|
+
*
|
|
41
|
+
* @default false
|
|
42
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/Navigator/doNotTrack
|
|
43
|
+
*/
|
|
44
|
+
respectDoNotTrack?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Respect the Global Privacy Control (GPC) browser signal
|
|
47
|
+
*
|
|
48
|
+
* If true and user has GPC enabled, no traceparent headers will be injected.
|
|
49
|
+
*
|
|
50
|
+
* @default false
|
|
51
|
+
* @see https://globalprivacycontrol.org/
|
|
52
|
+
*/
|
|
53
|
+
respectGPC?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Minimal browser SDK initialization
|
|
58
|
+
*
|
|
59
|
+
* Patches fetch() and XMLHttpRequest to automatically inject W3C traceparent headers.
|
|
60
|
+
* NO OpenTelemetry dependencies - just native browser APIs.
|
|
61
|
+
*
|
|
62
|
+
* Bundle size: ~2-5KB gzipped
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
interface AutotelWebConfig {
|
|
66
|
+
/**
|
|
67
|
+
* Service name for the browser application
|
|
68
|
+
* Used only for logging/debugging - not sent in headers
|
|
69
|
+
*/
|
|
70
|
+
service: string;
|
|
71
|
+
/**
|
|
72
|
+
* Enable debug logging to console
|
|
73
|
+
* @default false
|
|
74
|
+
*/
|
|
75
|
+
debug?: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Enable automatic traceparent injection on fetch calls
|
|
78
|
+
* @default true
|
|
79
|
+
*/
|
|
80
|
+
instrumentFetch?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Enable automatic traceparent injection on XMLHttpRequest
|
|
83
|
+
* @default true
|
|
84
|
+
*/
|
|
85
|
+
instrumentXHR?: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Privacy controls for traceparent header injection
|
|
88
|
+
*
|
|
89
|
+
* Configure origin filtering and privacy signal respecting (DNT, GPC)
|
|
90
|
+
* to ensure compliance with GDPR, CCPA, and user privacy preferences.
|
|
91
|
+
*
|
|
92
|
+
* @example Basic origin filtering
|
|
93
|
+
* ```typescript
|
|
94
|
+
* {
|
|
95
|
+
* privacy: {
|
|
96
|
+
* allowedOrigins: ['api.myapp.com'], // Only inject on API calls
|
|
97
|
+
* respectDoNotTrack: true // Respect user's DNT setting
|
|
98
|
+
* }
|
|
99
|
+
* }
|
|
100
|
+
* ```
|
|
101
|
+
*
|
|
102
|
+
* @example Block third-party analytics
|
|
103
|
+
* ```typescript
|
|
104
|
+
* {
|
|
105
|
+
* privacy: {
|
|
106
|
+
* blockedOrigins: ['analytics.google.com', 'facebook.com']
|
|
107
|
+
* }
|
|
108
|
+
* }
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
privacy?: PrivacyConfig;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Initialize autotel-web
|
|
115
|
+
*
|
|
116
|
+
* Patches fetch() and XMLHttpRequest to auto-inject traceparent headers.
|
|
117
|
+
*
|
|
118
|
+
* **SSR-safe:** Safe to call in SSR environments (checks for window).
|
|
119
|
+
* **Call once:** Subsequent calls are ignored.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```typescript
|
|
123
|
+
* import { init } from 'autotel-web'
|
|
124
|
+
*
|
|
125
|
+
* init({ service: 'my-frontend-app' })
|
|
126
|
+
*
|
|
127
|
+
* // Now all fetch/XHR calls include traceparent headers!
|
|
128
|
+
* fetch('/api/users') // <-- traceparent header automatically injected
|
|
129
|
+
* ```
|
|
130
|
+
*
|
|
131
|
+
* @example With React (client-only)
|
|
132
|
+
* ```typescript
|
|
133
|
+
* import { useEffect } from 'react'
|
|
134
|
+
* import { init } from 'autotel-web'
|
|
135
|
+
*
|
|
136
|
+
* function App() {
|
|
137
|
+
* useEffect(() => {
|
|
138
|
+
* init({ service: 'my-spa' })
|
|
139
|
+
* }, [])
|
|
140
|
+
*
|
|
141
|
+
* return <div>...</div>
|
|
142
|
+
* }
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
declare function init(userConfig: AutotelWebConfig): void;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Minimal functional API for browser tracing
|
|
149
|
+
*
|
|
150
|
+
* These are DX wrappers that DON'T create real browser spans.
|
|
151
|
+
* The real spans and timing happen on the backend via Autotel.
|
|
152
|
+
*
|
|
153
|
+
* The browser's job is just to propagate trace context via headers.
|
|
154
|
+
*/
|
|
155
|
+
/**
|
|
156
|
+
* Minimal trace context (browser-side)
|
|
157
|
+
*
|
|
158
|
+
* This is a lightweight version that just holds IDs.
|
|
159
|
+
* NO actual span object - the real span lives on the backend.
|
|
160
|
+
*/
|
|
161
|
+
interface TraceContext {
|
|
162
|
+
/** Current trace ID (may be extracted from request) */
|
|
163
|
+
readonly traceId: string;
|
|
164
|
+
/** Current span ID (generated for this browser "span") */
|
|
165
|
+
readonly spanId: string;
|
|
166
|
+
/** Correlation ID (same as trace ID) */
|
|
167
|
+
readonly correlationId: string;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Wrap a function with trace() for better DX
|
|
171
|
+
*
|
|
172
|
+
* **Important:** This does NOT create real spans in the browser.
|
|
173
|
+
* It's purely for API consistency. The real tracing happens on the backend.
|
|
174
|
+
*
|
|
175
|
+
* The traceparent header is automatically injected by init() on fetch/XHR calls.
|
|
176
|
+
*
|
|
177
|
+
* @example Basic usage
|
|
178
|
+
* ```typescript
|
|
179
|
+
* const fetchUser = trace(async (id: string) => {
|
|
180
|
+
* const response = await fetch(`/api/users/${id}`)
|
|
181
|
+
* return response.json()
|
|
182
|
+
* })
|
|
183
|
+
* ```
|
|
184
|
+
*
|
|
185
|
+
* @example With context (for accessing trace IDs)
|
|
186
|
+
* ```typescript
|
|
187
|
+
* const fetchUser = trace(ctx => async (id: string) => {
|
|
188
|
+
* console.log('Trace ID:', ctx.traceId)
|
|
189
|
+
* const response = await fetch(`/api/users/${id}`)
|
|
190
|
+
* return response.json()
|
|
191
|
+
* })
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
declare function trace<T extends (...args: any[]) => any>(fn: T | ((ctx: TraceContext) => T)): T;
|
|
195
|
+
/**
|
|
196
|
+
* Get the current trace context (if any)
|
|
197
|
+
*
|
|
198
|
+
* @returns Current trace context or undefined
|
|
199
|
+
*
|
|
200
|
+
* @example
|
|
201
|
+
* ```typescript
|
|
202
|
+
* const ctx = getActiveContext()
|
|
203
|
+
* if (ctx) {
|
|
204
|
+
* console.log('Trace ID:', ctx.traceId)
|
|
205
|
+
* }
|
|
206
|
+
* ```
|
|
207
|
+
*/
|
|
208
|
+
declare function getActiveContext(): TraceContext | undefined;
|
|
209
|
+
/**
|
|
210
|
+
* Manual helper to create a traceparent header
|
|
211
|
+
*
|
|
212
|
+
* Useful if you need to manually set headers or disable auto-instrumentation.
|
|
213
|
+
*
|
|
214
|
+
* @returns W3C traceparent header value
|
|
215
|
+
*
|
|
216
|
+
* @example
|
|
217
|
+
* ```typescript
|
|
218
|
+
* import { init, getTraceparent } from 'autotel-web'
|
|
219
|
+
*
|
|
220
|
+
* // Disable auto-instrumentation
|
|
221
|
+
* init({ service: 'my-app', instrumentFetch: false })
|
|
222
|
+
*
|
|
223
|
+
* // Manually inject headers
|
|
224
|
+
* fetch('/api/data', {
|
|
225
|
+
* headers: {
|
|
226
|
+
* 'traceparent': getTraceparent()
|
|
227
|
+
* }
|
|
228
|
+
* })
|
|
229
|
+
* ```
|
|
230
|
+
*/
|
|
231
|
+
declare function getTraceparent(): string;
|
|
232
|
+
/**
|
|
233
|
+
* Extract trace context from a traceparent header
|
|
234
|
+
*
|
|
235
|
+
* Useful for SSR scenarios where you want to continue a trace from the server.
|
|
236
|
+
*
|
|
237
|
+
* @param traceparent - W3C traceparent header value
|
|
238
|
+
* @returns Parsed trace context or undefined if invalid
|
|
239
|
+
*
|
|
240
|
+
* @example
|
|
241
|
+
* ```typescript
|
|
242
|
+
* // In an SSR handler
|
|
243
|
+
* const traceparent = request.headers.get('traceparent')
|
|
244
|
+
* if (traceparent) {
|
|
245
|
+
* const ctx = extractContext(traceparent)
|
|
246
|
+
* console.log('Continuing trace:', ctx?.traceId)
|
|
247
|
+
* }
|
|
248
|
+
* ```
|
|
249
|
+
*/
|
|
250
|
+
declare function extractContext(traceparent: string): TraceContext | undefined;
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Minimal W3C Trace Context implementation for browser
|
|
254
|
+
*
|
|
255
|
+
* Generates traceparent headers in the W3C format:
|
|
256
|
+
* traceparent: 00-{trace-id}-{span-id}-{flags}
|
|
257
|
+
*
|
|
258
|
+
* No OpenTelemetry dependencies - just crypto.getRandomValues()
|
|
259
|
+
*/
|
|
260
|
+
/**
|
|
261
|
+
* Generate a random 128-bit (16 byte) trace ID
|
|
262
|
+
* @returns 32 character hex string
|
|
263
|
+
*/
|
|
264
|
+
declare function generateTraceId(): string;
|
|
265
|
+
/**
|
|
266
|
+
* Generate a random 64-bit (8 byte) span ID
|
|
267
|
+
* @returns 16 character hex string
|
|
268
|
+
*/
|
|
269
|
+
declare function generateSpanId(): string;
|
|
270
|
+
/**
|
|
271
|
+
* Create a W3C traceparent header value
|
|
272
|
+
*
|
|
273
|
+
* Format: version-traceId-spanId-flags
|
|
274
|
+
* - version: 00 (W3C Trace Context spec)
|
|
275
|
+
* - traceId: 128-bit hex (32 chars)
|
|
276
|
+
* - spanId: 64-bit hex (16 chars)
|
|
277
|
+
* - flags: 01 (sampled)
|
|
278
|
+
*
|
|
279
|
+
* @param traceId - Optional existing trace ID (for continuing traces)
|
|
280
|
+
* @param parentSpanId - Optional parent span ID (unused in browser, included for API compat)
|
|
281
|
+
* @returns W3C traceparent header value
|
|
282
|
+
*
|
|
283
|
+
* @example
|
|
284
|
+
* ```typescript
|
|
285
|
+
* const header = createTraceparent()
|
|
286
|
+
* // "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
|
|
287
|
+
* ```
|
|
288
|
+
*/
|
|
289
|
+
declare function createTraceparent(traceId?: string, _parentSpanId?: string): string;
|
|
290
|
+
/**
|
|
291
|
+
* Parse a traceparent header value
|
|
292
|
+
* Useful for extracting trace context from incoming headers
|
|
293
|
+
*
|
|
294
|
+
* @param traceparent - W3C traceparent header value
|
|
295
|
+
* @returns Parsed components or null if invalid
|
|
296
|
+
*
|
|
297
|
+
* @example
|
|
298
|
+
* ```typescript
|
|
299
|
+
* const parsed = parseTraceparent('00-4bf92f...0e4736-00f067...902b7-01')
|
|
300
|
+
* console.log(parsed?.traceId) // "4bf92f...0e4736"
|
|
301
|
+
* ```
|
|
302
|
+
*/
|
|
303
|
+
declare function parseTraceparent(traceparent: string): {
|
|
304
|
+
version: string;
|
|
305
|
+
traceId: string;
|
|
306
|
+
spanId: string;
|
|
307
|
+
flags: string;
|
|
308
|
+
} | null;
|
|
309
|
+
|
|
310
|
+
export { type AutotelWebConfig, type PrivacyConfig, type TraceContext, createTraceparent, extractContext, generateSpanId, generateTraceId, getActiveContext, getTraceparent, init, parseTraceparent, trace };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
// src/traceparent.ts
|
|
2
|
+
function randomHex(bytes) {
|
|
3
|
+
const buffer = new Uint8Array(bytes);
|
|
4
|
+
crypto.getRandomValues(buffer);
|
|
5
|
+
return Array.from(buffer).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
6
|
+
}
|
|
7
|
+
function generateTraceId() {
|
|
8
|
+
return randomHex(16);
|
|
9
|
+
}
|
|
10
|
+
function generateSpanId() {
|
|
11
|
+
return randomHex(8);
|
|
12
|
+
}
|
|
13
|
+
function createTraceparent(traceId, _parentSpanId) {
|
|
14
|
+
const tid = traceId ?? generateTraceId();
|
|
15
|
+
const sid = generateSpanId();
|
|
16
|
+
const flags = "01";
|
|
17
|
+
return `00-${tid}-${sid}-${flags}`;
|
|
18
|
+
}
|
|
19
|
+
function parseTraceparent(traceparent) {
|
|
20
|
+
const parts = traceparent.split("-");
|
|
21
|
+
if (parts.length !== 4) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const [version, traceId, spanId, flags] = parts;
|
|
25
|
+
if (version.length !== 2 || traceId.length !== 32 || spanId.length !== 16 || flags.length !== 2) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
return { version, traceId, spanId, flags };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/privacy.ts
|
|
32
|
+
var PrivacyManager = class {
|
|
33
|
+
constructor(config2) {
|
|
34
|
+
this.config = config2;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Check if traceparent header should be injected for a given URL
|
|
38
|
+
*
|
|
39
|
+
* Decision order:
|
|
40
|
+
* 1. Check Do Not Track (if enabled)
|
|
41
|
+
* 2. Check Global Privacy Control (if enabled)
|
|
42
|
+
* 3. Check blockedOrigins (explicit deny)
|
|
43
|
+
* 4. Check allowedOrigins (explicit allow, if configured)
|
|
44
|
+
* 5. Default: allow
|
|
45
|
+
*
|
|
46
|
+
* @param url - Full URL or relative path of the request
|
|
47
|
+
* @returns true if traceparent should be injected, false otherwise
|
|
48
|
+
*/
|
|
49
|
+
shouldInjectTraceparent(url) {
|
|
50
|
+
if (this.config.respectDoNotTrack && this.isDoNotTrackEnabled()) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
if (this.config.respectGPC && this.isGPCEnabled()) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
const targetOrigin = this.extractOrigin(url);
|
|
57
|
+
if (this.config.blockedOrigins && this.matchesAnyOrigin(targetOrigin, this.config.blockedOrigins)) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
if (this.config.allowedOrigins && this.config.allowedOrigins.length > 0) {
|
|
61
|
+
return this.matchesAnyOrigin(targetOrigin, this.config.allowedOrigins);
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Check if Do Not Track is enabled in the browser
|
|
67
|
+
*/
|
|
68
|
+
isDoNotTrackEnabled() {
|
|
69
|
+
if (typeof navigator === "undefined") return false;
|
|
70
|
+
return navigator.doNotTrack === "1";
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Check if Global Privacy Control is enabled in the browser
|
|
74
|
+
*/
|
|
75
|
+
isGPCEnabled() {
|
|
76
|
+
if (typeof navigator === "undefined") return false;
|
|
77
|
+
const nav = navigator;
|
|
78
|
+
return nav.globalPrivacyControl === true;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Extract origin from a URL (handles both absolute and relative URLs)
|
|
82
|
+
*
|
|
83
|
+
* @param url - Full URL or relative path
|
|
84
|
+
* @returns Origin string (e.g., "https://api.example.com")
|
|
85
|
+
*/
|
|
86
|
+
extractOrigin(url) {
|
|
87
|
+
try {
|
|
88
|
+
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
89
|
+
return new URL(url).origin;
|
|
90
|
+
}
|
|
91
|
+
if (typeof window !== "undefined") {
|
|
92
|
+
return new URL(url, window.location.href).origin;
|
|
93
|
+
}
|
|
94
|
+
return "";
|
|
95
|
+
} catch {
|
|
96
|
+
return "";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Check if a target origin matches any of the configured origins
|
|
101
|
+
*
|
|
102
|
+
* Uses substring matching for flexibility (e.g., "example.com" matches "https://api.example.com")
|
|
103
|
+
*
|
|
104
|
+
* @param targetOrigin - Origin to check
|
|
105
|
+
* @param configuredOrigins - List of allowed or blocked origins
|
|
106
|
+
* @returns true if any origin matches
|
|
107
|
+
*/
|
|
108
|
+
matchesAnyOrigin(targetOrigin, configuredOrigins) {
|
|
109
|
+
return configuredOrigins.some((configuredOrigin) => {
|
|
110
|
+
const normalizedTarget = targetOrigin.toLowerCase();
|
|
111
|
+
const normalizedConfigured = configuredOrigin.toLowerCase();
|
|
112
|
+
return normalizedTarget.includes(normalizedConfigured);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
function getDenialReason(privacyManager2, url) {
|
|
117
|
+
const config2 = privacyManager2.config;
|
|
118
|
+
if (config2.respectDoNotTrack && typeof navigator !== "undefined") {
|
|
119
|
+
if (navigator.doNotTrack === "1") {
|
|
120
|
+
return "Do Not Track is enabled";
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (config2.respectGPC && typeof navigator !== "undefined") {
|
|
124
|
+
const nav = navigator;
|
|
125
|
+
if (nav.globalPrivacyControl === true) {
|
|
126
|
+
return "Global Privacy Control is enabled";
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
let targetOrigin = "";
|
|
130
|
+
try {
|
|
131
|
+
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
132
|
+
targetOrigin = new URL(url).origin;
|
|
133
|
+
} else if (typeof window !== "undefined") {
|
|
134
|
+
targetOrigin = new URL(url, window.location.href).origin;
|
|
135
|
+
}
|
|
136
|
+
} catch {
|
|
137
|
+
return "Invalid URL";
|
|
138
|
+
}
|
|
139
|
+
if (config2.blockedOrigins) {
|
|
140
|
+
const blocked = config2.blockedOrigins.some(
|
|
141
|
+
(origin) => targetOrigin.toLowerCase().includes(origin.toLowerCase())
|
|
142
|
+
);
|
|
143
|
+
if (blocked) {
|
|
144
|
+
return `Origin ${targetOrigin} is in blockedOrigins list`;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (config2.allowedOrigins && config2.allowedOrigins.length > 0) {
|
|
148
|
+
const allowed = config2.allowedOrigins.some(
|
|
149
|
+
(origin) => targetOrigin.toLowerCase().includes(origin.toLowerCase())
|
|
150
|
+
);
|
|
151
|
+
if (!allowed) {
|
|
152
|
+
return `Origin ${targetOrigin} is not in allowedOrigins list`;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/init.ts
|
|
159
|
+
var isInitialized = false;
|
|
160
|
+
var config;
|
|
161
|
+
var privacyManager;
|
|
162
|
+
var originalFetch;
|
|
163
|
+
var originalXHROpen;
|
|
164
|
+
var originalXHRSetRequestHeader;
|
|
165
|
+
function init(userConfig) {
|
|
166
|
+
if (typeof window === "undefined") {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (isInitialized) {
|
|
170
|
+
if (userConfig.debug) {
|
|
171
|
+
console.warn("[autotel-web] Already initialized. Skipping.");
|
|
172
|
+
}
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
validateConfig(userConfig);
|
|
176
|
+
config = userConfig;
|
|
177
|
+
if (config.privacy) {
|
|
178
|
+
privacyManager = new PrivacyManager(config.privacy);
|
|
179
|
+
}
|
|
180
|
+
if (config.instrumentFetch !== false) {
|
|
181
|
+
patchFetch();
|
|
182
|
+
}
|
|
183
|
+
if (config.instrumentXHR !== false) {
|
|
184
|
+
patchXMLHttpRequest();
|
|
185
|
+
}
|
|
186
|
+
isInitialized = true;
|
|
187
|
+
if (config.debug) {
|
|
188
|
+
console.log("[autotel-web] Initialized successfully", {
|
|
189
|
+
service: config.service,
|
|
190
|
+
instrumentFetch: config.instrumentFetch !== false,
|
|
191
|
+
instrumentXHR: config.instrumentXHR !== false,
|
|
192
|
+
privacyEnabled: !!config.privacy,
|
|
193
|
+
privacyConfig: config.privacy ? {
|
|
194
|
+
allowedOrigins: config.privacy.allowedOrigins?.length ?? 0,
|
|
195
|
+
blockedOrigins: config.privacy.blockedOrigins?.length ?? 0,
|
|
196
|
+
respectDoNotTrack: config.privacy.respectDoNotTrack ?? false,
|
|
197
|
+
respectGPC: config.privacy.respectGPC ?? false
|
|
198
|
+
} : null
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function patchFetch() {
|
|
203
|
+
originalFetch = window.fetch.bind(window);
|
|
204
|
+
window.fetch = function(input, init2) {
|
|
205
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
206
|
+
const headers = new Headers(init2?.headers);
|
|
207
|
+
if (!headers.has("traceparent")) {
|
|
208
|
+
if (privacyManager && !privacyManager.shouldInjectTraceparent(url)) {
|
|
209
|
+
if (config?.debug) {
|
|
210
|
+
const reason = getDenialReason(privacyManager, url);
|
|
211
|
+
console.log(
|
|
212
|
+
"[autotel-web] Skipped traceparent on fetch (privacy):",
|
|
213
|
+
url,
|
|
214
|
+
reason
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
} else {
|
|
218
|
+
headers.set("traceparent", createTraceparent());
|
|
219
|
+
if (config?.debug) {
|
|
220
|
+
console.log(
|
|
221
|
+
"[autotel-web] Injected traceparent on fetch:",
|
|
222
|
+
url,
|
|
223
|
+
headers.get("traceparent")
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return originalFetch(input, { ...init2, headers });
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function patchXMLHttpRequest() {
|
|
232
|
+
originalXHROpen = XMLHttpRequest.prototype.open;
|
|
233
|
+
originalXHRSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
|
|
234
|
+
const xhrHasTraceparent = /* @__PURE__ */ new WeakSet();
|
|
235
|
+
XMLHttpRequest.prototype.setRequestHeader = function(name, value) {
|
|
236
|
+
if (name.toLowerCase() === "traceparent") {
|
|
237
|
+
xhrHasTraceparent.add(this);
|
|
238
|
+
}
|
|
239
|
+
return originalXHRSetRequestHeader.call(this, name, value);
|
|
240
|
+
};
|
|
241
|
+
XMLHttpRequest.prototype.open = function(method, url, async = true, username, password) {
|
|
242
|
+
const result = originalXHROpen.call(this, method, url, async, username, password);
|
|
243
|
+
const urlStr = typeof url === "string" ? url : url.toString();
|
|
244
|
+
const xhr = this;
|
|
245
|
+
const originalOnReadyStateChange = xhr.onreadystatechange;
|
|
246
|
+
xhr.onreadystatechange = function(event) {
|
|
247
|
+
if (xhr.readyState === XMLHttpRequest.OPENED) {
|
|
248
|
+
if (!xhrHasTraceparent.has(xhr)) {
|
|
249
|
+
if (privacyManager && !privacyManager.shouldInjectTraceparent(urlStr)) {
|
|
250
|
+
if (config?.debug) {
|
|
251
|
+
const reason = getDenialReason(privacyManager, urlStr);
|
|
252
|
+
console.log(
|
|
253
|
+
"[autotel-web] Skipped traceparent on XHR (privacy):",
|
|
254
|
+
urlStr,
|
|
255
|
+
reason
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
try {
|
|
260
|
+
const traceparent = createTraceparent();
|
|
261
|
+
originalXHRSetRequestHeader.call(xhr, "traceparent", traceparent);
|
|
262
|
+
if (config?.debug) {
|
|
263
|
+
console.log(
|
|
264
|
+
"[autotel-web] Injected traceparent on XHR:",
|
|
265
|
+
urlStr,
|
|
266
|
+
traceparent
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
} catch (error) {
|
|
270
|
+
if (config?.debug) {
|
|
271
|
+
console.warn(
|
|
272
|
+
"[autotel-web] Failed to inject traceparent on XHR:",
|
|
273
|
+
error
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (originalOnReadyStateChange) {
|
|
281
|
+
return originalOnReadyStateChange.call(xhr, event);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
return result;
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
function validateConfig(userConfig) {
|
|
288
|
+
if (!userConfig.service || typeof userConfig.service !== "string") {
|
|
289
|
+
throw new Error("[autotel-web] service name is required and must be a string");
|
|
290
|
+
}
|
|
291
|
+
if (userConfig.service.length === 0) {
|
|
292
|
+
throw new Error("[autotel-web] service name cannot be empty");
|
|
293
|
+
}
|
|
294
|
+
if (userConfig.service.length > 255) {
|
|
295
|
+
console.warn(
|
|
296
|
+
"[autotel-web] service name is very long (> 255 chars). Consider using a shorter name."
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
if (userConfig.privacy) {
|
|
300
|
+
const { allowedOrigins, blockedOrigins } = userConfig.privacy;
|
|
301
|
+
if ((!allowedOrigins || allowedOrigins.length === 0) && (!blockedOrigins || blockedOrigins.length === 0) && !userConfig.privacy.respectDoNotTrack && !userConfig.privacy.respectGPC) {
|
|
302
|
+
console.warn(
|
|
303
|
+
"[autotel-web] privacy config provided but all options are empty/disabled. This has no effect."
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
if (allowedOrigins && blockedOrigins) {
|
|
307
|
+
const overlap = allowedOrigins.filter(
|
|
308
|
+
(allowed) => blockedOrigins.some(
|
|
309
|
+
(blocked) => allowed.toLowerCase().includes(blocked.toLowerCase())
|
|
310
|
+
)
|
|
311
|
+
);
|
|
312
|
+
if (overlap.length > 0) {
|
|
313
|
+
console.warn(
|
|
314
|
+
"[autotel-web] Some allowedOrigins match blockedOrigins. Blocklist takes precedence:",
|
|
315
|
+
overlap
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
const allOrigins = [
|
|
320
|
+
...allowedOrigins ?? [],
|
|
321
|
+
...blockedOrigins ?? []
|
|
322
|
+
];
|
|
323
|
+
allOrigins.forEach((origin) => {
|
|
324
|
+
if (origin.includes("://")) {
|
|
325
|
+
console.warn(
|
|
326
|
+
`[autotel-web] Origin "${origin}" includes protocol (://) - this is usually not needed. Just use the domain name.`
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// src/functional.ts
|
|
334
|
+
var currentContext;
|
|
335
|
+
function trace(fn) {
|
|
336
|
+
const expectsContext = isFactoryPattern(fn);
|
|
337
|
+
if (expectsContext) {
|
|
338
|
+
return ((...args) => {
|
|
339
|
+
const ctx = createContext();
|
|
340
|
+
const actualFn = fn(ctx);
|
|
341
|
+
return actualFn(...args);
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
return fn;
|
|
345
|
+
}
|
|
346
|
+
function isFactoryPattern(fn) {
|
|
347
|
+
if (typeof fn !== "function") return false;
|
|
348
|
+
const fnStr = fn.toString();
|
|
349
|
+
const contextHints = ["ctx", "context", "traceContext"];
|
|
350
|
+
return contextHints.some((hint) => {
|
|
351
|
+
const regex = new RegExp(`^\\s*(?:async\\s+)?(?:function\\s*)?\\(?\\s*${hint}\\s*[,)]`);
|
|
352
|
+
return regex.test(fnStr);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
function createContext() {
|
|
356
|
+
const traceparent = createTraceparent();
|
|
357
|
+
const parsed = parseTraceparent(traceparent);
|
|
358
|
+
if (!parsed) {
|
|
359
|
+
return {
|
|
360
|
+
traceId: "",
|
|
361
|
+
spanId: "",
|
|
362
|
+
correlationId: ""
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
const ctx = {
|
|
366
|
+
traceId: parsed.traceId,
|
|
367
|
+
spanId: parsed.spanId,
|
|
368
|
+
correlationId: parsed.traceId
|
|
369
|
+
};
|
|
370
|
+
currentContext = ctx;
|
|
371
|
+
return ctx;
|
|
372
|
+
}
|
|
373
|
+
function getActiveContext() {
|
|
374
|
+
return currentContext;
|
|
375
|
+
}
|
|
376
|
+
function getTraceparent() {
|
|
377
|
+
return createTraceparent();
|
|
378
|
+
}
|
|
379
|
+
function extractContext(traceparent) {
|
|
380
|
+
const parsed = parseTraceparent(traceparent);
|
|
381
|
+
if (!parsed) return void 0;
|
|
382
|
+
return {
|
|
383
|
+
traceId: parsed.traceId,
|
|
384
|
+
spanId: parsed.spanId,
|
|
385
|
+
correlationId: parsed.traceId
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export { createTraceparent, extractContext, generateSpanId, generateTraceId, getActiveContext, getTraceparent, init, parseTraceparent, trace };
|
|
390
|
+
//# sourceMappingURL=index.js.map
|
|
391
|
+
//# sourceMappingURL=index.js.map
|