@genn-inc/cluebase-frontend-sdk 0.0.1

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 (140) hide show
  1. package/README.md +222 -0
  2. package/dist/_generated/correlation-attribute-keys.d.ts +2 -0
  3. package/dist/_generated/correlation-attribute-keys.js +33 -0
  4. package/dist/_generated/identity-pii-policy.d.ts +38 -0
  5. package/dist/_generated/identity-pii-policy.js +103 -0
  6. package/dist/_generated/ingest-privacy-tables.d.ts +120 -0
  7. package/dist/_generated/ingest-privacy-tables.js +890 -0
  8. package/dist/_generated/otel-resource-keys.d.ts +21 -0
  9. package/dist/_generated/otel-resource-keys.js +72 -0
  10. package/dist/_generated/otel-span-conversion.d.ts +48 -0
  11. package/dist/_generated/otel-span-conversion.js +69 -0
  12. package/dist/_generated/parameter-snapshot-leaf.d.ts +11 -0
  13. package/dist/_generated/parameter-snapshot-leaf.js +37 -0
  14. package/dist/_generated/sdk-runtime-policy.d.ts +7 -0
  15. package/dist/_generated/sdk-runtime-policy.js +14 -0
  16. package/dist/_generated/usage-governor-tables.d.ts +15 -0
  17. package/dist/_generated/usage-governor-tables.js +44 -0
  18. package/dist/context/browser-storage.d.ts +15 -0
  19. package/dist/context/browser-storage.js +38 -0
  20. package/dist/context/environment.d.ts +37 -0
  21. package/dist/context/environment.js +229 -0
  22. package/dist/context/identity.d.ts +16 -0
  23. package/dist/context/identity.js +139 -0
  24. package/dist/context/organization.d.ts +16 -0
  25. package/dist/context/organization.js +60 -0
  26. package/dist/context/session.d.ts +28 -0
  27. package/dist/context/session.js +130 -0
  28. package/dist/context/tab.d.ts +22 -0
  29. package/dist/context/tab.js +117 -0
  30. package/dist/context/trace.d.ts +76 -0
  31. package/dist/context/trace.js +163 -0
  32. package/dist/exporter/batch.d.ts +94 -0
  33. package/dist/exporter/batch.js +485 -0
  34. package/dist/exporter/browser-outbox-records.d.ts +159 -0
  35. package/dist/exporter/browser-outbox-records.js +341 -0
  36. package/dist/exporter/browser-outbox.d.ts +112 -0
  37. package/dist/exporter/browser-outbox.js +876 -0
  38. package/dist/exporter/client.d.ts +92 -0
  39. package/dist/exporter/client.js +541 -0
  40. package/dist/exporter/delivery-response-contract.d.ts +103 -0
  41. package/dist/exporter/delivery-response-contract.js +147 -0
  42. package/dist/exporter/delivery-response.d.ts +37 -0
  43. package/dist/exporter/delivery-response.js +151 -0
  44. package/dist/exporter/diagnostics.d.ts +42 -0
  45. package/dist/exporter/diagnostics.js +148 -0
  46. package/dist/exporter/event-id.d.ts +10 -0
  47. package/dist/exporter/event-id.js +75 -0
  48. package/dist/exporter/frontend-resource.d.ts +20 -0
  49. package/dist/exporter/frontend-resource.js +52 -0
  50. package/dist/exporter/local-minimization.d.ts +29 -0
  51. package/dist/exporter/local-minimization.js +87 -0
  52. package/dist/exporter/observation-source-event-exporter.d.ts +69 -0
  53. package/dist/exporter/observation-source-event-exporter.js +154 -0
  54. package/dist/exporter/parameter-snapshot.d.ts +14 -0
  55. package/dist/exporter/parameter-snapshot.js +215 -0
  56. package/dist/exporter/path-template.d.ts +1 -0
  57. package/dist/exporter/path-template.js +67 -0
  58. package/dist/exporter/raw-event-to-source-signal.d.ts +12 -0
  59. package/dist/exporter/raw-event-to-source-signal.js +297 -0
  60. package/dist/exporter/retry.d.ts +8 -0
  61. package/dist/exporter/retry.js +8 -0
  62. package/dist/exporter/sanitize.d.ts +30 -0
  63. package/dist/exporter/sanitize.js +177 -0
  64. package/dist/exporter/schema-evidence.d.ts +20 -0
  65. package/dist/exporter/schema-evidence.js +232 -0
  66. package/dist/exporter/source-event.d.ts +99 -0
  67. package/dist/exporter/source-event.js +13 -0
  68. package/dist/exporter/span-to-source-signal.d.ts +8 -0
  69. package/dist/exporter/span-to-source-signal.js +174 -0
  70. package/dist/exporter/text-fingerprint.d.ts +9 -0
  71. package/dist/exporter/text-fingerprint.js +16 -0
  72. package/dist/index.d.ts +25 -0
  73. package/dist/index.js +158 -0
  74. package/dist/instrumentation/action.d.ts +2 -0
  75. package/dist/instrumentation/action.js +171 -0
  76. package/dist/instrumentation/cluebase-authored-attributes.d.ts +4 -0
  77. package/dist/instrumentation/cluebase-authored-attributes.js +32 -0
  78. package/dist/instrumentation/common.d.ts +73 -0
  79. package/dist/instrumentation/common.js +90 -0
  80. package/dist/instrumentation/dom.d.ts +63 -0
  81. package/dist/instrumentation/dom.js +640 -0
  82. package/dist/instrumentation/drag.d.ts +2 -0
  83. package/dist/instrumentation/drag.js +82 -0
  84. package/dist/instrumentation/error.d.ts +2 -0
  85. package/dist/instrumentation/error.js +318 -0
  86. package/dist/instrumentation/form.d.ts +2 -0
  87. package/dist/instrumentation/form.js +132 -0
  88. package/dist/instrumentation/input.d.ts +2 -0
  89. package/dist/instrumentation/input.js +180 -0
  90. package/dist/instrumentation/location.d.ts +11 -0
  91. package/dist/instrumentation/location.js +69 -0
  92. package/dist/instrumentation/naming-evidence.d.ts +20 -0
  93. package/dist/instrumentation/naming-evidence.js +11 -0
  94. package/dist/instrumentation/navigation.d.ts +2 -0
  95. package/dist/instrumentation/navigation.js +144 -0
  96. package/dist/instrumentation/network-request.d.ts +103 -0
  97. package/dist/instrumentation/network-request.js +285 -0
  98. package/dist/instrumentation/network-span-attributes.d.ts +25 -0
  99. package/dist/instrumentation/network-span-attributes.js +153 -0
  100. package/dist/instrumentation/network.d.ts +16 -0
  101. package/dist/instrumentation/network.js +502 -0
  102. package/dist/instrumentation/page.d.ts +2 -0
  103. package/dist/instrumentation/page.js +56 -0
  104. package/dist/instrumentation/performance.d.ts +2 -0
  105. package/dist/instrumentation/performance.js +365 -0
  106. package/dist/integrations/next-router.d.ts +16 -0
  107. package/dist/integrations/next-router.js +18 -0
  108. package/dist/integrations/react-router.d.ts +7 -0
  109. package/dist/integrations/react-router.js +61 -0
  110. package/dist/internal/internal-log.d.ts +23 -0
  111. package/dist/internal/internal-log.js +47 -0
  112. package/dist/internal/metrics.d.ts +10 -0
  113. package/dist/internal/metrics.js +50 -0
  114. package/dist/provider/_version.d.ts +1 -0
  115. package/dist/provider/_version.js +3 -0
  116. package/dist/provider/config.d.ts +12 -0
  117. package/dist/provider/config.js +177 -0
  118. package/dist/provider/constants.d.ts +62 -0
  119. package/dist/provider/constants.js +118 -0
  120. package/dist/provider/contracts.d.ts +51 -0
  121. package/dist/provider/contracts.js +46 -0
  122. package/dist/provider/privacy-policy-store.d.ts +20 -0
  123. package/dist/provider/privacy-policy-store.js +56 -0
  124. package/dist/provider/sampling.d.ts +2 -0
  125. package/dist/provider/sampling.js +17 -0
  126. package/dist/provider/sdk-event-pipeline.d.ts +60 -0
  127. package/dist/provider/sdk-event-pipeline.js +478 -0
  128. package/dist/provider/sdk-input-normalization.d.ts +7 -0
  129. package/dist/provider/sdk-input-normalization.js +113 -0
  130. package/dist/provider/sdk-otel-pipeline.d.ts +14 -0
  131. package/dist/provider/sdk-otel-pipeline.js +148 -0
  132. package/dist/provider/sdk-runtime-surfaces.d.ts +49 -0
  133. package/dist/provider/sdk-runtime-surfaces.js +216 -0
  134. package/dist/provider/sdk.d.ts +2 -0
  135. package/dist/provider/sdk.js +614 -0
  136. package/dist/provider/types.d.ts +323 -0
  137. package/dist/provider/types.js +1 -0
  138. package/dist/provider/usage-governor.d.ts +7 -0
  139. package/dist/provider/usage-governor.js +116 -0
  140. package/package.json +59 -0
