@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.
Files changed (140) hide show
  1. package/README.md +222 -0
  2. package/dist/_generated/correlation-attribute-keys.d.ts +2 -0
  3. package/dist/_generated/correlation-attribute-keys.js +33 -0
  4. package/dist/_generated/identity-pii-policy.d.ts +38 -0
  5. package/dist/_generated/identity-pii-policy.js +103 -0
  6. package/dist/_generated/ingest-privacy-tables.d.ts +120 -0
  7. package/dist/_generated/ingest-privacy-tables.js +890 -0
  8. package/dist/_generated/otel-resource-keys.d.ts +21 -0
  9. package/dist/_generated/otel-resource-keys.js +72 -0
  10. package/dist/_generated/otel-span-conversion.d.ts +48 -0
  11. package/dist/_generated/otel-span-conversion.js +69 -0
  12. package/dist/_generated/parameter-snapshot-leaf.d.ts +11 -0
  13. package/dist/_generated/parameter-snapshot-leaf.js +37 -0
  14. package/dist/_generated/sdk-runtime-policy.d.ts +7 -0
  15. package/dist/_generated/sdk-runtime-policy.js +14 -0
  16. package/dist/_generated/usage-governor-tables.d.ts +15 -0
  17. package/dist/_generated/usage-governor-tables.js +44 -0
  18. package/dist/context/browser-storage.d.ts +15 -0
  19. package/dist/context/browser-storage.js +38 -0
  20. package/dist/context/environment.d.ts +37 -0
  21. package/dist/context/environment.js +229 -0
  22. package/dist/context/identity.d.ts +16 -0
  23. package/dist/context/identity.js +139 -0
  24. package/dist/context/organization.d.ts +16 -0
  25. package/dist/context/organization.js +60 -0
  26. package/dist/context/session.d.ts +28 -0
  27. package/dist/context/session.js +130 -0
  28. package/dist/context/tab.d.ts +22 -0
  29. package/dist/context/tab.js +117 -0
  30. package/dist/context/trace.d.ts +76 -0
  31. package/dist/context/trace.js +163 -0
  32. package/dist/exporter/batch.d.ts +94 -0
  33. package/dist/exporter/batch.js +485 -0
  34. package/dist/exporter/browser-outbox-records.d.ts +159 -0
  35. package/dist/exporter/browser-outbox-records.js +341 -0
  36. package/dist/exporter/browser-outbox.d.ts +112 -0
  37. package/dist/exporter/browser-outbox.js +876 -0
  38. package/dist/exporter/client.d.ts +92 -0
  39. package/dist/exporter/client.js +541 -0
  40. package/dist/exporter/delivery-response-contract.d.ts +103 -0
  41. package/dist/exporter/delivery-response-contract.js +147 -0
  42. package/dist/exporter/delivery-response.d.ts +37 -0
  43. package/dist/exporter/delivery-response.js +151 -0
  44. package/dist/exporter/diagnostics.d.ts +42 -0
  45. package/dist/exporter/diagnostics.js +148 -0
  46. package/dist/exporter/event-id.d.ts +10 -0
  47. package/dist/exporter/event-id.js +75 -0
  48. package/dist/exporter/frontend-resource.d.ts +20 -0
  49. package/dist/exporter/frontend-resource.js +52 -0
  50. package/dist/exporter/local-minimization.d.ts +29 -0
  51. package/dist/exporter/local-minimization.js +87 -0
  52. package/dist/exporter/observation-source-event-exporter.d.ts +69 -0
  53. package/dist/exporter/observation-source-event-exporter.js +154 -0
  54. package/dist/exporter/parameter-snapshot.d.ts +14 -0
  55. package/dist/exporter/parameter-snapshot.js +215 -0
  56. package/dist/exporter/path-template.d.ts +1 -0
  57. package/dist/exporter/path-template.js +67 -0
  58. package/dist/exporter/raw-event-to-source-signal.d.ts +12 -0
  59. package/dist/exporter/raw-event-to-source-signal.js +297 -0
  60. package/dist/exporter/retry.d.ts +8 -0
  61. package/dist/exporter/retry.js +8 -0
  62. package/dist/exporter/sanitize.d.ts +30 -0
  63. package/dist/exporter/sanitize.js +177 -0
  64. package/dist/exporter/schema-evidence.d.ts +20 -0
  65. package/dist/exporter/schema-evidence.js +232 -0
  66. package/dist/exporter/source-event.d.ts +99 -0
  67. package/dist/exporter/source-event.js +13 -0
  68. package/dist/exporter/span-to-source-signal.d.ts +8 -0
  69. package/dist/exporter/span-to-source-signal.js +174 -0
  70. package/dist/exporter/text-fingerprint.d.ts +9 -0
  71. package/dist/exporter/text-fingerprint.js +16 -0
  72. package/dist/index.d.ts +25 -0
  73. package/dist/index.js +158 -0
  74. package/dist/instrumentation/action.d.ts +2 -0
  75. package/dist/instrumentation/action.js +171 -0
  76. package/dist/instrumentation/cluebase-authored-attributes.d.ts +4 -0
  77. package/dist/instrumentation/cluebase-authored-attributes.js +32 -0
  78. package/dist/instrumentation/common.d.ts +73 -0
  79. package/dist/instrumentation/common.js +90 -0
  80. package/dist/instrumentation/dom.d.ts +63 -0
  81. package/dist/instrumentation/dom.js +640 -0
  82. package/dist/instrumentation/drag.d.ts +2 -0
  83. package/dist/instrumentation/drag.js +82 -0
  84. package/dist/instrumentation/error.d.ts +2 -0
  85. package/dist/instrumentation/error.js +318 -0
  86. package/dist/instrumentation/form.d.ts +2 -0
  87. package/dist/instrumentation/form.js +132 -0
  88. package/dist/instrumentation/input.d.ts +2 -0
  89. package/dist/instrumentation/input.js +180 -0
  90. package/dist/instrumentation/location.d.ts +11 -0
  91. package/dist/instrumentation/location.js +69 -0
  92. package/dist/instrumentation/naming-evidence.d.ts +20 -0
  93. package/dist/instrumentation/naming-evidence.js +11 -0
  94. package/dist/instrumentation/navigation.d.ts +2 -0
  95. package/dist/instrumentation/navigation.js +144 -0
  96. package/dist/instrumentation/network-request.d.ts +103 -0
  97. package/dist/instrumentation/network-request.js +285 -0
  98. package/dist/instrumentation/network-span-attributes.d.ts +25 -0
  99. package/dist/instrumentation/network-span-attributes.js +153 -0
  100. package/dist/instrumentation/network.d.ts +16 -0
  101. package/dist/instrumentation/network.js +502 -0
  102. package/dist/instrumentation/page.d.ts +2 -0
  103. package/dist/instrumentation/page.js +56 -0
  104. package/dist/instrumentation/performance.d.ts +2 -0
  105. package/dist/instrumentation/performance.js +365 -0
  106. package/dist/integrations/next-router.d.ts +16 -0
  107. package/dist/integrations/next-router.js +18 -0
  108. package/dist/integrations/react-router.d.ts +7 -0
  109. package/dist/integrations/react-router.js +61 -0
  110. package/dist/internal/internal-log.d.ts +23 -0
  111. package/dist/internal/internal-log.js +47 -0
  112. package/dist/internal/metrics.d.ts +10 -0
  113. package/dist/internal/metrics.js +50 -0
  114. package/dist/provider/_version.d.ts +1 -0
  115. package/dist/provider/_version.js +3 -0
  116. package/dist/provider/config.d.ts +12 -0
  117. package/dist/provider/config.js +177 -0
  118. package/dist/provider/constants.d.ts +62 -0
  119. package/dist/provider/constants.js +118 -0
  120. package/dist/provider/contracts.d.ts +51 -0
  121. package/dist/provider/contracts.js +46 -0
  122. package/dist/provider/privacy-policy-store.d.ts +20 -0
  123. package/dist/provider/privacy-policy-store.js +56 -0
  124. package/dist/provider/sampling.d.ts +2 -0
  125. package/dist/provider/sampling.js +17 -0
  126. package/dist/provider/sdk-event-pipeline.d.ts +60 -0
  127. package/dist/provider/sdk-event-pipeline.js +478 -0
  128. package/dist/provider/sdk-input-normalization.d.ts +7 -0
  129. package/dist/provider/sdk-input-normalization.js +113 -0
  130. package/dist/provider/sdk-otel-pipeline.d.ts +14 -0
  131. package/dist/provider/sdk-otel-pipeline.js +148 -0
  132. package/dist/provider/sdk-runtime-surfaces.d.ts +49 -0
  133. package/dist/provider/sdk-runtime-surfaces.js +216 -0
  134. package/dist/provider/sdk.d.ts +2 -0
  135. package/dist/provider/sdk.js +614 -0
  136. package/dist/provider/types.d.ts +323 -0
  137. package/dist/provider/types.js +1 -0
  138. package/dist/provider/usage-governor.d.ts +7 -0
  139. package/dist/provider/usage-governor.js +116 -0
  140. package/package.json +59 -0
