@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
@@ -35,7 +35,7 @@ const ensureCssEscape = () => {
35
35
  },
36
36
  });
37
37
  };
38
- const emitElementClicked = (context, target, occurredAtMs) => {
38
+ const emitElementClicked = (context, target, occurredAtMs, event) => {
39
39
  const displayTarget = resolveClickDisplayTarget(target);
40
40
  if (context.shouldIgnoreElement?.(displayTarget)) {
41
41
  return;
@@ -92,6 +92,7 @@ const emitElementClicked = (context, target, occurredAtMs) => {
92
92
  ...buildScreenActionNamingEvidence(displayTarget),
93
93
  },
94
94
  });
95
+ context.bindEventContext?.(event);
95
96
  maybeEmitAriaToggleChanged(context, displayTarget);
96
97
  };
97
98
  const maybeEmitAriaToggleChanged = (context, element) => {
@@ -156,7 +157,7 @@ export const startActionCapture = (context) => {
156
157
  return;
157
158
  }
158
159
  try {
159
- emitElementClicked(context, event.target, Date.now());
160
+ emitElementClicked(context, event.target, Date.now(), event);
160
161
  }
161
162
  catch (error) {
162
163
  context.onInternalError("capture/action:click", error);
@@ -1,4 +1,2 @@
1
- /** 部品自身が測った値(文字列など)の属性名。 */
2
1
  export declare function cluebaseAuthoredPropertyAttribute(name: string): string;
3
- /** 部品自身が測った値(数値)の属性名。 */
4
2
  export declare function cluebaseAuthoredMetricAttribute(name: string): string;
@@ -1,32 +1,9 @@
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
1
  const RESERVED_FIELD_KEY_PREFIX = "cluebase.";
22
- /** 顧客の項目が運ばれる封筒。部品が測った値も同じ封筒を使う。 */
23
2
  const PROPERTY_ENVELOPE_PREFIX = "cluebase.properties.";
24
3
  const METRIC_ENVELOPE_PREFIX = "cluebase.metrics.";
25
- /** 部品自身が測った値(文字列など)の属性名。 */
26
4
  export function cluebaseAuthoredPropertyAttribute(name) {
27
5
  return `${PROPERTY_ENVELOPE_PREFIX}${RESERVED_FIELD_KEY_PREFIX}${name}`;
28
6
  }
29
- /** 部品自身が測った値(数値)の属性名。 */
30
7
  export function cluebaseAuthoredMetricAttribute(name) {
31
8
  return `${METRIC_ENVELOPE_PREFIX}${RESERVED_FIELD_KEY_PREFIX}${name}`;
32
9
  }
@@ -1,73 +1,10 @@
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
1
  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
2
  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
3
  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
4
  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
5
  export type HistoryWrappedFn<T extends History["pushState"] | History["replaceState"] = History["pushState"] | History["replaceState"]> = T & {
54
6
  [HISTORY_WRAPPED_MARKER]?: true;
55
7
  };
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
8
  export type WrappableHistory = Pick<History, "pushState" | "replaceState">;
67
9
  export declare const isHistoryAlreadyCluebaseWrapped: (historyApi: WrappableHistory) => boolean;
68
- /**
69
- * 画面操作 1 種類を指す安定キー。意味層は操作の identity 単位で行動名を付けるため、
70
- * 通信経路 (method / path) を持たない画面操作にも同じ粒度の識別子が要る。
71
- * 画面と操作対象の安定キーの組で「どの画面のどの操作か」を表す。
72
- */
73
10
  export declare const buildPrimaryActionKey: (screenKey: string | null | undefined, targetStableKey: string | null | undefined) => string | null;
@@ -1,24 +1,3 @@
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
1
  export const resolveUrlAgainstCurrentLocation = (value) => {
23
2
  try {
24
3
  return new URL(value);
@@ -38,14 +17,6 @@ export const resolveUrlAgainstCurrentLocation = (value) => {
38
17
  return null;
39
18
  }
40
19
  };
41
- /**
42
- * Strips the query string and hash from the given URL, returning
43
- * `origin + pathname` only. Falls back to a `split("?")` form if URL
44
- * parsing fails so callers never receive a query-bearing string.
45
- *
46
- * The query string is the primary leak surface for OAuth callbacks
47
- * (`?code=...&state=...`) and password reset links (`?token=...`).
48
- */
49
20
  export const stripQueryFromUrl = (url) => {
50
21
  try {
51
22
  const resolvedUrl = resolveUrlAgainstCurrentLocation(url);
@@ -58,33 +29,14 @@ export const stripQueryFromUrl = (url) => {
58
29
  return url.split("?")[0] ?? url;
59
30
  }
60
31
  };
61
- /**
62
- * Returns the current page URL with all query parameters and hash
63
- * fragments stripped. Returns `null` if `globalThis.location` is not
64
- * available (e.g., non-browser environments).
65
- *
66
- * THIS IS THE ONLY APPROVED WAY TO POPULATE A `pageUrl` FIELD ON ANY
67
- * INSTRUMENTATION EVENT PAYLOAD. Do not use `window.location.href`
68
- * directly.
69
- */
70
32
  export const currentPageUrl = () => {
71
33
  const href = globalThis.location?.href;
72
34
  return href ? stripQueryFromUrl(href) : null;
73
35
  };
74
- /**
75
- * Single sentinel marker used by Cluebase-owned History API wrappers. OTel-owned
76
- * navigation instrumentation may patch history independently and must remain
77
- * the primary navigation source.
78
- */
79
36
  export const HISTORY_WRAPPED_MARKER = "__cluebaseHistoryWrapped";
80
37
  export const isHistoryAlreadyCluebaseWrapped = (historyApi) => {
81
38
  const push = historyApi.pushState;
82
39
  const replace = historyApi.replaceState;
83
40
  return Boolean(push[HISTORY_WRAPPED_MARKER] || replace[HISTORY_WRAPPED_MARKER]);
84
41
  };
85
- /**
86
- * 画面操作 1 種類を指す安定キー。意味層は操作の identity 単位で行動名を付けるため、
87
- * 通信経路 (method / path) を持たない画面操作にも同じ粒度の識別子が要る。
88
- * 画面と操作対象の安定キーの組で「どの画面のどの操作か」を表す。
89
- */
90
42
  export const buildPrimaryActionKey = (screenKey, targetStableKey) => screenKey && targetStableKey ? `${screenKey}#${targetStableKey}` : null;
@@ -5,40 +5,15 @@ export declare const getElementRole: (element: Element) => string | null;
5
5
  export declare const getElementPlaceholder: (element: Element) => string | null;
6
6
  export declare const getElementAriaLabel: (element: Element) => string | null;
7
7
  export declare const getElementSectionHeading: (element: Element) => string | null;
8
- /** どの段が名前を決めたか。伏せられていても残る**機械が決めた値**です。 */
9
8
  export type AccessibleNameSource = "aria_labelledby" | "aria_label" | "native_label" | "content" | "title";
10
9
  export type AccessibleName = {
11
10
  name: string;
12
11
  source: AccessibleNameSource;
13
12
  };
14
- /**
15
- * 要素の名前を **1 本**に決めます。
16
- *
17
- * 順序は W3C の名前算出 (Accessible Name Computation) に従います。
18
- * `aria-labelledby` → `aria-label` → 対応ラベル / `alt` → 要素内の文言 → `title`。
19
- * **自前の優先順を作ってはいけません。** 顧客の画面を読み上げる道具が名乗る名前と
20
- * 食い違う名前を保管すると、行動の名付けが「利用者が見ている名前」から離れます。
21
- *
22
- * 生の候補を並べて持たないのも同じ理由です。どれを採るかを後段でもう一度決めると、
23
- * 決め方が 2 つになります。
24
- */
25
13
  export declare const getAccessibleName: (element: Element) => AccessibleName | null;
26
14
  export declare const getElementLabel: (element: Element) => string | null;
27
- /**
28
- * 押された要素の**祖先の並び**。`main > form[role=search] > button` の形で返します。
29
- *
30
- * 綴りは**機械が決めた値だけ**で作ります (要素の種類と役割)。顧客が書いた文言を
31
- * 混ぜないので、文言を伏せた状態でも「画面のどのまとまりの中の操作か」が残ります。
32
- */
33
15
  export declare const getAncestorChain: (element: Element) => string | null;
34
- /** 画面の題。頁が何の画面かを表す、顧客が書いた文言です。 */
35
16
  export declare const getDocumentTitle: (element: Element) => string | null;
36
- /**
37
- * 押した後に行く先。`href` を持つ要素はその値を返します。
38
- *
39
- * 問い合わせ文字列は落とします。**そこに載るのは利用者ごとに違う値**で、
40
- * 名前に入れると行動の型が利用者の数だけ増えます。
41
- */
42
17
  export declare const getNavigationHref: (element: Element) => string | null;
43
18
  export declare const resolveClickDisplayTarget: (element: Element) => Element;
44
19
  export declare const shouldCaptureClickTarget: (element: Element) => boolean;
@@ -82,19 +82,6 @@ const normalizeHumanText = (value) => {
82
82
  const normalized = value?.replace(/\s+/g, " ").trim() ?? "";
83
83
  return normalized ? normalized.slice(0, 120) : null;
84
84
  };
85
- const getSelectSelectionLabel = (element) => {
86
- const labels = Array.from(element.selectedOptions)
87
- .map((option) => normalizeHumanText(option.label || option.textContent))
88
- .filter((value) => Boolean(value));
89
- const uniqueLabels = Array.from(new Set(labels));
90
- if (uniqueLabels.length === 0) {
91
- return null;
92
- }
93
- if (uniqueLabels.length <= 3) {
94
- return uniqueLabels.join(", ");
95
- }
96
- return `${uniqueLabels.slice(0, 3).join(", ")} +${uniqueLabels.length - 3}`;
97
- };
98
85
  const SECTION_CONTAINER_SELECTOR = [
99
86
  "section",
100
87
  "article",
@@ -180,8 +167,11 @@ const getAssociatedLabel = (element) => {
180
167
  return null;
181
168
  };
182
169
  const readElementContentLabel = (element) => {
183
- if (element instanceof HTMLSelectElement) {
184
- return getSelectSelectionLabel(element);
170
+ if (element instanceof HTMLInputElement ||
171
+ element instanceof HTMLTextAreaElement ||
172
+ element instanceof HTMLSelectElement ||
173
+ (element instanceof HTMLElement && element.isContentEditable)) {
174
+ return null;
185
175
  }
186
176
  const ownText = Array.from(element.childNodes)
187
177
  .filter((node) => node.nodeType === Node.TEXT_NODE)
@@ -237,17 +227,6 @@ const readNativeHostLabel = (element) => {
237
227
  }
238
228
  return null;
239
229
  };
240
- /**
241
- * 要素の名前を **1 本**に決めます。
242
- *
243
- * 順序は W3C の名前算出 (Accessible Name Computation) に従います。
244
- * `aria-labelledby` → `aria-label` → 対応ラベル / `alt` → 要素内の文言 → `title`。
245
- * **自前の優先順を作ってはいけません。** 顧客の画面を読み上げる道具が名乗る名前と
246
- * 食い違う名前を保管すると、行動の名付けが「利用者が見ている名前」から離れます。
247
- *
248
- * 生の候補を並べて持たないのも同じ理由です。どれを採るかを後段でもう一度決めると、
249
- * 決め方が 2 つになります。
250
- */
251
230
  export const getAccessibleName = (element) => {
252
231
  const labelledBy = readAriaLabelledBy(element);
253
232
  if (labelledBy) {
@@ -272,16 +251,8 @@ export const getAccessibleName = (element) => {
272
251
  return null;
273
252
  };
274
253
  export const getElementLabel = (element) => getAccessibleName(element)?.name ?? null;
275
- // 祖先の並びは深さと長さの両方で止めます。深い画面ほど並びが伸びるため、
276
- // 深さだけで止めると 1 件あたりの大きさが顧客の画面構造に引きずられます。
277
254
  const MAX_ANCESTOR_CHAIN_DEPTH = 6;
278
255
  const MAX_ANCESTOR_CHAIN_LENGTH = 200;
279
- /**
280
- * 押された要素の**祖先の並び**。`main > form[role=search] > button` の形で返します。
281
- *
282
- * 綴りは**機械が決めた値だけ**で作ります (要素の種類と役割)。顧客が書いた文言を
283
- * 混ぜないので、文言を伏せた状態でも「画面のどのまとまりの中の操作か」が残ります。
284
- */
285
256
  export const getAncestorChain = (element) => {
286
257
  const segments = [];
287
258
  let current = element.parentElement;
@@ -299,14 +270,7 @@ export const getAncestorChain = (element) => {
299
270
  }
300
271
  return segments.join(" > ").slice(0, MAX_ANCESTOR_CHAIN_LENGTH);
301
272
  };
302
- /** 画面の題。頁が何の画面かを表す、顧客が書いた文言です。 */
303
273
  export const getDocumentTitle = (element) => normalizeHumanText(element.ownerDocument?.title ?? null);
304
- /**
305
- * 押した後に行く先。`href` を持つ要素はその値を返します。
306
- *
307
- * 問い合わせ文字列は落とします。**そこに載るのは利用者ごとに違う値**で、
308
- * 名前に入れると行動の型が利用者の数だけ増えます。
309
- */
310
274
  export const getNavigationHref = (element) => {
311
275
  const href = element.getAttribute("href");
312
276
  if (!href?.trim()) {
@@ -23,8 +23,6 @@ export const startDragCapture = (context) => {
23
23
  if (typeof document === "undefined") {
24
24
  return { stop: () => undefined };
25
25
  }
26
- // Drop payload summaries are retained as Cluebase-only UX evidence; accepted
27
- // browser OTel instrumentation does not expose this committed drop signal.
28
26
  const dropHandler = (event) => {
29
27
  if (context.getConsent() === "denied") {
30
28
  return;
@@ -68,6 +66,7 @@ export const startDragCapture = (context) => {
68
66
  dragTypes: dragPayload.dragTypes,
69
67
  },
70
68
  });
69
+ context.bindEventContext?.(event);
71
70
  }
72
71
  catch (error) {
73
72
  context.onInternalError("capture/drag:drop", error);
@@ -49,8 +49,6 @@ export const startFormCapture = (context) => {
49
49
  if (typeof document === "undefined") {
50
50
  return { stop: () => undefined };
51
51
  }
52
- // OTel browser user-action covers the submitter click; this retains
53
- // Cluebase-only form validation outcome evidence that OTel does not provide.
54
52
  const submitHandler = (event) => {
55
53
  if (context.getConsent() === "denied") {
56
54
  return;
@@ -118,6 +116,7 @@ export const startFormCapture = (context) => {
118
116
  ...buildScreenActionNamingEvidence(submitter ?? target),
119
117
  },
120
118
  });
119
+ context.bindEventContext?.(event);
121
120
  }
122
121
  catch (error) {
123
122
  context.onInternalError("capture/form:submit", error);
@@ -24,9 +24,7 @@ export const startInputCapture = (context) => {
24
24
  return { stop: () => undefined };
25
25
  }
26
26
  const dirtyControls = new Set();
27
- // OTel user-action covers discrete clicks; committed field values,
28
- // selections, toggles, and file counts are Cluebase-only UX evidence.
29
- const emitCommit = (target) => {
27
+ const emitCommit = (target, sourceEvent) => {
30
28
  if (context.getConsent() === "denied") {
31
29
  return;
32
30
  }
@@ -73,24 +71,24 @@ export const startInputCapture = (context) => {
73
71
  inputValueMeta: buildLocalValueDescriptor(target.value),
74
72
  },
75
73
  });
74
+ if (sourceEvent)
75
+ context.bindEventContext?.(sourceEvent);
76
76
  return;
77
77
  }
78
78
  if (target instanceof HTMLSelectElement) {
79
79
  const selectedOptions = Array.from(target.selectedOptions);
80
80
  const selectedValues = selectedOptions.map((option) => option.value);
81
- const selectedOptionLabels = selectedOptions
82
- .map((option) => (option.label || option.textContent || "").replace(/\s+/g, " ").trim())
83
- .filter((value) => value.length > 0);
84
81
  context.onEvent({
85
82
  type: "selection_committed",
86
83
  occurredAtMs: Date.now(),
87
84
  payload: {
88
85
  ...commonPayload,
89
86
  selectedCount: selectedValues.length,
90
- selectedOptionLabels,
91
87
  selectedValueMeta: buildLocalValueDescriptor(selectedValues),
92
88
  },
93
89
  });
90
+ if (sourceEvent)
91
+ context.bindEventContext?.(sourceEvent);
94
92
  return;
95
93
  }
96
94
  if (target instanceof HTMLInputElement && target.type === "file") {
@@ -107,6 +105,8 @@ export const startInputCapture = (context) => {
107
105
  fileDescriptors,
108
106
  },
109
107
  });
108
+ if (sourceEvent)
109
+ context.bindEventContext?.(sourceEvent);
110
110
  return;
111
111
  }
112
112
  context.onEvent({
@@ -117,6 +117,8 @@ export const startInputCapture = (context) => {
117
117
  inputValueMeta: buildLocalValueDescriptor(getElementValue(target)),
118
118
  },
119
119
  });
120
+ if (sourceEvent)
121
+ context.bindEventContext?.(sourceEvent);
120
122
  if (isDeferredCommitElement(target)) {
121
123
  dirtyControls.delete(target);
122
124
  }
@@ -146,7 +148,7 @@ export const startInputCapture = (context) => {
146
148
  if (!isInputCaptureElement(target)) {
147
149
  return;
148
150
  }
149
- emitCommit(target);
151
+ emitCommit(target, event);
150
152
  };
151
153
  const flushDirtyControls = () => {
152
154
  for (const target of Array.from(dirtyControls)) {
@@ -1,15 +1,3 @@
1
- /**
2
- * 画面上の操作が**何なのか**を後段が言い当てるための材料。
3
- *
4
- * 名付ける相手は、押された要素そのものを見られません。ここで採らなかったものは
5
- * 「無かった」のと同じになります。実測では、要素の鍵だけを渡した押下が
6
- * `execute_primary_action` / 確信度 0.18 になりました。
7
- *
8
- * 文言 (名前・題・行き先) は顧客が書いたもので、個人に結び付く値が混ざり得ます。
9
- * 伏せるかどうかは受付側が顧客の設定 1 本で決めるので、ここでは伏せません。
10
- * **文言が伏せられても、役割・祖先の並び・安定した識別子だけで慎重な一般名が
11
- * 作れる**ように、機械が決めた値を必ず一緒に載せます。
12
- */
13
1
  export type ScreenActionNamingEvidence = {
14
2
  accessibleName: string | null;
15
3
  accessibleNameSource: string | null;
@@ -71,8 +71,6 @@ export const startNavigationCapture = (context) => {
71
71
  viewId: previousViewId,
72
72
  screenKey: currentSnapshot.screenKey,
73
73
  path: currentSnapshot.path,
74
- // 離脱イベントは遷移後に送られるため、周囲の文脈から URL を取ると
75
- // 次の画面を指す。離れた画面自身の URL を payload で名乗る。
76
74
  urlCanonical: currentSnapshot.urlCanonical,
77
75
  timeOnPageMs: Math.max(0, now - viewEnteredAtMs),
78
76
  },
@@ -21,6 +21,7 @@ export type RequestTraceContext = {
21
21
  parentSpanId: string | null;
22
22
  requestId: string;
23
23
  requestSpanId: string;
24
+ actionId: string | null;
24
25
  interactionId: string;
25
26
  interactionInitiator?: string | null;
26
27
  interactionStartEventKind?: string | null;
@@ -54,6 +55,7 @@ export type NetworkRequestMeta = {
54
55
  parentSpanId: string | null;
55
56
  requestId: string;
56
57
  requestSpanId: string;
58
+ actionId: string | null;
57
59
  interactionId: string;
58
60
  interactionInitiator: string | null;
59
61
  interactionStartEventKind: string | null;
@@ -61,7 +63,6 @@ export type NetworkRequestMeta = {
61
63
  viewId: string | null;
62
64
  requestHeaders: HeaderRecord;
63
65
  queryKeys: string[];
64
- /** 値を持たない入れ物。顧客が名指しした項目だけ、方針が届いた時に値を持つ。 */
65
66
  requestParametersSnapshot: ParameterSnapshotBundle;
66
67
  requestMetadataSummary: RequestMetadataSummary;
67
68
  requestShapeEvidence: RequestShapeEvidence;
@@ -97,7 +98,6 @@ export declare const createRequestMeta: (input: {
97
98
  defaultRequestKind: "fetch" | "xhr";
98
99
  traceContext: RequestTraceContext;
99
100
  startedAtMs?: number;
100
- /** 顧客が init で足した落とす項目名。既定の落とす表は下の組み立てが自分で読む。 */
101
101
  deniedKeys?: readonly string[];
102
102
  }) => NetworkRequestMeta;
103
103
  export {};
@@ -236,8 +236,6 @@ export const createRequestMeta = (input) => {
236
236
  const sanitizedRequestHeaders = sanitizeHeaders(input.requestHeaders);
237
237
  const normalizedRequestBody = normalizeBody(input.body);
238
238
  const requestShape = resolveRequestShape(input.rawUrl);
239
- // 顧客の値が入る面。**既定は全部伏せる。**顧客が画面で名指しした項目だけを
240
- // `allowedPaths` が平文にする。方針がまだ届いていない間は空なので、伏せた側へ倒れる。
241
239
  const requestParametersSnapshot = buildParameterSnapshotBundle({ query: queryValues, body: normalizedRequestBody }, {
242
240
  allowedPaths: allowedValuePathsFor({
243
241
  method: input.method,
@@ -264,6 +262,7 @@ export const createRequestMeta = (input) => {
264
262
  parentSpanId: traceContext.parentSpanId,
265
263
  requestId: traceContext.requestId,
266
264
  requestSpanId: traceContext.requestSpanId,
265
+ actionId: traceContext.actionId ?? null,
267
266
  interactionId,
268
267
  interactionInitiator: traceContext.interactionInitiator ??
269
268
  (interactionId ? null : "background"),
@@ -1,11 +1,4 @@
1
1
  import { type Span } from "@opentelemetry/api";
2
- /**
3
- * 属性を書き込む側が、span に実際に求めるもの。
4
- *
5
- * **`Span` 全体(11 member)を求めていました。** ここで使うのは 2 つだけなので、
6
- * 試験の作り物は残りを持てず `as unknown as Span` で嘘をつくしかない状態でした
7
- * (`tests/README.md` §10)。3 つ目を使い始めたら、ここが変わって作り物が落ちます。
8
- */
9
2
  export type AttributeWritableSpan = {
10
3
  [K in "setAttribute" | "setStatus"]: (...args: Parameters<Span[K]>) => unknown;
11
4
  };
@@ -56,6 +56,7 @@ const setRequestSpanAttributes = (span, meta, eventName, status, observationCont
56
56
  span.setAttribute("cluebase.path_template", meta.pathTemplate);
57
57
  setOptionalString(span, "cluebase.target_service", meta.targetService);
58
58
  setOptionalString(span, "cluebase.parent_span_id", meta.parentSpanId);
59
+ setOptionalString(span, "cluebase.action.id", meta.actionId);
59
60
  setOptionalString(span, "cluebase.interaction.id", meta.interactionId);
60
61
  setOptionalString(span, "cluebase.interaction.initiator", meta.interactionInitiator);
61
62
  setOptionalString(span, "cluebase.interaction.start_event_kind", meta.interactionStartEventKind);
@@ -79,8 +80,6 @@ const setRequestSpanAttributes = (span, meta, eventName, status, observationCont
79
80
  setOptionalStringArray(span, cluebaseAuthoredPropertyAttribute("graphql_top_level_fields"), requestEvidence.graphqlTopLevelFields);
80
81
  setOptionalNumber(span, cluebaseAuthoredMetricAttribute("request_size_bytes"), meta.requestSizeBytes);
81
82
  span.setAttribute(cluebaseAuthoredPropertyAttribute("request_field_paths"), meta.queryKeys);
82
- // 顧客の値が入る面。**値は入れ物の中で伏せられている。**顧客が画面で名指しした
83
- // 項目だけが値を持ち、その項目名は下の控えに並ぶ。
84
83
  setJsonRecordAttribute(span, "cluebase.request_parameters_snapshot_json", meta.requestParametersSnapshot.snapshot);
85
84
  setJsonRecordAttribute(span, "cluebase.request_metadata_json", {
86
85
  ...meta.requestMetadataSummary,
@@ -142,7 +141,6 @@ export const setResponseSpanAttributes = (span, meta, input) => {
142
141
  deniedKeys: input.deniedKeys ?? [],
143
142
  });
144
143
  setJsonRecordAttribute(span, "cluebase.response_parameters_snapshot_json", responseParametersSnapshot.snapshot);
145
- // 平文で残した項目の控え。面をまたいで 1 本にする。空なら属性を出さない。
146
144
  setOptionalStringArray(span, "cluebase.privacy.allowed_keys", [
147
145
  ...new Set([
148
146
  ...meta.requestParametersSnapshot.allowedValueKeys,
@@ -18,7 +18,6 @@ let fetchInstrumentation = null;
18
18
  let xhrInstrumentation = null;
19
19
  let cluebaseNetworkPropagatorRegistered = false;
20
20
  const REQUEST_LIFECYCLE_ORPHAN_TIMEOUT_MS = 30000;
21
- // OTel fetch waits 300ms for browser resource timing before ending spans.
22
21
  const FETCH_SPAN_END_SETTLE_MS = 350;
23
22
  const pendingFetchSpanEndWaits = new Set();
24
23
  class ManagedFetchInstrumentation extends FetchInstrumentation {
@@ -70,6 +69,9 @@ const toTraceHeaders = (capture, traceContext) => {
70
69
  if (traceContext.parentSpanId) {
71
70
  headers["x-cluebase-parent-span-id"] = traceContext.parentSpanId;
72
71
  }
72
+ if (traceContext.actionId) {
73
+ headers["x-cluebase-action-id"] = traceContext.actionId;
74
+ }
73
75
  if (traceContext.interactionId) {
74
76
  headers["x-cluebase-interaction-id"] = traceContext.interactionId;
75
77
  }
@@ -99,6 +101,7 @@ const traceContextFromMeta = (meta) => ({
99
101
  parentSpanId: meta.parentSpanId,
100
102
  requestId: meta.requestId,
101
103
  requestSpanId: meta.requestSpanId,
104
+ actionId: meta.actionId,
102
105
  interactionId: meta.interactionId,
103
106
  interactionInitiator: meta.interactionInitiator,
104
107
  interactionStartEventKind: meta.interactionStartEventKind,
@@ -108,6 +111,7 @@ const CLUEBASE_NETWORK_PROPAGATION_FIELDS = [
108
111
  "x-cluebase-trace-id",
109
112
  "x-cluebase-request-id",
110
113
  "x-cluebase-request-span-id",
114
+ "x-cluebase-action-id",
111
115
  "x-cluebase-parent-span-id",
112
116
  "x-cluebase-interaction-id",
113
117
  "x-cluebase-origin-frontend-interaction-id",
@@ -471,6 +475,7 @@ export const startNetworkCapture = (context, options = {}) => {
471
475
  parentSpanId: null,
472
476
  requestId: generateRequestId(),
473
477
  requestSpanId: generateRequestSpanId(),
478
+ actionId: null,
474
479
  interactionId: generateInteractionId(),
475
480
  interactionInitiator: null,
476
481
  interactionStartEventKind: null,
@@ -4,8 +4,6 @@ export const startPageCapture = (context) => {
4
4
  return { stop: () => undefined };
5
5
  }
6
6
  let pageEnteredAtMs = Date.now();
7
- // Page-view events are emitted from OTel navigation/document-load signals.
8
- // This module only keeps Cluebase-specific time-on-page evidence.
9
7
  const handlePageLeave = () => {
10
8
  if (context.getConsent() === "denied") {
11
9
  return;
@@ -92,7 +92,6 @@ const observeEntryType = (observers, entryType, callback) => {
92
92
  observers.push(observer);
93
93
  }
94
94
  catch {
95
- // Some browsers expose an entry type but still reject buffered observation.
96
95
  }
97
96
  };
98
97
  const readPerformanceNavigation = () => {
@@ -233,9 +232,6 @@ export const startPerformanceCapture = (context) => {
233
232
  occurredAtMs: Date.now(),
234
233
  payload: {
235
234
  viewId,
236
- // must strip query string to avoid leaking OAuth
237
- // callback / password-reset / magic-link tokens that may
238
- // live in `window.location.href`. See `instrumentation/common.ts`.
239
235
  pageUrl: currentPageUrl() ?? "",
240
236
  collectorName: "performance_rum",
241
237
  aggregationKind: "per_view_window",
@@ -305,9 +301,6 @@ export const startPerformanceCapture = (context) => {
305
301
  window.addEventListener("pointerdown", interactionHandler, true);
306
302
  window.addEventListener("pagehide", pageHideHandler, true);
307
303
  document.addEventListener("visibilitychange", visibilityHandler, true);
308
- // Long tasks and resource load failures are retained as Cluebase summary-only
309
- // UX evidence because the accepted browser OTel modules above cover
310
- // document load and Web Vitals, not this per-view aggregate.
311
304
  observeEntryType(observers, "longtask", (list) => {
312
305
  for (const entry of list.getEntries()) {
313
306
  const nearInteraction = state.lastInteractionAtMs !== null &&
@@ -18,9 +18,6 @@ export const attachReactRouterIntegration = (options) => {
18
18
  options.onRouteChange(lastPath, nextPath);
19
19
  lastPath = nextPath;
20
20
  };
21
- // Idempotent guard for Cluebase-owned History API wrappers. OTel navigation
22
- // instrumentation may patch history separately and remains the primary
23
- // navigation source.
24
21
  const sentinelPushState = history.pushState;
25
22
  const sentinelReplaceState = history.replaceState;
26
23
  const alreadyWrapped = isHistoryAlreadyCluebaseWrapped(history);
@@ -1,10 +1,3 @@
1
- /**
2
- * SDK-internal diagnostic logger for the Frontend SDK.
3
- *
4
- * Mirror of Node SDK ``internal-log.ts`` and Python SDK ``_internal_log.py``.
5
- * Exposes a single configurable {@link InternalLogger}. Default is a no-op
6
- * so the SDK does not write to the host page's console.
7
- */
8
1
  export type InternalLogContext = {
9
2
  [key: string]: string | number | boolean | null;
10
3
  };