@genn-inc/cluebase-frontend-sdk 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +222 -0
- package/dist/_generated/correlation-attribute-keys.d.ts +2 -0
- package/dist/_generated/correlation-attribute-keys.js +33 -0
- package/dist/_generated/identity-pii-policy.d.ts +38 -0
- package/dist/_generated/identity-pii-policy.js +103 -0
- package/dist/_generated/ingest-privacy-tables.d.ts +120 -0
- package/dist/_generated/ingest-privacy-tables.js +890 -0
- package/dist/_generated/otel-resource-keys.d.ts +21 -0
- package/dist/_generated/otel-resource-keys.js +72 -0
- package/dist/_generated/otel-span-conversion.d.ts +48 -0
- package/dist/_generated/otel-span-conversion.js +69 -0
- package/dist/_generated/parameter-snapshot-leaf.d.ts +11 -0
- package/dist/_generated/parameter-snapshot-leaf.js +37 -0
- package/dist/_generated/sdk-runtime-policy.d.ts +7 -0
- package/dist/_generated/sdk-runtime-policy.js +14 -0
- package/dist/_generated/usage-governor-tables.d.ts +15 -0
- package/dist/_generated/usage-governor-tables.js +44 -0
- package/dist/context/browser-storage.d.ts +15 -0
- package/dist/context/browser-storage.js +38 -0
- package/dist/context/environment.d.ts +37 -0
- package/dist/context/environment.js +229 -0
- package/dist/context/identity.d.ts +16 -0
- package/dist/context/identity.js +139 -0
- package/dist/context/organization.d.ts +16 -0
- package/dist/context/organization.js +60 -0
- package/dist/context/session.d.ts +28 -0
- package/dist/context/session.js +130 -0
- package/dist/context/tab.d.ts +22 -0
- package/dist/context/tab.js +117 -0
- package/dist/context/trace.d.ts +76 -0
- package/dist/context/trace.js +163 -0
- package/dist/exporter/batch.d.ts +94 -0
- package/dist/exporter/batch.js +485 -0
- package/dist/exporter/browser-outbox-records.d.ts +159 -0
- package/dist/exporter/browser-outbox-records.js +341 -0
- package/dist/exporter/browser-outbox.d.ts +112 -0
- package/dist/exporter/browser-outbox.js +876 -0
- package/dist/exporter/client.d.ts +92 -0
- package/dist/exporter/client.js +541 -0
- package/dist/exporter/delivery-response-contract.d.ts +103 -0
- package/dist/exporter/delivery-response-contract.js +147 -0
- package/dist/exporter/delivery-response.d.ts +37 -0
- package/dist/exporter/delivery-response.js +151 -0
- package/dist/exporter/diagnostics.d.ts +42 -0
- package/dist/exporter/diagnostics.js +148 -0
- package/dist/exporter/event-id.d.ts +10 -0
- package/dist/exporter/event-id.js +75 -0
- package/dist/exporter/frontend-resource.d.ts +20 -0
- package/dist/exporter/frontend-resource.js +52 -0
- package/dist/exporter/local-minimization.d.ts +29 -0
- package/dist/exporter/local-minimization.js +87 -0
- package/dist/exporter/observation-source-event-exporter.d.ts +69 -0
- package/dist/exporter/observation-source-event-exporter.js +154 -0
- package/dist/exporter/parameter-snapshot.d.ts +14 -0
- package/dist/exporter/parameter-snapshot.js +215 -0
- package/dist/exporter/path-template.d.ts +1 -0
- package/dist/exporter/path-template.js +67 -0
- package/dist/exporter/raw-event-to-source-signal.d.ts +12 -0
- package/dist/exporter/raw-event-to-source-signal.js +297 -0
- package/dist/exporter/retry.d.ts +8 -0
- package/dist/exporter/retry.js +8 -0
- package/dist/exporter/sanitize.d.ts +30 -0
- package/dist/exporter/sanitize.js +177 -0
- package/dist/exporter/schema-evidence.d.ts +20 -0
- package/dist/exporter/schema-evidence.js +232 -0
- package/dist/exporter/source-event.d.ts +99 -0
- package/dist/exporter/source-event.js +13 -0
- package/dist/exporter/span-to-source-signal.d.ts +8 -0
- package/dist/exporter/span-to-source-signal.js +174 -0
- package/dist/exporter/text-fingerprint.d.ts +9 -0
- package/dist/exporter/text-fingerprint.js +16 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +158 -0
- package/dist/instrumentation/action.d.ts +2 -0
- package/dist/instrumentation/action.js +171 -0
- package/dist/instrumentation/cluebase-authored-attributes.d.ts +4 -0
- package/dist/instrumentation/cluebase-authored-attributes.js +32 -0
- package/dist/instrumentation/common.d.ts +73 -0
- package/dist/instrumentation/common.js +90 -0
- package/dist/instrumentation/dom.d.ts +63 -0
- package/dist/instrumentation/dom.js +640 -0
- package/dist/instrumentation/drag.d.ts +2 -0
- package/dist/instrumentation/drag.js +82 -0
- package/dist/instrumentation/error.d.ts +2 -0
- package/dist/instrumentation/error.js +318 -0
- package/dist/instrumentation/form.d.ts +2 -0
- package/dist/instrumentation/form.js +132 -0
- package/dist/instrumentation/input.d.ts +2 -0
- package/dist/instrumentation/input.js +180 -0
- package/dist/instrumentation/location.d.ts +11 -0
- package/dist/instrumentation/location.js +69 -0
- package/dist/instrumentation/naming-evidence.d.ts +20 -0
- package/dist/instrumentation/naming-evidence.js +11 -0
- package/dist/instrumentation/navigation.d.ts +2 -0
- package/dist/instrumentation/navigation.js +144 -0
- package/dist/instrumentation/network-request.d.ts +103 -0
- package/dist/instrumentation/network-request.js +285 -0
- package/dist/instrumentation/network-span-attributes.d.ts +25 -0
- package/dist/instrumentation/network-span-attributes.js +153 -0
- package/dist/instrumentation/network.d.ts +16 -0
- package/dist/instrumentation/network.js +502 -0
- package/dist/instrumentation/page.d.ts +2 -0
- package/dist/instrumentation/page.js +56 -0
- package/dist/instrumentation/performance.d.ts +2 -0
- package/dist/instrumentation/performance.js +365 -0
- package/dist/integrations/next-router.d.ts +16 -0
- package/dist/integrations/next-router.js +18 -0
- package/dist/integrations/react-router.d.ts +7 -0
- package/dist/integrations/react-router.js +61 -0
- package/dist/internal/internal-log.d.ts +23 -0
- package/dist/internal/internal-log.js +47 -0
- package/dist/internal/metrics.d.ts +10 -0
- package/dist/internal/metrics.js +50 -0
- package/dist/provider/_version.d.ts +1 -0
- package/dist/provider/_version.js +3 -0
- package/dist/provider/config.d.ts +12 -0
- package/dist/provider/config.js +177 -0
- package/dist/provider/constants.d.ts +62 -0
- package/dist/provider/constants.js +118 -0
- package/dist/provider/contracts.d.ts +51 -0
- package/dist/provider/contracts.js +46 -0
- package/dist/provider/privacy-policy-store.d.ts +20 -0
- package/dist/provider/privacy-policy-store.js +56 -0
- package/dist/provider/sampling.d.ts +2 -0
- package/dist/provider/sampling.js +17 -0
- package/dist/provider/sdk-event-pipeline.d.ts +60 -0
- package/dist/provider/sdk-event-pipeline.js +478 -0
- package/dist/provider/sdk-input-normalization.d.ts +7 -0
- package/dist/provider/sdk-input-normalization.js +113 -0
- package/dist/provider/sdk-otel-pipeline.d.ts +14 -0
- package/dist/provider/sdk-otel-pipeline.js +148 -0
- package/dist/provider/sdk-runtime-surfaces.d.ts +49 -0
- package/dist/provider/sdk-runtime-surfaces.js +216 -0
- package/dist/provider/sdk.d.ts +2 -0
- package/dist/provider/sdk.js +614 -0
- package/dist/provider/types.d.ts +323 -0
- package/dist/provider/types.js +1 -0
- package/dist/provider/usage-governor.d.ts +7 -0
- package/dist/provider/usage-governor.js +116 -0
- package/package.json +59 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { hasParameterSnapshotLeafShape } from "../_generated/parameter-snapshot-leaf.js";
|
|
2
|
+
import { FORBIDDEN_PARAMETER_VALUE, MASKED_VALUE, MAX_SANITIZE_DEPTH, TRUNCATED_PARAMETER_VALUE, } from "../provider/constants.js";
|
|
3
|
+
import { PARAMETER_CAPTURE_MODE } from "../provider/contracts.js";
|
|
4
|
+
import { isDeniedKey } from "./sanitize.js";
|
|
5
|
+
const SNAPSHOT_UNAVAILABLE_KEY = "_cluebase_snapshot_unavailable";
|
|
6
|
+
const normalizeParameterLookupPath = (path) => {
|
|
7
|
+
return path.replace(/\[\d+\]/g, "[]");
|
|
8
|
+
};
|
|
9
|
+
const isParameterInputObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
10
|
+
const inferLeafType = (value) => {
|
|
11
|
+
if (value === null) {
|
|
12
|
+
return "null";
|
|
13
|
+
}
|
|
14
|
+
if (Array.isArray(value)) {
|
|
15
|
+
return "array";
|
|
16
|
+
}
|
|
17
|
+
if (typeof value === "object") {
|
|
18
|
+
return "object";
|
|
19
|
+
}
|
|
20
|
+
switch (typeof value) {
|
|
21
|
+
case "string":
|
|
22
|
+
return "string";
|
|
23
|
+
case "number":
|
|
24
|
+
return "number";
|
|
25
|
+
case "boolean":
|
|
26
|
+
return "boolean";
|
|
27
|
+
default:
|
|
28
|
+
return "string";
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const measureSerializedBytes = (value) => {
|
|
32
|
+
try {
|
|
33
|
+
return new TextEncoder().encode(JSON.stringify(value)).length;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* 値をそのまま載せてよいのは、**顧客が画面で名指しした項目だけ**。
|
|
41
|
+
*
|
|
42
|
+
* 載せるのは文字・数・真偽に限ります。入れ子は葉に来ないので、それ以外の形が
|
|
43
|
+
* ここへ来たら伏せた側へ倒します。
|
|
44
|
+
*/
|
|
45
|
+
const releasedLeafValue = (value) => value === null ||
|
|
46
|
+
typeof value === "string" ||
|
|
47
|
+
typeof value === "number" ||
|
|
48
|
+
typeof value === "boolean"
|
|
49
|
+
? value
|
|
50
|
+
: MASKED_VALUE;
|
|
51
|
+
const buildLeaf = (value, captureMode,
|
|
52
|
+
// 顧客がこの項目を名指ししたか。**既定は false = 伏せる。**
|
|
53
|
+
released = false) => {
|
|
54
|
+
const leafType = inferLeafType(value);
|
|
55
|
+
const originalSizeBytes = captureMode === PARAMETER_CAPTURE_MODE.forbidden
|
|
56
|
+
? undefined
|
|
57
|
+
: measureSerializedBytes(value);
|
|
58
|
+
return {
|
|
59
|
+
type: leafType,
|
|
60
|
+
value: captureMode === PARAMETER_CAPTURE_MODE.forbidden
|
|
61
|
+
? FORBIDDEN_PARAMETER_VALUE
|
|
62
|
+
: released
|
|
63
|
+
? releasedLeafValue(value)
|
|
64
|
+
: MASKED_VALUE,
|
|
65
|
+
capture_mode: captureMode,
|
|
66
|
+
...(typeof originalSizeBytes === "number"
|
|
67
|
+
? { original_size_bytes: originalSizeBytes }
|
|
68
|
+
: {}),
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
export const buildUnavailableParameterSnapshot = () => ({
|
|
72
|
+
[SNAPSHOT_UNAVAILABLE_KEY]: buildLeaf("snapshot_unavailable", PARAMETER_CAPTURE_MODE.maskedFingerprint),
|
|
73
|
+
});
|
|
74
|
+
/**
|
|
75
|
+
* その節は葉か、降りる枝か。
|
|
76
|
+
*
|
|
77
|
+
* **見分ける手順をここに書きません。** ここには鍵の有無だけを見る版が書かれており、
|
|
78
|
+
* 受付側は型まで見ていました。`{ type: 123, capture_mode: null, value: 1 }` が
|
|
79
|
+
* **送る側にとって葉、受け取る側にとって枝**という状態で、受け取った側は中へ降りて
|
|
80
|
+
* `type` / `value` / `capture_mode` を項目名として扱っていました。降りる側の 1 つは
|
|
81
|
+
* 伏せる判定です。
|
|
82
|
+
*
|
|
83
|
+
* 手順は正本(`packages/cluebase-schema/.../observation-classification.schema.ts`)から
|
|
84
|
+
* 生成され、受付側も同じものを受け取ります。共有の部品は顧客の手元で解決できない
|
|
85
|
+
* ので取り込めず、**生成して同梱する**のがこの repo の作法です
|
|
86
|
+
* (`_generated/ingest-privacy-tables.ts` と同じ)。
|
|
87
|
+
*/
|
|
88
|
+
const isParameterSnapshotLeaf = (value) => hasParameterSnapshotLeafShape(value);
|
|
89
|
+
export const truncateParameterSnapshotValue = (value) => {
|
|
90
|
+
if (Array.isArray(value)) {
|
|
91
|
+
return value.map((entry) => truncateParameterSnapshotValue(entry));
|
|
92
|
+
}
|
|
93
|
+
if (isParameterSnapshotLeaf(value)) {
|
|
94
|
+
const originalSizeBytes = typeof value.original_size_bytes === "number"
|
|
95
|
+
? value.original_size_bytes
|
|
96
|
+
: undefined;
|
|
97
|
+
return {
|
|
98
|
+
type: value.type,
|
|
99
|
+
value: TRUNCATED_PARAMETER_VALUE,
|
|
100
|
+
capture_mode: PARAMETER_CAPTURE_MODE.truncated,
|
|
101
|
+
truncated_reason: "max_event_bytes",
|
|
102
|
+
...(typeof originalSizeBytes === "number"
|
|
103
|
+
? { original_size_bytes: originalSizeBytes }
|
|
104
|
+
: {}),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const truncated = {};
|
|
108
|
+
for (const [key, child] of Object.entries(value)) {
|
|
109
|
+
truncated[key] = truncateParameterSnapshotValue(child);
|
|
110
|
+
}
|
|
111
|
+
return truncated;
|
|
112
|
+
};
|
|
113
|
+
const buildAllowedPathLookup = (allowedPaths) => {
|
|
114
|
+
const lookup = new Map();
|
|
115
|
+
for (const entry of allowedPaths) {
|
|
116
|
+
const trimmed = entry.trim();
|
|
117
|
+
if (!trimmed) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const normalized = normalizeParameterLookupPath(trimmed);
|
|
121
|
+
if (!lookup.has(normalized)) {
|
|
122
|
+
lookup.set(normalized, trimmed);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return lookup;
|
|
126
|
+
};
|
|
127
|
+
export const buildParameterSnapshotBundle = (value, options) => {
|
|
128
|
+
const allowedPathLookup = buildAllowedPathLookup(options.allowedPaths);
|
|
129
|
+
const seen = new WeakSet();
|
|
130
|
+
const rootPathPrefix = options.pathPrefix?.trim() ?? "";
|
|
131
|
+
const walk = (current, path, depth) => {
|
|
132
|
+
if (depth > MAX_SANITIZE_DEPTH) {
|
|
133
|
+
return {
|
|
134
|
+
snapshot: buildLeaf(null, PARAMETER_CAPTURE_MODE.maskedFingerprint),
|
|
135
|
+
allowedValueKeys: [],
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
if (current === undefined ||
|
|
139
|
+
typeof current === "function" ||
|
|
140
|
+
typeof current === "symbol") {
|
|
141
|
+
return {
|
|
142
|
+
snapshot: undefined,
|
|
143
|
+
allowedValueKeys: [],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (Array.isArray(current)) {
|
|
147
|
+
if (current.length === 0) {
|
|
148
|
+
return {
|
|
149
|
+
snapshot: [],
|
|
150
|
+
allowedValueKeys: [],
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const representative = walk(current[0], `${path}[]`, depth + 1);
|
|
154
|
+
if (representative.snapshot === undefined) {
|
|
155
|
+
return {
|
|
156
|
+
snapshot: [],
|
|
157
|
+
allowedValueKeys: representative.allowedValueKeys,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
snapshot: [representative.snapshot],
|
|
162
|
+
allowedValueKeys: representative.allowedValueKeys,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (isParameterInputObject(current)) {
|
|
166
|
+
const objectValue = current;
|
|
167
|
+
if (seen.has(objectValue)) {
|
|
168
|
+
return {
|
|
169
|
+
snapshot: buildLeaf(null, PARAMETER_CAPTURE_MODE.maskedFingerprint),
|
|
170
|
+
allowedValueKeys: [],
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
seen.add(objectValue);
|
|
174
|
+
const snapshot = {};
|
|
175
|
+
const allowedValueKeys = [];
|
|
176
|
+
for (const [key, child] of Object.entries(objectValue)) {
|
|
177
|
+
const childPath = path ? `${path}.${key}` : key;
|
|
178
|
+
if (isDeniedKey(key, options.deniedKeys)) {
|
|
179
|
+
snapshot[key] = buildLeaf(child, PARAMETER_CAPTURE_MODE.forbidden);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const childBundle = walk(child, childPath, depth + 1);
|
|
183
|
+
if (childBundle.snapshot === undefined) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
snapshot[key] = childBundle.snapshot;
|
|
187
|
+
allowedValueKeys.push(...childBundle.allowedValueKeys);
|
|
188
|
+
}
|
|
189
|
+
seen.delete(objectValue);
|
|
190
|
+
return {
|
|
191
|
+
snapshot,
|
|
192
|
+
allowedValueKeys,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
const matchedAllowedPath = path
|
|
196
|
+
? (allowedPathLookup.get(normalizeParameterLookupPath(path)) ?? null)
|
|
197
|
+
: null;
|
|
198
|
+
const captureMode = PARAMETER_CAPTURE_MODE.maskedFingerprint;
|
|
199
|
+
return {
|
|
200
|
+
// 名指しされた項目だけ値を持つ。名指ししていない項目は伏せたまま。
|
|
201
|
+
snapshot: buildLeaf(current, captureMode, matchedAllowedPath !== null),
|
|
202
|
+
allowedValueKeys: matchedAllowedPath ? [matchedAllowedPath] : [],
|
|
203
|
+
};
|
|
204
|
+
};
|
|
205
|
+
const rootBundle = walk(value, rootPathPrefix, 0);
|
|
206
|
+
const snapshot = rootBundle.snapshot &&
|
|
207
|
+
typeof rootBundle.snapshot === "object" &&
|
|
208
|
+
!Array.isArray(rootBundle.snapshot)
|
|
209
|
+
? rootBundle.snapshot
|
|
210
|
+
: {};
|
|
211
|
+
return {
|
|
212
|
+
snapshot,
|
|
213
|
+
allowedValueKeys: [...new Set(rootBundle.allowedValueKeys)].sort(),
|
|
214
|
+
};
|
|
215
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const toPathTemplate: (pathname: string) => string;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const UUID_SEGMENT = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
2
|
+
const HEX_SEGMENT = /^[0-9a-f]{16,}$/i;
|
|
3
|
+
const NUMBER_SEGMENT = /^\d+$/;
|
|
4
|
+
const TOKEN_SEGMENT = /^[A-Za-z0-9_-]{24,}$/;
|
|
5
|
+
const BROKEN_PLACEHOLDER_SEGMENTS = new Set(["undefined", "null", "nan"]);
|
|
6
|
+
const RESOURCE_PARENT_SEGMENTS = new Set([
|
|
7
|
+
"accounts",
|
|
8
|
+
"companies",
|
|
9
|
+
"customers",
|
|
10
|
+
"epics",
|
|
11
|
+
"members",
|
|
12
|
+
"organizations",
|
|
13
|
+
"projects",
|
|
14
|
+
"sessions",
|
|
15
|
+
"spaces",
|
|
16
|
+
"tasks",
|
|
17
|
+
"teams",
|
|
18
|
+
"users",
|
|
19
|
+
"w",
|
|
20
|
+
"workspaces",
|
|
21
|
+
]);
|
|
22
|
+
const STATIC_RESOURCE_CHILD_SEGMENTS = new Set([
|
|
23
|
+
"batch",
|
|
24
|
+
"bulk",
|
|
25
|
+
"count",
|
|
26
|
+
"create",
|
|
27
|
+
"current",
|
|
28
|
+
"list",
|
|
29
|
+
"me",
|
|
30
|
+
"new",
|
|
31
|
+
"search",
|
|
32
|
+
"settings",
|
|
33
|
+
]);
|
|
34
|
+
const shouldTemplateSegment = (segment, previousSegment) => {
|
|
35
|
+
const normalized = segment.toLowerCase();
|
|
36
|
+
if (BROKEN_PLACEHOLDER_SEGMENTS.has(normalized))
|
|
37
|
+
return true;
|
|
38
|
+
if (NUMBER_SEGMENT.test(segment))
|
|
39
|
+
return true;
|
|
40
|
+
if (UUID_SEGMENT.test(segment))
|
|
41
|
+
return true;
|
|
42
|
+
if (HEX_SEGMENT.test(segment))
|
|
43
|
+
return true;
|
|
44
|
+
if (TOKEN_SEGMENT.test(segment))
|
|
45
|
+
return true;
|
|
46
|
+
if (previousSegment &&
|
|
47
|
+
RESOURCE_PARENT_SEGMENTS.has(previousSegment.toLowerCase()) &&
|
|
48
|
+
!STATIC_RESOURCE_CHILD_SEGMENTS.has(normalized)) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
};
|
|
53
|
+
export const toPathTemplate = (pathname) => {
|
|
54
|
+
if (!pathname || pathname === "/") {
|
|
55
|
+
return "/";
|
|
56
|
+
}
|
|
57
|
+
const normalizedPath = pathname.startsWith("/") ? pathname : `/${pathname}`;
|
|
58
|
+
const [pathOnly] = normalizedPath.split("?");
|
|
59
|
+
const rawSegments = pathOnly.split("/").filter((segment) => segment.length > 0);
|
|
60
|
+
const segments = rawSegments.map((segment, index) => shouldTemplateSegment(segment, rawSegments[index - 1] ?? null)
|
|
61
|
+
? ":id"
|
|
62
|
+
: segment);
|
|
63
|
+
if (segments.length === 0) {
|
|
64
|
+
return "/";
|
|
65
|
+
}
|
|
66
|
+
return `/${segments.join("/")}`;
|
|
67
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CluebaseContext, RawCapturedEvent } from "../provider/types.js";
|
|
2
|
+
import { type FrontendResourceInput } from "./frontend-resource.js";
|
|
3
|
+
import { type BrowserSourceSignalEvent } from "./source-event.js";
|
|
4
|
+
export declare const RAW_FRONTEND_SOURCE_SIGNAL_KIND = "frontend_raw_event";
|
|
5
|
+
type Options = {
|
|
6
|
+
context: CluebaseContext;
|
|
7
|
+
resource?: FrontendResourceInput;
|
|
8
|
+
deniedKeys?: readonly string[];
|
|
9
|
+
onMaskingError?: () => void;
|
|
10
|
+
};
|
|
11
|
+
export declare function rawEventToSourceSignalEvent(raw: RawCapturedEvent, options: Options): BrowserSourceSignalEvent;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
// 相関のために残す属性名は、正本から生成した部品を読みます。ここには手で書かれて
|
|
2
|
+
// おり、もう 1 か所の写しと 15 語食い違い、`cluebase.session_id` `mcp.session.id` が
|
|
3
|
+
// 欠けていて**落ちていました**(`session` を含むため落とす側に当たる)。
|
|
4
|
+
import { CLUEBASE_CORRELATION_ATTRIBUTE_KEYS } from "../_generated/correlation-attribute-keys.js";
|
|
5
|
+
import { cluebaseAuthoredPropertyAttribute } from "../instrumentation/cluebase-authored-attributes.js";
|
|
6
|
+
import { EVENT_SOURCE, EVENT_VERSION, SOURCE_SCHEMA_VERSION, } from "../provider/constants.js";
|
|
7
|
+
import { generateEventId, generateInteractionId, generateRequestId, generateRequestSpanId, generateTraceId, } from "./event-id.js";
|
|
8
|
+
import { isDeniedKey } from "./sanitize.js";
|
|
9
|
+
import { toFrontendSourceResource, } from "./frontend-resource.js";
|
|
10
|
+
import { SDK_SOURCE_SIGNAL_EVENT_NAME, SDK_SOURCE_SIGNAL_EVENT_TYPE, } from "./source-event.js";
|
|
11
|
+
export const RAW_FRONTEND_SOURCE_SIGNAL_KIND = "frontend_raw_event";
|
|
12
|
+
const PUBLIC_CUSTOM_EVENT_NAME_PATTERN = /^[A-Za-z0-9_.:-]{1,128}$/;
|
|
13
|
+
const isDynamicPayloadObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
14
|
+
function toJsonValue(value, deniedKeys = [], onMaskingError) {
|
|
15
|
+
if (value === null)
|
|
16
|
+
return null;
|
|
17
|
+
if (typeof value === "string" ||
|
|
18
|
+
typeof value === "number" ||
|
|
19
|
+
typeof value === "boolean") {
|
|
20
|
+
return typeof value === "number" && !Number.isFinite(value) ? null : value;
|
|
21
|
+
}
|
|
22
|
+
if (Array.isArray(value)) {
|
|
23
|
+
return value.map((entry) => toJsonValue(entry, deniedKeys, onMaskingError));
|
|
24
|
+
}
|
|
25
|
+
if (isDynamicPayloadObject(value)) {
|
|
26
|
+
const record = {};
|
|
27
|
+
let entries;
|
|
28
|
+
try {
|
|
29
|
+
entries = Object.entries(value);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
onMaskingError?.();
|
|
33
|
+
return record;
|
|
34
|
+
}
|
|
35
|
+
for (const [key, entry] of entries) {
|
|
36
|
+
if (!CLUEBASE_CORRELATION_ATTRIBUTE_KEYS.has(key) &&
|
|
37
|
+
isDeniedKey(key, deniedKeys)) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
record[key] = toJsonValue(entry, deniedKeys, onMaskingError);
|
|
41
|
+
}
|
|
42
|
+
return record;
|
|
43
|
+
}
|
|
44
|
+
if (typeof value === "bigint")
|
|
45
|
+
return value.toString();
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
function toJsonRecord(value, deniedKeys = [], onMaskingError) {
|
|
49
|
+
const record = {};
|
|
50
|
+
let entries;
|
|
51
|
+
try {
|
|
52
|
+
entries = Object.entries(value);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
onMaskingError?.();
|
|
56
|
+
return record;
|
|
57
|
+
}
|
|
58
|
+
for (const [key, entry] of entries) {
|
|
59
|
+
if (!CLUEBASE_CORRELATION_ATTRIBUTE_KEYS.has(key) &&
|
|
60
|
+
isDeniedKey(key, deniedKeys)) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
record[key] = toJsonValue(entry, deniedKeys, onMaskingError);
|
|
64
|
+
}
|
|
65
|
+
return record;
|
|
66
|
+
}
|
|
67
|
+
function isJsonRecord(value) {
|
|
68
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
69
|
+
}
|
|
70
|
+
function dropNonFiniteMetrics(payload) {
|
|
71
|
+
const metrics = payload.metrics;
|
|
72
|
+
if (!isJsonRecord(metrics)) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const sanitized = {};
|
|
76
|
+
for (const [key, value] of Object.entries(metrics)) {
|
|
77
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
78
|
+
sanitized[key] = value;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
payload.metrics = sanitized;
|
|
82
|
+
}
|
|
83
|
+
function stringPayload(payload, key) {
|
|
84
|
+
const value = payload[key];
|
|
85
|
+
return typeof value === "string" && value.trim().length > 0
|
|
86
|
+
? value.trim()
|
|
87
|
+
: null;
|
|
88
|
+
}
|
|
89
|
+
function trustedCustomOrigin(payload) {
|
|
90
|
+
const origin = stringPayload(payload, "customEventOrigin");
|
|
91
|
+
return origin === "explicit_track" || origin === "official_sdk_lifecycle"
|
|
92
|
+
? origin
|
|
93
|
+
: null;
|
|
94
|
+
}
|
|
95
|
+
const isRequestRawEvent = (eventName) => eventName === "request_started" ||
|
|
96
|
+
eventName === "request_finished" ||
|
|
97
|
+
eventName === "request_failed";
|
|
98
|
+
const numberPayload = (payload, key) => {
|
|
99
|
+
const value = payload[key];
|
|
100
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
101
|
+
};
|
|
102
|
+
function contextRecord(context, overrides = {}) {
|
|
103
|
+
return {
|
|
104
|
+
producer_id: context.producer_id,
|
|
105
|
+
anonymous_id: context.anonymous_id,
|
|
106
|
+
user_id: context.user_id,
|
|
107
|
+
organization_id: context.organization_id ?? null,
|
|
108
|
+
user_profile: context.user_profile,
|
|
109
|
+
session_id: overrides.session_id ?? context.session_id,
|
|
110
|
+
tab_id: context.tab_id,
|
|
111
|
+
trace_id: overrides.trace_id ?? context.trace_id,
|
|
112
|
+
request_id: overrides.request_id ?? context.request_id,
|
|
113
|
+
request_span_id: overrides.request_span_id ?? context.request_span_id,
|
|
114
|
+
interaction_id: overrides.interaction_id ?? context.interaction_id,
|
|
115
|
+
correlation_id: context.correlation_id,
|
|
116
|
+
sdk_collection_mode: context.sdk_collection_mode,
|
|
117
|
+
frontend_release: context.frontend_release,
|
|
118
|
+
locale: context.locale,
|
|
119
|
+
country: context.country,
|
|
120
|
+
device_type: context.device_type,
|
|
121
|
+
browser: context.browser,
|
|
122
|
+
os: context.os,
|
|
123
|
+
referrer: context.referrer,
|
|
124
|
+
browser_family: context.browser_family ?? null,
|
|
125
|
+
browser_major_version: context.browser_major_version ?? null,
|
|
126
|
+
browser_engine: context.browser_engine ?? null,
|
|
127
|
+
os_family: context.os_family ?? null,
|
|
128
|
+
os_major_version: context.os_major_version ?? null,
|
|
129
|
+
timezone: context.timezone ?? null,
|
|
130
|
+
pointer_type_candidate: context.pointer_type_candidate ?? null,
|
|
131
|
+
touch_capable: context.touch_capable ?? null,
|
|
132
|
+
cookies_enabled: context.cookies_enabled ?? null,
|
|
133
|
+
local_storage_available: context.local_storage_available ?? null,
|
|
134
|
+
session_storage_available: context.session_storage_available ?? null,
|
|
135
|
+
service_worker_supported: context.service_worker_supported ?? null,
|
|
136
|
+
webgl_supported: context.webgl_supported ?? null,
|
|
137
|
+
reduced_motion: context.reduced_motion ?? null,
|
|
138
|
+
color_scheme: context.color_scheme ?? null,
|
|
139
|
+
subject_traits: context.subject_traits,
|
|
140
|
+
organization_traits: context.organization_traits,
|
|
141
|
+
context_traits: context.context_traits,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
export function rawEventToSourceSignalEvent(raw, options) {
|
|
145
|
+
const context = options.context;
|
|
146
|
+
const eventId = generateEventId();
|
|
147
|
+
const occurredAtMs = raw.occurredAtMs ?? Date.now();
|
|
148
|
+
const occurredAt = new Date(occurredAtMs).toISOString();
|
|
149
|
+
const payload = toJsonRecord(raw.payload, options.deniedKeys, options.onMaskingError);
|
|
150
|
+
dropNonFiniteMetrics(payload);
|
|
151
|
+
const requestId = stringPayload(raw.payload, "requestId") ??
|
|
152
|
+
context.request_id ??
|
|
153
|
+
generateRequestId();
|
|
154
|
+
const requestSpanId = stringPayload(raw.payload, "requestSpanId") ??
|
|
155
|
+
context.request_span_id ??
|
|
156
|
+
generateRequestSpanId();
|
|
157
|
+
const traceId = stringPayload(raw.payload, "traceId") ??
|
|
158
|
+
context.trace_id ??
|
|
159
|
+
generateTraceId();
|
|
160
|
+
const interactionId = stringPayload(raw.payload, "interactionId") ??
|
|
161
|
+
context.interaction_id ??
|
|
162
|
+
generateInteractionId();
|
|
163
|
+
const sessionId = stringPayload(raw.payload, "sessionId") ?? context.session_id;
|
|
164
|
+
const occurrenceKind = stringPayload(raw.payload, "occurrenceKind");
|
|
165
|
+
const originFrontendInteractionId = stringPayload(raw.payload, "originFrontendInteractionId");
|
|
166
|
+
const attributes = {
|
|
167
|
+
"cluebase.raw_event.kind": RAW_FRONTEND_SOURCE_SIGNAL_KIND,
|
|
168
|
+
"cluebase.raw_event.type": raw.type,
|
|
169
|
+
"cluebase.raw_event.payload": payload,
|
|
170
|
+
"cluebase.event_source": EVENT_SOURCE,
|
|
171
|
+
};
|
|
172
|
+
const customEventOrigin = raw.type === "custom_emitted" ? trustedCustomOrigin(raw.payload) : null;
|
|
173
|
+
const customEventName = raw.type === "custom_emitted" && customEventOrigin
|
|
174
|
+
? stringPayload(raw.payload, "eventName")
|
|
175
|
+
: null;
|
|
176
|
+
if (customEventName &&
|
|
177
|
+
PUBLIC_CUSTOM_EVENT_NAME_PATTERN.test(customEventName)) {
|
|
178
|
+
attributes["cluebase.custom_event_name"] = customEventName;
|
|
179
|
+
}
|
|
180
|
+
if (customEventOrigin) {
|
|
181
|
+
attributes["cluebase.custom_event_origin"] = customEventOrigin;
|
|
182
|
+
}
|
|
183
|
+
if (traceId)
|
|
184
|
+
attributes["cluebase.trace_id"] = traceId;
|
|
185
|
+
if (requestId)
|
|
186
|
+
attributes["cluebase.request.id"] = requestId;
|
|
187
|
+
if (requestSpanId)
|
|
188
|
+
attributes["cluebase.request_span_id"] = requestSpanId;
|
|
189
|
+
if (interactionId)
|
|
190
|
+
attributes["cluebase.interaction.id"] = interactionId;
|
|
191
|
+
if (occurrenceKind)
|
|
192
|
+
attributes["cluebase.occurrence.kind"] = occurrenceKind;
|
|
193
|
+
if (originFrontendInteractionId) {
|
|
194
|
+
attributes["cluebase.origin_frontend_interaction.id"] =
|
|
195
|
+
originFrontendInteractionId;
|
|
196
|
+
}
|
|
197
|
+
if (sessionId)
|
|
198
|
+
attributes["cluebase.session.id"] = sessionId;
|
|
199
|
+
if (isRequestRawEvent(raw.type)) {
|
|
200
|
+
const method = stringPayload(raw.payload, "method");
|
|
201
|
+
const url = stringPayload(raw.payload, "url") ??
|
|
202
|
+
stringPayload(raw.payload, "requestUrl");
|
|
203
|
+
const path = stringPayload(raw.payload, "path");
|
|
204
|
+
const pathTemplate = stringPayload(raw.payload, "pathTemplate") ??
|
|
205
|
+
stringPayload(raw.payload, "route");
|
|
206
|
+
const targetService = stringPayload(raw.payload, "targetService");
|
|
207
|
+
const pageUrl = stringPayload(raw.payload, "pageUrl");
|
|
208
|
+
const viewId = stringPayload(raw.payload, "viewId");
|
|
209
|
+
const failureType = stringPayload(raw.payload, "failureType");
|
|
210
|
+
const outcomeClass = stringPayload(raw.payload, "outcomeClass");
|
|
211
|
+
const statusCode = numberPayload(raw.payload, "statusCode") ??
|
|
212
|
+
numberPayload(raw.payload, "status_code");
|
|
213
|
+
if (method) {
|
|
214
|
+
attributes["http.method"] = method;
|
|
215
|
+
attributes["http.request.method"] = method;
|
|
216
|
+
}
|
|
217
|
+
if (url) {
|
|
218
|
+
attributes["http.url"] = url;
|
|
219
|
+
attributes["url.full"] = url;
|
|
220
|
+
}
|
|
221
|
+
if (path)
|
|
222
|
+
attributes["cluebase.path"] = path;
|
|
223
|
+
if (pathTemplate) {
|
|
224
|
+
attributes["http.route"] = pathTemplate;
|
|
225
|
+
attributes["cluebase.path_template"] = pathTemplate;
|
|
226
|
+
}
|
|
227
|
+
if (targetService)
|
|
228
|
+
attributes["cluebase.target_service"] = targetService;
|
|
229
|
+
if (pageUrl)
|
|
230
|
+
attributes["cluebase.page.url"] = pageUrl;
|
|
231
|
+
if (viewId)
|
|
232
|
+
attributes["cluebase.view.id"] = viewId;
|
|
233
|
+
if (failureType) {
|
|
234
|
+
attributes[cluebaseAuthoredPropertyAttribute("failure_type")] = failureType;
|
|
235
|
+
}
|
|
236
|
+
if (outcomeClass)
|
|
237
|
+
attributes["cluebase.outcome_class"] = outcomeClass;
|
|
238
|
+
if (statusCode !== null) {
|
|
239
|
+
attributes["http.status_code"] = statusCode;
|
|
240
|
+
attributes["http.response.status_code"] = statusCode;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
event_id: eventId,
|
|
245
|
+
source_event_id: eventId,
|
|
246
|
+
source_schema_version: SOURCE_SCHEMA_VERSION,
|
|
247
|
+
source_event_type: SDK_SOURCE_SIGNAL_EVENT_TYPE,
|
|
248
|
+
source_event_kind: SDK_SOURCE_SIGNAL_EVENT_TYPE,
|
|
249
|
+
producer_id: context.producer_id,
|
|
250
|
+
service_name: EVENT_SOURCE,
|
|
251
|
+
event_name: SDK_SOURCE_SIGNAL_EVENT_NAME,
|
|
252
|
+
event_category: "context",
|
|
253
|
+
event_source: EVENT_SOURCE,
|
|
254
|
+
event_version: EVENT_VERSION,
|
|
255
|
+
occurred_at: occurredAt,
|
|
256
|
+
ingested_at: new Date().toISOString(),
|
|
257
|
+
status: "observed",
|
|
258
|
+
sdk_collection_mode: context.sdk_collection_mode,
|
|
259
|
+
privacy_state: "masked",
|
|
260
|
+
privacy_level: "default_masked",
|
|
261
|
+
masking_state: "masked",
|
|
262
|
+
allowed_value_keys: [],
|
|
263
|
+
privacy_summary: context.privacy_summary,
|
|
264
|
+
trace_id: traceId,
|
|
265
|
+
request_id: requestId,
|
|
266
|
+
request_span_id: requestSpanId,
|
|
267
|
+
interaction_id: interactionId,
|
|
268
|
+
frontend_release: context.frontend_release,
|
|
269
|
+
backend_release: null,
|
|
270
|
+
properties: {},
|
|
271
|
+
metrics: {},
|
|
272
|
+
sdk_signal: {
|
|
273
|
+
surface: "frontend",
|
|
274
|
+
kind: "span",
|
|
275
|
+
name: raw.type,
|
|
276
|
+
span_kind: "INTERNAL",
|
|
277
|
+
trace_id: traceId,
|
|
278
|
+
span_id: eventId,
|
|
279
|
+
parent_span_id: null,
|
|
280
|
+
start_time_unix_nano: `${BigInt(occurredAtMs) * 1000000n}`,
|
|
281
|
+
end_time_unix_nano: `${BigInt(occurredAtMs) * 1000000n}`,
|
|
282
|
+
status_code: null,
|
|
283
|
+
status_message: null,
|
|
284
|
+
instrumentation_scope_name: "@genn-inc/cluebase-frontend-sdk",
|
|
285
|
+
attributes,
|
|
286
|
+
events: [],
|
|
287
|
+
resource: toFrontendSourceResource(options.resource, options.deniedKeys),
|
|
288
|
+
context: contextRecord(context, {
|
|
289
|
+
session_id: sessionId,
|
|
290
|
+
trace_id: traceId,
|
|
291
|
+
request_id: requestId,
|
|
292
|
+
request_span_id: requestSpanId,
|
|
293
|
+
interaction_id: interactionId,
|
|
294
|
+
}),
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { RETRY_CONFIG } from "../provider/constants.js";
|
|
2
|
+
export type RetryPolicy = {
|
|
3
|
+
maxAttempts: number;
|
|
4
|
+
baseDelayMs: number;
|
|
5
|
+
maxDelayMs: number;
|
|
6
|
+
maxAdditiveJitterMs: number;
|
|
7
|
+
};
|
|
8
|
+
export declare const computeRetryDelay: (attempt: number, policy: RetryPolicy) => number;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { RETRY_CONFIG } from "../provider/constants.js";
|
|
2
|
+
export const computeRetryDelay = (attempt, policy) => {
|
|
3
|
+
const exponent = Math.max(0, attempt - 1);
|
|
4
|
+
const raw = policy.baseDelayMs * 2 ** exponent;
|
|
5
|
+
const capped = Math.min(raw, policy.maxDelayMs);
|
|
6
|
+
const jitter = Math.floor(Math.random() * Math.min(policy.maxAdditiveJitterMs, capped));
|
|
7
|
+
return capped + jitter;
|
|
8
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { detectIdentityPII } from "../_generated/identity-pii-policy.js";
|
|
2
|
+
export type SanitizationAccumulator = {
|
|
3
|
+
maskedValueCount: number;
|
|
4
|
+
droppedFieldCount: number;
|
|
5
|
+
maskedFields: Set<string>;
|
|
6
|
+
};
|
|
7
|
+
export declare const isDeniedKey: (key: string, deniedKeys?: readonly string[]) => boolean;
|
|
8
|
+
export declare const sanitizeHeaders: (headers: Record<string, string>, deniedKeys?: string[]) => Record<string, string>;
|
|
9
|
+
export declare const sanitizeObject: (value: unknown, deniedKeys?: string[], summary?: SanitizationAccumulator, pathPrefix?: string) => unknown;
|
|
10
|
+
export declare const parseUrlForNetwork: (rawUrl: string) => {
|
|
11
|
+
host: string;
|
|
12
|
+
pathTemplate: string;
|
|
13
|
+
path: string;
|
|
14
|
+
query: Record<string, string | string[]>;
|
|
15
|
+
urlMaskedQuery: string;
|
|
16
|
+
};
|
|
17
|
+
export declare const maybeParseJson: (value: unknown) => unknown;
|
|
18
|
+
export { detectIdentityPII };
|
|
19
|
+
/**
|
|
20
|
+
* Synchronous deterministic SHA-256 hash for identity values that look
|
|
21
|
+
* like PII. The identity sanitization path must remain synchronous
|
|
22
|
+
* because `identify(userId)` is sync-only by SDK contract.
|
|
23
|
+
*
|
|
24
|
+
* Output format: `cluebase_pii_hashed:<32-hex>`.
|
|
25
|
+
*
|
|
26
|
+
* The algorithm name, the digest length and the prefix are read from the
|
|
27
|
+
* generated policy, not written here, so the three SDKs produce the same
|
|
28
|
+
* token for the same input by construction rather than by assertion.
|
|
29
|
+
*/
|
|
30
|
+
export declare const hashIdentityValue: (value: string) => string;
|