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

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 (76) hide show
  1. package/README.md +21 -0
  2. package/dist/_generated/{identity-pii-policy.d.ts → identity-pii-policy-text-contains.d.ts} +1 -1
  3. package/dist/_generated/{identity-pii-policy.js → identity-pii-policy-text-contains.js} +1 -1
  4. package/dist/_generated/identity-pii-policy-whole-value.d.ts +37 -0
  5. package/dist/_generated/identity-pii-policy-whole-value.js +106 -0
  6. package/dist/_generated/otel-span-conversion.js +1 -9
  7. package/dist/context/action-context.js +27 -7
  8. package/dist/context/browser-storage.js +1 -2
  9. package/dist/context/environment.js +5 -3
  10. package/dist/context/identity.js +5 -9
  11. package/dist/context/organization.d.ts +1 -1
  12. package/dist/context/organization.js +6 -8
  13. package/dist/context/session.js +6 -8
  14. package/dist/context/tab.js +8 -12
  15. package/dist/context/trace.js +2 -1
  16. package/dist/exporter/batch.d.ts +3 -1
  17. package/dist/exporter/batch.js +6 -10
  18. package/dist/exporter/browser-outbox-records.d.ts +1 -1
  19. package/dist/exporter/browser-outbox-records.js +15 -6
  20. package/dist/exporter/browser-outbox.d.ts +5 -2
  21. package/dist/exporter/browser-outbox.js +48 -11
  22. package/dist/exporter/client.d.ts +2 -2
  23. package/dist/exporter/client.js +15 -30
  24. package/dist/exporter/delivery-response-contract.js +67 -17
  25. package/dist/exporter/delivery-response.d.ts +3 -1
  26. package/dist/exporter/delivery-response.js +31 -20
  27. package/dist/exporter/diagnostics.d.ts +1 -1
  28. package/dist/exporter/diagnostics.js +4 -1
  29. package/dist/exporter/event-id.js +1 -3
  30. package/dist/exporter/frontend-resource.js +5 -4
  31. package/dist/exporter/observation-source-event-exporter.d.ts +1 -0
  32. package/dist/exporter/observation-source-event-exporter.js +5 -2
  33. package/dist/exporter/path-template.js +3 -1
  34. package/dist/exporter/raw-event-to-source-signal.js +4 -2
  35. package/dist/exporter/sanitize.d.ts +3 -2
  36. package/dist/exporter/sanitize.js +4 -3
  37. package/dist/exporter/sdk-core-wasm.d.ts +5 -0
  38. package/dist/exporter/sdk-core-wasm.js +18 -0
  39. package/dist/exporter/source-event.d.ts +1 -0
  40. package/dist/exporter/span-to-source-signal.d.ts +1 -1
  41. package/dist/exporter/span-to-source-signal.js +122 -82
  42. package/dist/index.d.ts +5 -4
  43. package/dist/index.js +23 -11
  44. package/dist/instrumentation/action.js +9 -8
  45. package/dist/instrumentation/dom.js +13 -21
  46. package/dist/instrumentation/drag.js +0 -3
  47. package/dist/instrumentation/error.js +3 -1
  48. package/dist/instrumentation/form.js +5 -6
  49. package/dist/instrumentation/input.js +4 -8
  50. package/dist/instrumentation/location.js +3 -1
  51. package/dist/instrumentation/network-request.js +1 -1
  52. package/dist/instrumentation/network-span-attributes.js +2 -2
  53. package/dist/instrumentation/network.js +7 -3
  54. package/dist/instrumentation/performance.js +1 -2
  55. package/dist/internal/internal-log.js +2 -1
  56. package/dist/provider/_version.d.ts +1 -1
  57. package/dist/provider/_version.js +1 -1
  58. package/dist/provider/config.js +0 -1
  59. package/dist/provider/constants.d.ts +1 -1
  60. package/dist/provider/constants.js +0 -1
  61. package/dist/provider/sdk-error.d.ts +7 -0
  62. package/dist/provider/sdk-error.js +48 -0
  63. package/dist/provider/sdk-event-pipeline.d.ts +2 -1
  64. package/dist/provider/sdk-event-pipeline.js +6 -3
  65. package/dist/provider/sdk-input-normalization.js +2 -2
  66. package/dist/provider/sdk-otel-pipeline.js +5 -4
  67. package/dist/provider/sdk-runtime-surfaces.d.ts +15 -12
  68. package/dist/provider/sdk-runtime-surfaces.js +8 -7
  69. package/dist/provider/sdk.js +81 -12
  70. package/dist/provider/types.d.ts +6 -5
  71. package/dist/wasm/pkg-web/cluebase_sdk_core_wasm.d.ts +41 -0
  72. package/dist/wasm/pkg-web/cluebase_sdk_core_wasm.js +267 -0
  73. package/dist/wasm/pkg-web/cluebase_sdk_core_wasm_bg.wasm +0 -0
  74. package/dist/wasm/pkg-web/cluebase_sdk_core_wasm_bg.wasm.d.ts +12 -0
  75. package/dist/wasm/pkg-web/package.json +15 -0
  76. package/package.json +10 -3
@@ -56,7 +56,9 @@ export const toPathTemplate = (pathname) => {
56
56
  }
