@genn-inc/cluebase-frontend-sdk 0.0.1 → 0.0.2

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 (75) hide show
  1. package/README.md +11 -9
  2. package/dist/_generated/correlation-attribute-keys.js +4 -0
  3. package/dist/_generated/otel-span-conversion.d.ts +0 -34
  4. package/dist/_generated/otel-span-conversion.js +0 -32
  5. package/dist/context/action-context.d.ts +39 -0
  6. package/dist/context/action-context.js +298 -0
  7. package/dist/context/browser-storage.d.ts +0 -12
  8. package/dist/context/browser-storage.js +0 -13
  9. package/dist/context/identity.js +0 -4
  10. package/dist/context/organization.d.ts +0 -4
  11. package/dist/context/organization.js +0 -7
  12. package/dist/context/session.js +0 -3
  13. package/dist/context/tab.js +0 -5
  14. package/dist/context/trace.d.ts +13 -1
  15. package/dist/context/trace.js +119 -21
  16. package/dist/exporter/batch.d.ts +0 -12
  17. package/dist/exporter/batch.js +0 -27
  18. package/dist/exporter/browser-outbox.js +0 -17
  19. package/dist/exporter/client.d.ts +0 -15
  20. package/dist/exporter/client.js +0 -43
  21. package/dist/exporter/delivery-response-contract.js +0 -6
  22. package/dist/exporter/delivery-response.d.ts +0 -16
  23. package/dist/exporter/delivery-response.js +0 -19
  24. package/dist/exporter/frontend-resource.d.ts +0 -9
  25. package/dist/exporter/frontend-resource.js +0 -9
  26. package/dist/exporter/observation-source-event-exporter.d.ts +0 -28
  27. package/dist/exporter/observation-source-event-exporter.js +0 -18
  28. package/dist/exporter/parameter-snapshot.js +1 -24
  29. package/dist/exporter/raw-event-to-source-signal.js +18 -11
  30. package/dist/exporter/sanitize.d.ts +1 -12
  31. package/dist/exporter/sanitize.js +6 -39
  32. package/dist/exporter/source-event.d.ts +2 -0
  33. package/dist/exporter/span-to-source-signal.js +7 -3
  34. package/dist/exporter/text-fingerprint.d.ts +0 -8
  35. package/dist/exporter/text-fingerprint.js +0 -8
  36. package/dist/index.d.ts +0 -4
  37. package/dist/index.js +9 -39
  38. package/dist/instrumentation/action.js +3 -2
  39. package/dist/instrumentation/cluebase-authored-attributes.d.ts +0 -2
  40. package/dist/instrumentation/cluebase-authored-attributes.js +0 -23
  41. package/dist/instrumentation/common.d.ts +0 -63
  42. package/dist/instrumentation/common.js +0 -48
  43. package/dist/instrumentation/dom.d.ts +0 -25
  44. package/dist/instrumentation/dom.js +5 -41
  45. package/dist/instrumentation/drag.js +1 -2
  46. package/dist/instrumentation/form.js +1 -2
  47. package/dist/instrumentation/input.js +10 -8
  48. package/dist/instrumentation/naming-evidence.d.ts +0 -12
  49. package/dist/instrumentation/navigation.js +0 -2
  50. package/dist/instrumentation/network-request.d.ts +2 -2
  51. package/dist/instrumentation/network-request.js +1 -2
  52. package/dist/instrumentation/network-span-attributes.d.ts +0 -7
  53. package/dist/instrumentation/network-span-attributes.js +1 -3
  54. package/dist/instrumentation/network.js +6 -1
  55. package/dist/instrumentation/page.js +0 -2
  56. package/dist/instrumentation/performance.js +0 -7
  57. package/dist/integrations/react-router.js +0 -3
  58. package/dist/internal/internal-log.d.ts +0 -7
  59. package/dist/internal/internal-log.js +0 -11
  60. package/dist/provider/_version.d.ts +1 -1
  61. package/dist/provider/_version.js +1 -1
  62. package/dist/provider/config.d.ts +0 -7
  63. package/dist/provider/config.js +3 -16
  64. package/dist/provider/constants.d.ts +1 -17
  65. package/dist/provider/constants.js +1 -31
  66. package/dist/provider/privacy-policy-store.d.ts +0 -12
  67. package/dist/provider/privacy-policy-store.js +0 -12
  68. package/dist/provider/sdk-event-pipeline.d.ts +1 -1
  69. package/dist/provider/sdk-event-pipeline.js +41 -54
  70. package/dist/provider/sdk-otel-pipeline.js +50 -4
  71. package/dist/provider/sdk-runtime-surfaces.js +3 -1
  72. package/dist/provider/sdk.js +3 -23
  73. package/dist/provider/types.d.ts +3 -39
  74. package/dist/provider/usage-governor.js +0 -2
  75. package/package.json +3 -6
