@genn-inc/cluebase-frontend-sdk 0.0.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.
Files changed (140) hide show
  1. package/README.md +222 -0
  2. package/dist/_generated/correlation-attribute-keys.d.ts +2 -0
  3. package/dist/_generated/correlation-attribute-keys.js +33 -0
  4. package/dist/_generated/identity-pii-policy.d.ts +38 -0
  5. package/dist/_generated/identity-pii-policy.js +103 -0
  6. package/dist/_generated/ingest-privacy-tables.d.ts +120 -0
  7. package/dist/_generated/ingest-privacy-tables.js +890 -0
  8. package/dist/_generated/otel-resource-keys.d.ts +21 -0
  9. package/dist/_generated/otel-resource-keys.js +72 -0
  10. package/dist/_generated/otel-span-conversion.d.ts +48 -0
  11. package/dist/_generated/otel-span-conversion.js +69 -0
  12. package/dist/_generated/parameter-snapshot-leaf.d.ts +11 -0
  13. package/dist/_generated/parameter-snapshot-leaf.js +37 -0
  14. package/dist/_generated/sdk-runtime-policy.d.ts +7 -0
  15. package/dist/_generated/sdk-runtime-policy.js +14 -0
  16. package/dist/_generated/usage-governor-tables.d.ts +15 -0
  17. package/dist/_generated/usage-governor-tables.js +44 -0
  18. package/dist/context/browser-storage.d.ts +15 -0
  19. package/dist/context/browser-storage.js +38 -0
  20. package/dist/context/environment.d.ts +37 -0
  21. package/dist/context/environment.js +229 -0
  22. package/dist/context/identity.d.ts +16 -0
  23. package/dist/context/identity.js +139 -0
  24. package/dist/context/organization.d.ts +16 -0
  25. package/dist/context/organization.js +60 -0
  26. package/dist/context/session.d.ts +28 -0
  27. package/dist/context/session.js +130 -0
  28. package/dist/context/tab.d.ts +22 -0
  29. package/dist/context/tab.js +117 -0
  30. package/dist/context/trace.d.ts +76 -0
  31. package/dist/context/trace.js +163 -0
  32. package/dist/exporter/batch.d.ts +94 -0
  33. package/dist/exporter/batch.js +485 -0
  34. package/dist/exporter/browser-outbox-records.d.ts +159 -0
  35. package/dist/exporter/browser-outbox-records.js +341 -0
  36. package/dist/exporter/browser-outbox.d.ts +112 -0
  37. package/dist/exporter/browser-outbox.js +876 -0
  38. package/dist/exporter/client.d.ts +92 -0
  39. package/dist/exporter/client.js +541 -0
  40. package/dist/exporter/delivery-response-contract.d.ts +103 -0
  41. package/dist/exporter/delivery-response-contract.js +147 -0
  42. package/dist/exporter/delivery-response.d.ts +37 -0
  43. package/dist/exporter/delivery-response.js +151 -0
  44. package/dist/exporter/diagnostics.d.ts +42 -0
  45. package/dist/exporter/diagnostics.js +148 -0
  46. package/dist/exporter/event-id.d.ts +10 -0
  47. package/dist/exporter/event-id.js +75 -0
  48. package/dist/exporter/frontend-resource.d.ts +20 -0
  49. package/dist/exporter/frontend-resource.js +52 -0
  50. package/dist/exporter/local-minimization.d.ts +29 -0
  51. package/dist/exporter/local-minimization.js +87 -0
  52. package/dist/exporter/observation-source-event-exporter.d.ts +69 -0
  53. package/dist/exporter/observation-source-event-exporter.js +154 -0
  54. package/dist/exporter/parameter-snapshot.d.ts +14 -0
  55. package/dist/exporter/parameter-snapshot.js +215 -0
  56. package/dist/exporter/path-template.d.ts +1 -0
  57. package/dist/exporter/path-template.js +67 -0
  58. package/dist/exporter/raw-event-to-source-signal.d.ts +12 -0
  59. package/dist/exporter/raw-event-to-source-signal.js +297 -0
  60. package/dist/exporter/retry.d.ts +8 -0
  61. package/dist/exporter/retry.js +8 -0
  62. package/dist/exporter/sanitize.d.ts +30 -0
  63. package/dist/exporter/sanitize.js +177 -0
  64. package/dist/exporter/schema-evidence.d.ts +20 -0
  65. package/dist/exporter/schema-evidence.js +232 -0
  66. package/dist/exporter/source-event.d.ts +99 -0
  67. package/dist/exporter/source-event.js +13 -0
  68. package/dist/exporter/span-to-source-signal.d.ts +8 -0
  69. package/dist/exporter/span-to-source-signal.js +174 -0
  70. package/dist/exporter/text-fingerprint.d.ts +9 -0
  71. package/dist/exporter/text-fingerprint.js +16 -0
  72. package/dist/index.d.ts +25 -0
  73. package/dist/index.js +158 -0
  74. package/dist/instrumentation/action.d.ts +2 -0
  75. package/dist/instrumentation/action.js +171 -0
  76. package/dist/instrumentation/cluebase-authored-attributes.d.ts +4 -0
  77. package/dist/instrumentation/cluebase-authored-attributes.js +32 -0
  78. package/dist/instrumentation/common.d.ts +73 -0
  79. package/dist/instrumentation/common.js +90 -0
  80. package/dist/instrumentation/dom.d.ts +63 -0
  81. package/dist/instrumentation/dom.js +640 -0
  82. package/dist/instrumentation/drag.d.ts +2 -0
  83. package/dist/instrumentation/drag.js +82 -0
  84. package/dist/instrumentation/error.d.ts +2 -0
  85. package/dist/instrumentation/error.js +318 -0
  86. package/dist/instrumentation/form.d.ts +2 -0
  87. package/dist/instrumentation/form.js +132 -0
  88. package/dist/instrumentation/input.d.ts +2 -0
  89. package/dist/instrumentation/input.js +180 -0
  90. package/dist/instrumentation/location.d.ts +11 -0
  91. package/dist/instrumentation/location.js +69 -0
  92. package/dist/instrumentation/naming-evidence.d.ts +20 -0
  93. package/dist/instrumentation/naming-evidence.js +11 -0
  94. package/dist/instrumentation/navigation.d.ts +2 -0
  95. package/dist/instrumentation/navigation.js +144 -0
  96. package/dist/instrumentation/network-request.d.ts +103 -0
  97. package/dist/instrumentation/network-request.js +285 -0
  98. package/dist/instrumentation/network-span-attributes.d.ts +25 -0
  99. package/dist/instrumentation/network-span-attributes.js +153 -0
  100. package/dist/instrumentation/network.d.ts +16 -0
  101. package/dist/instrumentation/network.js +502 -0
  102. package/dist/instrumentation/page.d.ts +2 -0
  103. package/dist/instrumentation/page.js +56 -0
  104. package/dist/instrumentation/performance.d.ts +2 -0
  105. package/dist/instrumentation/performance.js +365 -0
  106. package/dist/integrations/next-router.d.ts +16 -0
  107. package/dist/integrations/next-router.js +18 -0
  108. package/dist/integrations/react-router.d.ts +7 -0
  109. package/dist/integrations/react-router.js +61 -0
  110. package/dist/internal/internal-log.d.ts +23 -0
  111. package/dist/internal/internal-log.js +47 -0
  112. package/dist/internal/metrics.d.ts +10 -0
  113. package/dist/internal/metrics.js +50 -0
  114. package/dist/provider/_version.d.ts +1 -0
  115. package/dist/provider/_version.js +3 -0
  116. package/dist/provider/config.d.ts +12 -0
  117. package/dist/provider/config.js +177 -0
  118. package/dist/provider/constants.d.ts +62 -0
  119. package/dist/provider/constants.js +118 -0
  120. package/dist/provider/contracts.d.ts +51 -0
  121. package/dist/provider/contracts.js +46 -0
  122. package/dist/provider/privacy-policy-store.d.ts +20 -0
  123. package/dist/provider/privacy-policy-store.js +56 -0
  124. package/dist/provider/sampling.d.ts +2 -0
  125. package/dist/provider/sampling.js +17 -0
  126. package/dist/provider/sdk-event-pipeline.d.ts +60 -0
  127. package/dist/provider/sdk-event-pipeline.js +478 -0
  128. package/dist/provider/sdk-input-normalization.d.ts +7 -0
  129. package/dist/provider/sdk-input-normalization.js +113 -0
  130. package/dist/provider/sdk-otel-pipeline.d.ts +14 -0
  131. package/dist/provider/sdk-otel-pipeline.js +148 -0
  132. package/dist/provider/sdk-runtime-surfaces.d.ts +49 -0
  133. package/dist/provider/sdk-runtime-surfaces.js +216 -0
  134. package/dist/provider/sdk.d.ts +2 -0
  135. package/dist/provider/sdk.js +614 -0
  136. package/dist/provider/types.d.ts +323 -0
  137. package/dist/provider/types.js +1 -0
  138. package/dist/provider/usage-governor.d.ts +7 -0
  139. package/dist/provider/usage-governor.js +116 -0
  140. package/package.json +59 -0
