@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.
Files changed (77) hide show
  1. package/README.md +11 -9
  2. package/dist/_generated/correlation-attribute-keys.js +4 -0
  3. package/dist/_generated/otel-span-conversion.d.ts +0 -34
  4. package/dist/_generated/otel-span-conversion.js +0 -32
  5. package/dist/context/action-context.d.ts +39 -0
  6. package/dist/context/action-context.js +318 -0
  7. package/dist/context/browser-storage.d.ts +0 -12
  8. package/dist/context/browser-storage.js +0 -13
  9. package/dist/context/identity.js +0 -4
  10. package/dist/context/organization.d.ts +0 -4
  11. package/dist/context/organization.js +0 -7
  12. package/dist/context/session.js +0 -3
  13. package/dist/context/tab.js +0 -5
  14. package/dist/context/trace.d.ts +13 -1
  15. package/dist/context/trace.js +119 -21
  16. package/dist/exporter/batch.d.ts +0 -12
  17. package/dist/exporter/batch.js +0 -27
  18. package/dist/exporter/browser-outbox.js +0 -17
  19. package/dist/exporter/client.d.ts +0 -15
  20. package/dist/exporter/client.js +0 -43
  21. package/dist/exporter/delivery-response-contract.js +0 -6
  22. package/dist/exporter/delivery-response.d.ts +0 -16
  23. package/dist/exporter/delivery-response.js +0 -19
  24. package/dist/exporter/frontend-resource.d.ts +0 -9
  25. package/dist/exporter/frontend-resource.js +0 -9
  26. package/dist/exporter/observation-source-event-exporter.d.ts +0 -28
  27. package/dist/exporter/observation-source-event-exporter.js +0 -18
  28. package/dist/exporter/parameter-snapshot.js +1 -24
  29. package/dist/exporter/raw-event-to-source-signal.js +18 -11
  30. package/dist/exporter/sanitize.d.ts +1 -12
  31. package/dist/exporter/sanitize.js +6 -39
  32. package/dist/exporter/source-event.d.ts +2 -0
  33. package/dist/exporter/span-to-source-signal.js +7 -3
  34. package/dist/exporter/text-fingerprint.d.ts +0 -8
  35. package/dist/exporter/text-fingerprint.js +0 -8
  36. package/dist/index.d.ts +0 -4
  37. package/dist/index.js +16 -44
  38. package/dist/instrumentation/action.js +3 -2
  39. package/dist/instrumentation/cluebase-authored-attributes.d.ts +0 -2
  40. package/dist/instrumentation/cluebase-authored-attributes.js +0 -23
  41. package/dist/instrumentation/common.d.ts +0 -63
  42. package/dist/instrumentation/common.js +0 -48
  43. package/dist/instrumentation/dom.d.ts +0 -25
  44. package/dist/instrumentation/dom.js +5 -41
  45. package/dist/instrumentation/drag.js +1 -2
  46. package/dist/instrumentation/form.js +1 -2
  47. package/dist/instrumentation/input.js +10 -8
  48. package/dist/instrumentation/naming-evidence.d.ts +0 -12
  49. package/dist/instrumentation/navigation.js +0 -2
  50. package/dist/instrumentation/network-request.d.ts +2 -2
  51. package/dist/instrumentation/network-request.js +1 -2
  52. package/dist/instrumentation/network-span-attributes.d.ts +0 -7
  53. package/dist/instrumentation/network-span-attributes.js +1 -3
  54. package/dist/instrumentation/network.js +6 -1
  55. package/dist/instrumentation/page.js +0 -2
  56. package/dist/instrumentation/performance.js +0 -7
  57. package/dist/integrations/react-router.js +0 -3
  58. package/dist/internal/internal-log.d.ts +0 -7
  59. package/dist/internal/internal-log.js +0 -11
  60. package/dist/provider/_version.d.ts +1 -1
  61. package/dist/provider/_version.js +1 -1
  62. package/dist/provider/config.d.ts +0 -7
  63. package/dist/provider/config.js +3 -16
  64. package/dist/provider/constants.d.ts +1 -17
  65. package/dist/provider/constants.js +1 -31
  66. package/dist/provider/privacy-policy-store.d.ts +0 -12
  67. package/dist/provider/privacy-policy-store.js +0 -12
  68. package/dist/provider/sdk-error.d.ts +7 -0
  69. package/dist/provider/sdk-error.js +48 -0
  70. package/dist/provider/sdk-event-pipeline.d.ts +1 -1
  71. package/dist/provider/sdk-event-pipeline.js +41 -54
  72. package/dist/provider/sdk-otel-pipeline.js +50 -4
  73. package/dist/provider/sdk-runtime-surfaces.js +3 -1
  74. package/dist/provider/sdk.js +18 -24
  75. package/dist/provider/types.d.ts +3 -39
  76. package/dist/provider/usage-governor.js +0 -2
  77. package/package.json +53 -58