@@ -1,10 +1,3 @@
1
- /**
2
- * SDK-internal diagnostic logger for the Frontend SDK.
3
- *
4
- * Mirror of Node SDK ``internal-log.ts`` and Python SDK ``_internal_log.py``.
5
- * Exposes a single configurable {@link InternalLogger}. Default is a no-op
6
- * so the SDK does not write to the host page's console.
7
- */
8
1
  const SAFE_CONTEXT_KEYS = new Set([
9
2
  "attempt",
10
3
  "code",
@@ -39,9 +32,5 @@ export function logSwallowed(operation, _cause, context = {}) {
39
32
  details[key] = value;
40
33
  }
41
34
  }
42
- // Exception text and stacks may contain customer payloads, credentials, or
43
- // source paths. The internal logger is an optional diagnostic sink, so it
44
- // keeps only bounded caller-provided classification fields, matching the
45
- // backend SDKs and the API/worker log contract.
46
35
  currentLogger.debug(`cluebase.sdk swallowed ${operation}`, details);
47
36
  }
@@ -1 +1 @@
1
- export declare const SDK_VERSION: "0.0.1";
1
+ export declare const SDK_VERSION: "0.0.2";
@@ -1,3 +1,3 @@
1
1
  // This file is generated by scripts/generate-version-module.mjs.
2
2
  // Do NOT edit by hand. The source of truth is `package.json` `version`.
3
- export const SDK_VERSION = "0.0.1";
3
+ export const SDK_VERSION = "0.0.2";
@@ -1,11 +1,4 @@
1
1
  import type { CluebaseInitOptions, ResolvedCluebaseConfig } from "./types.js";
2
- /**
3
- * Read an address Cluebase is able to send to, or `null` when it cannot.
4
- *
5
- * `cluebase.init` refuses such an endpoint loudly and `resolveConfig` drops it to
6
- * `""`; the two reactions differ but the question does not, so neither restates
7
- * the rule.
8
- */
9
2
  export declare const parseSendableUrl: (value: string) => URL | null;
10
3
  export declare const resolveConfig: (options: CluebaseInitOptions) => ResolvedCluebaseConfig;
11
4
  export declare const validateConfig: (config: ResolvedCluebaseConfig) => string[];
@@ -1,4 +1,4 @@
1
- import { ALWAYS_DENIED_KEYS, DEFAULT_CONFIG, DEFAULT_FRONTEND_SOURCE_IDENTIFIER, MAX_DIAGNOSTIC_TTL_MS, } from "./constants.js";
1
+ import { ALWAYS_DENIED_KEYS, DEFAULT_API_BASE_URL, DEFAULT_CONFIG, DEFAULT_FRONTEND_SOURCE_IDENTIFIER, MAX_DIAGNOSTIC_TTL_MS, } from "./constants.js";
2
2
  import { SDK_COLLECTION_MODE } from "./contracts.js";