@@ -0,0 +1,502 @@
1
+ import { propagation, trace, } from "@opentelemetry/api";
2
+ import { CompositePropagator, W3CTraceContextPropagator, } from "@opentelemetry/core";
3
+ import { FetchInstrumentation } from "@opentelemetry/instrumentation-fetch";
4
+ import { XMLHttpRequestInstrumentation, } from "@opentelemetry/instrumentation-xml-http-request";
5
+ import { shouldPropagateTraceHeaders } from "@opentelemetry/sdk-trace-web";
6
+ import { CLUEBASE_SDK_REQUEST_HEADER } from "../exporter/client.js";
7
+ import { generateInteractionId, generateRequestId, generateRequestSpanId, generateTraceId, } from "../exporter/event-id.js";
8
+ import { FRONTEND_EVENT_NAME } from "../provider/contracts.js";
9
+ import { stripQueryFromUrl } from "./common.js";
10
+ import { createRequestMeta, deriveFailureTypeFromStatus, getXhrResponseBody, headersToRecord, parseXhrHeaders, } from "./network-request.js";
11
+ import { setResponseSpanAttributes } from "./network-span-attributes.js";
12
+ const NETWORK_TRACER_NAME = "@genn-inc/cluebase-frontend-sdk/network";
13
+ const networkMetaBySpan = new WeakMap();
14
+ const xhrTraceContextBySpan = new WeakMap();
15
+ const pendingRequestLifecycles = new Map();
16
+ let activeCapture = null;
17
+ let fetchInstrumentation = null;
18
+ let xhrInstrumentation = null;
19
+ let cluebaseNetworkPropagatorRegistered = false;
20
+ const REQUEST_LIFECYCLE_ORPHAN_TIMEOUT_MS = 30000;
21
+ // OTel fetch waits 300ms for browser resource timing before ending spans.
22
+ const FETCH_SPAN_END_SETTLE_MS = 350;
23
+ const pendingFetchSpanEndWaits = new Set();
24
+ class ManagedFetchInstrumentation extends FetchInstrumentation {
25
+ teardown() {
26
+ this.disable();
27
+ const currentFetch = globalThis.fetch;
28
+ if (typeof currentFetch.__unwrap === "function") {
29
+ this._unwrap(globalThis, "fetch");
30
+ }
31
+ }
32
+ }
33
+ const normalizeTracePropagationOrigin = (value) => {
34
+ const trimmed = value.trim();
35
+ if (!trimmed)
36
+ return null;
37
+ try {
38
+ return new URL(trimmed).origin.toLowerCase();
39
+ }
40
+ catch {
41
+ return trimmed.toLowerCase();
42
+ }
43
+ };
44
+ const escapeRegExp = (value) => {
45
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
46
+ };
47
+ const urlPrefixPatterns = (prefixes) => {
48
+ return prefixes
49
+ .filter(Boolean)
50
+ .map((prefix) => new RegExp(`^${escapeRegExp(prefix)}`));
51
+ };
52
+ const tracePropagationPatterns = (origins) => {
53
+ return [...origins].map((origin) => new RegExp(`^${escapeRegExp(origin)}(?:/|$)`));
54
+ };
55
+ const hasSdkTransportHeader = (headers) => {
56
+ return Object.keys(headers).some((key) => key.toLowerCase() === CLUEBASE_SDK_REQUEST_HEADER.toLowerCase());
57
+ };
58
+ const shouldIgnoreUrl = (capture, url) => {
59
+ if (!url)
60
+ return false;
61
+ return capture.ignoreUrlPrefixes.some((prefix) => prefix && url.startsWith(prefix));
62
+ };
63
+ const toTraceHeaders = (capture, traceContext) => {
64
+ const observationContext = capture.context.getObservationContext?.() ?? null;
65
+ const headers = {
66
+ "x-cluebase-trace-id": traceContext.traceId,
67
+ "x-cluebase-request-id": traceContext.requestId,
68
+ "x-cluebase-request-span-id": traceContext.requestSpanId,
69
+ };
70
+ if (traceContext.parentSpanId) {
71
+ headers["x-cluebase-parent-span-id"] = traceContext.parentSpanId;
72
+ }
73
+ if (traceContext.interactionId) {
74
+ headers["x-cluebase-interaction-id"] = traceContext.interactionId;
75
+ }
76
+ if (traceContext.originFrontendInteractionId) {
77
+ headers["x-cluebase-origin-frontend-interaction-id"] =
78
+ traceContext.originFrontendInteractionId;
79
+ }
80
+ if (observationContext?.sessionId) {
81
+ headers["x-cluebase-session-id"] = observationContext.sessionId;
82
+ }
83
+ if (observationContext?.anonymousId) {
84
+ headers["x-cluebase-anonymous-id"] = observationContext.anonymousId;
85
+ }
86
+ if (observationContext?.userId) {
87
+ headers["x-cluebase-user-id"] = observationContext.userId;
88
+ }
89
+ if (observationContext?.organizationId) {
90
+ headers["x-cluebase-organization-id"] = observationContext.organizationId;
91
+ }
92
+ if (observationContext?.tabId) {
93
+ headers["x-cluebase-tab-id"] = observationContext.tabId;
94
+ }
95
+ return headers;
96
+ };
97
+ const traceContextFromMeta = (meta) => ({
98
+ traceId: meta.traceId,
99
+ parentSpanId: meta.parentSpanId,
100
+ requestId: meta.requestId,
101
+ requestSpanId: meta.requestSpanId,
102
+ interactionId: meta.interactionId,
103
+ interactionInitiator: meta.interactionInitiator,
104
+ interactionStartEventKind: meta.interactionStartEventKind,
105
+ originFrontendInteractionId: meta.originFrontendInteractionId,
106
+ });
107
+ const CLUEBASE_NETWORK_PROPAGATION_FIELDS = [
108
+ "x-cluebase-trace-id",
109
+ "x-cluebase-request-id",
110
+ "x-cluebase-request-span-id",
111
+ "x-cluebase-parent-span-id",
112
+ "x-cluebase-interaction-id",
113
+ "x-cluebase-origin-frontend-interaction-id",
114
+ "x-cluebase-session-id",
115
+ "x-cluebase-anonymous-id",
116
+ "x-cluebase-user-id",
117
+ "x-cluebase-organization-id",
118
+ "x-cluebase-tab-id",
119
+ ];
120
+ class CluebaseNetworkTextMapPropagator {
121
+ inject(otelContext, carrier, setter) {
122
+ const capture = activeCapture;
123
+ if (!capture || capture.context.getConsent() === "denied")
124
+ return;
125
+ const span = trace.getSpan(otelContext);
126
+ if (!span)
127
+ return;
128
+ const existingMeta = networkMetaBySpan.get(span);
129
+ const traceContext = existingMeta !== undefined
130
+ ? traceContextFromMeta(existingMeta)
131
+ : createOtelRequestTraceContext(capture, span);
132
+ xhrTraceContextBySpan.set(span, traceContext);
133
+ for (const [key, value] of Object.entries(toTraceHeaders(capture, traceContext))) {
134
+ setter.set(carrier, key, value);
135
+ }
136
+ }
137
+ extract(otelContext, _carrier, _getter) {
138
+ return otelContext;
139
+ }
140
+ fields() {
141
+ return CLUEBASE_NETWORK_PROPAGATION_FIELDS;
142
+ }
143
+ }
144
+ const cluebaseNetworkPropagator = new CompositePropagator({
145
+ propagators: [
146
+ new W3CTraceContextPropagator(),
147
+ new CluebaseNetworkTextMapPropagator(),
148
+ ],
149
+ });
150
+ export const createCluebaseNetworkPropagator = () => cluebaseNetworkPropagator;
151
+ const ensureCluebaseNetworkPropagator = () => {
152
+ if (cluebaseNetworkPropagatorRegistered)
153
+ return;
154
+ cluebaseNetworkPropagatorRegistered = propagation.setGlobalPropagator(cluebaseNetworkPropagator);
155
+ };
156
+ const readSpanAttributes = (span) => {
157
+ const attributes = span.attributes;
158
+ return attributes && typeof attributes === "object" ? attributes : {};
159
+ };
160
+ const readSpanStringAttribute = (span, keys) => {
161
+ const attributes = readSpanAttributes(span);
162
+ for (const key of keys) {
163
+ const value = attributes[key];
164
+ if (typeof value === "string" && value.length > 0)
165
+ return value;
166
+ }
167
+ return null;
168
+ };
169
+ const readSpanStartTimeMs = (span) => {
170
+ const startTime = span.startTime;
171
+ if (Array.isArray(startTime) &&
172
+ typeof startTime[0] === "number" &&
173
+ typeof startTime[1] === "number") {
174
+ return startTime[0] * 1000 + startTime[1] / 1000000;
175
+ }
176
+ if (startTime instanceof Date) {
177
+ return startTime.getTime();
178
+ }
179
+ if (typeof startTime === "number" && Number.isFinite(startTime)) {
180
+ return startTime;
181
+ }
182
+ return null;
183
+ };
184
+ const readOtelRequestUrl = (span) => readSpanStringAttribute(span, ["url.full", "http.url"]);
185
+ const readOtelRequestMethod = (span) => readSpanStringAttribute(span, ["http.request.method", "http.method"]);
186
+ const isRequestObject = (request) => typeof Request !== "undefined" && request instanceof Request;
187
+ const createOtelRequestTraceContext = (capture, span) => {
188
+ const rawUrl = readOtelRequestUrl(span);
189
+ const base = capture.createRequestTraceContext({
190
+ method: readOtelRequestMethod(span),
191
+ urlKey: rawUrl ? stripQueryFromUrl(rawUrl) : null,
192
+ });
193
+ const spanContext = span.spanContext();
194
+ const parentSpanContext = span.parentSpanContext;
195
+ const spanTraceId = typeof spanContext.traceId === "string" && spanContext.traceId.length > 0
196
+ ? spanContext.traceId
197
+ : base.traceId;
198
+ const spanId = typeof spanContext.spanId === "string" && spanContext.spanId.length > 0
199
+ ? spanContext.spanId
200
+ : null;
201
+ return {
202
+ ...base,
203
+ traceId: spanTraceId,
204
+ parentSpanId: parentSpanContext?.spanId ?? base.parentSpanId,
205
+ requestSpanId: spanId ?? base.requestSpanId,
206
+ };
207
+ };
208
+ const fetchInstrumentationMethod = (request, span) => {
209
+ const method = request.method ?? readOtelRequestMethod(span) ?? "GET";
210
+ return method.toUpperCase();
211
+ };
212
+ const fetchInstrumentationHeaders = (request) => isRequestObject(request)
213
+ ? headersToRecord(request.headers)
214
+ : headersToRecord(request.headers);
215
+ const fetchInstrumentationBody = (request) => isRequestObject(request) ? request.body : (request.body ?? null);
216
+ const injectFetchCorrelationHeaders = (capture, request, meta) => {
217
+ if (!shouldPropagateTraceHeaders(meta.rawUrl, tracePropagationPatterns(capture.tracePropagationOrigins))) {
218
+ return;
219
+ }
220
+ const headers = toTraceHeaders(capture, traceContextFromMeta(meta));
221
+ if (isRequestObject(request)) {
222
+ for (const [key, value] of Object.entries(headers)) {
223
+ request.headers.set(key, value);
224
+ }
225
+ return;
226
+ }
227
+ const nextHeaders = new Headers(request.headers);
228
+ for (const [key, value] of Object.entries(headers)) {
229
+ nextHeaders.set(key, value);
230
+ }
231
+ request.headers = nextHeaders;
232
+ };
233
+ const createFetchMetaFromOtelRequest = (capture, span, request) => {
234
+ const rawUrl = readOtelRequestUrl(span);
235
+ if (!rawUrl || shouldIgnoreUrl(capture, rawUrl))
236
+ return null;
237
+ const requestHeaders = fetchInstrumentationHeaders(request);
238
+ if (hasSdkTransportHeader(requestHeaders))
239
+ return null;
240
+ const traceContext = createOtelRequestTraceContext(capture, span);
241
+ return createRequestMeta({
242
+ context: capture.context,
243
+ method: fetchInstrumentationMethod(request, span),
244
+ rawUrl,
245
+ requestHeaders,
246
+ body: fetchInstrumentationBody(request),
247
+ defaultRequestKind: "fetch",
248
+ traceContext,
249
+ startedAtMs: readSpanStartTimeMs(span) ?? Date.now(),
250
+ deniedKeys: capture.deniedKeys,
251
+ });
252
+ };
253
+ const createXhrMetaFromOtelSpan = (capture, span, xhr) => {
254
+ const responseUrl = typeof xhr.responseURL === "string" && xhr.responseURL.length > 0
255
+ ? xhr.responseURL
256
+ : null;
257
+ const rawUrl = readOtelRequestUrl(span) ?? responseUrl;
258
+ if (!rawUrl || shouldIgnoreUrl(capture, rawUrl))
259
+ return null;
260
+ return createRequestMeta({
261
+ context: capture.context,
262
+ method: readOtelRequestMethod(span) ?? "GET",
263
+ rawUrl,
264
+ requestHeaders: {},
265
+ body: null,
266
+ defaultRequestKind: "xhr",
267
+ traceContext: xhrTraceContextBySpan.get(span) ??
268
+ createOtelRequestTraceContext(capture, span),
269
+ startedAtMs: readSpanStartTimeMs(span) ?? Date.now(),
270
+ deniedKeys: capture.deniedKeys,
271
+ });
272
+ };
273
+ const emitSyntheticRequestFailedSpan = (meta, failureType, endedAtMs = Date.now()) => {
274
+ const tracer = activeCapture?.tracerProvider?.getTracer(NETWORK_TRACER_NAME) ??
275
+ trace.getTracer(NETWORK_TRACER_NAME);
276
+ const span = tracer.startSpan(FRONTEND_EVENT_NAME.requestFailed, {
277
+ startTime: meta.startedAtMs,
278
+ });
279
+ setResponseSpanAttributes(span, meta, {
280
+ normalizedResponseBody: null,
281
+ failureType,
282
+ durationMs: Math.max(0, endedAtMs - meta.startedAtMs),
283
+ observationContext: activeCapture?.context.getObservationContext?.() ?? null,
284
+ deniedKeys: activeCapture?.deniedKeys,
285
+ });
286
+ span.end(endedAtMs);
287
+ activeCapture?.context.requestFlush?.();
288
+ };
289
+ const clearPendingRequestLifecycle = (requestSpanId) => {
290
+ const pending = pendingRequestLifecycles.get(requestSpanId);
291
+ if (pending?.timeoutId) {
292
+ clearTimeout(pending.timeoutId);
293
+ }
294
+ pendingRequestLifecycles.delete(requestSpanId);
295
+ };
296
+ const schedulePendingRequestLifecycle = (meta) => {
297
+ clearPendingRequestLifecycle(meta.requestSpanId);
298
+ const timeoutId = setTimeout(() => {
299
+ const pending = pendingRequestLifecycles.get(meta.requestSpanId);
300
+ if (!pending)
301
+ return;
302
+ pendingRequestLifecycles.delete(meta.requestSpanId);
303
+ emitSyntheticRequestFailedSpan(pending.meta, "capture_lifecycle_timeout");
304
+ }, REQUEST_LIFECYCLE_ORPHAN_TIMEOUT_MS);
305
+ pendingRequestLifecycles.set(meta.requestSpanId, { meta, timeoutId });
306
+ };
307
+ const trackPendingRequestLifecycle = (meta) => {
308
+ schedulePendingRequestLifecycle(meta);
309
+ };
310
+ const markRequestLifecycleTerminal = (meta) => {
311
+ clearPendingRequestLifecycle(meta.requestSpanId);
312
+ };
313
+ const failOpenRequestLifecycles = (failureType) => {
314
+ const pending = Array.from(pendingRequestLifecycles.values());
315
+ for (const entry of pending) {
316
+ if (entry.timeoutId) {
317
+ clearTimeout(entry.timeoutId);
318
+ }
319
+ emitSyntheticRequestFailedSpan(entry.meta, failureType);
320
+ }
321
+ pendingRequestLifecycles.clear();
322
+ };
323
+ const trackFetchSpanEndWait = () => {
324
+ const wait = new Promise((resolve) => {
325
+ setTimeout(resolve, FETCH_SPAN_END_SETTLE_MS);
326
+ });
327
+ pendingFetchSpanEndWaits.add(wait);
328
+ void wait.finally(() => pendingFetchSpanEndWaits.delete(wait));
329
+ };
330
+ const waitForFetchSpanEnd = async () => {
331
+ if (pendingFetchSpanEndWaits.size === 0)
332
+ return;
333
+ await Promise.allSettled([...pendingFetchSpanEndWaits]);
334
+ };
335
+ const readFetchResponseHeaders = (result) => result instanceof Response ? headersToRecord(result.headers) : {};
336
+ const readFetchResponseSizeBytes = (result) => {
337
+ if (!(result instanceof Response))
338
+ return undefined;
339
+ const contentLength = result.headers.get("content-length");
340
+ return contentLength && !Number.isNaN(Number(contentLength))
341
+ ? Number(contentLength)
342
+ : undefined;
343
+ };
344
+ const readFetchStatusCode = (result) => {
345
+ if (result instanceof Response)
346
+ return result.status;
347
+ const status = result.status;
348
+ return typeof status === "number" && status > 0 ? status : null;
349
+ };
350
+ const readFetchFailureType = (result, statusCode) => {
351
+ if (result instanceof Response)
352
+ return deriveFailureTypeFromStatus(result.status);
353
+ if (statusCode !== null)
354
+ return deriveFailureTypeFromStatus(statusCode);
355
+ return result.name || result.message || "fetch_error";
356
+ };
357
+ const configureFetchInstrumentation = (capture) => {
358
+ if (typeof globalThis.fetch !== "function")
359
+ return;
360
+ fetchInstrumentation = new ManagedFetchInstrumentation({
361
+ enabled: false,
362
+ semconvStabilityOptIn: "http/dup",
363
+ measureRequestSize: true,
364
+ ignoreNetworkEvents: false,
365
+ ignoreUrls: urlPrefixPatterns(capture.ignoreUrlPrefixes),
366
+ propagateTraceHeaderCorsUrls: tracePropagationPatterns(capture.tracePropagationOrigins),
367
+ requestHook: (span, request) => {
368
+ if (activeCapture !== capture ||
369
+ capture.context.getConsent() === "denied") {
370
+ return;
371
+ }
372
+ const meta = createFetchMetaFromOtelRequest(capture, span, request);
373
+ if (!meta)
374
+ return;
375
+ networkMetaBySpan.set(span, meta);
376
+ injectFetchCorrelationHeaders(capture, request, meta);
377
+ trackPendingRequestLifecycle(meta);
378
+ },
379
+ applyCustomAttributesOnSpan: (span, request, result) => {
380
+ if (capture.context.getConsent() === "denied")
381
+ return;
382
+ const meta = networkMetaBySpan.get(span) ??
383
+ createFetchMetaFromOtelRequest(capture, span, request);
384
+ if (!meta)
385
+ return;
386
+ networkMetaBySpan.set(span, meta);
387
+ const statusCode = readFetchStatusCode(result);
388
+ const failureType = readFetchFailureType(result, statusCode);
389
+ setResponseSpanAttributes(span, meta, {
390
+ statusCode,
391
+ responseHeaders: readFetchResponseHeaders(result),
392
+ normalizedResponseBody: null,
393
+ responseSizeBytes: readFetchResponseSizeBytes(result),
394
+ failureType,
395
+ durationMs: Date.now() - meta.startedAtMs,
396
+ observationContext: capture.context.getObservationContext?.() ?? null,
397
+ deniedKeys: capture.deniedKeys,
398
+ });
399
+ trackFetchSpanEndWait();
400
+ markRequestLifecycleTerminal(meta);
401
+ capture.context.requestFlush?.();
402
+ },
403
+ });
404
+ if (capture.tracerProvider) {
405
+ fetchInstrumentation.setTracerProvider(capture.tracerProvider);
406
+ }
407
+ fetchInstrumentation.enable();
408
+ };
409
+ const configureXhrInstrumentation = (capture) => {
410
+ if (!xhrInstrumentation) {
411
+ xhrInstrumentation = new XMLHttpRequestInstrumentation();
412
+ }
413
+ if (capture.tracerProvider) {
414
+ xhrInstrumentation.setTracerProvider(capture.tracerProvider);
415
+ }
416
+ const xhrConfig = {
417
+ enabled: true,
418
+ semconvStabilityOptIn: "http/dup",
419
+ measureRequestSize: true,
420
+ ignoreNetworkEvents: false,
421
+ ignoreUrls: urlPrefixPatterns(capture.ignoreUrlPrefixes),
422
+ propagateTraceHeaderCorsUrls: tracePropagationPatterns(capture.tracePropagationOrigins),
423
+ applyCustomAttributesOnSpan: (span, xhr) => {
424
+ if (capture.context.getConsent() === "denied")
425
+ return;
426
+ const meta = networkMetaBySpan.get(span) ??
427
+ createXhrMetaFromOtelSpan(capture, span, xhr);
428
+ if (!meta)
429
+ return;
430
+ networkMetaBySpan.set(span, meta);
431
+ trackPendingRequestLifecycle(meta);
432
+ const statusCode = xhr.status || null;
433
+ const failureType = statusCode === null
434
+ ? "network_error"
435
+ : deriveFailureTypeFromStatus(statusCode);
436
+ const responseHeaders = parseXhrHeaders(xhr.getAllResponseHeaders());
437
+ const normalizedResponseBody = getXhrResponseBody(xhr);
438
+ setResponseSpanAttributes(span, meta, {
439
+ statusCode,
440
+ responseHeaders,
441
+ normalizedResponseBody,
442
+ failureType,
443
+ durationMs: Date.now() - meta.startedAtMs,
444
+ observationContext: capture.context.getObservationContext?.() ?? null,
445
+ deniedKeys: capture.deniedKeys,
446
+ });
447
+ markRequestLifecycleTerminal(meta);
448
+ capture.context.requestFlush?.();
449
+ },
450
+ };
451
+ xhrInstrumentation.setConfig(xhrConfig);
452
+ xhrInstrumentation.enable();
453
+ };
454
+ export const startNetworkCapture = (context, options = {}) => {
455
+ if (activeCapture) {
456
+ return {
457
+ stop: () => undefined,
458
+ };
459
+ }
460
+ const capture = {
461
+ context,
462
+ tracerProvider: options.tracerProvider,
463
+ deniedKeys: options.deniedKeys ?? [],
464
+ ignoreUrlPrefixes: options.ignoreUrlPrefixes ?? [],
465
+ tracePropagationOrigins: new Set((options.tracePropagationOrigins ?? [])
466
+ .map(normalizeTracePropagationOrigin)
467
+ .filter((origin) => Boolean(origin))),
468
+ createRequestTraceContext: options.createRequestTraceContext ??
469
+ (() => ({
470
+ traceId: generateTraceId(),
471
+ parentSpanId: null,
472
+ requestId: generateRequestId(),
473
+ requestSpanId: generateRequestSpanId(),
474
+ interactionId: generateInteractionId(),
475
+ interactionInitiator: null,
476
+ interactionStartEventKind: null,
477
+ })),
478
+ };
479
+ activeCapture = capture;
480
+ ensureCluebaseNetworkPropagator();
481
+ if (typeof globalThis.fetch === "function") {
482
+ configureFetchInstrumentation(capture);
483
+ }
484
+ if (typeof globalThis.XMLHttpRequest !== "undefined") {
485
+ configureXhrInstrumentation(capture);
486
+ }
487
+ return {
488
+ flush: async () => {
489
+ await waitForFetchSpanEnd();
490
+ },
491
+ stop: () => {
492
+ if (activeCapture !== capture)
493
+ return;
494
+ failOpenRequestLifecycles("capture_stopped");
495
+ activeCapture = null;
496
+ fetchInstrumentation?.teardown();
497
+ xhrInstrumentation?.disable();
498
+ fetchInstrumentation = null;
499
+ xhrInstrumentation = null;
500
+ },
501
+ };
502
+ };
@@ -0,0 +1,2 @@
1
+ import type { CaptureModuleContext, CaptureStopHandle } from "../provider/types.js";
2
+ export declare const startPageCapture: (context: CaptureModuleContext) => CaptureStopHandle;
@@ -0,0 +1,56 @@
1
+ import { getCurrentPageSnapshot } from "./location.js";
2
+ export const startPageCapture = (context) => {
3
+ if (typeof window === "undefined" || typeof document === "undefined") {
4
+ return { stop: () => undefined };
5
+ }
6
+ let pageEnteredAtMs = Date.now();
7
+ // Page-view events are emitted from OTel navigation/document-load signals.
8
+ // This module only keeps Cluebase-specific time-on-page evidence.
9
+ const handlePageLeave = () => {
10
+ if (context.getConsent() === "denied") {
11
+ return;
12
+ }
13
+ const snapshot = getCurrentPageSnapshot();
14
+ context.onEvent({
15
+ type: "page_leave",
16
+ occurredAtMs: Date.now(),
17
+ payload: {
18
+ viewId: context.getViewId(),
19
+ screenKey: snapshot.screenKey,
20
+ path: snapshot.path,
21
+ urlCanonical: snapshot.urlCanonical,
22
+ timeOnPageMs: Math.max(0, Date.now() - pageEnteredAtMs),
23
+ },
24
+ });
25
+ pageEnteredAtMs = Date.now();
26
+ };
27
+ const visibilityHandler = () => {
28
+ if (document.visibilityState !== "hidden") {
29
+ return;
30
+ }
31
+ try {
32
+ handlePageLeave();
33
+ }
34
+ catch (error) {
35
+ context.onInternalError("capture/page:visibilitychange", error);
36
+ }
37
+ };
38
+ const pageHideHandler = () => {
39
+ try {
40
+ handlePageLeave();
41
+ }
42
+ catch (error) {
43
+ context.onInternalError("capture/page:pagehide", error);
44
+ }
45
+ };
46
+ document.addEventListener("visibilitychange", visibilityHandler, true);
47
+ window.addEventListener("pagehide", pageHideHandler, true);
48
+ window.addEventListener("beforeunload", pageHideHandler, true);
49
+ return {
50
+ stop: () => {
51
+ document.removeEventListener("visibilitychange", visibilityHandler, true);
52
+ window.removeEventListener("pagehide", pageHideHandler, true);
53
+ window.removeEventListener("beforeunload", pageHideHandler, true);
54
+ },
55
+ };
56
+ };
@@ -0,0 +1,2 @@
1
+ import type { CaptureModuleContext, CaptureStopHandle } from "../provider/types.js";
2
+ export declare const startPerformanceCapture: (context: CaptureModuleContext) => CaptureStopHandle;