@genn-inc/cluebase-frontend-sdk 0.0.1 → 0.0.3
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 +318 -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 +16 -44
- 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-error.d.ts +7 -0
- package/dist/provider/sdk-error.js +48 -0
- 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 +18 -24
- package/dist/provider/types.d.ts +3 -39
- package/dist/provider/usage-governor.js +0 -2
- package/package.json +53 -58
|
@@ -1,11 +1,4 @@
|
|
|
1
1
|
import { type Span } from "@opentelemetry/api";
|
|
2
|
-
/**
|
|
3
|
-
* 属性を書き込む側が、span に実際に求めるもの。
|
|
4
|
-
*
|
|
5
|
-
* **`Span` 全体(11 member)を求めていました。** ここで使うのは 2 つだけなので、
|
|
6
|
-
* 試験の作り物は残りを持てず `as unknown as Span` で嘘をつくしかない状態でした
|
|
7
|
-
* (`tests/README.md` §10)。3 つ目を使い始めたら、ここが変わって作り物が落ちます。
|
|
8
|
-
*/
|
|
9
2
|
export type AttributeWritableSpan = {
|
|
10
3
|
[K in "setAttribute" | "setStatus"]: (...args: Parameters<Span[K]>) => unknown;
|
|
11
4
|
};
|
|
@@ -56,6 +56,7 @@ const setRequestSpanAttributes = (span, meta, eventName, status, observationCont
|
|
|
56
56
|
span.setAttribute("cluebase.path_template", meta.pathTemplate);
|
|
57
57
|
setOptionalString(span, "cluebase.target_service", meta.targetService);
|
|
58
58
|
setOptionalString(span, "cluebase.parent_span_id", meta.parentSpanId);
|
|
59
|
+
setOptionalString(span, "cluebase.action.id", meta.actionId);
|
|
59
60
|
setOptionalString(span, "cluebase.interaction.id", meta.interactionId);
|
|
60
61
|
setOptionalString(span, "cluebase.interaction.initiator", meta.interactionInitiator);
|
|
61
62
|
setOptionalString(span, "cluebase.interaction.start_event_kind", meta.interactionStartEventKind);
|
|
@@ -79,8 +80,6 @@ const setRequestSpanAttributes = (span, meta, eventName, status, observationCont
|
|
|
79
80
|
setOptionalStringArray(span, cluebaseAuthoredPropertyAttribute("graphql_top_level_fields"), requestEvidence.graphqlTopLevelFields);
|
|
80
81
|
setOptionalNumber(span, cluebaseAuthoredMetricAttribute("request_size_bytes"), meta.requestSizeBytes);
|
|
81
82
|
span.setAttribute(cluebaseAuthoredPropertyAttribute("request_field_paths"), meta.queryKeys);
|
|
82
|
-
// 顧客の値が入る面。**値は入れ物の中で伏せられている。**顧客が画面で名指しした
|
|
83
|
-
// 項目だけが値を持ち、その項目名は下の控えに並ぶ。
|
|
84
83
|
setJsonRecordAttribute(span, "cluebase.request_parameters_snapshot_json", meta.requestParametersSnapshot.snapshot);
|
|
85
84
|
setJsonRecordAttribute(span, "cluebase.request_metadata_json", {
|
|
86
85
|
...meta.requestMetadataSummary,
|
|
@@ -142,7 +141,6 @@ export const setResponseSpanAttributes = (span, meta, input) => {
|
|
|
142
141
|
deniedKeys: input.deniedKeys ?? [],
|
|
143
142
|
});
|
|
144
143
|
setJsonRecordAttribute(span, "cluebase.response_parameters_snapshot_json", responseParametersSnapshot.snapshot);
|
|
145
|
-
// 平文で残した項目の控え。面をまたいで 1 本にする。空なら属性を出さない。
|
|
146
144
|
setOptionalStringArray(span, "cluebase.privacy.allowed_keys", [
|
|
147
145
|
...new Set([
|
|
148
146
|
...meta.requestParametersSnapshot.allowedValueKeys,
|
|
@@ -18,7 +18,6 @@ let fetchInstrumentation = null;
|
|
|
18
18
|
let xhrInstrumentation = null;
|
|
19
19
|
let cluebaseNetworkPropagatorRegistered = false;
|
|
20
20
|
const REQUEST_LIFECYCLE_ORPHAN_TIMEOUT_MS = 30000;
|
|
21
|
-
// OTel fetch waits 300ms for browser resource timing before ending spans.
|
|
22
21
|
const FETCH_SPAN_END_SETTLE_MS = 350;
|
|
23
22
|
const pendingFetchSpanEndWaits = new Set();
|
|
24
23
|
class ManagedFetchInstrumentation extends FetchInstrumentation {
|
|
@@ -70,6 +69,9 @@ const toTraceHeaders = (capture, traceContext) => {
|
|
|
70
69
|
if (traceContext.parentSpanId) {
|
|
71
70
|
headers["x-cluebase-parent-span-id"] = traceContext.parentSpanId;
|
|
72
71
|
}
|
|
72
|
+
if (traceContext.actionId) {
|
|
73
|
+
headers["x-cluebase-action-id"] = traceContext.actionId;
|
|
74
|
+
}
|
|
73
75
|
if (traceContext.interactionId) {
|
|
74
76
|
headers["x-cluebase-interaction-id"] = traceContext.interactionId;
|
|
75
77
|
}
|
|
@@ -99,6 +101,7 @@ const traceContextFromMeta = (meta) => ({
|
|
|
99
101
|
parentSpanId: meta.parentSpanId,
|
|
100
102
|
requestId: meta.requestId,
|
|
101
103
|
requestSpanId: meta.requestSpanId,
|
|
104
|
+
actionId: meta.actionId,
|
|
102
105
|
interactionId: meta.interactionId,
|
|
103
106
|
interactionInitiator: meta.interactionInitiator,
|
|
104
107
|
interactionStartEventKind: meta.interactionStartEventKind,
|
|
@@ -108,6 +111,7 @@ const CLUEBASE_NETWORK_PROPAGATION_FIELDS = [
|
|
|
108
111
|
"x-cluebase-trace-id",
|
|
109
112
|
"x-cluebase-request-id",
|
|
110
113
|
"x-cluebase-request-span-id",
|
|
114
|
+
"x-cluebase-action-id",
|
|
111
115
|
"x-cluebase-parent-span-id",
|
|
112
116
|
"x-cluebase-interaction-id",
|
|
113
117
|
"x-cluebase-origin-frontend-interaction-id",
|
|
@@ -471,6 +475,7 @@ export const startNetworkCapture = (context, options = {}) => {
|
|
|
471
475
|
parentSpanId: null,
|
|
472
476
|
requestId: generateRequestId(),
|
|
473
477
|
requestSpanId: generateRequestSpanId(),
|
|
478
|
+
actionId: null,
|
|
474
479
|
interactionId: generateInteractionId(),
|
|
475
480
|
interactionInitiator: null,
|
|
476
481
|
interactionStartEventKind: null,
|
|
@@ -4,8 +4,6 @@ export const startPageCapture = (context) => {
|
|
|
4
4
|
return { stop: () => undefined };
|
|
5
5
|
}
|
|
6
6
|
let pageEnteredAtMs = Date.now();
|
|
7
|
-
// Page-view events are emitted from OTel navigation/document-load signals.
|
|
8
|
-
// This module only keeps Cluebase-specific time-on-page evidence.
|
|
9
7
|
const handlePageLeave = () => {
|
|
10
8
|
if (context.getConsent() === "denied") {
|
|
11
9
|
return;
|
|
@@ -92,7 +92,6 @@ const observeEntryType = (observers, entryType, callback) => {
|
|
|
92
92
|
observers.push(observer);
|
|
93
93
|
}
|
|
94
94
|
catch {
|
|
95
|
-
// Some browsers expose an entry type but still reject buffered observation.
|
|
96
95
|
}
|
|
97
96
|
};
|
|
98
97
|
const readPerformanceNavigation = () => {
|
|
@@ -233,9 +232,6 @@ export const startPerformanceCapture = (context) => {
|
|
|
233
232
|
occurredAtMs: Date.now(),
|
|
234
233
|
payload: {
|
|
235
234
|
viewId,
|
|
236
|
-
// must strip query string to avoid leaking OAuth
|
|
237
|
-
// callback / password-reset / magic-link tokens that may
|
|
238
|
-
// live in `window.location.href`. See `instrumentation/common.ts`.
|
|
239
235
|
pageUrl: currentPageUrl() ?? "",
|
|
240
236
|
collectorName: "performance_rum",
|
|
241
237
|
aggregationKind: "per_view_window",
|
|
@@ -305,9 +301,6 @@ export const startPerformanceCapture = (context) => {
|
|
|
305
301
|
window.addEventListener("pointerdown", interactionHandler, true);
|
|
306
302
|
window.addEventListener("pagehide", pageHideHandler, true);
|
|
307
303
|
document.addEventListener("visibilitychange", visibilityHandler, true);
|
|
308
|
-
// Long tasks and resource load failures are retained as Cluebase summary-only
|
|
309
|
-
// UX evidence because the accepted browser OTel modules above cover
|
|
310
|
-
// document load and Web Vitals, not this per-view aggregate.
|
|
311
304
|
observeEntryType(observers, "longtask", (list) => {
|
|
312
305
|
for (const entry of list.getEntries()) {
|
|
313
306
|
const nearInteraction = state.lastInteractionAtMs !== null &&
|
|
@@ -18,9 +18,6 @@ export const attachReactRouterIntegration = (options) => {
|
|
|
18
18
|
options.onRouteChange(lastPath, nextPath);
|
|
19
19
|
lastPath = nextPath;
|
|
20
20
|
};
|
|
21
|
-
// Idempotent guard for Cluebase-owned History API wrappers. OTel navigation
|
|
22
|
-
// instrumentation may patch history separately and remains the primary
|
|
23
|
-
// navigation source.
|
|
24
21
|
const sentinelPushState = history.pushState;
|
|
25
22
|
const sentinelReplaceState = history.replaceState;
|
|
26
23
|
const alreadyWrapped = isHistoryAlreadyCluebaseWrapped(history);
|
|
@@ -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
|
export type InternalLogContext = {
|
|
9
2
|
[key: string]: string | number | boolean | null;
|
|
10
3
|
};
|
|
@@ -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.3";
|
|
@@ -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 [];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type SdkErrorDetails = {
|
|
2
|
+
error_name: string;
|
|
3
|
+
error_message: string;
|
|
4
|
+
error_stack?: string;
|
|
5
|
+
};
|
|
6
|
+
export declare const getSdkErrorDetails: (error: unknown) => SdkErrorDetails;
|
|
7
|
+
export declare const reportSdkError: (operation: string, error: unknown) => SdkErrorDetails;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const redactSensitiveText = (value) => value
|
|
2
|
+
.replace(/\bpk_[A-Za-z0-9_-]+\b/g, "[redacted-project-key]")
|
|
3
|
+
.replace(/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/-]+=*/gi, "$1 [redacted]")
|
|
4
|
+
.replace(/(https?:\/\/[^\s?]+)\?[^\s]*/gi, "$1?[redacted]")
|
|
5
|
+
.slice(0, 500);
|
|
6
|
+
const readErrorField = (error, field) => {
|
|
7
|
+
try {
|
|
8
|
+
const value = error?.[field];
|
|
9
|
+
return typeof value === "string" ? value : undefined;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
export const getSdkErrorDetails = (error) => {
|
|
16
|
+
let fallbackMessage;
|
|
17
|
+
try {
|
|
18
|
+
fallbackMessage = String(error);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
fallbackMessage = "Unknown thrown value";
|
|
22
|
+
}
|
|
23
|
+
const stack = readErrorField(error, "stack");
|
|
24
|
+
return {
|
|
25
|
+
error_name: readErrorField(error, "name") || "UnknownError",
|
|
26
|
+
error_message: redactSensitiveText(readErrorField(error, "message") || fallbackMessage),
|
|
27
|
+
...(stack
|
|
28
|
+
? {
|
|
29
|
+
error_stack: stack
|
|
30
|
+
.split(/\r?\n/)
|
|
31
|
+
.slice(1, 9)
|
|
32
|
+
.map(redactSensitiveText)
|
|
33
|
+
.join("\n"),
|
|
34
|
+
}
|
|
35
|
+
: {}),
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
export const reportSdkError = (operation, error) => {
|
|
39
|
+
const details = getSdkErrorDetails(error);
|
|
40
|
+
try {
|
|
41
|
+
if (typeof console !== "undefined" && typeof console.error === "function") {
|
|
42
|
+
const stack = details.error_stack ? `\n${details.error_stack}` : "";
|
|
43
|
+
console.error(`[cluebase] ${operation} failed (${details.error_name}): ${details.error_message}${stack}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch { }
|
|
47
|
+
return details;
|
|
48
|
+
};
|
|
@@ -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
|
}
|