@genn-inc/cluebase-frontend-sdk 0.0.3 → 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 (73) 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/browser-storage.js +1 -2
  8. package/dist/context/environment.js +5 -3
  9. package/dist/context/identity.js +5 -9
  10. package/dist/context/organization.d.ts +1 -1
  11. package/dist/context/organization.js +6 -8
  12. package/dist/context/session.js +6 -8
  13. package/dist/context/tab.js +8 -12
  14. package/dist/context/trace.js +2 -1
  15. package/dist/exporter/batch.d.ts +3 -1
  16. package/dist/exporter/batch.js +6 -10
  17. package/dist/exporter/browser-outbox-records.d.ts +1 -1
  18. package/dist/exporter/browser-outbox-records.js +15 -6
  19. package/dist/exporter/browser-outbox.d.ts +5 -2
  20. package/dist/exporter/browser-outbox.js +48 -11
  21. package/dist/exporter/client.d.ts +2 -2
  22. package/dist/exporter/client.js +15 -30
  23. package/dist/exporter/delivery-response-contract.js +67 -17
  24. package/dist/exporter/delivery-response.d.ts +3 -1
  25. package/dist/exporter/delivery-response.js +31 -20
  26. package/dist/exporter/diagnostics.d.ts +1 -1
  27. package/dist/exporter/diagnostics.js +4 -1
  28. package/dist/exporter/event-id.js +1 -3
  29. package/dist/exporter/frontend-resource.js +5 -4
  30. package/dist/exporter/observation-source-event-exporter.d.ts +1 -0
  31. package/dist/exporter/observation-source-event-exporter.js +5 -2
  32. package/dist/exporter/path-template.js +3 -1
  33. package/dist/exporter/raw-event-to-source-signal.js +4 -2
  34. package/dist/exporter/sanitize.d.ts +3 -2
  35. package/dist/exporter/sanitize.js +4 -3
  36. package/dist/exporter/sdk-core-wasm.d.ts +5 -0
  37. package/dist/exporter/sdk-core-wasm.js +18 -0
  38. package/dist/exporter/source-event.d.ts +1 -0
  39. package/dist/exporter/span-to-source-signal.d.ts +1 -1
  40. package/dist/exporter/span-to-source-signal.js +122 -82
  41. package/dist/index.d.ts +5 -4
  42. package/dist/index.js +16 -6
  43. package/dist/instrumentation/action.js +9 -8
  44. package/dist/instrumentation/dom.js +13 -21
  45. package/dist/instrumentation/drag.js +0 -3
  46. package/dist/instrumentation/error.js +3 -1
  47. package/dist/instrumentation/form.js +5 -6
  48. package/dist/instrumentation/input.js +4 -8
  49. package/dist/instrumentation/location.js +3 -1
  50. package/dist/instrumentation/network-request.js +1 -1
  51. package/dist/instrumentation/network-span-attributes.js +2 -2
  52. package/dist/instrumentation/network.js +7 -3
  53. package/dist/instrumentation/performance.js +1 -2
  54. package/dist/internal/internal-log.js +2 -1
  55. package/dist/provider/_version.d.ts +1 -1
  56. package/dist/provider/_version.js +1 -1
  57. package/dist/provider/config.js +0 -1
  58. package/dist/provider/constants.d.ts +1 -1
  59. package/dist/provider/constants.js +0 -1
  60. package/dist/provider/sdk-event-pipeline.d.ts +2 -1
  61. package/dist/provider/sdk-event-pipeline.js +6 -3
  62. package/dist/provider/sdk-input-normalization.js +2 -2
  63. package/dist/provider/sdk-otel-pipeline.js +5 -4
  64. package/dist/provider/sdk-runtime-surfaces.d.ts +15 -12
  65. package/dist/provider/sdk-runtime-surfaces.js +8 -7
  66. package/dist/provider/sdk.js +67 -12
  67. package/dist/provider/types.d.ts +6 -5
  68. package/dist/wasm/pkg-web/cluebase_sdk_core_wasm.d.ts +41 -0
  69. package/dist/wasm/pkg-web/cluebase_sdk_core_wasm.js +267 -0
  70. package/dist/wasm/pkg-web/cluebase_sdk_core_wasm_bg.wasm +0 -0
  71. package/dist/wasm/pkg-web/cluebase_sdk_core_wasm_bg.wasm.d.ts +12 -0
  72. package/dist/wasm/pkg-web/package.json +15 -0
  73. package/package.json +62 -53
@@ -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
@@ -17,8 +17,7 @@ const reportPublicSdkError = (operation, error) => {
17
17
  });
18
18
  }
19
19
  }
20
- catch {
21
- }
20
+ catch { }
22
21
  };