@@ -0,0 +1,174 @@
1
+ // 相関のために残す属性名は、正本から生成した部品を読みます。ここには手で書かれて
2
+ // おり、もう 1 か所の写しと 15 語食い違い、`cluebase.session_id` `mcp.session.id` が
3
+ // 欠けていて**落ちていました**(`session` を含むため落とす側に当たる)。
4
+ import { CLUEBASE_CORRELATION_ATTRIBUTE_KEYS } from "../_generated/correlation-attribute-keys.js";
5
+ import { attrString, hrTimeToUnixNano, nanosToIso, parentSpanId, } from "../_generated/otel-span-conversion.js";
6
+ import { EVENT_SOURCE, EVENT_VERSION, SOURCE_SCHEMA_VERSION, } from "../provider/constants.js";
7
+ import { generateEventId, generateInteractionId, generateRequestId, generateRequestSpanId, generateTraceId, } from "./event-id.js";
8
+ import { isDeniedKey } from "./sanitize.js";
9
+ import { toFrontendSourceResource } from "./frontend-resource.js";
10
+ import { SDK_SOURCE_SIGNAL_EVENT_NAME, SDK_SOURCE_SIGNAL_EVENT_TYPE, } from "./source-event.js";
11
+ const isDynamicJsonObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
12
+ function toJsonValue(value) {
13
+ if (value === null)
14
+ return null;
15
+ if (typeof value === "string" ||
16
+ typeof value === "number" ||
17
+ typeof value === "boolean") {
18
+ return typeof value === "number" && !Number.isFinite(value) ? null : value;
19
+ }
20
+ if (Array.isArray(value))
21
+ return value.map((entry) => toJsonValue(entry));
22
+ if (isDynamicJsonObject(value)) {
23
+ const record = {};
24
+ for (const [key, entry] of Object.entries(value)) {
25
+ record[key] = toJsonValue(entry);
26
+ }
27
+ return record;
28
+ }
29
+ if (typeof value === "bigint")
30
+ return value.toString();
31
+ return null;
32
+ }
33
+ function toJsonRecord(value, deniedKeys = []) {
34
+ const record = {};
35
+ for (const [key, entry] of Object.entries(value ?? {})) {
36
+ if (!CLUEBASE_CORRELATION_ATTRIBUTE_KEYS.has(key) &&
37
+ isDeniedKey(key, deniedKeys)) {
38
+ continue;
39
+ }
40
+ record[key] = toJsonValue(entry);
41
+ }
42
+ return record;
43
+ }
44
+ function instrumentationScopeName(span) {
45
+ const instrumentation = span;
46
+ return (instrumentation.instrumentationScope?.name ??
47
+ instrumentation.instrumentationLibrary?.name ??
48
+ null);
49
+ }
50
+ function contextRecord(context, overrides) {
51
+ return {
52
+ producer_id: context.producer_id,
53
+ anonymous_id: context.anonymous_id,
54
+ user_id: context.user_id,
55
+ organization_id: context.organization_id ?? null,
56
+ user_profile: context.user_profile,
57
+ session_id: context.session_id,
58
+ tab_id: context.tab_id,
59
+ trace_id: overrides.trace_id,
60
+ request_id: overrides.request_id,
61
+ request_span_id: overrides.request_span_id,
62
+ interaction_id: overrides.interaction_id,
63
+ correlation_id: context.correlation_id,
64
+ sdk_collection_mode: context.sdk_collection_mode,
65
+ frontend_release: context.frontend_release,
66
+ locale: context.locale,
67
+ country: context.country,
68
+ device_type: context.device_type,
69
+ browser: context.browser,
70
+ os: context.os,
71
+ referrer: context.referrer,
72
+ browser_family: context.browser_family ?? null,
73
+ browser_major_version: context.browser_major_version ?? null,
74
+ browser_engine: context.browser_engine ?? null,
75
+ os_family: context.os_family ?? null,
76
+ os_major_version: context.os_major_version ?? null,
77
+ timezone: context.timezone ?? null,
78
+ pointer_type_candidate: context.pointer_type_candidate ?? null,
79
+ touch_capable: context.touch_capable ?? null,
80
+ cookies_enabled: context.cookies_enabled ?? null,
81
+ local_storage_available: context.local_storage_available ?? null,
82
+ session_storage_available: context.session_storage_available ?? null,
83
+ service_worker_supported: context.service_worker_supported ?? null,
84
+ webgl_supported: context.webgl_supported ?? null,
85
+ reduced_motion: context.reduced_motion ?? null,
86
+ color_scheme: context.color_scheme ?? null,
87
+ subject_traits: context.subject_traits,
88
+ organization_traits: context.organization_traits,
89
+ context_traits: context.context_traits,
90
+ };
91
+ }
92
+ export function spanToSourceSignalEvent(span, options) {
93
+ const context = options.context;
94
+ const attributes = toJsonRecord(span.attributes, options.deniedKeys);
95
+ const spanContext = span.spanContext();
96
+ const traceId = attrString(attributes, "cluebase.trace_id") ??
97
+ spanContext.traceId ??
98
+ generateTraceId();
99
+ const spanId = spanContext.spanId || null;
100
+ const parentId = parentSpanId(span, attributes);
101
+ const startTimeUnixNano = hrTimeToUnixNano(span.startTime);
102
+ const endTimeUnixNano = hrTimeToUnixNano(span.endTime);
103
+ const eventId = generateEventId();
104
+ const requestSpanId = attrString(attributes, "cluebase.request_span_id") ??
105
+ spanId ??
106
+ context.request_span_id ??
107
+ generateRequestSpanId();
108
+ const requestId = attrString(attributes, "cluebase.request.id") ??
109
+ attrString(attributes, "cluebase.request_id") ??
110
+ context.request_id ??
111
+ generateRequestId();
112
+ const interactionId = attrString(attributes, "cluebase.interaction.id") ??
113
+ attrString(attributes, "cluebase.interaction_id") ??
114
+ context.interaction_id ??
115
+ generateInteractionId();
116
+ return {
117
+ event_id: eventId,
118
+ source_event_id: eventId,
119
+ source_schema_version: SOURCE_SCHEMA_VERSION,
120
+ source_event_type: SDK_SOURCE_SIGNAL_EVENT_TYPE,
121
+ source_event_kind: SDK_SOURCE_SIGNAL_EVENT_TYPE,
122
+ producer_id: context.producer_id,
123
+ service_name: EVENT_SOURCE,
124
+ event_name: SDK_SOURCE_SIGNAL_EVENT_NAME,
125
+ event_category: "context",
126
+ event_source: EVENT_SOURCE,
127
+ event_version: EVENT_VERSION,
128
+ occurred_at: nanosToIso(startTimeUnixNano),
129
+ ingested_at: new Date().toISOString(),
130
+ status: "observed",
131
+ sdk_collection_mode: context.sdk_collection_mode,
132
+ privacy_state: "masked",
133
+ privacy_level: "default_masked",
134
+ masking_state: "masked",
135
+ allowed_value_keys: [],
136
+ privacy_summary: context.privacy_summary,
137
+ trace_id: traceId,
138
+ span_id: spanId,
139
+ parent_span_id: parentId,
140
+ request_id: requestId,
141
+ request_span_id: requestSpanId,
142
+ interaction_id: interactionId,
143
+ frontend_release: context.frontend_release,
144
+ backend_release: null,
145
+ properties: {},
146
+ metrics: {},
147
+ sdk_signal: {
148
+ surface: "frontend",
149
+ kind: "span",
150
+ name: span.name,
151
+ span_kind: span.kind ?? null,
152
+ trace_id: traceId,
153
+ span_id: spanId,
154
+ parent_span_id: parentId,
155
+ start_time_unix_nano: startTimeUnixNano,
156
+ end_time_unix_nano: endTimeUnixNano,
157
+ status_code: span.status?.code ?? null,
158
+ status_message: span.status?.message ?? null,
159
+ instrumentation_scope_name: instrumentationScopeName(span),
160
+ attributes,
161
+ events: (span.events ?? []).map((event) => ({
162
+ name: event.name,
163
+ attributes: toJsonRecord(event.attributes, options.deniedKeys),
164
+ })),
165
+ resource: toFrontendSourceResource(span.resource, options.deniedKeys),
166
+ context: contextRecord(context, {
167
+ trace_id: traceId,
168
+ request_id: requestId,
169
+ request_span_id: requestSpanId,
170
+ interaction_id: interactionId,
171
+ }),
172
+ },
173
+ };
174
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * 文字列を短い印にたたむ。
3
+ *
4
+ * 中身そのものは残さない(項目の並びや入力欄の値は、そのままでは送れない)。
5
+ * 同じ入力なら必ず同じ印になり、印から元へは戻らない。
6
+ * FNV-1a の 32 bit 版。**衝突しない保証は無い**ので、突合の鍵ではなく
7
+ * 「同じか違うか」を見るための印としてのみ使う。
8
+ */
9
+ export declare const fingerprintText: (value: string) => string;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * 文字列を短い印にたたむ。
3
+ *
4
+ * 中身そのものは残さない(項目の並びや入力欄の値は、そのままでは送れない)。
5
+ * 同じ入力なら必ず同じ印になり、印から元へは戻らない。
6
+ * FNV-1a の 32 bit 版。**衝突しない保証は無い**ので、突合の鍵ではなく
7
+ * 「同じか違うか」を見るための印としてのみ使う。
8
+ */
9
+ export const fingerprintText = (value) => {
10
+ let hash = 0x811c9dc5;
11
+ for (let index = 0; index < value.length; index += 1) {
12
+ hash ^= value.charCodeAt(index);
13
+ hash = Math.imul(hash, 0x01000193);
14
+ }
15
+ return `fnv1a:${(hash >>> 0).toString(16).padStart(8, "0")}`;
16
+ };
@@ -0,0 +1,25 @@
1
+ import type { InternalMetricsSnapshot } from "./internal/metrics.js";
2
+ import { createCluebaseBrowserSdk } from "./provider/sdk.js";
3
+ import type { CluebaseBrowserSdk, CluebaseConsentState, CluebaseGroupTraits, CluebaseGroupType, CluebaseInitOptions, CluebaseSubjectTraits, CluebaseTrackMetrics, CluebaseTrackProperties } from "./provider/types.js";
4
+ declare const cluebase: {
5
+ /**
6
+ * SDK を初期化する。 `endpoint` / `projectKey` は必須で、 欠けていれば送信を
7
+ * 黙って捨てずに throw する。
8
+ */
9
+ init(options: CluebaseInitOptions): void;
10
+ identify(userId: string, traits?: CluebaseSubjectTraits): void;
11
+ group(groupType: CluebaseGroupType, groupKey: string, traits?: CluebaseGroupTraits): void;
12
+ reset(): void;
13
+ track(eventName: string, properties?: CluebaseTrackProperties, metrics?: CluebaseTrackMetrics): void;
14
+ setConsent(consent: CluebaseConsentState): void;
15
+ flush(): Promise<void>;
16
+ close(): Promise<void>;
17
+ getInternalMetrics(): InternalMetricsSnapshot;
18
+ getDiagnostics(): import("./exporter/diagnostics.js").DiagnosticsSnapshot;
19
+ };
20
+ export { EXPORT_RESULT_FAILED, EXPORT_RESULT_SUCCESS, ObservationSourceEventExporter, type ObservationSourceEventExporterOptions, } from "./exporter/observation-source-event-exporter.js";
21
+ export { attachNextRouterIntegration } from "./integrations/next-router.js";
22
+ export { attachReactRouterIntegration } from "./integrations/react-router.js";
23
+ export type { CluebaseBrowserSdk, CluebaseConsentState, CluebaseGroupTraits, CluebaseGroupType, CluebaseInitOptions, CluebaseSubjectTraits, CluebaseTrackMetrics, CluebaseTrackProperties, InternalMetricsSnapshot, };
24
+ export { createCluebaseBrowserSdk };
25
+ export default cluebase;
package/dist/index.js ADDED
@@ -0,0 +1,158 @@
1
+ import { parseSendableUrl } from "./provider/config.js";
2
+ import { createCluebaseBrowserSdk } from "./provider/sdk.js";
3
+ // SDK は host application 内で singleton として動く。
4
+ // 同じ tab で複数回 import されても 1 instance に統合する。
5
+ const GLOBAL_SDK_KEY = "__cluebase_frontend_sdk_singleton__";
6
+ const globalWithCluebaseSdk = globalThis;
7
+ const existingSdk = globalWithCluebaseSdk[GLOBAL_SDK_KEY];
8
+ const internalSdk = existingSdk ?? createCluebaseBrowserSdk();
9
+ if (!existingSdk) {
10
+ globalWithCluebaseSdk[GLOBAL_SDK_KEY] = internalSdk;
11
+ }
12
+ const reportPublicSdkError = (operation) => {
13
+ try {
14
+ if (typeof console !== "undefined" && typeof console.warn === "function") {
15
+ console.warn(`[cluebase] ${operation} failed`, {
16
+ error_code: "sdk_operation_failed",
17
+ });
18
+ }
19
+ }
20
+ catch {
21
+ // Cluebase は host application を絶対に失敗させない。
22
+ }
23
+ };
24
+ const safeCall = (operation, callback) => {
25
+ try {
26
+ callback();
27
+ }
28
+ catch {
29
+ reportPublicSdkError(operation);
30
+ }
31
+ };
32
+ const safeAsyncCall = async (operation, callback) => {
33
+ try {
34
+ await callback();
35
+ }
36
+ catch {
37
+ reportPublicSdkError(operation);
38
+ }
39
+ };
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
+ const REMOVED_PRIVACY_OPTIONS = {
45
+ allowedValuePaths: "cluebase.init: 'privacy.allowedValuePaths' was removed in " +
46
+ "@genn-inc/cluebase-frontend-sdk 0.0.1. The fields allowed to leave the browser in " +
47
+ "clear text are now chosen in the Cluebase privacy screen and delivered to the " +
48
+ "SDK automatically. Remove 'privacy.allowedValuePaths' from cluebase.init(...) " +
49
+ "and select those fields in the Cluebase privacy screen instead. Until then " +
50
+ "every value stays masked.",
51
+ };
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
+ */
68
+ const validateInitOptions = (options) => {
69
+ const privacy = options.privacy ?? {};
70
+ for (const [name, message] of Object.entries(REMOVED_PRIVACY_OPTIONS)) {
71
+ if (name in privacy) {
72
+ throw new Error(message);
73
+ }
74
+ }
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
+ const missing = REQUIRED_INIT_OPTIONS.filter((name) => {
78
+ const value = options[name];
79
+ return typeof value !== "string" || value.trim() === "";
80
+ });
81
+ if (missing.length > 0) {
82
+ throw new Error(`cluebase.init: ${missing.map((name) => `'${name}'`).join(", ")} ` +
83
+ `${missing.length === 1 ? "is" : "are"} required and must be a non-empty ` +
84
+ "string. Cluebase collects nothing until every required option is set. Pass " +
85
+ "them from your environment, for example " +
86
+ "endpoint=NEXT_PUBLIC_CLUEBASE_API_BASE_URL and " +
87
+ "projectKey=NEXT_PUBLIC_CLUEBASE_PROJECT_KEY.");
88
+ }
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) {
92
+ throw new Error(`cluebase.init: 'endpoint' must be an absolute http:// or https:// URL, but ` +
93
+ `received ${JSON.stringify(options.endpoint)}. An endpoint the SDK cannot ` +
94
+ "send to loses every event just as silently as one that was never passed. " +
95
+ "Pass the Cluebase API base URL from your environment, for example " +
96
+ "endpoint=NEXT_PUBLIC_CLUEBASE_API_BASE_URL.");
97
+ }
98
+ };
99
+ // Public API — namespace `cluebase` の 8 method
100
+ const cluebase = {
101
+ /**
102
+ * SDK を初期化する。 `endpoint` / `projectKey` は必須で、 欠けていれば送信を
103
+ * 黙って捨てずに throw する。
104
+ */
105
+ init(options) {
106
+ // Thrown before `safeCall` so the caller actually sees it; see
107
+ // `validateInitOptions`.
108
+ validateInitOptions(options);
109
+ safeCall("init", () => {
110
+ internalSdk.init(options);
111
+ });
112
+ },
113
+ identify(userId, traits) {
114
+ safeCall("identify", () => {
115
+ internalSdk.identify(userId, traits);
116
+ });
117
+ },
118
+ group(groupType, groupKey, traits) {
119
+ safeCall("group", () => {
120
+ if (groupType !== "organization") {
121
+ throw new Error(`unsupported group type: ${String(groupType)}`);
122
+ }
123
+ internalSdk.setOrganization(groupKey, traits);
124
+ });
125
+ },
126
+ reset() {
127
+ safeCall("reset", () => internalSdk.reset());
128
+ },
129
+ track(eventName, properties, metrics) {
130
+ safeCall("track", () => {
131
+ internalSdk.track(eventName, properties, metrics);
132
+ });
133
+ },
134
+ setConsent(consent) {
135
+ safeCall("setConsent", () => {
136
+ internalSdk.setConsent(consent);
137
+ });
138
+ },
139
+ async flush() {
140
+ await safeAsyncCall("flush", () => internalSdk.flush());
141
+ },
142
+ async close() {
143
+ await safeAsyncCall("close", () => internalSdk.close());
144
+ },
145
+ // 内部 metrics 取得 (= 顧客 debug 用、 production では通常不要)
146
+ getInternalMetrics() {
147
+ return internalSdk.getInternalMetrics();
148
+ },
149
+ getDiagnostics() {
150
+ return internalSdk.getDiagnostics();
151
+ },
152
+ };
153
+ // OTel-core exporter surface for advanced host integrations.
154
+ export { EXPORT_RESULT_FAILED, EXPORT_RESULT_SUCCESS, ObservationSourceEventExporter, } from "./exporter/observation-source-event-exporter.js";
155
+ export { attachNextRouterIntegration } from "./integrations/next-router.js";
156
+ export { attachReactRouterIntegration } from "./integrations/react-router.js";
157
+ export { createCluebaseBrowserSdk };
158
+ export default cluebase;
@@ -0,0 +1,2 @@
1
+ import type { CaptureModuleContext, CaptureStopHandle } from "../provider/types.js";
2
+ export declare const startActionCapture: (context: CaptureModuleContext) => CaptureStopHandle;
@@ -0,0 +1,171 @@
1
+ import { buildLocalTextDescriptor, buildLocalValueDescriptor, } from "../exporter/local-minimization.js";
2
+ import { extractFieldName, extractFieldType, getElementAriaLabel, getElementClassList, getElementLabel, getElementPlaceholder, getElementRole, getElementSectionHeading, isEditableElement, resolveClickDisplayTarget, resolveInteractionScopeDescriptor, resolveStableKeyDescriptor, shouldCaptureClickTarget, } from "./dom.js";
3
+ import { buildPrimaryActionKey } from "./common.js";
4
+ import { getCurrentPageSnapshot } from "./location.js";
5
+ import { buildScreenActionNamingEvidence } from "./naming-evidence.js";
6
+ const ariaToggleRoles = new Set([
7
+ "checkbox",
8
+ "menuitemcheckbox",
9
+ "menuitemradio",
10
+ "radio",
11
+ "switch",
12
+ ]);
13
+ const parseAriaChecked = (value) => {
14
+ if (value === "true")
15
+ return true;
16
+ if (value === "false")
17
+ return false;
18
+ return null;
19
+ };
20
+ const isNativeToggleInput = (element) => element instanceof HTMLInputElement &&
21
+ (element.type === "checkbox" || element.type === "radio");
22
+ const fallbackCssEscape = (value) => value
23
+ .replace(/^\d/, (match) => `\\3${match} `)
24
+ .replace(/[^a-zA-Z0-9_-]/g, (match) => `\\${match}`);
25
+ const ensureCssEscape = () => {
26
+ const globalWithCss = globalThis;
27
+ if (typeof globalWithCss.CSS?.escape === "function") {
28
+ return;
29
+ }
30
+ Object.defineProperty(globalWithCss, "CSS", {
31
+ configurable: true,
32
+ value: {
33
+ ...(globalWithCss.CSS ?? {}),
34
+ escape: fallbackCssEscape,
35
+ },
36
+ });
37
+ };
38
+ const emitElementClicked = (context, target, occurredAtMs) => {
39
+ const displayTarget = resolveClickDisplayTarget(target);
40
+ if (context.shouldIgnoreElement?.(displayTarget)) {
41
+ return;
42
+ }
43
+ if (!shouldCaptureClickTarget(displayTarget)) {
44
+ return;
45
+ }
46
+ const stableKey = resolveStableKeyDescriptor(displayTarget);
47
+ const interactionScope = resolveInteractionScopeDescriptor(displayTarget);
48
+ const interactionId = context.startInteraction?.({
49
+ nowMs: occurredAtMs,
50
+ interactionInitiator: "click",
51
+ interactionStartEventKind: "click",
52
+ targetStableKey: stableKey.stableKey,
53
+ formKey: stableKey.formKey,
54
+ interactionScopeKind: interactionScope?.interactionScopeKind,
55
+ interactionScopeKey: interactionScope?.interactionScopeKey,
56
+ }) ?? null;
57
+ context.onEvent({
58
+ type: "element_clicked",
59
+ occurredAtMs,
60
+ payload: {
61
+ viewId: context.getViewId(),
62
+ ...(interactionId
63
+ ? {
64
+ interactionId,
65
+ interactionInitiator: "click",
66
+ interactionStartEventKind: "click",
67
+ }
68
+ : {}),
69
+ elementTag: displayTarget.tagName.toLowerCase(),
70
+ elementRole: getElementRole(displayTarget),
71
+ primaryActionKey: buildPrimaryActionKey(getCurrentPageSnapshot().screenKey, stableKey.stableKey),
72
+ stableKey: stableKey.stableKey,
73
+ stableKeyQuality: stableKey.stableKeyQuality,
74
+ formKey: stableKey.formKey,
75
+ interactionScopeKind: interactionScope?.interactionScopeKind ?? null,
76
+ interactionScopeKey: interactionScope?.interactionScopeKey ?? null,
77
+ elementId: displayTarget.id || null,
78
+ elementClass: getElementClassList(displayTarget),
79
+ elementLabelMeta: buildLocalTextDescriptor(getElementLabel(displayTarget)),
80
+ ariaLabelMeta: buildLocalTextDescriptor(getElementAriaLabel(displayTarget)),
81
+ placeholderMeta: buildLocalTextDescriptor(getElementPlaceholder(displayTarget)),
82
+ clickTargetText: getElementLabel(displayTarget),
83
+ clickTargetAriaLabel: getElementAriaLabel(displayTarget),
84
+ clickTargetTitle: displayTarget.getAttribute("title") || null,
85
+ fieldName: extractFieldName(displayTarget),
86
+ fieldType: extractFieldType(displayTarget),
87
+ fieldDisplayLabel: getElementLabel(displayTarget),
88
+ fieldAriaLabel: getElementAriaLabel(displayTarget),
89
+ fieldPlaceholder: getElementPlaceholder(displayTarget),
90
+ isEditable: isEditableElement(displayTarget),
91
+ sectionHeading: getElementSectionHeading(displayTarget),
92
+ ...buildScreenActionNamingEvidence(displayTarget),
93
+ },
94
+ });
95
+ maybeEmitAriaToggleChanged(context, displayTarget);
96
+ };
97
+ const maybeEmitAriaToggleChanged = (context, element) => {
98
+ if (isNativeToggleInput(element)) {
99
+ return;
100
+ }
101
+ const role = getElementRole(element);
102
+ if (!ariaToggleRoles.has(role ?? "")) {
103
+ return;
104
+ }
105
+ window.setTimeout(() => {
106
+ try {
107
+ if (!element.isConnected || context.shouldIgnoreElement?.(element)) {
108
+ return;
109
+ }
110
+ const stableKey = resolveStableKeyDescriptor(element);
111
+ const interactionScope = resolveInteractionScopeDescriptor(element);
112
+ const checked = parseAriaChecked(element.getAttribute("aria-checked"));
113
+ context.onEvent({
114
+ type: "toggle_changed",
115
+ occurredAtMs: Date.now(),
116
+ payload: {
117
+ viewId: context.getViewId(),
118
+ elementTag: element.tagName.toLowerCase(),
119
+ elementRole: role,
120
+ stableKey: stableKey.stableKey,
121
+ stableKeyQuality: stableKey.stableKeyQuality,
122
+ formKey: stableKey.formKey,
123
+ interactionScopeKind: interactionScope?.interactionScopeKind ?? null,
124
+ interactionScopeKey: interactionScope?.interactionScopeKey ?? null,
125
+ fieldNameMeta: buildLocalTextDescriptor(extractFieldName(element)),
126
+ elementLabelMeta: buildLocalTextDescriptor(getElementLabel(element)),
127
+ ariaLabelMeta: buildLocalTextDescriptor(getElementAriaLabel(element)),
128
+ placeholderMeta: buildLocalTextDescriptor(getElementPlaceholder(element)),
129
+ fieldName: extractFieldName(element),
130
+ fieldType: extractFieldType(element),
131
+ fieldDisplayLabel: getElementLabel(element),
132
+ fieldAriaLabel: getElementAriaLabel(element),
133
+ fieldPlaceholder: getElementPlaceholder(element),
134
+ checked,
135
+ inputValueMeta: buildLocalValueDescriptor(checked),
136
+ sectionHeading: getElementSectionHeading(element),
137
+ ...buildScreenActionNamingEvidence(element),
138
+ },
139
+ });
140
+ }
141
+ catch (error) {
142
+ context.onInternalError("capture/action:toggle", error);
143
+ }
144
+ }, 0);
145
+ };
146
+ export const startActionCapture = (context) => {
147
+ if (typeof document === "undefined") {
148
+ return { stop: () => undefined };
149
+ }
150
+ ensureCssEscape();
151
+ const clickHandler = (event) => {
152
+ if (context.getConsent() === "denied") {
153
+ return;
154
+ }
155
+ if (!(event.target instanceof Element)) {
156
+ return;
157
+ }
158
+ try {
159
+ emitElementClicked(context, event.target, Date.now());
160
+ }
161
+ catch (error) {
162
+ context.onInternalError("capture/action:click", error);
163
+ }
164
+ };
165
+ document.addEventListener("click", clickHandler, true);
166
+ return {
167
+ stop: () => {
168
+ document.removeEventListener("click", clickHandler, true);
169
+ },
170
+ };
171
+ };
@@ -0,0 +1,4 @@
1
+ /** 部品自身が測った値(文字列など)の属性名。 */
2
+ export declare function cluebaseAuthoredPropertyAttribute(name: string): string;
3
+ /** 部品自身が測った値(数値)の属性名。 */
4
+ export declare function cluebaseAuthoredMetricAttribute(name: string): string;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * 配る部品**自身が測った値**を封筒に載せる時の、属性の名前。
3
+ *
4
+ * 顧客が `track` で付けた項目は `cluebase.properties.<綴り>` /
5
+ * `cluebase.metrics.<綴り>` という封筒で運ばれます。**配る部品も同じ封筒を使う**
6
+ * ため、封筒を開けた受け口から見ると、顧客が名付けた業務項目と、部品が測った
7
+ * 値の区別が付きません。
8
+ *
9
+ * 実際にそれが起きました。分析が `duration_ms` を顧客の業務項目だと解釈し、
10
+ * 利用者へ**「duration_ms を測りました」**と答えました(実測 2026-08-15)。
11
+ *
12
+ * そこで、部品が測った値は封筒の中で**予約された名前**(`cluebase.` で始まる)を
13
+ * 名乗ります。顧客の項目がこの接頭辞を持つことはないため、封筒を開けた時点で
14
+ * 混ざりません。**顧客が偶然 `request_kind` という業務項目を送ってきても、
15
+ * それは顧客のものとして扱われます。**
16
+ *
17
+ * **受け口側で名前を付け替えていません。** 付け替える形にすると、受け口が
18
+ * 「部品が送る綴り」の一覧を持つことになり、その一覧が古くなります
19
+ * (`CLAUDE.md` §0.2)。**名乗るのは名付けた側の責任です。**
20
+ */
21
+ const RESERVED_FIELD_KEY_PREFIX = "cluebase.";
22
+ /** 顧客の項目が運ばれる封筒。部品が測った値も同じ封筒を使う。 */
23
+ const PROPERTY_ENVELOPE_PREFIX = "cluebase.properties.";
24
+ const METRIC_ENVELOPE_PREFIX = "cluebase.metrics.";
25
+ /** 部品自身が測った値(文字列など)の属性名。 */
26
+ export function cluebaseAuthoredPropertyAttribute(name) {
27
+ return `${PROPERTY_ENVELOPE_PREFIX}${RESERVED_FIELD_KEY_PREFIX}${name}`;
28
+ }
29
+ /** 部品自身が測った値(数値)の属性名。 */
30
+ export function cluebaseAuthoredMetricAttribute(name) {
31
+ return `${METRIC_ENVELOPE_PREFIX}${RESERVED_FIELD_KEY_PREFIX}${name}`;
32
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Shared instrumentation helpers used across multiple capture modules
3
+ * (network, performance, navigation, etc.).
4
+ *
5
+ * The most important contract here is URL sanitization for any payload
6
+ * that may carry the user's current browsing URL. The page URL must never
7
+ * be emitted with query string intact, because OAuth callback /
8
+ * password-reset / magic-link flows place secret tokens in the query
9
+ * string. Any instrumentation module that needs a "page URL" field on
10
+ * its outgoing event payload MUST go through `currentPageUrl()` here.
11
+ *
12
+ * Previously `instrumentation/performance.ts` emitted
13
+ * `pageUrl: window.location.href` directly while
14
+ * `instrumentation/network.ts` had its own internal stripping helper.
15
+ * This module unifies the behavior so all callers share a single
16
+ * implementation and a single test surface.
17
+ */
18
+ /**
19
+ * Resolves `value` to a URL, falling back to the current document
20
+ * location for relative URLs. Returns `null` if neither parsing succeeds.
21
+ */
22
+ export declare const resolveUrlAgainstCurrentLocation: (value: string) => URL | null;
23
+ /**
24
+ * Strips the query string and hash from the given URL, returning
25
+ * `origin + pathname` only. Falls back to a `split("?")` form if URL
26
+ * parsing fails so callers never receive a query-bearing string.
27
+ *
28
+ * The query string is the primary leak surface for OAuth callbacks
29
+ * (`?code=...&state=...`) and password reset links (`?token=...`).
30
+ */
31
+ export declare const stripQueryFromUrl: (url: string) => string;
32
+ /**
33
+ * Returns the current page URL with all query parameters and hash
34
+ * fragments stripped. Returns `null` if `globalThis.location` is not
35
+ * available (e.g., non-browser environments).
36
+ *
37
+ * THIS IS THE ONLY APPROVED WAY TO POPULATE A `pageUrl` FIELD ON ANY
38
+ * INSTRUMENTATION EVENT PAYLOAD. Do not use `window.location.href`
39
+ * directly.
40
+ */
41
+ export declare const currentPageUrl: () => string | null;
42
+ /**
43
+ * Single sentinel marker used by Cluebase-owned History API wrappers. OTel-owned
44
+ * navigation instrumentation may patch history independently and must remain
45
+ * the primary navigation source.
46
+ */
47
+ export declare const HISTORY_WRAPPED_MARKER: "__cluebaseHistoryWrapped";
48
+ /**
49
+ * Type guard for a history.pushState / replaceState function that
50
+ * carries the unified Cluebase sentinel marker. Use this in any module
51
+ * that monkey-patches the History API.
52
+ */
53
+ export type HistoryWrappedFn<T extends History["pushState"] | History["replaceState"] = History["pushState"] | History["replaceState"]> = T & {
54
+ [HISTORY_WRAPPED_MARKER]?: true;
55
+ };
56
+ /**
57
+ * Returns true if either history.pushState or history.replaceState
58
+ * is already Cluebase-wrapped.
59
+ */
60
+ /**
61
+ * 差し替え済みかを見る側が、履歴の口に実際に求めるもの。
62
+ *
63
+ * **`History` 全体を求めていました。** ここで読むのは 2 つだけなので、試験の
64
+ * 作り物は `as unknown as History` で嘘をつくしかない状態でした(`tests/README.md` §10)。
65
+ */
66
+ export type WrappableHistory = Pick<History, "pushState" | "replaceState">;
67
+ export declare const isHistoryAlreadyCluebaseWrapped: (historyApi: WrappableHistory) => boolean;
68
+ /**
69
+ * 画面操作 1 種類を指す安定キー。意味層は操作の identity 単位で行動名を付けるため、
70
+ * 通信経路 (method / path) を持たない画面操作にも同じ粒度の識別子が要る。
71
+ * 画面と操作対象の安定キーの組で「どの画面のどの操作か」を表す。
72
+ */
73
+ export declare const buildPrimaryActionKey: (screenKey: string | null | undefined, targetStableKey: string | null | undefined) => string | null;