@interfere/react 9.0.1 → 10.0.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 +4 -4
- package/dist/api.d.mts +25 -0
- package/dist/api.d.mts.map +1 -0
- package/dist/api.mjs +68 -0
- package/dist/api.mjs.map +1 -0
- package/dist/error-boundary.d.mts +11 -4
- package/dist/error-boundary.d.mts.map +1 -1
- package/dist/error-boundary.mjs +6 -3
- package/dist/error-boundary.mjs.map +1 -1
- package/dist/internal/browser-context.d.mts +6 -0
- package/dist/internal/browser-context.d.mts.map +1 -0
- package/dist/internal/browser-context.mjs +59 -0
- package/dist/internal/browser-context.mjs.map +1 -0
- package/dist/internal/capture-boundary.d.mts +5 -1
- package/dist/internal/capture-boundary.d.mts.map +1 -1
- package/dist/internal/capture-boundary.mjs +9 -5
- package/dist/internal/capture-boundary.mjs.map +1 -1
- package/dist/internal/capture.d.mts +16 -5
- package/dist/internal/capture.d.mts.map +1 -1
- package/dist/internal/capture.mjs +20 -16
- package/dist/internal/capture.mjs.map +1 -1
- package/dist/internal/config.d.mts +20 -4
- package/dist/internal/config.d.mts.map +1 -1
- package/dist/internal/config.mjs +12 -12
- package/dist/internal/config.mjs.map +1 -1
- package/dist/internal/consent.d.mts.map +1 -1
- package/dist/internal/consent.mjs +3 -1
- package/dist/internal/consent.mjs.map +1 -1
- package/dist/internal/console-patch.d.mts +19 -0
- package/dist/internal/console-patch.d.mts.map +1 -0
- package/dist/internal/console-patch.mjs +62 -0
- package/dist/internal/console-patch.mjs.map +1 -0
- package/dist/internal/dom/actionable.d.mts +27 -0
- package/dist/internal/dom/actionable.d.mts.map +1 -0
- package/dist/internal/dom/actionable.mjs +62 -0
- package/dist/internal/dom/actionable.mjs.map +1 -0
- package/dist/internal/kernel-registry.d.mts +8 -0
- package/dist/internal/kernel-registry.d.mts.map +1 -0
- package/dist/internal/kernel-registry.mjs +31 -0
- package/dist/internal/kernel-registry.mjs.map +1 -0
- package/dist/internal/kernel.d.mts +267 -0
- package/dist/internal/kernel.d.mts.map +1 -0
- package/dist/internal/kernel.mjs +322 -0
- package/dist/internal/kernel.mjs.map +1 -0
- package/dist/internal/otel/exporter.d.mts +93 -0
- package/dist/internal/otel/exporter.d.mts.map +1 -0
- package/dist/internal/otel/exporter.mjs +212 -0
- package/dist/internal/otel/exporter.mjs.map +1 -0
- package/dist/internal/otel/index.d.mts +6 -0
- package/dist/internal/otel/index.mjs +6 -0
- package/dist/internal/otel/instrumentations.d.mts +42 -0
- package/dist/internal/otel/instrumentations.d.mts.map +1 -0
- package/dist/internal/otel/instrumentations.mjs +150 -0
- package/dist/internal/otel/instrumentations.mjs.map +1 -0
- package/dist/internal/otel/page-scope-context-manager.d.mts +32 -0
- package/dist/internal/otel/page-scope-context-manager.d.mts.map +1 -0
- package/dist/internal/otel/page-scope-context-manager.mjs +36 -0
- package/dist/internal/otel/page-scope-context-manager.mjs.map +1 -0
- package/dist/internal/otel/propagation.d.mts +21 -0
- package/dist/internal/otel/propagation.d.mts.map +1 -0
- package/dist/internal/otel/propagation.mjs +40 -0
- package/dist/internal/otel/propagation.mjs.map +1 -0
- package/dist/internal/otel/provider.d.mts +107 -0
- package/dist/internal/otel/provider.d.mts.map +1 -0
- package/dist/internal/otel/provider.mjs +151 -0
- package/dist/internal/otel/provider.mjs.map +1 -0
- package/dist/internal/otel/web-vitals.d.mts +35 -0
- package/dist/internal/otel/web-vitals.d.mts.map +1 -0
- package/dist/internal/otel/web-vitals.mjs +162 -0
- package/dist/internal/otel/web-vitals.mjs.map +1 -0
- package/dist/internal/page-lifecycle.d.mts +21 -0
- package/dist/internal/page-lifecycle.d.mts.map +1 -0
- package/dist/internal/page-lifecycle.mjs +33 -0
- package/dist/internal/page-lifecycle.mjs.map +1 -0
- package/dist/internal/plugin-runtime.d.mts +0 -2
- package/dist/internal/plugin-runtime.d.mts.map +1 -1
- package/dist/internal/plugin-runtime.mjs +1 -7
- package/dist/internal/plugin-runtime.mjs.map +1 -1
- package/dist/internal/react-context.d.mts +45 -0
- package/dist/internal/react-context.d.mts.map +1 -0
- package/dist/internal/react-context.mjs +34 -0
- package/dist/internal/react-context.mjs.map +1 -0
- package/dist/internal/sw.d.mts +22 -2
- package/dist/internal/sw.d.mts.map +1 -1
- package/dist/internal/sw.mjs +30 -3
- package/dist/internal/sw.mjs.map +1 -1
- package/dist/internal/version.d.mts +3 -1
- package/dist/internal/version.d.mts.map +1 -1
- package/dist/internal/version.mjs +4 -2
- package/dist/internal/version.mjs.map +1 -1
- package/dist/internal/wrapper-singleton.d.mts +47 -0
- package/dist/internal/wrapper-singleton.d.mts.map +1 -0
- package/dist/internal/wrapper-singleton.mjs +73 -0
- package/dist/internal/wrapper-singleton.mjs.map +1 -0
- package/dist/package.mjs +1 -1
- package/dist/plugins/errors.d.mts.map +1 -1
- package/dist/plugins/errors.mjs +18 -25
- package/dist/plugins/errors.mjs.map +1 -1
- package/dist/plugins/lib/loader.d.mts +1 -2
- package/dist/plugins/lib/loader.d.mts.map +1 -1
- package/dist/plugins/lib/loader.mjs +2 -11
- package/dist/plugins/lib/loader.mjs.map +1 -1
- package/dist/plugins/lib/types.d.mts +3 -2
- package/dist/plugins/lib/types.d.mts.map +1 -1
- package/dist/plugins/logs.d.mts +13 -0
- package/dist/plugins/logs.d.mts.map +1 -0
- package/dist/plugins/logs.mjs +53 -0
- package/dist/plugins/logs.mjs.map +1 -0
- package/dist/plugins/rage-clicks.d.mts.map +1 -1
- package/dist/plugins/rage-clicks.mjs +12 -10
- package/dist/plugins/rage-clicks.mjs.map +1 -1
- package/dist/plugins/replay.d.mts.map +1 -1
- package/dist/plugins/replay.mjs +58 -19
- package/dist/plugins/replay.mjs.map +1 -1
- package/dist/provider.d.mts +11 -20
- package/dist/provider.d.mts.map +1 -1
- package/dist/provider.mjs +13 -14
- package/dist/provider.mjs.map +1 -1
- package/dist/react-error-handler.d.mts +21 -5
- package/dist/react-error-handler.d.mts.map +1 -1
- package/dist/react-error-handler.mjs +15 -7
- package/dist/react-error-handler.mjs.map +1 -1
- package/dist/sw.d.mts +2 -0
- package/dist/sw.mjs +2 -0
- package/dist/tracking/api.d.mts +41 -15
- package/dist/tracking/api.d.mts.map +1 -1
- package/dist/tracking/api.mjs +122 -104
- package/dist/tracking/api.mjs.map +1 -1
- package/dist/tracking/device.d.mts +30 -7
- package/dist/tracking/device.d.mts.map +1 -1
- package/dist/tracking/device.mjs +70 -46
- package/dist/tracking/device.mjs.map +1 -1
- package/dist/tracking/geo.d.mts +11 -3
- package/dist/tracking/geo.d.mts.map +1 -1
- package/dist/tracking/geo.mjs +33 -29
- package/dist/tracking/geo.mjs.map +1 -1
- package/dist/tracking/session.d.mts +3 -1
- package/dist/tracking/session.d.mts.map +1 -1
- package/dist/tracking/session.mjs.map +1 -1
- package/dist/util/bot.d.mts +10 -0
- package/dist/util/bot.d.mts.map +1 -0
- package/dist/util/bot.mjs +14 -0
- package/dist/util/bot.mjs.map +1 -0
- package/dist/util/global.d.mts +10 -0
- package/dist/util/global.d.mts.map +1 -0
- package/dist/util/global.mjs +12 -0
- package/dist/util/global.mjs.map +1 -0
- package/dist/util/log.d.mts.map +1 -1
- package/dist/util/log.mjs +8 -1
- package/dist/util/log.mjs.map +1 -1
- package/dist/util/stringify.d.mts +9 -0
- package/dist/util/stringify.d.mts.map +1 -0
- package/dist/util/stringify.mjs +16 -0
- package/dist/util/stringify.mjs.map +1 -0
- package/package.json +73 -20
- package/dist/internal/client.d.mts +0 -48
- package/dist/internal/client.d.mts.map +0 -1
- package/dist/internal/client.mjs +0 -146
- package/dist/internal/client.mjs.map +0 -1
- package/dist/internal/context.d.mts +0 -6
- package/dist/internal/context.d.mts.map +0 -1
- package/dist/internal/context.mjs +0 -32
- package/dist/internal/context.mjs.map +0 -1
- package/dist/internal/envelope.d.mts +0 -15
- package/dist/internal/envelope.d.mts.map +0 -1
- package/dist/internal/envelope.mjs +0 -24
- package/dist/internal/envelope.mjs.map +0 -1
- package/dist/internal/errors.d.mts +0 -4
- package/dist/internal/errors.d.mts.map +0 -1
- package/dist/internal/errors.mjs +0 -4
- package/dist/internal/errors.mjs.map +0 -1
- package/dist/plugins/device.d.mts +0 -6
- package/dist/plugins/device.d.mts.map +0 -1
- package/dist/plugins/device.mjs +0 -13
- package/dist/plugins/device.mjs.map +0 -1
- package/dist/plugins/pages.d.mts +0 -6
- package/dist/plugins/pages.d.mts.map +0 -1
- package/dist/plugins/pages.mjs +0 -102
- package/dist/plugins/pages.mjs.map +0 -1
- package/dist/transport/http.d.mts +0 -21
- package/dist/transport/http.d.mts.map +0 -1
- package/dist/transport/http.mjs +0 -72
- package/dist/transport/http.mjs.map +0 -1
- package/dist/transport/queue.d.mts +0 -34
- package/dist/transport/queue.d.mts.map +0 -1
- package/dist/transport/queue.mjs +0 -95
- package/dist/transport/queue.mjs.map +0 -1
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { PRODUCER_VERSION } from "../version.mjs";
|
|
2
|
+
import { diag } from "@opentelemetry/api";
|
|
3
|
+
import { ExportResultCode } from "@opentelemetry/core";
|
|
4
|
+
import { JsonLogsSerializer } from "@opentelemetry/otlp-transformer/build/esm/logs/json/logs.js";
|
|
5
|
+
import { JsonMetricsSerializer } from "@opentelemetry/otlp-transformer/build/esm/metrics/json/metrics.js";
|
|
6
|
+
import { JsonTraceSerializer } from "@opentelemetry/otlp-transformer/build/esm/trace/json/trace.js";
|
|
7
|
+
import { AggregationTemporality } from "@opentelemetry/sdk-metrics";
|
|
8
|
+
//#region src/internal/otel/exporter.ts
|
|
9
|
+
/**
|
|
10
|
+
* Opaque collector ingest path. Single endpoint for every OTLP signal —
|
|
11
|
+
* server-side dispatches by which `resource*` block the request carries.
|
|
12
|
+
*
|
|
13
|
+
* Semantically nondescript so common adblock filter lists (EasyPrivacy,
|
|
14
|
+
* uBlock) don't fingerprint it the way they do `traces` / `metrics` /
|
|
15
|
+
* `logs` / `telemetry`. Customer-domain proxy mode is still the
|
|
16
|
+
* recommended path for browser SDKs; this is the second line of defence
|
|
17
|
+
* for direct-ingestion (`pub-token`) clients.
|
|
18
|
+
*/
|
|
19
|
+
const COLLECTOR_SINK_PATH = "/v2/sink";
|
|
20
|
+
/**
|
|
21
|
+
* Identity gate values the collector accepts via URL query when their
|
|
22
|
+
* matching headers can't be set.
|
|
23
|
+
*
|
|
24
|
+
* `navigator.sendBeacon` (the only browser transport that survives
|
|
25
|
+
* `visibilitychange→hidden`) only attaches `Content-Type` via the
|
|
26
|
+
* `Blob` — no other request headers. We rely on that exclusively for
|
|
27
|
+
* the browser SDK's primary path, so the identity gates the collector
|
|
28
|
+
* enforces (`x-interfere-producer-version`, `x-interfere-pub-token`)
|
|
29
|
+
* have to ride the URL instead. Mirrors `PRODUCER_VERSION_QUERY` /
|
|
30
|
+
* `PUB_TOKEN_QUERY` on
|
|
31
|
+
* `services/collector/src/{modules/v2/middleware,auth/surface}.ts`.
|
|
32
|
+
*
|
|
33
|
+
* Pub-token query exposure note: pub-tokens are public by design (they
|
|
34
|
+
* ship in browser bundles, visible in DevTools), so URL exposure is no
|
|
35
|
+
* worse than the existing header path. API keys deliberately have **no**
|
|
36
|
+
* query fallback — see `surfaceAuth` for the matching server-side
|
|
37
|
+
* rationale.
|
|
38
|
+
*/
|
|
39
|
+
const PRODUCER_VERSION_QUERY = "_pv";
|
|
40
|
+
const PUB_TOKEN_QUERY = "_pt";
|
|
41
|
+
const PUB_TOKEN_HEADER = "x-interfere-pub-token";
|
|
42
|
+
const JSON_CONTENT_TYPE = "application/json";
|
|
43
|
+
/** Exported for direct unit testing. */
|
|
44
|
+
function buildSinkUrl(collectorUrl) {
|
|
45
|
+
return `${collectorUrl.endsWith("/") ? collectorUrl.slice(0, -1) : collectorUrl}${COLLECTOR_SINK_PATH}`;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Beacon-flavoured sink URL — encodes the producer-version (always)
|
|
49
|
+
* and pub-token (when present in `authHeaders`) as query parameters
|
|
50
|
+
* so `navigator.sendBeacon` can authenticate without setting custom
|
|
51
|
+
* request headers.
|
|
52
|
+
*
|
|
53
|
+
* Exported for direct unit testing.
|
|
54
|
+
*/
|
|
55
|
+
function buildBeaconUrl(input) {
|
|
56
|
+
const params = new URLSearchParams({ [PRODUCER_VERSION_QUERY]: PRODUCER_VERSION });
|
|
57
|
+
const pubToken = input.authHeaders.get(PUB_TOKEN_HEADER);
|
|
58
|
+
if (pubToken) params.set(PUB_TOKEN_QUERY, pubToken);
|
|
59
|
+
return `${buildSinkUrl(input.collectorUrl)}?${params.toString()}`;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Returns true iff the runtime can dispatch a `navigator.sendBeacon`
|
|
63
|
+
* call. Browser-only; server contexts (SSR, prerender, Vitest's `node`
|
|
64
|
+
* environment) trip the typeof guards.
|
|
65
|
+
*/
|
|
66
|
+
function canSendBeacon() {
|
|
67
|
+
return typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function";
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Dispatches an OTLP payload via `navigator.sendBeacon` and always
|
|
71
|
+
* reports `SUCCESS` to the BSP. `sendBeacon` is fire-and-forget by
|
|
72
|
+
* spec — a `true` return only means "the user agent accepted the
|
|
73
|
+
* payload into its delivery queue", not "the server received it".
|
|
74
|
+
*
|
|
75
|
+
* Why never report `FAILED`:
|
|
76
|
+
* - `BatchSpanProcessor._flushOneBatch` rejects its promise on
|
|
77
|
+
* `code !== SUCCESS`, and that rejection propagates up through
|
|
78
|
+
* `forceFlush()` / `shutdown()`. In production, that surfaces
|
|
79
|
+
* during Vercel function teardown (and other awaited unload
|
|
80
|
+
* paths) as an unhandled rejection. In tests, it surfaces as
|
|
81
|
+
* `provider.test.tsx > passes through useSession` — vitest's
|
|
82
|
+
* `afterEach(close)` awaits `kernel.dispose() → BSP.shutdown
|
|
83
|
+
* → _flushAll`, the rejection bubbles, and the test fails.
|
|
84
|
+
* - `sendBeacon`'s `false` return signals a *local* drop (per-call
|
|
85
|
+
* ~64KiB ceiling, per-page queue full, disallowed scheme). No
|
|
86
|
+
* network request was attempted, so the service-worker backstop
|
|
87
|
+
* in `internal/sw.ts` can't intercept it either. Retrying inside
|
|
88
|
+
* the exporter would just hit the same browser limit again.
|
|
89
|
+
*
|
|
90
|
+
* The data is gone, but the export *completed* in the only sense
|
|
91
|
+
* that matters to the BSP. A `diag.debug` keeps visibility for any
|
|
92
|
+
* OTel diag listener wired into the kernel.
|
|
93
|
+
*/
|
|
94
|
+
function reportBeacon(url, payload, signal, resultCallback) {
|
|
95
|
+
if (!payload || payload.byteLength === 0) {
|
|
96
|
+
diag.debug(`[interfere/beacon] ${signal} serializer returned no bytes`);
|
|
97
|
+
resultCallback({ code: ExportResultCode.SUCCESS });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (!canSendBeacon()) {
|
|
101
|
+
diag.debug(`[interfere/beacon] navigator.sendBeacon unavailable; dropping ${signal} batch (${payload.byteLength}B)`);
|
|
102
|
+
resultCallback({ code: ExportResultCode.SUCCESS });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const blob = new Blob([payload], { type: JSON_CONTENT_TYPE });
|
|
106
|
+
if (!navigator.sendBeacon(url, blob)) diag.debug(`[interfere/beacon] navigator.sendBeacon refused ${signal} payload (oversized or queue full); dropping ${payload.byteLength}B`);
|
|
107
|
+
resultCallback({ code: ExportResultCode.SUCCESS });
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Browser-side OTLP trace exporter.
|
|
111
|
+
*
|
|
112
|
+
* Dispatches every export via `navigator.sendBeacon` — which is the
|
|
113
|
+
* only browser transport that reliably commits a request the page is
|
|
114
|
+
* also tearing down (`visibilitychange→hidden`, hard navigation). The
|
|
115
|
+
* `keepalive: true` fetch path the OTLP HTTP exporter ships with
|
|
116
|
+
* works for small payloads but falls back to ordinary fetch (which
|
|
117
|
+
* the renderer aborts on unload) once the cumulative 64KiB / 9-
|
|
118
|
+
* concurrent budget is exhausted. Production data on 2026-05-11
|
|
119
|
+
* attributed ~15% browser-fetch span loss to that fallback; the
|
|
120
|
+
* beacon path closes that hole by design.
|
|
121
|
+
*
|
|
122
|
+
* Identity (`x-interfere-producer-version`, `x-interfere-pub-token`)
|
|
123
|
+
* rides the URL because beacons can't carry custom headers. The
|
|
124
|
+
* collector accepts both query and header paths — see
|
|
125
|
+
* `services/collector/src/{modules/v2/middleware,auth/surface}.ts`.
|
|
126
|
+
*
|
|
127
|
+
* No retry on failure. The service worker backstop captures 5xx /
|
|
128
|
+
* network failures separately by intercepting the same beacon POST
|
|
129
|
+
* and queueing into IndexedDB for replay (`internal/sw.ts`).
|
|
130
|
+
*/
|
|
131
|
+
var BeaconTraceExporter = class {
|
|
132
|
+
url;
|
|
133
|
+
constructor(url) {
|
|
134
|
+
this.url = url;
|
|
135
|
+
}
|
|
136
|
+
export(spans, resultCallback) {
|
|
137
|
+
if (spans.length === 0) {
|
|
138
|
+
resultCallback({ code: ExportResultCode.SUCCESS });
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
reportBeacon(this.url, JsonTraceSerializer.serializeRequest(spans), "trace", resultCallback);
|
|
142
|
+
}
|
|
143
|
+
shutdown() {
|
|
144
|
+
return Promise.resolve();
|
|
145
|
+
}
|
|
146
|
+
forceFlush() {
|
|
147
|
+
return Promise.resolve();
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
/**
|
|
151
|
+
* Browser-side OTLP log exporter — same beacon transport, log
|
|
152
|
+
* payload. See `BeaconTraceExporter` for the design rationale.
|
|
153
|
+
*/
|
|
154
|
+
var BeaconLogExporter = class {
|
|
155
|
+
url;
|
|
156
|
+
constructor(url) {
|
|
157
|
+
this.url = url;
|
|
158
|
+
}
|
|
159
|
+
export(logs, resultCallback) {
|
|
160
|
+
if (logs.length === 0) {
|
|
161
|
+
resultCallback({ code: ExportResultCode.SUCCESS });
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
reportBeacon(this.url, JsonLogsSerializer.serializeRequest(logs), "log", resultCallback);
|
|
165
|
+
}
|
|
166
|
+
shutdown() {
|
|
167
|
+
return Promise.resolve();
|
|
168
|
+
}
|
|
169
|
+
forceFlush() {
|
|
170
|
+
return Promise.resolve();
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
/**
|
|
174
|
+
* Browser-side OTLP metric exporter — beacon transport, metric
|
|
175
|
+
* payload, `DELTA` temporality.
|
|
176
|
+
*
|
|
177
|
+
* `DELTA` matches what the OTLP HTTP exporter the kit's previous
|
|
178
|
+
* `OTLPMetricExporter` was configured with (see the
|
|
179
|
+
* `temporalityPreference: AggregationTemporalityPreference.DELTA`
|
|
180
|
+
* arg `provider.ts` used to pass). Returning the same temporality
|
|
181
|
+
* for every instrument type keeps the wire shape downstream
|
|
182
|
+
* consumers receive unchanged across the migration.
|
|
183
|
+
*/
|
|
184
|
+
var BeaconMetricExporter = class {
|
|
185
|
+
url;
|
|
186
|
+
constructor(url) {
|
|
187
|
+
this.url = url;
|
|
188
|
+
}
|
|
189
|
+
export(metrics, resultCallback) {
|
|
190
|
+
reportBeacon(this.url, JsonMetricsSerializer.serializeRequest(metrics), "metric", resultCallback);
|
|
191
|
+
}
|
|
192
|
+
selectAggregationTemporality() {
|
|
193
|
+
return AggregationTemporality.DELTA;
|
|
194
|
+
}
|
|
195
|
+
forceFlush() {
|
|
196
|
+
return Promise.resolve();
|
|
197
|
+
}
|
|
198
|
+
shutdown() {
|
|
199
|
+
return Promise.resolve();
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
function createBeaconTraceExporter(input) {
|
|
203
|
+
return new BeaconTraceExporter(buildBeaconUrl(input));
|
|
204
|
+
}
|
|
205
|
+
function createBeaconMetricExporter(input) {
|
|
206
|
+
return new BeaconMetricExporter(buildBeaconUrl(input));
|
|
207
|
+
}
|
|
208
|
+
function createBeaconLogExporter(input) {
|
|
209
|
+
return new BeaconLogExporter(buildBeaconUrl(input));
|
|
210
|
+
}
|
|
211
|
+
//#endregion
|
|
212
|
+
export { buildBeaconUrl, buildSinkUrl, createBeaconLogExporter, createBeaconMetricExporter, createBeaconTraceExporter };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exporter.mjs","names":[],"sources":["../../../src/internal/otel/exporter.ts"],"sourcesContent":["import { diag } from \"@opentelemetry/api\";\nimport type { ExportResult } from \"@opentelemetry/core\";\nimport { ExportResultCode } from \"@opentelemetry/core\";\n// Deep imports against the package build output — the barrel\n// (`@opentelemetry/otlp-transformer`) re-exports the JSON *and*\n// Protobuf serializers, and the protobuf side reaches `protobufjs`\n// which transitively hits `worker_threads.MessageChannel` (via\n// `import-in-the-middle`). Vite externalizes `worker_threads` for\n// the browser test environment with a runtime-throwing stub; the\n// kernel's `await import(\"./otel/index.js\")` chunk inherits any file\n// reachable from the package root through Vite's dep optimizer, so\n// importing from the barrel breaks `provider.test.tsx` with\n// \"Failed to fetch dynamically imported module\" on the chunk URL.\n//\n// The `*/json/trace.js` (etc.) modules import only `@opentelemetry/\n// api` and the package's own JSON-only utility files — no protobuf\n// in the transitive graph. We also pin the file extension (`.js`)\n// instead of letting the directory's `index.js` resolution kick in\n// because resolvers (bun on Linux vs macOS, vite vs raw esbuild)\n// disagree about whether a missing extension on a directory-shaped\n// specifier should resolve to `<dir>/index.js` or fail. The explicit\n// `.js` is the only spelling all of them agree on.\n//\n// `@opentelemetry/otlp-transformer` doesn't ship an `\"exports\"` field\n// in its `package.json`, so these deep paths are the supported escape\n// hatch (the OTel HTTP exporters take the same approach internally —\n// see `@opentelemetry/exporter-trace-otlp-http/build/esm/platform/\n// browser/OTLPTraceExporter.js`). Pinned via the workspace catalog so\n// a minor version bump can't quietly relocate the build directory.\nimport { JsonLogsSerializer } from \"@opentelemetry/otlp-transformer/build/esm/logs/json/logs.js\";\nimport { JsonMetricsSerializer } from \"@opentelemetry/otlp-transformer/build/esm/metrics/json/metrics.js\";\nimport { JsonTraceSerializer } from \"@opentelemetry/otlp-transformer/build/esm/trace/json/trace.js\";\nimport type { ReadableLogRecord } from \"@opentelemetry/sdk-logs\";\nimport type {\n PushMetricExporter,\n ResourceMetrics,\n} from \"@opentelemetry/sdk-metrics\";\nimport { AggregationTemporality } from \"@opentelemetry/sdk-metrics\";\nimport type { ReadableSpan, SpanExporter } from \"@opentelemetry/sdk-trace-base\";\n\nimport { PRODUCER_VERSION } from \"../version.js\";\n\n/**\n * Opaque collector ingest path. Single endpoint for every OTLP signal —\n * server-side dispatches by which `resource*` block the request carries.\n *\n * Semantically nondescript so common adblock filter lists (EasyPrivacy,\n * uBlock) don't fingerprint it the way they do `traces` / `metrics` /\n * `logs` / `telemetry`. Customer-domain proxy mode is still the\n * recommended path for browser SDKs; this is the second line of defence\n * for direct-ingestion (`pub-token`) clients.\n */\nconst COLLECTOR_SINK_PATH = \"/v2/sink\";\n\n/**\n * Identity gate values the collector accepts via URL query when their\n * matching headers can't be set.\n *\n * `navigator.sendBeacon` (the only browser transport that survives\n * `visibilitychange→hidden`) only attaches `Content-Type` via the\n * `Blob` — no other request headers. We rely on that exclusively for\n * the browser SDK's primary path, so the identity gates the collector\n * enforces (`x-interfere-producer-version`, `x-interfere-pub-token`)\n * have to ride the URL instead. Mirrors `PRODUCER_VERSION_QUERY` /\n * `PUB_TOKEN_QUERY` on\n * `services/collector/src/{modules/v2/middleware,auth/surface}.ts`.\n *\n * Pub-token query exposure note: pub-tokens are public by design (they\n * ship in browser bundles, visible in DevTools), so URL exposure is no\n * worse than the existing header path. API keys deliberately have **no**\n * query fallback — see `surfaceAuth` for the matching server-side\n * rationale.\n */\nconst PRODUCER_VERSION_QUERY = \"_pv\";\nconst PUB_TOKEN_QUERY = \"_pt\";\nconst PUB_TOKEN_HEADER = \"x-interfere-pub-token\";\n\nconst JSON_CONTENT_TYPE = \"application/json\";\n\ninterface ExporterCommon {\n /**\n * Auth identity from the kernel's resolved ingest target — currently\n * `x-interfere-pub-token` only (proxy mode sets nothing because the\n * proxy server stamps `x-api-key` upstream). Encoded into the beacon\n * URL via `buildBeaconUrl` rather than into request headers because\n * `sendBeacon` doesn't carry custom headers.\n */\n authHeaders: Headers;\n /** Base collector URL — the opaque sink path is appended. */\n collectorUrl: string;\n}\n\n/** Exported for direct unit testing. */\nexport function buildSinkUrl(collectorUrl: string): string {\n const stripped = collectorUrl.endsWith(\"/\")\n ? collectorUrl.slice(0, -1)\n : collectorUrl;\n return `${stripped}${COLLECTOR_SINK_PATH}`;\n}\n\n/**\n * Beacon-flavoured sink URL — encodes the producer-version (always)\n * and pub-token (when present in `authHeaders`) as query parameters\n * so `navigator.sendBeacon` can authenticate without setting custom\n * request headers.\n *\n * Exported for direct unit testing.\n */\nexport function buildBeaconUrl(input: ExporterCommon): string {\n const params = new URLSearchParams({\n [PRODUCER_VERSION_QUERY]: PRODUCER_VERSION,\n });\n const pubToken = input.authHeaders.get(PUB_TOKEN_HEADER);\n if (pubToken) {\n params.set(PUB_TOKEN_QUERY, pubToken);\n }\n return `${buildSinkUrl(input.collectorUrl)}?${params.toString()}`;\n}\n\n/**\n * Returns true iff the runtime can dispatch a `navigator.sendBeacon`\n * call. Browser-only; server contexts (SSR, prerender, Vitest's `node`\n * environment) trip the typeof guards.\n */\nfunction canSendBeacon(): boolean {\n return (\n typeof navigator !== \"undefined\" &&\n typeof navigator.sendBeacon === \"function\"\n );\n}\n\n/**\n * Dispatches an OTLP payload via `navigator.sendBeacon` and always\n * reports `SUCCESS` to the BSP. `sendBeacon` is fire-and-forget by\n * spec — a `true` return only means \"the user agent accepted the\n * payload into its delivery queue\", not \"the server received it\".\n *\n * Why never report `FAILED`:\n * - `BatchSpanProcessor._flushOneBatch` rejects its promise on\n * `code !== SUCCESS`, and that rejection propagates up through\n * `forceFlush()` / `shutdown()`. In production, that surfaces\n * during Vercel function teardown (and other awaited unload\n * paths) as an unhandled rejection. In tests, it surfaces as\n * `provider.test.tsx > passes through useSession` — vitest's\n * `afterEach(close)` awaits `kernel.dispose() → BSP.shutdown\n * → _flushAll`, the rejection bubbles, and the test fails.\n * - `sendBeacon`'s `false` return signals a *local* drop (per-call\n * ~64KiB ceiling, per-page queue full, disallowed scheme). No\n * network request was attempted, so the service-worker backstop\n * in `internal/sw.ts` can't intercept it either. Retrying inside\n * the exporter would just hit the same browser limit again.\n *\n * The data is gone, but the export *completed* in the only sense\n * that matters to the BSP. A `diag.debug` keeps visibility for any\n * OTel diag listener wired into the kernel.\n */\nfunction reportBeacon(\n url: string,\n payload: Uint8Array | undefined,\n signal: \"trace\" | \"log\" | \"metric\",\n resultCallback: (result: ExportResult) => void\n): void {\n if (!payload || payload.byteLength === 0) {\n diag.debug(`[interfere/beacon] ${signal} serializer returned no bytes`);\n resultCallback({ code: ExportResultCode.SUCCESS });\n return;\n }\n if (!canSendBeacon()) {\n diag.debug(\n `[interfere/beacon] navigator.sendBeacon unavailable; dropping ${signal} batch (${payload.byteLength}B)`\n );\n resultCallback({ code: ExportResultCode.SUCCESS });\n return;\n }\n // `Blob` is the only sendBeacon argument shape that lets us pin\n // `Content-Type: application/json` on the request — using the raw\n // `Uint8Array` would default to `application/octet-stream`, which\n // the collector's content-type allowlist rejects with 415.\n //\n // The cast is necessary because `JsonTraceSerializer.serializeRequest`\n // returns `Uint8Array<ArrayBufferLike>`, but `Blob`'s constructor\n // narrows `BlobPart` to `Uint8Array<ArrayBuffer>` (excluding\n // `SharedArrayBuffer`). The serializer never returns shared-buffer-\n // backed bytes (it allocates a fresh `Uint8Array(byteLength)`\n // internally) so the runtime check the type system is asking for is\n // already true; a structural cast preserves that without forcing an\n // unnecessary buffer copy.\n const blob = new Blob([payload as Uint8Array<ArrayBuffer>], {\n type: JSON_CONTENT_TYPE,\n });\n if (!navigator.sendBeacon(url, blob)) {\n diag.debug(\n `[interfere/beacon] navigator.sendBeacon refused ${signal} payload (oversized or queue full); dropping ${payload.byteLength}B`\n );\n }\n resultCallback({ code: ExportResultCode.SUCCESS });\n}\n\n/**\n * Browser-side OTLP trace exporter.\n *\n * Dispatches every export via `navigator.sendBeacon` — which is the\n * only browser transport that reliably commits a request the page is\n * also tearing down (`visibilitychange→hidden`, hard navigation). The\n * `keepalive: true` fetch path the OTLP HTTP exporter ships with\n * works for small payloads but falls back to ordinary fetch (which\n * the renderer aborts on unload) once the cumulative 64KiB / 9-\n * concurrent budget is exhausted. Production data on 2026-05-11\n * attributed ~15% browser-fetch span loss to that fallback; the\n * beacon path closes that hole by design.\n *\n * Identity (`x-interfere-producer-version`, `x-interfere-pub-token`)\n * rides the URL because beacons can't carry custom headers. The\n * collector accepts both query and header paths — see\n * `services/collector/src/{modules/v2/middleware,auth/surface}.ts`.\n *\n * No retry on failure. The service worker backstop captures 5xx /\n * network failures separately by intercepting the same beacon POST\n * and queueing into IndexedDB for replay (`internal/sw.ts`).\n */\nclass BeaconTraceExporter implements SpanExporter {\n private readonly url: string;\n\n constructor(url: string) {\n this.url = url;\n }\n\n export(\n spans: ReadableSpan[],\n resultCallback: (result: ExportResult) => void\n ): void {\n if (spans.length === 0) {\n resultCallback({ code: ExportResultCode.SUCCESS });\n return;\n }\n reportBeacon(\n this.url,\n JsonTraceSerializer.serializeRequest(spans),\n \"trace\",\n resultCallback\n );\n }\n\n shutdown(): Promise<void> {\n return Promise.resolve();\n }\n\n forceFlush(): Promise<void> {\n return Promise.resolve();\n }\n}\n\n/**\n * Browser-side OTLP log exporter — same beacon transport, log\n * payload. See `BeaconTraceExporter` for the design rationale.\n */\nclass BeaconLogExporter {\n private readonly url: string;\n\n constructor(url: string) {\n this.url = url;\n }\n\n export(\n logs: ReadableLogRecord[],\n resultCallback: (result: ExportResult) => void\n ): void {\n if (logs.length === 0) {\n resultCallback({ code: ExportResultCode.SUCCESS });\n return;\n }\n reportBeacon(\n this.url,\n JsonLogsSerializer.serializeRequest(logs),\n \"log\",\n resultCallback\n );\n }\n\n shutdown(): Promise<void> {\n return Promise.resolve();\n }\n\n forceFlush(): Promise<void> {\n return Promise.resolve();\n }\n}\n\n/**\n * Browser-side OTLP metric exporter — beacon transport, metric\n * payload, `DELTA` temporality.\n *\n * `DELTA` matches what the OTLP HTTP exporter the kit's previous\n * `OTLPMetricExporter` was configured with (see the\n * `temporalityPreference: AggregationTemporalityPreference.DELTA`\n * arg `provider.ts` used to pass). Returning the same temporality\n * for every instrument type keeps the wire shape downstream\n * consumers receive unchanged across the migration.\n */\nclass BeaconMetricExporter implements PushMetricExporter {\n private readonly url: string;\n\n constructor(url: string) {\n this.url = url;\n }\n\n export(\n metrics: ResourceMetrics,\n resultCallback: (result: ExportResult) => void\n ): void {\n reportBeacon(\n this.url,\n JsonMetricsSerializer.serializeRequest(metrics),\n \"metric\",\n resultCallback\n );\n }\n\n selectAggregationTemporality(): AggregationTemporality {\n return AggregationTemporality.DELTA;\n }\n\n forceFlush(): Promise<void> {\n return Promise.resolve();\n }\n\n shutdown(): Promise<void> {\n return Promise.resolve();\n }\n}\n\nexport function createBeaconTraceExporter(\n input: ExporterCommon\n): BeaconTraceExporter {\n return new BeaconTraceExporter(buildBeaconUrl(input));\n}\n\nexport function createBeaconMetricExporter(\n input: ExporterCommon\n): BeaconMetricExporter {\n return new BeaconMetricExporter(buildBeaconUrl(input));\n}\n\nexport function createBeaconLogExporter(\n input: ExporterCommon\n): BeaconLogExporter {\n return new BeaconLogExporter(buildBeaconUrl(input));\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAoDA,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;AAqB5B,MAAM,yBAAyB;AAC/B,MAAM,kBAAkB;AACxB,MAAM,mBAAmB;AAEzB,MAAM,oBAAoB;;AAgB1B,SAAgB,aAAa,cAA8B;CAIzD,OAAO,GAHU,aAAa,SAAS,IAAI,GACvC,aAAa,MAAM,GAAG,GAAG,GACzB,eACiB;;;;;;;;;;AAWvB,SAAgB,eAAe,OAA+B;CAC5D,MAAM,SAAS,IAAI,gBAAgB,GAChC,yBAAyB,kBAC3B,CAAC;CACF,MAAM,WAAW,MAAM,YAAY,IAAI,iBAAiB;CACxD,IAAI,UACF,OAAO,IAAI,iBAAiB,SAAS;CAEvC,OAAO,GAAG,aAAa,MAAM,aAAa,CAAC,GAAG,OAAO,UAAU;;;;;;;AAQjE,SAAS,gBAAyB;CAChC,OACE,OAAO,cAAc,eACrB,OAAO,UAAU,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BpC,SAAS,aACP,KACA,SACA,QACA,gBACM;CACN,IAAI,CAAC,WAAW,QAAQ,eAAe,GAAG;EACxC,KAAK,MAAM,sBAAsB,OAAO,+BAA+B;EACvE,eAAe,EAAE,MAAM,iBAAiB,SAAS,CAAC;EAClD;;CAEF,IAAI,CAAC,eAAe,EAAE;EACpB,KAAK,MACH,iEAAiE,OAAO,UAAU,QAAQ,WAAW,IACtG;EACD,eAAe,EAAE,MAAM,iBAAiB,SAAS,CAAC;EAClD;;CAeF,MAAM,OAAO,IAAI,KAAK,CAAC,QAAmC,EAAE,EAC1D,MAAM,mBACP,CAAC;CACF,IAAI,CAAC,UAAU,WAAW,KAAK,KAAK,EAClC,KAAK,MACH,mDAAmD,OAAO,+CAA+C,QAAQ,WAAW,GAC7H;CAEH,eAAe,EAAE,MAAM,iBAAiB,SAAS,CAAC;;;;;;;;;;;;;;;;;;;;;;;;AAyBpD,IAAM,sBAAN,MAAkD;CAChD;CAEA,YAAY,KAAa;EACvB,KAAK,MAAM;;CAGb,OACE,OACA,gBACM;EACN,IAAI,MAAM,WAAW,GAAG;GACtB,eAAe,EAAE,MAAM,iBAAiB,SAAS,CAAC;GAClD;;EAEF,aACE,KAAK,KACL,oBAAoB,iBAAiB,MAAM,EAC3C,SACA,eACD;;CAGH,WAA0B;EACxB,OAAO,QAAQ,SAAS;;CAG1B,aAA4B;EAC1B,OAAO,QAAQ,SAAS;;;;;;;AAQ5B,IAAM,oBAAN,MAAwB;CACtB;CAEA,YAAY,KAAa;EACvB,KAAK,MAAM;;CAGb,OACE,MACA,gBACM;EACN,IAAI,KAAK,WAAW,GAAG;GACrB,eAAe,EAAE,MAAM,iBAAiB,SAAS,CAAC;GAClD;;EAEF,aACE,KAAK,KACL,mBAAmB,iBAAiB,KAAK,EACzC,OACA,eACD;;CAGH,WAA0B;EACxB,OAAO,QAAQ,SAAS;;CAG1B,aAA4B;EAC1B,OAAO,QAAQ,SAAS;;;;;;;;;;;;;;AAe5B,IAAM,uBAAN,MAAyD;CACvD;CAEA,YAAY,KAAa;EACvB,KAAK,MAAM;;CAGb,OACE,SACA,gBACM;EACN,aACE,KAAK,KACL,sBAAsB,iBAAiB,QAAQ,EAC/C,UACA,eACD;;CAGH,+BAAuD;EACrD,OAAO,uBAAuB;;CAGhC,aAA4B;EAC1B,OAAO,QAAQ,SAAS;;CAG1B,WAA0B;EACxB,OAAO,QAAQ,SAAS;;;AAI5B,SAAgB,0BACd,OACqB;CACrB,OAAO,IAAI,oBAAoB,eAAe,MAAM,CAAC;;AAGvD,SAAgB,2BACd,OACsB;CACtB,OAAO,IAAI,qBAAqB,eAAe,MAAM,CAAC;;AAGxD,SAAgB,wBACd,OACmB;CACnB,OAAO,IAAI,kBAAkB,eAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createBeaconMetricExporter, createBeaconTraceExporter } from "./exporter.mjs";
|
|
2
|
+
import { InstrumentationsInput, registerBundledInstrumentations } from "./instrumentations.mjs";
|
|
3
|
+
import { readPropagationFromDocument } from "./propagation.mjs";
|
|
4
|
+
import { OtelProviderHandle, OtelProviderInput, buildOtelProvider } from "./provider.mjs";
|
|
5
|
+
import { captureWebVitals } from "./web-vitals.mjs";
|
|
6
|
+
export { type InstrumentationsInput, type OtelProviderHandle, type OtelProviderInput, buildOtelProvider, captureWebVitals, createBeaconMetricExporter, createBeaconTraceExporter, readPropagationFromDocument, registerBundledInstrumentations };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createBeaconMetricExporter, createBeaconTraceExporter } from "./exporter.mjs";
|
|
2
|
+
import { registerBundledInstrumentations } from "./instrumentations.mjs";
|
|
3
|
+
import { readPropagationFromDocument } from "./propagation.mjs";
|
|
4
|
+
import { buildOtelProvider } from "./provider.mjs";
|
|
5
|
+
import { captureWebVitals } from "./web-vitals.mjs";
|
|
6
|
+
export { buildOtelProvider, captureWebVitals, createBeaconMetricExporter, createBeaconTraceExporter, readPropagationFromDocument, registerBundledInstrumentations };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { WebTracerProvider } from "@opentelemetry/sdk-trace-web";
|
|
2
|
+
|
|
3
|
+
//#region src/internal/otel/instrumentations.d.ts
|
|
4
|
+
interface InstrumentationsInput {
|
|
5
|
+
/**
|
|
6
|
+
* URLs the SDK exempts from `fetch` and `XHR` instrumentation —
|
|
7
|
+
* typically our own ingest/OTLP endpoints, so the SDK doesn't trace
|
|
8
|
+
* its own export requests in an infinite loop. Combined with the
|
|
9
|
+
* `THIRD_PARTY_IGNORE` defaults and the customer's `ignoreUrls`.
|
|
10
|
+
*/
|
|
11
|
+
ignoreUrls: (string | RegExp)[];
|
|
12
|
+
/**
|
|
13
|
+
* Customer-supplied URL patterns the fetch + XHR instrumentations
|
|
14
|
+
* inject `traceparent` + `baggage` headers on. Same-origin requests
|
|
15
|
+
* always propagate.
|
|
16
|
+
*/
|
|
17
|
+
propagateContextUrls?: (string | RegExp)[];
|
|
18
|
+
/**
|
|
19
|
+
* Pathname → low-cardinality route template (e.g. `/blog/[slug]`).
|
|
20
|
+
* Drives span renaming + `url.path` / `http.route` attrs across
|
|
21
|
+
* fetch, document-load, user-interaction, and long-task.
|
|
22
|
+
*/
|
|
23
|
+
resolveRoute?: (pathname: string) => string | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* The kernel's private provider — instrumentations register against
|
|
26
|
+
* it, not the global one, so customer OTel setups stay untouched.
|
|
27
|
+
*/
|
|
28
|
+
tracerProvider: WebTracerProvider;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Registers every browser auto-instrumentation against the kernel's
|
|
32
|
+
* private provider. Every instrumentation is enriched with the same
|
|
33
|
+
* route-template-aware URL attributes so dashboards can slice fetch /
|
|
34
|
+
* interaction / long-task / resource-load by `http.route` without
|
|
35
|
+
* cardinality blow-up from raw pathnames.
|
|
36
|
+
*
|
|
37
|
+
* Returns a disposer that unregisters everything; called by
|
|
38
|
+
* `kernel.dispose()`.
|
|
39
|
+
*/
|
|
40
|
+
declare function registerBundledInstrumentations(input: InstrumentationsInput): () => void;
|
|
41
|
+
//#endregion
|
|
42
|
+
export { InstrumentationsInput, registerBundledInstrumentations };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instrumentations.d.mts","names":[],"sources":["../../../src/internal/otel/instrumentations.ts"],"mappings":";;;UAgHiB,qBAAA;;AAAjB;;;;;EAOE,UAAA,YAAsB,MAAA;EAiBW;;;;;EAXjC,oBAAA,aAAiC,MAAA;EAMjC;;;;;EAAA,YAAA,IAAgB,QAAA;EAkBF;;;;EAbd,cAAA,EAAgB,iBAAA;AAAA;;;;;;;;;;;iBAaF,+BAAA,CACd,KAAA,EAAO,qBAAA"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { describeActionable, isActionable } from "../dom/actionable.mjs";
|
|
2
|
+
import { registerInstrumentations } from "@opentelemetry/instrumentation";
|
|
3
|
+
import { BrowserNavigationInstrumentation, defaultSanitizeUrl } from "@opentelemetry/instrumentation-browser-navigation";
|
|
4
|
+
import { DocumentLoadInstrumentation } from "@opentelemetry/instrumentation-document-load";
|
|
5
|
+
import { FetchInstrumentation } from "@opentelemetry/instrumentation-fetch";
|
|
6
|
+
import { LongTaskInstrumentation } from "@opentelemetry/instrumentation-long-task";
|
|
7
|
+
import { UserInteractionInstrumentation } from "@opentelemetry/instrumentation-user-interaction";
|
|
8
|
+
import { XMLHttpRequestInstrumentation } from "@opentelemetry/instrumentation-xml-http-request";
|
|
9
|
+
//#region src/internal/otel/instrumentations.ts
|
|
10
|
+
const ATTR_HTTP_ROUTE = "http.route";
|
|
11
|
+
const ATTR_URL_FULL = "url.full";
|
|
12
|
+
const ATTR_URL_PATH = "url.path";
|
|
13
|
+
/**
|
|
14
|
+
* Mirrors the `semconvStabilityOptIn` constant from the internal
|
|
15
|
+
* `observability/browser/rum.ts`. Pins the HTTP attribute set to the
|
|
16
|
+
* stable-namespace conventions (`url.path`, `server.address`, …) so
|
|
17
|
+
* dashboards that filter on those keys aren't broken by minor
|
|
18
|
+
* upgrades of the instrumentation packages.
|
|
19
|
+
*/
|
|
20
|
+
const SEMCONV_STABILITY_OPT_IN = "http,database,messaging,gen_ai_latest_experimental";
|
|
21
|
+
/**
|
|
22
|
+
* Hosts/paths whose CLIENT fetch spans we never want to emit: they
|
|
23
|
+
* always become 1-span orphan traces (no traceparent propagation to
|
|
24
|
+
* 3rd-party origins) and clutter live tail. Conservative, customer-
|
|
25
|
+
* generic list — common analytics / ads / auth / replay vendors that
|
|
26
|
+
* many apps use. Customer-specific noise (Statsig CDNs, our own
|
|
27
|
+
* BetterStack, etc.) belongs in the customer's own `ignoreUrls` config,
|
|
28
|
+
* not hardcoded into a public SDK.
|
|
29
|
+
*/
|
|
30
|
+
const THIRD_PARTY_IGNORE = [
|
|
31
|
+
/(?:^|\/\/)clerk\./,
|
|
32
|
+
/\.clerk\./,
|
|
33
|
+
/\.sentry\./,
|
|
34
|
+
/\.intercom\./,
|
|
35
|
+
/\.posthog\./,
|
|
36
|
+
/\.googletagmanager\.com/,
|
|
37
|
+
/\.google-analytics\.com/,
|
|
38
|
+
/\.googleapis\.com/,
|
|
39
|
+
/\.doubleclick\.net/,
|
|
40
|
+
/\.facebook\.com/,
|
|
41
|
+
/\.fbcdn\.net/,
|
|
42
|
+
/\.analytics\.google\.com/,
|
|
43
|
+
/px\.ads\.linkedin\.com/,
|
|
44
|
+
/[?&]_rsc=/
|
|
45
|
+
];
|
|
46
|
+
const NEVER_MATCH = /^$/;
|
|
47
|
+
const ESCAPE_RE = /[.*+?^${}()|[\]\\]/g;
|
|
48
|
+
function sameOriginPattern() {
|
|
49
|
+
if (typeof window === "undefined") return NEVER_MATCH;
|
|
50
|
+
const { protocol, host } = window.location;
|
|
51
|
+
return new RegExp(`^${protocol}//${host.replace(ESCAPE_RE, "\\$&")}`);
|
|
52
|
+
}
|
|
53
|
+
function stampInteractionAttrs(eventType, element, span) {
|
|
54
|
+
const desc = describeActionable(element);
|
|
55
|
+
span.setAttribute("ui.event_type", eventType);
|
|
56
|
+
span.setAttribute("ui.target.tag", desc.tag);
|
|
57
|
+
if (desc.id) span.setAttribute("ui.target.id", desc.id);
|
|
58
|
+
if (desc.name) span.setAttribute("ui.target.name", desc.name);
|
|
59
|
+
if (desc.role) span.setAttribute("ui.target.role", desc.role);
|
|
60
|
+
if (desc.ariaLabel) span.setAttribute("ui.target.aria_label", desc.ariaLabel);
|
|
61
|
+
if (desc.text) span.setAttribute("ui.target.text", desc.text);
|
|
62
|
+
}
|
|
63
|
+
function stampUrlAttrs(span, resolveRoute) {
|
|
64
|
+
if (typeof window === "undefined") return;
|
|
65
|
+
const pathname = window.location.pathname;
|
|
66
|
+
const route = resolveRoute?.(pathname);
|
|
67
|
+
span.setAttribute(ATTR_URL_PATH, route ?? pathname);
|
|
68
|
+
span.setAttribute(ATTR_URL_FULL, window.location.href);
|
|
69
|
+
if (route) span.setAttribute(ATTR_HTTP_ROUTE, route);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Registers every browser auto-instrumentation against the kernel's
|
|
73
|
+
* private provider. Every instrumentation is enriched with the same
|
|
74
|
+
* route-template-aware URL attributes so dashboards can slice fetch /
|
|
75
|
+
* interaction / long-task / resource-load by `http.route` without
|
|
76
|
+
* cardinality blow-up from raw pathnames.
|
|
77
|
+
*
|
|
78
|
+
* Returns a disposer that unregisters everything; called by
|
|
79
|
+
* `kernel.dispose()`.
|
|
80
|
+
*/
|
|
81
|
+
function registerBundledInstrumentations(input) {
|
|
82
|
+
const { tracerProvider, ignoreUrls, propagateContextUrls = [] } = input;
|
|
83
|
+
const resolveRoute = input.resolveRoute;
|
|
84
|
+
const origin = sameOriginPattern();
|
|
85
|
+
const fetchIgnore = [...ignoreUrls, ...THIRD_PARTY_IGNORE];
|
|
86
|
+
return registerInstrumentations({
|
|
87
|
+
tracerProvider,
|
|
88
|
+
instrumentations: [
|
|
89
|
+
new FetchInstrumentation({
|
|
90
|
+
propagateTraceHeaderCorsUrls: [origin, ...propagateContextUrls],
|
|
91
|
+
ignoreUrls: fetchIgnore,
|
|
92
|
+
semconvStabilityOptIn: SEMCONV_STABILITY_OPT_IN,
|
|
93
|
+
measureRequestSize: true,
|
|
94
|
+
applyCustomAttributesOnSpan: (span, request) => {
|
|
95
|
+
const url = request instanceof Request ? request.url : request?.toString();
|
|
96
|
+
if (typeof url !== "string") return;
|
|
97
|
+
try {
|
|
98
|
+
const parsed = new URL(url);
|
|
99
|
+
span.setAttribute(ATTR_URL_PATH, parsed.pathname);
|
|
100
|
+
if (origin.test(url) || propagateContextUrls.some((re) => re instanceof RegExp ? re.test(url) : url.includes(re))) {
|
|
101
|
+
const method = (request instanceof Request ? request.method : void 0) ?? "GET";
|
|
102
|
+
const route = resolveRoute?.(parsed.pathname);
|
|
103
|
+
if (route) span.setAttribute(ATTR_HTTP_ROUTE, route);
|
|
104
|
+
span.updateName(`${method} ${route ?? parsed.pathname}`);
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}),
|
|
111
|
+
new XMLHttpRequestInstrumentation({
|
|
112
|
+
propagateTraceHeaderCorsUrls: [origin, ...propagateContextUrls],
|
|
113
|
+
ignoreUrls: fetchIgnore,
|
|
114
|
+
semconvStabilityOptIn: SEMCONV_STABILITY_OPT_IN
|
|
115
|
+
}),
|
|
116
|
+
new DocumentLoadInstrumentation({
|
|
117
|
+
semconvStabilityOptIn: SEMCONV_STABILITY_OPT_IN,
|
|
118
|
+
applyCustomAttributesOnSpan: { resourceFetch: (span, entry) => {
|
|
119
|
+
const name = entry?.name;
|
|
120
|
+
if (typeof name !== "string") return;
|
|
121
|
+
try {
|
|
122
|
+
const url = new URL(name);
|
|
123
|
+
const isCrossOrigin = typeof window !== "undefined" && url.origin !== window.location.origin;
|
|
124
|
+
span.updateName(isCrossOrigin ? `resource: ${url.host}${url.pathname}` : `resource: ${url.pathname}`);
|
|
125
|
+
span.setAttribute(ATTR_URL_FULL, name);
|
|
126
|
+
span.setAttribute(ATTR_URL_PATH, url.pathname);
|
|
127
|
+
span.setAttribute("server.address", url.host);
|
|
128
|
+
} catch {}
|
|
129
|
+
} }
|
|
130
|
+
}),
|
|
131
|
+
new BrowserNavigationInstrumentation({ sanitizeUrl: defaultSanitizeUrl }),
|
|
132
|
+
new UserInteractionInstrumentation({
|
|
133
|
+
eventNames: ["click", "submit"],
|
|
134
|
+
shouldPreventSpanCreation: (eventType, element, span) => {
|
|
135
|
+
if (eventType === "click" && !isActionable(element)) return true;
|
|
136
|
+
stampInteractionAttrs(eventType, element, span);
|
|
137
|
+
stampUrlAttrs(span, resolveRoute);
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}),
|
|
141
|
+
new LongTaskInstrumentation({ observerCallback: (span, info) => {
|
|
142
|
+
stampUrlAttrs(span, resolveRoute);
|
|
143
|
+
const containerType = info.longtaskEntry.attribution[0]?.containerType;
|
|
144
|
+
if (containerType) span.setAttribute("longtask.container_type", containerType);
|
|
145
|
+
} })
|
|
146
|
+
]
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
//#endregion
|
|
150
|
+
export { registerBundledInstrumentations };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instrumentations.mjs","names":[],"sources":["../../../src/internal/otel/instrumentations.ts"],"sourcesContent":["import type { Span } from \"@opentelemetry/api\";\nimport { registerInstrumentations } from \"@opentelemetry/instrumentation\";\nimport {\n BrowserNavigationInstrumentation,\n defaultSanitizeUrl,\n} from \"@opentelemetry/instrumentation-browser-navigation\";\nimport { DocumentLoadInstrumentation } from \"@opentelemetry/instrumentation-document-load\";\nimport { FetchInstrumentation } from \"@opentelemetry/instrumentation-fetch\";\nimport {\n LongTaskInstrumentation,\n type ObserverCallbackInformation,\n} from \"@opentelemetry/instrumentation-long-task\";\nimport { UserInteractionInstrumentation } from \"@opentelemetry/instrumentation-user-interaction\";\nimport { XMLHttpRequestInstrumentation } from \"@opentelemetry/instrumentation-xml-http-request\";\nimport type { WebTracerProvider } from \"@opentelemetry/sdk-trace-web\";\n\nimport { describeActionable, isActionable } from \"../dom/actionable.js\";\n\nconst ATTR_HTTP_ROUTE = \"http.route\" as const;\nconst ATTR_URL_FULL = \"url.full\" as const;\nconst ATTR_URL_PATH = \"url.path\" as const;\n\n/**\n * Mirrors the `semconvStabilityOptIn` constant from the internal\n * `observability/browser/rum.ts`. Pins the HTTP attribute set to the\n * stable-namespace conventions (`url.path`, `server.address`, …) so\n * dashboards that filter on those keys aren't broken by minor\n * upgrades of the instrumentation packages.\n */\nconst SEMCONV_STABILITY_OPT_IN =\n \"http,database,messaging,gen_ai_latest_experimental\";\n\n/**\n * Hosts/paths whose CLIENT fetch spans we never want to emit: they\n * always become 1-span orphan traces (no traceparent propagation to\n * 3rd-party origins) and clutter live tail. Conservative, customer-\n * generic list — common analytics / ads / auth / replay vendors that\n * many apps use. Customer-specific noise (Statsig CDNs, our own\n * BetterStack, etc.) belongs in the customer's own `ignoreUrls` config,\n * not hardcoded into a public SDK.\n */\nconst THIRD_PARTY_IGNORE: RegExp[] = [\n /(?:^|\\/\\/)clerk\\./,\n /\\.clerk\\./,\n /\\.sentry\\./,\n /\\.intercom\\./,\n /\\.posthog\\./,\n /\\.googletagmanager\\.com/,\n /\\.google-analytics\\.com/,\n /\\.googleapis\\.com/,\n /\\.doubleclick\\.net/,\n /\\.facebook\\.com/,\n /\\.fbcdn\\.net/,\n /\\.analytics\\.google\\.com/,\n /px\\.ads\\.linkedin\\.com/,\n /[?&]_rsc=/,\n];\n\nconst NEVER_MATCH = /^$/;\nconst ESCAPE_RE = /[.*+?^${}()|[\\]\\\\]/g;\n\nfunction sameOriginPattern(): RegExp {\n if (typeof window === \"undefined\") {\n return NEVER_MATCH;\n }\n const { protocol, host } = window.location;\n return new RegExp(`^${protocol}//${host.replace(ESCAPE_RE, \"\\\\$&\")}`);\n}\n\nfunction stampInteractionAttrs(\n eventType: string,\n element: HTMLElement,\n span: Span\n): void {\n const desc = describeActionable(element);\n span.setAttribute(\"ui.event_type\", eventType);\n span.setAttribute(\"ui.target.tag\", desc.tag);\n if (desc.id) {\n span.setAttribute(\"ui.target.id\", desc.id);\n }\n if (desc.name) {\n span.setAttribute(\"ui.target.name\", desc.name);\n }\n if (desc.role) {\n span.setAttribute(\"ui.target.role\", desc.role);\n }\n if (desc.ariaLabel) {\n span.setAttribute(\"ui.target.aria_label\", desc.ariaLabel);\n }\n // Truncated visible label — useful for triage without exfiltrating\n // full DOM contents. Only meaningful on the direct target.\n if (desc.text) {\n span.setAttribute(\"ui.target.text\", desc.text);\n }\n}\n\nfunction stampUrlAttrs(\n span: Span,\n resolveRoute: ((pathname: string) => string | undefined) | undefined\n): void {\n if (typeof window === \"undefined\") {\n return;\n }\n const pathname = window.location.pathname;\n const route = resolveRoute?.(pathname);\n span.setAttribute(ATTR_URL_PATH, route ?? pathname);\n span.setAttribute(ATTR_URL_FULL, window.location.href);\n if (route) {\n span.setAttribute(ATTR_HTTP_ROUTE, route);\n }\n}\n\nexport interface InstrumentationsInput {\n /**\n * URLs the SDK exempts from `fetch` and `XHR` instrumentation —\n * typically our own ingest/OTLP endpoints, so the SDK doesn't trace\n * its own export requests in an infinite loop. Combined with the\n * `THIRD_PARTY_IGNORE` defaults and the customer's `ignoreUrls`.\n */\n ignoreUrls: (string | RegExp)[];\n /**\n * Customer-supplied URL patterns the fetch + XHR instrumentations\n * inject `traceparent` + `baggage` headers on. Same-origin requests\n * always propagate.\n */\n propagateContextUrls?: (string | RegExp)[];\n /**\n * Pathname → low-cardinality route template (e.g. `/blog/[slug]`).\n * Drives span renaming + `url.path` / `http.route` attrs across\n * fetch, document-load, user-interaction, and long-task.\n */\n resolveRoute?: (pathname: string) => string | undefined;\n /**\n * The kernel's private provider — instrumentations register against\n * it, not the global one, so customer OTel setups stay untouched.\n */\n tracerProvider: WebTracerProvider;\n}\n\n/**\n * Registers every browser auto-instrumentation against the kernel's\n * private provider. Every instrumentation is enriched with the same\n * route-template-aware URL attributes so dashboards can slice fetch /\n * interaction / long-task / resource-load by `http.route` without\n * cardinality blow-up from raw pathnames.\n *\n * Returns a disposer that unregisters everything; called by\n * `kernel.dispose()`.\n */\nexport function registerBundledInstrumentations(\n input: InstrumentationsInput\n): () => void {\n const { tracerProvider, ignoreUrls, propagateContextUrls = [] } = input;\n const resolveRoute = input.resolveRoute;\n const origin = sameOriginPattern();\n const fetchIgnore = [...ignoreUrls, ...THIRD_PARTY_IGNORE];\n\n return registerInstrumentations({\n tracerProvider,\n instrumentations: [\n new FetchInstrumentation({\n propagateTraceHeaderCorsUrls: [origin, ...propagateContextUrls],\n ignoreUrls: fetchIgnore,\n semconvStabilityOptIn: SEMCONV_STABILITY_OPT_IN,\n measureRequestSize: true,\n applyCustomAttributesOnSpan: (\n span: Span,\n request: Request | RequestInit\n ) => {\n const url =\n request instanceof Request ? request.url : request?.toString();\n if (typeof url !== \"string\") {\n return;\n }\n try {\n const parsed = new URL(url);\n span.setAttribute(ATTR_URL_PATH, parsed.pathname);\n const isTracked =\n origin.test(url) ||\n propagateContextUrls.some((re) =>\n re instanceof RegExp ? re.test(url) : url.includes(re)\n );\n if (isTracked) {\n const method =\n (request instanceof Request ? request.method : undefined) ??\n \"GET\";\n const route = resolveRoute?.(parsed.pathname);\n if (route) {\n span.setAttribute(ATTR_HTTP_ROUTE, route);\n }\n span.updateName(`${method} ${route ?? parsed.pathname}`);\n }\n } catch {\n return;\n }\n },\n }),\n new XMLHttpRequestInstrumentation({\n propagateTraceHeaderCorsUrls: [origin, ...propagateContextUrls],\n ignoreUrls: fetchIgnore,\n semconvStabilityOptIn: SEMCONV_STABILITY_OPT_IN,\n }),\n new DocumentLoadInstrumentation({\n semconvStabilityOptIn: SEMCONV_STABILITY_OPT_IN,\n // Default `resourceFetch` span name is the literal string\n // `\"resourceFetch\"` for every entry — useless when a single\n // page load emits 30 of them. Replace with the pathname (or\n // origin+path for cross-origin) so the waterfall is scannable.\n applyCustomAttributesOnSpan: {\n resourceFetch: (span, entry) => {\n const name = entry?.name;\n if (typeof name !== \"string\") {\n return;\n }\n try {\n const url = new URL(name);\n const isCrossOrigin =\n typeof window !== \"undefined\" &&\n url.origin !== window.location.origin;\n span.updateName(\n isCrossOrigin\n ? `resource: ${url.host}${url.pathname}`\n : `resource: ${url.pathname}`\n );\n span.setAttribute(ATTR_URL_FULL, name);\n span.setAttribute(ATTR_URL_PATH, url.pathname);\n span.setAttribute(\"server.address\", url.host);\n } catch {\n // Malformed/relative URL — leave the default name and\n // let the entry's own attributes carry whatever info\n // the SDK already stamped.\n }\n },\n },\n }),\n // `BrowserNavigationInstrumentation` emits a `browser.navigation`\n // log record per hard navigation (page load) and soft navigation\n // (history.pushState, hash change, back/forward). The default\n // sanitizer redacts credentials in the URL and common sensitive\n // query params (`token`, `api_key`, `secret`, …) before emit.\n new BrowserNavigationInstrumentation({\n sanitizeUrl: defaultSanitizeUrl,\n }),\n new UserInteractionInstrumentation({\n eventNames: [\"click\", \"submit\"],\n // Despite the name, `shouldPreventSpanCreation` is also the\n // hook for *enriching* spans (return falsy → create the span;\n // the callback may set attributes on the way through). We use\n // it as both: stamp a stable target descriptor for\n // dashboarding, then suppress spans on elements that aren't\n // actionable (scroll containers, body clicks, etc.).\n shouldPreventSpanCreation: (eventType, element, span) => {\n if (eventType === \"click\" && !isActionable(element)) {\n return true;\n }\n stampInteractionAttrs(eventType, element, span);\n stampUrlAttrs(span, resolveRoute);\n return false;\n },\n }),\n // Long-task spans default to a generic `longtask` name with\n // attribution embedded as attributes — but no page context.\n // Empty `url.path` makes it impossible to triage which routes\n // are jank-heavy. Stamp it in the observer callback so\n // dashboards can slice by route.\n new LongTaskInstrumentation({\n observerCallback: (span, info: ObserverCallbackInformation) => {\n stampUrlAttrs(span, resolveRoute);\n // PerformanceLongTaskTiming attribution[0].containerType is\n // the surface that hosted the offending task. Most are\n // `\"window\"` (main frame); the few that aren't (`\"iframe\"`,\n // `\"embed\"`, `\"object\"`) are the actionable ones — surface\n // them as a top-level attr instead of leaving them buried\n // in `longtask.attribution.container_type`.\n const containerType =\n info.longtaskEntry.attribution[0]?.containerType;\n if (containerType) {\n span.setAttribute(\"longtask.container_type\", containerType);\n }\n },\n }),\n ],\n });\n}\n"],"mappings":";;;;;;;;;AAkBA,MAAM,kBAAkB;AACxB,MAAM,gBAAgB;AACtB,MAAM,gBAAgB;;;;;;;;AAStB,MAAM,2BACJ;;;;;;;;;;AAWF,MAAM,qBAA+B;CACnC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;AAED,MAAM,cAAc;AACpB,MAAM,YAAY;AAElB,SAAS,oBAA4B;CACnC,IAAI,OAAO,WAAW,aACpB,OAAO;CAET,MAAM,EAAE,UAAU,SAAS,OAAO;CAClC,OAAO,IAAI,OAAO,IAAI,SAAS,IAAI,KAAK,QAAQ,WAAW,OAAO,GAAG;;AAGvE,SAAS,sBACP,WACA,SACA,MACM;CACN,MAAM,OAAO,mBAAmB,QAAQ;CACxC,KAAK,aAAa,iBAAiB,UAAU;CAC7C,KAAK,aAAa,iBAAiB,KAAK,IAAI;CAC5C,IAAI,KAAK,IACP,KAAK,aAAa,gBAAgB,KAAK,GAAG;CAE5C,IAAI,KAAK,MACP,KAAK,aAAa,kBAAkB,KAAK,KAAK;CAEhD,IAAI,KAAK,MACP,KAAK,aAAa,kBAAkB,KAAK,KAAK;CAEhD,IAAI,KAAK,WACP,KAAK,aAAa,wBAAwB,KAAK,UAAU;CAI3D,IAAI,KAAK,MACP,KAAK,aAAa,kBAAkB,KAAK,KAAK;;AAIlD,SAAS,cACP,MACA,cACM;CACN,IAAI,OAAO,WAAW,aACpB;CAEF,MAAM,WAAW,OAAO,SAAS;CACjC,MAAM,QAAQ,eAAe,SAAS;CACtC,KAAK,aAAa,eAAe,SAAS,SAAS;CACnD,KAAK,aAAa,eAAe,OAAO,SAAS,KAAK;CACtD,IAAI,OACF,KAAK,aAAa,iBAAiB,MAAM;;;;;;;;;;;;AAyC7C,SAAgB,gCACd,OACY;CACZ,MAAM,EAAE,gBAAgB,YAAY,uBAAuB,EAAE,KAAK;CAClE,MAAM,eAAe,MAAM;CAC3B,MAAM,SAAS,mBAAmB;CAClC,MAAM,cAAc,CAAC,GAAG,YAAY,GAAG,mBAAmB;CAE1D,OAAO,yBAAyB;EAC9B;EACA,kBAAkB;GAChB,IAAI,qBAAqB;IACvB,8BAA8B,CAAC,QAAQ,GAAG,qBAAqB;IAC/D,YAAY;IACZ,uBAAuB;IACvB,oBAAoB;IACpB,8BACE,MACA,YACG;KACH,MAAM,MACJ,mBAAmB,UAAU,QAAQ,MAAM,SAAS,UAAU;KAChE,IAAI,OAAO,QAAQ,UACjB;KAEF,IAAI;MACF,MAAM,SAAS,IAAI,IAAI,IAAI;MAC3B,KAAK,aAAa,eAAe,OAAO,SAAS;MAMjD,IAJE,OAAO,KAAK,IAAI,IAChB,qBAAqB,MAAM,OACzB,cAAc,SAAS,GAAG,KAAK,IAAI,GAAG,IAAI,SAAS,GAAG,CACvD,EACY;OACb,MAAM,UACH,mBAAmB,UAAU,QAAQ,SAAS,KAAA,MAC/C;OACF,MAAM,QAAQ,eAAe,OAAO,SAAS;OAC7C,IAAI,OACF,KAAK,aAAa,iBAAiB,MAAM;OAE3C,KAAK,WAAW,GAAG,OAAO,GAAG,SAAS,OAAO,WAAW;;aAEpD;MACN;;;IAGL,CAAC;GACF,IAAI,8BAA8B;IAChC,8BAA8B,CAAC,QAAQ,GAAG,qBAAqB;IAC/D,YAAY;IACZ,uBAAuB;IACxB,CAAC;GACF,IAAI,4BAA4B;IAC9B,uBAAuB;IAKvB,6BAA6B,EAC3B,gBAAgB,MAAM,UAAU;KAC9B,MAAM,OAAO,OAAO;KACpB,IAAI,OAAO,SAAS,UAClB;KAEF,IAAI;MACF,MAAM,MAAM,IAAI,IAAI,KAAK;MACzB,MAAM,gBACJ,OAAO,WAAW,eAClB,IAAI,WAAW,OAAO,SAAS;MACjC,KAAK,WACH,gBACI,aAAa,IAAI,OAAO,IAAI,aAC5B,aAAa,IAAI,WACtB;MACD,KAAK,aAAa,eAAe,KAAK;MACtC,KAAK,aAAa,eAAe,IAAI,SAAS;MAC9C,KAAK,aAAa,kBAAkB,IAAI,KAAK;aACvC;OAMX;IACF,CAAC;GAMF,IAAI,iCAAiC,EACnC,aAAa,oBACd,CAAC;GACF,IAAI,+BAA+B;IACjC,YAAY,CAAC,SAAS,SAAS;IAO/B,4BAA4B,WAAW,SAAS,SAAS;KACvD,IAAI,cAAc,WAAW,CAAC,aAAa,QAAQ,EACjD,OAAO;KAET,sBAAsB,WAAW,SAAS,KAAK;KAC/C,cAAc,MAAM,aAAa;KACjC,OAAO;;IAEV,CAAC;GAMF,IAAI,wBAAwB,EAC1B,mBAAmB,MAAM,SAAsC;IAC7D,cAAc,MAAM,aAAa;IAOjC,MAAM,gBACJ,KAAK,cAAc,YAAY,IAAI;IACrC,IAAI,eACF,KAAK,aAAa,2BAA2B,cAAc;MAGhE,CAAC;GACH;EACF,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Context } from "@opentelemetry/api";
|
|
2
|
+
import { ZoneContextManager } from "@opentelemetry/context-zone";
|
|
3
|
+
|
|
4
|
+
//#region src/internal/otel/page-scope-context-manager.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* `ZoneContextManager` returns `ROOT_CONTEXT` whenever `context.active()` is
|
|
7
|
+
* called outside a zone the manager itself created — which in practice is
|
|
8
|
+
* most of the time:
|
|
9
|
+
*
|
|
10
|
+
* - app code calling `fetch(...)` from a `useEffect`
|
|
11
|
+
* - `PerformanceObserver` callbacks (long-task, paint timing, …)
|
|
12
|
+
* - top-level await / module init code
|
|
13
|
+
* - listeners attached before the SDK booted
|
|
14
|
+
*
|
|
15
|
+
* Spans created from those code paths default to "no parent" → root, which
|
|
16
|
+
* is why a single page load produces N disjoint traces (one per
|
|
17
|
+
* instrumentation) instead of one waterfall.
|
|
18
|
+
*
|
|
19
|
+
* This subclass adds a single fallback: if the underlying zone lookup yields
|
|
20
|
+
* `ROOT_CONTEXT`, return the page-scope context instead. The page-scope
|
|
21
|
+
* context is built once at SDK init from the `<meta name="traceparent">`
|
|
22
|
+
* tag emitted by the SSR layout. If the meta tag is absent the page scope
|
|
23
|
+
* stays at `ROOT_CONTEXT` and the manager behaves like a stock
|
|
24
|
+
* `ZoneContextManager`.
|
|
25
|
+
*/
|
|
26
|
+
declare class PageScopeContextManager extends ZoneContextManager {
|
|
27
|
+
private pageScope;
|
|
28
|
+
setPageScope(ctx: Context): void;
|
|
29
|
+
active(): Context;
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { PageScopeContextManager };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page-scope-context-manager.d.mts","names":[],"sources":["../../../src/internal/otel/page-scope-context-manager.ts"],"mappings":";;;;;;AAyBA;;;;;;;;;;;;;;;;;;;cAAa,uBAAA,SAAgC,kBAAA;EAAA,QACnC,SAAA;EAER,YAAA,CAAa,GAAA,EAAK,OAAA;EAIT,MAAA,CAAA,GAAU,OAAA;AAAA"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ROOT_CONTEXT } from "@opentelemetry/api";
|
|
2
|
+
import { ZoneContextManager } from "@opentelemetry/context-zone";
|
|
3
|
+
//#region src/internal/otel/page-scope-context-manager.ts
|
|
4
|
+
/**
|
|
5
|
+
* `ZoneContextManager` returns `ROOT_CONTEXT` whenever `context.active()` is
|
|
6
|
+
* called outside a zone the manager itself created — which in practice is
|
|
7
|
+
* most of the time:
|
|
8
|
+
*
|
|
9
|
+
* - app code calling `fetch(...)` from a `useEffect`
|
|
10
|
+
* - `PerformanceObserver` callbacks (long-task, paint timing, …)
|
|
11
|
+
* - top-level await / module init code
|
|
12
|
+
* - listeners attached before the SDK booted
|
|
13
|
+
*
|
|
14
|
+
* Spans created from those code paths default to "no parent" → root, which
|
|
15
|
+
* is why a single page load produces N disjoint traces (one per
|
|
16
|
+
* instrumentation) instead of one waterfall.
|
|
17
|
+
*
|
|
18
|
+
* This subclass adds a single fallback: if the underlying zone lookup yields
|
|
19
|
+
* `ROOT_CONTEXT`, return the page-scope context instead. The page-scope
|
|
20
|
+
* context is built once at SDK init from the `<meta name="traceparent">`
|
|
21
|
+
* tag emitted by the SSR layout. If the meta tag is absent the page scope
|
|
22
|
+
* stays at `ROOT_CONTEXT` and the manager behaves like a stock
|
|
23
|
+
* `ZoneContextManager`.
|
|
24
|
+
*/
|
|
25
|
+
var PageScopeContextManager = class extends ZoneContextManager {
|
|
26
|
+
pageScope = ROOT_CONTEXT;
|
|
27
|
+
setPageScope(ctx) {
|
|
28
|
+
this.pageScope = ctx;
|
|
29
|
+
}
|
|
30
|
+
active() {
|
|
31
|
+
const ctx = super.active();
|
|
32
|
+
return ctx === ROOT_CONTEXT ? this.pageScope : ctx;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
//#endregion
|
|
36
|
+
export { PageScopeContextManager };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page-scope-context-manager.mjs","names":[],"sources":["../../../src/internal/otel/page-scope-context-manager.ts"],"sourcesContent":["import type { Context } from \"@opentelemetry/api\";\nimport { ROOT_CONTEXT } from \"@opentelemetry/api\";\nimport { ZoneContextManager } from \"@opentelemetry/context-zone\";\n\n/**\n * `ZoneContextManager` returns `ROOT_CONTEXT` whenever `context.active()` is\n * called outside a zone the manager itself created — which in practice is\n * most of the time:\n *\n * - app code calling `fetch(...)` from a `useEffect`\n * - `PerformanceObserver` callbacks (long-task, paint timing, …)\n * - top-level await / module init code\n * - listeners attached before the SDK booted\n *\n * Spans created from those code paths default to \"no parent\" → root, which\n * is why a single page load produces N disjoint traces (one per\n * instrumentation) instead of one waterfall.\n *\n * This subclass adds a single fallback: if the underlying zone lookup yields\n * `ROOT_CONTEXT`, return the page-scope context instead. The page-scope\n * context is built once at SDK init from the `<meta name=\"traceparent\">`\n * tag emitted by the SSR layout. If the meta tag is absent the page scope\n * stays at `ROOT_CONTEXT` and the manager behaves like a stock\n * `ZoneContextManager`.\n */\nexport class PageScopeContextManager extends ZoneContextManager {\n private pageScope: Context = ROOT_CONTEXT;\n\n setPageScope(ctx: Context): void {\n this.pageScope = ctx;\n }\n\n override active(): Context {\n const ctx = super.active();\n return ctx === ROOT_CONTEXT ? this.pageScope : ctx;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAyBA,IAAa,0BAAb,cAA6C,mBAAmB;CAC9D,YAA6B;CAE7B,aAAa,KAAoB;EAC/B,KAAK,YAAY;;CAGnB,SAA2B;EACzB,MAAM,MAAM,MAAM,QAAQ;EAC1B,OAAO,QAAQ,eAAe,KAAK,YAAY"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Context } from "@opentelemetry/api";
|
|
2
|
+
|
|
3
|
+
//#region src/internal/otel/propagation.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Reads the W3C `traceparent` (and optional `tracestate`) meta tag from the
|
|
6
|
+
* document head and extracts an OTel `Context`. SSR renderers (Next, Vite,
|
|
7
|
+
* …) inject the server-side request span's context so the client SDK can
|
|
8
|
+
* stitch its spans onto the same trace.
|
|
9
|
+
*
|
|
10
|
+
* Returns `null` when:
|
|
11
|
+
* - called server-side (no `document`)
|
|
12
|
+
* - no `traceparent` meta tag is present
|
|
13
|
+
* - the extracted context has an invalid trace id
|
|
14
|
+
* - the `sampled` flag is unset — the browser doesn't honor the bit when
|
|
15
|
+
* creating child spans, so an unsampled parent would orphan every
|
|
16
|
+
* browser span under a trace with no recorded server segments. Drop
|
|
17
|
+
* instead so spans fall back to a fresh root.
|
|
18
|
+
*/
|
|
19
|
+
declare function readPropagationFromDocument(): Context | null;
|
|
20
|
+
//#endregion
|
|
21
|
+
export { readPropagationFromDocument };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"propagation.d.mts","names":[],"sources":["../../../src/internal/otel/propagation.ts"],"mappings":";;;;;AAuCA;;;;;;;;;;;;;iBAAgB,2BAAA,CAAA,GAA+B,OAAA"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ROOT_CONTEXT, defaultTextMapGetter, trace } from "@opentelemetry/api";
|
|
2
|
+
import { W3CTraceContextPropagator } from "@opentelemetry/core";
|
|
3
|
+
//#region src/internal/otel/propagation.ts
|
|
4
|
+
const TRACEPARENT_META = "traceparent";
|
|
5
|
+
const TRACESTATE_META = "tracestate";
|
|
6
|
+
const INVALID_TRACE_ID = "00000000000000000000000000000000";
|
|
7
|
+
const TRACE_FLAGS_SAMPLED = 1;
|
|
8
|
+
const W3C_PROPAGATOR = new W3CTraceContextPropagator();
|
|
9
|
+
function readMeta(name) {
|
|
10
|
+
if (typeof document === "undefined") return null;
|
|
11
|
+
return document.querySelector(`meta[name="${name}"]`)?.getAttribute("content") ?? null;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Reads the W3C `traceparent` (and optional `tracestate`) meta tag from the
|
|
15
|
+
* document head and extracts an OTel `Context`. SSR renderers (Next, Vite,
|
|
16
|
+
* …) inject the server-side request span's context so the client SDK can
|
|
17
|
+
* stitch its spans onto the same trace.
|
|
18
|
+
*
|
|
19
|
+
* Returns `null` when:
|
|
20
|
+
* - called server-side (no `document`)
|
|
21
|
+
* - no `traceparent` meta tag is present
|
|
22
|
+
* - the extracted context has an invalid trace id
|
|
23
|
+
* - the `sampled` flag is unset — the browser doesn't honor the bit when
|
|
24
|
+
* creating child spans, so an unsampled parent would orphan every
|
|
25
|
+
* browser span under a trace with no recorded server segments. Drop
|
|
26
|
+
* instead so spans fall back to a fresh root.
|
|
27
|
+
*/
|
|
28
|
+
function readPropagationFromDocument() {
|
|
29
|
+
const traceparent = readMeta(TRACEPARENT_META);
|
|
30
|
+
if (!traceparent) return null;
|
|
31
|
+
const carrier = { traceparent };
|
|
32
|
+
const tracestate = readMeta(TRACESTATE_META);
|
|
33
|
+
if (tracestate) carrier[TRACESTATE_META] = tracestate;
|
|
34
|
+
const extracted = W3C_PROPAGATOR.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter);
|
|
35
|
+
const spanCtx = trace.getSpanContext(extracted);
|
|
36
|
+
if (!spanCtx || spanCtx.traceId === INVALID_TRACE_ID || (spanCtx.traceFlags & TRACE_FLAGS_SAMPLED) === 0) return null;
|
|
37
|
+
return extracted;
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
export { readPropagationFromDocument };
|