@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.
- package/README.md +21 -0
- package/dist/_generated/{identity-pii-policy.d.ts → identity-pii-policy-text-contains.d.ts} +1 -1
- package/dist/_generated/{identity-pii-policy.js → identity-pii-policy-text-contains.js} +1 -1
- package/dist/_generated/identity-pii-policy-whole-value.d.ts +37 -0
- package/dist/_generated/identity-pii-policy-whole-value.js +106 -0
- package/dist/_generated/otel-span-conversion.js +1 -9
- package/dist/context/action-context.js +27 -7
- package/dist/context/browser-storage.js +1 -2
- package/dist/context/environment.js +5 -3
- package/dist/context/identity.js +5 -9
- package/dist/context/organization.d.ts +1 -1
- package/dist/context/organization.js +6 -8
- package/dist/context/session.js +6 -8
- package/dist/context/tab.js +8 -12
- package/dist/context/trace.js +2 -1
- package/dist/exporter/batch.d.ts +3 -1
- package/dist/exporter/batch.js +6 -10
- package/dist/exporter/browser-outbox-records.d.ts +1 -1
- package/dist/exporter/browser-outbox-records.js +15 -6
- package/dist/exporter/browser-outbox.d.ts +5 -2
- package/dist/exporter/browser-outbox.js +48 -11
- package/dist/exporter/client.d.ts +2 -2
- package/dist/exporter/client.js +15 -30
- package/dist/exporter/delivery-response-contract.js +67 -17
- package/dist/exporter/delivery-response.d.ts +3 -1
- package/dist/exporter/delivery-response.js +31 -20
- package/dist/exporter/diagnostics.d.ts +1 -1
- package/dist/exporter/diagnostics.js +4 -1
- package/dist/exporter/event-id.js +1 -3
- package/dist/exporter/frontend-resource.js +5 -4
- package/dist/exporter/observation-source-event-exporter.d.ts +1 -0
- package/dist/exporter/observation-source-event-exporter.js +5 -2
- package/dist/exporter/path-template.js +3 -1
- package/dist/exporter/raw-event-to-source-signal.js +4 -2
- package/dist/exporter/sanitize.d.ts +3 -2
- package/dist/exporter/sanitize.js +4 -3
- package/dist/exporter/sdk-core-wasm.d.ts +5 -0
- package/dist/exporter/sdk-core-wasm.js +18 -0
- package/dist/exporter/source-event.d.ts +1 -0
- package/dist/exporter/span-to-source-signal.d.ts +1 -1
- package/dist/exporter/span-to-source-signal.js +122 -82
- package/dist/index.d.ts +5 -4
- package/dist/index.js +23 -11
- package/dist/instrumentation/action.js +9 -8
- package/dist/instrumentation/dom.js +13 -21
- package/dist/instrumentation/drag.js +0 -3
- package/dist/instrumentation/error.js +3 -1
- package/dist/instrumentation/form.js +5 -6
- package/dist/instrumentation/input.js +4 -8
- package/dist/instrumentation/location.js +3 -1
- package/dist/instrumentation/network-request.js +1 -1
- package/dist/instrumentation/network-span-attributes.js +2 -2
- package/dist/instrumentation/network.js +7 -3
- package/dist/instrumentation/performance.js +1 -2
- package/dist/internal/internal-log.js +2 -1
- package/dist/provider/_version.d.ts +1 -1
- package/dist/provider/_version.js +1 -1
- package/dist/provider/config.js +0 -1
- package/dist/provider/constants.d.ts +1 -1
- package/dist/provider/constants.js +0 -1
- package/dist/provider/sdk-error.d.ts +7 -0
- package/dist/provider/sdk-error.js +48 -0
- package/dist/provider/sdk-event-pipeline.d.ts +2 -1
- package/dist/provider/sdk-event-pipeline.js +6 -3
- package/dist/provider/sdk-input-normalization.js +2 -2
- package/dist/provider/sdk-otel-pipeline.js +5 -4
- package/dist/provider/sdk-runtime-surfaces.d.ts +15 -12
- package/dist/provider/sdk-runtime-surfaces.js +8 -7
- package/dist/provider/sdk.js +81 -12
- package/dist/provider/types.d.ts +6 -5
- package/dist/wasm/pkg-web/cluebase_sdk_core_wasm.d.ts +41 -0
- package/dist/wasm/pkg-web/cluebase_sdk_core_wasm.js +267 -0
- package/dist/wasm/pkg-web/cluebase_sdk_core_wasm_bg.wasm +0 -0
- package/dist/wasm/pkg-web/cluebase_sdk_core_wasm_bg.wasm.d.ts +12 -0
- package/dist/wasm/pkg-web/package.json +15 -0
- 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
|
|
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")] =
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
3
|
-
import { EVENT_SOURCE, EVENT_VERSION, SOURCE_SCHEMA_VERSION, } from "../provider/constants.js";
|
|
4
|
-
import { generateEventId
|
|
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 {
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
31
|
-
const
|
|
32
|
-
for (const [key,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
100
|
-
|
|
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(
|
|
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:
|
|
140
|
-
span_id:
|
|
141
|
-
parent_span_id:
|
|
142
|
-
request_id:
|
|
143
|
-
request_span_id:
|
|
144
|
-
action_id:
|
|
145
|
-
interaction_id:
|
|
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:
|
|
155
|
-
trace_id:
|
|
156
|
-
span_id:
|
|
157
|
-
parent_span_id:
|
|
158
|
-
start_time_unix_nano:
|
|
159
|
-
end_time_unix_nano:
|
|
160
|
-
status_code:
|
|
161
|
-
status_message:
|
|
162
|
-
instrumentation_scope_name:
|
|
163
|
-
attributes,
|
|
164
|
-
events:
|
|
165
|
-
|
|
166
|
-
|
|
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:
|
|
171
|
-
request_id:
|
|
172
|
-
request_span_id:
|
|
173
|
-
action_id:
|
|
174
|
-
interaction_id:
|
|
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 {
|
|
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 {
|
|
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 = [
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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 {
|
|
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)
|
|
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") {
|