57
57
  const normalizedPath = pathname.startsWith("/") ? pathname : `/${pathname}`;
58
58
  const [pathOnly] = normalizedPath.split("?");
59
- const rawSegments = pathOnly.split("/").filter((segment) => segment.length > 0);
59
+ const rawSegments = pathOnly
60
+ .split("/")
61
+ .filter((segment) => segment.length > 0);
60
62
  const segments = rawSegments.map((segment, index) => shouldTemplateSegment(segment, rawSegments[index - 1] ?? null)
61
63
  ? ":id"
62
64
  : segment);
@@ -2,8 +2,8 @@ import { CLUEBASE_CORRELATION_ATTRIBUTE_KEYS } from "../_generated/correlation-a
2
2
  import { cluebaseAuthoredPropertyAttribute } from "../instrumentation/cluebase-authored-attributes.js";
3
3
  import { EVENT_SOURCE, EVENT_VERSION, SOURCE_SCHEMA_VERSION, } from "../provider/constants.js";
4
4
  import { generateEventId, generateInteractionId, generateRequestId, generateRequestSpanId, generateTraceId, } from "./event-id.js";
5
- import { isDeniedKey } from "./sanitize.js";
6
5
  import { toFrontendSourceResource, } from "./frontend-resource.js";
6
+ import { isDeniedKey } from "./sanitize.js";
7
7
  import { SDK_SOURCE_SIGNAL_EVENT_NAME, SDK_SOURCE_SIGNAL_EVENT_TYPE, } from "./source-event.js";
8
8
  export const RAW_FRONTEND_SOURCE_SIGNAL_KIND = "frontend_raw_event";
9
9
  const PUBLIC_CUSTOM_EVENT_NAME_PATTERN = /^[A-Za-z0-9_.:-]{1,128}$/;
