@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.
Files changed (75) 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 +298 -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 +9 -39
  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-event-pipeline.d.ts +1 -1
  69. package/dist/provider/sdk-event-pipeline.js +41 -54
  70. package/dist/provider/sdk-otel-pipeline.js +50 -4
  71. package/dist/provider/sdk-runtime-surfaces.js +3 -1
  72. package/dist/provider/sdk.js +3 -23
  73. package/dist/provider/types.d.ts +3 -39
  74. package/dist/provider/usage-governor.js +0 -2
  75. package/package.json +3 -6
@@ -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;
package/dist/index.js CHANGED
@@ -1,7 +1,5 @@
1
1
  import { parseSendableUrl } from "./provider/config.js";
2
2
  import { createCluebaseBrowserSdk } from "./provider/sdk.js";
3
- // SDK は host application 内で singleton として動く。
4
- // 同じ tab で複数回 import されても 1 instance に統合する。
5
3
  const GLOBAL_SDK_KEY = "__cluebase_frontend_sdk_singleton__";
6
4
  const globalWithCluebaseSdk = globalThis;
7
5
  const existingSdk = globalWithCluebaseSdk[GLOBAL_SDK_KEY];
@@ -18,7 +16,6 @@ const reportPublicSdkError = (operation) => {
18
16
  }
19
17
  }
20
18
  catch {
21
- // Cluebase は host application を絶対に失敗させない。
22
19
  }
23
20
  };
24
21
  const safeCall = (operation, callback) => {
@@ -37,10 +34,6 @@ const safeAsyncCall = async (operation, callback) => {
37
34
  reportPublicSdkError(operation);
38
35
  }
39
36
  };
40
- // `privacy.allowedValuePaths` used to let the caller name the request/response
41
- // fields that may leave the browser in clear text. That choice now belongs to
42
- // the Cluebase privacy screen and reaches the SDK through the delivered privacy
43
- // policy, so the option has nothing left to apply to.
44
37
  const REMOVED_PRIVACY_OPTIONS = {
45
38
  allowedValuePaths: "cluebase.init: 'privacy.allowedValuePaths' was removed in " +
46
39
  "@genn-inc/cluebase-frontend-sdk 0.0.1. The fields allowed to leave the browser in " +
@@ -49,22 +42,7 @@ const REMOVED_PRIVACY_OPTIONS = {
49
42
  "and select those fields in the Cluebase privacy screen instead. Until then " +
50
43
  "every value stays masked.",
51
44
  };
52
- const REQUIRED_INIT_OPTIONS = [
53
- "endpoint",
54
- "projectKey",
55
- ];
56
- /**
57
- * Raise on a setup mistake instead of collecting nothing and warning once.
58
- *
59
- * This is the one place `cluebase.init` is allowed to throw. The `cluebase.*`
60
- * never-propagate contract exists so a telemetry fault cannot break a page the
61
- * visitor is using; it is not there to hide a setting that quietly stopped
62
- * applying, nor a key the caller forgot. Both mistakes come from the build's
63
- * own configuration, so they surface on the developer's first page load rather
64
- * than on a visitor's — and left silent they are invisible afterwards: the page
65
- * renders, `getDiagnostics()` reports an open lifecycle with no errors, and not
66
- * one event is ever delivered.
67
- */
45
+ const REQUIRED_INIT_OPTIONS = ["projectKey"];
68
46
  const validateInitOptions = (options) => {
69
47
  const privacy = options.privacy ?? {};
70
48
  for (const [name, message] of Object.entries(REMOVED_PRIVACY_OPTIONS)) {
@@ -72,8 +50,6 @@ const validateInitOptions = (options) => {
72
50
  throw new Error(message);
73
51
  }
74
52
  }
75
- // Anything that is not a string reads as absent, so a stray `undefined` from
76
- // the bundler's env replacement cannot become the literal "undefined" endpoint.
77
53
  const missing = REQUIRED_INIT_OPTIONS.filter((name) => {
78
54
  const value = options[name];
79
55
  return typeof value !== "string" || value.trim() === "";
@@ -83,12 +59,15 @@ const validateInitOptions = (options) => {
83
59
  `${missing.length === 1 ? "is" : "are"} required and must be a non-empty ` +
84
60
  "string. Cluebase collects nothing until every required option is set. Pass " +
85
61
  "them from your environment, for example " +
86
- "endpoint=NEXT_PUBLIC_CLUEBASE_API_BASE_URL and " +
87
- "projectKey=NEXT_PUBLIC_CLUEBASE_PROJECT_KEY.");
62
+ "projectKey=NEXT_PUBLIC_CLUEBASE_PROJECT_KEY. The endpoint is optional " +
63
+ "and defaults to the hosted Cluebase API.");
88
64
  }
89
- // Reporting a typo against the setting that caused it, rather than letting
90
- // it surface later as delivery that never arrives.
91
- if (parseSendableUrl(options.endpoint) === null) {
65
+ if (options.endpoint !== undefined &&
66
+ typeof options.endpoint !== "string") {
67
+ throw new Error("cluebase.init: 'endpoint' must be a string when provided. Omit it to " +
68
+ "use the hosted Cluebase endpoint.");
69
+ }
70
+ if (options.endpoint?.trim() && parseSendableUrl(options.endpoint) === null) {
92
71
  throw new Error(`cluebase.init: 'endpoint' must be an absolute http:// or https:// URL, but ` +
93
72
  `received ${JSON.stringify(options.endpoint)}. An endpoint the SDK cannot ` +
94
73
  "send to loses every event just as silently as one that was never passed. " +
@@ -96,15 +75,8 @@ const validateInitOptions = (options) => {
96
75
  "endpoint=NEXT_PUBLIC_CLUEBASE_API_BASE_URL.");
97
76
  }
98
77
  };
99
- // Public API — namespace `cluebase` の 8 method
100
78
  const cluebase = {
101
- /**
102
- * SDK を初期化する。 `endpoint` / `projectKey` は必須で、 欠けていれば送信を
103
- * 黙って捨てずに throw する。
104
- */
105
79
  init(options) {
106
- // Thrown before `safeCall` so the caller actually sees it; see
107
- // `validateInitOptions`.
108
80
  validateInitOptions(options);
109
81
  safeCall("init", () => {
110
82
  internalSdk.init(options);
@@ -142,7 +114,6 @@ const cluebase = {
142
114
  async close() {
143
115
  await safeAsyncCall("close", () => internalSdk.close());
144
116
  },
145
- // 内部 metrics 取得 (= 顧客 debug 用、 production では通常不要)
146
117
  getInternalMetrics() {
147
118
  return internalSdk.getInternalMetrics();
148
119
  },
@@ -150,7 +121,6 @@ const cluebase = {
150
121
  return internalSdk.getDiagnostics();
151
122
  },
152
123
  };
153
- // OTel-core exporter surface for advanced host integrations.
154
124
  export { EXPORT_RESULT_FAILED, EXPORT_RESULT_SUCCESS, ObservationSourceEventExporter, } from "./exporter/observation-source-event-exporter.js";
155
125
  export { attachNextRouterIntegration } from "./integrations/next-router.js";
156
126
  export { attachReactRouterIntegration } from "./integrations/react-router.js";