@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/index.js
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,85 +22,6 @@
|
|
|
21
22
|
* // Manual capture:
|
|
22
23
|
* captureError(new Error('Something went wrong'));
|
|
23
24
|
*/
|
|
24
|
-
export { BrowserClient } from "./client";
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
let _client = null;
|
|
28
|
-
/** Initialize the global Squasher browser client. Throws if called twice. */
|
|
29
|
-
export function init(config) {
|
|
30
|
-
if (_client) {
|
|
31
|
-
throw new Error("squasher.init() called more than once. Call close() first if re-initializing.");
|
|
32
|
-
}
|
|
33
|
-
_client = new BrowserClient(config);
|
|
34
|
-
return _client;
|
|
35
|
-
}
|
|
36
|
-
/** Get the global client (throws if not initialized). */
|
|
37
|
-
export function getClient() {
|
|
38
|
-
if (!_client) {
|
|
39
|
-
throw new Error("Squasher not initialized. Call init() first.");
|
|
40
|
-
}
|
|
41
|
-
return _client;
|
|
42
|
-
}
|
|
43
|
-
/** Capture an error using the global client. */
|
|
44
|
-
export function captureError(error, extra) {
|
|
45
|
-
getClient().captureError(error, extra);
|
|
46
|
-
}
|
|
47
|
-
/** Capture a message using the global client. */
|
|
48
|
-
export function captureMessage(message, level) {
|
|
49
|
-
getClient().captureMessage(message, level);
|
|
50
|
-
}
|
|
51
|
-
/** Capture a generalized telemetry event using the global client. */
|
|
52
|
-
export function captureTelemetry(event) {
|
|
53
|
-
getClient().captureTelemetry(event);
|
|
54
|
-
}
|
|
55
|
-
/** Track a product analytics event using the global client. */
|
|
56
|
-
export function track(eventName, properties, context) {
|
|
57
|
-
getClient().track(eventName, properties, context);
|
|
58
|
-
}
|
|
59
|
-
/** Identify a distinct user or visitor using the global client. */
|
|
60
|
-
export function identify(distinctId, traits, context) {
|
|
61
|
-
getClient().identify(distinctId, traits, context);
|
|
62
|
-
}
|
|
63
|
-
/** Capture a page navigation event using the global client. */
|
|
64
|
-
export function page(name, properties, context) {
|
|
65
|
-
getClient().page(name, properties, context);
|
|
66
|
-
}
|
|
67
|
-
/** Capture a screen event using the global client. */
|
|
68
|
-
export function screen(name, properties, context) {
|
|
69
|
-
getClient().screen(name, properties, context);
|
|
70
|
-
}
|
|
71
|
-
/** Capture an agent span using the global client. */
|
|
72
|
-
export function captureSpan(name, context) {
|
|
73
|
-
getClient().captureSpan(name, context);
|
|
74
|
-
}
|
|
75
|
-
/** Capture a tool call using the global client. */
|
|
76
|
-
export function captureToolCall(name, context) {
|
|
77
|
-
getClient().captureToolCall(name, context);
|
|
78
|
-
}
|
|
79
|
-
/** Capture an LLM generation event using the global client. */
|
|
80
|
-
export function captureGeneration(message, context) {
|
|
81
|
-
getClient().captureGeneration(message, context);
|
|
82
|
-
}
|
|
83
|
-
/** Set user context on the global client. */
|
|
84
|
-
export function setUser(user) {
|
|
85
|
-
getClient().setUser(user);
|
|
86
|
-
}
|
|
87
|
-
/** Set a single tag on the global client. */
|
|
88
|
-
export function setTag(key, value) {
|
|
89
|
-
getClient().setTag(key, value);
|
|
90
|
-
}
|
|
91
|
-
/** Set multiple tags on the global client. */
|
|
92
|
-
export function setTags(tags) {
|
|
93
|
-
getClient().setTags(tags);
|
|
94
|
-
}
|
|
95
|
-
/** Add a breadcrumb on the global client. */
|
|
96
|
-
export function addBreadcrumb(crumb) {
|
|
97
|
-
getClient().addBreadcrumb(crumb);
|
|
98
|
-
}
|
|
99
|
-
/** Close the global client (stop timers, flush vitals, remove handlers). */
|
|
100
|
-
export function close() {
|
|
101
|
-
if (_client) {
|
|
102
|
-
_client.close();
|
|
103
|
-
_client = null;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
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";
|
package/dist/next.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Next.js Web Vitals bridge for @squasher-ai/browser.
|
|
3
|
+
*
|
|
4
|
+
* Drop `<SquasherNextWebVitals />` into your root layout to capture both
|
|
5
|
+
* Core Web Vitals (LCP, CLS, INP, FCP, TTFB) and Next.js framework metrics
|
|
6
|
+
* (`Next.js-hydration`, `Next.js-route-change-to-render`, `Next.js-render`).
|
|
7
|
+
*
|
|
8
|
+
* The component hard-imports `next/web-vitals` and `next/navigation` so it
|
|
9
|
+
* is **only** valid in a Next.js app. Non-Next consumers should rely on the
|
|
10
|
+
* auto-collector that runs from `init()` and not import this module.
|
|
11
|
+
*
|
|
12
|
+
* Internally it bridges Next's `useReportWebVitals` hook into the existing
|
|
13
|
+
* browser client transport, suppressing the SDK's native `web-vitals`
|
|
14
|
+
* collector on mount so Core metrics aren't reported twice.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* import { SquasherNextWebVitals } from '@squasher-ai/browser/next';
|
|
18
|
+
*
|
|
19
|
+
* export default function RootLayout({ children }) {
|
|
20
|
+
* return (
|
|
21
|
+
* <html>
|
|
22
|
+
* <body>
|
|
23
|
+
* <SquasherNextWebVitals />
|
|
24
|
+
* {children}
|
|
25
|
+
* </body>
|
|
26
|
+
* </html>
|
|
27
|
+
* );
|
|
28
|
+
* }
|
|
29
|
+
*/
|
|
30
|
+
interface SquasherNextWebVitalsProps {
|
|
31
|
+
/**
|
|
32
|
+
* Override the route tag attached to every metric. Defaults to the
|
|
33
|
+
* current `usePathname()` value, with the pathname re-evaluated lazily
|
|
34
|
+
* so a single hook subscription doesn't re-fire on every navigation.
|
|
35
|
+
*/
|
|
36
|
+
path?: string;
|
|
37
|
+
}
|
|
38
|
+
export declare function SquasherNextWebVitals({ path }?: SquasherNextWebVitalsProps): null;
|
|
39
|
+
export {};
|
|
40
|
+
//# sourceMappingURL=next.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"next.d.ts","sourceRoot":"","sources":["../src/next.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAeH,UAAU,0BAA0B;IAClC;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,qBAAqB,CAAC,EAAE,IAAI,EAAE,GAAE,0BAA+B,QA+B9E"}
|
package/dist/next.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Next.js Web Vitals bridge for @squasher-ai/browser.
|
|
3
|
+
*
|
|
4
|
+
* Drop `<SquasherNextWebVitals />` into your root layout to capture both
|
|
5
|
+
* Core Web Vitals (LCP, CLS, INP, FCP, TTFB) and Next.js framework metrics
|
|
6
|
+
* (`Next.js-hydration`, `Next.js-route-change-to-render`, `Next.js-render`).
|
|
7
|
+
*
|
|
8
|
+
* The component hard-imports `next/web-vitals` and `next/navigation` so it
|
|
9
|
+
* is **only** valid in a Next.js app. Non-Next consumers should rely on the
|
|
10
|
+
* auto-collector that runs from `init()` and not import this module.
|
|
11
|
+
*
|
|
12
|
+
* Internally it bridges Next's `useReportWebVitals` hook into the existing
|
|
13
|
+
* browser client transport, suppressing the SDK's native `web-vitals`
|
|
14
|
+
* collector on mount so Core metrics aren't reported twice.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* import { SquasherNextWebVitals } from '@squasher-ai/browser/next';
|
|
18
|
+
*
|
|
19
|
+
* export default function RootLayout({ children }) {
|
|
20
|
+
* return (
|
|
21
|
+
* <html>
|
|
22
|
+
* <body>
|
|
23
|
+
* <SquasherNextWebVitals />
|
|
24
|
+
* {children}
|
|
25
|
+
* </body>
|
|
26
|
+
* </html>
|
|
27
|
+
* );
|
|
28
|
+
* }
|
|
29
|
+
*/
|
|
30
|
+
"use client";
|
|
31
|
+
import { usePathname } from "next/navigation";
|
|
32
|
+
import { useReportWebVitals } from "next/web-vitals";
|
|
33
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
34
|
+
import { getClient } from "./runtime/client-singleton.js";
|
|
35
|
+
const SUPPORTED_WEB_VITALS = new Set(["CLS", "FCP", "INP", "LCP", "TTFB"]);
|
|
36
|
+
function isSupportedWebVital(name) {
|
|
37
|
+
return SUPPORTED_WEB_VITALS.has(name);
|
|
38
|
+
}
|
|
39
|
+
export function SquasherNextWebVitals({ path } = {}) {
|
|
40
|
+
const pathname = usePathname();
|
|
41
|
+
const currentPath = path ?? pathname ?? "/";
|
|
42
|
+
const stabilizedPath = useRef(currentPath);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
stabilizedPath.current = currentPath;
|
|
45
|
+
}, [currentPath]);
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
try {
|
|
48
|
+
getClient().suppressNativeVitalsCollection();
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Squasher SDK not initialized — nothing to suppress. Bridge still
|
|
52
|
+
// runs so the host can call init() asynchronously.
|
|
53
|
+
}
|
|
54
|
+
}, []);
|
|
55
|
+
const report = useCallback((metric) => {
|
|
56
|
+
if (!isSupportedWebVital(metric.name))
|
|
57
|
+
return;
|
|
58
|
+
try {
|
|
59
|
+
getClient().recordWebVital(metric, stabilizedPath.current);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// SDK not yet initialized; drop this metric rather than throw.
|
|
63
|
+
}
|
|
64
|
+
}, []);
|
|
65
|
+
useReportWebVitals(report);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
package/dist/react.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,SAAS,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,SAAS,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAKlE,UAAU,KAAK;IACb,QAAQ,EAAE,SAAS,CAAC;IACpB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,iDAAiD;IACjD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,KAAK,IAAI,CAAC;CACxD;AAED,UAAU,KAAK;IACb,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,qBAAa,qBAAsB,SAAQ,SAAS,CAAC,KAAK,EAAE,KAAK,CAAC;gBACpD,KAAK,EAAE,KAAK;IAKxB,MAAM,CAAC,wBAAwB,IAAI,KAAK;IAI/B,iBAAiB,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,GAAG,IAAI;IAc3D,MAAM,IAAI,SAAS;CAM7B"}
|
package/dist/react.js
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
* </SquasherErrorBoundary>
|
|
13
13
|
*/
|
|
14
14
|
import { Component } from "react";
|
|
15
|
-
import {
|
|
15
|
+
import { attempt } from "@squasher-ai/result-utils";
|
|
16
|
+
import { getClient } from "./runtime/client-singleton.js";
|
|
16
17
|
export class SquasherErrorBoundary extends Component {
|
|
17
18
|
constructor(props) {
|
|
18
19
|
super(props);
|
|
@@ -22,15 +23,16 @@ export class SquasherErrorBoundary extends Component {
|
|
|
22
23
|
return { hasError: true };
|
|
23
24
|
}
|
|
24
25
|
componentDidCatch(error, errorInfo) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
26
|
+
attempt({
|
|
27
|
+
try: () => {
|
|
28
|
+
const client = getClient();
|
|
29
|
+
const extra = {};
|
|
30
|
+
if (errorInfo.componentStack)
|
|
31
|
+
extra.componentStack = errorInfo.componentStack;
|
|
32
|
+
client.captureError(error, extra);
|
|
33
|
+
},
|
|
34
|
+
catch: () => undefined,
|
|
35
|
+
});
|
|
34
36
|
this.props.onError?.(error, errorInfo);
|
|
35
37
|
}
|
|
36
38
|
render() {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface ReplayNetworkEventPayload {
|
|
2
|
+
durationMs: number;
|
|
3
|
+
initiatorType: string;
|
|
4
|
+
method?: string;
|
|
5
|
+
pageUrl: string;
|
|
6
|
+
protocol?: string | null;
|
|
7
|
+
requestUrl: string;
|
|
8
|
+
source: "fetch" | "resource";
|
|
9
|
+
startOffsetMs: number;
|
|
10
|
+
statusCode?: number | null;
|
|
11
|
+
statusText?: string | null;
|
|
12
|
+
transferSize?: number | null;
|
|
13
|
+
}
|
|
14
|
+
export interface ReplayNetworkCaptureOptions {
|
|
15
|
+
debug: boolean;
|
|
16
|
+
emitCustomEvent?: (tag: string, payload: ReplayNetworkEventPayload) => void;
|
|
17
|
+
ignoredUrlPrefixes?: string[];
|
|
18
|
+
replayStartedAtMs: number;
|
|
19
|
+
}
|
|
20
|
+
export declare function installReplayNetworkCapture({ debug, emitCustomEvent, ignoredUrlPrefixes, replayStartedAtMs, }: ReplayNetworkCaptureOptions): () => void;
|
|
21
|
+
//# sourceMappingURL=replay-network.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay-network.d.ts","sourceRoot":"","sources":["../src/replay-network.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,yBAAyB;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,OAAO,GAAG,UAAU,CAAC;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,2BAA2B;IAC1C,KAAK,EAAE,OAAO,CAAC;IACf,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,yBAAyB,KAAK,IAAI,CAAC;IAC5E,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC9B,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAsED,wBAAgB,2BAA2B,CAAC,EAC1C,KAAK,EACL,eAAe,EACf,kBAAuB,EACvB,iBAAiB,GAClB,EAAE,2BAA2B,GAAG,MAAM,IAAI,CA6J1C"}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { attempt, attemptAsync } from "@squasher-ai/result-utils";
|
|
2
|
+
import { readRuntimeNumber, readRuntimeProperty, readRuntimeString } from "@squasher-ai/sdk-core";
|
|
3
|
+
const NETWORK_EVENT_TAG = "squasher:network";
|
|
4
|
+
const MAX_NETWORK_EVENTS = 400;
|
|
5
|
+
function clampOffset(offsetMs) {
|
|
6
|
+
return Math.max(0, Math.round(offsetMs));
|
|
7
|
+
}
|
|
8
|
+
function roundDuration(durationMs) {
|
|
9
|
+
return Math.max(0, Math.round(durationMs));
|
|
10
|
+
}
|
|
11
|
+
function readFiniteNumber(value) {
|
|
12
|
+
return value !== null && value !== undefined && Number.isFinite(value) ? value : null;
|
|
13
|
+
}
|
|
14
|
+
function readFiniteString(value) {
|
|
15
|
+
return value && value.length > 0 ? value : null;
|
|
16
|
+
}
|
|
17
|
+
function getCurrentPageUrl() {
|
|
18
|
+
return globalThis.location?.href ?? "";
|
|
19
|
+
}
|
|
20
|
+
function shouldIgnoreRequestUrl(requestUrl, ignoredUrlPrefixes) {
|
|
21
|
+
return ignoredUrlPrefixes.some((prefix) => requestUrl.startsWith(prefix));
|
|
22
|
+
}
|
|
23
|
+
function toResourceKey(payload) {
|
|
24
|
+
return [
|
|
25
|
+
payload.source,
|
|
26
|
+
payload.requestUrl,
|
|
27
|
+
payload.initiatorType,
|
|
28
|
+
payload.startOffsetMs,
|
|
29
|
+
payload.durationMs,
|
|
30
|
+
].join("|");
|
|
31
|
+
}
|
|
32
|
+
function buildResourcePayload(entry, replayStartedAtMs) {
|
|
33
|
+
if (entry.entryType !== "resource" && entry.entryType !== "navigation") {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const initiatorType = readRuntimeString(readRuntimeProperty(entry, "initiatorType"));
|
|
37
|
+
const nextHopProtocol = readRuntimeString(readRuntimeProperty(entry, "nextHopProtocol"));
|
|
38
|
+
const renderBlockingStatus = readRuntimeString(readRuntimeProperty(entry, "renderBlockingStatus"));
|
|
39
|
+
const responseStatus = readRuntimeNumber(readRuntimeProperty(entry, "responseStatus"));
|
|
40
|
+
const transferSize = readRuntimeNumber(readRuntimeProperty(entry, "transferSize"));
|
|
41
|
+
const absoluteStartMs = performance.timeOrigin + entry.startTime;
|
|
42
|
+
return {
|
|
43
|
+
durationMs: roundDuration(entry.duration),
|
|
44
|
+
initiatorType: entry.entryType === "navigation"
|
|
45
|
+
? "navigation"
|
|
46
|
+
: readFiniteString(initiatorType) || "resource",
|
|
47
|
+
pageUrl: getCurrentPageUrl(),
|
|
48
|
+
protocol: readFiniteString(nextHopProtocol),
|
|
49
|
+
requestUrl: entry.name,
|
|
50
|
+
source: "resource",
|
|
51
|
+
startOffsetMs: clampOffset(absoluteStartMs - replayStartedAtMs),
|
|
52
|
+
statusCode: readFiniteNumber(responseStatus),
|
|
53
|
+
statusText: readFiniteString(renderBlockingStatus),
|
|
54
|
+
transferSize: readFiniteNumber(transferSize),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function installReplayNetworkCapture({ debug, emitCustomEvent, ignoredUrlPrefixes = [], replayStartedAtMs, }) {
|
|
58
|
+
if (!emitCustomEvent || !globalThis.window) {
|
|
59
|
+
return () => undefined;
|
|
60
|
+
}
|
|
61
|
+
let emittedCount = 0;
|
|
62
|
+
let customEventAvailable = true;
|
|
63
|
+
const seenResources = new Set();
|
|
64
|
+
const cleanups = [];
|
|
65
|
+
const emit = (payload) => {
|
|
66
|
+
if (!customEventAvailable || emittedCount >= MAX_NETWORK_EVENTS) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const emitted = attempt({
|
|
70
|
+
try: () => {
|
|
71
|
+
emitCustomEvent(NETWORK_EVENT_TAG, payload);
|
|
72
|
+
return true;
|
|
73
|
+
},
|
|
74
|
+
catch: ({ error }) => {
|
|
75
|
+
customEventAvailable = false;
|
|
76
|
+
if (debug) {
|
|
77
|
+
console.warn("[squasher] Replay network event capture disabled", {
|
|
78
|
+
error: error.message,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
if (!emitted)
|
|
85
|
+
return;
|
|
86
|
+
emittedCount += 1;
|
|
87
|
+
if (debug) {
|
|
88
|
+
console.log("[squasher] Replay network event recorded", {
|
|
89
|
+
requestUrl: payload.requestUrl,
|
|
90
|
+
source: payload.source,
|
|
91
|
+
statusCode: payload.statusCode ?? null,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
if (globalThis.PerformanceObserver) {
|
|
96
|
+
const supportedEntryTypes = globalThis.PerformanceObserver.supportedEntryTypes ?? [];
|
|
97
|
+
const observer = new globalThis.PerformanceObserver((list) => {
|
|
98
|
+
for (const entry of list.getEntries()) {
|
|
99
|
+
const payload = buildResourcePayload(entry, replayStartedAtMs);
|
|
100
|
+
if (!payload) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (shouldIgnoreRequestUrl(payload.requestUrl, ignoredUrlPrefixes)) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const absoluteStartMs = performance.timeOrigin + entry.startTime;
|
|
107
|
+
if (payload.initiatorType === "fetch" && absoluteStartMs >= replayStartedAtMs) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const key = toResourceKey(payload);
|
|
111
|
+
if (seenResources.has(key)) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
seenResources.add(key);
|
|
115
|
+
emit(payload);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
attempt({
|
|
119
|
+
try: () => {
|
|
120
|
+
if (supportedEntryTypes.includes("navigation")) {
|
|
121
|
+
observer.observe({ buffered: true, type: "navigation" });
|
|
122
|
+
}
|
|
123
|
+
if (supportedEntryTypes.includes("resource")) {
|
|
124
|
+
observer.observe({ buffered: true, type: "resource" });
|
|
125
|
+
}
|
|
126
|
+
cleanups.push(() => observer.disconnect());
|
|
127
|
+
},
|
|
128
|
+
catch: () => {
|
|
129
|
+
observer.disconnect();
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
if (window.fetch) {
|
|
134
|
+
const originalFetch = window.fetch.bind(window);
|
|
135
|
+
window.fetch = async (input, init) => {
|
|
136
|
+
const requestStartedAtMs = Date.now();
|
|
137
|
+
const inputIsRequest = globalThis.Request && input instanceof globalThis.Request;
|
|
138
|
+
const method = init?.method?.toUpperCase() || (inputIsRequest ? input.method.toUpperCase() : "GET");
|
|
139
|
+
const requestUrl = Object.prototype.toString.call(input) === "[object String]"
|
|
140
|
+
? String(input)
|
|
141
|
+
: input instanceof URL
|
|
142
|
+
? input.href
|
|
143
|
+
: inputIsRequest
|
|
144
|
+
? input.url
|
|
145
|
+
: String(input);
|
|
146
|
+
const pageUrl = getCurrentPageUrl();
|
|
147
|
+
if (shouldIgnoreRequestUrl(requestUrl, ignoredUrlPrefixes)) {
|
|
148
|
+
return originalFetch(input, init);
|
|
149
|
+
}
|
|
150
|
+
return attemptAsync({
|
|
151
|
+
try: async () => {
|
|
152
|
+
const response = await originalFetch(input, init);
|
|
153
|
+
emit({
|
|
154
|
+
durationMs: roundDuration(Date.now() - requestStartedAtMs),
|
|
155
|
+
initiatorType: "fetch",
|
|
156
|
+
method,
|
|
157
|
+
pageUrl,
|
|
158
|
+
requestUrl,
|
|
159
|
+
source: "fetch",
|
|
160
|
+
startOffsetMs: clampOffset(requestStartedAtMs - replayStartedAtMs),
|
|
161
|
+
statusCode: response.status,
|
|
162
|
+
statusText: response.statusText || null,
|
|
163
|
+
transferSize: null,
|
|
164
|
+
});
|
|
165
|
+
return response;
|
|
166
|
+
},
|
|
167
|
+
catch: ({ cause, error }) => {
|
|
168
|
+
emit({
|
|
169
|
+
durationMs: roundDuration(Date.now() - requestStartedAtMs),
|
|
170
|
+
initiatorType: "fetch",
|
|
171
|
+
method,
|
|
172
|
+
pageUrl,
|
|
173
|
+
requestUrl,
|
|
174
|
+
source: "fetch",
|
|
175
|
+
startOffsetMs: clampOffset(requestStartedAtMs - replayStartedAtMs),
|
|
176
|
+
statusCode: null,
|
|
177
|
+
statusText: error.message || "network error",
|
|
178
|
+
transferSize: null,
|
|
179
|
+
});
|
|
180
|
+
throw cause;
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
};
|
|
184
|
+
cleanups.push(() => {
|
|
185
|
+
window.fetch = originalFetch;
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
return () => {
|
|
189
|
+
for (const cleanup of cleanups) {
|
|
190
|
+
cleanup();
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
}
|
package/dist/replay-privacy.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ReplayPrivacyConfig } from "./types";
|
|
2
|
-
type ReplayRecordFn = typeof import("rrweb")["record"];
|
|
1
|
+
import type { ReplayPrivacyConfig } from "./types.js";
|
|
2
|
+
type ReplayRecordFn = (typeof import("rrweb"))["record"];
|
|
3
3
|
type ReplayRecordOptions = NonNullable<Parameters<ReplayRecordFn>[0]>;
|
|
4
4
|
type ReplayEmit = NonNullable<ReplayRecordOptions["emit"]>;
|
|
5
5
|
type ReplayEvent = Parameters<ReplayEmit>[0];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"replay-privacy.d.ts","sourceRoot":"","sources":["../src/replay-privacy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"replay-privacy.d.ts","sourceRoot":"","sources":["../src/replay-privacy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEtD,KAAK,cAAc,GAAG,CAAC,cAAc,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;AACzD,KAAK,mBAAmB,GAAG,WAAW,CAAC,UAAU,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACtE,KAAK,UAAU,GAAG,WAAW,CAAC,mBAAmB,CAAC,MAAM,CAAC,CAAC,CAAC;AAC3D,KAAK,WAAW,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;AAE7C,MAAM,MAAM,0BAA0B,GAAG,mBAAmB,GAAG;IAC7D,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,eAAO,MAAM,sBAAsB,iBAAiB,CAAC;AAkBrD,wBAAgB,wBAAwB,CAAC,KAAK,EAAE;IAC9C,IAAI,EAAE,UAAU,CAAC;IACjB,OAAO,CAAC,EAAE,mBAAmB,CAAC;CAC/B,GAAG,0BAA0B,CAiB7B;AAED,wBAAgB,uBAAuB,CAAC,CAAC,SAAS,WAAW,EAC3D,KAAK,EAAE,CAAC,EACR,OAAO,CAAC,EAAE,mBAAmB,GAC5B,CAAC,CAUH"}
|
package/dist/replay-privacy.js
CHANGED
|
@@ -7,9 +7,16 @@ const MASKED_TEXT_KEYS = new Set([
|
|
|
7
7
|
"title",
|
|
8
8
|
"value",
|
|
9
9
|
]);
|
|
10
|
+
// Force rrweb to emit a fresh Meta+FullSnapshot every minute. Without this,
|
|
11
|
+
// only the first segment of a session contains a snapshot, so any later
|
|
12
|
+
// segment loss (or seeking to a non-zero anchor) leaves the player with
|
|
13
|
+
// nothing to render. 60s matches Sentry's buffer-mode checkout cadence and
|
|
14
|
+
// keeps each segment independently seekable.
|
|
15
|
+
const CHECKOUT_EVERY_NMS = 60_000;
|
|
10
16
|
export function buildReplayRecordOptions(input) {
|
|
11
17
|
const options = {
|
|
12
18
|
emit: input.emit,
|
|
19
|
+
checkoutEveryNms: CHECKOUT_EVERY_NMS,
|
|
13
20
|
};
|
|
14
21
|
if (input.privacy?.blockSelector) {
|
|
15
22
|
options.blockSelector = input.privacy.blockSelector;
|
|
@@ -25,36 +32,59 @@ export function applyReplayEventPrivacy(event, privacy) {
|
|
|
25
32
|
if (!privacy?.maskAllText) {
|
|
26
33
|
return event;
|
|
27
34
|
}
|
|
28
|
-
|
|
35
|
+
redactReplayValue(event, {
|
|
29
36
|
currentTagName: null,
|
|
30
37
|
key: "",
|
|
31
38
|
});
|
|
39
|
+
return event;
|
|
32
40
|
}
|
|
33
41
|
function redactReplayValue(value, context) {
|
|
34
|
-
if (typeof value === "string") {
|
|
35
|
-
return shouldMaskString(context) ? maskReplayText(value) : value;
|
|
36
|
-
}
|
|
37
42
|
if (Array.isArray(value)) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
value.forEach((entry, index) => {
|
|
44
|
+
if (isString(entry) && shouldMaskString(context)) {
|
|
45
|
+
value.splice(index, 1, maskReplayText(String(entry)));
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
redactReplayValue(entry, context);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
return;
|
|
42
52
|
}
|
|
53
|
+
if (!isReplayRecord(value))
|
|
54
|
+
return;
|
|
43
55
|
const nextTagName = readTagName(value) ?? context.currentTagName;
|
|
44
|
-
const nextValue = {};
|
|
45
56
|
for (const [entryKey, entryValue] of Object.entries(value)) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
57
|
+
const nextContext = { currentTagName: nextTagName, key: entryKey };
|
|
58
|
+
if (isString(entryValue) && shouldMaskString(nextContext)) {
|
|
59
|
+
Object.defineProperty(value, entryKey, {
|
|
60
|
+
configurable: true,
|
|
61
|
+
enumerable: true,
|
|
62
|
+
value: maskReplayText(String(entryValue)),
|
|
63
|
+
writable: true,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
redactReplayValue(entryValue, nextContext);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function isReplayRecord(value) {
|
|
72
|
+
if (value === null)
|
|
73
|
+
return false;
|
|
74
|
+
try {
|
|
75
|
+
const prototype = Object.getPrototypeOf(Object(value));
|
|
76
|
+
return prototype === null || Object.getPrototypeOf(prototype) === null;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return false;
|
|
50
80
|
}
|
|
51
|
-
return nextValue;
|
|
52
81
|
}
|
|
53
|
-
function
|
|
54
|
-
return
|
|
82
|
+
function isString(value) {
|
|
83
|
+
return Object.prototype.toString.call(value) === "[object String]";
|
|
55
84
|
}
|
|
56
85
|
function readTagName(value) {
|
|
57
|
-
|
|
86
|
+
const tagName = Object.getOwnPropertyDescriptor(Object(value), "tagName")?.value;
|
|
87
|
+
return isString(tagName) ? String(tagName).toLowerCase() : null;
|
|
58
88
|
}
|
|
59
89
|
function maskReplayText(value) {
|
|
60
90
|
return value.replace(/[^\s]/g, "*");
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
interface ReplayUploadClient {
|
|
2
|
+
url: string;
|
|
3
|
+
headerKey: string;
|
|
4
|
+
debug: boolean;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* In-memory retry queue for replay payloads.
|
|
8
|
+
*
|
|
9
|
+
* Semantics:
|
|
10
|
+
* - Enqueue a payload; `flush()` tries to send the oldest pending job.
|
|
11
|
+
* - Successful 2xx -> drop the job.
|
|
12
|
+
* - 4xx (except 429) -> drop the job (non-retryable client error).
|
|
13
|
+
* - 5xx, 429, or network error -> increment attempts; reschedule with
|
|
14
|
+
* exponential backoff (1s, 2s, 4s). After MAX_ATTEMPTS, the job is dropped.
|
|
15
|
+
* - Concurrency: a `sending` flag prevents two concurrent `flush` runs from
|
|
16
|
+
* double-sending the same head-of-queue job.
|
|
17
|
+
* - Queue length is capped at MAX_PENDING_PAYLOADS to bound memory.
|
|
18
|
+
*/
|
|
19
|
+
export declare class ReplayUploader {
|
|
20
|
+
private readonly client;
|
|
21
|
+
private readonly queue;
|
|
22
|
+
private sending;
|
|
23
|
+
private activeController;
|
|
24
|
+
private retryTimer;
|
|
25
|
+
private disposed;
|
|
26
|
+
constructor(client: ReplayUploadClient);
|
|
27
|
+
enqueue(body: string): void;
|
|
28
|
+
/**
|
|
29
|
+
* Returns every payload body and clears the queue. The active fetch is
|
|
30
|
+
* aborted first so pagehide can transfer ownership of the head payload to
|
|
31
|
+
* sendBeacon instead of losing the initial replay snapshot on navigation.
|
|
32
|
+
*/
|
|
33
|
+
drain(): string[];
|
|
34
|
+
private oldestDroppableIndex;
|
|
35
|
+
dispose(): void;
|
|
36
|
+
flush(): Promise<void>;
|
|
37
|
+
private scheduleRetry;
|
|
38
|
+
private send;
|
|
39
|
+
}
|
|
40
|
+
export {};
|
|
41
|
+
//# sourceMappingURL=replay-retry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay-retry.d.ts","sourceRoot":"","sources":["../src/replay-retry.ts"],"names":[],"mappings":"AAmBA,UAAU,kBAAkB;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,OAAO,CAAC;CAChB;AAmBD;;;;;;;;;;;;GAYG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqB;IAC5C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAyB;IAC/C,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,gBAAgB,CAAgC;IACxD,OAAO,CAAC,UAAU,CAA8C;IAChE,OAAO,CAAC,QAAQ,CAAS;gBAEb,MAAM,EAAE,kBAAkB;IAItC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAY3B;;;;OAIG;IACH,KAAK,IAAI,MAAM,EAAE;IAYjB,OAAO,CAAC,oBAAoB;IAI5B,OAAO,IAAI,IAAI;IAWT,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAkD5B,OAAO,CAAC,aAAa;YAQP,IAAI;CAwDnB"}
|