@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,13 @@
|
|
|
1
|
+
import type { CanonicalBaseEvent, ClueContext, FrontendEventCategory, FrontendEventName, FrontendEventStatus } from "../core/types";
|
|
2
|
+
export type CanonicalBaseInput = {
|
|
3
|
+
eventName: FrontendEventName;
|
|
4
|
+
eventCategory: FrontendEventCategory;
|
|
5
|
+
status: FrontendEventStatus;
|
|
6
|
+
context: ClueContext;
|
|
7
|
+
occurredAtMs?: number;
|
|
8
|
+
traceId?: string | null;
|
|
9
|
+
requestId?: string | null;
|
|
10
|
+
requestSpanId?: string | null;
|
|
11
|
+
interactionId?: string | null;
|
|
12
|
+
};
|
|
13
|
+
export declare const createCanonicalBaseEvent: (input: CanonicalBaseInput) => CanonicalBaseEvent;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { EVENT_SOURCE, EVENT_VERSION, SOURCE_SCHEMA_VERSION, SURFACE_TYPE, } from "../core/constants";
|
|
2
|
+
import { generateEventId } from "./event-id";
|
|
3
|
+
export const createCanonicalBaseEvent = (input) => {
|
|
4
|
+
const eventId = generateEventId();
|
|
5
|
+
const occurredAt = new Date(input.occurredAtMs ?? Date.now()).toISOString();
|
|
6
|
+
const ingestedAt = new Date().toISOString();
|
|
7
|
+
return {
|
|
8
|
+
event_id: eventId,
|
|
9
|
+
source_event_id: eventId,
|
|
10
|
+
source_schema_version: SOURCE_SCHEMA_VERSION,
|
|
11
|
+
source_event_type: input.eventName,
|
|
12
|
+
source_event_kind: input.eventCategory,
|
|
13
|
+
producer_id: input.context.producer_id,
|
|
14
|
+
event_name: input.eventName,
|
|
15
|
+
event_category: input.eventCategory,
|
|
16
|
+
event_source: EVENT_SOURCE,
|
|
17
|
+
event_version: EVENT_VERSION,
|
|
18
|
+
occurred_at: occurredAt,
|
|
19
|
+
ingested_at: ingestedAt,
|
|
20
|
+
status: input.status,
|
|
21
|
+
sdk_collection_mode: input.context.sdk_collection_mode,
|
|
22
|
+
// project / tenant / privacy authority is resolved server-side at ingest.
|
|
23
|
+
anonymous_id: input.context.anonymous_id,
|
|
24
|
+
user_id: input.context.user_id,
|
|
25
|
+
account_id: input.context.account_id,
|
|
26
|
+
organization_id: input.context.organization_id ?? null,
|
|
27
|
+
user_profile: input.context.user_profile ?? undefined,
|
|
28
|
+
account_profile: input.context.account_profile ?? undefined,
|
|
29
|
+
workspace_id: input.context.workspace_id,
|
|
30
|
+
session_id: input.context.session_id,
|
|
31
|
+
tab_id: input.context.tab_id,
|
|
32
|
+
trace_id: input.traceId ?? input.context.trace_id,
|
|
33
|
+
request_id: input.requestId ?? input.context.request_id,
|
|
34
|
+
request_span_id: input.requestSpanId ?? input.context.request_span_id,
|
|
35
|
+
interaction_id: input.interactionId ?? input.context.interaction_id,
|
|
36
|
+
correlation_id: input.context.correlation_id,
|
|
37
|
+
surface_type: SURFACE_TYPE,
|
|
38
|
+
environment: input.context.environment,
|
|
39
|
+
frontend_release: input.context.frontend_release,
|
|
40
|
+
backend_release: null,
|
|
41
|
+
feature_flags: input.context.feature_flags,
|
|
42
|
+
experiment_variant: input.context.experiment_variant,
|
|
43
|
+
device_type: input.context.device_type,
|
|
44
|
+
browser: input.context.browser,
|
|
45
|
+
os: input.context.os,
|
|
46
|
+
locale: input.context.locale,
|
|
47
|
+
country: input.context.country,
|
|
48
|
+
referrer: input.context.referrer,
|
|
49
|
+
browser_family: input.context.browser_family,
|
|
50
|
+
browser_major_version: input.context.browser_major_version,
|
|
51
|
+
browser_engine: input.context.browser_engine,
|
|
52
|
+
os_family: input.context.os_family,
|
|
53
|
+
os_major_version: input.context.os_major_version,
|
|
54
|
+
timezone: input.context.timezone,
|
|
55
|
+
pointer_type_candidate: input.context.pointer_type_candidate,
|
|
56
|
+
touch_capable: input.context.touch_capable,
|
|
57
|
+
cookies_enabled: input.context.cookies_enabled,
|
|
58
|
+
local_storage_available: input.context.local_storage_available,
|
|
59
|
+
session_storage_available: input.context.session_storage_available,
|
|
60
|
+
service_worker_supported: input.context.service_worker_supported,
|
|
61
|
+
webgl_supported: input.context.webgl_supported,
|
|
62
|
+
reduced_motion: input.context.reduced_motion,
|
|
63
|
+
color_scheme: input.context.color_scheme,
|
|
64
|
+
account_status: input.context.account_status,
|
|
65
|
+
plan_tier: input.context.plan_tier,
|
|
66
|
+
billing_status: input.context.billing_status,
|
|
67
|
+
customer_lifecycle_stage: input.context.customer_lifecycle_stage,
|
|
68
|
+
organization_size_bucket: input.context.organization_size_bucket,
|
|
69
|
+
user_role: input.context.user_role,
|
|
70
|
+
permission_group: input.context.permission_group,
|
|
71
|
+
onboarding_state: input.context.onboarding_state,
|
|
72
|
+
trial_days_remaining_bucket: input.context.trial_days_remaining_bucket,
|
|
73
|
+
customer_health_bucket: input.context.customer_health_bucket,
|
|
74
|
+
properties: {},
|
|
75
|
+
metrics: {},
|
|
76
|
+
};
|
|
77
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const generateEventId: () => string;
|
|
2
|
+
export declare const generateAnonymousId: () => string;
|
|
3
|
+
export declare const generateSessionId: () => string;
|
|
4
|
+
export declare const generateTabId: () => string;
|
|
5
|
+
export declare const generateRequestId: () => string;
|
|
6
|
+
export declare const generateRequestSpanId: () => string;
|
|
7
|
+
export declare const generateInteractionId: () => string;
|
|
8
|
+
export declare const generateViewId: () => string;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const fallbackRandom = () => {
|
|
2
|
+
return `${Math.random().toString(16).slice(2)}${Date.now().toString(16)}`;
|
|
3
|
+
};
|
|
4
|
+
const randomUuidLike = () => {
|
|
5
|
+
try {
|
|
6
|
+
if (typeof globalThis.crypto?.randomUUID === "function") {
|
|
7
|
+
return globalThis.crypto.randomUUID();
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
// Ignore and fallback.
|
|
12
|
+
}
|
|
13
|
+
const seed = fallbackRandom().padEnd(32, "0").slice(0, 32);
|
|
14
|
+
return `${seed.slice(0, 8)}-${seed.slice(8, 12)}-${seed.slice(12, 16)}-${seed.slice(16, 20)}-${seed.slice(20, 32)}`;
|
|
15
|
+
};
|
|
16
|
+
export const generateEventId = () => {
|
|
17
|
+
return randomUuidLike();
|
|
18
|
+
};
|
|
19
|
+
export const generateAnonymousId = () => {
|
|
20
|
+
return `anon_${randomUuidLike()}`;
|
|
21
|
+
};
|
|
22
|
+
export const generateSessionId = () => {
|
|
23
|
+
return `sess_${randomUuidLike()}`;
|
|
24
|
+
};
|
|
25
|
+
export const generateTabId = () => {
|
|
26
|
+
return `tab_${randomUuidLike()}`;
|
|
27
|
+
};
|
|
28
|
+
export const generateRequestId = () => {
|
|
29
|
+
return `req_${randomUuidLike()}`;
|
|
30
|
+
};
|
|
31
|
+
export const generateRequestSpanId = () => {
|
|
32
|
+
return `rsp_${randomUuidLike()}`;
|
|
33
|
+
};
|
|
34
|
+
export const generateInteractionId = () => {
|
|
35
|
+
return `int_${randomUuidLike()}`;
|
|
36
|
+
};
|
|
37
|
+
export const generateViewId = () => {
|
|
38
|
+
return `view_${randomUuidLike()}`;
|
|
39
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const toPathTemplate: (pathname: string) => string;
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
export const toPathTemplate = (pathname) => {
|
|
6
|
+
if (!pathname || pathname === "/") {
|
|
7
|
+
return "/";
|
|
8
|
+
}
|
|
9
|
+
const normalizedPath = pathname.startsWith("/") ? pathname : `/${pathname}`;
|
|
10
|
+
const [pathOnly] = normalizedPath.split("?");
|
|
11
|
+
const segments = pathOnly
|
|
12
|
+
.split("/")
|
|
13
|
+
.filter((segment) => segment.length > 0)
|
|
14
|
+
.map((segment) => {
|
|
15
|
+
if (NUMBER_SEGMENT.test(segment)) {
|
|
16
|
+
return ":id";
|
|
17
|
+
}
|
|
18
|
+
if (UUID_SEGMENT.test(segment)) {
|
|
19
|
+
return ":uuid";
|
|
20
|
+
}
|
|
21
|
+
if (HEX_SEGMENT.test(segment)) {
|
|
22
|
+
return ":hex";
|
|
23
|
+
}
|
|
24
|
+
if (TOKEN_SEGMENT.test(segment)) {
|
|
25
|
+
return ":token";
|
|
26
|
+
}
|
|
27
|
+
return segment;
|
|
28
|
+
});
|
|
29
|
+
if (segments.length === 0) {
|
|
30
|
+
return "/";
|
|
31
|
+
}
|
|
32
|
+
return `/${segments.join("/")}`;
|
|
33
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
type LocalValueType = "string" | "number" | "boolean" | "null" | "object" | "array";
|
|
2
|
+
export type LocalTextDescriptor = {
|
|
3
|
+
present: true;
|
|
4
|
+
length: number;
|
|
5
|
+
};
|
|
6
|
+
export type LocalValueDescriptor = {
|
|
7
|
+
present: true;
|
|
8
|
+
valueType: LocalValueType;
|
|
9
|
+
length?: number;
|
|
10
|
+
itemCount?: number;
|
|
11
|
+
keyCount?: number;
|
|
12
|
+
fileCount?: number;
|
|
13
|
+
};
|
|
14
|
+
export type LocalStructuredSummary = LocalValueDescriptor | {
|
|
15
|
+
present: true;
|
|
16
|
+
valueType: "object";
|
|
17
|
+
keyCount: number;
|
|
18
|
+
fields: Record<string, LocalStructuredSummary | null>;
|
|
19
|
+
} | {
|
|
20
|
+
present: true;
|
|
21
|
+
valueType: "array";
|
|
22
|
+
itemCount: number;
|
|
23
|
+
items: Array<LocalStructuredSummary | null>;
|
|
24
|
+
};
|
|
25
|
+
export declare const buildLocalTextDescriptor: (value: string | null | undefined) => LocalTextDescriptor | null;
|
|
26
|
+
export declare const buildLocalValueDescriptor: (value: unknown) => LocalValueDescriptor | null;
|
|
27
|
+
export declare const buildLocalStructuredSummary: (value: unknown, depth?: number) => LocalStructuredSummary | null;
|
|
28
|
+
export declare const buildLocalQuerySummary: (query: Record<string, string | string[]>) => Record<string, LocalStructuredSummary | null>;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const MAX_SUMMARY_DEPTH = 4;
|
|
2
|
+
const MAX_SUMMARY_KEYS = 32;
|
|
3
|
+
const MAX_SUMMARY_ITEMS = 12;
|
|
4
|
+
export const buildLocalTextDescriptor = (value) => {
|
|
5
|
+
if (value == null) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
const trimmed = value.trim();
|
|
9
|
+
if (!trimmed) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
present: true,
|
|
14
|
+
length: trimmed.length,
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
export const buildLocalValueDescriptor = (value) => {
|
|
18
|
+
if (value == null) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
if (typeof value === "string") {
|
|
22
|
+
const trimmed = value.trim();
|
|
23
|
+
return trimmed
|
|
24
|
+
? { present: true, valueType: "string", length: trimmed.length }
|
|
25
|
+
: null;
|
|
26
|
+
}
|
|
27
|
+
if (typeof value === "number") {
|
|
28
|
+
return { present: true, valueType: "number" };
|
|
29
|
+
}
|
|
30
|
+
if (typeof value === "boolean") {
|
|
31
|
+
return { present: true, valueType: "boolean" };
|
|
32
|
+
}
|
|
33
|
+
if (Array.isArray(value)) {
|
|
34
|
+
return {
|
|
35
|
+
present: true,
|
|
36
|
+
valueType: "array",
|
|
37
|
+
itemCount: value.length,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (typeof value === "object") {
|
|
41
|
+
const record = value;
|
|
42
|
+
const fileCount = typeof record.fileCount === "number" && Number.isFinite(record.fileCount)
|
|
43
|
+
? Math.max(0, Math.floor(record.fileCount))
|
|
44
|
+
: undefined;
|
|
45
|
+
return {
|
|
46
|
+
present: true,
|
|
47
|
+
valueType: "object",
|
|
48
|
+
keyCount: Object.keys(record).length,
|
|
49
|
+
...(typeof fileCount === "number" ? { fileCount } : {}),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
};
|
|
54
|
+
export const buildLocalStructuredSummary = (value, depth = 0) => {
|
|
55
|
+
if (depth >= MAX_SUMMARY_DEPTH) {
|
|
56
|
+
return buildLocalValueDescriptor(value);
|
|
57
|
+
}
|
|
58
|
+
if (Array.isArray(value)) {
|
|
59
|
+
return {
|
|
60
|
+
present: true,
|
|
61
|
+
valueType: "array",
|
|
62
|
+
itemCount: value.length,
|
|
63
|
+
items: value
|
|
64
|
+
.slice(0, MAX_SUMMARY_ITEMS)
|
|
65
|
+
.map((entry) => buildLocalStructuredSummary(entry, depth + 1)),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (value && typeof value === "object") {
|
|
69
|
+
const record = value;
|
|
70
|
+
const limitedEntries = Object.entries(record).slice(0, MAX_SUMMARY_KEYS);
|
|
71
|
+
return {
|
|
72
|
+
present: true,
|
|
73
|
+
valueType: "object",
|
|
74
|
+
keyCount: Object.keys(record).length,
|
|
75
|
+
fields: Object.fromEntries(limitedEntries.map(([key, child]) => [
|
|
76
|
+
key,
|
|
77
|
+
buildLocalStructuredSummary(child, depth + 1),
|
|
78
|
+
])),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return buildLocalValueDescriptor(value);
|
|
82
|
+
};
|
|
83
|
+
export const buildLocalQuerySummary = (query) => {
|
|
84
|
+
return Object.fromEntries(Object.entries(query).map(([key, value]) => [
|
|
85
|
+
key,
|
|
86
|
+
buildLocalStructuredSummary(value),
|
|
87
|
+
]));
|
|
88
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const maskString: (value: string | null | undefined) => string | null;
|
|
2
|
+
export declare const maskStringArray: (value: string[] | null | undefined) => string[] | null;
|
|
3
|
+
export declare const maskUnknown: (value: unknown) => unknown;
|
|
4
|
+
export declare const maskDomPath: (domPath: string | null | undefined) => string | null;
|
|
5
|
+
export declare const maskMessage: (message: string | null | undefined) => string | null;
|
|
6
|
+
export declare const maskUrl: (urlValue: string | null | undefined) => string | null;
|
|
7
|
+
export declare const maskInputValue: () => string;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { MASKED_VALUE } from "../core/constants";
|
|
2
|
+
export const maskString = (value) => {
|
|
3
|
+
if (value == null) {
|
|
4
|
+
return null;
|
|
5
|
+
}
|
|
6
|
+
if (!value.trim()) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
return MASKED_VALUE;
|
|
10
|
+
};
|
|
11
|
+
export const maskStringArray = (value) => {
|
|
12
|
+
if (!value || value.length === 0) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
return value.map(() => MASKED_VALUE);
|
|
16
|
+
};
|
|
17
|
+
export const maskUnknown = (value) => {
|
|
18
|
+
if (value == null) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
if (typeof value === "string" ||
|
|
22
|
+
typeof value === "number" ||
|
|
23
|
+
typeof value === "boolean" ||
|
|
24
|
+
typeof value === "bigint") {
|
|
25
|
+
return MASKED_VALUE;
|
|
26
|
+
}
|
|
27
|
+
if (Array.isArray(value)) {
|
|
28
|
+
return value.map((entry) => maskUnknown(entry));
|
|
29
|
+
}
|
|
30
|
+
if (typeof value === "object") {
|
|
31
|
+
const out = {};
|
|
32
|
+
for (const [key, child] of Object.entries(value)) {
|
|
33
|
+
out[key] = maskUnknown(child);
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
return MASKED_VALUE;
|
|
38
|
+
};
|
|
39
|
+
export const maskDomPath = (domPath) => {
|
|
40
|
+
return maskString(domPath);
|
|
41
|
+
};
|
|
42
|
+
export const maskMessage = (message) => {
|
|
43
|
+
return maskString(message);
|
|
44
|
+
};
|
|
45
|
+
export const maskUrl = (urlValue) => {
|
|
46
|
+
if (!urlValue) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const url = new URL(urlValue, globalThis.location?.origin);
|
|
51
|
+
const query = url.search ? `?${MASKED_VALUE}` : "";
|
|
52
|
+
return `${url.origin}${url.pathname}${query}`;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return MASKED_VALUE;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
export const maskInputValue = () => {
|
|
59
|
+
return MASKED_VALUE;
|
|
60
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ParameterSnapshotValue } from "../core/types";
|
|
2
|
+
type ParameterSnapshotBundle = {
|
|
3
|
+
snapshot: ParameterSnapshotValue;
|
|
4
|
+
allowedValueKeys: string[];
|
|
5
|
+
};
|
|
6
|
+
type ParameterSnapshotOptions = {
|
|
7
|
+
allowedPaths: string[];
|
|
8
|
+
deniedKeys: string[];
|
|
9
|
+
pathPrefix?: string;
|
|
10
|
+
};
|
|
11
|
+
export declare const buildUnavailableParameterSnapshot: () => ParameterSnapshotValue;
|
|
12
|
+
export declare const truncateParameterSnapshotValue: (value: ParameterSnapshotValue) => ParameterSnapshotValue;
|
|
13
|
+
export declare const buildParameterSnapshotBundle: (value: unknown, options: ParameterSnapshotOptions) => ParameterSnapshotBundle;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { FORBIDDEN_PARAMETER_VALUE, MASKED_VALUE, MAX_SANITIZE_DEPTH, TRUNCATED_PARAMETER_VALUE, } from "../core/constants";
|
|
2
|
+
import { PARAMETER_CAPTURE_MODE } from "../core/contracts";
|
|
3
|
+
import { isDeniedKey } from "./sanitize";
|
|
4
|
+
const SNAPSHOT_UNAVAILABLE_KEY = "_clue_snapshot_unavailable";
|
|
5
|
+
const normalizeParameterLookupPath = (path) => {
|
|
6
|
+
return path.replace(/\[\d+\]/g, "[]");
|
|
7
|
+
};
|
|
8
|
+
const inferLeafType = (value) => {
|
|
9
|
+
if (value === null) {
|
|
10
|
+
return "null";
|
|
11
|
+
}
|
|
12
|
+
if (Array.isArray(value)) {
|
|
13
|
+
return "array";
|
|
14
|
+
}
|
|
15
|
+
if (typeof value === "object") {
|
|
16
|
+
return "object";
|
|
17
|
+
}
|
|
18
|
+
switch (typeof value) {
|
|
19
|
+
case "string":
|
|
20
|
+
return "string";
|
|
21
|
+
case "number":
|
|
22
|
+
return "number";
|
|
23
|
+
case "boolean":
|
|
24
|
+
return "boolean";
|
|
25
|
+
default:
|
|
26
|
+
return "string";
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const toPlaintextLeafValue = (value) => {
|
|
30
|
+
if (value === null) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
if (typeof value === "string" ||
|
|
34
|
+
typeof value === "number" ||
|
|
35
|
+
typeof value === "boolean") {
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
return String(value);
|
|
39
|
+
};
|
|
40
|
+
const measureSerializedBytes = (value) => {
|
|
41
|
+
try {
|
|
42
|
+
return new TextEncoder().encode(JSON.stringify(value)).length;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const buildLeaf = (value, captureMode) => {
|
|
49
|
+
const leafType = inferLeafType(value);
|
|
50
|
+
const originalSizeBytes = captureMode === PARAMETER_CAPTURE_MODE.forbidden
|
|
51
|
+
? undefined
|
|
52
|
+
: measureSerializedBytes(value);
|
|
53
|
+
return {
|
|
54
|
+
type: leafType,
|
|
55
|
+
value: captureMode === PARAMETER_CAPTURE_MODE.allowedPlaintext
|
|
56
|
+
? toPlaintextLeafValue(value)
|
|
57
|
+
: captureMode === PARAMETER_CAPTURE_MODE.forbidden
|
|
58
|
+
? FORBIDDEN_PARAMETER_VALUE
|
|
59
|
+
: MASKED_VALUE,
|
|
60
|
+
capture_mode: captureMode,
|
|
61
|
+
...(typeof originalSizeBytes === "number"
|
|
62
|
+
? { original_size_bytes: originalSizeBytes }
|
|
63
|
+
: {}),
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
export const buildUnavailableParameterSnapshot = () => ({
|
|
67
|
+
[SNAPSHOT_UNAVAILABLE_KEY]: buildLeaf("snapshot_unavailable", PARAMETER_CAPTURE_MODE.maskedFingerprint),
|
|
68
|
+
});
|
|
69
|
+
const isParameterSnapshotLeaf = (value) => {
|
|
70
|
+
return (!!value &&
|
|
71
|
+
typeof value === "object" &&
|
|
72
|
+
!Array.isArray(value) &&
|
|
73
|
+
"type" in value &&
|
|
74
|
+
"value" in value &&
|
|
75
|
+
"capture_mode" in value);
|
|
76
|
+
};
|
|
77
|
+
export const truncateParameterSnapshotValue = (value) => {
|
|
78
|
+
if (Array.isArray(value)) {
|
|
79
|
+
return value.map((entry) => truncateParameterSnapshotValue(entry));
|
|
80
|
+
}
|
|
81
|
+
if (isParameterSnapshotLeaf(value)) {
|
|
82
|
+
const originalSizeBytes = typeof value.original_size_bytes === "number"
|
|
83
|
+
? value.original_size_bytes
|
|
84
|
+
: value.capture_mode === PARAMETER_CAPTURE_MODE.allowedPlaintext
|
|
85
|
+
? measureSerializedBytes(value.value)
|
|
86
|
+
: undefined;
|
|
87
|
+
return {
|
|
88
|
+
type: value.type,
|
|
89
|
+
value: TRUNCATED_PARAMETER_VALUE,
|
|
90
|
+
capture_mode: PARAMETER_CAPTURE_MODE.truncated,
|
|
91
|
+
truncated_reason: "max_event_bytes",
|
|
92
|
+
...(typeof originalSizeBytes === "number"
|
|
93
|
+
? { original_size_bytes: originalSizeBytes }
|
|
94
|
+
: {}),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const truncated = {};
|
|
98
|
+
for (const [key, child] of Object.entries(value)) {
|
|
99
|
+
truncated[key] = truncateParameterSnapshotValue(child);
|
|
100
|
+
}
|
|
101
|
+
return truncated;
|
|
102
|
+
};
|
|
103
|
+
const buildAllowedPathLookup = (allowedPaths) => {
|
|
104
|
+
const lookup = new Map();
|
|
105
|
+
for (const entry of allowedPaths) {
|
|
106
|
+
const trimmed = entry.trim();
|
|
107
|
+
if (!trimmed) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const normalized = normalizeParameterLookupPath(trimmed);
|
|
111
|
+
if (!lookup.has(normalized)) {
|
|
112
|
+
lookup.set(normalized, trimmed);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return lookup;
|
|
116
|
+
};
|
|
117
|
+
export const buildParameterSnapshotBundle = (value, options) => {
|
|
118
|
+
const allowedPathLookup = buildAllowedPathLookup(options.allowedPaths);
|
|
119
|
+
const seen = new WeakSet();
|
|
120
|
+
const rootPathPrefix = options.pathPrefix?.trim() ?? "";
|
|
121
|
+
const walk = (current, path, depth) => {
|
|
122
|
+
if (depth > MAX_SANITIZE_DEPTH) {
|
|
123
|
+
return {
|
|
124
|
+
snapshot: buildLeaf(null, PARAMETER_CAPTURE_MODE.maskedFingerprint),
|
|
125
|
+
allowedValueKeys: [],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (current === undefined ||
|
|
129
|
+
typeof current === "function" ||
|
|
130
|
+
typeof current === "symbol") {
|
|
131
|
+
return {
|
|
132
|
+
snapshot: undefined,
|
|
133
|
+
allowedValueKeys: [],
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (Array.isArray(current)) {
|
|
137
|
+
if (current.length === 0) {
|
|
138
|
+
return {
|
|
139
|
+
snapshot: [],
|
|
140
|
+
allowedValueKeys: [],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const representative = walk(current[0], `${path}[]`, depth + 1);
|
|
144
|
+
if (representative.snapshot === undefined) {
|
|
145
|
+
return {
|
|
146
|
+
snapshot: [],
|
|
147
|
+
allowedValueKeys: representative.allowedValueKeys,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
snapshot: [representative.snapshot],
|
|
152
|
+
allowedValueKeys: representative.allowedValueKeys,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
if (current && typeof current === "object") {
|
|
156
|
+
const objectValue = current;
|
|
157
|
+
if (seen.has(objectValue)) {
|
|
158
|
+
return {
|
|
159
|
+
snapshot: buildLeaf(null, PARAMETER_CAPTURE_MODE.maskedFingerprint),
|
|
160
|
+
allowedValueKeys: [],
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
seen.add(objectValue);
|
|
164
|
+
const snapshot = {};
|
|
165
|
+
const allowedValueKeys = [];
|
|
166
|
+
for (const [key, child] of Object.entries(objectValue)) {
|
|
167
|
+
const childPath = path ? `${path}.${key}` : key;
|
|
168
|
+
if (isDeniedKey(key, options.deniedKeys)) {
|
|
169
|
+
snapshot[key] = buildLeaf(child, PARAMETER_CAPTURE_MODE.forbidden);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const childBundle = walk(child, childPath, depth + 1);
|
|
173
|
+
if (childBundle.snapshot === undefined) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
snapshot[key] = childBundle.snapshot;
|
|
177
|
+
allowedValueKeys.push(...childBundle.allowedValueKeys);
|
|
178
|
+
}
|
|
179
|
+
seen.delete(objectValue);
|
|
180
|
+
return {
|
|
181
|
+
snapshot,
|
|
182
|
+
allowedValueKeys,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
const matchedAllowedPath = path
|
|
186
|
+
? allowedPathLookup.get(normalizeParameterLookupPath(path)) ?? null
|
|
187
|
+
: null;
|
|
188
|
+
const captureMode = matchedAllowedPath
|
|
189
|
+
? PARAMETER_CAPTURE_MODE.allowedPlaintext
|
|
190
|
+
: PARAMETER_CAPTURE_MODE.maskedFingerprint;
|
|
191
|
+
return {
|
|
192
|
+
snapshot: buildLeaf(current, captureMode),
|
|
193
|
+
allowedValueKeys: matchedAllowedPath ? [matchedAllowedPath] : [],
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
const rootBundle = walk(value, rootPathPrefix, 0);
|
|
197
|
+
const snapshot = rootBundle.snapshot &&
|
|
198
|
+
typeof rootBundle.snapshot === "object" &&
|
|
199
|
+
!Array.isArray(rootBundle.snapshot)
|
|
200
|
+
? rootBundle.snapshot
|
|
201
|
+
: {};
|
|
202
|
+
return {
|
|
203
|
+
snapshot,
|
|
204
|
+
allowedValueKeys: [...new Set(rootBundle.allowedValueKeys)].sort(),
|
|
205
|
+
};
|
|
206
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const isDeniedKey: (key: string, deniedKeys?: string[]) => boolean;
|
|
2
|
+
export declare const sanitizeHeaders: (headers: Record<string, string>, deniedKeys?: string[]) => Record<string, string>;
|
|
3
|
+
export declare const sanitizeObject: (value: unknown, deniedKeys?: string[]) => unknown;
|
|
4
|
+
export declare const parseUrlForNetwork: (rawUrl: string) => {
|
|
5
|
+
host: string;
|
|
6
|
+
pathTemplate: string;
|
|
7
|
+
path: string;
|
|
8
|
+
query: Record<string, string | string[]>;
|
|
9
|
+
urlMaskedQuery: string;
|
|
10
|
+
};
|
|
11
|
+
export declare const maybeParseJson: (value: unknown) => unknown;
|