@clue-ai/browser-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 +100 -0
- package/dist/authoring/overlay.d.ts +12 -0
- package/dist/authoring/overlay.js +468 -0
- package/dist/authoring/recording.d.ts +125 -0
- package/dist/authoring/recording.js +481 -0
- package/dist/authoring/service-logo.d.ts +1 -0
- package/dist/authoring/service-logo.generated.d.ts +1 -0
- package/dist/authoring/service-logo.generated.js +3 -0
- package/dist/authoring/service-logo.js +1 -0
- package/dist/authoring/session.d.ts +23 -0
- package/dist/authoring/session.js +127 -0
- package/dist/authoring/surface.d.ts +11 -0
- package/dist/authoring/surface.js +63 -0
- package/dist/authoring/toolbar-constants.d.ts +23 -0
- package/dist/authoring/toolbar-constants.js +42 -0
- package/dist/authoring/toolbar-drag.d.ts +29 -0
- package/dist/authoring/toolbar-drag.js +270 -0
- package/dist/authoring/toolbar-view.d.ts +21 -0
- package/dist/authoring/toolbar-view.js +2584 -0
- package/dist/capture/action.d.ts +2 -0
- package/dist/capture/action.js +62 -0
- package/dist/capture/dom.d.ts +23 -0
- package/dist/capture/dom.js +329 -0
- package/dist/capture/drag.d.ts +2 -0
- package/dist/capture/drag.js +75 -0
- package/dist/capture/error.d.ts +2 -0
- package/dist/capture/error.js +193 -0
- package/dist/capture/form.d.ts +2 -0
- package/dist/capture/form.js +137 -0
- package/dist/capture/frustration.d.ts +2 -0
- package/dist/capture/frustration.js +171 -0
- package/dist/capture/input.d.ts +2 -0
- package/dist/capture/input.js +109 -0
- package/dist/capture/location.d.ts +10 -0
- package/dist/capture/location.js +42 -0
- package/dist/capture/navigation.d.ts +2 -0
- package/dist/capture/navigation.js +100 -0
- package/dist/capture/network.d.ts +13 -0
- package/dist/capture/network.js +903 -0
- package/dist/capture/page.d.ts +2 -0
- package/dist/capture/page.js +78 -0
- package/dist/capture/performance.d.ts +2 -0
- package/dist/capture/performance.js +268 -0
- package/dist/context/account.d.ts +12 -0
- package/dist/context/account.js +129 -0
- package/dist/context/environment.d.ts +42 -0
- package/dist/context/environment.js +208 -0
- package/dist/context/identity.d.ts +14 -0
- package/dist/context/identity.js +123 -0
- package/dist/context/session.d.ts +28 -0
- package/dist/context/session.js +155 -0
- package/dist/context/tab.d.ts +22 -0
- package/dist/context/tab.js +142 -0
- package/dist/context/trace.d.ts +32 -0
- package/dist/context/trace.js +65 -0
- package/dist/core/config.d.ts +4 -0
- package/dist/core/config.js +199 -0
- package/dist/core/constants.d.ts +43 -0
- package/dist/core/constants.js +109 -0
- package/dist/core/contracts.d.ts +58 -0
- package/dist/core/contracts.js +53 -0
- package/dist/core/sdk.d.ts +2 -0
- package/dist/core/sdk.js +831 -0
- package/dist/core/types.d.ts +413 -0
- package/dist/core/types.js +1 -0
- package/dist/core/usage-governor.d.ts +7 -0
- package/dist/core/usage-governor.js +127 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +36 -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 +37 -0
- package/dist/internal/metrics.d.ts +9 -0
- package/dist/internal/metrics.js +38 -0
- package/dist/normalize/builders.d.ts +15 -0
- package/dist/normalize/builders.js +786 -0
- package/dist/normalize/canonical.d.ts +13 -0
- package/dist/normalize/canonical.js +77 -0
- package/dist/normalize/event-id.d.ts +8 -0
- package/dist/normalize/event-id.js +39 -0
- package/dist/normalize/path-template.d.ts +1 -0
- package/dist/normalize/path-template.js +33 -0
- package/dist/privacy/local-minimization.d.ts +29 -0
- package/dist/privacy/local-minimization.js +88 -0
- package/dist/privacy/mask.d.ts +7 -0
- package/dist/privacy/mask.js +60 -0
- package/dist/privacy/parameter-snapshot.d.ts +14 -0
- package/dist/privacy/parameter-snapshot.js +206 -0
- package/dist/privacy/sanitize.d.ts +11 -0
- package/dist/privacy/sanitize.js +145 -0
- package/dist/privacy/schema-evidence.d.ts +20 -0
- package/dist/privacy/schema-evidence.js +238 -0
- package/dist/transport/batch.d.ts +37 -0
- package/dist/transport/batch.js +182 -0
- package/dist/transport/client.d.ts +61 -0
- package/dist/transport/client.js +267 -0
- package/dist/transport/queue.d.ts +22 -0
- package/dist/transport/queue.js +56 -0
- package/dist/transport/retry.d.ts +14 -0
- package/dist/transport/retry.js +46 -0
- package/package.json +38 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { ALWAYS_DENIED_KEYS, MASKED_VALUE, MAX_SANITIZE_DEPTH, } from "../core/constants";
|
|
2
|
+
import { toPathTemplate } from "../normalize/path-template";
|
|
3
|
+
const normalizeHeaderKey = (key) => key.trim().toLowerCase();
|
|
4
|
+
const normalizeLookupKey = (key) => key
|
|
5
|
+
.trim()
|
|
6
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
7
|
+
.toLowerCase()
|
|
8
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
9
|
+
.replace(/^_+|_+$/g, "");
|
|
10
|
+
const toDeniedSet = (deniedKeys) => {
|
|
11
|
+
return new Set([...ALWAYS_DENIED_KEYS, ...deniedKeys]
|
|
12
|
+
.map((entry) => normalizeLookupKey(entry))
|
|
13
|
+
.filter(Boolean));
|
|
14
|
+
};
|
|
15
|
+
const isDeniedKeyWithSet = (key, deniedSet) => {
|
|
16
|
+
const normalized = normalizeLookupKey(key);
|
|
17
|
+
if (!normalized) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
if (deniedSet.has(normalized)) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
const padded = `_${normalized}_`;
|
|
24
|
+
for (const denied of deniedSet) {
|
|
25
|
+
if (padded.includes(`_${denied}_`)) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
};
|
|
31
|
+
export const isDeniedKey = (key, deniedKeys = []) => {
|
|
32
|
+
return isDeniedKeyWithSet(key, toDeniedSet(deniedKeys));
|
|
33
|
+
};
|
|
34
|
+
export const sanitizeHeaders = (headers, deniedKeys = []) => {
|
|
35
|
+
const deniedSet = toDeniedSet(deniedKeys);
|
|
36
|
+
const sanitized = {};
|
|
37
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
38
|
+
if (isDeniedKeyWithSet(key, deniedSet)) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
sanitized[normalizeHeaderKey(key)] = value;
|
|
42
|
+
}
|
|
43
|
+
return sanitized;
|
|
44
|
+
};
|
|
45
|
+
export const sanitizeObject = (value, deniedKeys = []) => {
|
|
46
|
+
const deniedSet = toDeniedSet(deniedKeys);
|
|
47
|
+
const seen = new WeakSet();
|
|
48
|
+
const walk = (current, depth) => {
|
|
49
|
+
if (current == null) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
if (depth > MAX_SANITIZE_DEPTH) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
if (Array.isArray(current)) {
|
|
56
|
+
return current.map((entry) => {
|
|
57
|
+
const next = walk(entry, depth + 1);
|
|
58
|
+
return next === undefined ? null : next;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (typeof current === "object") {
|
|
62
|
+
const objectValue = current;
|
|
63
|
+
if (seen.has(objectValue)) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
seen.add(objectValue);
|
|
67
|
+
const sanitized = {};
|
|
68
|
+
for (const [key, child] of Object.entries(objectValue)) {
|
|
69
|
+
if (isDeniedKeyWithSet(key, deniedSet)) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const next = walk(child, depth + 1);
|
|
73
|
+
if (next === undefined) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
sanitized[key] = next;
|
|
77
|
+
}
|
|
78
|
+
seen.delete(objectValue);
|
|
79
|
+
return sanitized;
|
|
80
|
+
}
|
|
81
|
+
if (typeof current === "function" || typeof current === "symbol") {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
return current;
|
|
85
|
+
};
|
|
86
|
+
return walk(value, 0);
|
|
87
|
+
};
|
|
88
|
+
export const parseUrlForNetwork = (rawUrl) => {
|
|
89
|
+
const toQueryRecord = (url) => {
|
|
90
|
+
const query = {};
|
|
91
|
+
url.searchParams.forEach((value, key) => {
|
|
92
|
+
const current = query[key];
|
|
93
|
+
if (current === undefined) {
|
|
94
|
+
query[key] = value;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (Array.isArray(current)) {
|
|
98
|
+
query[key] = [...current, value];
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
query[key] = [current, value];
|
|
102
|
+
});
|
|
103
|
+
return query;
|
|
104
|
+
};
|
|
105
|
+
try {
|
|
106
|
+
const url = new URL(rawUrl, globalThis.location?.origin);
|
|
107
|
+
const query = url.search ? `?${MASKED_VALUE}` : "";
|
|
108
|
+
return {
|
|
109
|
+
host: url.host,
|
|
110
|
+
pathTemplate: toPathTemplate(url.pathname),
|
|
111
|
+
path: url.pathname,
|
|
112
|
+
query: toQueryRecord(url),
|
|
113
|
+
urlMaskedQuery: `${url.origin}${url.pathname}${query}`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return {
|
|
118
|
+
host: "",
|
|
119
|
+
pathTemplate: "/",
|
|
120
|
+
path: "/",
|
|
121
|
+
query: {},
|
|
122
|
+
urlMaskedQuery: MASKED_VALUE,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
export const maybeParseJson = (value) => {
|
|
127
|
+
if (typeof value !== "string") {
|
|
128
|
+
return value;
|
|
129
|
+
}
|
|
130
|
+
const trimmed = value.trim();
|
|
131
|
+
if (!trimmed) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
const isJsonLike = (trimmed.startsWith("{") && trimmed.endsWith("}")) ||
|
|
135
|
+
(trimmed.startsWith("[") && trimmed.endsWith("]"));
|
|
136
|
+
if (!isJsonLike) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
return JSON.parse(trimmed);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
type SchemaEvidence = {
|
|
2
|
+
schemaHash: string | null;
|
|
3
|
+
fieldPaths: string[];
|
|
4
|
+
shapeSize: number;
|
|
5
|
+
};
|
|
6
|
+
export declare const buildSchemaEvidence: (value: unknown) => SchemaEvidence;
|
|
7
|
+
export declare const extractErrorCodeCandidate: (value: unknown) => string | null;
|
|
8
|
+
export declare const deriveOutcomeClass: (input: {
|
|
9
|
+
statusCode?: number | null;
|
|
10
|
+
failureType?: string | null;
|
|
11
|
+
responseBody?: unknown;
|
|
12
|
+
}) => string;
|
|
13
|
+
export type GraphqlEvidence = {
|
|
14
|
+
requestKind: "graphql" | null;
|
|
15
|
+
graphqlOperationName: string | null;
|
|
16
|
+
graphqlOperationType: "query" | "mutation" | "subscription" | null;
|
|
17
|
+
graphqlTopLevelFields: string[];
|
|
18
|
+
};
|
|
19
|
+
export declare const extractGraphqlEvidence: (body: unknown) => GraphqlEvidence;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
const MAX_FIELD_PATHS = 100;
|
|
2
|
+
const MAX_DEPTH = 8;
|
|
3
|
+
const fnv1a = (value) => {
|
|
4
|
+
let hash = 0x811c9dc5;
|
|
5
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
6
|
+
hash ^= value.charCodeAt(index);
|
|
7
|
+
hash = Math.imul(hash, 0x01000193);
|
|
8
|
+
}
|
|
9
|
+
return `fnv1a:${(hash >>> 0).toString(16).padStart(8, "0")}`;
|
|
10
|
+
};
|
|
11
|
+
const valueType = (value) => {
|
|
12
|
+
if (value === null) {
|
|
13
|
+
return "null";
|
|
14
|
+
}
|
|
15
|
+
if (Array.isArray(value)) {
|
|
16
|
+
return "array";
|
|
17
|
+
}
|
|
18
|
+
if (typeof value === "object") {
|
|
19
|
+
return "object";
|
|
20
|
+
}
|
|
21
|
+
return typeof value;
|
|
22
|
+
};
|
|
23
|
+
const buildShape = (value, path, fieldPaths, depth) => {
|
|
24
|
+
if (depth > MAX_DEPTH) {
|
|
25
|
+
return { type: "max_depth" };
|
|
26
|
+
}
|
|
27
|
+
if (Array.isArray(value)) {
|
|
28
|
+
if (path && fieldPaths.length < MAX_FIELD_PATHS) {
|
|
29
|
+
fieldPaths.push(`${path}[]`);
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
type: "array",
|
|
33
|
+
items: value.length > 0
|
|
34
|
+
? buildShape(value[0], `${path}[]`, fieldPaths, depth + 1)
|
|
35
|
+
: "empty",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (value && typeof value === "object") {
|
|
39
|
+
const record = value;
|
|
40
|
+
const fields = {};
|
|
41
|
+
for (const key of Object.keys(record).sort()) {
|
|
42
|
+
const childPath = path ? `${path}.${key}` : key;
|
|
43
|
+
if (fieldPaths.length < MAX_FIELD_PATHS) {
|
|
44
|
+
fieldPaths.push(childPath);
|
|
45
|
+
}
|
|
46
|
+
fields[key] = buildShape(record[key], childPath, fieldPaths, depth + 1);
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
type: "object",
|
|
50
|
+
fields,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return { type: valueType(value) };
|
|
54
|
+
};
|
|
55
|
+
export const buildSchemaEvidence = (value) => {
|
|
56
|
+
if (value === null || value === undefined) {
|
|
57
|
+
return {
|
|
58
|
+
schemaHash: null,
|
|
59
|
+
fieldPaths: [],
|
|
60
|
+
shapeSize: 0,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const fieldPaths = [];
|
|
64
|
+
const shape = buildShape(value, "", fieldPaths, 0);
|
|
65
|
+
const uniqueFieldPaths = [...new Set(fieldPaths)].sort();
|
|
66
|
+
return {
|
|
67
|
+
schemaHash: fnv1a(JSON.stringify(shape)),
|
|
68
|
+
fieldPaths: uniqueFieldPaths,
|
|
69
|
+
shapeSize: uniqueFieldPaths.length,
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
const sanitizeToken = (value) => {
|
|
73
|
+
if (typeof value !== "string") {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const trimmed = value.trim();
|
|
77
|
+
if (!trimmed || trimmed.length > 96) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
if (!/^[A-Za-z0-9_.:-]+$/.test(trimmed)) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
return trimmed.toLowerCase();
|
|
84
|
+
};
|
|
85
|
+
export const extractErrorCodeCandidate = (value) => {
|
|
86
|
+
if (!value || typeof value !== "object") {
|
|
87
|
+
return sanitizeToken(value);
|
|
88
|
+
}
|
|
89
|
+
if (Array.isArray(value)) {
|
|
90
|
+
for (const entry of value) {
|
|
91
|
+
const candidate = extractErrorCodeCandidate(entry);
|
|
92
|
+
if (candidate) {
|
|
93
|
+
return candidate;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const record = value;
|
|
99
|
+
for (const key of ["code", "error_code", "type", "error"]) {
|
|
100
|
+
const candidate = sanitizeToken(record[key]);
|
|
101
|
+
if (candidate) {
|
|
102
|
+
return candidate;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
for (const key of ["errors", "detail", "extensions"]) {
|
|
106
|
+
const candidate = extractErrorCodeCandidate(record[key]);
|
|
107
|
+
if (candidate) {
|
|
108
|
+
return candidate;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
};
|
|
113
|
+
export const deriveOutcomeClass = (input) => {
|
|
114
|
+
const failureType = input.failureType ?? null;
|
|
115
|
+
if (failureType) {
|
|
116
|
+
if (failureType === "AbortError" || failureType.includes("abort")) {
|
|
117
|
+
return "aborted";
|
|
118
|
+
}
|
|
119
|
+
if (failureType.includes("timeout")) {
|
|
120
|
+
return "timeout";
|
|
121
|
+
}
|
|
122
|
+
return "network_failed";
|
|
123
|
+
}
|
|
124
|
+
const statusCode = input.statusCode ?? 0;
|
|
125
|
+
if (statusCode === 401) {
|
|
126
|
+
return "auth_denied";
|
|
127
|
+
}
|
|
128
|
+
if (statusCode === 403) {
|
|
129
|
+
return "permission_denied";
|
|
130
|
+
}
|
|
131
|
+
if (statusCode === 409) {
|
|
132
|
+
return "conflict";
|
|
133
|
+
}
|
|
134
|
+
if (statusCode === 422) {
|
|
135
|
+
return "validation_error";
|
|
136
|
+
}
|
|
137
|
+
if (statusCode === 429) {
|
|
138
|
+
return "rate_limited";
|
|
139
|
+
}
|
|
140
|
+
if (statusCode >= 500) {
|
|
141
|
+
return "server_error";
|
|
142
|
+
}
|
|
143
|
+
if (statusCode >= 400) {
|
|
144
|
+
return "client_error";
|
|
145
|
+
}
|
|
146
|
+
return "success";
|
|
147
|
+
};
|
|
148
|
+
const stripGraphqlNoise = (query) => {
|
|
149
|
+
return query
|
|
150
|
+
.replace(/#[^\n\r]*/g, "")
|
|
151
|
+
.replace(/"([^"\\]|\\.)*"/g, "\"\"")
|
|
152
|
+
.replace(/'([^'\\]|\\.)*'/g, "''");
|
|
153
|
+
};
|
|
154
|
+
const readTopLevelGraphqlFields = (query) => {
|
|
155
|
+
const openIndex = query.indexOf("{");
|
|
156
|
+
if (openIndex < 0) {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
const fields = [];
|
|
160
|
+
let depth = 0;
|
|
161
|
+
let parenDepth = 0;
|
|
162
|
+
let token = "";
|
|
163
|
+
for (let index = openIndex; index < query.length; index += 1) {
|
|
164
|
+
const char = query[index];
|
|
165
|
+
if (char === "(") {
|
|
166
|
+
if (depth === 1) {
|
|
167
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(token) && token !== "on") {
|
|
168
|
+
fields.push(token);
|
|
169
|
+
}
|
|
170
|
+
token = "";
|
|
171
|
+
}
|
|
172
|
+
parenDepth += 1;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (char === ")") {
|
|
176
|
+
parenDepth = Math.max(0, parenDepth - 1);
|
|
177
|
+
token = "";
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (char === "{") {
|
|
181
|
+
depth += 1;
|
|
182
|
+
token = "";
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (char === "}") {
|
|
186
|
+
if (depth === 1 && /^[A-Za-z_][A-Za-z0-9_]*$/.test(token)) {
|
|
187
|
+
fields.push(token);
|
|
188
|
+
}
|
|
189
|
+
depth -= 1;
|
|
190
|
+
token = "";
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (depth !== 1 || parenDepth > 0) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (/[A-Za-z0-9_]/.test(char)) {
|
|
197
|
+
token += char;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(token) && token !== "on") {
|
|
201
|
+
fields.push(token);
|
|
202
|
+
}
|
|
203
|
+
token = "";
|
|
204
|
+
}
|
|
205
|
+
return [...new Set(fields)].slice(0, 20).sort();
|
|
206
|
+
};
|
|
207
|
+
export const extractGraphqlEvidence = (body) => {
|
|
208
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
209
|
+
return {
|
|
210
|
+
requestKind: null,
|
|
211
|
+
graphqlOperationName: null,
|
|
212
|
+
graphqlOperationType: null,
|
|
213
|
+
graphqlTopLevelFields: [],
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
const record = body;
|
|
217
|
+
const query = typeof record.query === "string" ? record.query : null;
|
|
218
|
+
if (!query) {
|
|
219
|
+
return {
|
|
220
|
+
requestKind: null,
|
|
221
|
+
graphqlOperationName: null,
|
|
222
|
+
graphqlOperationType: null,
|
|
223
|
+
graphqlTopLevelFields: [],
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
const normalizedQuery = stripGraphqlNoise(query);
|
|
227
|
+
const operationMatch = normalizedQuery.match(/\b(query|mutation|subscription)\s*([A-Za-z_][A-Za-z0-9_]*)?/);
|
|
228
|
+
const operationType = operationMatch?.[1] ?? "query";
|
|
229
|
+
const operationName = typeof record.operationName === "string" && record.operationName.trim()
|
|
230
|
+
? record.operationName.trim()
|
|
231
|
+
: operationMatch?.[2] ?? null;
|
|
232
|
+
return {
|
|
233
|
+
requestKind: "graphql",
|
|
234
|
+
graphqlOperationName: operationName,
|
|
235
|
+
graphqlOperationType: operationType,
|
|
236
|
+
graphqlTopLevelFields: readTopLevelGraphqlFields(normalizedQuery),
|
|
237
|
+
};
|
|
238
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { CanonicalBaseEvent, IngestUsageGovernorFeedback } from "../core/types";
|
|
2
|
+
import type { InternalMetricsStore } from "../internal/metrics";
|
|
3
|
+
import type { IngestClient } from "./client";
|
|
4
|
+
export type BatchTransportOptions = {
|
|
5
|
+
enabled: boolean;
|
|
6
|
+
flushIntervalMs: number;
|
|
7
|
+
maxPayloadBytes: number;
|
|
8
|
+
client: IngestClient;
|
|
9
|
+
metrics?: InternalMetricsStore;
|
|
10
|
+
queueMaxBytes?: number;
|
|
11
|
+
onGovernorFeedback?: (feedback: IngestUsageGovernorFeedback) => void;
|
|
12
|
+
};
|
|
13
|
+
export declare class BatchTransport {
|
|
14
|
+
private readonly queue;
|
|
15
|
+
private readonly client;
|
|
16
|
+
private readonly enabled;
|
|
17
|
+
private readonly flushIntervalMs;
|
|
18
|
+
private readonly maxPayloadBytes;
|
|
19
|
+
private readonly flushThresholdBytes;
|
|
20
|
+
private readonly metrics;
|
|
21
|
+
private readonly onGovernorFeedback;
|
|
22
|
+
private idleHandle;
|
|
23
|
+
private flushing;
|
|
24
|
+
private flushAgainAfterCurrent;
|
|
25
|
+
private pendingBatches;
|
|
26
|
+
constructor(options: BatchTransportOptions);
|
|
27
|
+
start(): void;
|
|
28
|
+
stop(): void;
|
|
29
|
+
enqueue(event: CanonicalBaseEvent): void;
|
|
30
|
+
private scheduleIdleFlush;
|
|
31
|
+
flush(): Promise<void>;
|
|
32
|
+
flushOnUnload(): void;
|
|
33
|
+
clear(): void;
|
|
34
|
+
private partitionEvents;
|
|
35
|
+
private splitBatch;
|
|
36
|
+
private shouldRetainFailedBatch;
|
|
37
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { EventQueue } from "./queue";
|
|
2
|
+
export class BatchTransport {
|
|
3
|
+
constructor(options) {
|
|
4
|
+
this.enabled = options.enabled;
|
|
5
|
+
this.flushIntervalMs = Math.max(100, options.flushIntervalMs);
|
|
6
|
+
this.maxPayloadBytes = Math.max(1024, options.maxPayloadBytes);
|
|
7
|
+
this.flushThresholdBytes = Math.floor(this.maxPayloadBytes * 0.8);
|
|
8
|
+
this.client = options.client;
|
|
9
|
+
this.metrics = options.metrics ?? null;
|
|
10
|
+
this.onGovernorFeedback = options.onGovernorFeedback ?? null;
|
|
11
|
+
this.queue = new EventQueue({
|
|
12
|
+
metrics: this.metrics ?? undefined,
|
|
13
|
+
maxItems: Number.MAX_SAFE_INTEGER,
|
|
14
|
+
maxBytes: options.queueMaxBytes,
|
|
15
|
+
});
|
|
16
|
+
this.idleHandle = null;
|
|
17
|
+
this.flushing = false;
|
|
18
|
+
this.flushAgainAfterCurrent = false;
|
|
19
|
+
this.pendingBatches = [];
|
|
20
|
+
}
|
|
21
|
+
start() {
|
|
22
|
+
if (!this.enabled) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
stop() {
|
|
27
|
+
if (this.idleHandle) {
|
|
28
|
+
clearTimeout(this.idleHandle);
|
|
29
|
+
this.idleHandle = null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
enqueue(event) {
|
|
33
|
+
this.queue.enqueue(event);
|
|
34
|
+
this.scheduleIdleFlush();
|
|
35
|
+
if (!this.enabled) {
|
|
36
|
+
void this.flush();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (this.queue.bytes() >= this.flushThresholdBytes) {
|
|
40
|
+
void this.flush();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
scheduleIdleFlush() {
|
|
44
|
+
if (!this.enabled) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (this.idleHandle) {
|
|
48
|
+
clearTimeout(this.idleHandle);
|
|
49
|
+
}
|
|
50
|
+
this.idleHandle = setTimeout(() => {
|
|
51
|
+
this.idleHandle = null;
|
|
52
|
+
void this.flush();
|
|
53
|
+
}, this.flushIntervalMs);
|
|
54
|
+
}
|
|
55
|
+
async flush() {
|
|
56
|
+
if (this.flushing) {
|
|
57
|
+
if (!this.enabled) {
|
|
58
|
+
this.flushAgainAfterCurrent = true;
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (this.queue.isEmpty() && this.pendingBatches.length === 0) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
this.flushing = true;
|
|
66
|
+
try {
|
|
67
|
+
if (this.pendingBatches.length === 0) {
|
|
68
|
+
this.pendingBatches.push(...this.partitionEvents(this.queue.drain()));
|
|
69
|
+
}
|
|
70
|
+
while (this.pendingBatches.length > 0) {
|
|
71
|
+
const batch = this.pendingBatches.shift();
|
|
72
|
+
if (!batch || batch.events.length === 0) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
this.metrics?.increment("batch_flush_count");
|
|
76
|
+
const result = await this.client.send(batch.events, {
|
|
77
|
+
maxPayloadBytes: this.maxPayloadBytes,
|
|
78
|
+
metadata: batch.metadata,
|
|
79
|
+
});
|
|
80
|
+
if (result.governor) {
|
|
81
|
+
this.onGovernorFeedback?.(result.governor);
|
|
82
|
+
}
|
|
83
|
+
if (!result.ok) {
|
|
84
|
+
if (result.retryStrategy === "split_batch") {
|
|
85
|
+
const splitBatches = this.splitBatch(batch);
|
|
86
|
+
if (splitBatches.length > 0) {
|
|
87
|
+
this.pendingBatches.unshift(...splitBatches);
|
|
88
|
+
this.scheduleIdleFlush();
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
this.metrics?.increment("dropped_count", batch.events.length);
|
|
92
|
+
this.metrics?.increment("oversized_event_dropped_count");
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (this.shouldRetainFailedBatch(result.retryStrategy)) {
|
|
96
|
+
this.pendingBatches.unshift(batch);
|
|
97
|
+
this.scheduleIdleFlush();
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
this.metrics?.increment("dropped_count", batch.events.length);
|
|
101
|
+
this.metrics?.increment("transport_failed_count");
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
this.flushing = false;
|
|
108
|
+
}
|
|
109
|
+
if (!this.enabled &&
|
|
110
|
+
(this.flushAgainAfterCurrent ||
|
|
111
|
+
this.pendingBatches.length > 0 ||
|
|
112
|
+
!this.queue.isEmpty())) {
|
|
113
|
+
this.flushAgainAfterCurrent = false;
|
|
114
|
+
await this.flush();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
flushOnUnload() {
|
|
118
|
+
this.stop();
|
|
119
|
+
const pendingBatches = [
|
|
120
|
+
...this.pendingBatches,
|
|
121
|
+
...this.partitionEvents(this.queue.drain()),
|
|
122
|
+
];
|
|
123
|
+
this.pendingBatches = [];
|
|
124
|
+
for (const batch of pendingBatches) {
|
|
125
|
+
void this.client.send(batch.events, {
|
|
126
|
+
useBeacon: true,
|
|
127
|
+
maxPayloadBytes: this.maxPayloadBytes,
|
|
128
|
+
metadata: batch.metadata,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
clear() {
|
|
133
|
+
this.queue.clear();
|
|
134
|
+
this.pendingBatches = [];
|
|
135
|
+
}
|
|
136
|
+
partitionEvents(events) {
|
|
137
|
+
const batches = [];
|
|
138
|
+
let metadata = this.client.createBatchEnvelopeMetadata();
|
|
139
|
+
let current = [];
|
|
140
|
+
for (const event of events) {
|
|
141
|
+
const candidate = [...current, event];
|
|
142
|
+
if (current.length > 0 &&
|
|
143
|
+
this.client.measurePayloadBytes(candidate, metadata) >
|
|
144
|
+
this.maxPayloadBytes) {
|
|
145
|
+
batches.push({ events: current, metadata });
|
|
146
|
+
metadata = this.client.createBatchEnvelopeMetadata();
|
|
147
|
+
current = [];
|
|
148
|
+
}
|
|
149
|
+
if (this.client.measurePayloadBytes([event], metadata) >
|
|
150
|
+
this.maxPayloadBytes) {
|
|
151
|
+
this.metrics?.increment("dropped_count");
|
|
152
|
+
this.metrics?.increment("oversized_event_count");
|
|
153
|
+
this.metrics?.increment("oversized_event_dropped_count");
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
current.push(event);
|
|
157
|
+
}
|
|
158
|
+
if (current.length > 0) {
|
|
159
|
+
batches.push({ events: current, metadata });
|
|
160
|
+
}
|
|
161
|
+
return batches;
|
|
162
|
+
}
|
|
163
|
+
splitBatch(batch) {
|
|
164
|
+
if (batch.events.length <= 1) {
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
const midpoint = Math.ceil(batch.events.length / 2);
|
|
168
|
+
return [
|
|
169
|
+
{
|
|
170
|
+
events: batch.events.slice(0, midpoint),
|
|
171
|
+
metadata: this.client.createBatchEnvelopeMetadata(),
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
events: batch.events.slice(midpoint),
|
|
175
|
+
metadata: this.client.createBatchEnvelopeMetadata(),
|
|
176
|
+
},
|
|
177
|
+
].filter((child) => child.events.length > 0);
|
|
178
|
+
}
|
|
179
|
+
shouldRetainFailedBatch(retryStrategy) {
|
|
180
|
+
return retryStrategy === null || retryStrategy === "retry_same_batch";
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { CanonicalBaseEvent, ClueEnvironment, IngestUsageGovernorFeedback } from "../core/types";
|
|
2
|
+
import type { InternalMetricsStore } from "../internal/metrics";
|
|
3
|
+
export declare const CLUE_SDK_REQUEST_HEADER = "x-clue-sdk-request";
|
|
4
|
+
export declare const CLUE_ENVIRONMENT_HEADER = "x-clue-environment";
|
|
5
|
+
export declare const CLUE_SDK_VERSION_HEADER = "x-clue-sdk-version";
|
|
6
|
+
export declare const CLUE_SOURCE_SCHEMA_VERSION_HEADER = "x-clue-source-schema-version";
|
|
7
|
+
export declare const CLUE_SERVICE_KEY_HEADER = "x-clue-service-key";
|
|
8
|
+
export declare const CLUE_BROWSER_TOKEN_HEADER = "x-clue-browser-token";
|
|
9
|
+
export type IngestClientOptions = {
|
|
10
|
+
endpoint: string;
|
|
11
|
+
projectKey: string;
|
|
12
|
+
environment: ClueEnvironment;
|
|
13
|
+
serviceKey?: string;
|
|
14
|
+
producerId: string;
|
|
15
|
+
browserTokenProvider?: () => string | Promise<string>;
|
|
16
|
+
sdkType: "browser";
|
|
17
|
+
sdkVersion: string;
|
|
18
|
+
schemaVersion: number;
|
|
19
|
+
metrics?: InternalMetricsStore;
|
|
20
|
+
};
|
|
21
|
+
export type SendEventsOptions = {
|
|
22
|
+
keepalive?: boolean;
|
|
23
|
+
useBeacon?: boolean;
|
|
24
|
+
maxPayloadBytes?: number;
|
|
25
|
+
metadata?: BatchEnvelopeMetadata;
|
|
26
|
+
};
|
|
27
|
+
export type SendEventsResult = {
|
|
28
|
+
ok: boolean;
|
|
29
|
+
status: number | null;
|
|
30
|
+
attempts: number;
|
|
31
|
+
governor: IngestUsageGovernorFeedback | null;
|
|
32
|
+
retryStrategy: IngestRetryStrategy | null;
|
|
33
|
+
};
|
|
34
|
+
export type BatchEnvelopeMetadata = {
|
|
35
|
+
batch_id: string;
|
|
36
|
+
idempotency_key: string;
|
|
37
|
+
sent_at: string;
|
|
38
|
+
};
|
|
39
|
+
export type IngestRetryStrategy = "split_batch" | "retry_same_batch" | "stop_and_report" | "mark_failed" | "drop_event";
|
|
40
|
+
export declare class IngestClient {
|
|
41
|
+
private readonly endpoint;
|
|
42
|
+
private readonly projectKey;
|
|
43
|
+
private readonly environment;
|
|
44
|
+
private readonly serviceKey;
|
|
45
|
+
private readonly producerId;
|
|
46
|
+
private readonly browserTokenProvider;
|
|
47
|
+
private readonly sdkType;
|
|
48
|
+
private readonly sdkVersion;
|
|
49
|
+
private readonly schemaVersion;
|
|
50
|
+
private readonly metrics;
|
|
51
|
+
constructor(options: IngestClientOptions);
|
|
52
|
+
send(events: CanonicalBaseEvent[], options?: SendEventsOptions): Promise<SendEventsResult>;
|
|
53
|
+
measurePayloadBytes(events: CanonicalBaseEvent[], metadata?: BatchEnvelopeMetadata): number;
|
|
54
|
+
private stringifyPayload;
|
|
55
|
+
private withSourceEvidence;
|
|
56
|
+
createBatchEnvelopeMetadata(): BatchEnvelopeMetadata;
|
|
57
|
+
private createId;
|
|
58
|
+
private trySendBeacon;
|
|
59
|
+
private resolveBrowserToken;
|
|
60
|
+
private readIngestResponse;
|
|
61
|
+
}
|