@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/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Squasher
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Squasher
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { BrowserClient } from "../../client.js";
|
|
2
|
+
import type { ReplayRecorderConfig } from "../../replay.js";
|
|
3
|
+
import type { BrowserClientRuntime, BrowserTransport } from "../../runtime/client.js";
|
|
4
|
+
import type { BrowserConfig, BrowserErrorEvent } from "../../types.js";
|
|
5
|
+
import type { VitalsCollectorConfig } from "../../vitals.js";
|
|
6
|
+
export declare const installAutocaptureMock: import("vitest").Mock<(onBreadcrumb: import("../../console-capture.js").BreadcrumbCallback, options: import("../../autocapture.js").AutocaptureOptions) => () => void>;
|
|
7
|
+
export declare const installGlobalErrorHandlersMock: import("vitest").Mock<(onError: (event: BrowserErrorEvent) => void) => () => void>;
|
|
8
|
+
export declare const replayCtorMock: import("vitest").Mock<(config: ReplayRecorderConfig) => void>;
|
|
9
|
+
export declare const replayDisposeMock: import("vitest").Mock<() => void>;
|
|
10
|
+
export declare const startVitalsCollectionMock: import("vitest").Mock<(transport: BrowserTransport, config: VitalsCollectorConfig) => void>;
|
|
11
|
+
export declare const vitalsSuppressMock: import("vitest").Mock<() => void>;
|
|
12
|
+
export declare const browserClientRuntime: BrowserClientRuntime;
|
|
13
|
+
export declare function buildConfig(overrides?: Partial<BrowserConfig>): BrowserConfig;
|
|
14
|
+
export declare function createClient(overrides?: Partial<BrowserConfig>): BrowserClient;
|
|
15
|
+
export declare function readBeaconEvent(callIndex: number): Promise<BrowserErrorEvent>;
|
|
16
|
+
export declare function setupClientTest(): void;
|
|
17
|
+
export declare function teardownClientTest(): void;
|
|
18
|
+
//# sourceMappingURL=harness.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"harness.d.ts","sourceRoot":"","sources":["../../../src/__tests__/client/harness.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,KAAK,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAGtF,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACvE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAE7D,eAAO,MAAM,sBAAsB,wKAAsD,CAAC;AAC1F,eAAO,MAAM,8BAA8B,oFACkB,CAAC;AAC9D,eAAO,MAAM,cAAc,iCAAkB,oBAAoB,KAAK,IAAI,CAAG,CAAC;AAC9E,eAAO,MAAM,iBAAiB,8BAAe,IAAI,CAAG,CAAC;AACrD,eAAO,MAAM,yBAAyB,oCAClB,gBAAgB,UAAU,qBAAqB,KAAK,IAAI,CAAG,CAAC;AAChF,eAAO,MAAM,kBAAkB,8BAAe,IAAI,CAAG,CAAC;AAEtD,eAAO,MAAM,oBAAoB,EAAE,oBAYlC,CAAC;AAuBF,wBAAgB,WAAW,CAAC,SAAS,GAAE,OAAO,CAAC,aAAa,CAAM,GAAG,aAAa,CASjF;AAED,wBAAgB,YAAY,CAAC,SAAS,GAAE,OAAO,CAAC,aAAa,CAAM,GAAG,aAAa,CAElF;AAED,wBAAsB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAQnF;AAED,wBAAgB,eAAe,IAAI,IAAI,CAwCtC;AAED,wBAAgB,kBAAkB,IAAI,IAAI,CAGzC"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { vi } from "vitest";
|
|
2
|
+
import { BrowserClient } from "../../client.js";
|
|
3
|
+
import { resetSessionId } from "../../session.js";
|
|
4
|
+
import { Transport } from "../../transport.js";
|
|
5
|
+
export const installAutocaptureMock = vi.fn();
|
|
6
|
+
export const installGlobalErrorHandlersMock = vi.fn();
|
|
7
|
+
export const replayCtorMock = vi.fn();
|
|
8
|
+
export const replayDisposeMock = vi.fn();
|
|
9
|
+
export const startVitalsCollectionMock = vi.fn();
|
|
10
|
+
export const vitalsSuppressMock = vi.fn();
|
|
11
|
+
export const browserClientRuntime = {
|
|
12
|
+
createReplayRecorder: (config) => {
|
|
13
|
+
replayCtorMock(config);
|
|
14
|
+
return { dispose: replayDisposeMock };
|
|
15
|
+
},
|
|
16
|
+
createTransport: (config) => new Transport(config),
|
|
17
|
+
installAutocapture: (onBreadcrumb, options) => installAutocaptureMock(onBreadcrumb, options),
|
|
18
|
+
installGlobalErrorHandlers: (onError) => installGlobalErrorHandlersMock(onError),
|
|
19
|
+
startVitalsCollection: (transport, config) => {
|
|
20
|
+
startVitalsCollectionMock(transport, config);
|
|
21
|
+
return { suppress: vitalsSuppressMock };
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
function createStorage() {
|
|
25
|
+
const storage = {};
|
|
26
|
+
return {
|
|
27
|
+
clear: () => {
|
|
28
|
+
for (const key of Object.keys(storage))
|
|
29
|
+
delete storage[key];
|
|
30
|
+
},
|
|
31
|
+
getItem: (key) => storage[key] ?? null,
|
|
32
|
+
key: (index) => Object.keys(storage)[index] ?? null,
|
|
33
|
+
get length() {
|
|
34
|
+
return Object.keys(storage).length;
|
|
35
|
+
},
|
|
36
|
+
removeItem: (key) => {
|
|
37
|
+
delete storage[key];
|
|
38
|
+
},
|
|
39
|
+
setItem: (key, value) => {
|
|
40
|
+
storage[key] = value;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function buildConfig(overrides = {}) {
|
|
45
|
+
return {
|
|
46
|
+
apiKey: "sq_pk_test",
|
|
47
|
+
projectId: "project-123",
|
|
48
|
+
enableVitals: false,
|
|
49
|
+
enableErrorCapture: false,
|
|
50
|
+
enableAutoBreadcrumbs: false,
|
|
51
|
+
...overrides,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export function createClient(overrides = {}) {
|
|
55
|
+
return new BrowserClient(buildConfig(overrides), browserClientRuntime);
|
|
56
|
+
}
|
|
57
|
+
export async function readBeaconEvent(callIndex) {
|
|
58
|
+
const beaconCall = vi.mocked(navigator.sendBeacon).mock.calls[callIndex];
|
|
59
|
+
const payloadBlob = beaconCall?.[1];
|
|
60
|
+
if (!(payloadBlob instanceof Blob)) {
|
|
61
|
+
throw new Error("expected sendBeacon payload to be a Blob");
|
|
62
|
+
}
|
|
63
|
+
return JSON.parse(await payloadBlob.text());
|
|
64
|
+
}
|
|
65
|
+
export function setupClientTest() {
|
|
66
|
+
resetSessionId();
|
|
67
|
+
vi.useFakeTimers();
|
|
68
|
+
vi.restoreAllMocks();
|
|
69
|
+
const consoleLog = vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
70
|
+
const consoleWarn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
71
|
+
installAutocaptureMock.mockReset();
|
|
72
|
+
installGlobalErrorHandlersMock.mockReset();
|
|
73
|
+
replayCtorMock.mockReset();
|
|
74
|
+
replayDisposeMock.mockReset();
|
|
75
|
+
startVitalsCollectionMock.mockReset();
|
|
76
|
+
vitalsSuppressMock.mockReset();
|
|
77
|
+
vi.stubGlobal("console", { ...console, log: consoleLog, warn: consoleWarn });
|
|
78
|
+
vi.stubGlobal("crypto", {
|
|
79
|
+
randomUUID: vi.fn().mockReturnValue("aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"),
|
|
80
|
+
});
|
|
81
|
+
vi.stubGlobal("sessionStorage", createStorage());
|
|
82
|
+
vi.stubGlobal("navigator", {
|
|
83
|
+
sendBeacon: vi.fn().mockReturnValue(true),
|
|
84
|
+
});
|
|
85
|
+
vi.stubGlobal("fetch", vi.fn());
|
|
86
|
+
vi.stubGlobal("document", {
|
|
87
|
+
addEventListener: vi.fn(),
|
|
88
|
+
referrer: "https://referrer.example/from",
|
|
89
|
+
removeEventListener: vi.fn(),
|
|
90
|
+
title: "Settings",
|
|
91
|
+
visibilityState: "visible",
|
|
92
|
+
});
|
|
93
|
+
vi.stubGlobal("window", {
|
|
94
|
+
addEventListener: vi.fn(),
|
|
95
|
+
removeEventListener: vi.fn(),
|
|
96
|
+
});
|
|
97
|
+
vi.stubGlobal("location", {
|
|
98
|
+
href: "https://example.com/settings?tab=profile",
|
|
99
|
+
pathname: "/settings",
|
|
100
|
+
search: "?tab=profile",
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
export function teardownClientTest() {
|
|
104
|
+
vi.useRealTimers();
|
|
105
|
+
vi.unstubAllGlobals();
|
|
106
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ReplayNetworkCaptureOptions } from "../../replay-network.js";
|
|
2
|
+
import type { ReplayPrivacyRecordOptions } from "../../replay-privacy.js";
|
|
3
|
+
import { ReplayRecorder, type ReplayRecorderConfig } from "../../replay.js";
|
|
4
|
+
import type { MockReplayEvent } from "./test-helpers.js";
|
|
5
|
+
interface ReplayPayload {
|
|
6
|
+
session_id: string;
|
|
7
|
+
events: MockReplayEvent[];
|
|
8
|
+
page_url: string;
|
|
9
|
+
started_at: string;
|
|
10
|
+
visitor_id?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare const replayUrl = "https://ingest.squasher.ai/v1/projects/project-123/replays";
|
|
13
|
+
export declare const sessionId = "11111111-2222-4333-8444-555555555555";
|
|
14
|
+
export declare const installReplayNetworkCaptureMock: import("vitest").Mock<(options: ReplayNetworkCaptureOptions) => () => void>;
|
|
15
|
+
export declare const rrwebRecordMock: import("vitest").Mock<(options: ReplayPrivacyRecordOptions) => (() => void) | undefined>;
|
|
16
|
+
export declare const rrwebStopMock: import("vitest").Mock<() => void>;
|
|
17
|
+
export declare const rrwebAddCustomEventMock: import("vitest").Mock<(tag: string, payload: import("../../replay-network.js").ReplayNetworkEventPayload) => void>;
|
|
18
|
+
export declare function setupReplayCoverageTest(): void;
|
|
19
|
+
export declare function teardownReplayCoverageTest(): void;
|
|
20
|
+
export declare function createRecorder(overrides?: Partial<ReplayRecorderConfig>): ReplayRecorder;
|
|
21
|
+
export declare function readyRecorder(overrides?: Partial<ReplayRecorderConfig>): Promise<ReplayRecorder>;
|
|
22
|
+
export declare function emitFromRecorder(event: MockReplayEvent, overrides?: Partial<ReplayRecorderConfig>): Promise<ReplayRecorder>;
|
|
23
|
+
export declare function emitRecordedEvent(event: MockReplayEvent): void;
|
|
24
|
+
export declare function flushFinal(eventName: "beforeunload" | "pagehide"): Promise<void>;
|
|
25
|
+
export declare function fireLifecycleEvent(eventName: "beforeunload" | "pagehide"): void;
|
|
26
|
+
export declare function getPayload(callIndex?: number): ReplayPayload;
|
|
27
|
+
export declare function drainMicrotasks(turns?: number): Promise<void>;
|
|
28
|
+
export {};
|
|
29
|
+
//# sourceMappingURL=coverage-harness.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coverage-harness.d.ts","sourceRoot":"","sources":["../../../src/__tests__/replay/coverage-harness.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,yBAAyB,CAAC;AAC3E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,yBAAyB,CAAC;AAC1E,OAAO,EAAE,cAAc,EAAE,KAAK,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAG5E,OAAO,KAAK,EAAE,eAAe,EAAc,MAAM,mBAAmB,CAAC;AAErE,UAAU,aAAa;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,eAAe,EAAE,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,eAAO,MAAM,SAAS,+DAA+D,CAAC;AACtF,eAAO,MAAM,SAAS,yCAAyC,CAAC;AAChE,eAAO,MAAM,+BAA+B,kCAC1B,2BAA2B,KAAK,MAAM,IAAI,CAAG,CAAC;AAChE,eAAO,MAAM,eAAe,kCACV,0BAA0B,KAAK,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAG,CAAC;AAC7E,eAAO,MAAM,aAAa,8BAAe,IAAI,CAAG,CAAC;AACjD,eAAO,MAAM,uBAAuB,oHACkC,CAAC;AAkCvE,wBAAgB,uBAAuB,IAAI,IAAI,CAkC9C;AAED,wBAAgB,0BAA0B,IAAI,IAAI,CAKjD;AAED,wBAAgB,cAAc,CAAC,SAAS,GAAE,OAAO,CAAC,oBAAoB,CAAM,GAAG,cAAc,CAa5F;AAED,wBAAsB,aAAa,CACjC,SAAS,GAAE,OAAO,CAAC,oBAAoB,CAAM,GAC5C,OAAO,CAAC,cAAc,CAAC,CAIzB;AAED,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,eAAe,EACtB,SAAS,GAAE,OAAO,CAAC,oBAAoB,CAAM,GAC5C,OAAO,CAAC,cAAc,CAAC,CAIzB;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAE9D;AAED,wBAAsB,UAAU,CAAC,SAAS,EAAE,cAAc,GAAG,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAGtF;AAED,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,cAAc,GAAG,UAAU,GAAG,IAAI,CAE/E;AAED,wBAAgB,UAAU,CAAC,SAAS,SAAI,GAAG,aAAa,CAOvD;AAED,wBAAsB,eAAe,CAAC,KAAK,SAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAI/D"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { expect, vi } from "vitest";
|
|
2
|
+
import { ReplayRecorder } from "../../replay.js";
|
|
3
|
+
import { resetSessionId } from "../../session.js";
|
|
4
|
+
export const replayUrl = "https://ingest.squasher.ai/v1/projects/project-123/replays";
|
|
5
|
+
export const sessionId = "11111111-2222-4333-8444-555555555555";
|
|
6
|
+
export const installReplayNetworkCaptureMock = vi.fn();
|
|
7
|
+
export const rrwebRecordMock = vi.fn();
|
|
8
|
+
export const rrwebStopMock = vi.fn();
|
|
9
|
+
export const rrwebAddCustomEventMock = vi.fn();
|
|
10
|
+
const replayRuntime = {
|
|
11
|
+
installNetworkCapture: (options) => installReplayNetworkCaptureMock(options),
|
|
12
|
+
loadRecordingAdapter: async () => ({
|
|
13
|
+
emitCustomEvent: rrwebAddCustomEventMock,
|
|
14
|
+
start: rrwebRecordMock,
|
|
15
|
+
}),
|
|
16
|
+
};
|
|
17
|
+
let activeRecorder;
|
|
18
|
+
let emitReplayEvent;
|
|
19
|
+
let listeners = {};
|
|
20
|
+
function createStorage() {
|
|
21
|
+
const storage = {};
|
|
22
|
+
return {
|
|
23
|
+
clear: () => {
|
|
24
|
+
for (const key of Object.keys(storage))
|
|
25
|
+
delete storage[key];
|
|
26
|
+
},
|
|
27
|
+
getItem: (key) => storage[key] ?? null,
|
|
28
|
+
key: (index) => Object.keys(storage)[index] ?? null,
|
|
29
|
+
get length() {
|
|
30
|
+
return Object.keys(storage).length;
|
|
31
|
+
},
|
|
32
|
+
removeItem: (key) => {
|
|
33
|
+
delete storage[key];
|
|
34
|
+
},
|
|
35
|
+
setItem: (key, value) => {
|
|
36
|
+
storage[key] = value;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export function setupReplayCoverageTest() {
|
|
41
|
+
activeRecorder = undefined;
|
|
42
|
+
emitReplayEvent = undefined;
|
|
43
|
+
listeners = {};
|
|
44
|
+
resetSessionId();
|
|
45
|
+
vi.useFakeTimers();
|
|
46
|
+
vi.restoreAllMocks();
|
|
47
|
+
vi.stubGlobal("crypto", { randomUUID: vi.fn().mockReturnValue(sessionId) });
|
|
48
|
+
vi.stubGlobal("sessionStorage", createStorage());
|
|
49
|
+
vi.stubGlobal("location", { href: "https://example.com/dashboard" });
|
|
50
|
+
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response()));
|
|
51
|
+
vi.stubGlobal("navigator", {
|
|
52
|
+
sendBeacon: vi.fn().mockReturnValue(true),
|
|
53
|
+
});
|
|
54
|
+
vi.stubGlobal("document", {});
|
|
55
|
+
vi.stubGlobal("window", {
|
|
56
|
+
addEventListener: vi.fn((event, listener) => {
|
|
57
|
+
listeners[event] = [...(listeners[event] ?? []), listener];
|
|
58
|
+
}),
|
|
59
|
+
removeEventListener: vi.fn((event, listener) => {
|
|
60
|
+
listeners[event] = (listeners[event] ?? []).filter((value) => value !== listener);
|
|
61
|
+
}),
|
|
62
|
+
});
|
|
63
|
+
installReplayNetworkCaptureMock.mockReset();
|
|
64
|
+
rrwebAddCustomEventMock.mockReset();
|
|
65
|
+
rrwebStopMock.mockReset();
|
|
66
|
+
rrwebRecordMock.mockReset();
|
|
67
|
+
installReplayNetworkCaptureMock.mockImplementation(() => () => undefined);
|
|
68
|
+
rrwebRecordMock.mockImplementation((options) => {
|
|
69
|
+
emitReplayEvent = options.emit;
|
|
70
|
+
return rrwebStopMock;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
export function teardownReplayCoverageTest() {
|
|
74
|
+
activeRecorder?.dispose();
|
|
75
|
+
activeRecorder = undefined;
|
|
76
|
+
vi.useRealTimers();
|
|
77
|
+
vi.unstubAllGlobals();
|
|
78
|
+
}
|
|
79
|
+
export function createRecorder(overrides = {}) {
|
|
80
|
+
activeRecorder = new ReplayRecorder({
|
|
81
|
+
endpoint: "https://ingest.squasher.ai",
|
|
82
|
+
projectId: "project-123",
|
|
83
|
+
apiKey: "sq_pk_test",
|
|
84
|
+
debug: false,
|
|
85
|
+
sampleRate: 1,
|
|
86
|
+
...overrides,
|
|
87
|
+
}, replayRuntime);
|
|
88
|
+
return activeRecorder;
|
|
89
|
+
}
|
|
90
|
+
export async function readyRecorder(overrides = {}) {
|
|
91
|
+
const recorder = createRecorder(overrides);
|
|
92
|
+
await recorder.ready;
|
|
93
|
+
return recorder;
|
|
94
|
+
}
|
|
95
|
+
export async function emitFromRecorder(event, overrides = {}) {
|
|
96
|
+
const recorder = await readyRecorder(overrides);
|
|
97
|
+
emitReplayEvent?.(event);
|
|
98
|
+
return recorder;
|
|
99
|
+
}
|
|
100
|
+
export function emitRecordedEvent(event) {
|
|
101
|
+
emitReplayEvent?.(event);
|
|
102
|
+
}
|
|
103
|
+
export async function flushFinal(eventName) {
|
|
104
|
+
listeners[eventName]?.[0]?.();
|
|
105
|
+
await Promise.resolve();
|
|
106
|
+
}
|
|
107
|
+
export function fireLifecycleEvent(eventName) {
|
|
108
|
+
listeners[eventName]?.[0]?.();
|
|
109
|
+
}
|
|
110
|
+
export function getPayload(callIndex = 0) {
|
|
111
|
+
const body = vi.mocked(fetch).mock.calls[callIndex]?.[1]?.body;
|
|
112
|
+
expect(Object.prototype.toString.call(body)).toBe("[object String]");
|
|
113
|
+
if (Object.prototype.toString.call(body) !== "[object String]") {
|
|
114
|
+
throw new Error("expected replay request body to be a string");
|
|
115
|
+
}
|
|
116
|
+
return JSON.parse(String(body));
|
|
117
|
+
}
|
|
118
|
+
export async function drainMicrotasks(turns = 10) {
|
|
119
|
+
if (turns <= 0)
|
|
120
|
+
return;
|
|
121
|
+
await Promise.resolve();
|
|
122
|
+
await drainMicrotasks(turns - 1);
|
|
123
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ReplayPrivacyRecordOptions } from "../../replay-privacy.js";
|
|
2
|
+
export type MockReplayValue = boolean | null | number | string | MockReplayObject | MockReplayValue[];
|
|
3
|
+
export interface MockReplayObject {
|
|
4
|
+
[key: string]: MockReplayValue;
|
|
5
|
+
}
|
|
6
|
+
export type MockReplayEvent = {
|
|
7
|
+
type: number;
|
|
8
|
+
timestamp: number;
|
|
9
|
+
data: MockReplayObject;
|
|
10
|
+
};
|
|
11
|
+
export type ReplayEmit = NonNullable<ReplayPrivacyRecordOptions["emit"]>;
|
|
12
|
+
export declare function createReplayEvent(type: number, timestamp: number): MockReplayEvent;
|
|
13
|
+
//# sourceMappingURL=test-helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-helpers.d.ts","sourceRoot":"","sources":["../../../src/__tests__/replay/test-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,yBAAyB,CAAC;AAE1E,MAAM,MAAM,eAAe,GACvB,OAAO,GACP,IAAI,GACJ,MAAM,GACN,MAAM,GACN,gBAAgB,GAChB,eAAe,EAAE,CAAC;AAEtB,MAAM,WAAW,gBAAgB;IAC/B,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,CAAC;CAChC;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,gBAAgB,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,WAAW,CAAC,0BAA0B,CAAC,MAAM,CAAC,CAAC,CAAC;AAEzE,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,eAAe,CAQlF"}
|
package/dist/autocapture.d.ts
CHANGED
|
@@ -1,21 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Auto-capture breadcrumbs
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* Categories:
|
|
9
|
-
* - "navigation" — route changes (pushState, replaceState, popstate)
|
|
10
|
-
* - "ui.click" — user clicks on elements
|
|
11
|
-
* - "fetch" — failed fetch requests (4xx/5xx)
|
|
12
|
-
* - "console" — console.error / console.warn calls
|
|
2
|
+
* Auto-capture breadcrumbs. Categories emitted:
|
|
3
|
+
* - "navigation" — pushState/replaceState/popstate
|
|
4
|
+
* - "ui.click" — clicks on elements
|
|
5
|
+
* - "fetch" — 4xx/5xx fetch responses
|
|
6
|
+
* - "console" — console.error / console.warn calls
|
|
13
7
|
*/
|
|
14
|
-
import type
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
8
|
+
import { type BreadcrumbCallback } from "./console-capture.js";
|
|
9
|
+
import type { BrowserErrorEvent } from "./types.js";
|
|
10
|
+
export type { BreadcrumbCallback } from "./console-capture.js";
|
|
11
|
+
export interface AutocaptureOptions {
|
|
12
|
+
captureBreadcrumbs?: boolean;
|
|
13
|
+
onConsoleError?: (event: BrowserErrorEvent) => void;
|
|
14
|
+
}
|
|
15
|
+
/** Installs all hooks. Returned function removes them and restores originals. */
|
|
16
|
+
export declare function installAutocapture(onBreadcrumb: BreadcrumbCallback, options?: AutocaptureOptions): () => void;
|
|
21
17
|
//# sourceMappingURL=autocapture.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"autocapture.d.ts","sourceRoot":"","sources":["../src/autocapture.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"autocapture.d.ts","sourceRoot":"","sources":["../src/autocapture.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,EAAkB,KAAK,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC/E,OAAO,KAAK,EAAE,iBAAiB,EAAc,MAAM,YAAY,CAAC;AAEhE,YAAY,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE/D,MAAM,WAAW,kBAAkB;IACjC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;CACrD;AAED,iFAAiF;AACjF,wBAAgB,kBAAkB,CAChC,YAAY,EAAE,kBAAkB,EAChC,OAAO,GAAE,kBAAuB,GAC/B,MAAM,IAAI,CAwBZ"}
|
package/dist/autocapture.js
CHANGED
|
@@ -1,26 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Auto-capture breadcrumbs
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* Categories:
|
|
9
|
-
* - "navigation" — route changes (pushState, replaceState, popstate)
|
|
10
|
-
* - "ui.click" — user clicks on elements
|
|
11
|
-
* - "fetch" — failed fetch requests (4xx/5xx)
|
|
12
|
-
* - "console" — console.error / console.warn calls
|
|
2
|
+
* Auto-capture breadcrumbs. Categories emitted:
|
|
3
|
+
* - "navigation" — pushState/replaceState/popstate
|
|
4
|
+
* - "ui.click" — clicks on elements
|
|
5
|
+
* - "fetch" — 4xx/5xx fetch responses
|
|
6
|
+
* - "console" — console.error / console.warn calls
|
|
13
7
|
*/
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
*/
|
|
18
|
-
export function installAutocapture(onBreadcrumb) {
|
|
8
|
+
import { attemptAsync } from "@squasher-ai/result-utils";
|
|
9
|
+
import { readRuntimeProperty, readRuntimeString } from "@squasher-ai/sdk-core";
|
|
10
|
+
import { captureConsole } from "./console-capture.js";
|
|
11
|
+
/** Installs all hooks. Returned function removes them and restores originals. */
|
|
12
|
+
export function installAutocapture(onBreadcrumb, options = {}) {
|
|
19
13
|
const cleanups = [];
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
const captureBreadcrumbs = options.captureBreadcrumbs ?? true;
|
|
15
|
+
if (captureBreadcrumbs) {
|
|
16
|
+
cleanups.push(captureNavigation(onBreadcrumb));
|
|
17
|
+
cleanups.push(captureClicks(onBreadcrumb));
|
|
18
|
+
cleanups.push(captureFetch(onBreadcrumb));
|
|
19
|
+
}
|
|
20
|
+
if (captureBreadcrumbs || options.onConsoleError) {
|
|
21
|
+
cleanups.push(captureConsole(onBreadcrumb, {
|
|
22
|
+
captureBreadcrumbs,
|
|
23
|
+
onConsoleError: options.onConsoleError,
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
24
26
|
return () => {
|
|
25
27
|
for (const cleanup of cleanups) {
|
|
26
28
|
cleanup();
|
|
@@ -43,13 +45,13 @@ function captureNavigation(onBreadcrumb) {
|
|
|
43
45
|
};
|
|
44
46
|
// Patch history.pushState
|
|
45
47
|
const origPushState = history.pushState.bind(history);
|
|
46
|
-
history.pushState =
|
|
48
|
+
history.pushState = (...args) => {
|
|
47
49
|
origPushState(...args);
|
|
48
50
|
emitNavCrumb();
|
|
49
51
|
};
|
|
50
52
|
// Patch history.replaceState
|
|
51
53
|
const origReplaceState = history.replaceState.bind(history);
|
|
52
|
-
history.replaceState =
|
|
54
|
+
history.replaceState = (...args) => {
|
|
53
55
|
origReplaceState(...args);
|
|
54
56
|
emitNavCrumb();
|
|
55
57
|
};
|
|
@@ -65,20 +67,21 @@ function captureNavigation(onBreadcrumb) {
|
|
|
65
67
|
// ─── Clicks ─────────────────────────────────────────────────────────────
|
|
66
68
|
function captureClicks(onBreadcrumb) {
|
|
67
69
|
const handler = (event) => {
|
|
68
|
-
const target = event.target;
|
|
70
|
+
const target = readClickTarget(event.target);
|
|
69
71
|
if (!target)
|
|
70
72
|
return;
|
|
71
73
|
const descriptor = describeElement(target);
|
|
72
74
|
if (!descriptor)
|
|
73
75
|
return;
|
|
76
|
+
const data = { tag: target.tagName.toLowerCase() };
|
|
77
|
+
if (target.id)
|
|
78
|
+
data.id = target.id;
|
|
79
|
+
if (target.className)
|
|
80
|
+
data.className = target.className;
|
|
74
81
|
onBreadcrumb({
|
|
75
82
|
category: "ui.click",
|
|
76
83
|
message: descriptor,
|
|
77
|
-
data
|
|
78
|
-
tag: target.tagName?.toLowerCase(),
|
|
79
|
-
id: target.id || undefined,
|
|
80
|
-
className: target.className || undefined,
|
|
81
|
-
},
|
|
84
|
+
data,
|
|
82
85
|
});
|
|
83
86
|
};
|
|
84
87
|
// Use capture phase to catch clicks even if stopPropagation is called
|
|
@@ -87,27 +90,35 @@ function captureClicks(onBreadcrumb) {
|
|
|
87
90
|
document.removeEventListener("click", handler, true);
|
|
88
91
|
};
|
|
89
92
|
}
|
|
93
|
+
function readClickTarget(target) {
|
|
94
|
+
const tagName = readRuntimeString(readRuntimeProperty(target, "tagName"));
|
|
95
|
+
if (!tagName)
|
|
96
|
+
return null;
|
|
97
|
+
return {
|
|
98
|
+
className: readRuntimeString(readRuntimeProperty(target, "className")) ?? "",
|
|
99
|
+
id: readRuntimeString(readRuntimeProperty(target, "id")) ?? "",
|
|
100
|
+
tagName,
|
|
101
|
+
textContent: readRuntimeString(readRuntimeProperty(target, "textContent")) ?? "",
|
|
102
|
+
};
|
|
103
|
+
}
|
|
90
104
|
/**
|
|
91
105
|
* Build a human-readable descriptor for a DOM element.
|
|
92
106
|
* e.g. "button#submit-btn.primary" or "a.nav-link"
|
|
93
107
|
*/
|
|
94
108
|
function describeElement(el) {
|
|
95
|
-
const tag = el.tagName
|
|
96
|
-
if (!tag)
|
|
97
|
-
return null;
|
|
109
|
+
const tag = el.tagName.toLowerCase();
|
|
98
110
|
let desc = tag;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
desc += `#${htmlEl.id}`;
|
|
111
|
+
if (el.id) {
|
|
112
|
+
desc += `#${el.id}`;
|
|
102
113
|
}
|
|
103
|
-
if (
|
|
104
|
-
const classes =
|
|
114
|
+
if (el.className) {
|
|
115
|
+
const classes = el.className.trim().split(/\s+/).slice(0, 2).join(".");
|
|
105
116
|
if (classes) {
|
|
106
117
|
desc += `.${classes}`;
|
|
107
118
|
}
|
|
108
119
|
}
|
|
109
120
|
// Add text content for buttons/links (truncated)
|
|
110
|
-
const textContent =
|
|
121
|
+
const textContent = el.textContent?.trim();
|
|
111
122
|
if (textContent && (tag === "button" || tag === "a")) {
|
|
112
123
|
const truncated = textContent.length > 30 ? `${textContent.slice(0, 30)}...` : textContent;
|
|
113
124
|
desc += `[${truncated}]`;
|
|
@@ -116,76 +127,47 @@ function describeElement(el) {
|
|
|
116
127
|
}
|
|
117
128
|
// ─── Fetch ──────────────────────────────────────────────────────────────
|
|
118
129
|
function captureFetch(onBreadcrumb) {
|
|
119
|
-
if (
|
|
130
|
+
if (!globalThis.window?.fetch)
|
|
120
131
|
return () => { };
|
|
121
132
|
const origFetch = window.fetch.bind(window);
|
|
122
|
-
window.fetch = async
|
|
133
|
+
window.fetch = async (input, init) => {
|
|
123
134
|
const method = init?.method?.toUpperCase() || "GET";
|
|
124
|
-
const url =
|
|
125
|
-
|
|
126
|
-
:
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
+
const url = input instanceof URL ? input.href : input instanceof Request ? input.url : String(input);
|
|
136
|
+
return attemptAsync({
|
|
137
|
+
try: async () => {
|
|
138
|
+
const response = await origFetch(input, init);
|
|
139
|
+
// Only breadcrumb on error responses (4xx/5xx)
|
|
140
|
+
if (response.status >= 400) {
|
|
141
|
+
onBreadcrumb({
|
|
142
|
+
category: "fetch",
|
|
143
|
+
message: `${method} ${url} [${response.status}]`,
|
|
144
|
+
level: response.status >= 500 ? "error" : "warning",
|
|
145
|
+
data: {
|
|
146
|
+
method,
|
|
147
|
+
url,
|
|
148
|
+
status: response.status,
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return response;
|
|
153
|
+
},
|
|
154
|
+
catch: ({ cause, error }) => {
|
|
155
|
+
// Network error
|
|
135
156
|
onBreadcrumb({
|
|
136
157
|
category: "fetch",
|
|
137
|
-
message: `${method} ${url} [
|
|
138
|
-
level:
|
|
158
|
+
message: `${method} ${url} [network error]`,
|
|
159
|
+
level: "error",
|
|
139
160
|
data: {
|
|
140
161
|
method,
|
|
141
162
|
url,
|
|
142
|
-
|
|
163
|
+
error: error.message,
|
|
143
164
|
},
|
|
144
165
|
});
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
catch (error) {
|
|
149
|
-
// Network error
|
|
150
|
-
onBreadcrumb({
|
|
151
|
-
category: "fetch",
|
|
152
|
-
message: `${method} ${url} [network error]`,
|
|
153
|
-
level: "error",
|
|
154
|
-
data: {
|
|
155
|
-
method,
|
|
156
|
-
url,
|
|
157
|
-
error: error instanceof Error ? error.message : String(error),
|
|
158
|
-
},
|
|
159
|
-
});
|
|
160
|
-
throw error;
|
|
161
|
-
}
|
|
162
|
-
};
|
|
163
|
-
return () => {
|
|
164
|
-
window.fetch = origFetch;
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
// ─── Console ────────────────────────────────────────────────────────────
|
|
168
|
-
function captureConsole(onBreadcrumb) {
|
|
169
|
-
const origError = console.error.bind(console);
|
|
170
|
-
const origWarn = console.warn.bind(console);
|
|
171
|
-
console.error = (...args) => {
|
|
172
|
-
onBreadcrumb({
|
|
173
|
-
category: "console",
|
|
174
|
-
message: args.map(String).join(" ").slice(0, 200),
|
|
175
|
-
level: "error",
|
|
176
|
-
});
|
|
177
|
-
origError(...args);
|
|
178
|
-
};
|
|
179
|
-
console.warn = (...args) => {
|
|
180
|
-
onBreadcrumb({
|
|
181
|
-
category: "console",
|
|
182
|
-
message: args.map(String).join(" ").slice(0, 200),
|
|
183
|
-
level: "warning",
|
|
166
|
+
throw cause;
|
|
167
|
+
},
|
|
184
168
|
});
|
|
185
|
-
origWarn(...args);
|
|
186
169
|
};
|
|
187
170
|
return () => {
|
|
188
|
-
|
|
189
|
-
console.warn = origWarn;
|
|
171
|
+
window.fetch = origFetch;
|
|
190
172
|
};
|
|
191
173
|
}
|