@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.
- package/README.md +11 -9
- package/dist/_generated/correlation-attribute-keys.js +4 -0
- package/dist/_generated/otel-span-conversion.d.ts +0 -34
- package/dist/_generated/otel-span-conversion.js +0 -32
- package/dist/context/action-context.d.ts +39 -0
- package/dist/context/action-context.js +298 -0
- package/dist/context/browser-storage.d.ts +0 -12
- package/dist/context/browser-storage.js +0 -13
- package/dist/context/identity.js +0 -4
- package/dist/context/organization.d.ts +0 -4
- package/dist/context/organization.js +0 -7
- package/dist/context/session.js +0 -3
- package/dist/context/tab.js +0 -5
- package/dist/context/trace.d.ts +13 -1
- package/dist/context/trace.js +119 -21
- package/dist/exporter/batch.d.ts +0 -12
- package/dist/exporter/batch.js +0 -27
- package/dist/exporter/browser-outbox.js +0 -17
- package/dist/exporter/client.d.ts +0 -15
- package/dist/exporter/client.js +0 -43
- package/dist/exporter/delivery-response-contract.js +0 -6
- package/dist/exporter/delivery-response.d.ts +0 -16
- package/dist/exporter/delivery-response.js +0 -19
- package/dist/exporter/frontend-resource.d.ts +0 -9
- package/dist/exporter/frontend-resource.js +0 -9
- package/dist/exporter/observation-source-event-exporter.d.ts +0 -28
- package/dist/exporter/observation-source-event-exporter.js +0 -18
- package/dist/exporter/parameter-snapshot.js +1 -24
- package/dist/exporter/raw-event-to-source-signal.js +18 -11
- package/dist/exporter/sanitize.d.ts +1 -12
- package/dist/exporter/sanitize.js +6 -39
- package/dist/exporter/source-event.d.ts +2 -0
- package/dist/exporter/span-to-source-signal.js +7 -3
- package/dist/exporter/text-fingerprint.d.ts +0 -8
- package/dist/exporter/text-fingerprint.js +0 -8
- package/dist/index.d.ts +0 -4
- package/dist/index.js +9 -39
- package/dist/instrumentation/action.js +3 -2
- package/dist/instrumentation/cluebase-authored-attributes.d.ts +0 -2
- package/dist/instrumentation/cluebase-authored-attributes.js +0 -23
- package/dist/instrumentation/common.d.ts +0 -63
- package/dist/instrumentation/common.js +0 -48
- package/dist/instrumentation/dom.d.ts +0 -25
- package/dist/instrumentation/dom.js +5 -41
- package/dist/instrumentation/drag.js +1 -2
- package/dist/instrumentation/form.js +1 -2
- package/dist/instrumentation/input.js +10 -8
- package/dist/instrumentation/naming-evidence.d.ts +0 -12
- package/dist/instrumentation/navigation.js +0 -2
- package/dist/instrumentation/network-request.d.ts +2 -2
- package/dist/instrumentation/network-request.js +1 -2
- package/dist/instrumentation/network-span-attributes.d.ts +0 -7
- package/dist/instrumentation/network-span-attributes.js +1 -3
- package/dist/instrumentation/network.js +6 -1
- package/dist/instrumentation/page.js +0 -2
- package/dist/instrumentation/performance.js +0 -7
- package/dist/integrations/react-router.js +0 -3
- package/dist/internal/internal-log.d.ts +0 -7
- package/dist/internal/internal-log.js +0 -11
- package/dist/provider/_version.d.ts +1 -1
- package/dist/provider/_version.js +1 -1
- package/dist/provider/config.d.ts +0 -7
- package/dist/provider/config.js +3 -16
- package/dist/provider/constants.d.ts +1 -17
- package/dist/provider/constants.js +1 -31
- package/dist/provider/privacy-policy-store.d.ts +0 -12
- package/dist/provider/privacy-policy-store.js +0 -12
- package/dist/provider/sdk-event-pipeline.d.ts +1 -1
- package/dist/provider/sdk-event-pipeline.js +41 -54
- package/dist/provider/sdk-otel-pipeline.js +50 -4
- package/dist/provider/sdk-runtime-surfaces.js +3 -1
- package/dist/provider/sdk.js +3 -23
- package/dist/provider/types.d.ts +3 -39
- package/dist/provider/usage-governor.js +0 -2
- 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,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
|
|
184
|
-
|
|
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
|
-
|
|
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
|
};
|