autotel-web 1.7.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,7 +22,7 @@ Ultra-lightweight browser SDK for distributed tracing (**1.6KB gzipped**)
22
22
  ✅ **W3C trace propagation** - Automatic `traceparent` header injection on fetch/XHR
23
23
  ✅ **SSR-safe** - Works with Next.js, Remix, and other SSR frameworks
24
24
  ✅ **Framework-agnostic** - Works with React, Vue, Svelte, Angular, vanilla JS
25
- ✅ **No real spans** - Browser just propagates context, backend does real tracing
25
+ ✅ **No Zone.js** - [Context propagation without global patching](#context-propagation-without-zonejs)
26
26
 
27
27
  ## Installation
28
28
 
@@ -34,7 +34,14 @@ pnpm add autotel-web
34
34
  yarn add autotel-web
35
35
  ```
36
36
 
37
- **Important:** You do NOT need to install any `@opentelemetry/*` packages. This package has **zero dependencies**.
37
+ **Important:** You do NOT need to install any `@opentelemetry/*` packages yourself. One install gives you both modes below.
38
+
39
+ ### Lean vs Full mode
40
+
41
+ - **Lean (default)** – `import { init } from 'autotel-web'`. Zero dependencies, ~1.6KB gzipped. Only injects W3C `traceparent` on fetch/XHR; no real spans in the browser. Backend does the real tracing.
42
+ - **Full** – `import { initFull } from 'autotel-web/full'`. Real spans (navigation, fetch/XHR, optional user interaction), optional `http.client.network_timing` events, **Web Vitals** (LCP, INP, CLS, FCP, TTFB), **unhandled error capture**, optional long-task capture, sampling, and OTLP export. No Zone.js; bundle size is larger (~40–50KB gzipped). Use when you need client-side spans and export from the browser.
43
+
44
+ Use lean mode by default; use full mode when you need real browser spans and network timing. You can use dynamic import to load full mode only when needed: `import('autotel-web/full').then(({ initFull }) => initFull(config))`.
38
45
 
39
46
  ## Quick Start
40
47
 
@@ -78,6 +85,37 @@ app.get('/api/users', async (req, res) => {
78
85
 
79
86
  Open your observability platform (Honeycomb, Datadog, Jaeger, etc.) and see the complete trace from browser → backend → database!
80
87
 
88
+ ## Full mode (real spans)
89
+
90
+ 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.
91
+
92
+ ```typescript
93
+ import { initFull } from 'autotel-web/full'
94
+
95
+ initFull({
96
+ service: 'my-app',
97
+ endpoint: 'https://your-collector.example.com/v1/traces', // OTLP HTTP
98
+ sampleRate: 0.1, // e.g. 10% in production
99
+ captureNavigation: true, // document load spans (default: true)
100
+ captureFetch: true,
101
+ captureXHR: true,
102
+ captureNetworkTiming: true, // http.client.network_timing events (semantic-conventions#3385)
103
+ captureErrors: true, // unhandled errors → span.recordException (default: true)
104
+ captureWebVitals: true, // LCP, INP, CLS, FCP, TTFB as web_vitals span (default: true)
105
+ webVitals: { reportAllChanges: false }, // pass through to web-vitals (default: false)
106
+ captureLongTasks: false, // long-task spans (main thread >= 50ms); opt-in, can be noisy
107
+ copyHttpSpanAttributesToEvent: false,
108
+ userInteraction: {
109
+ enabled: true,
110
+ selectors: ['button', 'a', '[data-track]'],
111
+ },
112
+ privacy: { allowedOrigins: ['api.myapp.com'], respectDoNotTrack: true },
113
+ debug: false,
114
+ })
115
+ ```
116
+
117
+ With sensible defaults, **one `initFull()`** gives you: **navigation spans**, **fetch/XHR spans** with W3C propagation, **http.client.network_timing** events, **Web Vitals** (LCP, INP, CLS, FCP, TTFB) as a single `web_vitals` span per page, and **unhandled error capture** (window errors and unhandled promise rejections). Optional: **user interaction** spans (clicks on configurable selectors), **long-task** spans (opt-in via `captureLongTasks: true`), **sampling** (`sampleRate` or custom `sampler`), and **setAttribute** / **addEvent** / **span()** on the active span. Async context propagation is best-effort (no Zone.js).
118
+
81
119
  ## Framework Integration
82
120
 
83
121
  ### React (Client-Only)
@@ -348,6 +386,32 @@ init({
348
386
  })
349
387
  ```
350
388
 
389
+ ### `initFull(config)` (full mode)
390
+
391
+ Initialize full browser tracing. Import from `autotel-web/full`. Call once, client-side only.
392
+
393
+ ```typescript
394
+ interface AutotelWebFullConfig {
395
+ service: string
396
+ endpoint?: string // OTLP traces URL (e.g. https://api.example.com/v1/traces)
397
+ spanProcessor?: SpanProcessor // Custom processor instead of endpoint
398
+ sampleRate?: number // 0–1, e.g. 0.1 in production
399
+ sampler?: Sampler // Custom sampler (overrides sampleRate)
400
+ captureNavigation?: boolean // default true
401
+ captureFetch?: boolean // default true
402
+ captureXHR?: boolean // default true
403
+ captureNetworkTiming?: boolean // http.client.network_timing events (default true)
404
+ captureErrors?: boolean // unhandled errors → span.recordException (default true)
405
+ captureWebVitals?: boolean // LCP, INP, CLS, FCP, TTFB as web_vitals span (default true)
406
+ webVitals?: { reportAllChanges?: boolean } // pass through to web-vitals (default false)
407
+ captureLongTasks?: boolean // long-task spans (main thread >= 50ms); opt-in (default false)
408
+ copyHttpSpanAttributesToEvent?: boolean
409
+ userInteraction?: { enabled: boolean; selectors?: string[] }
410
+ privacy?: PrivacyConfig
411
+ debug?: boolean
412
+ }
413
+ ```
414
+
351
415
  ### `trace(fn)` and `trace(ctx => fn)`
352
416
 
353
417
  Wrap functions with automatic tracing.
@@ -370,32 +434,36 @@ const user = await fetchUser('123')
370
434
 
371
435
  ```typescript
372
436
  export const fetchUser = trace(ctx => async (id: string) => {
373
- ctx.setAttribute('user.id', id)
374
-
437
+ // ctx.traceId, ctx.spanId available (lean mode)
375
438
  const response = await fetch(`/api/users/${id}`)
376
- const user = await response.json()
377
-
378
- ctx.setAttribute('user.email', user.email)
379
- return user
439
+ return response.json()
380
440
  })
381
441
 
382
442
  // Usage
383
443
  const user = await fetchUser('123')
384
444
  ```
385
445
 
386
- ### `span(name, fn)`
446
+ For **custom attributes and real spans** in the browser, use **full mode** (`autotel-web/full`): `setAttribute`, `addEvent`, and `span()` operate on the active OTel span.
447
+
448
+ ### `span(name, fn)` (full mode only)
387
449
 
388
- Create a manual span for a block of code:
450
+ Create a manual span. Import from `autotel-web/full`:
389
451
 
390
452
  ```typescript
391
- import { span } from 'autotel-web'
453
+ import { span } from 'autotel-web/full'
392
454
 
393
- const result = await span('processData', async (ctx) => {
394
- ctx.setAttribute('data.size', data.length)
395
- return await processData(data)
455
+ const result = await span('processData', (s) => {
456
+ s.setAttribute('data.size', data.length)
457
+ const out = processData(data)
458
+ s.end()
459
+ return out
396
460
  })
397
461
  ```
398
462
 
463
+ ### `setAttribute(key, value)` / `addEvent(name, attributes)` (full mode only)
464
+
465
+ Set attributes or add events on the active span. Import from `autotel-web/full`.
466
+
399
467
  ### `getActiveContext()`
400
468
 
401
469
  Get the current active trace context:
@@ -769,11 +837,8 @@ export default function MyComponent() { ... }
769
837
 
770
838
  ## Bundle Size
771
839
 
772
- - **Unminified:** 5.05KB
773
- - **Gzipped:** **1.6KB** 🎉
774
- - **Brotli:** ~1.4KB (typical)
775
-
776
- **Zero dependencies.** No `@opentelemetry/*` packages. Just pure JavaScript using native `crypto.getRandomValues()`.
840
+ - **Lean mode** (`autotel-web`): **~1.6KB gzipped**. Zero dependencies. Pure JavaScript using native `crypto.getRandomValues()`.
841
+ - **Full mode** (`autotel-web/full`): ~40–50KB gzipped (includes OpenTelemetry SDK and instrumentations). No Zone.js. Use when you need real spans and export from the browser.
777
842
 
778
843
  ## Architecture: Header-Only Approach
779
844
 
@@ -820,13 +885,14 @@ The official OpenTelemetry browser SDK (`@opentelemetry/sdk-trace-web`) is a **f
820
885
 
821
886
  ### When to Use autotel-web (This Package)
822
887
 
823
- ✅ You only need **trace correlation** between frontend and backend
888
+ ✅ You only need **trace correlation** between frontend and backend → use **lean mode** (`init` from `autotel-web`)
889
+ ✅ You want **real browser spans and network timing** but **one install and no Zone.js** → use **full mode** (`initFull` from `autotel-web/full`)
824
890
  ✅ Your backend **already exports to a collector** (OTLP, Datadog, etc.)
825
- ✅ You want **minimal bundle size impact** (~1.6KB vs ~55KB)
891
+ ✅ You want **minimal bundle size impact** (~1.6KB for lean vs ~55KB for full OTel with Zone)
826
892
  ✅ You want to **avoid Zone.js** (conflicts with Angular, adds complexity)
827
893
  ✅ You prefer **zero dependencies** and simpler maintenance
828
894
 
829
- **Bottom Line:** If your backend already does tracing, you don't need full OpenTelemetry in the browser. Just propagate the trace context with autotel-web.
895
+ **Bottom Line:** For trace correlation only, use lean mode. For real browser spans and network timing with a single install and no Zone.js, use full mode (`autotel-web/full`).
830
896
 
831
897
  ## Performance Impact
832
898
 
@@ -850,6 +916,101 @@ autotel-web has **effectively zero performance overhead**:
850
916
 
851
917
  **Real-world impact:** Imperceptible. The network request itself takes orders of magnitude longer than the header injection.
852
918
 
919
+ ## Context Propagation Without Zone.js
920
+
921
+ Browser tracing with OpenTelemetry typically needs **context propagation**: when you start a span (e.g., "user clicked button"), any async work that follows—fetch, setTimeout, Promise chains—should run in that same trace context so the backend sees one continuous trace. In Node.js, OpenTelemetry uses AsyncLocalStorage to keep context across async boundaries. In the browser, there is no built-in "async context" that follows every boundary. **Zone.js** is the usual way to get that: it patches globals (setTimeout, Promise, fetch, etc.) so that any code that runs "later" still runs inside the same zone—and thus the same trace context.
922
+
923
+ This section explains when you might need Zone.js, its pitfalls, and how autotel-web gets you reliable tracing without it.
924
+
925
+ ### When You Might Think You Need Zone.js
926
+
927
+ You might think you need **async context that survives every boundary** when:
928
+
929
+ - You start a span in one place (e.g., click handler) and want **all** follow-up work—nested setTimeout, microtasks, fetch callbacks, requestAnimationFrame—to stay under that span without you wrapping each boundary
930
+ - You have deep or framework-driven async (e.g., React state updates → effects → fetch → more effects) and you can't or don't want to wrap every step
931
+ - You rely on "current span" or "current trace ID" in code that runs in callbacks you don't control (e.g., third-party lib that calls your callback after a delay)
932
+
933
+ In those cases, Zone.js gives you one execution context that follows the entire async tree. OpenTelemetry can attach the active span to that zone, and every callback runs in the same context.
934
+
935
+ ### Pitfalls of Zone.js
936
+
937
+ **1. Bundle size and cost**
938
+ Zone.js is on the order of ~12–15 KB minified/gzipped. For a browser SDK that aims to be small (autotel-web lean is ~1.6 KB), adding Zone significantly increases size for every user.
939
+
940
+ **2. Global patching**
941
+ Zone.js patches `setTimeout`, `setInterval`, `Promise`, `fetch`, `XHR`, `addEventListener`, and more. That can:
942
+
943
+ - Conflict with other libraries that also patch or depend on "vanilla" behavior
944
+ - Cause hard-to-debug issues in frameworks (e.g., Angular uses Zone; other frameworks don't and sometimes assume no patching)
945
+ - Break or confuse code that relies on exact timing or microtask ordering
946
+
947
+ **3. Framework and tooling friction**
948
+ Some bundlers, test runners, and frameworks have had issues with Zone (e.g., Next.js, Vite, or Jest in the past). You can end up debugging "why is my context wrong only in tests" or "why does this break in production build."
949
+
950
+ **4. Implicit behavior**
951
+ Context "just following" everywhere is convenient but implicit. When something goes wrong (wrong span, wrong trace), the cause is not obvious: it's "whatever Zone did." Explicit propagation (e.g., "this span covers this function") is easier to reason about and debug.
952
+
953
+ **5. Maintenance**
954
+ Zone.js is not part of the web platform. New APIs (e.g., new promise helpers, scheduler APIs) may need new patches. You depend on the Zone maintainers and the OpenTelemetry Zone plugin to keep up.
955
+
956
+ ### How autotel-web Works Without Zone.js
957
+
958
+ autotel-web is designed so you can get **useful, reliable browser → backend tracing** without Zone.js. It does that in three ways: lean mode, full mode with targeted instrumentation, and an explicit `trace()` API.
959
+
960
+ #### 1. Lean Mode: No Real Browser Spans
961
+
962
+ In **lean mode** (`init()` from `autotel-web`), the browser does **not** create real OpenTelemetry spans. It only:
963
+
964
+ - Injects the W3C `traceparent` header on every `fetch` and XHR request
965
+
966
+ So the "context" that matters is on the **request**: the backend receives `traceparent`, continues the trace, and does all span creation and export. There is no "current span" in the browser to lose across async boundaries. You don't need Zone for this.
967
+
968
+ **Use lean mode when:** You care about distributed traces (browser → API → services) and are fine with the backend owning the spans.
969
+
970
+ #### 2. Full Mode: Instrumentation That Propagates on the Wire
971
+
972
+ In **full mode** (`initFull()` from `autotel-web/full`), the browser **does** create real spans (navigation, fetch/XHR, Web Vitals, etc.). Context can be lost across arbitrary async boundaries (e.g., `setTimeout`), but autotel-web focuses on the boundaries that matter for tracing:
973
+
974
+ - **Fetch / XHR**: The OpenTelemetry fetch and XHR instrumentations wrap the real `fetch` and `XMLHttpRequest`. When your code calls `fetch()`, the instrumentation starts a span and injects `traceparent` into the request. When the response comes back, the callback runs in the same invocation chain as the one that called `fetch`, so the span is still active and can be ended. You don't cross a "lost" boundary in the common case.
975
+ - **Document load / navigation**: Handled by the document-load instrumentation; the span covers the load and its natural async work.
976
+
977
+ So for "user did something → app called fetch → backend continued the trace," context is preserved **along the path that the instrumentation controls**. You don't need Zone for that.
978
+
979
+ **Use full mode when:** You want real browser spans (and optional Web Vitals, errors, etc.) and your critical paths are "start → fetch/XHR → done" or "navigation."
980
+
981
+ #### 3. Explicit `trace()` for Critical Paths
982
+
983
+ When you have a flow that **does** cross boundaries where context would be lost (e.g., "click → setTimeout → fetch → update UI"), you can wrap the whole logical operation in **one** `trace()`:
984
+
985
+ ```typescript
986
+ import { trace } from 'autotel-web'
987
+
988
+ const handleConvert = trace(ctx => async () => {
989
+ setLoading(true)
990
+ await new Promise(r => setTimeout(r, 0)) // context still inside this trace()
991
+ const res = await fetch('/api/convert', { ... })
992
+ const data = await res.json()
993
+ setResult(data)
994
+ setLoading(false)
995
+ })
996
+ ```
997
+
998
+ Because the entire flow is inside a single `trace()` call, the fetch call runs "under" that logical span; the fetch instrumentation will see the active context (in full mode) or at least the header injection (in lean mode) keeps the same trace ID on the request. You don't need Zone to keep context inside that one async function.
999
+
1000
+ **Use explicit `trace()` when:** You have a clear "one user action → one chain of async work" and you're okay wrapping that chain once.
1001
+
1002
+ ### Summary: Zone.js vs autotel-web
1003
+
1004
+ | Need | Zone.js | autotel-web approach |
1005
+ |------|--------|------------------------|
1006
+ | Trace from browser to backend | Not required | Lean mode: inject `traceparent`; backend continues trace. |
1007
+ | Real browser spans (navigation, fetch, Web Vitals) | Not required for the common path | Full mode: instrument fetch/XHR and document load so context is preserved on the wire and in the main async chain. |
1008
+ | Context across arbitrary async (setTimeout, third‑party callbacks) | Helps | Explicit `trace()` around the whole flow; or accept best-effort. |
1009
+ | Small bundle, no global patching | N/A | Lean mode is ~1.6 KB; full mode avoids Zone. |
1010
+ | Fewer framework/tooling issues | N/A | No Zone dependency. |
1011
+
1012
+ **Bottom line:** You might need Zone.js if you want "current span" to follow **every** async boundary with no explicit wrapping. autotel-web avoids Zone by: (1) not creating real browser spans in lean mode, (2) in full mode, instrumenting the boundaries that matter for tracing (fetch, XHR, load), and (3) offering an explicit `trace()` so you can wrap critical paths once. That covers most real-world needs without Zone's pitfalls.
1013
+
853
1014
  ## Examples
854
1015
 
855
1016
  See the `apps/` directory at the repository root for complete working examples:
@@ -0,0 +1,89 @@
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/functional.ts
32
+ var currentContext;
33
+ function trace(fn) {
34
+ const expectsContext = isFactoryPattern(fn);
35
+ if (expectsContext) {
36
+ return ((...args) => {
37
+ const ctx = createContext();
38
+ const actualFn = fn(ctx);
39
+ return actualFn(...args);
40
+ });
41
+ }
42
+ return fn;
43
+ }
44
+ function isFactoryPattern(fn) {
45
+ if (typeof fn !== "function") return false;
46
+ const fnStr = fn.toString();
47
+ const contextHints = ["ctx", "context", "traceContext"];
48
+ return contextHints.some((hint) => {
49
+ const regex = new RegExp(`^\\s*(?:async\\s+)?(?:function\\s*)?\\(?\\s*${hint}\\s*[,)]`);
50
+ return regex.test(fnStr);
51
+ });
52
+ }
53
+ function createContext() {
54
+ const traceparent = createTraceparent();
55
+ const parsed = parseTraceparent(traceparent);
56
+ if (!parsed) {
57
+ return {
58
+ traceId: "",
59
+ spanId: "",
60
+ correlationId: ""
61
+ };
62
+ }
63
+ const ctx = {
64
+ traceId: parsed.traceId,
65
+ spanId: parsed.spanId,
66
+ correlationId: parsed.traceId
67
+ };
68
+ currentContext = ctx;
69
+ return ctx;
70
+ }
71
+ function getActiveContext() {
72
+ return currentContext;
73
+ }
74
+ function getTraceparent() {
75
+ return createTraceparent();
76
+ }
77
+ function extractContext(traceparent) {
78
+ const parsed = parseTraceparent(traceparent);
79
+ if (!parsed) return void 0;
80
+ return {
81
+ traceId: parsed.traceId,
82
+ spanId: parsed.spanId,
83
+ correlationId: parsed.traceId
84
+ };
85
+ }
86
+
87
+ export { createTraceparent, extractContext, generateSpanId, generateTraceId, getActiveContext, getTraceparent, parseTraceparent, trace };
88
+ //# sourceMappingURL=chunk-FYEN2WET.js.map
89
+ //# sourceMappingURL=chunk-FYEN2WET.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/traceparent.ts","../src/functional.ts"],"names":[],"mappings":";AAYA,SAAS,UAAU,KAAA,EAAuB;AACxC,EAAA,MAAM,MAAA,GAAS,IAAI,UAAA,CAAW,KAAK,CAAA;AACnC,EAAA,MAAA,CAAO,gBAAgB,MAAM,CAAA;AAC7B,EAAA,OAAO,MAAM,IAAA,CAAK,MAAM,CAAA,CACrB,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,QAAA,CAAS,EAAE,EAAE,QAAA,CAAS,CAAA,EAAG,GAAG,CAAC,CAAA,CAC1C,KAAK,EAAE,CAAA;AACZ;AAMO,SAAS,eAAA,GAA0B;AACxC,EAAA,OAAO,UAAU,EAAE,CAAA;AACrB;AAMO,SAAS,cAAA,GAAyB;AACvC,EAAA,OAAO,UAAU,CAAC,CAAA;AACpB;AAqBO,SAAS,iBAAA,CACd,SACA,aAAA,EACQ;AACR,EAAA,MAAM,GAAA,GAAM,WAAW,eAAA,EAAgB;AACvC,EAAA,MAAM,MAAM,cAAA,EAAe;AAC3B,EAAA,MAAM,KAAA,GAAQ,IAAA;AAEd,EAAA,OAAO,CAAA,GAAA,EAAM,GAAG,CAAA,CAAA,EAAI,GAAG,IAAI,KAAK,CAAA,CAAA;AAClC;AAeO,SAAS,iBAAiB,WAAA,EAKxB;AACP,EAAA,MAAM,KAAA,GAAQ,WAAA,CAAY,KAAA,CAAM,GAAG,CAAA;AAEnC,EAAA,IAAI,KAAA,CAAM,WAAW,CAAA,EAAG;AACtB,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,MAAM,CAAC,OAAA,EAAS,OAAA,EAAS,MAAA,EAAQ,KAAK,CAAA,GAAI,KAAA;AAG1C,EAAA,IACE,OAAA,CAAQ,MAAA,KAAW,CAAA,IACnB,OAAA,CAAQ,MAAA,KAAW,EAAA,IACnB,MAAA,CAAO,MAAA,KAAW,EAAA,IAClB,KAAA,CAAM,MAAA,KAAW,CAAA,EACjB;AACA,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,OAAO,EAAE,OAAA,EAAS,OAAA,EAAS,MAAA,EAAQ,KAAA,EAAM;AAC3C;;;AC7EA,IAAI,cAAA;AA2BG,SAAS,MACd,EAAA,EACG;AAEH,EAAA,MAAM,cAAA,GAAiB,iBAAiB,EAAE,CAAA;AAE1C,EAAA,IAAI,cAAA,EAAgB;AAElB,IAAA,QAAQ,IAAI,IAAA,KAAgB;AAE1B,MAAA,MAAM,MAAM,aAAA,EAAc;AAG1B,MAAA,MAAM,QAAA,GAAY,GAAgC,GAAG,CAAA;AAGrD,MAAA,OAAO,QAAA,CAAS,GAAG,IAAI,CAAA;AAAA,IACzB,CAAA;AAAA,EACF;AAIA,EAAA,OAAO,EAAA;AACT;AAKA,SAAS,iBAAiB,EAAA,EAAsB;AAC9C,EAAA,IAAI,OAAO,EAAA,KAAO,UAAA,EAAY,OAAO,KAAA;AAErC,EAAA,MAAM,KAAA,GAAQ,GAAG,QAAA,EAAS;AAG1B,EAAA,MAAM,YAAA,GAAe,CAAC,KAAA,EAAO,SAAA,EAAW,cAAc,CAAA;AAEtD,EAAA,OAAO,YAAA,CAAa,IAAA,CAAK,CAAC,IAAA,KAAS;AAEjC,IAAA,MAAM,KAAA,GAAQ,IAAI,MAAA,CAAO,CAAA,4CAAA,EAA+C,IAAI,CAAA,QAAA,CAAU,CAAA;AACtF,IAAA,OAAO,KAAA,CAAM,KAAK,KAAK,CAAA;AAAA,EACzB,CAAC,CAAA;AACH;AAMA,SAAS,aAAA,GAA8B;AAGrC,EAAA,MAAM,cAAc,iBAAA,EAAkB;AACtC,EAAA,MAAM,MAAA,GAAS,iBAAiB,WAAW,CAAA;AAE3C,EAAA,IAAI,CAAC,MAAA,EAAQ;AAEX,IAAA,OAAO;AAAA,MACL,OAAA,EAAS,EAAA;AAAA,MACT,MAAA,EAAQ,EAAA;AAAA,MACR,aAAA,EAAe;AAAA,KACjB;AAAA,EACF;AAEA,EAAA,MAAM,GAAA,GAAoB;AAAA,IACxB,SAAS,MAAA,CAAO,OAAA;AAAA,IAChB,QAAQ,MAAA,CAAO,MAAA;AAAA,IACf,eAAe,MAAA,CAAO;AAAA,GACxB;AAEA,EAAA,cAAA,GAAiB,GAAA;AACjB,EAAA,OAAO,GAAA;AACT;AAeO,SAAS,gBAAA,GAA6C;AAC3D,EAAA,OAAO,cAAA;AACT;AAwBO,SAAS,cAAA,GAAyB;AACvC,EAAA,OAAO,iBAAA,EAAkB;AAC3B;AAoBO,SAAS,eAAe,WAAA,EAA+C;AAC5E,EAAA,MAAM,MAAA,GAAS,iBAAiB,WAAW,CAAA;AAC3C,EAAA,IAAI,CAAC,QAAQ,OAAO,MAAA;AAEpB,EAAA,OAAO;AAAA,IACL,SAAS,MAAA,CAAO,OAAA;AAAA,IAChB,QAAQ,MAAA,CAAO,MAAA;AAAA,IACf,eAAe,MAAA,CAAO;AAAA,GACxB;AACF","file":"chunk-FYEN2WET.js","sourcesContent":["/**\n * Minimal W3C Trace Context implementation for browser\n *\n * Generates traceparent headers in the W3C format:\n * traceparent: 00-{trace-id}-{span-id}-{flags}\n *\n * No OpenTelemetry dependencies - just crypto.getRandomValues()\n */\n\n/**\n * Generate random hex string of specified byte length\n */\nfunction randomHex(bytes: number): string {\n const buffer = new Uint8Array(bytes);\n crypto.getRandomValues(buffer);\n return Array.from(buffer)\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n}\n\n/**\n * Generate a random 128-bit (16 byte) trace ID\n * @returns 32 character hex string\n */\nexport function generateTraceId(): string {\n return randomHex(16); // 16 bytes = 32 hex chars\n}\n\n/**\n * Generate a random 64-bit (8 byte) span ID\n * @returns 16 character hex string\n */\nexport function generateSpanId(): string {\n return randomHex(8); // 8 bytes = 16 hex chars\n}\n\n/**\n * Create a W3C traceparent header value\n *\n * Format: version-traceId-spanId-flags\n * - version: 00 (W3C Trace Context spec)\n * - traceId: 128-bit hex (32 chars)\n * - spanId: 64-bit hex (16 chars)\n * - flags: 01 (sampled)\n *\n * @param traceId - Optional existing trace ID (for continuing traces)\n * @param parentSpanId - Optional parent span ID (unused in browser, included for API compat)\n * @returns W3C traceparent header value\n *\n * @example\n * ```typescript\n * const header = createTraceparent()\n * // \"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01\"\n * ```\n */\nexport function createTraceparent(\n traceId?: string,\n _parentSpanId?: string\n): string {\n const tid = traceId ?? generateTraceId();\n const sid = generateSpanId();\n const flags = '01'; // sampled=1\n\n return `00-${tid}-${sid}-${flags}`;\n}\n\n/**\n * Parse a traceparent header value\n * Useful for extracting trace context from incoming headers\n *\n * @param traceparent - W3C traceparent header value\n * @returns Parsed components or null if invalid\n *\n * @example\n * ```typescript\n * const parsed = parseTraceparent('00-4bf92f...0e4736-00f067...902b7-01')\n * console.log(parsed?.traceId) // \"4bf92f...0e4736\"\n * ```\n */\nexport function parseTraceparent(traceparent: string): {\n version: string;\n traceId: string;\n spanId: string;\n flags: string;\n} | null {\n const parts = traceparent.split('-');\n\n if (parts.length !== 4) {\n return null;\n }\n\n const [version, traceId, spanId, flags] = parts;\n\n // Validate format\n if (\n version.length !== 2 ||\n traceId.length !== 32 ||\n spanId.length !== 16 ||\n flags.length !== 2\n ) {\n return null;\n }\n\n return { version, traceId, spanId, flags };\n}\n","/**\n * Minimal functional API for browser tracing\n *\n * These are DX wrappers that DON'T create real browser spans.\n * The real spans and timing happen on the backend via Autotel.\n *\n * The browser's job is just to propagate trace context via headers.\n */\n\nimport { createTraceparent, parseTraceparent } from './traceparent';\n\n/**\n * Minimal trace context (browser-side)\n *\n * This is a lightweight version that just holds IDs.\n * NO actual span object - the real span lives on the backend.\n */\nexport interface TraceContext {\n /** Current trace ID (may be extracted from request) */\n readonly traceId: string;\n /** Current span ID (generated for this browser \"span\") */\n readonly spanId: string;\n /** Correlation ID (same as trace ID) */\n readonly correlationId: string;\n}\n\n// Store current trace context (if any)\nlet currentContext: TraceContext | undefined;\n\n/**\n * Wrap a function with trace() for better DX\n *\n * **Important:** This does NOT create real spans in the browser.\n * It's purely for API consistency. The real tracing happens on the backend.\n *\n * The traceparent header is automatically injected by init() on fetch/XHR calls.\n *\n * @example Basic usage\n * ```typescript\n * const fetchUser = trace(async (id: string) => {\n * const response = await fetch(`/api/users/${id}`)\n * return response.json()\n * })\n * ```\n *\n * @example With context (for accessing trace IDs)\n * ```typescript\n * const fetchUser = trace(ctx => async (id: string) => {\n * console.log('Trace ID:', ctx.traceId)\n * const response = await fetch(`/api/users/${id}`)\n * return response.json()\n * })\n * ```\n */\nexport function trace<T extends (...args: any[]) => any>(\n fn: T | ((ctx: TraceContext) => T)\n): T {\n // Check if function expects a context parameter (factory pattern)\n const expectsContext = isFactoryPattern(fn);\n\n if (expectsContext) {\n // Factory pattern: trace(ctx => async (data) => ...)\n return ((...args: any[]) => {\n // Generate a new trace context for this call\n const ctx = createContext();\n\n // Call factory to get the actual function\n const actualFn = (fn as (ctx: TraceContext) => T)(ctx);\n\n // Execute the function\n return actualFn(...args);\n }) as T;\n }\n\n // Direct pattern: trace(async (data) => ...)\n // Just return the function as-is since headers are auto-injected\n return fn as T;\n}\n\n/**\n * Check if a function expects a context parameter (factory pattern)\n */\nfunction isFactoryPattern(fn: unknown): boolean {\n if (typeof fn !== 'function') return false;\n\n const fnStr = fn.toString();\n\n // Look for common parameter names that indicate context\n const contextHints = ['ctx', 'context', 'traceContext'];\n\n return contextHints.some((hint) => {\n // Match parameter name at start of function\n const regex = new RegExp(`^\\\\s*(?:async\\\\s+)?(?:function\\\\s*)?\\\\(?\\\\s*${hint}\\\\s*[,)]`);\n return regex.test(fnStr);\n });\n}\n\n/**\n * Create a minimal trace context\n * Generates new IDs for this \"span\" (browser-side only)\n */\nfunction createContext(): TraceContext {\n // Parse the current traceparent if we have one\n // (This would come from SSR or previous span)\n const traceparent = createTraceparent();\n const parsed = parseTraceparent(traceparent);\n\n if (!parsed) {\n // Fallback if parsing fails\n return {\n traceId: '',\n spanId: '',\n correlationId: '',\n };\n }\n\n const ctx: TraceContext = {\n traceId: parsed.traceId,\n spanId: parsed.spanId,\n correlationId: parsed.traceId,\n };\n\n currentContext = ctx;\n return ctx;\n}\n\n/**\n * Get the current trace context (if any)\n *\n * @returns Current trace context or undefined\n *\n * @example\n * ```typescript\n * const ctx = getActiveContext()\n * if (ctx) {\n * console.log('Trace ID:', ctx.traceId)\n * }\n * ```\n */\nexport function getActiveContext(): TraceContext | undefined {\n return currentContext;\n}\n\n/**\n * Manual helper to create a traceparent header\n *\n * Useful if you need to manually set headers or disable auto-instrumentation.\n *\n * @returns W3C traceparent header value\n *\n * @example\n * ```typescript\n * import { init, getTraceparent } from 'autotel-web'\n *\n * // Disable auto-instrumentation\n * init({ service: 'my-app', instrumentFetch: false })\n *\n * // Manually inject headers\n * fetch('/api/data', {\n * headers: {\n * 'traceparent': getTraceparent()\n * }\n * })\n * ```\n */\nexport function getTraceparent(): string {\n return createTraceparent();\n}\n\n/**\n * Extract trace context from a traceparent header\n *\n * Useful for SSR scenarios where you want to continue a trace from the server.\n *\n * @param traceparent - W3C traceparent header value\n * @returns Parsed trace context or undefined if invalid\n *\n * @example\n * ```typescript\n * // In an SSR handler\n * const traceparent = request.headers.get('traceparent')\n * if (traceparent) {\n * const ctx = extractContext(traceparent)\n * console.log('Continuing trace:', ctx?.traceId)\n * }\n * ```\n */\nexport function extractContext(traceparent: string): TraceContext | undefined {\n const parsed = parseTraceparent(traceparent);\n if (!parsed) return undefined;\n\n return {\n traceId: parsed.traceId,\n spanId: parsed.spanId,\n correlationId: parsed.traceId,\n };\n}\n"]}
package/dist/full.d.ts ADDED
@@ -0,0 +1,126 @@
1
+ import { context } from '@opentelemetry/api';
2
+ import { SpanProcessor, Sampler } from '@opentelemetry/sdk-trace-base';
3
+ import { P as PrivacyConfig } from './functional-04Wr-1U_.js';
4
+ export { T as TraceContext, e as extractContext, g as getActiveContext, a as getTraceparent, t as trace } from './functional-04Wr-1U_.js';
5
+
6
+ /**
7
+ * Full browser tracing with OpenTelemetry SDK
8
+ *
9
+ * Single install: npm install autotel-web. Import from 'autotel-web/full'.
10
+ * No Zone.js - uses default context manager. Async context propagation is best-effort.
11
+ *
12
+ * @see https://github.com/open-telemetry/semantic-conventions/issues/3385 (http.client.network_timing)
13
+ */
14
+
15
+ interface AutotelWebFullConfig {
16
+ /** Service name for the browser application */
17
+ service: string;
18
+ /**
19
+ * OTLP endpoint URL for trace export (e.g. https://api.example.com/v1/traces).
20
+ * If not set, no export (spans still created; use spanProcessor for custom export).
21
+ */
22
+ endpoint?: string;
23
+ /**
24
+ * Custom span processor(s). If provided, used instead of default BatchSpanProcessor + OTLP exporter.
25
+ * When endpoint is set, this is ignored.
26
+ */
27
+ spanProcessor?: SpanProcessor;
28
+ /**
29
+ * Sample rate 0–1. Default 1.0. Use e.g. 0.1 in production.
30
+ */
31
+ sampleRate?: number;
32
+ /** Custom sampler. If set, sampleRate is ignored. */
33
+ sampler?: Sampler;
34
+ /** Enable document load / navigation spans. @default true */
35
+ captureNavigation?: boolean;
36
+ /** Enable fetch instrumentation. @default true */
37
+ captureFetch?: boolean;
38
+ /** Enable XMLHttpRequest instrumentation. @default true */
39
+ captureXHR?: boolean;
40
+ /**
41
+ * Emit http.client.network_timing events from Resource Timing API.
42
+ * @default true
43
+ */
44
+ captureNetworkTiming?: boolean;
45
+ /**
46
+ * Copy original HTTP span attributes onto network_timing event for backends that need them.
47
+ * @default false
48
+ */
49
+ copyHttpSpanAttributesToEvent?: boolean;
50
+ /** Optional user interaction (click) spans. */
51
+ userInteraction?: {
52
+ enabled: boolean;
53
+ /** CSS selectors for elements to track (e.g. ['button', '[data-track]']). Default: ['button', 'a'] */
54
+ selectors?: string[];
55
+ };
56
+ /**
57
+ * Record unhandled errors (window.onerror, unhandledrejection) on active span or create unhandled_error span.
58
+ * @default true
59
+ */
60
+ captureErrors?: boolean;
61
+ /**
62
+ * Capture Web Vitals (LCP, INP, CLS, FCP, TTFB) and report as attributes on a web_vitals span.
63
+ * @default true
64
+ */
65
+ captureWebVitals?: boolean;
66
+ /**
67
+ * Options for Web Vitals. reportAllChanges: pass through to web-vitals (default false for stability).
68
+ */
69
+ webVitals?: {
70
+ reportAllChanges?: boolean;
71
+ };
72
+ /**
73
+ * Capture long tasks (main thread blocking >= 50ms) as long_task spans. Opt-in; can be noisy.
74
+ * @default false
75
+ */
76
+ captureLongTasks?: boolean;
77
+ /** Privacy controls (origin filtering, DNT, GPC). Applied to which requests get traced. */
78
+ privacy?: PrivacyConfig;
79
+ /** Enable debug logging. @default false */
80
+ debug?: boolean;
81
+ }
82
+ /**
83
+ * Initialize full browser tracing (spans + optional export).
84
+ *
85
+ * Call once, client-side only. Uses OpenTelemetry WebTracerProvider; no Zone.js.
86
+ *
87
+ * @example
88
+ * ```ts
89
+ * import { initFull } from 'autotel-web/full'
90
+ * initFull({
91
+ * service: 'my-app',
92
+ * endpoint: 'https://api.example.com/v1/traces',
93
+ * sampleRate: 0.1,
94
+ * captureNetworkTiming: true,
95
+ * userInteraction: { enabled: true, selectors: ['button', '[data-track]'] }
96
+ * })
97
+ * ```
98
+ */
99
+ declare function initFull(config: AutotelWebFullConfig): void;
100
+ /**
101
+ * Create a span with the current context (full mode).
102
+ */
103
+ declare function span<T>(name: string, fn: (s: {
104
+ setAttribute: (k: string, v: string | number | boolean) => void;
105
+ end: () => void;
106
+ }) => T): T;
107
+ /**
108
+ * Set attribute on the active span (full mode).
109
+ */
110
+ declare function setAttribute(key: string, value: string | number | boolean): void;
111
+ /**
112
+ * Add an event to the active span (full mode).
113
+ */
114
+ declare function addEvent(name: string, attributes?: Record<string, string | number | boolean>): void;
115
+ /**
116
+ * Run a function with the given context (for manual async propagation in full mode).
117
+ */
118
+ declare function runWithContext<T>(ctx: ReturnType<typeof context.active>, fn: () => T): T;
119
+
120
+ /**
121
+ * Reset full initialization state (for testing).
122
+ * @internal
123
+ */
124
+ declare function resetFullForTesting(): void;
125
+
126
+ export { type AutotelWebFullConfig, addEvent, initFull, resetFullForTesting, runWithContext, setAttribute, span };