@squasher-ai/browser 0.1.0 → 0.2.0
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/LICENSE +21 -21
- package/dist/__tests__/client/harness.d.ts +18 -0
- package/dist/__tests__/client/harness.d.ts.map +1 -0
- package/dist/__tests__/client/harness.js +106 -0
- package/dist/__tests__/replay/coverage-harness.d.ts +29 -0
- package/dist/__tests__/replay/coverage-harness.d.ts.map +1 -0
- package/dist/__tests__/replay/coverage-harness.js +123 -0
- package/dist/__tests__/replay/test-helpers.d.ts +13 -0
- package/dist/__tests__/replay/test-helpers.d.ts.map +1 -0
- package/dist/__tests__/replay/test-helpers.js +9 -0
- package/dist/autocapture.d.ts +14 -18
- package/dist/autocapture.d.ts.map +1 -1
- package/dist/autocapture.js +77 -95
- package/dist/client.d.ts +12 -19
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +56 -72
- package/dist/config.d.ts +25 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +26 -0
- package/dist/console-capture.d.ts +8 -0
- package/dist/console-capture.d.ts.map +1 -0
- package/dist/console-capture.js +79 -0
- package/dist/errors.d.ts +2 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +8 -5
- package/dist/index.d.ts +5 -38
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -82
- package/dist/next.d.ts +40 -0
- package/dist/next.d.ts.map +1 -0
- package/dist/next.js +67 -0
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +12 -10
- package/dist/replay-network.d.ts +21 -0
- package/dist/replay-network.d.ts.map +1 -0
- package/dist/replay-network.js +193 -0
- package/dist/replay-privacy.d.ts +2 -2
- package/dist/replay-privacy.d.ts.map +1 -1
- package/dist/replay-privacy.js +47 -17
- package/dist/replay-retry.d.ts +41 -0
- package/dist/replay-retry.d.ts.map +1 -0
- package/dist/replay-retry.js +193 -0
- package/dist/replay.d.ts +18 -5
- package/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +131 -71
- package/dist/runtime/client-singleton.d.ts +25 -0
- package/dist/runtime/client-singleton.d.ts.map +1 -0
- package/dist/runtime/client-singleton.js +66 -0
- package/dist/runtime/client.d.ts +22 -0
- package/dist/runtime/client.d.ts.map +1 -0
- package/dist/runtime/client.js +12 -0
- package/dist/runtime/replay.d.ts +13 -0
- package/dist/runtime/replay.d.ts.map +1 -0
- package/dist/runtime/replay.js +15 -0
- package/dist/session.d.ts +0 -10
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +20 -21
- package/dist/telemetry.d.ts +1 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +13 -10
- package/dist/trace-context.d.ts +9 -0
- package/dist/trace-context.d.ts.map +1 -0
- package/dist/trace-context.js +13 -0
- package/dist/transport.d.ts +11 -19
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +86 -66
- package/dist/types.d.ts +37 -135
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -7
- package/dist/vitals.d.ts +55 -2
- package/dist/vitals.d.ts.map +1 -1
- package/dist/vitals.js +115 -16
- package/package.json +17 -3
- package/dist/__tests__/client.test.d.ts +0 -2
- package/dist/__tests__/client.test.d.ts.map +0 -1
- package/dist/__tests__/client.test.js +0 -103
- package/dist/__tests__/errors.test.d.ts +0 -2
- package/dist/__tests__/errors.test.d.ts.map +0 -1
- package/dist/__tests__/errors.test.js +0 -80
- package/dist/__tests__/replay-privacy.test.d.ts +0 -2
- package/dist/__tests__/replay-privacy.test.d.ts.map +0 -1
- package/dist/__tests__/replay-privacy.test.js +0 -124
- package/dist/__tests__/replay-startup.test.d.ts +0 -2
- package/dist/__tests__/replay-startup.test.d.ts.map +0 -1
- package/dist/__tests__/replay-startup.test.js +0 -99
- package/dist/__tests__/replay.test.d.ts +0 -2
- package/dist/__tests__/replay.test.d.ts.map +0 -1
- package/dist/__tests__/replay.test.js +0 -217
- package/dist/__tests__/session.test.d.ts +0 -2
- package/dist/__tests__/session.test.d.ts.map +0 -1
- package/dist/__tests__/session.test.js +0 -46
- package/dist/__tests__/transport.test.d.ts +0 -2
- package/dist/__tests__/transport.test.d.ts.map +0 -1
- package/dist/__tests__/transport.test.js +0 -101
- package/dist/__tests__/types.test.d.ts +0 -2
- package/dist/__tests__/types.test.d.ts.map +0 -1
- package/dist/__tests__/types.test.js +0 -139
package/dist/client.d.ts
CHANGED
|
@@ -1,15 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* Manages:
|
|
5
|
-
* - Configuration with sensible defaults
|
|
6
|
-
* - User context, tags, and breadcrumbs
|
|
7
|
-
* - Web Vitals collection (via vitals.ts)
|
|
8
|
-
* - Global error capture (via errors.ts)
|
|
9
|
-
* - Auto-capture breadcrumbs (via autocapture.ts)
|
|
10
|
-
* - Transport (via transport.ts)
|
|
11
|
-
*/
|
|
12
|
-
import type { Breadcrumb, BrowserConfig, BrowserErrorEvent, JsonObject, Level, UserContext } from "./types";
|
|
1
|
+
import { type BrowserClientRuntime } from "./runtime/client.js";
|
|
2
|
+
import type { Breadcrumb, BrowserConfig, BrowserErrorEvent, JsonObject, Level, UserContext } from "./types.js";
|
|
3
|
+
import { type ExternalVitalInput } from "./vitals.js";
|
|
13
4
|
export declare class BrowserClient {
|
|
14
5
|
private config;
|
|
15
6
|
private transport;
|
|
@@ -19,12 +10,13 @@ export declare class BrowserClient {
|
|
|
19
10
|
private disabled;
|
|
20
11
|
private cleanups;
|
|
21
12
|
private replayRecorder;
|
|
22
|
-
|
|
13
|
+
private vitalsHandle;
|
|
14
|
+
constructor(config: BrowserConfig, runtime?: BrowserClientRuntime);
|
|
23
15
|
setUser(user: UserContext | undefined): void;
|
|
24
16
|
setTag(key: string, value: string): void;
|
|
25
17
|
setTags(tags: Record<string, string>): void;
|
|
26
18
|
addBreadcrumb(crumb: Omit<Breadcrumb, "timestamp">): void;
|
|
27
|
-
captureError(error: Error, extra?:
|
|
19
|
+
captureError(error: Error, extra?: JsonObject): void;
|
|
28
20
|
captureMessage(message: string, level?: Level): void;
|
|
29
21
|
captureTelemetry(event: BrowserErrorEvent): void;
|
|
30
22
|
track(eventName: string, properties?: JsonObject, context?: Partial<BrowserErrorEvent>): void;
|
|
@@ -34,12 +26,13 @@ export declare class BrowserClient {
|
|
|
34
26
|
captureSpan(name: string, context?: Partial<BrowserErrorEvent>): void;
|
|
35
27
|
captureToolCall(name: string, context?: Partial<BrowserErrorEvent>): void;
|
|
36
28
|
captureGeneration(message: string, context?: Partial<BrowserErrorEvent>): void;
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
29
|
+
/** Record a web vital from an external collector (e.g. Next.js `useReportWebVitals`). */
|
|
30
|
+
recordWebVital(metric: ExternalVitalInput, route?: string): void;
|
|
31
|
+
/** Stop the SDK's native `web-vitals` collector to avoid double-counting. */
|
|
32
|
+
suppressNativeVitalsCollection(): void;
|
|
33
|
+
private vitalsConfig;
|
|
34
|
+
/** Flush vitals and stop timers. Call before unmounting a SPA. */
|
|
41
35
|
close(): void;
|
|
42
|
-
private shouldSample;
|
|
43
36
|
/** Enrich an error event with current context and send it. */
|
|
44
37
|
private sendErrorEvent;
|
|
45
38
|
}
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,oBAAoB,EAG1B,MAAM,qBAAqB,CAAC;AAa7B,OAAO,KAAK,EACV,UAAU,EACV,aAAa,EACb,iBAAiB,EACjB,UAAU,EACV,KAAK,EACL,WAAW,EACZ,MAAM,YAAY,CAAC;AACpB,OAAO,EAAoB,KAAK,kBAAkB,EAA8B,MAAM,aAAa,CAAC;AAEpG,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,SAAS,CAAmB;IACpC,OAAO,CAAC,WAAW,CAAoB;IACvC,OAAO,CAAC,IAAI,CAA0B;IACtC,OAAO,CAAC,IAAI,CAA8B;IAC1C,OAAO,CAAC,QAAQ,CAAU;IAC1B,OAAO,CAAC,QAAQ,CAAyB;IACzC,OAAO,CAAC,cAAc,CAAqC;IAC3D,OAAO,CAAC,YAAY,CAAsC;gBAE9C,MAAM,EAAE,aAAa,EAAE,OAAO,GAAE,oBAAkD;IA4F9F,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,SAAS,GAAG,IAAI;IAI5C,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAIxC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI;IAI3C,aAAa,CAAC,KAAK,EAAE,IAAI,CAAC,UAAU,EAAE,WAAW,CAAC,GAAG,IAAI;IAUzD,YAAY,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,UAAU,GAAG,IAAI;IAIpD,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,GAAE,KAAc,GAAG,IAAI;IAI5D,gBAAgB,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI;IAKhD,KAAK,CACH,SAAS,EAAE,MAAM,EACjB,UAAU,CAAC,EAAE,UAAU,EACvB,OAAO,GAAE,OAAO,CAAC,iBAAiB,CAAM,GACvC,IAAI;IAIP,QAAQ,CACN,UAAU,EAAE,MAAM,EAClB,MAAM,CAAC,EAAE,UAAU,EACnB,OAAO,GAAE,OAAO,CAAC,iBAAiB,CAAM,GACvC,IAAI;IAKP,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,UAAU,EAAE,OAAO,GAAE,OAAO,CAAC,iBAAiB,CAAM,GAAG,IAAI;IAI3F,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,UAAU,EAAE,OAAO,GAAE,OAAO,CAAC,iBAAiB,CAAM,GAAG,IAAI;IAI7F,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,OAAO,CAAC,iBAAiB,CAAM,GAAG,IAAI;IAIzE,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,OAAO,CAAC,iBAAiB,CAAM,GAAG,IAAI;IAI7E,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,OAAO,CAAC,iBAAiB,CAAM,GAAG,IAAI;IAIlF,yFAAyF;IACzF,cAAc,CAAC,MAAM,EAAE,kBAAkB,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI;IAMhE,6EAA6E;IAC7E,8BAA8B,IAAI,IAAI;IAItC,OAAO,CAAC,YAAY;IAKpB,kEAAkE;IAClE,KAAK,IAAI,IAAI;IAWb,8DAA8D;IAC9D,OAAO,CAAC,cAAc;CAqBvB"}
|
package/dist/client.js
CHANGED
|
@@ -1,43 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* - Global error capture (via errors.ts)
|
|
9
|
-
* - Auto-capture breadcrumbs (via autocapture.ts)
|
|
10
|
-
* - Transport (via transport.ts)
|
|
11
|
-
*/
|
|
12
|
-
import { installAutocapture } from "./autocapture";
|
|
13
|
-
import { buildErrorEvent, buildMessageEvent, installGlobalErrorHandlers } from "./errors";
|
|
14
|
-
import { ReplayRecorder } from "./replay";
|
|
15
|
-
import { buildGenerationEvent, buildIdentifyEvent, buildPageEvent, buildScreenEvent, buildSpanEvent, buildToolCallEvent, buildTrackEvent, mergeIdentifiedUser, prepareBrowserEvent, } from "./telemetry";
|
|
16
|
-
import { Transport } from "./transport";
|
|
17
|
-
import { startVitalsCollection } from "./vitals";
|
|
18
|
-
const DEFAULT_ENDPOINT = "https://ingest.squasher.ai";
|
|
19
|
-
function resolveConfig(config) {
|
|
20
|
-
return {
|
|
21
|
-
apiKey: config.apiKey,
|
|
22
|
-
projectId: config.projectId,
|
|
23
|
-
endpoint: config.endpoint ?? DEFAULT_ENDPOINT,
|
|
24
|
-
environment: config.environment,
|
|
25
|
-
release: config.release,
|
|
26
|
-
debug: config.debug ?? false,
|
|
27
|
-
sampleRate: config.sampleRate ?? 1,
|
|
28
|
-
vitalsSampleRate: config.vitalsSampleRate ?? 1,
|
|
29
|
-
enableVitals: config.enableVitals ?? true,
|
|
30
|
-
enableErrorCapture: config.enableErrorCapture ?? true,
|
|
31
|
-
enableAutoBreadcrumbs: config.enableAutoBreadcrumbs ?? true,
|
|
32
|
-
beforeSend: config.beforeSend,
|
|
33
|
-
maxBreadcrumbs: config.maxBreadcrumbs ?? 30,
|
|
34
|
-
vitalsBufferSize: config.vitalsBufferSize ?? 10,
|
|
35
|
-
vitalsFlushIntervalMs: config.vitalsFlushIntervalMs ?? 10_000,
|
|
36
|
-
replayEnabled: config.replay?.enabled ?? false,
|
|
37
|
-
replaySampleRate: config.replay?.sampleRate ?? 1,
|
|
38
|
-
replayPrivacy: config.replay?.privacy,
|
|
39
|
-
};
|
|
40
|
-
}
|
|
1
|
+
import { shouldSampleTelemetry } from "@squasher-ai/sdk-core";
|
|
2
|
+
import { resolveConfig } from "./config.js";
|
|
3
|
+
import { buildErrorEvent, buildMessageEvent } from "./errors.js";
|
|
4
|
+
import { defaultBrowserClientRuntime, } from "./runtime/client.js";
|
|
5
|
+
import { getSessionId } from "./session.js";
|
|
6
|
+
import { buildGenerationEvent, buildIdentifyEvent, buildPageEvent, buildScreenEvent, buildSpanEvent, buildToolCallEvent, buildTrackEvent, mergeIdentifiedUser, prepareBrowserEvent, } from "./telemetry.js";
|
|
7
|
+
import { mapExternalVital } from "./vitals.js";
|
|
41
8
|
export class BrowserClient {
|
|
42
9
|
config;
|
|
43
10
|
transport;
|
|
@@ -47,7 +14,8 @@ export class BrowserClient {
|
|
|
47
14
|
disabled;
|
|
48
15
|
cleanups = [];
|
|
49
16
|
replayRecorder = null;
|
|
50
|
-
|
|
17
|
+
vitalsHandle = null;
|
|
18
|
+
constructor(config, runtime = defaultBrowserClientRuntime) {
|
|
51
19
|
if (!config.apiKey)
|
|
52
20
|
throw new Error("Squasher: apiKey is required");
|
|
53
21
|
if (!config.projectId)
|
|
@@ -58,8 +26,7 @@ export class BrowserClient {
|
|
|
58
26
|
if (this.config.debug) {
|
|
59
27
|
console.warn("[squasher] SDK initialized in disabled mode");
|
|
60
28
|
}
|
|
61
|
-
|
|
62
|
-
this.transport = new Transport({
|
|
29
|
+
this.transport = runtime.createTransport({
|
|
63
30
|
endpoint: this.config.endpoint,
|
|
64
31
|
projectId: this.config.projectId,
|
|
65
32
|
apiKey: this.config.apiKey,
|
|
@@ -69,8 +36,7 @@ export class BrowserClient {
|
|
|
69
36
|
});
|
|
70
37
|
return;
|
|
71
38
|
}
|
|
72
|
-
|
|
73
|
-
this.transport = new Transport({
|
|
39
|
+
this.transport = runtime.createTransport({
|
|
74
40
|
endpoint: this.config.endpoint,
|
|
75
41
|
projectId: this.config.projectId,
|
|
76
42
|
apiKey: this.config.apiKey,
|
|
@@ -78,36 +44,45 @@ export class BrowserClient {
|
|
|
78
44
|
vitalsBufferSize: this.config.vitalsBufferSize,
|
|
79
45
|
vitalsFlushIntervalMs: this.config.vitalsFlushIntervalMs,
|
|
80
46
|
});
|
|
81
|
-
// Start Web Vitals collection
|
|
82
47
|
if (this.config.enableVitals) {
|
|
83
|
-
startVitalsCollection(this.transport, {
|
|
48
|
+
this.vitalsHandle = runtime.startVitalsCollection(this.transport, {
|
|
84
49
|
sampleRate: this.config.vitalsSampleRate,
|
|
85
50
|
environment: this.config.environment,
|
|
86
51
|
release: this.config.release,
|
|
87
52
|
debug: this.config.debug,
|
|
88
53
|
});
|
|
89
54
|
}
|
|
90
|
-
// Install global error handlers
|
|
91
55
|
if (this.config.enableErrorCapture) {
|
|
92
|
-
const cleanup = installGlobalErrorHandlers((event) => {
|
|
56
|
+
const cleanup = runtime.installGlobalErrorHandlers((event) => {
|
|
93
57
|
this.sendErrorEvent(event);
|
|
94
58
|
});
|
|
95
59
|
this.cleanups.push(cleanup);
|
|
96
60
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const cleanup = installAutocapture((crumb) => {
|
|
61
|
+
if (this.config.enableAutoBreadcrumbs || this.config.captureConsoleErrors) {
|
|
62
|
+
const cleanup = runtime.installAutocapture((crumb) => {
|
|
100
63
|
this.addBreadcrumb(crumb);
|
|
64
|
+
}, {
|
|
65
|
+
captureBreadcrumbs: this.config.enableAutoBreadcrumbs,
|
|
66
|
+
onConsoleError: this.config.captureConsoleErrors
|
|
67
|
+
? (event) => {
|
|
68
|
+
this.sendErrorEvent(event);
|
|
69
|
+
}
|
|
70
|
+
: undefined,
|
|
101
71
|
});
|
|
102
72
|
this.cleanups.push(cleanup);
|
|
103
73
|
}
|
|
104
74
|
if (this.config.replayEnabled) {
|
|
105
|
-
this.replayRecorder =
|
|
75
|
+
this.replayRecorder = runtime.createReplayRecorder({
|
|
106
76
|
endpoint: this.config.endpoint,
|
|
107
77
|
projectId: this.config.projectId,
|
|
108
78
|
apiKey: this.config.apiKey,
|
|
109
79
|
debug: this.config.debug,
|
|
110
80
|
sampleRate: this.config.replaySampleRate,
|
|
81
|
+
environment: this.config.environment,
|
|
82
|
+
release: this.config.release,
|
|
83
|
+
getDistinctId: () => this.user?.id,
|
|
84
|
+
getUserId: () => this.user?.id,
|
|
85
|
+
getVisitorId: () => getSessionId(),
|
|
111
86
|
privacy: this.config.replayPrivacy,
|
|
112
87
|
});
|
|
113
88
|
}
|
|
@@ -117,11 +92,11 @@ export class BrowserClient {
|
|
|
117
92
|
vitals: this.config.enableVitals,
|
|
118
93
|
errorCapture: this.config.enableErrorCapture,
|
|
119
94
|
autoBreadcrumbs: this.config.enableAutoBreadcrumbs,
|
|
95
|
+
consoleErrors: this.config.captureConsoleErrors,
|
|
120
96
|
replay: this.config.replayEnabled,
|
|
121
97
|
});
|
|
122
98
|
}
|
|
123
99
|
}
|
|
124
|
-
// ─── Context ──────────────────────────────────────────────────────────
|
|
125
100
|
setUser(user) {
|
|
126
101
|
this.user = user;
|
|
127
102
|
}
|
|
@@ -140,7 +115,6 @@ export class BrowserClient {
|
|
|
140
115
|
this.breadcrumbs = this.breadcrumbs.slice(-this.config.maxBreadcrumbs);
|
|
141
116
|
}
|
|
142
117
|
}
|
|
143
|
-
// ─── Capture ──────────────────────────────────────────────────────────
|
|
144
118
|
captureError(error, extra) {
|
|
145
119
|
this.captureTelemetry(buildErrorEvent(error, extra));
|
|
146
120
|
}
|
|
@@ -150,8 +124,6 @@ export class BrowserClient {
|
|
|
150
124
|
captureTelemetry(event) {
|
|
151
125
|
if (this.disabled)
|
|
152
126
|
return;
|
|
153
|
-
if (!this.shouldSample())
|
|
154
|
-
return;
|
|
155
127
|
this.sendErrorEvent(event);
|
|
156
128
|
}
|
|
157
129
|
track(eventName, properties, context = {}) {
|
|
@@ -176,11 +148,22 @@ export class BrowserClient {
|
|
|
176
148
|
captureGeneration(message, context = {}) {
|
|
177
149
|
this.captureTelemetry(buildGenerationEvent(message, context));
|
|
178
150
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
151
|
+
/** Record a web vital from an external collector (e.g. Next.js `useReportWebVitals`). */
|
|
152
|
+
recordWebVital(metric, route) {
|
|
153
|
+
if (this.disabled)
|
|
154
|
+
return;
|
|
155
|
+
const event = mapExternalVital(metric, this.vitalsConfig(), route);
|
|
156
|
+
this.transport.enqueueVital(event);
|
|
157
|
+
}
|
|
158
|
+
/** Stop the SDK's native `web-vitals` collector to avoid double-counting. */
|
|
159
|
+
suppressNativeVitalsCollection() {
|
|
160
|
+
this.vitalsHandle?.suppress();
|
|
161
|
+
}
|
|
162
|
+
vitalsConfig() {
|
|
163
|
+
const { vitalsSampleRate: sampleRate, environment, release, debug } = this.config;
|
|
164
|
+
return { sampleRate, environment, release, debug };
|
|
165
|
+
}
|
|
166
|
+
/** Flush vitals and stop timers. Call before unmounting a SPA. */
|
|
184
167
|
close() {
|
|
185
168
|
this.replayRecorder?.dispose();
|
|
186
169
|
this.replayRecorder = null;
|
|
@@ -190,14 +173,6 @@ export class BrowserClient {
|
|
|
190
173
|
this.cleanups = [];
|
|
191
174
|
this.transport.dispose();
|
|
192
175
|
}
|
|
193
|
-
// ─── Private ──────────────────────────────────────────────────────────
|
|
194
|
-
shouldSample() {
|
|
195
|
-
if (this.config.sampleRate >= 1)
|
|
196
|
-
return true;
|
|
197
|
-
if (this.config.sampleRate <= 0)
|
|
198
|
-
return false;
|
|
199
|
-
return Math.random() < this.config.sampleRate;
|
|
200
|
-
}
|
|
201
176
|
/** Enrich an error event with current context and send it. */
|
|
202
177
|
sendErrorEvent(event) {
|
|
203
178
|
const prepared = prepareBrowserEvent(event, {
|
|
@@ -208,8 +183,17 @@ export class BrowserClient {
|
|
|
208
183
|
tags: this.tags,
|
|
209
184
|
user: this.user,
|
|
210
185
|
});
|
|
211
|
-
if (prepared)
|
|
212
|
-
|
|
186
|
+
if (!prepared)
|
|
187
|
+
return;
|
|
188
|
+
if (!this.config.debug && !shouldSampleTelemetry(prepared, this.config.sampling)) {
|
|
189
|
+
if (!prepared.measurements?.length)
|
|
190
|
+
return;
|
|
191
|
+
this.transport.sendEvent({
|
|
192
|
+
...prepared,
|
|
193
|
+
attributes: { ...prepared.attributes, "squasher.sampled": false },
|
|
194
|
+
});
|
|
195
|
+
return;
|
|
213
196
|
}
|
|
197
|
+
this.transport.sendEvent(prepared);
|
|
214
198
|
}
|
|
215
199
|
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type ResolvedTelemetrySampling } from "@squasher-ai/sdk-core";
|
|
2
|
+
import type { BrowserConfig, BrowserErrorEvent, ReplayPrivacyConfig } from "./types.js";
|
|
3
|
+
export interface ResolvedConfig {
|
|
4
|
+
apiKey: string;
|
|
5
|
+
projectId: string;
|
|
6
|
+
endpoint: string;
|
|
7
|
+
environment?: string;
|
|
8
|
+
release?: string;
|
|
9
|
+
debug: boolean;
|
|
10
|
+
sampling: ResolvedTelemetrySampling;
|
|
11
|
+
vitalsSampleRate: number;
|
|
12
|
+
enableVitals: boolean;
|
|
13
|
+
enableErrorCapture: boolean;
|
|
14
|
+
enableAutoBreadcrumbs: boolean;
|
|
15
|
+
captureConsoleErrors: boolean;
|
|
16
|
+
beforeSend?: (event: BrowserErrorEvent) => BrowserErrorEvent | null;
|
|
17
|
+
maxBreadcrumbs: number;
|
|
18
|
+
vitalsBufferSize: number;
|
|
19
|
+
vitalsFlushIntervalMs: number;
|
|
20
|
+
replayEnabled: boolean;
|
|
21
|
+
replaySampleRate: number;
|
|
22
|
+
replayPrivacy?: ReplayPrivacyConfig;
|
|
23
|
+
}
|
|
24
|
+
export declare function resolveConfig(config: BrowserConfig): ResolvedConfig;
|
|
25
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,yBAAyB,EAC/B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAExF,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,EAAE,yBAAyB,CAAC;IACpC,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,OAAO,CAAC;IACtB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,qBAAqB,EAAE,OAAO,CAAC;IAC/B,oBAAoB,EAAE,OAAO,CAAC;IAC9B,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,iBAAiB,GAAG,IAAI,CAAC;IACpE,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,MAAM,CAAC;IACzB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,aAAa,EAAE,OAAO,CAAC;IACvB,gBAAgB,EAAE,MAAM,CAAC;IACzB,aAAa,CAAC,EAAE,mBAAmB,CAAC;CACrC;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,cAAc,CAwBnE"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { DEFAULT_INGEST_ENDPOINT, detectRelease, resolveDebugFlag, resolveTelemetrySampling, } from "@squasher-ai/sdk-core";
|
|
2
|
+
export function resolveConfig(config) {
|
|
3
|
+
return {
|
|
4
|
+
apiKey: config.apiKey,
|
|
5
|
+
projectId: config.projectId,
|
|
6
|
+
endpoint: config.endpoint ?? DEFAULT_INGEST_ENDPOINT,
|
|
7
|
+
environment: config.environment,
|
|
8
|
+
// In the browser, detectRelease only picks up build-time-injected vars
|
|
9
|
+
// (Next.js inlines `process.env.NEXT_PUBLIC_*` etc).
|
|
10
|
+
release: config.release ?? detectRelease() ?? undefined,
|
|
11
|
+
debug: resolveDebugFlag(config.debug),
|
|
12
|
+
sampling: resolveTelemetrySampling(config.sampling),
|
|
13
|
+
vitalsSampleRate: config.vitalsSampleRate ?? 1,
|
|
14
|
+
enableVitals: config.enableVitals ?? true,
|
|
15
|
+
enableErrorCapture: config.enableErrorCapture ?? true,
|
|
16
|
+
enableAutoBreadcrumbs: config.enableAutoBreadcrumbs ?? true,
|
|
17
|
+
captureConsoleErrors: config.captureConsoleErrors ?? false,
|
|
18
|
+
beforeSend: config.beforeSend,
|
|
19
|
+
maxBreadcrumbs: config.maxBreadcrumbs ?? 30,
|
|
20
|
+
vitalsBufferSize: config.vitalsBufferSize ?? 10,
|
|
21
|
+
vitalsFlushIntervalMs: config.vitalsFlushIntervalMs ?? 10_000,
|
|
22
|
+
replayEnabled: config.replay?.enabled ?? true,
|
|
23
|
+
replaySampleRate: config.replay?.sampleRate ?? 1,
|
|
24
|
+
replayPrivacy: config.replay?.privacy,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Breadcrumb, BrowserErrorEvent } from "./types.js";
|
|
2
|
+
export type BreadcrumbCallback = (crumb: Omit<Breadcrumb, "timestamp">) => void;
|
|
3
|
+
export interface ConsoleCaptureOptions {
|
|
4
|
+
captureBreadcrumbs: boolean;
|
|
5
|
+
onConsoleError?: (event: BrowserErrorEvent) => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function captureConsole(onBreadcrumb: BreadcrumbCallback, options: ConsoleCaptureOptions): () => void;
|
|
8
|
+
//# sourceMappingURL=console-capture.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"console-capture.d.ts","sourceRoot":"","sources":["../src/console-capture.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAGhE,MAAM,MAAM,kBAAkB,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,UAAU,EAAE,WAAW,CAAC,KAAK,IAAI,CAAC;AAEhF,MAAM,WAAW,qBAAqB;IACpC,kBAAkB,EAAE,OAAO,CAAC;IAC5B,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;CACrD;AAED,wBAAgB,cAAc,CAC5B,YAAY,EAAE,kBAAkB,EAChC,OAAO,EAAE,qBAAqB,GAC7B,MAAM,IAAI,CAkCZ"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { buildErrorEvent, buildMessageEvent } from "./errors.js";
|
|
2
|
+
export function captureConsole(onBreadcrumb, options) {
|
|
3
|
+
const origError = console.error.bind(console);
|
|
4
|
+
const origWarn = console.warn.bind(console);
|
|
5
|
+
console.error = (...args) => {
|
|
6
|
+
if (!isInternalSquasherConsole(args)) {
|
|
7
|
+
const message = formatConsoleArgs(args);
|
|
8
|
+
if (options.captureBreadcrumbs) {
|
|
9
|
+
onBreadcrumb({
|
|
10
|
+
category: "console",
|
|
11
|
+
message: message.slice(0, 200),
|
|
12
|
+
level: "error",
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
options.onConsoleError?.(buildConsoleErrorEvent(args, message));
|
|
16
|
+
}
|
|
17
|
+
origError(...args);
|
|
18
|
+
};
|
|
19
|
+
console.warn = (...args) => {
|
|
20
|
+
if (options.captureBreadcrumbs && !isInternalSquasherConsole(args)) {
|
|
21
|
+
onBreadcrumb({
|
|
22
|
+
category: "console",
|
|
23
|
+
message: formatConsoleArgs(args).slice(0, 200),
|
|
24
|
+
level: "warning",
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
origWarn(...args);
|
|
28
|
+
};
|
|
29
|
+
return () => {
|
|
30
|
+
console.error = origError;
|
|
31
|
+
console.warn = origWarn;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function buildConsoleErrorEvent(args, message) {
|
|
35
|
+
const error = args.find((arg) => arg instanceof Error);
|
|
36
|
+
if (error instanceof Error) {
|
|
37
|
+
const event = buildErrorEvent(error, {
|
|
38
|
+
argument_count: args.length,
|
|
39
|
+
source: "console",
|
|
40
|
+
});
|
|
41
|
+
event.message = message || event.message;
|
|
42
|
+
event.type = event.type === "Error" ? "ConsoleError" : `ConsoleError(${event.type})`;
|
|
43
|
+
return event;
|
|
44
|
+
}
|
|
45
|
+
const event = buildMessageEvent(message || "console.error", "error");
|
|
46
|
+
event.type = "ConsoleError";
|
|
47
|
+
event.extra = {
|
|
48
|
+
argument_count: args.length,
|
|
49
|
+
source: "console",
|
|
50
|
+
};
|
|
51
|
+
return event;
|
|
52
|
+
}
|
|
53
|
+
function isInternalSquasherConsole(args) {
|
|
54
|
+
const first = args[0];
|
|
55
|
+
return (Object.prototype.toString.call(first) === "[object String]" &&
|
|
56
|
+
String(first).startsWith("[squasher]"));
|
|
57
|
+
}
|
|
58
|
+
function formatConsoleArgs(args) {
|
|
59
|
+
return args.map(formatConsoleArg).join(" ").slice(0, 1_000);
|
|
60
|
+
}
|
|
61
|
+
function formatConsoleArg(arg) {
|
|
62
|
+
if (arg instanceof Error) {
|
|
63
|
+
return `${arg.name}: ${arg.message}`;
|
|
64
|
+
}
|
|
65
|
+
if (arg === null) {
|
|
66
|
+
return "null";
|
|
67
|
+
}
|
|
68
|
+
if (arg === undefined) {
|
|
69
|
+
return "undefined";
|
|
70
|
+
}
|
|
71
|
+
const tag = Object.prototype.toString.call(arg);
|
|
72
|
+
if (tag === "[object String]" ||
|
|
73
|
+
tag === "[object Number]" ||
|
|
74
|
+
tag === "[object Boolean]" ||
|
|
75
|
+
tag === "[object BigInt]") {
|
|
76
|
+
return String(arg);
|
|
77
|
+
}
|
|
78
|
+
return Object.prototype.toString.call(arg);
|
|
79
|
+
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Parses Error.stack into StackFrame[] using the same regex pattern as
|
|
9
9
|
* @squasher-ai/node. In-app heuristic: node_modules = not in_app.
|
|
10
10
|
*/
|
|
11
|
-
import type { BrowserErrorEvent, StackFrame } from "./types";
|
|
11
|
+
import type { BrowserErrorEvent, JsonObject, StackFrame } from "./types.js";
|
|
12
12
|
/**
|
|
13
13
|
* Parse an Error.stack string into structured StackFrame[].
|
|
14
14
|
*/
|
|
@@ -16,7 +16,7 @@ export declare function parseStack(stack?: string): StackFrame[] | undefined;
|
|
|
16
16
|
/**
|
|
17
17
|
* Build a BrowserErrorEvent from an Error object.
|
|
18
18
|
*/
|
|
19
|
-
export declare function buildErrorEvent(error: Error, extra?:
|
|
19
|
+
export declare function buildErrorEvent(error: Error, extra?: JsonObject): BrowserErrorEvent;
|
|
20
20
|
/**
|
|
21
21
|
* Build a BrowserErrorEvent from a string message.
|
|
22
22
|
*/
|
package/dist/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAW5E;;GAEG;AACH,wBAAgB,UAAU,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,UAAU,EAAE,GAAG,SAAS,CA8BnE;AAWD;;GAEG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,UAAU,GAAG,iBAAiB,CAWnF;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,KAAK,GAAE,iBAAiB,CAAC,OAAO,CAAU,GACzC,iBAAiB,CAOnB;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,GAC1C,MAAM,IAAI,CAuCZ"}
|
package/dist/errors.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* @squasher-ai/node. In-app heuristic: node_modules = not in_app.
|
|
10
10
|
*/
|
|
11
11
|
const SDK_NAME = "@squasher-ai/browser";
|
|
12
|
-
const SDK_VERSION = "0.
|
|
12
|
+
const SDK_VERSION = "0.2.0";
|
|
13
13
|
/** Stack trace line regex — works for V8 (Chrome, Node) and SpiderMonkey (Firefox). */
|
|
14
14
|
const STACK_LINE_RE = /^\s*at\s+(?:(.+?)\s+\()?(.+?):(\d+):(\d+)\)?/;
|
|
15
15
|
/** Firefox stack format: "functionName@filename:line:col" */
|
|
@@ -33,10 +33,7 @@ export function parseStack(stack) {
|
|
|
33
33
|
if (!match)
|
|
34
34
|
continue;
|
|
35
35
|
const [, fn, filename, lineno, colno] = match;
|
|
36
|
-
const isExternal = filename
|
|
37
|
-
filename?.includes("extensions/") ||
|
|
38
|
-
filename?.startsWith("chrome-extension://") ||
|
|
39
|
-
false;
|
|
36
|
+
const isExternal = isExternalFilename(filename);
|
|
40
37
|
frames.push({
|
|
41
38
|
function: fn || "<anonymous>",
|
|
42
39
|
filename,
|
|
@@ -47,6 +44,12 @@ export function parseStack(stack) {
|
|
|
47
44
|
}
|
|
48
45
|
return frames.length > 0 ? frames : undefined;
|
|
49
46
|
}
|
|
47
|
+
function isExternalFilename(filename) {
|
|
48
|
+
return (filename?.includes("node_modules") ||
|
|
49
|
+
filename?.includes("extensions/") ||
|
|
50
|
+
filename?.startsWith("chrome-extension://") ||
|
|
51
|
+
false);
|
|
52
|
+
}
|
|
50
53
|
/**
|
|
51
54
|
* Build a BrowserErrorEvent from an Error object.
|
|
52
55
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* - Core Web Vitals (LCP, CLS, INP, FCP, TTFB)
|
|
6
6
|
* - JavaScript errors (window.onerror, unhandledrejection)
|
|
7
7
|
* - Navigation breadcrumbs, click breadcrumbs, fetch error breadcrumbs
|
|
8
|
+
* - Optional console.error events
|
|
8
9
|
* - Session context for error ↔ vitals correlation
|
|
9
10
|
*
|
|
10
11
|
* Usage:
|
|
@@ -21,42 +22,8 @@
|
|
|
21
22
|
* // Manual capture:
|
|
22
23
|
* captureError(new Error('Something went wrong'));
|
|
23
24
|
*/
|
|
24
|
-
export { BrowserClient } from "./client";
|
|
25
|
-
export
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
/** Initialize the global Squasher browser client. Throws if called twice. */
|
|
29
|
-
export declare function init(config: BrowserConfig): BrowserClient;
|
|
30
|
-
/** Get the global client (throws if not initialized). */
|
|
31
|
-
export declare function getClient(): BrowserClient;
|
|
32
|
-
/** Capture an error using the global client. */
|
|
33
|
-
export declare function captureError(error: Error, extra?: Record<string, unknown>): void;
|
|
34
|
-
/** Capture a message using the global client. */
|
|
35
|
-
export declare function captureMessage(message: string, level?: Level): void;
|
|
36
|
-
/** Capture a generalized telemetry event using the global client. */
|
|
37
|
-
export declare function captureTelemetry(event: BrowserErrorEvent): void;
|
|
38
|
-
/** Track a product analytics event using the global client. */
|
|
39
|
-
export declare function track(eventName: string, properties?: JsonObject, context?: Partial<BrowserErrorEvent>): void;
|
|
40
|
-
/** Identify a distinct user or visitor using the global client. */
|
|
41
|
-
export declare function identify(distinctId: string, traits?: JsonObject, context?: Partial<BrowserErrorEvent>): void;
|
|
42
|
-
/** Capture a page navigation event using the global client. */
|
|
43
|
-
export declare function page(name: string, properties?: JsonObject, context?: Partial<BrowserErrorEvent>): void;
|
|
44
|
-
/** Capture a screen event using the global client. */
|
|
45
|
-
export declare function screen(name: string, properties?: JsonObject, context?: Partial<BrowserErrorEvent>): void;
|
|
46
|
-
/** Capture an agent span using the global client. */
|
|
47
|
-
export declare function captureSpan(name: string, context?: Partial<BrowserErrorEvent>): void;
|
|
48
|
-
/** Capture a tool call using the global client. */
|
|
49
|
-
export declare function captureToolCall(name: string, context?: Partial<BrowserErrorEvent>): void;
|
|
50
|
-
/** Capture an LLM generation event using the global client. */
|
|
51
|
-
export declare function captureGeneration(message: string, context?: Partial<BrowserErrorEvent>): void;
|
|
52
|
-
/** Set user context on the global client. */
|
|
53
|
-
export declare function setUser(user: UserContext | undefined): void;
|
|
54
|
-
/** Set a single tag on the global client. */
|
|
55
|
-
export declare function setTag(key: string, value: string): void;
|
|
56
|
-
/** Set multiple tags on the global client. */
|
|
57
|
-
export declare function setTags(tags: Record<string, string>): void;
|
|
58
|
-
/** Add a breadcrumb on the global client. */
|
|
59
|
-
export declare function addBreadcrumb(crumb: Omit<Breadcrumb, "timestamp">): void;
|
|
60
|
-
/** Close the global client (stop timers, flush vitals, remove handlers). */
|
|
61
|
-
export declare function close(): void;
|
|
25
|
+
export { BrowserClient } from "./client.js";
|
|
26
|
+
export { addBreadcrumb, captureError, captureGeneration, captureMessage, captureSpan, captureTelemetry, captureToolCall, close, getClient, identify, init, page, screen, setTag, setTags, setUser, track, } from "./runtime/client-singleton.js";
|
|
27
|
+
export { createSpanId, createTraceContext, createTraceId } from "./trace-context.js";
|
|
28
|
+
export type { AnalyticsContext, Breadcrumb, BrowserConfig, BrowserErrorEvent, DeviceType, IngestBatchPayload, IngestResponse, JsonObject, Level, LlmContext, PageContext, ReplayConfig, ReplayPrivacyConfig, SessionContext, StackFrame, TelemetryKind, TelemetryMeasurement, ToolCallContext, TraceContext, UserContext, VisitorContext, VitalEvent, VitalMetricName, VitalRating, VitalsPayload, } from "./types.js";
|
|
62
29
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EACL,aAAa,EACb,YAAY,EACZ,iBAAiB,EACjB,cAAc,EACd,WAAW,EACX,gBAAgB,EAChB,eAAe,EACf,KAAK,EACL,SAAS,EACT,QAAQ,EACR,IAAI,EACJ,IAAI,EACJ,MAAM,EACN,MAAM,EACN,OAAO,EACP,OAAO,EACP,KAAK,GACN,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACrF,YAAY,EACV,gBAAgB,EAChB,UAAU,EACV,aAAa,EACb,iBAAiB,EACjB,UAAU,EACV,kBAAkB,EAClB,cAAc,EACd,UAAU,EACV,KAAK,EACL,UAAU,EACV,WAAW,EACX,YAAY,EACZ,mBAAmB,EACnB,cAAc,EACd,UAAU,EACV,aAAa,EACb,oBAAoB,EACpB,eAAe,EACf,YAAY,EACZ,WAAW,EACX,cAAc,EACd,UAAU,EACV,eAAe,EACf,WAAW,EACX,aAAa,GACd,MAAM,YAAY,CAAC"}
|