@obsrviq/tracker 0.7.0 → 0.7.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@obsrviq/tracker",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
package/src/network.ts CHANGED
@@ -2,7 +2,7 @@ import type { NetworkEvent, NetworkResourceType, NetworkTiming } from '@obsrviq/
2
2
  import type { InstrumentCtx, Teardown } from './context.js';
3
3
  import { maskString } from './mask.js';
4
4
  import { tokenBucket } from './ratelimit.js';
5
- import { injectFetchTrace, newTrace, traceTarget } from './trace.js';
5
+ import { applyFetchTrace, newTrace, traceIdOf, traceTarget } from './trace.js';
6
6
  import { uuid } from './util.js';
7
7
 
8
8
  const BODY_CAP = 4 * 1024; // 4 KB body preview cap
@@ -345,11 +345,10 @@ function wrapFetch(ctx: InstrumentCtx, tr: TimingResolver, emitNet: EmitNet): Te
345
345
  let traceId: string | undefined;
346
346
  if (traceTarget(rawUrl, ctx.config)) {
347
347
  try {
348
- const tr = newTrace();
349
- const inj = injectFetchTrace(input, init, tr.header);
348
+ const inj = applyFetchTrace(input, init);
350
349
  input = inj.input;
351
350
  init = inj.init;
352
- traceId = tr.traceId;
351
+ traceId = inj.traceId || undefined;
353
352
  } catch {
354
353
  /* never let propagation break the request */
355
354
  }
@@ -485,12 +484,19 @@ function wrapXhr(ctx: InstrumentCtx, tr: TimingResolver, emitNet: EmitNet): Tear
485
484
  if (ctx.config.captureNetworkBodies) meta.body = stringifyBody(body);
486
485
  // Trace-context propagation (first-party only). setRequestHeader is valid
487
486
  // here — after open(), before the real send() below. Our own setRequestHeader
488
- // patch records it into meta.reqHeaders (traceparent is allowlisted).
487
+ // patch records it into meta.reqHeaders (traceparent is allowlisted), so a
488
+ // traceparent the APP already set is visible here: respect it (record its id,
489
+ // don't re-set — XHR COMBINES duplicate headers into an invalid "v1, v2").
489
490
  if (traceTarget(meta.rawUrl, ctx.config)) {
490
491
  try {
491
- const tr = newTrace();
492
- this.setRequestHeader('traceparent', tr.header);
493
- meta.traceId = tr.traceId;
492
+ const appSet = meta.reqHeaders['traceparent'];
493
+ if (appSet) {
494
+ meta.traceId = traceIdOf(appSet) ?? undefined;
495
+ } else {
496
+ const tr = newTrace();
497
+ this.setRequestHeader('traceparent', tr.header);
498
+ meta.traceId = tr.traceId;
499
+ }
494
500
  } catch {
495
501
  /* header rejected (e.g. send already dispatched) — skip, never throw */
496
502
  }
package/src/trace.ts CHANGED
@@ -77,24 +77,54 @@ function hostAllowed(hostname: string, hosts: string[]): boolean {
77
77
  return false;
78
78
  }
79
79
 
80
- /** Return the fetch args to call `orig` with, carrying `traceparent`. Never
81
- * throws upward is the caller's job but we keep this total and side-effect-free
82
- * so a rebuild failure can fall back to the originals. */
83
- export function injectFetchTrace(
80
+ /** The 32-hex trace id inside a `traceparent` value, or null if malformed. */
81
+ export function traceIdOf(header: string | null | undefined): string | null {
82
+ if (!header) return null;
83
+ const parts = header.split('-');
84
+ return parts.length >= 2 && /^[0-9a-f]{32}$/.test(parts[1]!) ? parts[1]! : null;
85
+ }
86
+
87
+ /** The traceparent already on this fetch call, honoring fetch's header semantics:
88
+ * init.headers (when present) REPLACE a Request's headers, so they win. */
89
+ function existingFetchTraceparent(input: RequestInfo | URL, init: RequestInit | undefined): string | null {
90
+ const hasInitHeaders = !!init && 'headers' in init && init.headers != null;
91
+ const src = hasInitHeaders
92
+ ? (init!.headers as HeadersInit)
93
+ : input instanceof Request
94
+ ? input.headers
95
+ : undefined;
96
+ if (!src) return null;
97
+ try {
98
+ return new Headers(src).get('traceparent');
99
+ } catch {
100
+ return null;
101
+ }
102
+ }
103
+
104
+ /** Ensure a fetch call carries a `traceparent`, and report the trace id that will
105
+ * actually be ON THE WIRE. If the app already set one, we DON'T overwrite it (and
106
+ * return ITS id, so the recorded event matches what the backend sees); otherwise
107
+ * we mint + inject ours. Total + side-effect-free so a caller can fall back to the
108
+ * originals if it throws. */
109
+ export function applyFetchTrace(
84
110
  input: RequestInfo | URL,
85
111
  init: RequestInit | undefined,
86
- header: string,
87
- ): { input: RequestInfo | URL; init: RequestInit | undefined } {
112
+ ): { input: RequestInfo | URL; init: RequestInit | undefined; traceId: string } {
113
+ const existing = existingFetchTraceparent(input, init);
114
+ if (existing) {
115
+ // Respect the app's own trace context — record its id, change nothing.
116
+ return { input, init, traceId: traceIdOf(existing) ?? '' };
117
+ }
118
+ const tr = newTrace();
88
119
  const hasInitHeaders = !!init && 'headers' in init && init.headers != null;
89
120
  // fetch(Request) with no override headers → rebuild the Request so we don't drop
90
- // its own headers. When init.headers IS given, fetch uses THOSE (spec: they
91
- // replace the Request's), so we inject there instead.
121
+ // its own headers. When init.headers IS given, fetch uses THOSE, so inject there.
92
122
  if (input instanceof Request && !hasInitHeaders) {
93
123
  const h = new Headers(input.headers);
94
- if (!h.has('traceparent')) h.set('traceparent', header);
95
- return { input: new Request(input, { headers: h }), init };
124
+ h.set('traceparent', tr.header);
125
+ return { input: new Request(input, { headers: h }), init, traceId: tr.traceId };
96
126
  }
97
127
  const h = new Headers((init?.headers as HeadersInit | undefined) ?? undefined);
98
- if (!h.has('traceparent')) h.set('traceparent', header);
99
- return { input, init: { ...(init || {}), headers: h } };
128
+ h.set('traceparent', tr.header);
129
+ return { input, init: { ...(init || {}), headers: h }, traceId: tr.traceId };
100
130
  }