@genn-inc/cluebase-frontend-sdk 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +318 -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 +16 -44
- 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-error.d.ts +7 -0
- package/dist/provider/sdk-error.js +48 -0
- 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 +18 -24
- package/dist/provider/types.d.ts +3 -39
- package/dist/provider/usage-governor.js +0 -2
- package/package.json +53 -58
package/dist/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { parseSendableUrl } from "./provider/config.js";
|
|
2
2
|
import { createCluebaseBrowserSdk } from "./provider/sdk.js";
|
|
3
|
-
|
|
4
|
-
// 同じ tab で複数回 import されても 1 instance に統合する。
|
|
3
|
+
import { reportSdkError } from "./provider/sdk-error.js";
|
|
5
4
|
const GLOBAL_SDK_KEY = "__cluebase_frontend_sdk_singleton__";
|
|
6
5
|
const globalWithCluebaseSdk = globalThis;
|
|
7
6
|
const existingSdk = globalWithCluebaseSdk[GLOBAL_SDK_KEY];
|
|
@@ -9,7 +8,8 @@ const internalSdk = existingSdk ?? createCluebaseBrowserSdk();
|
|
|
9
8
|
if (!existingSdk) {
|
|
10
9
|
globalWithCluebaseSdk[GLOBAL_SDK_KEY] = internalSdk;
|
|
11
10
|
}
|
|
12
|
-
const reportPublicSdkError = (operation) => {
|
|
11
|
+
const reportPublicSdkError = (operation, error) => {
|
|
12
|
+
reportSdkError(operation, error);
|
|
13
13
|
try {
|
|
14
14
|
if (typeof console !== "undefined" && typeof console.warn === "function") {
|
|
15
15
|
console.warn(`[cluebase] ${operation} failed`, {
|
|
@@ -18,29 +18,24 @@ const reportPublicSdkError = (operation) => {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
catch {
|
|
21
|
-
// Cluebase は host application を絶対に失敗させない。
|
|
22
21
|
}
|
|
23
22
|
};
|
|
24
23
|
const safeCall = (operation, callback) => {
|
|
25
24
|
try {
|
|
26
25
|
callback();
|
|
27
26
|
}
|
|
28
|
-
catch {
|
|
29
|
-
reportPublicSdkError(operation);
|
|
27
|
+
catch (error) {
|
|
28
|
+
reportPublicSdkError(operation, error);
|
|
30
29
|
}
|
|
31
30
|
};
|
|
32
31
|
const safeAsyncCall = async (operation, callback) => {
|
|
33
32
|
try {
|
|
34
33
|
await callback();
|
|
35
34
|
}
|
|
36
|
-
catch {
|
|
37
|
-
reportPublicSdkError(operation);
|
|
35
|
+
catch (error) {
|
|
36
|
+
reportPublicSdkError(operation, error);
|
|
38
37
|
}
|
|
39
38
|
};
|
|
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
39
|
const REMOVED_PRIVACY_OPTIONS = {
|
|
45
40
|
allowedValuePaths: "cluebase.init: 'privacy.allowedValuePaths' was removed in " +
|
|
46
41
|
"@genn-inc/cluebase-frontend-sdk 0.0.1. The fields allowed to leave the browser in " +
|
|
@@ -49,22 +44,7 @@ const REMOVED_PRIVACY_OPTIONS = {
|
|
|
49
44
|
"and select those fields in the Cluebase privacy screen instead. Until then " +
|
|
50
45
|
"every value stays masked.",
|
|
51
46
|
};
|
|
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
|
-
*/
|
|
47
|
+
const REQUIRED_INIT_OPTIONS = ["projectKey"];
|
|
68
48
|
const validateInitOptions = (options) => {
|
|
69
49
|
const privacy = options.privacy ?? {};
|
|
70
50
|
for (const [name, message] of Object.entries(REMOVED_PRIVACY_OPTIONS)) {
|
|
@@ -72,8 +52,6 @@ const validateInitOptions = (options) => {
|
|
|
72
52
|
throw new Error(message);
|
|
73
53
|
}
|
|
74
54
|
}
|
|
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
55
|
const missing = REQUIRED_INIT_OPTIONS.filter((name) => {
|
|
78
56
|
const value = options[name];
|
|
79
57
|
return typeof value !== "string" || value.trim() === "";
|
|
@@ -83,12 +61,15 @@ const validateInitOptions = (options) => {
|
|
|
83
61
|
`${missing.length === 1 ? "is" : "are"} required and must be a non-empty ` +
|
|
84
62
|
"string. Cluebase collects nothing until every required option is set. Pass " +
|
|
85
63
|
"them from your environment, for example " +
|
|
86
|
-
"
|
|
87
|
-
"
|
|
64
|
+
"projectKey=NEXT_PUBLIC_CLUEBASE_PROJECT_KEY. The endpoint is optional " +
|
|
65
|
+
"and defaults to the hosted Cluebase API.");
|
|
66
|
+
}
|
|
67
|
+
if (options.endpoint !== undefined &&
|
|
68
|
+
typeof options.endpoint !== "string") {
|
|
69
|
+
throw new Error("cluebase.init: 'endpoint' must be a string when provided. Omit it to " +
|
|
70
|
+
"use the hosted Cluebase endpoint.");
|
|
88
71
|
}
|
|
89
|
-
|
|
90
|
-
// it surface later as delivery that never arrives.
|
|
91
|
-
if (parseSendableUrl(options.endpoint) === null) {
|
|
72
|
+
if (options.endpoint?.trim() && parseSendableUrl(options.endpoint) === null) {
|
|
92
73
|
throw new Error(`cluebase.init: 'endpoint' must be an absolute http:// or https:// URL, but ` +
|
|
93
74
|
`received ${JSON.stringify(options.endpoint)}. An endpoint the SDK cannot ` +
|
|
94
75
|
"send to loses every event just as silently as one that was never passed. " +
|
|
@@ -96,15 +77,8 @@ const validateInitOptions = (options) => {
|
|
|
96
77
|
"endpoint=NEXT_PUBLIC_CLUEBASE_API_BASE_URL.");
|
|
97
78
|
}
|
|
98
79
|
};
|
|
99
|
-
// Public API — namespace `cluebase` の 8 method
|
|
100
80
|
const cluebase = {
|
|
101
|
-
/**
|
|
102
|
-
* SDK を初期化する。 `endpoint` / `projectKey` は必須で、 欠けていれば送信を
|
|
103
|
-
* 黙って捨てずに throw する。
|
|
104
|
-
*/
|
|
105
81
|
init(options) {
|
|
106
|
-
// Thrown before `safeCall` so the caller actually sees it; see
|
|
107
|
-
// `validateInitOptions`.
|
|
108
82
|
validateInitOptions(options);
|
|
109
83
|
safeCall("init", () => {
|
|
110
84
|
internalSdk.init(options);
|
|
@@ -142,7 +116,6 @@ const cluebase = {
|
|
|
142
116
|
async close() {
|
|
143
117
|
await safeAsyncCall("close", () => internalSdk.close());
|
|
144
118
|
},
|
|
145
|
-
// 内部 metrics 取得 (= 顧客 debug 用、 production では通常不要)
|
|
146
119
|
getInternalMetrics() {
|
|
147
120
|
return internalSdk.getInternalMetrics();
|
|
148
121
|
},
|
|
@@ -150,7 +123,6 @@ const cluebase = {
|
|
|
150
123
|
return internalSdk.getDiagnostics();
|
|
151
124
|
},
|
|
152
125
|
};
|
|
153
|
-
// OTel-core exporter surface for advanced host integrations.
|
|
154
126
|
export { EXPORT_RESULT_FAILED, EXPORT_RESULT_SUCCESS, ObservationSourceEventExporter, } from "./exporter/observation-source-event-exporter.js";
|
|
155
127
|
export { attachNextRouterIntegration } from "./integrations/next-router.js";
|
|
156
128
|
export { attachReactRouterIntegration } from "./integrations/react-router.js";
|
|
@@ -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"),
|