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