@hyperframes/studio 0.8.177 → 0.8.178
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/dist/assets/{hyperframes-player-BulZP0_1.js → hyperframes-player-Dfw61RJK.js} +1 -1
- package/dist/assets/{index-B1Gypobp.js → index-Bi6yVTq9.js} +118 -117
- package/dist/assets/{index-CGncBUsJ.js → index-eTRj5rY2.js} +1 -1
- package/dist/{chunk-BLXESG6S.js → chunk-563JPIL4.js} +2 -0
- package/dist/index.html +1 -1
- package/dist/index.js +32 -5
- package/dist/player.js +1 -1
- package/package.json +8 -8
- package/src/hooks/useFileTree.test.tsx +34 -0
- package/src/hooks/useFileTree.ts +22 -4
- package/src/main.tsx +7 -24
- package/src/player/hooks/useTimelinePlayer.ts +2 -2
- package/src/player/sceneSwap.test.ts +10 -3
- package/src/player/sceneSwap.ts +3 -0
- package/src/utils/rejectionTelemetry.test.ts +39 -0
- package/src/utils/rejectionTelemetry.ts +33 -0
|
@@ -1010,6 +1010,7 @@ function markScenesStale(iframe, files) {
|
|
|
1010
1010
|
}
|
|
1011
1011
|
meta.content = JSON.stringify(parts);
|
|
1012
1012
|
}
|
|
1013
|
+
var supersededByNewerEdit = () => new DOMException("superseded by a newer edit", "AbortError");
|
|
1013
1014
|
function sceneSwapFor(iframe) {
|
|
1014
1015
|
const win = iframe.contentWindow;
|
|
1015
1016
|
const swap = win?.__hfSwapScenes;
|
|
@@ -1535,6 +1536,7 @@ export {
|
|
|
1535
1536
|
announcePreviewPromoted,
|
|
1536
1537
|
onPreviewPromoted,
|
|
1537
1538
|
markScenesStale,
|
|
1539
|
+
supersededByNewerEdit,
|
|
1538
1540
|
sceneSwapFor,
|
|
1539
1541
|
Player
|
|
1540
1542
|
};
|
package/dist/index.html
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
document.documentElement.dataset.theme = "paper";
|
|
15
15
|
}
|
|
16
16
|
</script>
|
|
17
|
-
<script type="module" crossorigin src="/assets/index-
|
|
17
|
+
<script type="module" crossorigin src="/assets/index-Bi6yVTq9.js"></script>
|
|
18
18
|
<link rel="stylesheet" crossorigin href="/assets/index-CESvLzz0.css">
|
|
19
19
|
</head>
|
|
20
20
|
<body>
|
package/dist/index.js
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
sceneSwapFor,
|
|
40
40
|
secondsToFrame,
|
|
41
41
|
stepFrameTime,
|
|
42
|
+
supersededByNewerEdit,
|
|
42
43
|
thumbnailRevisionOf,
|
|
43
44
|
timelineSliderToZoomPercent,
|
|
44
45
|
timelineZoomPercentToSlider,
|
|
@@ -47,7 +48,7 @@ import {
|
|
|
47
48
|
usePreviewVariablesStore,
|
|
48
49
|
whenPreviewBooted,
|
|
49
50
|
writeStudioUiPreferences
|
|
50
|
-
} from "./chunk-
|
|
51
|
+
} from "./chunk-563JPIL4.js";
|
|
51
52
|
import {
|
|
52
53
|
Button,
|
|
53
54
|
ContextMenu,
|
|
@@ -32628,7 +32629,7 @@ function useTimelinePlayer({
|
|
|
32628
32629
|
const refreshPlayer = useCallback41(() => {
|
|
32629
32630
|
const iframe = iframeRef.current;
|
|
32630
32631
|
if (!iframe) return;
|
|
32631
|
-
swapCancelRef.current?.abort(
|
|
32632
|
+
swapCancelRef.current?.abort(supersededByNewerEdit());
|
|
32632
32633
|
const cancel = new AbortController();
|
|
32633
32634
|
swapCancelRef.current = cancel;
|
|
32634
32635
|
logReload("refreshPlayer", () => ({ stack: new Error("refreshPlayer").stack }));
|
|
@@ -75094,8 +75095,31 @@ import { useState as useState117, useCallback as useCallback104, useRef as useRe
|
|
|
75094
75095
|
|
|
75095
75096
|
// src/hooks/useFileTree.ts
|
|
75096
75097
|
import { useState as useState116, useCallback as useCallback101, useEffect as useEffect112, useMemo as useMemo71, useRef as useRef128 } from "react";
|
|
75098
|
+
|
|
75099
|
+
// src/utils/rejectionTelemetry.ts
|
|
75100
|
+
function errorProps(value) {
|
|
75101
|
+
if (value instanceof Error) {
|
|
75102
|
+
return {
|
|
75103
|
+
error_message: value.message,
|
|
75104
|
+
error_name: value.name,
|
|
75105
|
+
stack_trace: value.stack?.slice(0, 4e3) ?? null
|
|
75106
|
+
};
|
|
75107
|
+
}
|
|
75108
|
+
return { error_message: String(value), error_name: null, stack_trace: null };
|
|
75109
|
+
}
|
|
75110
|
+
|
|
75111
|
+
// src/hooks/useFileTree.ts
|
|
75097
75112
|
import { encodeUrlPath as encodeUrlPath4 } from "@hyperframes/parsers";
|
|
75098
75113
|
var EMPTY_FILE_LIST = [];
|
|
75114
|
+
async function readTreeListing(projectId, signal) {
|
|
75115
|
+
const res = await studioApiFetch(buildProjectApiPath(projectId), { signal });
|
|
75116
|
+
if (!res.ok) throw new Error(`tree fetch failed: ${res.status}`);
|
|
75117
|
+
return res.json();
|
|
75118
|
+
}
|
|
75119
|
+
function reportRefreshFailure(err) {
|
|
75120
|
+
const { error_name, error_message } = errorProps(err);
|
|
75121
|
+
trackStudioEvent("file_tree_refresh_failed", { error_name, error_message });
|
|
75122
|
+
}
|
|
75099
75123
|
function useFileTree({ projectId, projectIdRef }) {
|
|
75100
75124
|
const [fetched, setFetched] = useState116(null);
|
|
75101
75125
|
const current = fetched?.projectId === projectId ? fetched : null;
|
|
@@ -75131,6 +75155,7 @@ function useFileTree({ projectId, projectIdRef }) {
|
|
|
75131
75155
|
}, [projectId]);
|
|
75132
75156
|
const refreshRequestRef = useRef128(0);
|
|
75133
75157
|
const refreshAbortRef = useRef128(null);
|
|
75158
|
+
const failureReportedRef = useRef128(false);
|
|
75134
75159
|
const refreshFileTree = useCallback101(async () => {
|
|
75135
75160
|
const pid = projectIdRef.current;
|
|
75136
75161
|
if (!pid) return;
|
|
@@ -75140,12 +75165,14 @@ function useFileTree({ projectId, projectIdRef }) {
|
|
|
75140
75165
|
refreshAbortRef.current = controller;
|
|
75141
75166
|
let data;
|
|
75142
75167
|
try {
|
|
75143
|
-
|
|
75144
|
-
data = await res.json();
|
|
75168
|
+
data = await readTreeListing(pid, controller.signal);
|
|
75145
75169
|
} catch (err) {
|
|
75146
75170
|
if (err instanceof DOMException && err.name === "AbortError") return;
|
|
75147
|
-
|
|
75171
|
+
if (!failureReportedRef.current) reportRefreshFailure(err);
|
|
75172
|
+
failureReportedRef.current = true;
|
|
75173
|
+
return;
|
|
75148
75174
|
}
|
|
75175
|
+
failureReportedRef.current = false;
|
|
75149
75176
|
if (data.files && requestId === refreshRequestRef.current) {
|
|
75150
75177
|
setFetched(
|
|
75151
75178
|
(prev) => prev?.projectId === pid ? {
|
package/dist/player.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.178",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -59,12 +59,12 @@
|
|
|
59
59
|
"dockview-react": "8.3.1",
|
|
60
60
|
"gsap": "^3.13.0",
|
|
61
61
|
"mediabunny": "^1.45.3",
|
|
62
|
-
"@hyperframes/
|
|
63
|
-
"@hyperframes/
|
|
64
|
-
"@hyperframes/
|
|
65
|
-
"@hyperframes/
|
|
66
|
-
"@hyperframes/
|
|
67
|
-
"@hyperframes/
|
|
62
|
+
"@hyperframes/core": "0.8.178",
|
|
63
|
+
"@hyperframes/parsers": "0.8.178",
|
|
64
|
+
"@hyperframes/sdk": "0.8.178",
|
|
65
|
+
"@hyperframes/player": "0.8.178",
|
|
66
|
+
"@hyperframes/studio-server": "0.8.178",
|
|
67
|
+
"@hyperframes/shader-transitions": "0.8.178"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
70
|
"@tailwindcss/vite": "^4.3.3",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"vite": "^6.4.2",
|
|
82
82
|
"vitest": "^4.1.11",
|
|
83
83
|
"zustand": "^5.0.0",
|
|
84
|
-
"@hyperframes/producer": "0.8.
|
|
84
|
+
"@hyperframes/producer": "0.8.178"
|
|
85
85
|
},
|
|
86
86
|
"peerDependencies": {
|
|
87
87
|
"react": "19",
|
|
@@ -9,7 +9,10 @@ vi.mock("../components/feedback/projectProvenance", () => ({
|
|
|
9
9
|
captureProjectProvenance: vi.fn(),
|
|
10
10
|
}));
|
|
11
11
|
|
|
12
|
+
vi.mock("../utils/studioTelemetry", () => ({ trackStudioEvent: vi.fn() }));
|
|
13
|
+
|
|
12
14
|
import { captureProjectProvenance } from "../components/feedback/projectProvenance";
|
|
15
|
+
import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
13
16
|
|
|
14
17
|
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
15
18
|
|
|
@@ -244,6 +247,37 @@ describe("useFileTree.refreshFileTree", () => {
|
|
|
244
247
|
fetchSpy.mockRestore();
|
|
245
248
|
});
|
|
246
249
|
|
|
250
|
+
it("resolves when the server is gone, and reports it once per unreachable streak", async () => {
|
|
251
|
+
const track = vi.mocked(trackStudioEvent);
|
|
252
|
+
track.mockClear();
|
|
253
|
+
let serverUp = true;
|
|
254
|
+
const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(async () => {
|
|
255
|
+
if (!serverUp) throw new TypeError("Failed to fetch");
|
|
256
|
+
return new Response(JSON.stringify({ files: ["index.html"], compositions: ["index.html"] }));
|
|
257
|
+
});
|
|
258
|
+
const handleRef = await renderHarness("project-a");
|
|
259
|
+
|
|
260
|
+
serverUp = false;
|
|
261
|
+
await act(async () => {
|
|
262
|
+
await handleRef.current?.refresh();
|
|
263
|
+
await handleRef.current?.refresh();
|
|
264
|
+
});
|
|
265
|
+
expect(handleRef.current?.compositions).toEqual(["index.html"]);
|
|
266
|
+
expect(track).toHaveBeenCalledTimes(1);
|
|
267
|
+
expect(track).toHaveBeenCalledWith("file_tree_refresh_failed", {
|
|
268
|
+
error_name: "TypeError",
|
|
269
|
+
error_message: "Failed to fetch",
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
serverUp = true;
|
|
273
|
+
await act(async () => handleRef.current?.refresh());
|
|
274
|
+
serverUp = false;
|
|
275
|
+
await act(async () => handleRef.current?.refresh());
|
|
276
|
+
expect(track).toHaveBeenCalledTimes(2);
|
|
277
|
+
|
|
278
|
+
fetchSpy.mockRestore();
|
|
279
|
+
});
|
|
280
|
+
|
|
247
281
|
it("keeps the prior compositions list when a refresh response omits the field", async () => {
|
|
248
282
|
let call = 0;
|
|
249
283
|
const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(async () => {
|
package/src/hooks/useFileTree.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { FONT_EXT } from "../utils/mediaTypes";
|
|
|
4
4
|
import { fontFamilyFromAssetPath, type ImportedFontAsset } from "../components/editor/fontAssets";
|
|
5
5
|
import { captureProjectProvenance } from "../components/feedback/projectProvenance";
|
|
6
6
|
import { studioApiFetch } from "../utils/studioApiFetch";
|
|
7
|
+
import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
8
|
+
import { errorProps } from "../utils/rejectionTelemetry";
|
|
7
9
|
import { encodeUrlPath } from "@hyperframes/parsers";
|
|
8
10
|
|
|
9
11
|
interface UseFileTreeOptions {
|
|
@@ -23,6 +25,19 @@ interface FetchedFileTree {
|
|
|
23
25
|
// and defeat useMemo/useCallback deps downstream (e.g. the `assets` memo below).
|
|
24
26
|
const EMPTY_FILE_LIST: string[] = [];
|
|
25
27
|
|
|
28
|
+
type TreeListing = { files?: string[]; compositions?: string[] };
|
|
29
|
+
|
|
30
|
+
async function readTreeListing(projectId: string, signal: AbortSignal): Promise<TreeListing> {
|
|
31
|
+
const res = await studioApiFetch(buildProjectApiPath(projectId), { signal });
|
|
32
|
+
if (!res.ok) throw new Error(`tree fetch failed: ${res.status}`);
|
|
33
|
+
return res.json();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function reportRefreshFailure(err: unknown): void {
|
|
37
|
+
const { error_name, error_message } = errorProps(err);
|
|
38
|
+
trackStudioEvent("file_tree_refresh_failed", { error_name, error_message });
|
|
39
|
+
}
|
|
40
|
+
|
|
26
41
|
/**
|
|
27
42
|
* Holds the tree with the `projectId` it was fetched for; exposes empty/not-loaded
|
|
28
43
|
* whenever that id doesn't match the current one, so a switch can't read another project's tree.
|
|
@@ -74,6 +89,7 @@ export function useFileTree({ projectId, projectIdRef }: UseFileTreeOptions) {
|
|
|
74
89
|
|
|
75
90
|
const refreshRequestRef = useRef(0);
|
|
76
91
|
const refreshAbortRef = useRef<AbortController | null>(null);
|
|
92
|
+
const failureReportedRef = useRef(false);
|
|
77
93
|
|
|
78
94
|
const refreshFileTree = useCallback(async () => {
|
|
79
95
|
const pid = projectIdRef.current;
|
|
@@ -84,14 +100,16 @@ export function useFileTree({ projectId, projectIdRef }: UseFileTreeOptions) {
|
|
|
84
100
|
const requestId = ++refreshRequestRef.current;
|
|
85
101
|
const controller = new AbortController();
|
|
86
102
|
refreshAbortRef.current = controller;
|
|
87
|
-
let data:
|
|
103
|
+
let data: TreeListing;
|
|
88
104
|
try {
|
|
89
|
-
|
|
90
|
-
data = await res.json();
|
|
105
|
+
data = await readTreeListing(pid, controller.signal);
|
|
91
106
|
} catch (err) {
|
|
92
107
|
if (err instanceof DOMException && err.name === "AbortError") return;
|
|
93
|
-
|
|
108
|
+
if (!failureReportedRef.current) reportRefreshFailure(err);
|
|
109
|
+
failureReportedRef.current = true;
|
|
110
|
+
return;
|
|
94
111
|
}
|
|
112
|
+
failureReportedRef.current = false;
|
|
95
113
|
if (data.files && requestId === refreshRequestRef.current) {
|
|
96
114
|
setFetched((prev) =>
|
|
97
115
|
prev?.projectId === pid
|
package/src/main.tsx
CHANGED
|
@@ -6,6 +6,11 @@ import { ShowThemeToggle } from "./components/ThemeToggle";
|
|
|
6
6
|
import { StudioErrorBoundary } from "./components/StudioErrorBoundary";
|
|
7
7
|
import { readIconTokens } from "./styles/iconTokens";
|
|
8
8
|
import { trackStudioEvent } from "./utils/studioTelemetry";
|
|
9
|
+
import {
|
|
10
|
+
errorProps,
|
|
11
|
+
isCompositionAssetError,
|
|
12
|
+
isExtensionOnlyStack,
|
|
13
|
+
} from "./utils/rejectionTelemetry";
|
|
9
14
|
import { prefetchPreviewForHash } from "./utils/previewPrefetch";
|
|
10
15
|
import { bindThumbnailPageLifecycle } from "./player/lib/thumbnailPageLifecycle";
|
|
11
16
|
import "./styles/studio.css";
|
|
@@ -17,30 +22,6 @@ prefetchPreviewForHash(window.location.hash);
|
|
|
17
22
|
window.addEventListener("hashchange", () => prefetchPreviewForHash(window.location.hash));
|
|
18
23
|
trackStudioEvent("session_start");
|
|
19
24
|
|
|
20
|
-
function errorProps(value: unknown): {
|
|
21
|
-
error_message: string;
|
|
22
|
-
error_name: string | null;
|
|
23
|
-
stack_trace: string | null;
|
|
24
|
-
} {
|
|
25
|
-
if (value instanceof Error) {
|
|
26
|
-
return {
|
|
27
|
-
error_message: value.message,
|
|
28
|
-
error_name: value.name,
|
|
29
|
-
stack_trace: value.stack?.slice(0, 4000) ?? null,
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
return { error_message: String(value), error_name: null, stack_trace: null };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// fallow-ignore-next-line complexity
|
|
36
|
-
function isCompositionAssetError(msg: string, name: string | null): boolean {
|
|
37
|
-
if (msg.includes("Error fetching") && (msg.includes("404") || msg.includes("Not Found")))
|
|
38
|
-
return true;
|
|
39
|
-
if (name === "EncodingError" || msg.includes("unsupported or unrecognizable format")) return true;
|
|
40
|
-
if (msg.includes("MEDIA_ERR_SRC_NOT_SUPPORTED")) return true;
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
25
|
const ERROR_CAP = 50;
|
|
45
26
|
let errorCount = 0;
|
|
46
27
|
let rejectionCount = 0;
|
|
@@ -114,6 +95,8 @@ window.addEventListener("unhandledrejection", (event) => {
|
|
|
114
95
|
}
|
|
115
96
|
return;
|
|
116
97
|
}
|
|
98
|
+
// Not ours: an extension's own promise. Dropped before the cap so it cannot silence ours.
|
|
99
|
+
if (isExtensionOnlyStack(props.stack_trace)) return;
|
|
117
100
|
|
|
118
101
|
rejectionCount++;
|
|
119
102
|
if (rejectionCount > ERROR_CAP) {
|
|
@@ -42,7 +42,7 @@ import { isStudioManualEditGestureLiveIn } from "../../components/editor/manualE
|
|
|
42
42
|
import { createPreviewMessageHandler } from "./previewMessageRouter";
|
|
43
43
|
import { timelineElementsChanged } from "./timelinePlayerSync";
|
|
44
44
|
import { safeContentDocument } from "./timelineSyncHydration";
|
|
45
|
-
import { sceneSwapFor } from "../sceneSwap";
|
|
45
|
+
import { sceneSwapFor, supersededByNewerEdit } from "../sceneSwap";
|
|
46
46
|
import { whileScriptWrites } from "../previewReloading";
|
|
47
47
|
|
|
48
48
|
export interface UseTimelinePlayerOptions {
|
|
@@ -489,7 +489,7 @@ export function useTimelinePlayer({
|
|
|
489
489
|
const refreshPlayer = useCallback(() => {
|
|
490
490
|
const iframe = iframeRef.current;
|
|
491
491
|
if (!iframe) return;
|
|
492
|
-
swapCancelRef.current?.abort(
|
|
492
|
+
swapCancelRef.current?.abort(supersededByNewerEdit());
|
|
493
493
|
const cancel = new AbortController();
|
|
494
494
|
swapCancelRef.current = cancel;
|
|
495
495
|
logReload("refreshPlayer", () => ({ stack: new Error("refreshPlayer").stack }));
|
|
@@ -5,7 +5,9 @@ import {
|
|
|
5
5
|
announcePreviewDocumentLoaded,
|
|
6
6
|
onPreviewContentReplaced,
|
|
7
7
|
sceneSwapFor,
|
|
8
|
+
supersededByNewerEdit,
|
|
8
9
|
} from "./sceneSwap";
|
|
10
|
+
import { errorProps } from "../utils/rejectionTelemetry";
|
|
9
11
|
|
|
10
12
|
afterEach(() => {
|
|
11
13
|
vi.useRealTimers();
|
|
@@ -83,10 +85,15 @@ describe("sceneSwapFor", () => {
|
|
|
83
85
|
.mockImplementation(async () => new Response("<html></html>"));
|
|
84
86
|
const { iframe, swap } = swappableIframe();
|
|
85
87
|
const cancel = new AbortController();
|
|
86
|
-
cancel.abort(
|
|
87
|
-
await
|
|
88
|
-
|
|
88
|
+
cancel.abort(supersededByNewerEdit());
|
|
89
|
+
const rejection = await sceneSwapFor(iframe)!("/preview", () => true, cancel.signal).catch(
|
|
90
|
+
(error: unknown) => error,
|
|
89
91
|
);
|
|
92
|
+
// The rejection listener files AbortErrors as cancellations, never as unhandled failures.
|
|
93
|
+
expect(errorProps(rejection)).toMatchObject({
|
|
94
|
+
error_name: "AbortError",
|
|
95
|
+
error_message: "superseded by a newer edit",
|
|
96
|
+
});
|
|
90
97
|
expect(fetchSpy).not.toHaveBeenCalled();
|
|
91
98
|
expect(swap).not.toHaveBeenCalled();
|
|
92
99
|
});
|
package/src/player/sceneSwap.ts
CHANGED
|
@@ -71,6 +71,9 @@ export function markScenesStale(iframe: HTMLIFrameElement | null, files: readonl
|
|
|
71
71
|
meta.content = JSON.stringify(parts);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
export const supersededByNewerEdit = () =>
|
|
75
|
+
new DOMException("superseded by a newer edit", "AbortError");
|
|
76
|
+
|
|
74
77
|
/** Null when the preview cannot swap; rejects when superseded, cancelled or a full reload is needed. */
|
|
75
78
|
export function sceneSwapFor(
|
|
76
79
|
iframe: HTMLIFrameElement,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { isExtensionOnlyStack } from "./rejectionTelemetry";
|
|
3
|
+
|
|
4
|
+
describe("isExtensionOnlyStack", () => {
|
|
5
|
+
it("is true when every V8 frame is an extension script", () => {
|
|
6
|
+
const stack = [
|
|
7
|
+
"TypeError: Cannot read properties of undefined (reading 'M_ID')",
|
|
8
|
+
" at Y (chrome-extension://abcdefghijklmnop/executors/200.js:1:761)",
|
|
9
|
+
" at E (chrome-extension://abcdefghijklmnop/executors/200.js:1:1442)",
|
|
10
|
+
].join("\n");
|
|
11
|
+
expect(isExtensionOnlyStack(stack)).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("is true for a wallet's single-frame connect failure", () => {
|
|
15
|
+
const stack =
|
|
16
|
+
"i: Failed to connect to MetaMask\n at Object.connect (chrome-extension://abcdefghijklmnop/scripts/inpage.js:7:84292)";
|
|
17
|
+
expect(isExtensionOnlyStack(stack)).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("reads Firefox and Safari frames", () => {
|
|
21
|
+
expect(isExtensionOnlyStack("connect@moz-extension://1234-abcd/inpage.js:3:10\n")).toBe(true);
|
|
22
|
+
expect(isExtensionOnlyStack("f@safari-web-extension://1234-abcd/content.js:1:5")).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("is false when a Studio frame is in the stack, even under an extension's fetch wrapper", () => {
|
|
26
|
+
const stack = [
|
|
27
|
+
"TypeError: Failed to fetch",
|
|
28
|
+
" at o (chrome-extension://abcdefghijklmnop/frame_ant/frame_ant.js:2:18135)",
|
|
29
|
+
" at window.fetch (chrome-extension://abcdefghijklmnop/frame_ant/frame_ant.js:2:18200)",
|
|
30
|
+
" at Dt (http://localhost:3002/assets/index-abc.js:49:296453)",
|
|
31
|
+
].join("\n");
|
|
32
|
+
expect(isExtensionOnlyStack(stack)).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("is false for a stack with no frames to judge", () => {
|
|
36
|
+
expect(isExtensionOnlyStack(null)).toBe(false);
|
|
37
|
+
expect(isExtensionOnlyStack("Error: chrome-extension://abcdefghijklmnop failed")).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export function errorProps(value: unknown): {
|
|
2
|
+
error_message: string;
|
|
3
|
+
error_name: string | null;
|
|
4
|
+
stack_trace: string | null;
|
|
5
|
+
} {
|
|
6
|
+
if (value instanceof Error) {
|
|
7
|
+
return {
|
|
8
|
+
error_message: value.message,
|
|
9
|
+
error_name: value.name,
|
|
10
|
+
stack_trace: value.stack?.slice(0, 4000) ?? null,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
return { error_message: String(value), error_name: null, stack_trace: null };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// fallow-ignore-next-line complexity
|
|
17
|
+
export function isCompositionAssetError(msg: string, name: string | null): boolean {
|
|
18
|
+
if (msg.includes("Error fetching") && (msg.includes("404") || msg.includes("Not Found")))
|
|
19
|
+
return true;
|
|
20
|
+
if (name === "EncodingError" || msg.includes("unsupported or unrecognizable format")) return true;
|
|
21
|
+
if (msg.includes("MEDIA_ERR_SRC_NOT_SUPPORTED")) return true;
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// A frame is a line ending in `:line:col`, in both the V8 (`at f (url:1:2)`) and Gecko/WebKit (`f@url:1:2`) forms.
|
|
26
|
+
const STACK_FRAME = /:\d+:\d+\)?\s*$/;
|
|
27
|
+
const EXTENSION_URL = /\b(?:chrome|moz|safari|safari-web)-extension:\/\//;
|
|
28
|
+
|
|
29
|
+
/** True when every frame is an extension's script: a failure no Studio code took part in. */
|
|
30
|
+
export function isExtensionOnlyStack(stack: string | null): boolean {
|
|
31
|
+
const frames = stack?.split("\n").filter((line) => STACK_FRAME.test(line)) ?? [];
|
|
32
|
+
return frames.length > 0 && frames.every((line) => EXTENSION_URL.test(line));
|
|
33
|
+
}
|