@@ -236,7 +236,8 @@ export function rawEventToSourceSignalEvent(raw, options) {
236
236
  if (viewId)
237
237
  attributes["cluebase.view.id"] = viewId;
238
238
  if (failureType) {
239
- attributes[cluebaseAuthoredPropertyAttribute("failure_type")] = failureType;
239
+ attributes[cluebaseAuthoredPropertyAttribute("failure_type")] =
240
+ failureType;
240
241
  }
241
242
  if (outcomeClass)
242
243
  attributes["cluebase.outcome_class"] = outcomeClass;
@@ -291,6 +292,7 @@ export function rawEventToSourceSignalEvent(raw, options) {
291
292
  attributes,
292
293
  events: [],
293
294
  resource: toFrontendSourceResource(options.resource, options.deniedKeys),
295
+ raw_resource: toFrontendSourceResource(options.resource, options.deniedKeys),
294
296
  context: contextRecord(context, {
295
297
  session_id: sessionId,
296
298
  trace_id: traceId,
@@ -1,4 +1,5 @@
1
- import { detectIdentityPII } from "../_generated/identity-pii-policy.js";
1
+ import { detectTextContainsPII } from "../_generated/identity-pii-policy-text-contains.js";
2
+ import { detectWholeValuePII } from "../_generated/identity-pii-policy-whole-value.js";
2
3
  export type SanitizationAccumulator = {
3
4
  maskedValueCount: number;
4
5
  droppedFieldCount: number;
@@ -15,5 +16,5 @@ export declare const parseUrlForNetwork: (rawUrl: string) => {
15
16
  urlMaskedQuery: string;
16
17
  };
17
18
  export declare const maybeParseJson: (value: unknown) => unknown;
18
- export { detectIdentityPII };
19
+ export { detectTextContainsPII, detectWholeValuePII };
19
20
  export declare const hashIdentityValue: (value: string) => string;
@@ -1,6 +1,7 @@
1
1
  import { sha256 } from "@noble/hashes/sha256";
2
2
  import { bytesToHex, utf8ToBytes } from "@noble/hashes/utils";
3
- import { detectIdentityPII, PII_HASH_HEX_LENGTH, PII_HASH_PREFIX, } from "../_generated/identity-pii-policy.js";
3
+ import { detectTextContainsPII, PII_HASH_HEX_LENGTH, PII_HASH_PREFIX, } from "../_generated/identity-pii-policy-text-contains.js";
4
+ import { detectWholeValuePII } from "../_generated/identity-pii-policy-whole-value.js";
4
5
  import { isDeniedLookupKey, toDeniedLookupKeySet, } from "../_generated/ingest-privacy-tables.js";
5
6
  import { toPathTemplate } from "../exporter/path-template.js";
6
7
  import { ALWAYS_DENIED_KEYS, MASKED_VALUE, MAX_SANITIZE_DEPTH, } from "../provider/constants.js";
@@ -67,7 +68,7 @@ export const sanitizeObject = (value, deniedKeys = [], summary, pathPrefix = "",
67
68
  if (maskIdentityPii &&
68
69
  !preserveExplicitTraits &&
69
70
  typeof current === "string" &&
70
- detectIdentityPII(current) !== null) {
71
+ detectTextContainsPII(current) !== null) {
71
72
  if (current === MASKED_VALUE)
72
73
  return current;
73
74
  if (summary) {
@@ -140,5 +141,5 @@ export const maybeParseJson = (value) => {
140
141
  return null;
141
142
  }
142
143
  };
143
- export { detectIdentityPII };
144
+ export { detectTextContainsPII, detectWholeValuePII };
144
145
  export const hashIdentityValue = (value) => `${PII_HASH_PREFIX}${bytesToHex(sha256(utf8ToBytes(value))).slice(0, PII_HASH_HEX_LENGTH)}`;
@@ -0,0 +1,5 @@
1
+ import init from "../wasm/pkg-web/cluebase_sdk_core_wasm.js";
2
+ export type SdkCoreWasmInitInput = Parameters<typeof init>[0];
3
+ export declare const initSdkCoreWasm: (input?: SdkCoreWasmInitInput) => Promise<void>;
4
+ export declare const isSdkCoreWasmReady: () => boolean;
5
+ export declare const spanToSdkSignalJson: (spanJson: string, optionsJson: string) => string;
@@ -0,0 +1,18 @@
1
+ import init, { spanToSdkSignalJson as wasmSpanToSdkSignalJson, } from "../wasm/pkg-web/cluebase_sdk_core_wasm.js";
2
+ let readyPromise = null;
3
+ let ready = false;
4
+ export const initSdkCoreWasm = (input) => {
5
+ if (!readyPromise) {
6
+ readyPromise = init(input).then(() => {
7
+ ready = true;
8
+ });
9
+ }
10
+ return readyPromise;
11
+ };
12
+ export const isSdkCoreWasmReady = () => ready;
13
+ export const spanToSdkSignalJson = (spanJson, optionsJson) => {
14
+ if (!ready) {
15
+ throw new Error("cluebase sdk core wasm is not initialized; call initSdkCoreWasm() first");
16
+ }
17
+ return wasmSpanToSdkSignalJson(spanJson, optionsJson);
18
+ };
@@ -61,6 +61,7 @@ export type BrowserSdkSignal = {
61
61
  attributes: Record<string, CanonicalJsonValue>;
62
62
  }>;
63
63
  resource: Record<string, CanonicalJsonValue>;
64
+ raw_resource: Record<string, CanonicalJsonValue>;
64
65
  context: BrowserSdkSignalContext;
65
66
  };
66
67
  export type BrowserSourceSignalEvent = {
@@ -5,4 +5,4 @@ export interface SpanToSourceSignalOptions {
5
5
  context: CluebaseContext;
6
6
  deniedKeys?: readonly string[];
7
7
  }
8
- export declare function spanToSourceSignalEvent(span: ReadableSpan, options: SpanToSourceSignalOptions): BrowserSourceSignalEvent;
8
+ export declare function spanToSourceSignalEvent(span: ReadableSpan, options: SpanToSourceSignalOptions): Promise<BrowserSourceSignalEvent>;
@@ -1,42 +1,69 @@
1
1
  import { CLUEBASE_CORRELATION_ATTRIBUTE_KEYS } from "../_generated/correlation-attribute-keys.js";
2
- import { attrString, hrTimeToUnixNano, nanosToIso, parentSpanId, } from "../_generated/otel-span-conversion.js";
3
- import { EVENT_SOURCE, EVENT_VERSION, SOURCE_SCHEMA_VERSION, } from "../provider/constants.js";
4
- import { generateEventId, generateInteractionId, generateRequestId, generateRequestSpanId, generateTraceId, } from "./event-id.js";
2
+ import { nanosToIso, parentSpanId } from "../_generated/otel-span-conversion.js";
3
+ import { ALWAYS_DENIED_KEYS, EVENT_SOURCE, EVENT_VERSION, SOURCE_SCHEMA_VERSION, } from "../provider/constants.js";
4
+ import { generateEventId } from "./event-id.js";
5
5
  import { isDeniedKey } from "./sanitize.js";
6
- import { toFrontendSourceResource } from "./frontend-resource.js";
6
+ import { initSdkCoreWasm, isSdkCoreWasmReady, spanToSdkSignalJson, } from "./sdk-core-wasm.js";
7
7
  import { SDK_SOURCE_SIGNAL_EVENT_NAME, SDK_SOURCE_SIGNAL_EVENT_TYPE, } from "./source-event.js";
8
- const isDynamicJsonObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
9
- function toJsonValue(value) {
10
- if (value === null)
11
- return null;
12
- if (typeof value === "string" ||
13
- typeof value === "number" ||
14
- typeof value === "boolean") {
15
- return typeof value === "number" && !Number.isFinite(value) ? null : value;
8
+ function withCoreMasking(summary, masking) {
9
+ const maskedValueCount = masking?.masked_value_count ?? 0;
10
+ const maskedFields = masking?.masked_fields ?? [];
11
+ if (!summary) {
12
+ if (maskedValueCount === 0 && maskedFields.length === 0)
13
+ return undefined;
14
+ return {
15
+ level: "masked",
16
+ masked_value_count: maskedValueCount,
17
+ dropped_field_count: 0,
18
+ masked_fields: [...maskedFields].sort(),
19
+ rule_set_version: null,
20
+ };
16
21
  }
17
- if (Array.isArray(value))
18
- return value.map((entry) => toJsonValue(entry));
19
- if (isDynamicJsonObject(value)) {
20
- const record = {};
21
- for (const [key, entry] of Object.entries(value)) {
22
- record[key] = toJsonValue(entry);
23
- }
24
- return record;
25
- }
26
- if (typeof value === "bigint")
27
- return value.toString();
28
- return null;
22
+ return {
23
+ ...summary,
24
+ masked_value_count: summary.masked_value_count + maskedValueCount,
25
+ masked_fields: [
26
+ ...new Set([...summary.masked_fields, ...maskedFields]),
27
+ ].sort(),
28
+ };
29
+ }
30
+ function rawAttrString(attributes, key) {
31
+ const value = attributes?.[key];
32
+ return typeof value === "string" && value.trim().length > 0
33
+ ? value.trim()
34
+ : null;
29
35
  }
30
- function toJsonRecord(value, deniedKeys = []) {
31
- const record = {};
32
- for (const [key, entry] of Object.entries(value ?? {})) {
36
+ function preFilterDeniedAttributes(attributes, deniedKeys) {
37
+ const filtered = {};
38
+ for (const [key, value] of Object.entries(attributes ?? {})) {
33
39
  if (!CLUEBASE_CORRELATION_ATTRIBUTE_KEYS.has(key) &&
34
40
  isDeniedKey(key, deniedKeys)) {
35
41
  continue;
36
42
  }
37
- record[key] = toJsonValue(entry);
43
+ filtered[key] = value;
44
+ }
45
+ return filtered;
46
+ }
47
+ function seedFallbackAttribute(attributesForCore, attributeKey, candidateKeys, fallback) {
48
+ const alreadyPresent = candidateKeys.some((candidateKey) => rawAttrString(attributesForCore, candidateKey) !== null);
49
+ if (alreadyPresent || !fallback)
50
+ return;
51
+ attributesForCore[attributeKey] = fallback;
52
+ }
53
+ function resourceAttributesInput(resource) {
54
+ const input = resource;
55
+ try {
56
+ return { ...(input?.attributes ?? {}) };
57
+ }
58
+ catch {
59
+ return {};
38
60
  }
39
- return record;
61
+ }
62
+ const FIXED_CORE_DENIED_KEYS = Object.freeze([...new Set(ALWAYS_DENIED_KEYS)].sort());
63
+ function coreDeniedKeys(deniedKeys) {
64
+ if (deniedKeys.length === 0)
65
+ return FIXED_CORE_DENIED_KEYS;
66
+ return [...new Set([...FIXED_CORE_DENIED_KEYS, ...deniedKeys])].sort();
40
67
  }
41
68
  function instrumentationScopeName(span) {
42
69
  const instrumentation = span;
@@ -87,34 +114,49 @@ function contextRecord(context, overrides) {
87
114
  context_traits: context.context_traits,
88
115
  };
89
116
  }
90
- export function spanToSourceSignalEvent(span, options) {
117
+ export async function spanToSourceSignalEvent(span, options) {
118
+ if (!isSdkCoreWasmReady()) {
119
+ await initSdkCoreWasm();
120
+ }
91
121
  const context = options.context;
92
- const attributes = toJsonRecord(span.attributes, options.deniedKeys);
122
+ const deniedKeys = options.deniedKeys ?? [];
93
123
  const spanContext = span.spanContext();
94
- const traceId = attrString(attributes, "cluebase.trace_id") ??
95
- spanContext.traceId ??
96
- generateTraceId();
97
124
  const spanId = spanContext.spanId || null;
98
- const parentId = parentSpanId(span, attributes);
99
- const startTimeUnixNano = hrTimeToUnixNano(span.startTime);
100
- const endTimeUnixNano = hrTimeToUnixNano(span.endTime);
125
+ const parentId = parentSpanId(span, span.attributes);
126
+ const traceId = rawAttrString(span.attributes, "cluebase.trace_id") ??
127
+ spanContext.traceId ??
128
+ undefined;
129
+ const attributesForCore = preFilterDeniedAttributes(span.attributes, deniedKeys);
130
+ seedFallbackAttribute(attributesForCore, "cluebase.request_span_id", ["cluebase.request_span_id"], spanId ?? context.request_span_id);
131
+ seedFallbackAttribute(attributesForCore, "cluebase.request_id", ["cluebase.request.id", "cluebase.request_id"], context.request_id);
132
+ seedFallbackAttribute(attributesForCore, "cluebase.action_id", ["cluebase.action.id", "cluebase.action_id"], context.action_id ?? undefined);
133
+ seedFallbackAttribute(attributesForCore, "cluebase.interaction_id", ["cluebase.interaction.id", "cluebase.interaction_id"], context.interaction_id);
134
+ const corePayload = {
135
+ name: span.name,
136
+ kind: typeof span.kind === "number" ? span.kind : undefined,
137
+ trace_id: traceId,
138
+ span_id: spanId ?? undefined,
139
+ parent_span_id: parentId ?? undefined,
140
+ start_time: span.startTime,
141
+ end_time: span.endTime,
142
+ status: span.status
143
+ ? { code: span.status.code ?? undefined, message: span.status.message }
144
+ : undefined,
145
+ attributes: attributesForCore,
146
+ events: (span.events ?? []).map((event) => ({
147
+ name: event.name,
148
+ attributes: preFilterDeniedAttributes(event.attributes, deniedKeys),
149
+ })),
150
+ resource_attributes: resourceAttributesInput(span.resource),
151
+ instrumentation_scope_name: instrumentationScopeName(span) ?? undefined,
152
+ };
153
+ const coreOptions = {
154
+ denied_keys: coreDeniedKeys(deniedKeys),
155
+ surface: "frontend",
156
+ };
157
+ const core = JSON.parse(spanToSdkSignalJson(JSON.stringify(corePayload), JSON.stringify(coreOptions)));
158
+ const resolvedTraceId = core.trace_id ?? traceId ?? "";
101
159
  const eventId = generateEventId();
102
- const requestSpanId = attrString(attributes, "cluebase.request_span_id") ??
103
- spanId ??
104
- context.request_span_id ??
105
- generateRequestSpanId();
106
- const requestId = attrString(attributes, "cluebase.request.id") ??
107
- attrString(attributes, "cluebase.request_id") ??
108
- context.request_id ??
109
- generateRequestId();
110
- const actionId = attrString(attributes, "cluebase.action.id") ??
111
- attrString(attributes, "cluebase.action_id") ??
112
- context.action_id ??
113
- null;
114
- const interactionId = attrString(attributes, "cluebase.interaction.id") ??
115
- attrString(attributes, "cluebase.interaction_id") ??
116
- context.interaction_id ??
117
- generateInteractionId();
118
160
  return {
119
161
  event_id: eventId,
120
162
  source_event_id: eventId,
@@ -127,7 +169,7 @@ export function spanToSourceSignalEvent(span, options) {
127
169
  event_category: "context",
128
170
  event_source: EVENT_SOURCE,
129
171
  event_version: EVENT_VERSION,
130
- occurred_at: nanosToIso(startTimeUnixNano),
172
+ occurred_at: nanosToIso(core.start_time_unix_nano),
131
173
  ingested_at: new Date().toISOString(),
132
174
  status: "observed",
133
175
  sdk_collection_mode: context.sdk_collection_mode,
@@ -135,14 +177,14 @@ export function spanToSourceSignalEvent(span, options) {
135
177
  privacy_level: "default_masked",
136
178
  masking_state: "masked",
137
179
  allowed_value_keys: [],
138
- privacy_summary: context.privacy_summary,
139
- trace_id: traceId,
140
- span_id: spanId,
141
- parent_span_id: parentId,
142
- request_id: requestId,
143
- request_span_id: requestSpanId,
144
- action_id: actionId,
145
- interaction_id: interactionId,
180
+ privacy_summary: withCoreMasking(context.privacy_summary, core.privacy_masking),
181
+ trace_id: resolvedTraceId,
182
+ span_id: core.span_id,
183
+ parent_span_id: core.parent_span_id,
184
+ request_id: core.request_id,
185
+ request_span_id: core.request_span_id,
186
+ action_id: core.action_id,
187
+ interaction_id: core.interaction_id,
146
188
  frontend_release: context.frontend_release,
147
189
  backend_release: null,
148
190
  properties: {},
@@ -151,27 +193,25 @@ export function spanToSourceSignalEvent(span, options) {
151
193
  surface: "frontend",
152
194
  kind: "span",
153
195
  name: span.name,
154
- span_kind: span.kind ?? null,
155
- trace_id: traceId,
156
- span_id: spanId,
157
- parent_span_id: parentId,
158
- start_time_unix_nano: startTimeUnixNano,
159
- end_time_unix_nano: endTimeUnixNano,
160
- status_code: span.status?.code ?? null,
161
- status_message: span.status?.message ?? null,
162
- instrumentation_scope_name: instrumentationScopeName(span),
163
- attributes,
164
- events: (span.events ?? []).map((event) => ({
165
- name: event.name,
166
- attributes: toJsonRecord(event.attributes, options.deniedKeys),
167
- })),
168
- resource: toFrontendSourceResource(span.resource, options.deniedKeys),
196
+ span_kind: core.span_kind,
197
+ trace_id: resolvedTraceId,
198
+ span_id: core.span_id,
199
+ parent_span_id: core.parent_span_id,
200
+ start_time_unix_nano: core.start_time_unix_nano,
201
+ end_time_unix_nano: core.end_time_unix_nano,
202
+ status_code: core.status_code ?? null,
203
+ status_message: core.status_message,
204
+ instrumentation_scope_name: core.instrumentation_scope_name,
205
+ attributes: core.attributes,
206
+ events: core.events,
207
+ resource: core.resource,
208
+ raw_resource: core.raw_resource,
169
209
  context: contextRecord(context, {
170
- trace_id: traceId,
171
- request_id: requestId,
172
- request_span_id: requestSpanId,
173
- action_id: actionId,
174
- interaction_id: interactionId,
210
+ trace_id: resolvedTraceId,
211
+ request_id: core.request_id,
212
+ request_span_id: core.request_span_id,
213
+ action_id: core.action_id,
214
+ interaction_id: core.interaction_id,
175
215
  }),
176
216
  },
177
217
  };
package/dist/index.d.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  import type { InternalMetricsSnapshot } from "./internal/metrics.js";
2
- import { createCluebaseBrowserSdk } from "./provider/sdk.js";
3
- import type { CluebaseBrowserSdk, CluebaseConsentState, CluebaseGroupTraits, CluebaseGroupType, CluebaseInitOptions, CluebaseSubjectTraits, CluebaseTrackMetrics, CluebaseTrackProperties } from "./provider/types.js";
2
+ import type { CluebaseConsentState, CluebaseGroupTraits, CluebaseGroupType, CluebaseInitOptions, CluebaseSubjectTraits, CluebaseTrackMetrics, CluebaseTrackProperties } from "./provider/types.js";
4
3
  declare const cluebase: {
5
4
  init(options: CluebaseInitOptions): void;
6
5
  identify(userId: string, traits?: CluebaseSubjectTraits): void;
7
6
  group(groupType: CluebaseGroupType, groupKey: string, traits?: CluebaseGroupTraits): void;
7
+ linkExternalAccount(namespace: string, externalId: string): void;
8
+ logout(): void;
8
9
  reset(): void;
9
10
  track(eventName: string, properties?: CluebaseTrackProperties, metrics?: CluebaseTrackMetrics): void;
10
11
  setConsent(consent: CluebaseConsentState): void;
@@ -12,10 +13,10 @@ declare const cluebase: {
12
13
  close(): Promise<void>;
13
14
  getInternalMetrics(): InternalMetricsSnapshot;
14
15
  getDiagnostics(): import("./exporter/diagnostics.js").DiagnosticsSnapshot;
16
+ withActionContext<T>(callback: () => T): T;
15
17
  };
16
18
  export { EXPORT_RESULT_FAILED, EXPORT_RESULT_SUCCESS, ObservationSourceEventExporter, type ObservationSourceEventExporterOptions, } from "./exporter/observation-source-event-exporter.js";
17
19
  export { attachNextRouterIntegration } from "./integrations/next-router.js";
18
20
  export { attachReactRouterIntegration } from "./integrations/react-router.js";
19
- export type { CluebaseBrowserSdk, CluebaseConsentState, CluebaseGroupTraits, CluebaseGroupType, CluebaseInitOptions, CluebaseSubjectTraits, CluebaseTrackMetrics, CluebaseTrackProperties, InternalMetricsSnapshot, };
20
- export { createCluebaseBrowserSdk };
21
+ export type { CluebaseConsentState, CluebaseGroupTraits, CluebaseGroupType, CluebaseInitOptions, CluebaseSubjectTraits, CluebaseTrackMetrics, CluebaseTrackProperties, InternalMetricsSnapshot, };
21
22
  export default cluebase;
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { parseSendableUrl } from "./provider/config.js";
2
2
  import { createCluebaseBrowserSdk } from "./provider/sdk.js";
3
+ import { reportSdkError } from "./provider/sdk-error.js";
3
4
  const GLOBAL_SDK_KEY = "__cluebase_frontend_sdk_singleton__";
4
5
  const globalWithCluebaseSdk = globalThis;
5
6
  const existingSdk = globalWithCluebaseSdk[GLOBAL_SDK_KEY];
@@ -7,7 +8,8 @@ const internalSdk = existingSdk ?? createCluebaseBrowserSdk();
7
8
  if (!existingSdk) {
8
9
  globalWithCluebaseSdk[GLOBAL_SDK_KEY] = internalSdk;
9
10
  }
10
- const reportPublicSdkError = (operation) => {
11
+ const reportPublicSdkError = (operation, error) => {
12
+ reportSdkError(operation, error);
11
13
  try {
12
14
  if (typeof console !== "undefined" && typeof console.warn === "function") {
13
15
  console.warn(`[cluebase] ${operation} failed`, {
@@ -15,23 +17,22 @@ const reportPublicSdkError = (operation) => {
15
17
  });
16
18
  }
17
19
  }
18
- catch {
19
- }
20
+ catch { }
20
21
  };
21
22
  const safeCall = (operation, callback) => {
22
23
  try {
23
24
  callback();
24
25
  }
25
- catch {
26
- reportPublicSdkError(operation);
26
+ catch (error) {
27
+ reportPublicSdkError(operation, error);
27
28
  }
28
29
  };
29
30
  const safeAsyncCall = async (operation, callback) => {
30
31
  try {
31
32
  await callback();
32
33
  }
33
- catch {
34
- reportPublicSdkError(operation);
34
+ catch (error) {
35
+ reportPublicSdkError(operation, error);
35
36
  }
36
37
  };
37
38
  const REMOVED_PRIVACY_OPTIONS = {
@@ -42,7 +43,9 @@ const REMOVED_PRIVACY_OPTIONS = {
42
43
  "and select those fields in the Cluebase privacy screen instead. Until then " +
43
44
  "every value stays masked.",
44
45
  };
45
- const REQUIRED_INIT_OPTIONS = ["projectKey"];
46
+ const REQUIRED_INIT_OPTIONS = [
47
+ "projectKey",
48
+ ];
46
49
  const validateInitOptions = (options) => {
47
50
  const privacy = options.privacy ?? {};
48
51
  for (const [name, message] of Object.entries(REMOVED_PRIVACY_OPTIONS)) {
@@ -62,8 +65,7 @@ const validateInitOptions = (options) => {
62
65
  "projectKey=NEXT_PUBLIC_CLUEBASE_PROJECT_KEY. The endpoint is optional " +
63
66
  "and defaults to the hosted Cluebase API.");
64
67
  }
65
- if (options.endpoint !== undefined &&
66
- typeof options.endpoint !== "string") {
68
+ if (options.endpoint !== undefined && typeof options.endpoint !== "string") {
67
69
  throw new Error("cluebase.init: 'endpoint' must be a string when provided. Omit it to " +
68
70
  "use the hosted Cluebase endpoint.");
69
71
  }
@@ -95,6 +97,14 @@ const cluebase = {
95
97
  internalSdk.setOrganization(groupKey, traits);
96
98
  });
97
99
  },
100
+ linkExternalAccount(namespace, externalId) {
101
+ safeCall("linkExternalAccount", () => {
102
+ internalSdk.linkExternalAccount(namespace, externalId);
103
+ });
104
+ },
105
+ logout() {
106
+ safeCall("logout", () => internalSdk.logout());
107
+ },
98
108
  reset() {
99
109
  safeCall("reset", () => internalSdk.reset());
100
110
  },
@@ -120,9 +130,11 @@ const cluebase = {
120
130
  getDiagnostics() {
121
131
  return internalSdk.getDiagnostics();
122
132
  },
133
+ withActionContext(callback) {
134
+ return internalSdk.withActionContext(callback);
135
+ },
123
136
  };
124
137
  export { EXPORT_RESULT_FAILED, EXPORT_RESULT_SUCCESS, ObservationSourceEventExporter, } from "./exporter/observation-source-event-exporter.js";
125
138
  export { attachNextRouterIntegration } from "./integrations/next-router.js";
126
139
  export { attachReactRouterIntegration } from "./integrations/react-router.js";
127
- export { createCluebaseBrowserSdk };
128
140
  export default cluebase;
@@ -1,6 +1,6 @@
1
1
  import { buildLocalTextDescriptor, buildLocalValueDescriptor, } from "../exporter/local-minimization.js";
2
- import { extractFieldName, extractFieldType, getElementAriaLabel, getElementClassList, getElementLabel, getElementPlaceholder, getElementRole, getElementSectionHeading, isEditableElement, resolveClickDisplayTarget, resolveInteractionScopeDescriptor, resolveStableKeyDescriptor, shouldCaptureClickTarget, } from "./dom.js";
3
2
  import { buildPrimaryActionKey } from "./common.js";
3
+ import { extractFieldName, extractFieldType, getElementAriaLabel, getElementClassList, getElementLabel, getElementPlaceholder, getElementRole, getElementSectionHeading, isEditableElement, resolveClickDisplayTarget, resolveInteractionScopeDescriptor, resolveStableKeyDescriptor, shouldCaptureClickTarget, } from "./dom.js";
4
4
  import { getCurrentPageSnapshot } from "./location.js";
5
5
  import { buildScreenActionNamingEvidence } from "./naming-evidence.js";
6
6
  const ariaToggleRoles = new Set([
@@ -37,14 +37,12 @@ const ensureCssEscape = () => {
37
37
  };
38
38
  const emitElementClicked = (context, target, occurredAtMs, event) => {
39
39
  const displayTarget = resolveClickDisplayTarget(target);
40
- if (context.shouldIgnoreElement?.(displayTarget)) {
41
- return;
42
- }
43
40
  if (!shouldCaptureClickTarget(displayTarget)) {
44
41
  return;
45
42
  }
46
43
  const stableKey = resolveStableKeyDescriptor(displayTarget);
47
44
  const interactionScope = resolveInteractionScopeDescriptor(displayTarget);
45
+ const pageSnapshot = getCurrentPageSnapshot();
48
46
  const interactionId = context.startInteraction?.({
49
47
  nowMs: occurredAtMs,
50
48
  interactionInitiator: "click",
@@ -59,6 +57,7 @@ const emitElementClicked = (context, target, occurredAtMs, event) => {
59
57
  occurredAtMs,
60
58
  payload: {
61
59
  viewId: context.getViewId(),
60
+ screenKey: pageSnapshot.screenKey,
62
61
  ...(interactionId
63
62
  ? {
64
63
  interactionId,
@@ -68,7 +67,7 @@ const emitElementClicked = (context, target, occurredAtMs, event) => {
68
67
  : {}),
69
68
  elementTag: displayTarget.tagName.toLowerCase(),
70
69
  elementRole: getElementRole(displayTarget),
71
- primaryActionKey: buildPrimaryActionKey(getCurrentPageSnapshot().screenKey, stableKey.stableKey),
70
+ primaryActionKey: buildPrimaryActionKey(pageSnapshot.screenKey, stableKey.stableKey),
72
71
  stableKey: stableKey.stableKey,
73
72
  stableKeyQuality: stableKey.stableKeyQuality,
74
73
  formKey: stableKey.formKey,
@@ -93,9 +92,10 @@ const emitElementClicked = (context, target, occurredAtMs, event) => {
93
92
  },
94
93
  });
95
94
  context.bindEventContext?.(event);
96
- maybeEmitAriaToggleChanged(context, displayTarget);
95
+ context.requestFlush?.();
96
+ maybeEmitAriaToggleChanged(context, displayTarget, pageSnapshot.screenKey);
97
97
  };
98
- const maybeEmitAriaToggleChanged = (context, element) => {
98
+ const maybeEmitAriaToggleChanged = (context, element, screenKey) => {
99
99
  if (isNativeToggleInput(element)) {
100
100
  return;
101
101
  }
@@ -105,7 +105,7 @@ const maybeEmitAriaToggleChanged = (context, element) => {
105
105
  }
106
106
  window.setTimeout(() => {
107
107
  try {
108
- if (!element.isConnected || context.shouldIgnoreElement?.(element)) {
108
+ if (!element.isConnected) {
109
109
  return;
110
110
  }
111
111
  const stableKey = resolveStableKeyDescriptor(element);
@@ -116,6 +116,7 @@ const maybeEmitAriaToggleChanged = (context, element) => {
116
116
  occurredAtMs: Date.now(),
117
117
  payload: {
118
118
  viewId: context.getViewId(),
119
+ screenKey,
119
120
  elementTag: element.tagName.toLowerCase(),
120
121
  elementRole: role,
121
122
  stableKey: stableKey.stableKey,
@@ -158,11 +158,11 @@ const getAssociatedLabel = (element) => {
158
158
  if (label) {
159
159
  return label.replace(/\s+/g, " ").slice(0, 120);
160
160
  }
161
- }
162
- const closestLabel = element.closest("label");
163
- const closestLabelText = closestLabel?.textContent?.trim();
164
- if (closestLabelText) {
165
- return closestLabelText.replace(/\s+/g, " ").slice(0, 120);
161
+ const closestLabel = element.closest("label");
162
+ const closestLabelText = closestLabel?.textContent?.trim();
163
+ if (closestLabelText) {
164
+ return closestLabelText.replace(/\s+/g, " ").slice(0, 120);
165
+ }
166
166
  }
167
167
  return null;
168
168
  };
@@ -173,6 +173,10 @@ const readElementContentLabel = (element) => {
173
173
  (element instanceof HTMLElement && element.isContentEditable)) {
174
174
  return null;
175
175
  }
176
+ const role = getElementRole(element);
177
+ if (!role || !CLICK_ACTION_ROLES.has(role)) {
178
+ return null;
179
+ }
176
180
  const ownText = Array.from(element.childNodes)
177
181
  .filter((node) => node.nodeType === Node.TEXT_NODE)
178
182
  .map((node) => node.textContent?.trim() ?? "")
@@ -182,26 +186,11 @@ const readElementContentLabel = (element) => {
182
186
  if (ownText) {
183
187
  return ownText.slice(0, 120);
184
188
  }
185
- const isInteractiveDisplayTarget = element instanceof HTMLButtonElement ||
186
- element instanceof HTMLAnchorElement ||
187
- element.getAttribute("role") === "button" ||
188
- element.getAttribute("role") === "link";
189
189
  const descendantCandidates = Array.from(element.querySelectorAll("label, p, span, strong, em, small, button, a, [role='button'], [role='link']"))
190
190
  .map((candidate) => candidate.textContent?.trim() ?? "")
191
191
  .map((value) => value.replace(/\s+/g, " ").trim())
192
192
  .filter((value) => value.length > 0 && value.length <= 80);
193
- if (isInteractiveDisplayTarget && descendantCandidates.length > 0) {
194
- return descendantCandidates[0] ?? null;
195
- }
196
193
  if (descendantCandidates.length > 0) {
197
- descendantCandidates.sort((left, right) => {
198
- const leftWordCount = left.split(/\s+/).length;
199
- const rightWordCount = right.split(/\s+/).length;
200
- if (leftWordCount !== rightWordCount) {
201
- return leftWordCount - rightWordCount;
202
- }
203
- return left.length - right.length;
204
- });
205
194
  return descendantCandidates[0] ?? null;
206
195
  }
207
196
  return getTextPreview(element);
@@ -246,7 +235,10 @@ export const getAccessibleName = (element) => {
246
235
  }
247
236
  const title = getElementTitle(element);
248
237
  if (title?.trim()) {
249
- return { name: title.trim().replace(/\s+/g, " ").slice(0, 120), source: "title" };
238
+ return {
239
+ name: title.trim().replace(/\s+/g, " ").slice(0, 120),
240
+ source: "title",
241
+ };
250
242
  }
251
243
  return null;
252
244
  };
@@ -31,9 +31,6 @@ export const startDragCapture = (context) => {
31
31
  if (!(target instanceof Element)) {
32
32
  return;
33
33
  }
34
- if (context.shouldIgnoreElement?.(target)) {
35
- return;
36
- }
37
34
  try {
38
35
  const stableKey = resolveStableKeyDescriptor(target);
39
36
  const interactionScope = resolveInteractionScopeDescriptor(target);
@@ -54,7 +54,9 @@ const readStringAttribute = (logRecord, key) => {
54
54
  };
55
55
  const readNumberAttribute = (logRecord, key) => {
56
56
  const value = logRecord.attributes?.[key];
57
- return typeof value === "number" && Number.isFinite(value) ? value : undefined;
57
+ return typeof value === "number" && Number.isFinite(value)
58
+ ? value
59
+ : undefined;
58
60
  };
59
61
  export const startErrorCapture = (context) => {
60
62
  if (typeof window === "undefined") {