23
22
  const safeCall = (operation, callback) => {
24
23
  try {
@@ -44,7 +43,9 @@ const REMOVED_PRIVACY_OPTIONS = {
44
43
  "and select those fields in the Cluebase privacy screen instead. Until then " +
45
44
  "every value stays masked.",
46
45
  };
47
- const REQUIRED_INIT_OPTIONS = ["projectKey"];
46
+ const REQUIRED_INIT_OPTIONS = [
47
+ "projectKey",
48
+ ];
48
49
  const validateInitOptions = (options) => {
49
50
  const privacy = options.privacy ?? {};
50
51
  for (const [name, message] of Object.entries(REMOVED_PRIVACY_OPTIONS)) {
@@ -64,8 +65,7 @@ const validateInitOptions = (options) => {
64
65
  "projectKey=NEXT_PUBLIC_CLUEBASE_PROJECT_KEY. The endpoint is optional " +
65
66
  "and defaults to the hosted Cluebase API.");
66
67
  }
67
- if (options.endpoint !== undefined &&
68
- typeof options.endpoint !== "string") {
68
+ if (options.endpoint !== undefined && typeof options.endpoint !== "string") {
69
69
  throw new Error("cluebase.init: 'endpoint' must be a string when provided. Omit it to " +
70
70
  "use the hosted Cluebase endpoint.");
71
71
  }
@@ -97,6 +97,14 @@ const cluebase = {
97
97
  internalSdk.setOrganization(groupKey, traits);
98
98
  });
99
99
  },
100
+ linkExternalAccount(namespace, externalId) {
101
+ safeCall("linkExternalAccount", () => {
102
+ internalSdk.linkExternalAccount(namespace, externalId);
103
+ });
104
+ },
105
+ logout() {
106
+ safeCall("logout", () => internalSdk.logout());
107
+ },
100
108
  reset() {
101
109
  safeCall("reset", () => internalSdk.reset());
102
110
  },
@@ -122,9 +130,11 @@ const cluebase = {
122
130
  getDiagnostics() {
123
131
  return internalSdk.getDiagnostics();
124
132
  },
133
+ withActionContext(callback) {
134
+ return internalSdk.withActionContext(callback);
135
+ },
125
136
  };
126
137
  export { EXPORT_RESULT_FAILED, EXPORT_RESULT_SUCCESS, ObservationSourceEventExporter, } from "./exporter/observation-source-event-exporter.js";
127
138
  export { attachNextRouterIntegration } from "./integrations/next-router.js";
128
139
  export { attachReactRouterIntegration } from "./integrations/react-router.js";
129
- export { createCluebaseBrowserSdk };
130
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") {
@@ -1,9 +1,9 @@
1
1
  import { buildLocalTextDescriptor } from "../exporter/local-minimization.js";
2
- import { extractFieldName, extractFieldType, getElementAriaLabel, getElementClassList, getElementLabel, getElementPlaceholder, getElementRole, getElementSectionHeading, isEditableElement, resolveClickDisplayTarget, resolveInteractionScopeDescriptor, resolveStableKeyDescriptor, } from "./dom.js";
2
+ import { fingerprintText } from "../exporter/text-fingerprint.js";
3
3
  import { buildPrimaryActionKey } from "./common.js";
4
+ import { extractFieldName, extractFieldType, getElementAriaLabel, getElementClassList, getElementLabel, getElementPlaceholder, getElementRole, getElementSectionHeading, isEditableElement, resolveClickDisplayTarget, resolveInteractionScopeDescriptor, resolveStableKeyDescriptor, } from "./dom.js";
4
5
  import { getCurrentPageSnapshot } from "./location.js";
5
6
  import { buildScreenActionNamingEvidence } from "./naming-evidence.js";
6
- import { fingerprintText } from "../exporter/text-fingerprint.js";
7
7
  const VALIDATION_REASON_KEYS = [
8
8
  "valueMissing",
9
9
  "typeMismatch",
@@ -57,9 +57,6 @@ export const startFormCapture = (context) => {
57
57
  if (!(target instanceof HTMLFormElement)) {
58
58
  return;
59
59
  }
60
- if (context.shouldIgnoreElement?.(target)) {
61
- return;
62
- }
63
60
  try {
64
61
  const formKey = target.id || target.getAttribute("name") || null;
65
62
  const interactionScope = resolveInteractionScopeDescriptor(target);
@@ -75,14 +72,16 @@ export const startFormCapture = (context) => {
75
72
  ? "validation_blocked"
76
73
  : "unknown";
77
74
  const resultSource = validationEvidence.invalidFieldCount > 0 ? "browser_validation" : null;
75
+ const pageSnapshot = getCurrentPageSnapshot();
78
76
  context.onEvent({
79
77
  type: "form_submitted",
80
78
  occurredAtMs: Date.now(),
81
79
  payload: {
82
80
  viewId: context.getViewId(),
81
+ screenKey: pageSnapshot.screenKey,
83
82
  formId: target.id || null,
84
83
  formKey,
85
- primaryActionKey: buildPrimaryActionKey(getCurrentPageSnapshot().screenKey, formKey ?? submitterStableKey?.stableKey ?? null),
84
+ primaryActionKey: buildPrimaryActionKey(pageSnapshot.screenKey, formKey ?? submitterStableKey?.stableKey ?? null),
86
85
  formName: target.getAttribute("name"),
87
86
  interactionScopeKind: interactionScope?.interactionScopeKind ?? null,
88
87
  interactionScopeKey: interactionScope?.interactionScopeKey ?? null,
@@ -1,6 +1,6 @@
1
1
  import { buildLocalTextDescriptor, buildLocalValueDescriptor, } from "../exporter/local-minimization.js";
2
- import { extractFieldName, extractFieldType, getElementAriaLabel, getElementLabel, getElementPlaceholder, getElementRole, getElementSectionHeading, getElementValue, resolveInteractionScopeDescriptor, resolveStableKeyDescriptor, } from "./dom.js";
3
2
  import { buildPrimaryActionKey } from "./common.js";
3
+ import { extractFieldName, extractFieldType, getElementAriaLabel, getElementLabel, getElementPlaceholder, getElementRole, getElementSectionHeading, getElementValue, resolveInteractionScopeDescriptor, resolveStableKeyDescriptor, } from "./dom.js";
4
4
  import { getCurrentPageSnapshot } from "./location.js";
5
5
  import { buildScreenActionNamingEvidence } from "./naming-evidence.js";
6
6
  const buildFileDescriptors = (input) => {
@@ -28,23 +28,22 @@ export const startInputCapture = (context) => {
28
28
  if (context.getConsent() === "denied") {
29
29
  return;
30
30
  }
31
- if (context.shouldIgnoreElement?.(target)) {
32
- return;
33
- }
34
31
  if (target instanceof HTMLInputElement && target.type === "password") {
35
32
  return;
36
33
  }
37
34
  try {
38
35
  const stableKey = resolveStableKeyDescriptor(target);
39
36
  const interactionScope = resolveInteractionScopeDescriptor(target);
37
+ const pageSnapshot = getCurrentPageSnapshot();
40
38
  const commonPayload = {
41
39
  viewId: context.getViewId(),
40
+ screenKey: pageSnapshot.screenKey,
42
41
  formId: stableKey.formKey,
43
42
  formKey: stableKey.formKey,
44
43
  fieldType: extractFieldType(target),
45
44
  elementTag: target.tagName.toLowerCase(),
46
45
  elementRole: getElementRole(target),
47
- primaryActionKey: buildPrimaryActionKey(getCurrentPageSnapshot().screenKey, stableKey.stableKey),
46
+ primaryActionKey: buildPrimaryActionKey(pageSnapshot.screenKey, stableKey.stableKey),
48
47
  stableKey: stableKey.stableKey,
49
48
  stableKeyQuality: stableKey.stableKeyQuality,
50
49
  interactionScopeKind: interactionScope?.interactionScopeKind ?? null,
@@ -138,9 +137,6 @@ export const startInputCapture = (context) => {
138
137
  if (context.getConsent() === "denied") {
139
138
  return;
140
139
  }
141
- if (context.shouldIgnoreElement?.(target)) {
142
- return;
143
- }
144
140
  dirtyControls.add(target);
145
141
  };
146
142
  const changeHandler = (event) => {
@@ -53,7 +53,9 @@ export const getPageSnapshotFromUrl = (href) => {
53
53
  }
54
54
  let path = null;
55
55
  try {
56
- path = href ? new URL(href, globalThis.location?.origin).pathname || "/" : null;
56
+ path = href
57
+ ? new URL(href, globalThis.location?.origin).pathname || "/"
58
+ : null;
57
59
  }
58
60
  catch {
59
61
  path = null;
@@ -1,9 +1,9 @@
1
1
  import { buildLocalQuerySummary, buildLocalStructuredSummary, } from "../exporter/local-minimization.js";
2
2
  import { buildParameterSnapshotBundle, } from "../exporter/parameter-snapshot.js";
3
- import { allowedValuePathsFor } from "../provider/privacy-policy-store.js";
4
3
  import { toPathTemplate } from "../exporter/path-template.js";
5
4
  import { maybeParseJson, sanitizeHeaders } from "../exporter/sanitize.js";
6
5
  import { buildSchemaEvidence, deriveOutcomeClass, extractErrorCodeCandidate, extractGraphqlEvidence, } from "../exporter/schema-evidence.js";
6
+ import { allowedValuePathsFor } from "../provider/privacy-policy-store.js";
7
7
  import { currentPageUrl, resolveUrlAgainstCurrentLocation, stripQueryFromUrl, } from "./common.js";
8
8
  const MAX_CAPTURED_RESPONSE_BODY_BYTES = 16 * 1024;
9
9
  export const headersToRecord = (headers) => {