3
3
  const normalizeKeyList = (keys) => {
4
4
  if (!keys || keys.length === 0) {
@@ -64,17 +64,7 @@ const normalizeDiagnosticConfig = (value) => {
64
64
  enabledBy: value?.enabledBy?.trim() || null,
65
65
  };
66
66
  };
67
- // The protocols Cluebase can actually deliver over. Every place that rejects an
68
- // address reads this one set, so a browser-only scheme cannot be refused here
69
- // and accepted there.
70
67
  const SENDABLE_PROTOCOLS = new Set(["http:", "https:"]);
71
- /**
72
- * Read an address Cluebase is able to send to, or `null` when it cannot.
73
- *
74
- * `cluebase.init` refuses such an endpoint loudly and `resolveConfig` drops it to
75
- * `""`; the two reactions differ but the question does not, so neither restates
76
- * the rule.
77
- */
78
68
  export const parseSendableUrl = (value) => {
79
69
  try {
80
70
  const parsed = new URL(value.trim());
@@ -90,8 +80,6 @@ const normalizeTracePropagationOrigin = (value) => {
90
80
  if (!trimmed) {
91
81
  return null;
92
82
  }
93
- // Unlike an endpoint, an origin that does not parse is kept as written: the
94
- // caller matches these against request URLs itself.
95
83
  try {
96
84
  const parsed = new URL(trimmed);
97
85
  return SENDABLE_PROTOCOLS.has(parsed.protocol) ? parsed.origin : null;
@@ -100,10 +88,9 @@ const normalizeTracePropagationOrigin = (value) => {
100
88
  return trimmed;
101
89
  }
102
90
  };
103
- // The SDK does not emit a ProjectService environment. The server resolves
104
- // dev/prod from the `projectKey` prefix and ProjectService.
105
91
  export const resolveConfig = (options) => {
106
- const originalEndpoint = normalizeConfiguredEndpoint(options.endpoint);
92
+ const configuredEndpoint = options.endpoint?.trim() || DEFAULT_API_BASE_URL;
93
+ const originalEndpoint = normalizeConfiguredEndpoint(configuredEndpoint);
107
94
  const customDenied = normalizeKeyList(options.privacy?.deniedKeys);
108
95
  const deniedKeys = [...new Set([...ALWAYS_DENIED_KEYS, ...customDenied])];
109
96
  const tracePropagationOrigins = [
@@ -7,6 +7,7 @@ export declare const EVENT_VERSION = 1;
7
7
  export declare const SOURCE_SCHEMA_VERSION = "1";
8
8
  export declare const SURFACE_TYPE: "web";
9
9
  export declare const DEFAULT_FRONTEND_SOURCE_IDENTIFIER = "frontend";
10
+ export declare const DEFAULT_API_BASE_URL = "https://api.cluebase.io";
10
11
  export declare const MASKED_VALUE = "__cluebase_masked__";
11
12
  export declare const FORBIDDEN_PARAMETER_VALUE = "__forbidden__";
12
13
  export declare const TRUNCATED_PARAMETER_VALUE = "__truncated__";
@@ -32,23 +33,6 @@ export declare const STORAGE_KEYS: {
32
33
  readonly sessionStartedAt: "cluebase:session_started_at";
33
34
  readonly sessionLastActivityAt: "cluebase:session_last_activity_at";
34
35
  };
35
- /**
36
- * SDK-side denied keys. Stripped from event properties / metrics /
37
- * traits / headers / body fragments before the value is set as an OTel
38
- * span attribute or attached to a source-signal envelope.
39
- *
40
- * This list is a strict superset of the server-side hard-deny list
41
- * (`INGEST_HARD_DENY_KEYS`) **by construction**: both are emitted from the
42
- * same canonical data file,
43
- * `packages/cluebase-shared/src/ingest/ingest-privacy-policy.v1.json`.
44
- * It is not a hand-kept copy asserted by a parity test any more.
45
- *
46
- * `@cluebase/shared` is `private: true` and the published SDK is built with
47
- * `tsc` alone (no bundling), so the SDK cannot import it at customer
48
- * runtime. The shared-contract generator therefore writes the table into
49
- * this package instead; the generated file's banner names the command to
50
- * re-run. Do not re-declare the list, or the command, here.
51
- */
52
36
  export declare const ALWAYS_DENIED_KEYS: readonly string[];
53
37
  export declare const MAX_SCHEMA_DEPTH = 6;
54
38
  export declare const MAX_SCHEMA_KEYS = 512;
@@ -1,9 +1,6 @@
1
1
  import { SDK_ALWAYS_DENIED_KEYS } from "../_generated/ingest-privacy-tables.js";
2
2
  import { SDK_RETRY_POLICY } from "../_generated/sdk-runtime-policy.js";
3
3
  import { SDK_VERSION } from "./_version.js";
4
- // SDK_VERSION SoT is `package.json` `version`, generated into
5
- // `_version.ts` by `scripts/generate-version-module.mjs` at build time.
6
- // Re-export it here so call sites keep the existing import path.
7
4
  export { SDK_VERSION };
8
5
  export const SDK_NAME = "@genn-inc/cluebase-frontend-sdk";
9
6
  export const EVENT_SOURCE = "frontend_sdk";
@@ -11,18 +8,11 @@ export const EVENT_VERSION = 1;
11
8
  export const SOURCE_SCHEMA_VERSION = "1";
12
9
  export const SURFACE_TYPE = "web";
13
10
  export const DEFAULT_FRONTEND_SOURCE_IDENTIFIER = "frontend";
14
- // Canonical mask sentinel — matches Node SDK / Python SDK / `@cluebase/shared`
15
- // (`__cluebase_masked__`). Aligning across the 3 SDKs lets downstream analytics
16
- // queries and redaction UIs detect masked values by a single string literal.
11
+ export const DEFAULT_API_BASE_URL = "https://api.cluebase.io";
17
12
  export const MASKED_VALUE = "__cluebase_masked__";
18
13
  export const FORBIDDEN_PARAMETER_VALUE = "__forbidden__";
19
14
  export const TRUNCATED_PARAMETER_VALUE = "__truncated__";
20
15
  export const DEFAULT_CONSENT = "unknown";
21
- // 送信中の取りやめが「同意の取り消し」だったのかを、中断の理由で見分けます。
22
- // 立てる側 (`batch.ts`) と読む側 (`client.ts`) が別の module なので、字面を
23
- // 両方へ書くと片方だけずれても誰も気づきません。**ずれると取り消し後の束が
24
- // 破棄ではなく再送**になり、利用者が拒否した後の観測が顧客のサイトから出ます。
25
- // 正本を 1 つにして、両端がここを読みます (AGENTS §0.2)。
26
16
  export const CONSENT_DENIED_ABORT_REASON = "consent_denied";
27
17
  export const DEFAULT_SESSION_TIMEOUT_MS = 30 * 60 * 1000;
28
18
  export const DEFAULT_LIFECYCLE_INTERACTION_GRACE_WINDOW_MS = 30000;
@@ -30,9 +20,6 @@ export const DEFAULT_MAX_EVENT_PAYLOAD_BYTES = 48 * 1024;
30
20
  export const DEFAULT_BATCH_FLUSH_THRESHOLD_BYTES = 48 * 1024;
31
21
  export const DEFAULT_MAX_BATCH_PAYLOAD_BYTES = 60 * 1024;
32
22
  export const DEFAULT_MAX_BATCH_QUEUE_BYTES = 300 * 1024;
33
- // EventQueue の保持件数上限。 maxBytes (300KB) と二重にガードして OOM を防ぐ。
34
- // queueMaxBytes は payload size 起点で削るが、件数ベースの暴走 (極小 event の連射等)
35
- // を捕捉するために件数上限も併設する。
36
23
  export const DEFAULT_MAX_BATCH_QUEUE_ITEMS = 5000;
37
24
  export const DEFAULT_DIAGNOSTIC_TTL_MS = 600000;
38
25
  export const MAX_DIAGNOSTIC_TTL_MS = 1800000;
@@ -47,23 +34,6 @@ export const STORAGE_KEYS = {
47
34
  sessionStartedAt: "cluebase:session_started_at",
48
35
  sessionLastActivityAt: "cluebase:session_last_activity_at",
49
36
  };
50
- /**
51
- * SDK-side denied keys. Stripped from event properties / metrics /
52
- * traits / headers / body fragments before the value is set as an OTel
53
- * span attribute or attached to a source-signal envelope.
54
- *
55
- * This list is a strict superset of the server-side hard-deny list
56
- * (`INGEST_HARD_DENY_KEYS`) **by construction**: both are emitted from the
57
- * same canonical data file,
58
- * `packages/cluebase-shared/src/ingest/ingest-privacy-policy.v1.json`.
59
- * It is not a hand-kept copy asserted by a parity test any more.
60
- *
61
- * `@cluebase/shared` is `private: true` and the published SDK is built with
62
- * `tsc` alone (no bundling), so the SDK cannot import it at customer
63
- * runtime. The shared-contract generator therefore writes the table into
64
- * this package instead; the generated file's banner names the command to
65
- * re-run. Do not re-declare the list, or the command, here.
66
- */
67
37
  export const ALWAYS_DENIED_KEYS = SDK_ALWAYS_DENIED_KEYS;
68
38
  export const MAX_SCHEMA_DEPTH = 6;
69
39
  export const MAX_SCHEMA_KEYS = 512;
@@ -1,18 +1,6 @@
1
- /**
2
- * 届いた方針を適用する。**契約に合わない形は捨てて、伏せた側へ倒れます。**
3
- * 途中まで読めた分を活かすと、顧客が決めていない項目を平文にし得ます。
4
- */
5
1
  export declare const applyDeliveredPrivacyPolicy: (policy: unknown) => void;
6
- /** まだ何も適用していない状態へ戻す。= 全部伏せる。 */
7
2
  export declare const resetPrivacyPolicy: () => void;
8
- /** 塊の要求に載せる合印。持っていなければ載せない。 */
9
3
  export declare const appliedPrivacyPolicyToken: () => string | undefined;
10
- /**
11
- * この入口・この向きで、顧客が平文にした項目名。
12
- *
13
- * **入口が決まらない時は空を返します。**曖昧に当てると、顧客が 1 つの入口について
14
- * 決めたことが、関係のない通信まで平文にします。
15
- */
16
4
  export declare const allowedValuePathsFor: (input: {
17
5
  method: string | null | undefined;
18
6
  pathTemplate: string | null | undefined;
@@ -2,10 +2,6 @@ import { ingestPrivacyPolicySchema, } from "../exporter/delivery-response-contra
2
2
  const policyKey = (input) => `${input.direction} ${input.method.toUpperCase()} ${input.pathTemplate}`;
3
3
  let appliedToken = null;
4
4
  let allowedPathsByEntrance = new Map();
5
- /**
6
- * 届いた方針を適用する。**契約に合わない形は捨てて、伏せた側へ倒れます。**
7
- * 途中まで読めた分を活かすと、顧客が決めていない項目を平文にし得ます。
8
- */
9
5
  export const applyDeliveredPrivacyPolicy = (policy) => {
10
6
  const parsed = ingestPrivacyPolicySchema.safeParse(policy);
11
7
  if (!parsed.success) {
@@ -32,19 +28,11 @@ const setPrivacyPolicy = (policy) => {
32
28
  allowedPathsByEntrance = next;
33
29
  appliedToken = policy.token;
34
30
  };
35
- /** まだ何も適用していない状態へ戻す。= 全部伏せる。 */
36
31
  export const resetPrivacyPolicy = () => {
37
32
  allowedPathsByEntrance = new Map();
38
33
  appliedToken = null;
39
34
  };
40
- /** 塊の要求に載せる合印。持っていなければ載せない。 */
41
35
  export const appliedPrivacyPolicyToken = () => appliedToken ?? undefined;
42
- /**
43
- * この入口・この向きで、顧客が平文にした項目名。
44
- *
45
- * **入口が決まらない時は空を返します。**曖昧に当てると、顧客が 1 つの入口について
46
- * 決めたことが、関係のない通信まで平文にします。
47
- */
48
36
  export const allowedValuePathsFor = (input) => {
49
37
  if (!input.method || !input.pathTemplate)
50
38
  return [];
@@ -49,7 +49,7 @@ export declare class ProviderEventPipeline {
49
49
  enqueueRawEvent(event: RawCapturedEvent, skipSessionTouch?: boolean): void;
50
50
  applyUsageGovernorFeedback(feedback: IngestUsageGovernorFeedback | null | undefined): void;
51
51
  refreshSessionCaptureEligibility(): void;
52
- buildContext(): CluebaseContext;
52
+ buildContext(event?: RawCapturedEvent): CluebaseContext;
53
53
  private attachInteractionContext;
54
54
  private attachCurrentLocation;
55
55
  private handleSessionLifecycle;
@@ -4,7 +4,6 @@ import { rawEventToSourceSignalEvent } from "../exporter/raw-event-to-source-sig
4
4
  import { createFrontendResourceAttributes } from "../exporter/frontend-resource.js";
5
5
  import { sanitizeObject, } from "../exporter/sanitize.js";
6
6
  import { DEFAULT_INGEST_USAGE_GOVERNOR_FEEDBACK, hasUsageGovernorFeedbackChanged, isCostCapabilityDisabled, normalizeIngestUsageGovernorFeedback, } from "./usage-governor.js";
7
- import { DEFAULT_LIFECYCLE_INTERACTION_GRACE_WINDOW_MS, } from "./constants.js";
8
7
  import { FRONTEND_EVENT_NAME } from "./contracts.js";
9
8
  import { shouldSampleSession } from "./sampling.js";
10
9
  import { stripQueryAndHashFromUrl, toStringOrNull, toSubjectProfile, } from "./sdk-input-normalization.js";
@@ -20,13 +19,6 @@ const TRACE_ROOT_EVENTS = new Set([
20
19
  "drag_drop_completed",
21
20
  "custom_emitted",
22
21
  ]);
23
- const SUBJECT_PROFILE_TRAIT_KEYS = new Set([
24
- "name",
25
- "displayName",
26
- "email",
27
- "emailAddress",
28
- "avatarUrl",
29
- ]);
30
22
  export const OFFICIAL_CUSTOM_EVENT_NAME = {
31
23
  identityIdentified: "identity_identified",
32
24
  identityReset: "identity_reset",
@@ -146,7 +138,7 @@ export class ProviderEventPipeline {
146
138
  }
147
139
  }
148
140
  try {
149
- const context = this.buildContext();
141
+ const context = this.buildContext(eventWithLocation);
150
142
  if (SUMMARY_EVENT_NAMES.has(eventWithLocation.type)) {
151
143
  const summaryCount = eventWithLocation.payload.summaryCount;
152
144
  const droppedCount = eventWithLocation.payload.droppedCount;
@@ -211,7 +203,7 @@ export class ProviderEventPipeline {
211
203
  const rate = config.sampling.sessionSampleRate;
212
204
  this.sessionCaptureEligible = shouldSampleSession(rate);
213
205
  }
214
- buildContext() {
206
+ buildContext(event) {
215
207
  const runtime = this.runtime;
216
208
  if (!runtime) {
217
209
  throw new Error("SDK is not initialized");
@@ -221,6 +213,9 @@ export class ProviderEventPipeline {
221
213
  const organization = runtime.organizationManager.getContext();
222
214
  const environment = runtime.environmentManager.getContext();
223
215
  const trace = runtime.traceManager.getSnapshot();
216
+ const actionContext = runtime.traceManager.getBoundActionContext();
217
+ const eventActionId = toStringOrNull(event?.payload.actionId);
218
+ const eventInteractionId = toStringOrNull(event?.payload.interactionId);
224
219
  const requestContext = runtime.traceManager.createRequestContext() ??
225
220
  new TraceManager().createRequestContext();
226
221
  const privacySummary = {
@@ -228,9 +223,9 @@ export class ProviderEventPipeline {
228
223
  droppedFieldCount: 0,
229
224
  maskedFields: new Set(),
230
225
  };
231
- const subjectTraits = sanitizeContextTraits(identity.traits, runtime.config.privacy.deniedKeys, SUBJECT_PROFILE_TRAIT_KEYS, privacySummary, "subject.trait");
232
- const organizationTraits = sanitizeContextTraits(organization.traits, runtime.config.privacy.deniedKeys, new Set(), privacySummary, "organization.trait");
233
- const contextTraits = sanitizeContextTraits(environment.contextTraits, runtime.config.privacy.deniedKeys, new Set(), privacySummary, "context.trait");
226
+ const subjectTraits = sanitizeContextTraits(identity.traits, runtime.config.privacy.deniedKeys, new Set(), privacySummary, "subject.trait", false, true);
227
+ const organizationTraits = sanitizeContextTraits(organization.traits, runtime.config.privacy.deniedKeys, new Set(), privacySummary, "organization.trait", false, true);
228
+ const contextTraits = sanitizeContextTraits(environment.contextTraits, runtime.config.privacy.deniedKeys, new Set(), privacySummary, "context.trait", false, true);
234
229
  return {
235
230
  producer_id: runtime.config.producerId,
236
231
  anonymous_id: identity.anonymousId,
@@ -239,10 +234,12 @@ export class ProviderEventPipeline {
239
234
  user_profile: toSubjectProfile(identity.traits),
240
235
  session_id: session.sessionId,
241
236
  tab_id: runtime.tabManager.getTabId(),
242
- trace_id: trace.traceId ?? requestContext.traceId,
237
+ trace_id: actionContext?.traceId ?? trace.traceId ?? requestContext.traceId,
243
238
  request_id: requestContext.requestId,
244
239
  request_span_id: requestContext.requestSpanId,
245
- interaction_id: runtime.traceManager.getActiveInteractionId() ??
240
+ action_id: eventActionId ?? actionContext?.actionId ?? requestContext.actionId,
241
+ interaction_id: eventInteractionId ??
242
+ actionContext?.interactionId ??
246
243
  requestContext.interactionId,
247
244
  correlation_id: null,
248
245
  sdk_collection_mode: runtime.config.collection.mode,
@@ -283,49 +280,42 @@ export class ProviderEventPipeline {
283
280
  attachInteractionContext(event) {
284
281
  const traceManager = this.runtime?.traceManager;
285
282
  const nowMs = event.occurredAtMs ?? Date.now();
286
- const payloadInteractionId = toStringOrNull(event.payload.interactionId);
283
+ const actionContext = traceManager?.getBoundActionContext();
284
+ const payloadInteractionId = toStringOrNull(event.payload.interactionId) ??
285
+ actionContext?.interactionId ??
286
+ null;
287
+ const payloadActionId = toStringOrNull(event.payload.actionId) ?? actionContext?.actionId ?? null;
287
288
  const occurrenceKind = "frontend_action";
288
289
  if (event.type === "page_view") {
289
- const interactionId = traceManager?.startFrontendAction({
290
- interactionId: payloadInteractionId,
291
- interactionInitiator: toStringOrNull(event.payload.interactionInitiator) ?? "page_visit",
292
- interactionStartEventKind: toStringOrNull(event.payload.interactionStartEventKind) ??
293
- "page_view",
294
- viewId: toStringOrNull(event.payload.viewId),
295
- nowMs,
296
- }) ?? payloadInteractionId;
297
- if (!interactionId)
290
+ if (!payloadActionId && !payloadInteractionId)
298
291
  return event;
299
292
  return {
300
293
  ...event,
301
294
  payload: {
302
295
  ...event.payload,
303
- interactionId,
304
- interactionInitiator: toStringOrNull(event.payload.interactionInitiator) ?? "page_visit",
305
- interactionStartEventKind: toStringOrNull(event.payload.interactionStartEventKind) ??
306
- "page_view",
307
- occurrenceKind,
296
+ ...(payloadActionId ? { actionId: payloadActionId } : {}),
297
+ ...(payloadInteractionId
298
+ ? { interactionId: payloadInteractionId }
299
+ : {}),
308
300
  },
309
301
  };
310
302
  }
311
303
  if (event.type === "page_leave") {
312
- const interactionId = payloadInteractionId ??
313
- traceManager?.getActiveInteractionId() ??
314
- null;
315
- if (!interactionId)
304
+ if (!payloadActionId && !payloadInteractionId)
316
305
  return event;
317
- traceManager?.endFrontendAction(interactionId);
318
306
  return {
319
307
  ...event,
320
308
  payload: {
321
309
  ...event.payload,
322
- interactionId,
323
- occurrenceKind,
310
+ ...(payloadActionId ? { actionId: payloadActionId } : {}),
311
+ ...(payloadInteractionId
312
+ ? { interactionId: payloadInteractionId }
313
+ : {}),
324
314
  },
325
315
  };
326
316
  }
327
317
  if (isOfficialCustomLifecycleEvent(event)) {
328
- const lifecycleInteraction = traceManager?.getRecentInteractionSnapshot(nowMs, DEFAULT_LIFECYCLE_INTERACTION_GRACE_WINDOW_MS) ?? null;
318
+ const lifecycleInteraction = traceManager?.getBoundActionContext() ?? null;
329
319
  const interactionId = payloadInteractionId ?? lifecycleInteraction?.interactionId ?? null;
330
320
  if (!interactionId)
331
321
  return event;
@@ -333,6 +323,11 @@ export class ProviderEventPipeline {
333
323
  ...event,
334
324
  payload: {
335
325
  ...event.payload,
326
+ ...(lifecycleInteraction?.actionId
327
+ ? { actionId: lifecycleInteraction.actionId }
328
+ : payloadActionId
329
+ ? { actionId: payloadActionId }
330
+ : {}),
336
331
  interactionId,
337
332
  occurrenceKind,
338
333
  interactionInitiator: toStringOrNull(event.payload.interactionInitiator) ??
@@ -364,6 +359,11 @@ export class ProviderEventPipeline {
364
359
  ...event,
365
360
  payload: {
366
361
  ...event.payload,
362
+ ...(traceManager?.getActiveActionId()
363
+ ? { actionId: traceManager.getActiveActionId() }
364
+ : payloadActionId
365
+ ? { actionId: payloadActionId }
366
+ : {}),
367
367
  interactionId,
368
368
  occurrenceKind,
369
369
  interactionInitiator: toStringOrNull(event.payload.interactionInitiator) ??
@@ -373,20 +373,7 @@ export class ProviderEventPipeline {
373
373
  },
374
374
  };
375
375
  }
376
- const activeInteractionId = payloadInteractionId ??
377
- traceManager?.getActiveInteractionId() ??
378
- null;
379
- if (!activeInteractionId || activeInteractionId === payloadInteractionId) {
380
- return event;
381
- }
382
- return {
383
- ...event,
384
- payload: {
385
- ...event.payload,
386
- interactionId: activeInteractionId,
387
- occurrenceKind,
388
- },
389
- };
376
+ return event;
390
377
  }
391
378
  attachCurrentLocation(event) {
392
379
  const payload = event.payload;
@@ -469,8 +456,8 @@ export class ProviderEventPipeline {
469
456
  traceManager.setActiveSpan(eventId);
470
457
  }
471
458
  }
472
- function sanitizeContextTraits(traits, deniedKeys, excludedKeys = new Set(), summary, pathPrefix = "") {
473
- const sanitized = sanitizeObject(traits, deniedKeys, summary, pathPrefix);
459
+ function sanitizeContextTraits(traits, deniedKeys, excludedKeys = new Set(), summary, pathPrefix = "", maskIdentityPii = true, preserveExplicitTraits = false) {
460
+ const sanitized = sanitizeObject(traits, deniedKeys, summary, pathPrefix, maskIdentityPii, preserveExplicitTraits);
474
461
  if (typeof sanitized !== "object" || sanitized === null || Array.isArray(sanitized)) {
475
462
  return {};
476
463
  }
@@ -1,4 +1,4 @@
1
- import { trace } from "@opentelemetry/api";
1
+ import { context, propagation, trace } from "@opentelemetry/api";
2
2
  import { resourceFromAttributes } from "@opentelemetry/resources";
3
3
  import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
4
4
  import { WebTracerProvider } from "@opentelemetry/sdk-trace-web";
@@ -7,6 +7,15 @@ import { ObservationSourceEventExporter } from "../exporter/observation-source-e
7
7
  import { createFrontendResourceAttributes } from "../exporter/frontend-resource.js";
8
8
  import { createCluebaseNetworkPropagator } from "../instrumentation/network.js";
9
9
  import { getSessionCaptureEligibility } from "./sdk-event-pipeline.js";
10
+ const globalSnapshots = new WeakMap();
11
+ const readContextManager = () => Reflect.get(context, "_getContextManager").call(context);
12
+ const readPropagator = () => Reflect.get(propagation, "_getGlobalPropagator").call(propagation);
13
+ const isNoopContextManager = (manager) => manager.constructor.name === "NoopContextManager";
14
+ const isNoopPropagator = (propagator) => propagator.constructor.name === "NoopTextMapPropagator";
15
+ const getTracerDelegate = (provider) => {
16
+ const candidate = provider;
17
+ return candidate.getDelegate?.();
18
+ };
10
19
  export const createCluebaseOtelPipeline = ({ resolved, getTransport, getContext, }) => {
11
20
  if (!getTransport()) {
12
21
  return null;
@@ -73,9 +82,22 @@ export const createCluebaseOtelPipeline = ({ resolved, getTransport, getContext,
73
82
  const mode = resolved.otelProviderMode;
74
83
  const shouldRegister = mode === "register" || (mode === "detect" && !hasHostInstalledProvider());
75
84
  if (shouldRegister) {
85
+ const contextBeforeRegister = readContextManager();
86
+ const propagatorBeforeRegister = readPropagator();
76
87
  provider.register({
77
88
  propagator: createCluebaseNetworkPropagator(),
78
89
  });
90
+ globalSnapshots.set(provider, {
91
+ installedTracer: getTracerDelegate(trace.getTracerProvider()) === provider ? provider : null,
92
+ installedContextManager: isNoopContextManager(contextBeforeRegister)
93
+ ? readContextManager()
94
+ : null,
95
+ installedPropagator: isNoopPropagator(propagatorBeforeRegister)
96
+ ? readPropagator()
97
+ : null,
98
+ contextWasAlreadyRegistered: !isNoopContextManager(contextBeforeRegister),
99
+ propagatorWasAlreadyRegistered: !isNoopPropagator(propagatorBeforeRegister),
100
+ });
79
101
  }
80
102
  return provider;
81
103
  };
@@ -95,7 +117,10 @@ export const shutdownCluebaseOtelProvider = (provider) => {
95
117
  if (!provider) {
96
118
  return;
97
119
  }
98
- void provider.shutdown().catch(() => undefined);
120
+ void provider
121
+ .shutdown()
122
+ .catch(() => undefined)
123
+ .finally(() => restoreCluebaseOtelGlobals(provider));
99
124
  };
100
125
  export const shutdownCluebaseOtelProviderAsync = async (provider) => {
101
126
  if (!provider) {
@@ -105,8 +130,30 @@ export const shutdownCluebaseOtelProviderAsync = async (provider) => {
105
130
  await provider.shutdown();
106
131
  }
107
132
  catch {
108
- // shutdown is best-effort per OTel contract.
109
133
  }
134
+ finally {
135
+ restoreCluebaseOtelGlobals(provider);
136
+ }
137
+ };
138
+ const restoreCluebaseOtelGlobals = (provider) => {
139
+ const snapshot = globalSnapshots.get(provider);
140
+ if (!snapshot)
141
+ return;
142
+ if (snapshot.installedTracer &&
143
+ getTracerDelegate(trace.getTracerProvider()) === snapshot.installedTracer) {
144
+ trace.disable();
145
+ }
146
+ if (!snapshot.contextWasAlreadyRegistered &&
147
+ snapshot.installedContextManager &&
148
+ readContextManager() === snapshot.installedContextManager) {
149
+ context.disable();
150
+ }
151
+ if (!snapshot.propagatorWasAlreadyRegistered &&
152
+ snapshot.installedPropagator &&
153
+ readPropagator() === snapshot.installedPropagator) {
154
+ propagation.disable();
155
+ }
156
+ globalSnapshots.delete(provider);
110
157
  };
111
158
  export const forceFlushCluebaseOtelProvider = async (provider, signal) => {
112
159
  if (!provider) {
@@ -116,7 +163,6 @@ export const forceFlushCluebaseOtelProvider = async (provider, signal) => {
116
163
  await awaitWithAbort(provider.forceFlush(), signal);
117
164
  }
118
165
  catch {
119
- // forceFlush is best-effort per OTel contract.
120
166
  }
121
167
  };
122
168
  const awaitWithAbort = async (promise, signal) => {
@@ -47,6 +47,9 @@ export class ProviderRuntimeSurfaces {
47
47
  input.interactionId ??
48
48
  null);
49
49
  },
50
+ bindEventContext: (event) => {
51
+ this.getTraceManager()?.bindEventContext(event);
52
+ },
50
53
  requestFlush: () => {
51
54
  void this.flushCaptureSummaries("capture_flush");
52
55
  if (this.flushDebounceTimer)
@@ -208,7 +211,6 @@ export class ProviderRuntimeSurfaces {
208
211
  dispose();
209
212
  }
210
213
  catch {
211
- // Ignore.
212
214
  }
213
215
  }
214
216
  this.unloadHandlers = [];