@@ -92,32 +92,13 @@ const invalidResponse = () => ({
92
92
  acceptedPartial: false,
93
93
  reason: "invalid_response",
94
94
  });
95
- /**
96
- * Decide from the HTTP status alone whether a failed exchange with Cluebase is
97
- * worth replaying.
98
- *
99
- * This is the single owner of that judgement. Batch delivery consults it when
100
- * the body carries no usable verdict, and browser-token acquisition consults it
101
- * because that endpoint carries no verdict at all. Without one owner the same
102
- * status is permanent on one path and transient on the other, and a request
103
- * that can never succeed is replayed until the customer's events expire.
104
- *
105
- * A status is only replayed when it says "the same request may succeed later".
106
- * That is the rule the SDK's own core already applies — OpenTelemetry's OTLP
107
- * exporter retries 429/502/503/504 and nothing else (`isExportRetryable`) —
108
- * widened here to the statuses this ingest contract additionally defines.
109
- */
110
95
  export const classifyIngestHttpStatus = (status) => {
111
- // A 2xx that got this far means the body did not match the contract. That can
112
- // be a transient rewrite in transit, so it stays replayable.
113
96
  if (status >= 200 && status < 300)
114
97
  return "retry_same_batch";
115
98
  if (status === 409)
116
99
  return "mark_failed";
117
100
  if (status === 413)
118
101
  return "split_batch";
119
- // Deterministic refusals: the request is wrong, unauthorized, or aimed at
120
- // something that is not there. Replaying it changes nothing.
121
102
  if ([400, 401, 403, 404, 405, 406, 410, 415].includes(status)) {
122
103
  return "stop_and_report";
123
104
  }
@@ -6,15 +6,6 @@ export type FrontendResourceInput = FrontendResourceAttributes | {
6
6
  type ResourceRecord = {
7
7
  [key: string]: string;
8
8
  };
9
- /**
10
- * Build the only Resource attributes emitted by the browser SDK's own OTel
11
- * provider. Values are intentionally strings so the same snapshot can be
12
- * used to construct an OTel Resource and to enrich raw source signals.
13
- */
14
9
  export declare const createFrontendResourceAttributes: (resolved: ResolvedCluebaseConfig) => FrontendResourceAttributes;
15
- /**
16
- * Copy only approved frontend Resource attributes into the serialized source
17
- * signal. The returned object is a detached, bounded JSON-safe snapshot.
18
- */
19
10
  export declare const toFrontendSourceResource: (resource: FrontendResourceInput | undefined, deniedKeys?: readonly string[]) => ResourceRecord;
20
11
  export {};
@@ -4,11 +4,6 @@ import { CLUEBASE_OTEL_SDK_SOURCE_REQUIRED_RESOURCE_KEYS, CLUEBASE_OTEL_SDK_SOUR
4
4
  const FRONTEND_RESOURCE_KEYS = new Set(CLUEBASE_OTEL_SDK_SOURCE_RESOURCE_KEYS);
5
5
  const FRONTEND_REQUIRED_RESOURCE_KEYS = new Set(CLUEBASE_OTEL_SDK_SOURCE_REQUIRED_RESOURCE_KEYS);
6
6
  const MAX_RESOURCE_VALUE_BYTES = 512;
7
- /**
8
- * Build the only Resource attributes emitted by the browser SDK's own OTel
9
- * provider. Values are intentionally strings so the same snapshot can be
10
- * used to construct an OTel Resource and to enrich raw source signals.
11
- */
12
7
  export const createFrontendResourceAttributes = (resolved) => ({
13
8
  "service.name": resolved.producerId,
14
9
  "service.version": resolved.release.frontendRelease ?? "unknown",
@@ -20,10 +15,6 @@ export const createFrontendResourceAttributes = (resolved) => ({
20
15
  "cluebase.sdk.collection_mode": resolved.collection?.mode ?? "standard",
21
16
  "cluebase.runtime_language": "browser",
22
17
  });
23
- /**
24
- * Copy only approved frontend Resource attributes into the serialized source
25
- * signal. The returned object is a detached, bounded JSON-safe snapshot.
26
- */
27
18
  export const toFrontendSourceResource = (resource, deniedKeys = []) => {
28
19
  const input = resource && "attributes" in resource ? resource.attributes : resource;
29
20
  const output = {};
@@ -1,15 +1,3 @@
1
- /**
2
- * Browser-SDK observation source event exporter.
3
- *
4
- * Production OTel {@link SpanExporter} for the Frontend SDK. Forwards each
5
- * {@link ReadableSpan} as a source-signal envelope and routes it onto the
6
- * SDK's existing transport buffer. Final classification and canonical shaping
7
- * happen in the Cluebase backend ingestion core.
8
- *
9
- * The exporter is registered onto the global `WebTracerProvider` in
10
- * `provider/sdk.ts:cluebase.init`. `enqueue(event)` accepts source-signal
11
- * envelopes from capture modules.
12
- */
13
1
  import { type ExportResult, ExportResultCode } from "@opentelemetry/core";
14
2
  import type { ReadableSpan, SpanExporter } from "@opentelemetry/sdk-trace-base";
15
3
  import type { CluebaseContext } from "../provider/types.js";
@@ -22,20 +10,8 @@ export interface ObservationSourceEventExporterOptions {
22
10
  getCollectionPermit?: (span: ReadableSpan | null) => CollectionPermit | undefined;
23
11
  flushImpl: () => Promise<void>;
24
12
  shutdownImpl?: () => Promise<void>;
25
- /**
26
- * Returns the active {@link CluebaseContext} for the current SDK instance.
27
- * Called once per `export()` invocation so browser-environment fields
28
- * (device, browser, locale, etc.) reflect the SDK state at export time.
29
- */
30
13
  getContext: () => CluebaseContext;
31
14
  deniedKeys?: readonly string[];
32
- /**
33
- * URL prefixes that should NOT produce source-signal events.
34
- * Used to skip spans that the Cluebase ingest BatchTransport itself
35
- * generates when the host installs ``@opentelemetry/instrumentation-fetch``
36
- * (otherwise Cluebase's own POSTs would loop back through the exporter as
37
- * fetch spans, generating duplicate ingest events ad-infinitum).
38
- */
39
15
  ignoreUrlPrefixes?: readonly string[];
40
16
  }
41
17
  export declare class ObservationSourceEventExporter implements SpanExporter {
@@ -52,10 +28,6 @@ export declare class ObservationSourceEventExporter implements SpanExporter {
52
28
  private skippedLoopSpanCount;
53
29
  constructor(options: ObservationSourceEventExporterOptions);
54
30
  private ignoreSpanReason;
55
- /**
56
- * Enqueue a source-signal event emitted by capture modules through
57
- * `context.onEvent`.
58
- */
59
31
  enqueue(event: BrowserSourceSignalEvent): void;
60
32
  export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void;
61
33
  forceFlush(): Promise<void>;
@@ -1,15 +1,3 @@
1
- /**
2
- * Browser-SDK observation source event exporter.
3
- *
4
- * Production OTel {@link SpanExporter} for the Frontend SDK. Forwards each
5
- * {@link ReadableSpan} as a source-signal envelope and routes it onto the
6
- * SDK's existing transport buffer. Final classification and canonical shaping
7
- * happen in the Cluebase backend ingestion core.
8
- *
9
- * The exporter is registered onto the global `WebTracerProvider` in
10
- * `provider/sdk.ts:cluebase.init`. `enqueue(event)` accepts source-signal
11
- * envelopes from capture modules.
12
- */
13
1
  import { ExportResultCode } from "@opentelemetry/core";
14
2
  import { logSwallowed } from "../internal/internal-log.js";
15
3
  import { spanToSourceSignalEvent } from "./span-to-source-signal.js";
@@ -50,10 +38,6 @@ export class ObservationSourceEventExporter {
50
38
  }
51
39
  return null;
52
40
  }
53
- /**
54
- * Enqueue a source-signal event emitted by capture modules through
55
- * `context.onEvent`.
56
- */
57
41
  enqueue(event) {
58
42
  if (this.closed)
59
43
  return;
@@ -122,7 +106,6 @@ export class ObservationSourceEventExporter {
122
106
  await this.flushImpl();
123
107
  }
124
108
  catch (cause) {
125
- // forceFlush is best-effort; swallow to keep the OTel contract.
126
109
  logSwallowed("forceFlush", cause);
127
110
  }
128
111
  }
@@ -139,7 +122,6 @@ export class ObservationSourceEventExporter {
139
122
  }
140
123
  }
141
124
  catch (cause) {
142
- // shutdown is non-raising per the OTel contract.
143
125
  logSwallowed("shutdown", cause);
144
126
  }
145
127
  }
@@ -36,21 +36,13 @@ const measureSerializedBytes = (value) => {
36
36
  return undefined;
37
37
  }
38
38
  };
39
- /**
40
- * 値をそのまま載せてよいのは、**顧客が画面で名指しした項目だけ**。
41
- *
42
- * 載せるのは文字・数・真偽に限ります。入れ子は葉に来ないので、それ以外の形が
43
- * ここへ来たら伏せた側へ倒します。
44
- */
45
39
  const releasedLeafValue = (value) => value === null ||
46
40
  typeof value === "string" ||
47
41
  typeof value === "number" ||
48
42
  typeof value === "boolean"
49
43
  ? value
50
44
  : MASKED_VALUE;
51
- const buildLeaf = (value, captureMode,
52
- // 顧客がこの項目を名指ししたか。**既定は false = 伏せる。**
53
- released = false) => {
45
+ const buildLeaf = (value, captureMode, released = false) => {
54
46
  const leafType = inferLeafType(value);
55
47
  const originalSizeBytes = captureMode === PARAMETER_CAPTURE_MODE.forbidden
56
48
  ? undefined
@@ -71,20 +63,6 @@ released = false) => {
71
63
  export const buildUnavailableParameterSnapshot = () => ({
72
64
  [SNAPSHOT_UNAVAILABLE_KEY]: buildLeaf("snapshot_unavailable", PARAMETER_CAPTURE_MODE.maskedFingerprint),
73
65
  });
74
- /**
75
- * その節は葉か、降りる枝か。
76
- *
77
- * **見分ける手順をここに書きません。** ここには鍵の有無だけを見る版が書かれており、
78
- * 受付側は型まで見ていました。`{ type: 123, capture_mode: null, value: 1 }` が
79
- * **送る側にとって葉、受け取る側にとって枝**という状態で、受け取った側は中へ降りて
80
- * `type` / `value` / `capture_mode` を項目名として扱っていました。降りる側の 1 つは
81
- * 伏せる判定です。
82
- *
83
- * 手順は正本(`packages/cluebase-schema/.../observation-classification.schema.ts`)から
84
- * 生成され、受付側も同じものを受け取ります。共有の部品は顧客の手元で解決できない
85
- * ので取り込めず、**生成して同梱する**のがこの repo の作法です
86
- * (`_generated/ingest-privacy-tables.ts` と同じ)。
87
- */
88
66
  const isParameterSnapshotLeaf = (value) => hasParameterSnapshotLeafShape(value);
89
67
  export const truncateParameterSnapshotValue = (value) => {
90
68
  if (Array.isArray(value)) {
@@ -197,7 +175,6 @@ export const buildParameterSnapshotBundle = (value, options) => {
197
175
  : null;
198
176
  const captureMode = PARAMETER_CAPTURE_MODE.maskedFingerprint;
199
177
  return {
200
- // 名指しされた項目だけ値を持つ。名指ししていない項目は伏せたまま。
201
178
  snapshot: buildLeaf(current, captureMode, matchedAllowedPath !== null),
202
179
  allowedValueKeys: matchedAllowedPath ? [matchedAllowedPath] : [],
203
180
  };
@@ -1,6 +1,3 @@
1
- // 相関のために残す属性名は、正本から生成した部品を読みます。ここには手で書かれて
2
- // おり、もう 1 か所の写しと 15 語食い違い、`cluebase.session_id` `mcp.session.id` が
3
- // 欠けていて**落ちていました**(`session` を含むため落とす側に当たる)。
4
1
  import { CLUEBASE_CORRELATION_ATTRIBUTE_KEYS } from "../_generated/correlation-attribute-keys.js";
5
2
  import { cluebaseAuthoredPropertyAttribute } from "../instrumentation/cluebase-authored-attributes.js";
6
3
  import { EVENT_SOURCE, EVENT_VERSION, SOURCE_SCHEMA_VERSION, } from "../provider/constants.js";
@@ -11,7 +8,7 @@ import { SDK_SOURCE_SIGNAL_EVENT_NAME, SDK_SOURCE_SIGNAL_EVENT_TYPE, } from "./s
11
8
  export const RAW_FRONTEND_SOURCE_SIGNAL_KIND = "frontend_raw_event";
12
9
  const PUBLIC_CUSTOM_EVENT_NAME_PATTERN = /^[A-Za-z0-9_.:-]{1,128}$/;
13
10
  const isDynamicPayloadObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
14
- function toJsonValue(value, deniedKeys = [], onMaskingError) {
11
+ function toJsonValue(value, deniedKeys = [], onMaskingError, preserveExplicitProperties = false) {
15
12
  if (value === null)
16
13
  return null;
17
14
  if (typeof value === "string" ||
@@ -20,7 +17,7 @@ function toJsonValue(value, deniedKeys = [], onMaskingError) {
20
17
  return typeof value === "number" && !Number.isFinite(value) ? null : value;
21
18
  }
22
19
  if (Array.isArray(value)) {
23
- return value.map((entry) => toJsonValue(entry, deniedKeys, onMaskingError));
20
+ return value.map((entry) => toJsonValue(entry, deniedKeys, onMaskingError, preserveExplicitProperties));
24
21
  }
25
22
  if (isDynamicPayloadObject(value)) {
26
23
  const record = {};
@@ -33,11 +30,12 @@ function toJsonValue(value, deniedKeys = [], onMaskingError) {
33
30
  return record;
34
31
  }
35
32
  for (const [key, entry] of entries) {
36
- if (!CLUEBASE_CORRELATION_ATTRIBUTE_KEYS.has(key) &&
33
+ if (!preserveExplicitProperties &&
34
+ !CLUEBASE_CORRELATION_ATTRIBUTE_KEYS.has(key) &&
37
35
  isDeniedKey(key, deniedKeys)) {
38
36
  continue;
39
37
  }
40
- record[key] = toJsonValue(entry, deniedKeys, onMaskingError);
38
+ record[key] = toJsonValue(entry, deniedKeys, onMaskingError, preserveExplicitProperties);
41
39
  }
42
40
  return record;
43
41
  }
@@ -45,7 +43,7 @@ function toJsonValue(value, deniedKeys = [], onMaskingError) {
45
43
  return value.toString();
46
44
  return null;
47
45
  }
48
- function toJsonRecord(value, deniedKeys = [], onMaskingError) {
46
+ function toJsonRecord(value, deniedKeys = [], onMaskingError, preserveExplicitProperties = false) {
49
47
  const record = {};
50
48
  let entries;
51
49
  try {
@@ -60,7 +58,10 @@ function toJsonRecord(value, deniedKeys = [], onMaskingError) {
60
58
  isDeniedKey(key, deniedKeys)) {
61
59
  continue;
62
60
  }
63
- record[key] = toJsonValue(entry, deniedKeys, onMaskingError);
61
+ record[key] =
62
+ preserveExplicitProperties && key === "properties"
63
+ ? toJsonValue(entry, deniedKeys, onMaskingError, true)
64
+ : toJsonValue(entry, deniedKeys, onMaskingError);
64
65
  }
65
66
  return record;
66
67
  }
@@ -111,6 +112,7 @@ function contextRecord(context, overrides = {}) {
111
112
  trace_id: overrides.trace_id ?? context.trace_id,
112
113
  request_id: overrides.request_id ?? context.request_id,
113
114
  request_span_id: overrides.request_span_id ?? context.request_span_id,
115
+ action_id: overrides.action_id ?? context.action_id,
114
116
  interaction_id: overrides.interaction_id ?? context.interaction_id,
115
117
  correlation_id: context.correlation_id,
116
118
  sdk_collection_mode: context.sdk_collection_mode,
@@ -146,7 +148,8 @@ export function rawEventToSourceSignalEvent(raw, options) {
146
148
  const eventId = generateEventId();
147
149
  const occurredAtMs = raw.occurredAtMs ?? Date.now();
148
150
  const occurredAt = new Date(occurredAtMs).toISOString();
149
- const payload = toJsonRecord(raw.payload, options.deniedKeys, options.onMaskingError);
151
+ const customEventOrigin = raw.type === "custom_emitted" ? trustedCustomOrigin(raw.payload) : null;
152
+ const payload = toJsonRecord(raw.payload, options.deniedKeys, options.onMaskingError, customEventOrigin === "explicit_track");
150
153
  dropNonFiniteMetrics(payload);
151
154
  const requestId = stringPayload(raw.payload, "requestId") ??
152
155
  context.request_id ??
@@ -154,6 +157,7 @@ export function rawEventToSourceSignalEvent(raw, options) {
154
157
  const requestSpanId = stringPayload(raw.payload, "requestSpanId") ??
155
158
  context.request_span_id ??
156
159
  generateRequestSpanId();
160
+ const actionId = stringPayload(raw.payload, "actionId") ?? context.action_id ?? null;
157
161
  const traceId = stringPayload(raw.payload, "traceId") ??
158
162
  context.trace_id ??
159
163
  generateTraceId();
@@ -169,7 +173,6 @@ export function rawEventToSourceSignalEvent(raw, options) {
169
173
  "cluebase.raw_event.payload": payload,
170
174
  "cluebase.event_source": EVENT_SOURCE,
171
175
  };
172
- const customEventOrigin = raw.type === "custom_emitted" ? trustedCustomOrigin(raw.payload) : null;
173
176
  const customEventName = raw.type === "custom_emitted" && customEventOrigin
174
177
  ? stringPayload(raw.payload, "eventName")
175
178
  : null;
@@ -186,6 +189,8 @@ export function rawEventToSourceSignalEvent(raw, options) {
186
189
  attributes["cluebase.request.id"] = requestId;
187
190
  if (requestSpanId)
188
191
  attributes["cluebase.request_span_id"] = requestSpanId;
192
+ if (actionId)
193
+ attributes["cluebase.action.id"] = actionId;
189
194
  if (interactionId)
190
195
  attributes["cluebase.interaction.id"] = interactionId;
191
196
  if (occurrenceKind)
@@ -264,6 +269,7 @@ export function rawEventToSourceSignalEvent(raw, options) {
264
269
  trace_id: traceId,
265
270
  request_id: requestId,
266
271
  request_span_id: requestSpanId,
272
+ action_id: actionId,
267
273
  interaction_id: interactionId,
268
274
  frontend_release: context.frontend_release,
269
275
  backend_release: null,
@@ -290,6 +296,7 @@ export function rawEventToSourceSignalEvent(raw, options) {
290
296
  trace_id: traceId,
291
297
  request_id: requestId,
292
298
  request_span_id: requestSpanId,
299
+ action_id: actionId,
293
300
  interaction_id: interactionId,
294
301
  }),
295
302
  },
@@ -6,7 +6,7 @@ export type SanitizationAccumulator = {
6
6
  };
7
7
  export declare const isDeniedKey: (key: string, deniedKeys?: readonly string[]) => boolean;
8
8
  export declare const sanitizeHeaders: (headers: Record<string, string>, deniedKeys?: string[]) => Record<string, string>;
9
- export declare const sanitizeObject: (value: unknown, deniedKeys?: string[], summary?: SanitizationAccumulator, pathPrefix?: string) => unknown;
9
+ export declare const sanitizeObject: (value: unknown, deniedKeys?: string[], summary?: SanitizationAccumulator, pathPrefix?: string, maskIdentityPii?: boolean, preserveExplicitTraits?: boolean) => unknown;
10
10
  export declare const parseUrlForNetwork: (rawUrl: string) => {
11
11
  host: string;
12
12
  pathTemplate: string;
@@ -16,15 +16,4 @@ export declare const parseUrlForNetwork: (rawUrl: string) => {
16
16
  };
17
17
  export declare const maybeParseJson: (value: unknown) => unknown;
18
18
  export { detectIdentityPII };
19
- /**
20
- * Synchronous deterministic SHA-256 hash for identity values that look
21
- * like PII. The identity sanitization path must remain synchronous
22
- * because `identify(userId)` is sync-only by SDK contract.
23
- *
24
- * Output format: `cluebase_pii_hashed:<32-hex>`.
25
- *
26
- * The algorithm name, the digest length and the prefix are read from the
27
- * generated policy, not written here, so the three SDKs produce the same
28
- * token for the same input by construction rather than by assertion.
29
- */
30
19
  export declare const hashIdentityValue: (value: string) => string;
@@ -1,9 +1,6 @@
1
1
  import { sha256 } from "@noble/hashes/sha256";
2
2
  import { bytesToHex, utf8ToBytes } from "@noble/hashes/utils";
3
- // 個人情報の形を見分ける規則も、正本から生成した部品を読みます。
4
3
  import { detectIdentityPII, PII_HASH_HEX_LENGTH, PII_HASH_PREFIX, } from "../_generated/identity-pii-policy.js";
5
- // 落とす項目名・同形異字を畳む表・突き合わせの手順は、正本から生成した部品を
6
- // 読みます。ここへ写すと受付側とずれます(AGENTS §0.2)。
7
4
  import { isDeniedLookupKey, toDeniedLookupKeySet, } from "../_generated/ingest-privacy-tables.js";
8
5
  import { toPathTemplate } from "../exporter/path-template.js";
9
6
  import { ALWAYS_DENIED_KEYS, MASKED_VALUE, MAX_SANITIZE_DEPTH, } from "../provider/constants.js";
@@ -25,7 +22,7 @@ export const sanitizeHeaders = (headers, deniedKeys = []) => {
25
22
  }
26
23
  return sanitized;
27
24
  };
28
- export const sanitizeObject = (value, deniedKeys = [], summary, pathPrefix = "") => {
25
+ export const sanitizeObject = (value, deniedKeys = [], summary, pathPrefix = "", maskIdentityPii = true, preserveExplicitTraits = false) => {
29
26
  const deniedSet = toDeniedSet(deniedKeys);
30
27
  const seen = new WeakSet();
31
28
  const walk = (current, depth, path) => {
@@ -49,7 +46,7 @@ export const sanitizeObject = (value, deniedKeys = [], summary, pathPrefix = "")
49
46
  seen.add(objectValue);
50
47
  const sanitized = {};
51
48
  for (const [key, child] of Object.entries(objectValue)) {
52
- if (isDeniedKeyWithSet(key, deniedSet)) {
49
+ if (!preserveExplicitTraits && isDeniedKeyWithSet(key, deniedSet)) {
53
50
  if (summary)
54
51
  summary.droppedFieldCount += 1;
55
52
  continue;
@@ -67,13 +64,10 @@ export const sanitizeObject = (value, deniedKeys = [], summary, pathPrefix = "")
67
64
  if (typeof current === "function" || typeof current === "symbol") {
68
65
  return undefined;
69
66
  }
70
- // Value-level PII shape detection. The denied-key gate above catches
71
- // `email = "..."`, but a non-denied key like
72
- // `metadata.customer_contact = "alice@example.com"` would still leak
73
- // the email verbatim. Replace any string whose shape matches an
74
- // identity-PII pattern (email / phone / SSN / credit_card with
75
- // Luhn) with the canonical masked sentinel.
76
- if (typeof current === "string" && detectIdentityPII(current) !== null) {
67
+ if (maskIdentityPii &&
68
+ !preserveExplicitTraits &&
69
+ typeof current === "string" &&
70
+ detectIdentityPII(current) !== null) {
77
71
  if (current === MASKED_VALUE)
78
72
  return current;
79
73
  if (summary) {
@@ -146,32 +140,5 @@ export const maybeParseJson = (value) => {
146
140
  return null;
147
141
  }
148
142
  };
149
- // ---------------------------------------------------------------------------
150
- // Identity-value PII detection for browser payloads.
151
- //
152
- // The frontend SDK does not propagate identity via W3C Baggage (browser-side
153
- // never opens outgoing HTTP to non-Cluebase services), so the leak vector here is
154
- // the canonical span attribute `cluebase.actor.user_id` getting captured in raw
155
- // observation storage / facts_json. Hashing PII-shaped identity values
156
- // prevents that.
157
- //
158
- // この面は値の**中**を走査します(裏の配る部品は両端で留めます)。違いは本物で、
159
- // 意図したものです——**問いが違うからです。** ここは「この文字列が個人情報を
160
- // 含むか」、裏は「この値そのものが個人情報か」。正本
161
- // (`identity-pii-policy.v1.json` の面 `text_contains_pii`)に 1 度だけ書かれ、
162
- // 下の生成物から読みます。**面の名前は生成物の `PII_SURFACE` が名乗ります。**
163
- // 判定の手順は**正本から出した部品が持ちます**。ここには手順が手で書かれており、
164
- // 受付側にも同じ手順の 2 本目が手で書かれていました(AGENTS §0.2)。
165
143
  export { detectIdentityPII };
166
- /**
167
- * Synchronous deterministic SHA-256 hash for identity values that look
168
- * like PII. The identity sanitization path must remain synchronous
169
- * because `identify(userId)` is sync-only by SDK contract.
170
- *
171
- * Output format: `cluebase_pii_hashed:<32-hex>`.
172
- *
173
- * The algorithm name, the digest length and the prefix are read from the
174
- * generated policy, not written here, so the three SDKs produce the same
175
- * token for the same input by construction rather than by assertion.
176
- */
177
144
  export const hashIdentityValue = (value) => `${PII_HASH_PREFIX}${bytesToHex(sha256(utf8ToBytes(value))).slice(0, PII_HASH_HEX_LENGTH)}`;
@@ -12,6 +12,7 @@ export type BrowserSdkSignalContext = {
12
12
  trace_id: string;
13
13
  request_id: string;
14
14
  request_span_id: string;
15
+ action_id: string | null;
15
16
  interaction_id: string;
16
17
  correlation_id: string | null;
17
18
  sdk_collection_mode: SdkCollectionMode;
@@ -88,6 +89,7 @@ export type BrowserSourceSignalEvent = {
88
89
  parent_span_id?: string | null;
89
90
  request_id: string;
90
91
  request_span_id: string;
92
+ action_id: string | null;
91
93
  interaction_id: string;
92
94
  frontend_release: string | null;
93
95
  backend_release: string | null;
@@ -1,6 +1,3 @@
1
- // 相関のために残す属性名は、正本から生成した部品を読みます。ここには手で書かれて
2
- // おり、もう 1 か所の写しと 15 語食い違い、`cluebase.session_id` `mcp.session.id` が
3
- // 欠けていて**落ちていました**(`session` を含むため落とす側に当たる)。
4
1
  import { CLUEBASE_CORRELATION_ATTRIBUTE_KEYS } from "../_generated/correlation-attribute-keys.js";
5
2
  import { attrString, hrTimeToUnixNano, nanosToIso, parentSpanId, } from "../_generated/otel-span-conversion.js";
6
3
  import { EVENT_SOURCE, EVENT_VERSION, SOURCE_SCHEMA_VERSION, } from "../provider/constants.js";
@@ -59,6 +56,7 @@ function contextRecord(context, overrides) {
59
56
  trace_id: overrides.trace_id,
60
57
  request_id: overrides.request_id,
61
58
  request_span_id: overrides.request_span_id,
59
+ action_id: overrides.action_id,
62
60
  interaction_id: overrides.interaction_id,
63
61
  correlation_id: context.correlation_id,
64
62
  sdk_collection_mode: context.sdk_collection_mode,
@@ -109,6 +107,10 @@ export function spanToSourceSignalEvent(span, options) {
109
107
  attrString(attributes, "cluebase.request_id") ??
110
108
  context.request_id ??
111
109
  generateRequestId();
110
+ const actionId = attrString(attributes, "cluebase.action.id") ??
111
+ attrString(attributes, "cluebase.action_id") ??
112
+ context.action_id ??
113
+ null;
112
114
  const interactionId = attrString(attributes, "cluebase.interaction.id") ??
113
115
  attrString(attributes, "cluebase.interaction_id") ??
114
116
  context.interaction_id ??
@@ -139,6 +141,7 @@ export function spanToSourceSignalEvent(span, options) {
139
141
  parent_span_id: parentId,
140
142
  request_id: requestId,
141
143
  request_span_id: requestSpanId,
144
+ action_id: actionId,
142
145
  interaction_id: interactionId,
143
146
  frontend_release: context.frontend_release,
144
147
  backend_release: null,
@@ -167,6 +170,7 @@ export function spanToSourceSignalEvent(span, options) {
167
170
  trace_id: traceId,
168
171
  request_id: requestId,
169
172
  request_span_id: requestSpanId,
173
+ action_id: actionId,
170
174
  interaction_id: interactionId,
171
175
  }),
172
176
  },
@@ -1,9 +1 @@
1
- /**
2
- * 文字列を短い印にたたむ。
3
- *
4
- * 中身そのものは残さない(項目の並びや入力欄の値は、そのままでは送れない)。
5
- * 同じ入力なら必ず同じ印になり、印から元へは戻らない。
6
- * FNV-1a の 32 bit 版。**衝突しない保証は無い**ので、突合の鍵ではなく
7
- * 「同じか違うか」を見るための印としてのみ使う。
8
- */
9
1
  export declare const fingerprintText: (value: string) => string;
@@ -1,11 +1,3 @@
1
- /**
2
- * 文字列を短い印にたたむ。
3
- *
4
- * 中身そのものは残さない(項目の並びや入力欄の値は、そのままでは送れない)。
5
- * 同じ入力なら必ず同じ印になり、印から元へは戻らない。
6
- * FNV-1a の 32 bit 版。**衝突しない保証は無い**ので、突合の鍵ではなく
7
- * 「同じか違うか」を見るための印としてのみ使う。
8
- */
9
1
  export const fingerprintText = (value) => {
10
2
  let hash = 0x811c9dc5;
11
3
  for (let index = 0; index < value.length; index += 1) {
package/dist/index.d.ts CHANGED
@@ -2,10 +2,6 @@ import type { InternalMetricsSnapshot } from "./internal/metrics.js";
2
2
  import { createCluebaseBrowserSdk } from "./provider/sdk.js";
3
3
  import type { CluebaseBrowserSdk, CluebaseConsentState, CluebaseGroupTraits, CluebaseGroupType, CluebaseInitOptions, CluebaseSubjectTraits, CluebaseTrackMetrics, CluebaseTrackProperties } from "./provider/types.js";
4
4
  declare const cluebase: {
5
- /**
6
- * SDK を初期化する。 `endpoint` / `projectKey` は必須で、 欠けていれば送信を
7
- * 黙って捨てずに throw する。
8
- */
9
5
  init(options: CluebaseInitOptions): void;
10
6
  identify(userId: string, traits?: CluebaseSubjectTraits): void;
11
7
  group(groupType: CluebaseGroupType, groupKey: string, traits?: CluebaseGroupTraits): void;