@@ -0,0 +1,177 @@
1
+ import { sha256 } from "@noble/hashes/sha256";
2
+ import { bytesToHex, utf8ToBytes } from "@noble/hashes/utils";
3
+ // 個人情報の形を見分ける規則も、正本から生成した部品を読みます。
4
+ import { detectIdentityPII, PII_HASH_HEX_LENGTH, PII_HASH_PREFIX, } from "../_generated/identity-pii-policy.js";
5
+ // 落とす項目名・同形異字を畳む表・突き合わせの手順は、正本から生成した部品を
6
+ // 読みます。ここへ写すと受付側とずれます(AGENTS §0.2)。
7
+ import { isDeniedLookupKey, toDeniedLookupKeySet, } from "../_generated/ingest-privacy-tables.js";
8
+ import { toPathTemplate } from "../exporter/path-template.js";
9
+ import { ALWAYS_DENIED_KEYS, MASKED_VALUE, MAX_SANITIZE_DEPTH, } from "../provider/constants.js";
10
+ const normalizeHeaderKey = (key) => key.trim().toLowerCase();
11
+ const toDeniedSet = (deniedKeys) => toDeniedLookupKeySet([...ALWAYS_DENIED_KEYS, ...deniedKeys]);
12
+ const isDeniedKeyWithSet = (key, deniedSet) => isDeniedLookupKey(key, deniedSet);
13
+ const isSanitizableObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
14
+ export const isDeniedKey = (key, deniedKeys = []) => {
15
+ return isDeniedKeyWithSet(key, toDeniedSet(deniedKeys));
16
+ };
17
+ export const sanitizeHeaders = (headers, deniedKeys = []) => {
18
+ const deniedSet = toDeniedSet(deniedKeys);
19
+ const sanitized = {};
20
+ for (const [key, value] of Object.entries(headers)) {
21
+ if (isDeniedKeyWithSet(key, deniedSet)) {
22
+ continue;
23
+ }
24
+ sanitized[normalizeHeaderKey(key)] = value;
25
+ }
26
+ return sanitized;
27
+ };
28
+ export const sanitizeObject = (value, deniedKeys = [], summary, pathPrefix = "") => {
29
+ const deniedSet = toDeniedSet(deniedKeys);
30
+ const seen = new WeakSet();
31
+ const walk = (current, depth, path) => {
32
+ if (current == null) {
33
+ return null;
34
+ }
35
+ if (depth > MAX_SANITIZE_DEPTH) {
36
+ return null;
37
+ }
38
+ if (Array.isArray(current)) {
39
+ return current.map((entry, index) => {
40
+ const next = walk(entry, depth + 1, `${path}[${index}]`);
41
+ return next === undefined ? null : next;
42
+ });
43
+ }
44
+ if (isSanitizableObject(current)) {
45
+ const objectValue = current;
46
+ if (seen.has(objectValue)) {
47
+ return null;
48
+ }
49
+ seen.add(objectValue);
50
+ const sanitized = {};
51
+ for (const [key, child] of Object.entries(objectValue)) {
52
+ if (isDeniedKeyWithSet(key, deniedSet)) {
53
+ if (summary)
54
+ summary.droppedFieldCount += 1;
55
+ continue;
56
+ }
57
+ const childPath = path ? `${path}.${key}` : key;
58
+ const next = walk(child, depth + 1, childPath);
59
+ if (next === undefined) {
60
+ continue;
61
+ }
62
+ sanitized[key] = next;
63
+ }
64
+ seen.delete(objectValue);
65
+ return sanitized;
66
+ }
67
+ if (typeof current === "function" || typeof current === "symbol") {
68
+ return undefined;
69
+ }
70
+ // Value-level PII shape detection. The denied-key gate above catches
71
+ // `email = "..."`, but a non-denied key like
72
+ // `metadata.customer_contact = "alice@example.com"` would still leak
73
+ // the email verbatim. Replace any string whose shape matches an
74
+ // identity-PII pattern (email / phone / SSN / credit_card with
75
+ // Luhn) with the canonical masked sentinel.
76
+ if (typeof current === "string" && detectIdentityPII(current) !== null) {
77
+ if (current === MASKED_VALUE)
78
+ return current;
79
+ if (summary) {
80
+ summary.maskedValueCount += 1;
81
+ if (path)
82
+ summary.maskedFields.add(path);
83
+ }
84
+ return MASKED_VALUE;
85
+ }
86
+ return current;
87
+ };
88
+ return walk(value, 0, pathPrefix);
89
+ };
90
+ export const parseUrlForNetwork = (rawUrl) => {
91
+ const toQueryRecord = (url) => {
92
+ const query = {};
93
+ url.searchParams.forEach((value, key) => {
94
+ const current = query[key];
95
+ if (current === undefined) {
96
+ query[key] = value;
97
+ return;
98
+ }
99
+ if (Array.isArray(current)) {
100
+ query[key] = [...current, value];
101
+ return;
102
+ }
103
+ query[key] = [current, value];
104
+ });
105
+ return query;
106
+ };
107
+ try {
108
+ const url = new URL(rawUrl, globalThis.location?.origin);
109
+ const query = url.search ? `?${MASKED_VALUE}` : "";
110
+ return {
111
+ host: url.host,
112
+ pathTemplate: toPathTemplate(url.pathname),
113
+ path: url.pathname,
114
+ query: toQueryRecord(url),
115
+ urlMaskedQuery: `${url.origin}${url.pathname}${query}`,
116
+ };
117
+ }
118
+ catch {
119
+ return {
120
+ host: "",
121
+ pathTemplate: "/",
122
+ path: "/",
123
+ query: {},
124
+ urlMaskedQuery: MASKED_VALUE,
125
+ };
126
+ }
127
+ };
128
+ export const maybeParseJson = (value) => {
129
+ if (typeof value !== "string") {
130
+ return value;
131
+ }
132
+ const trimmed = value.trim();
133
+ if (!trimmed) {
134
+ return null;
135
+ }
136
+ const isJsonLike = (trimmed.startsWith("{") && trimmed.endsWith("}")) ||
137
+ (trimmed.startsWith("[") && trimmed.endsWith("]"));
138
+ if (!isJsonLike) {
139
+ return null;
140
+ }
141
+ try {
142
+ const parsed = JSON.parse(trimmed);
143
+ return parsed;
144
+ }
145
+ catch {
146
+ return null;
147
+ }
148
+ };
149
+ // ---------------------------------------------------------------------------
150
+ // Identity-value PII detection for browser payloads.
151
+ //
152
+ // The frontend SDK does not propagate identity via W3C Baggage (browser-side
153
+ // never opens outgoing HTTP to non-Cluebase services), so the leak vector here is
154
+ // the canonical span attribute `cluebase.actor.user_id` getting captured in raw
155
+ // observation storage / facts_json. Hashing PII-shaped identity values
156
+ // prevents that.
157
+ //
158
+ // この面は値の**中**を走査します(裏の配る部品は両端で留めます)。違いは本物で、
159
+ // 意図したものです——**問いが違うからです。** ここは「この文字列が個人情報を
160
+ // 含むか」、裏は「この値そのものが個人情報か」。正本
161
+ // (`identity-pii-policy.v1.json` の面 `text_contains_pii`)に 1 度だけ書かれ、
162
+ // 下の生成物から読みます。**面の名前は生成物の `PII_SURFACE` が名乗ります。**
163
+ // 判定の手順は**正本から出した部品が持ちます**。ここには手順が手で書かれており、
164
+ // 受付側にも同じ手順の 2 本目が手で書かれていました(AGENTS §0.2)。
165
+ export { detectIdentityPII };
166
+ /**
167
+ * Synchronous deterministic SHA-256 hash for identity values that look
168
+ * like PII. The identity sanitization path must remain synchronous
169
+ * because `identify(userId)` is sync-only by SDK contract.
170
+ *
171
+ * Output format: `cluebase_pii_hashed:<32-hex>`.
172
+ *
173
+ * The algorithm name, the digest length and the prefix are read from the
174
+ * generated policy, not written here, so the three SDKs produce the same
175
+ * token for the same input by construction rather than by assertion.
176
+ */
177
+ export const hashIdentityValue = (value) => `${PII_HASH_PREFIX}${bytesToHex(sha256(utf8ToBytes(value))).slice(0, PII_HASH_HEX_LENGTH)}`;
@@ -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,232 @@
1
+ import { fingerprintText } from "./text-fingerprint.js";
2
+ const MAX_FIELD_PATHS = 100;
3
+ const MAX_DEPTH = 8;
4
+ const isReadableObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
5
+ const valueType = (value) => {
6
+ if (value === null) {
7
+ return "null";
8
+ }
9
+ if (Array.isArray(value)) {
10
+ return "array";
11
+ }
12
+ if (typeof value === "object") {
13
+ return "object";
14
+ }
15
+ return typeof value;
16
+ };
17
+ const buildShape = (value, path, fieldPaths, depth) => {
18
+ if (depth > MAX_DEPTH) {
19
+ return { type: "max_depth" };
20
+ }
21
+ if (Array.isArray(value)) {
22
+ if (path && fieldPaths.length < MAX_FIELD_PATHS) {
23
+ fieldPaths.push(`${path}[]`);
24
+ }
25
+ return {
26
+ type: "array",
27
+ items: value.length > 0
28
+ ? buildShape(value[0], `${path}[]`, fieldPaths, depth + 1)
29
+ : "empty",
30
+ };
31
+ }
32
+ if (isReadableObject(value)) {
33
+ const fields = {};
34
+ for (const key of Object.keys(value).sort()) {
35
+ const childPath = path ? `${path}.${key}` : key;
36
+ if (fieldPaths.length < MAX_FIELD_PATHS) {
37
+ fieldPaths.push(childPath);
38
+ }
39
+ fields[key] = buildShape(value[key], childPath, fieldPaths, depth + 1);
40
+ }
41
+ return {
42
+ type: "object",
43
+ fields,
44
+ };
45
+ }
46
+ return { type: valueType(value) };
47
+ };
48
+ export const buildSchemaEvidence = (value) => {
49
+ if (value === null || value === undefined) {
50
+ return {
51
+ schemaHash: null,
52
+ fieldPaths: [],
53
+ shapeSize: 0,
54
+ };
55
+ }
56
+ const fieldPaths = [];
57
+ const shape = buildShape(value, "", fieldPaths, 0);
58
+ const uniqueFieldPaths = [...new Set(fieldPaths)].sort();
59
+ return {
60
+ schemaHash: fingerprintText(JSON.stringify(shape)),
61
+ fieldPaths: uniqueFieldPaths,
62
+ shapeSize: uniqueFieldPaths.length,
63
+ };
64
+ };
65
+ const sanitizeToken = (value) => {
66
+ if (typeof value !== "string") {
67
+ return null;
68
+ }
69
+ const trimmed = value.trim();
70
+ if (!trimmed || trimmed.length > 96) {
71
+ return null;
72
+ }
73
+ if (!/^[A-Za-z0-9_.:-]+$/.test(trimmed)) {
74
+ return null;
75
+ }
76
+ return trimmed.toLowerCase();
77
+ };
78
+ export const extractErrorCodeCandidate = (value) => {
79
+ if (Array.isArray(value)) {
80
+ for (const entry of value) {
81
+ const candidate = extractErrorCodeCandidate(entry);
82
+ if (candidate) {
83
+ return candidate;
84
+ }
85
+ }
86
+ return null;
87
+ }
88
+ if (!isReadableObject(value)) {
89
+ return sanitizeToken(value);
90
+ }
91
+ for (const key of ["code", "error_code", "type", "error"]) {
92
+ const candidate = sanitizeToken(value[key]);
93
+ if (candidate) {
94
+ return candidate;
95
+ }
96
+ }
97
+ for (const key of ["errors", "detail", "extensions"]) {
98
+ const candidate = extractErrorCodeCandidate(value[key]);
99
+ if (candidate) {
100
+ return candidate;
101
+ }
102
+ }
103
+ return null;
104
+ };
105
+ export const deriveOutcomeClass = (input) => {
106
+ const statusCode = input.statusCode ?? null;
107
+ if (statusCode !== null && statusCode > 0) {
108
+ if (statusCode === 401) {
109
+ return "auth_denied";
110
+ }
111
+ if (statusCode === 403) {
112
+ return "permission_denied";
113
+ }
114
+ if (statusCode === 409) {
115
+ return "conflict";
116
+ }
117
+ if (statusCode === 422) {
118
+ return "validation_error";
119
+ }
120
+ if (statusCode === 429) {
121
+ return "rate_limited";
122
+ }
123
+ if (statusCode >= 500) {
124
+ return "server_error";
125
+ }
126
+ if (statusCode >= 400) {
127
+ return "client_error";
128
+ }
129
+ return "success";
130
+ }
131
+ const failureType = input.failureType ?? null;
132
+ if (failureType) {
133
+ if (failureType === "AbortError" || failureType.includes("abort")) {
134
+ return "aborted";
135
+ }
136
+ if (failureType.includes("timeout")) {
137
+ return "timeout";
138
+ }
139
+ return "network_failed";
140
+ }
141
+ return "success";
142
+ };
143
+ const stripGraphqlNoise = (query) => {
144
+ return query
145
+ .replace(/#[^\n\r]*/g, "")
146
+ .replace(/"([^"\\]|\\.)*"/g, '""')
147
+ .replace(/'([^'\\]|\\.)*'/g, "''");
148
+ };
149
+ const readTopLevelGraphqlFields = (query) => {
150
+ const openIndex = query.indexOf("{");
151
+ if (openIndex < 0) {
152
+ return [];
153
+ }
154
+ const fields = [];
155
+ let depth = 0;
156
+ let parenDepth = 0;
157
+ let token = "";
158
+ for (let index = openIndex; index < query.length; index += 1) {
159
+ const char = query[index];
160
+ if (char === "(") {
161
+ if (depth === 1) {
162
+ if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(token) && token !== "on") {
163
+ fields.push(token);
164
+ }
165
+ token = "";
166
+ }
167
+ parenDepth += 1;
168
+ continue;
169
+ }
170
+ if (char === ")") {
171
+ parenDepth = Math.max(0, parenDepth - 1);
172
+ token = "";
173
+ continue;
174
+ }
175
+ if (char === "{") {
176
+ depth += 1;
177
+ token = "";
178
+ continue;
179
+ }
180
+ if (char === "}") {
181
+ if (depth === 1 && /^[A-Za-z_][A-Za-z0-9_]*$/.test(token)) {
182
+ fields.push(token);
183
+ }
184
+ depth -= 1;
185
+ token = "";
186
+ continue;
187
+ }
188
+ if (depth !== 1 || parenDepth > 0) {
189
+ continue;
190
+ }
191
+ if (/[A-Za-z0-9_]/.test(char)) {
192
+ token += char;
193
+ continue;
194
+ }
195
+ if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(token) && token !== "on") {
196
+ fields.push(token);
197
+ }
198
+ token = "";
199
+ }
200
+ return [...new Set(fields)].slice(0, 20).sort();
201
+ };
202
+ export const extractGraphqlEvidence = (body) => {
203
+ if (!isReadableObject(body)) {
204
+ return {
205
+ requestKind: null,
206
+ graphqlOperationName: null,
207
+ graphqlOperationType: null,
208
+ graphqlTopLevelFields: [],
209
+ };
210
+ }
211
+ const query = typeof body.query === "string" ? body.query : null;
212
+ if (!query) {
213
+ return {
214
+ requestKind: null,
215
+ graphqlOperationName: null,
216
+ graphqlOperationType: null,
217
+ graphqlTopLevelFields: [],
218
+ };
219
+ }
220
+ const normalizedQuery = stripGraphqlNoise(query);
221
+ const operationMatch = normalizedQuery.match(/\b(query|mutation|subscription)\s*([A-Za-z_][A-Za-z0-9_]*)?/);
222
+ const operationType = operationMatch?.[1] ?? "query";
223
+ const operationName = typeof body.operationName === "string" && body.operationName.trim()
224
+ ? body.operationName.trim()
225
+ : (operationMatch?.[2] ?? null);
226
+ return {
227
+ requestKind: "graphql",
228
+ graphqlOperationName: operationName,
229
+ graphqlOperationType: operationType,
230
+ graphqlTopLevelFields: readTopLevelGraphqlFields(normalizedQuery),
231
+ };
232
+ };
@@ -0,0 +1,99 @@
1
+ import type { CanonicalJsonValue, CluebasePrivacySummary, CluebaseSubjectProfile, SdkCollectionMode } from "../provider/types.js";
2
+ export declare const SDK_SOURCE_SIGNAL_EVENT_NAME = "sdk_source_signal_observed";
3
+ export declare const SDK_SOURCE_SIGNAL_EVENT_TYPE = "sdk_source_signal";
4
+ export type BrowserSdkSignalContext = {
5
+ producer_id: string | null;
6
+ anonymous_id: string | null;
7
+ user_id: string | null;
8
+ organization_id: string | null;
9
+ user_profile: CluebaseSubjectProfile | null;
10
+ session_id: string | null;
11
+ tab_id: string;
12
+ trace_id: string;
13
+ request_id: string;
14
+ request_span_id: string;
15
+ interaction_id: string;
16
+ correlation_id: string | null;
17
+ sdk_collection_mode: SdkCollectionMode;
18
+ frontend_release: string | null;
19
+ locale: string | null;
20
+ country: string | null;
21
+ device_type: string | null;
22
+ browser: string | null;
23
+ os: string | null;
24
+ referrer: string | null;
25
+ browser_family: string | null;
26
+ browser_major_version: string | null;
27
+ browser_engine: string | null;
28
+ os_family: string | null;
29
+ os_major_version: string | null;
30
+ timezone: string | null;
31
+ pointer_type_candidate: string | null;
32
+ touch_capable: boolean | null;
33
+ cookies_enabled: boolean | null;
34
+ local_storage_available: boolean | null;
35
+ session_storage_available: boolean | null;
36
+ service_worker_supported: boolean | null;
37
+ webgl_supported: boolean | null;
38
+ reduced_motion: string | null;
39
+ color_scheme: string | null;
40
+ subject_traits: Record<string, CanonicalJsonValue>;
41
+ organization_traits: Record<string, CanonicalJsonValue>;
42
+ context_traits: Record<string, CanonicalJsonValue>;
43
+ };
44
+ export type BrowserSdkSignal = {
45
+ surface: "frontend";
46
+ kind: "span";
47
+ name: string;
48
+ span_kind: string | number | null;
49
+ trace_id: string;
50
+ span_id: string | null;
51
+ parent_span_id: string | null;
52
+ start_time_unix_nano: string;
53
+ end_time_unix_nano: string;
54
+ status_code: number | null;
55
+ status_message: string | null;
56
+ instrumentation_scope_name: string | null;
57
+ attributes: Record<string, CanonicalJsonValue>;
58
+ events: Array<{
59
+ name: string;
60
+ attributes: Record<string, CanonicalJsonValue>;
61
+ }>;
62
+ resource: Record<string, CanonicalJsonValue>;
63
+ context: BrowserSdkSignalContext;
64
+ };
65
+ export type BrowserSourceSignalEvent = {
66
+ event_id: string;
67
+ source_event_id?: string;
68
+ source_schema_version?: string;
69
+ source_event_type: typeof SDK_SOURCE_SIGNAL_EVENT_TYPE;
70
+ source_event_kind: typeof SDK_SOURCE_SIGNAL_EVENT_TYPE;
71
+ producer_id: string | null;
72
+ service_name?: string | null;
73
+ event_name: typeof SDK_SOURCE_SIGNAL_EVENT_NAME;
74
+ event_category: "context";
75
+ event_source: "frontend_sdk";
76
+ event_version: number;
77
+ occurred_at: string;
78
+ ingested_at: string;
79
+ status: string;
80
+ sdk_collection_mode: string;
81
+ privacy_state?: "allow" | "masked" | "blocked";
82
+ privacy_level?: "default_masked" | "allowlisted";
83
+ masking_state?: "masked" | "partially_unmasked" | "unmasked";
84
+ allowed_value_keys?: string[];
85
+ privacy_summary?: CluebasePrivacySummary;
86
+ trace_id: string;
87
+ span_id?: string | null;
88
+ parent_span_id?: string | null;
89
+ request_id: string;
90
+ request_span_id: string;
91
+ interaction_id: string;
92
+ frontend_release: string | null;
93
+ backend_release: string | null;
94
+ properties: Record<string, CanonicalJsonValue>;
95
+ metrics: Record<string, number>;
96
+ sdk_signal: BrowserSdkSignal;
97
+ };
98
+ export type BrowserSourceEvent = BrowserSourceSignalEvent;
99
+ export declare function isBrowserSourceSignalEvent(event: unknown): event is BrowserSourceSignalEvent;
@@ -0,0 +1,13 @@
1
+ export const SDK_SOURCE_SIGNAL_EVENT_NAME = "sdk_source_signal_observed";
2
+ export const SDK_SOURCE_SIGNAL_EVENT_TYPE = "sdk_source_signal";
3
+ const isReadableObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
4
+ export function isBrowserSourceSignalEvent(event) {
5
+ if (!isReadableObject(event)) {
6
+ return false;
7
+ }
8
+ const sdkSignal = event.sdk_signal;
9
+ return (event.event_name === SDK_SOURCE_SIGNAL_EVENT_NAME &&
10
+ event.source_event_type === SDK_SOURCE_SIGNAL_EVENT_TYPE &&
11
+ event.source_event_kind === SDK_SOURCE_SIGNAL_EVENT_TYPE &&
12
+ isReadableObject(sdkSignal));
13
+ }
@@ -0,0 +1,8 @@
1
+ import type { ReadableSpan } from "@opentelemetry/sdk-trace-base";
2
+ import type { CluebaseContext } from "../provider/types.js";
3
+ import { type BrowserSourceSignalEvent } from "./source-event.js";
4
+ export interface SpanToSourceSignalOptions {
5
+ context: CluebaseContext;
6
+ deniedKeys?: readonly string[];
7
+ }
8
+ export declare function spanToSourceSignalEvent(span: ReadableSpan, options: SpanToSourceSignalOptions): BrowserSourceSignalEvent;