@devaccel-labs/app-comments-toolbar 1.0.0 → 1.3.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/README.md +152 -18
- package/dist/react.js +516 -61
- package/dist/server.js +342 -14
- package/dist/types/capture/api.d.ts +30 -0
- package/dist/types/capture/editor.d.ts +10 -0
- package/dist/types/capture/ignore.d.ts +19 -0
- package/dist/types/capture/index.d.ts +1 -0
- package/dist/types/capture/picker.d.ts +9 -0
- package/dist/types/capture/picture.d.ts +29 -0
- package/dist/types/capture/surface.d.ts +39 -0
- package/dist/types/core/index.d.ts +2 -2
- package/dist/types/core/resolve.d.ts +2 -1
- package/dist/types/core/types.d.ts +11 -0
- package/dist/types/debug-context.d.ts +7 -0
- package/dist/types/dictation.d.ts +9 -0
- package/dist/types/embed/controller.d.ts +61 -0
- package/dist/types/embed/index.d.ts +1 -0
- package/dist/types/embed/options.d.ts +35 -0
- package/dist/types/help/index.d.ts +1 -0
- package/dist/types/help/panel-index.d.ts +1 -0
- package/dist/types/help/panel-styles.d.ts +1 -0
- package/dist/types/help/panel.d.ts +4 -0
- package/dist/types/help/pill-styles.d.ts +11 -0
- package/dist/types/help/pill.d.ts +44 -0
- package/dist/types/help/react.d.ts +12 -0
- package/dist/types/help/requests.d.ts +2 -0
- package/dist/types/help/types.d.ts +119 -0
- package/dist/types/help/wire.d.ts +61 -0
- package/dist/types/native-wire.d.ts +63 -0
- package/dist/types/native.d.ts +43 -0
- package/dist/types/react.d.ts +8 -1
- package/dist/types/replay.d.ts +6 -0
- package/dist/types/runtime/api.d.ts +250 -0
- package/dist/types/runtime/attachment-model.d.ts +16 -0
- package/dist/types/runtime/attachments.d.ts +43 -7
- package/dist/types/runtime/boot.d.ts +39 -0
- package/dist/types/runtime/browser-label.d.ts +5 -0
- package/dist/types/runtime/build-id.d.ts +7 -0
- package/dist/types/runtime/capture-ui.d.ts +45 -21
- package/dist/types/runtime/clarity-model.d.ts +260 -0
- package/dist/types/runtime/clarity-ui.d.ts +62 -0
- package/dist/types/runtime/client.d.ts +6 -197
- package/dist/types/runtime/composer-size.d.ts +30 -0
- package/dist/types/runtime/dashboard.d.ts +17 -2
- package/dist/types/runtime/debug-context-core.d.ts +88 -0
- package/dist/types/runtime/debug-context-host.d.ts +39 -0
- package/dist/types/runtime/debug-context-model.d.ts +352 -0
- package/dist/types/runtime/debug-context-redact.d.ts +48 -0
- package/dist/types/runtime/debug-context.d.ts +36 -0
- package/dist/types/runtime/details-model.d.ts +7 -0
- package/dist/types/runtime/details.d.ts +7 -5
- package/dist/types/runtime/dictation-engine.d.ts +98 -0
- package/dist/types/runtime/dictation-model.d.ts +28 -0
- package/dist/types/runtime/dictation-ui.d.ts +56 -0
- package/dist/types/runtime/dictation.d.ts +66 -0
- package/dist/types/runtime/draft-dock.d.ts +32 -0
- package/dist/types/runtime/draft-model.d.ts +104 -0
- package/dist/types/runtime/draft-store.d.ts +15 -0
- package/dist/types/runtime/edits.d.ts +1 -1
- package/dist/types/runtime/font.d.ts +16 -0
- package/dist/types/runtime/format.d.ts +30 -1
- package/dist/types/runtime/host-watch.d.ts +20 -0
- package/dist/types/runtime/icons.d.ts +16 -2
- package/dist/types/runtime/index.d.ts +3 -1
- package/dist/types/runtime/ink.d.ts +8 -0
- package/dist/types/runtime/kind-model.d.ts +14 -0
- package/dist/types/runtime/live-model.d.ts +25 -0
- package/dist/types/runtime/loader.d.ts +5 -11
- package/dist/types/runtime/markup-editor.d.ts +54 -0
- package/dist/types/runtime/markup-model.d.ts +126 -0
- package/dist/types/runtime/markup.d.ts +69 -0
- package/dist/types/runtime/mention-parse.d.ts +18 -0
- package/dist/types/runtime/mentions.d.ts +4 -4
- package/dist/types/runtime/page-background.d.ts +9 -0
- package/dist/types/runtime/page-image.d.ts +123 -0
- package/dist/types/runtime/placement.d.ts +6 -3
- package/dist/types/runtime/record.d.ts +18 -2
- package/dist/types/runtime/related.d.ts +1 -1
- package/dist/types/runtime/replay-model.d.ts +80 -0
- package/dist/types/runtime/replay.d.ts +5 -38
- package/dist/types/runtime/runtime.d.ts +6 -0
- package/dist/types/runtime/screen.d.ts +47 -0
- package/dist/types/runtime/screenshot.d.ts +60 -6
- package/dist/types/runtime/session-source.d.ts +45 -0
- package/dist/types/runtime/session.d.ts +20 -18
- package/dist/types/runtime/shield.d.ts +9 -0
- package/dist/types/runtime/shot.d.ts +45 -0
- package/dist/types/runtime/sketch.d.ts +26 -0
- package/dist/types/runtime/styles.d.ts +1 -1
- package/dist/types/runtime/sync.d.ts +1 -0
- package/dist/types/runtime/thread-model.d.ts +18 -0
- package/dist/types/runtime/ticket-form-model.d.ts +57 -0
- package/dist/types/runtime/ticket-form.d.ts +9 -0
- package/dist/types/runtime/toolbar.d.ts +37 -1
- package/dist/types/runtime/ui-scale.d.ts +17 -0
- package/dist/types/runtime/update.d.ts +37 -0
- package/dist/types/runtime/use-debug-context.d.ts +27 -0
- package/dist/types/server/index.d.ts +22 -0
- package/dist/types/server/support.d.ts +20 -0
- package/dist/types/server.d.ts +2 -0
- package/dist/types/vite/plugin.d.ts +15 -0
- package/dist/vite.js +121 -41
- package/package.json +26 -13
- package/src/capture/api.ts +128 -0
- package/src/capture/editor.tsx +178 -0
- package/src/capture/ignore.ts +38 -0
- package/src/capture/index.ts +27 -0
- package/src/capture/picker.tsx +171 -0
- package/src/capture/picture.ts +90 -0
- package/src/capture/surface.tsx +185 -0
- package/src/compiler/annotate.ts +202 -0
- package/src/compiler/babel.d.ts +11 -0
- package/src/compiler/enabled.ts +55 -0
- package/src/compiler/index.ts +2 -0
- package/src/core/index.ts +8 -0
- package/src/core/resolve.ts +307 -0
- package/src/core/types.ts +58 -0
- package/src/debug-context.ts +47 -0
- package/src/dictation.ts +17 -0
- package/src/embed/controller.ts +215 -0
- package/src/embed/index.ts +68 -0
- package/src/embed/options.ts +100 -0
- package/src/help/index.ts +38 -0
- package/src/help/panel-index.ts +17 -0
- package/src/help/panel-styles.ts +123 -0
- package/src/help/panel.ts +840 -0
- package/src/help/pill-styles.ts +75 -0
- package/src/help/pill.ts +338 -0
- package/src/help/react.tsx +143 -0
- package/src/help/requests.ts +111 -0
- package/src/help/types.ts +124 -0
- package/src/help/wire.ts +55 -0
- package/src/native-wire.ts +124 -0
- package/src/native.ts +282 -0
- package/src/react.ts +27 -0
- package/src/replay.ts +13 -0
- package/src/runtime/api.ts +531 -0
- package/src/runtime/attachment-model.ts +42 -0
- package/src/runtime/attachments.tsx +1260 -0
- package/src/runtime/boot.ts +95 -0
- package/src/runtime/browser-label.ts +31 -0
- package/src/runtime/build-id.ts +10 -0
- package/src/runtime/capture-ui.tsx +306 -0
- package/src/runtime/capture.ts +508 -0
- package/src/runtime/clarity-model.ts +634 -0
- package/src/runtime/clarity-ui.tsx +454 -0
- package/src/runtime/client.ts +151 -0
- package/src/runtime/composer-size.tsx +146 -0
- package/src/runtime/dashboard.tsx +1130 -0
- package/src/runtime/debug-context-core.ts +681 -0
- package/src/runtime/debug-context-host.ts +56 -0
- package/src/runtime/debug-context-model.ts +1319 -0
- package/src/runtime/debug-context-redact.ts +259 -0
- package/src/runtime/debug-context.ts +613 -0
- package/src/runtime/details-model.ts +30 -0
- package/src/runtime/details.tsx +395 -0
- package/src/runtime/dictation-engine.ts +616 -0
- package/src/runtime/dictation-model.ts +79 -0
- package/src/runtime/dictation-ui.tsx +363 -0
- package/src/runtime/dictation.ts +271 -0
- package/src/runtime/dock.ts +108 -0
- package/src/runtime/draft-dock.tsx +124 -0
- package/src/runtime/draft-model.ts +251 -0
- package/src/runtime/draft-store.ts +149 -0
- package/src/runtime/edits.tsx +165 -0
- package/src/runtime/font.ts +42 -0
- package/src/runtime/format.ts +129 -0
- package/src/runtime/freshness.ts +23 -0
- package/src/runtime/handoff.ts +76 -0
- package/src/runtime/host-watch.ts +64 -0
- package/src/runtime/icons.tsx +295 -0
- package/src/runtime/idle.ts +20 -0
- package/src/runtime/index.ts +11 -0
- package/src/runtime/ink.ts +6 -0
- package/src/runtime/kind-model.ts +20 -0
- package/src/runtime/live-model.ts +32 -0
- package/src/runtime/loader.tsx +87 -0
- package/src/runtime/markup-editor.tsx +1333 -0
- package/src/runtime/markup-model.ts +485 -0
- package/src/runtime/markup.ts +382 -0
- package/src/runtime/members.ts +58 -0
- package/src/runtime/mention-parse.ts +56 -0
- package/src/runtime/mentions.tsx +205 -0
- package/src/runtime/page-background.ts +51 -0
- package/src/runtime/page-image.ts +1592 -0
- package/src/runtime/placement.ts +336 -0
- package/src/runtime/record.ts +402 -0
- package/src/runtime/redact.ts +30 -0
- package/src/runtime/related.ts +34 -0
- package/src/runtime/render-page.ts +2 -0
- package/src/runtime/replay-model.ts +232 -0
- package/src/runtime/replay.ts +173 -0
- package/src/runtime/runtime-version.ts +7 -0
- package/src/runtime/runtime.tsx +51 -0
- package/src/runtime/screen.ts +182 -0
- package/src/runtime/screenshot.ts +260 -0
- package/src/runtime/session-source.ts +112 -0
- package/src/runtime/session.ts +108 -0
- package/src/runtime/shield.ts +34 -0
- package/src/runtime/shot.tsx +422 -0
- package/src/runtime/sketch.ts +139 -0
- package/src/runtime/styles.ts +1541 -0
- package/src/runtime/sync.ts +300 -0
- package/src/runtime/thread-model.ts +71 -0
- package/src/runtime/ticket-form-model.ts +128 -0
- package/src/runtime/ticket-form.tsx +342 -0
- package/src/runtime/toolbar.tsx +4427 -0
- package/src/runtime/ui-scale.ts +75 -0
- package/src/runtime/update.ts +98 -0
- package/src/runtime/use-debug-context.ts +81 -0
- package/src/server/index.ts +133 -0
- package/src/server/support.ts +499 -0
- package/src/server.ts +4 -0
- package/src/vite/plugin.ts +102 -0
- package/src/vite.ts +1 -0
- package/dist/chunks/chunk-CSKXLF33.js +0 -153
- package/dist/chunks/chunk-HUM5FQII.js +0 -160
- package/dist/chunks/render-page-76MLIT6M.js +0 -1421
- package/dist/chunks/runtime-JX75TKER.js +0 -9267
- package/dist/chunks/screenshot-66ARTBGR.js +0 -12
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debug context on a web page: the hooks (console, `error`, `unhandledrejection`, fetch, XHR, resources,
|
|
3
|
+
* CSP, online, visibility, performance) that feed the shared core (`./debug-context-core.ts`), and what
|
|
4
|
+
* only a page has: its address, the browser, the element pointed at, storage.
|
|
5
|
+
*
|
|
6
|
+
* Runs only while someone is signed in to the toolbar and the host turned debug context on in code. Nothing
|
|
7
|
+
* leaves the page until a report is posted.
|
|
8
|
+
*/
|
|
9
|
+
import type { Anchor } from "../core/index.js";
|
|
10
|
+
import { browserName, systemName } from "./browser-label.js";
|
|
11
|
+
import { runtimeBuild } from "./build-id.js";
|
|
12
|
+
import {
|
|
13
|
+
BODY_CHARS,
|
|
14
|
+
createDebugCore,
|
|
15
|
+
errorOf,
|
|
16
|
+
excerptOf,
|
|
17
|
+
FOREIGN_FRAME,
|
|
18
|
+
GRAPHQL_READ_CHARS,
|
|
19
|
+
patch,
|
|
20
|
+
within,
|
|
21
|
+
type DebugCaptureStats,
|
|
22
|
+
} from "./debug-context-core.js";
|
|
23
|
+
import type { DebugContextFile, ResolvedDebugContext } from "./debug-context-model.js";
|
|
24
|
+
import { newTally, scrubText, scrubUrl, type RedactionTally } from "./debug-context-redact.js";
|
|
25
|
+
import type { ReplayEvent } from "./replay-model.js";
|
|
26
|
+
|
|
27
|
+
export type { DebugCaptureStats } from "./debug-context-core.js";
|
|
28
|
+
|
|
29
|
+
export type DebugSnapshotInput = {
|
|
30
|
+
element?: Element | null;
|
|
31
|
+
anchor?: Anchor | null;
|
|
32
|
+
/** The toolbar's step trail (its replay buffer), oldest first. */
|
|
33
|
+
steps?: readonly ReplayEvent[];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type DebugCapture = {
|
|
37
|
+
/** The moment the person started the report: the file's clock and the "still pending" list come from it. */
|
|
38
|
+
mark(): void;
|
|
39
|
+
/** The file, redacted and bounded, or null when the host's `beforeCapture` refused it or threw. */
|
|
40
|
+
snapshot(input?: DebugSnapshotInput): Promise<DebugContextFile | null>;
|
|
41
|
+
/** An error the host's error boundary caught (`componentStacks`). */
|
|
42
|
+
reportError(error: unknown, componentStack?: string | null): void;
|
|
43
|
+
/** Time spent in this module's hooks, for measuring the hot path. */
|
|
44
|
+
stats(): DebugCaptureStats;
|
|
45
|
+
stop(): void;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type DebugCaptureEnv = {
|
|
49
|
+
/** Requests that are the toolbar's own (the comment service): never recorded. */
|
|
50
|
+
ignoreUrl?: (url: string) => boolean;
|
|
51
|
+
window?: Window & typeof globalThis;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** The text of a body as far as `max` characters, without reading the rest. */
|
|
55
|
+
async function readCapped(response: Response, max: number): Promise<string> {
|
|
56
|
+
const reader = response.body?.getReader();
|
|
57
|
+
if (!reader) return "";
|
|
58
|
+
const decoder = new TextDecoder();
|
|
59
|
+
let text = "";
|
|
60
|
+
let complete = false;
|
|
61
|
+
try {
|
|
62
|
+
while (text.length < max) {
|
|
63
|
+
const { done, value } = await reader.read();
|
|
64
|
+
if (done) {
|
|
65
|
+
complete = true;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
text += decoder.decode(value, { stream: true });
|
|
69
|
+
}
|
|
70
|
+
} finally {
|
|
71
|
+
if (!complete) void reader.cancel().catch(() => undefined);
|
|
72
|
+
}
|
|
73
|
+
return text.slice(0, max);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const SKELETON_ATTRS =
|
|
77
|
+
/^(?:id|class|role|type|name|for|tabindex|disabled|hidden|href|src|data-testid|data-test|data-cs-cmp|data-cs-file|aria-[a-z-]+)$/;
|
|
78
|
+
const SKIPPED_TAGS = new Set(["SCRIPT", "STYLE", "NOSCRIPT", "TEMPLATE"]);
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The element as HTML with its structure and the attributes that identify things, and none of its text or
|
|
82
|
+
* values: each text run becomes `[text:N]`. `data-cs-private` subtrees are left out.
|
|
83
|
+
*/
|
|
84
|
+
function skeleton(element: Element, budget: number, base: string, tally: RedactionTally): string {
|
|
85
|
+
let out = "";
|
|
86
|
+
const visit = (node: Element, depth: number) => {
|
|
87
|
+
if (out.length > budget) return;
|
|
88
|
+
const tag = node.tagName.toLowerCase();
|
|
89
|
+
if (SKIPPED_TAGS.has(node.tagName)) return;
|
|
90
|
+
const attrs: string[] = [];
|
|
91
|
+
for (const attr of [...node.attributes].slice(0, 20)) {
|
|
92
|
+
if (!SKELETON_ATTRS.test(attr.name)) continue;
|
|
93
|
+
const value =
|
|
94
|
+
attr.name === "href" || attr.name === "src"
|
|
95
|
+
? scrubUrl(attr.value, 160, base, tally)
|
|
96
|
+
: scrubText(attr.value, 120, tally);
|
|
97
|
+
attrs.push(`${attr.name}="${value.replace(/"/g, """)}"`);
|
|
98
|
+
}
|
|
99
|
+
const open = `<${tag}${attrs.length ? ` ${attrs.join(" ")}` : ""}`;
|
|
100
|
+
if (node.hasAttribute("data-cs-private") || tag === "svg" || depth > 6) {
|
|
101
|
+
out += `${open} />`;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
out += `${open}>`;
|
|
105
|
+
let children = 0;
|
|
106
|
+
for (const child of node.childNodes) {
|
|
107
|
+
if (children++ > 20 || out.length > budget) {
|
|
108
|
+
out += "…";
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
if (child.nodeType === 3) {
|
|
112
|
+
const length = (child.textContent ?? "").trim().length;
|
|
113
|
+
if (length) out += `[text:${length}]`;
|
|
114
|
+
} else if (child.nodeType === 1) visit(child as Element, depth + 1);
|
|
115
|
+
}
|
|
116
|
+
out += `</${tag}>`;
|
|
117
|
+
};
|
|
118
|
+
visit(element, 0);
|
|
119
|
+
return out.length > budget ? `${out.slice(0, budget)}…` : out;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const STYLE_NAMES = [
|
|
123
|
+
"display",
|
|
124
|
+
"visibility",
|
|
125
|
+
"position",
|
|
126
|
+
"z-index",
|
|
127
|
+
"overflow",
|
|
128
|
+
"opacity",
|
|
129
|
+
"pointer-events",
|
|
130
|
+
"transform",
|
|
131
|
+
"width",
|
|
132
|
+
"height",
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
export function startDebugContext(
|
|
136
|
+
resolved: ResolvedDebugContext,
|
|
137
|
+
env: DebugCaptureEnv = {},
|
|
138
|
+
): DebugCapture {
|
|
139
|
+
const win = env.window ?? window;
|
|
140
|
+
const ignoreUrl = env.ignoreUrl ?? (() => false);
|
|
141
|
+
const core = createDebugCore(resolved, {
|
|
142
|
+
pageUrl: () => win.location.href,
|
|
143
|
+
ignoreUrl,
|
|
144
|
+
clock: () => win.performance.now(),
|
|
145
|
+
});
|
|
146
|
+
const { signals, options, timed } = core;
|
|
147
|
+
const restores: Array<() => void> = [];
|
|
148
|
+
const listeners: Array<() => void> = [];
|
|
149
|
+
const perf = { longTasks: 0, longestTaskMs: 0, inpMs: 0, lcpMs: 0, cls: 0 };
|
|
150
|
+
|
|
151
|
+
// ------------------------------------------------------------ console
|
|
152
|
+
const consoleObject = win.console;
|
|
153
|
+
const levels: Array<"error" | "warn" | "log" | "info" | "debug"> = [];
|
|
154
|
+
if (signals.errors) levels.push("error", "warn");
|
|
155
|
+
if (signals.consoleLog) levels.push("log", "info", "debug");
|
|
156
|
+
for (const level of levels) {
|
|
157
|
+
const original = consoleObject[level];
|
|
158
|
+
if (typeof original !== "function") continue;
|
|
159
|
+
const wrapped = function (this: unknown, ...args: unknown[]) {
|
|
160
|
+
if (!core.stopped) timed(() => core.consoleCall(level, args));
|
|
161
|
+
return original.apply(this ?? consoleObject, args);
|
|
162
|
+
};
|
|
163
|
+
restores.push(patch(consoleObject, level, wrapped as Console[typeof level]));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const listen = <K extends keyof WindowEventMap>(
|
|
167
|
+
type: K,
|
|
168
|
+
handler: (event: WindowEventMap[K]) => void,
|
|
169
|
+
capture = false,
|
|
170
|
+
) => {
|
|
171
|
+
const wrapped = (event: WindowEventMap[K]) => timed(() => handler(event));
|
|
172
|
+
win.addEventListener(type, wrapped as EventListener, capture);
|
|
173
|
+
listeners.push(() => win.removeEventListener(type, wrapped as EventListener, capture));
|
|
174
|
+
};
|
|
175
|
+
const listenDocument = <K extends keyof DocumentEventMap>(
|
|
176
|
+
type: K,
|
|
177
|
+
handler: (event: DocumentEventMap[K]) => void,
|
|
178
|
+
) => {
|
|
179
|
+
const wrapped = (event: DocumentEventMap[K]) => timed(() => handler(event));
|
|
180
|
+
win.document.addEventListener(type, wrapped as EventListener);
|
|
181
|
+
listeners.push(() => win.document.removeEventListener(type, wrapped as EventListener));
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// ------------------------------------------------------------ errors, resources, CSP
|
|
185
|
+
if (signals.errors || signals.resources) {
|
|
186
|
+
listen(
|
|
187
|
+
"error",
|
|
188
|
+
(event) => {
|
|
189
|
+
const target = event.target;
|
|
190
|
+
if (target && target !== win && typeof (target as Element).tagName === "string") {
|
|
191
|
+
if (!signals.resources) return;
|
|
192
|
+
const element = target as HTMLElement & { src?: string; href?: string };
|
|
193
|
+
const url = element.src || element.href || "";
|
|
194
|
+
if (!url || ignoreUrl(url)) return;
|
|
195
|
+
core.pushError(
|
|
196
|
+
"resource",
|
|
197
|
+
"error",
|
|
198
|
+
`Failed to load <${element.tagName.toLowerCase()}> ${scrubUrl(url, 300, win.location.href, core.tally)}`,
|
|
199
|
+
);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (!signals.errors) return;
|
|
203
|
+
const { message, stack } =
|
|
204
|
+
event.error !== undefined && event.error !== null
|
|
205
|
+
? errorOf(event.error)
|
|
206
|
+
: { message: event.message, stack: undefined };
|
|
207
|
+
const source = event.filename
|
|
208
|
+
? `${event.filename}:${event.lineno}:${event.colno}`
|
|
209
|
+
: undefined;
|
|
210
|
+
if (source && FOREIGN_FRAME.test(source)) return;
|
|
211
|
+
if (!stack && /^Script error\.?$/.test(message || "")) return;
|
|
212
|
+
core.pushError("uncaught", "error", message || "error", stack, source);
|
|
213
|
+
},
|
|
214
|
+
true,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
if (signals.errors) {
|
|
218
|
+
listen("unhandledrejection", (event) => {
|
|
219
|
+
const reason = event.reason as unknown;
|
|
220
|
+
if (reason instanceof DOMException && reason.name === "AbortError") return;
|
|
221
|
+
const { message, stack } = errorOf(reason);
|
|
222
|
+
core.pushError("rejection", "error", message, stack);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
if (signals.resources) {
|
|
226
|
+
listenDocument("securitypolicyviolation", (event) => {
|
|
227
|
+
if (ignoreUrl(event.blockedURI)) return;
|
|
228
|
+
const blocked = event.blockedURI
|
|
229
|
+
? scrubUrl(event.blockedURI, 200, win.location.href, core.tally)
|
|
230
|
+
: "inline";
|
|
231
|
+
core.pushError(
|
|
232
|
+
"csp",
|
|
233
|
+
"error",
|
|
234
|
+
`CSP blocked ${blocked} (${event.effectiveDirective || event.violatedDirective})`,
|
|
235
|
+
undefined,
|
|
236
|
+
event.sourceFile
|
|
237
|
+
? `${event.sourceFile}:${event.lineNumber}:${event.columnNumber}`
|
|
238
|
+
: undefined,
|
|
239
|
+
);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ------------------------------------------------------------ lifecycle
|
|
244
|
+
if (signals.lifecycle) {
|
|
245
|
+
listen("online", () => core.noteLifecycle("online"));
|
|
246
|
+
listen("offline", () => core.noteLifecycle("offline"));
|
|
247
|
+
listenDocument("visibilitychange", () =>
|
|
248
|
+
core.noteLifecycle(win.document.visibilityState === "hidden" ? "hidden" : "visible"),
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ------------------------------------------------------------ network
|
|
253
|
+
if (signals.network) {
|
|
254
|
+
const host = win as unknown as { fetch?: typeof fetch };
|
|
255
|
+
const originalFetch = host.fetch;
|
|
256
|
+
if (typeof originalFetch === "function") {
|
|
257
|
+
const wrappedFetch = function (this: unknown, input: RequestInfo | URL, init?: RequestInit) {
|
|
258
|
+
if (core.stopped) return originalFetch.call(win, input, init);
|
|
259
|
+
const url =
|
|
260
|
+
typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
261
|
+
const id = timed(() => {
|
|
262
|
+
const method =
|
|
263
|
+
init?.method ?? (typeof input === "object" && "method" in input ? input.method : "GET");
|
|
264
|
+
return core.requestStarted(method, url);
|
|
265
|
+
});
|
|
266
|
+
const call = originalFetch.call(win, input, init) as Promise<Response>;
|
|
267
|
+
if (id === null) return call;
|
|
268
|
+
return call.then(
|
|
269
|
+
(response) => {
|
|
270
|
+
timed(() => {
|
|
271
|
+
const failed = response.status >= 400;
|
|
272
|
+
// A copy is made only when its text may be read: a failed response, or a GraphQL answer that
|
|
273
|
+
// may hold errors behind a 200. It has to exist before the app reads the body.
|
|
274
|
+
let copy: Response | null = null;
|
|
275
|
+
if (
|
|
276
|
+
(failed && (signals.responseExcerpts || signals.bodies)) ||
|
|
277
|
+
(!failed && /graphql/i.test(url))
|
|
278
|
+
) {
|
|
279
|
+
try {
|
|
280
|
+
copy = response.clone();
|
|
281
|
+
} catch {
|
|
282
|
+
copy = null;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
// One read of the one copy, however often it is asked for: an unread copy would hold the whole
|
|
286
|
+
// response in memory.
|
|
287
|
+
let read: Promise<string> | null = null;
|
|
288
|
+
const source = copy;
|
|
289
|
+
core.requestFinished(id, response.status, {
|
|
290
|
+
header: (name) => response.headers.get(name),
|
|
291
|
+
requestBody: init?.body,
|
|
292
|
+
graphqlReadable: source !== null,
|
|
293
|
+
...(source
|
|
294
|
+
? {
|
|
295
|
+
text: (max: number) => {
|
|
296
|
+
read ??= readCapped(source, Math.max(max, signals.bodies ? BODY_CHARS : 0));
|
|
297
|
+
return read.then((text) => text.slice(0, max));
|
|
298
|
+
},
|
|
299
|
+
}
|
|
300
|
+
: {}),
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
return response;
|
|
304
|
+
},
|
|
305
|
+
(caught: unknown) => {
|
|
306
|
+
timed(() => {
|
|
307
|
+
if (caught instanceof DOMException && caught.name === "AbortError")
|
|
308
|
+
core.requestAborted(id);
|
|
309
|
+
else core.requestFinished(id, 0, { header: () => null, requestBody: init?.body });
|
|
310
|
+
});
|
|
311
|
+
throw caught;
|
|
312
|
+
},
|
|
313
|
+
);
|
|
314
|
+
};
|
|
315
|
+
restores.push(patch(host, "fetch", wrappedFetch as typeof fetch));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const Xhr = (win as unknown as { XMLHttpRequest?: typeof XMLHttpRequest }).XMLHttpRequest;
|
|
319
|
+
if (Xhr) {
|
|
320
|
+
const opened = new WeakMap<XMLHttpRequest, { method: string; url: string }>();
|
|
321
|
+
const proto = Xhr.prototype;
|
|
322
|
+
const originalOpen = proto.open;
|
|
323
|
+
const originalSend = proto.send;
|
|
324
|
+
const open = function (
|
|
325
|
+
this: XMLHttpRequest,
|
|
326
|
+
method: string,
|
|
327
|
+
url: string | URL,
|
|
328
|
+
...rest: unknown[]
|
|
329
|
+
) {
|
|
330
|
+
if (!core.stopped) timed(() => opened.set(this, { method, url: String(url) }));
|
|
331
|
+
return (originalOpen as (...args: unknown[]) => void).call(this, method, url, ...rest);
|
|
332
|
+
};
|
|
333
|
+
const send = function (
|
|
334
|
+
this: XMLHttpRequest,
|
|
335
|
+
body?: Document | XMLHttpRequestBodyInit | null,
|
|
336
|
+
) {
|
|
337
|
+
timed(() => {
|
|
338
|
+
const info = opened.get(this);
|
|
339
|
+
if (!info || core.stopped) return;
|
|
340
|
+
const id = core.requestStarted(info.method, new URL(info.url, win.location.href).href);
|
|
341
|
+
if (id === null) return;
|
|
342
|
+
let wasAborted = false;
|
|
343
|
+
this.addEventListener("abort", () => {
|
|
344
|
+
wasAborted = true;
|
|
345
|
+
});
|
|
346
|
+
this.addEventListener("loadend", () =>
|
|
347
|
+
timed(() => {
|
|
348
|
+
if (wasAborted) {
|
|
349
|
+
core.requestAborted(id);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const text = () => {
|
|
353
|
+
try {
|
|
354
|
+
if (this.responseType === "" || this.responseType === "text")
|
|
355
|
+
return this.responseText;
|
|
356
|
+
if (this.responseType === "json") return JSON.stringify(this.response);
|
|
357
|
+
} catch {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
return null;
|
|
361
|
+
};
|
|
362
|
+
core.requestFinished(id, this.status, {
|
|
363
|
+
header: (name) => {
|
|
364
|
+
try {
|
|
365
|
+
return this.getResponseHeader(name);
|
|
366
|
+
} catch {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
requestBody: typeof body === "string" ? body : undefined,
|
|
371
|
+
graphqlReadable: true,
|
|
372
|
+
text: async (max) => text()?.slice(0, Math.min(max, GRAPHQL_READ_CHARS)) ?? null,
|
|
373
|
+
});
|
|
374
|
+
}),
|
|
375
|
+
);
|
|
376
|
+
});
|
|
377
|
+
return originalSend.call(this, body);
|
|
378
|
+
};
|
|
379
|
+
restores.push(patch(proto, "open", open as typeof proto.open));
|
|
380
|
+
restores.push(patch(proto, "send", send as typeof proto.send));
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ------------------------------------------------------------ performance
|
|
385
|
+
const observers: PerformanceObserver[] = [];
|
|
386
|
+
if (signals.performance && typeof PerformanceObserver === "function") {
|
|
387
|
+
const observe = (
|
|
388
|
+
type: string,
|
|
389
|
+
onEntry: (entry: PerformanceEntry) => void,
|
|
390
|
+
/** `durationThreshold` for `event` entries, which the DOM typings do not list yet. */
|
|
391
|
+
extra: { durationThreshold?: number } = {},
|
|
392
|
+
) => {
|
|
393
|
+
try {
|
|
394
|
+
const observer = new PerformanceObserver((list) =>
|
|
395
|
+
timed(() => list.getEntries().forEach(onEntry)),
|
|
396
|
+
);
|
|
397
|
+
observer.observe({ type, buffered: true, ...extra } as PerformanceObserverInit);
|
|
398
|
+
observers.push(observer);
|
|
399
|
+
} catch {
|
|
400
|
+
// This browser does not report that entry type.
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
observe("longtask", (entry) => {
|
|
404
|
+
perf.longTasks += 1;
|
|
405
|
+
perf.longestTaskMs = Math.max(perf.longestTaskMs, entry.duration);
|
|
406
|
+
});
|
|
407
|
+
observe("largest-contentful-paint", (entry) => {
|
|
408
|
+
perf.lcpMs = entry.startTime;
|
|
409
|
+
});
|
|
410
|
+
observe("layout-shift", (entry) => {
|
|
411
|
+
const shift = entry as PerformanceEntry & { value?: number; hadRecentInput?: boolean };
|
|
412
|
+
if (!shift.hadRecentInput) perf.cls += shift.value ?? 0;
|
|
413
|
+
});
|
|
414
|
+
observe(
|
|
415
|
+
"event",
|
|
416
|
+
(entry) => {
|
|
417
|
+
const event = entry as PerformanceEntry & { interactionId?: number };
|
|
418
|
+
if (event.interactionId) perf.inpMs = Math.max(perf.inpMs, entry.duration);
|
|
419
|
+
},
|
|
420
|
+
{ durationThreshold: 40 },
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ------------------------------------------------------------ the page's own sections
|
|
425
|
+
const storage = async (fileTally: RedactionTally): Promise<DebugContextFile["storage"]> => {
|
|
426
|
+
const allowed = new Set(options.storageValueKeys ?? []);
|
|
427
|
+
const read = (area: Storage | null) => {
|
|
428
|
+
if (!area) return undefined;
|
|
429
|
+
const out: { key: string; bytes: number; value?: string }[] = [];
|
|
430
|
+
try {
|
|
431
|
+
for (let index = 0; index < area.length && out.length < 100; index += 1) {
|
|
432
|
+
const key = area.key(index);
|
|
433
|
+
if (key === null) continue;
|
|
434
|
+
const value = area.getItem(key) ?? "";
|
|
435
|
+
out.push({
|
|
436
|
+
key: scrubText(key, 120, fileTally),
|
|
437
|
+
bytes: (key.length + value.length) * 2,
|
|
438
|
+
...(allowed.has(key) ? { value: excerptOf(value, 500, fileTally) } : {}),
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
} catch {
|
|
442
|
+
return undefined;
|
|
443
|
+
}
|
|
444
|
+
return out.length ? out : undefined;
|
|
445
|
+
};
|
|
446
|
+
let local: Storage | null = null;
|
|
447
|
+
let session: Storage | null = null;
|
|
448
|
+
try {
|
|
449
|
+
local = win.localStorage;
|
|
450
|
+
session = win.sessionStorage;
|
|
451
|
+
} catch {
|
|
452
|
+
// Storage is blocked (sandboxed frame, privacy mode).
|
|
453
|
+
}
|
|
454
|
+
const cookies = win.document.cookie
|
|
455
|
+
.split(";")
|
|
456
|
+
.map((part) => part.split("=")[0]!.trim())
|
|
457
|
+
.filter(Boolean)
|
|
458
|
+
.slice(0, 50)
|
|
459
|
+
.map((name) => scrubText(name, 80, fileTally));
|
|
460
|
+
const factory = win.indexedDB as
|
|
461
|
+
| (IDBFactory & { databases?: () => Promise<Array<{ name?: string }>> })
|
|
462
|
+
| undefined;
|
|
463
|
+
const databases =
|
|
464
|
+
typeof factory?.databases === "function" ? await within(factory.databases(), 200) : undefined;
|
|
465
|
+
const indexedDb = (databases ?? [])
|
|
466
|
+
.flatMap((db) => (db.name ? [scrubText(db.name, 80, fileTally)] : []))
|
|
467
|
+
.slice(0, 30);
|
|
468
|
+
const localKeys = read(local);
|
|
469
|
+
const sessionKeys = read(session);
|
|
470
|
+
return {
|
|
471
|
+
...(localKeys ? { local: localKeys } : {}),
|
|
472
|
+
...(sessionKeys ? { session: sessionKeys } : {}),
|
|
473
|
+
...(cookies.length ? { cookies } : {}),
|
|
474
|
+
...(indexedDb.length ? { indexedDb } : {}),
|
|
475
|
+
};
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
const snapshot = async (input: DebugSnapshotInput = {}): Promise<DebugContextFile | null> => {
|
|
479
|
+
const reportAt = core.takeMark();
|
|
480
|
+
const fileTally = newTally();
|
|
481
|
+
const shared = await core.shared(reportAt, input.steps, fileTally, runtimeBuild);
|
|
482
|
+
const { join, routePattern, ...sections } = shared;
|
|
483
|
+
const file: DebugContextFile = { ...core.skeletonFile(reportAt, "web"), ...sections };
|
|
484
|
+
file.join = { ...file.join, ...join };
|
|
485
|
+
const nav = win.navigator;
|
|
486
|
+
|
|
487
|
+
if (signals.page) {
|
|
488
|
+
const navigations = input.steps?.filter((step) => step.type === "navigate").length ?? 0;
|
|
489
|
+
file.page = {
|
|
490
|
+
url: scrubUrl(win.location.href, 1000, undefined, fileTally),
|
|
491
|
+
route: input.anchor?.route ?? win.location.pathname,
|
|
492
|
+
...(routePattern ? { routePattern } : {}),
|
|
493
|
+
// Titles often name a record ("Invoice for …"); a customer's report leaves them out.
|
|
494
|
+
...(resolved.audience === "reviewer" && win.document.title
|
|
495
|
+
? { title: scrubText(win.document.title, 160, fileTally) }
|
|
496
|
+
: {}),
|
|
497
|
+
...(win.document.referrer
|
|
498
|
+
? { referrer: scrubUrl(win.document.referrer, 300, undefined, fileTally) }
|
|
499
|
+
: {}),
|
|
500
|
+
loadedAgoMs: Math.max(0, Math.round(reportAt - win.performance.timeOrigin)),
|
|
501
|
+
softNavigations: Math.max(0, navigations - 1),
|
|
502
|
+
visibility: win.document.visibilityState,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (signals.environment) {
|
|
507
|
+
const agent = nav.userAgent;
|
|
508
|
+
const media = (query: string) => {
|
|
509
|
+
try {
|
|
510
|
+
return win.matchMedia(query).matches;
|
|
511
|
+
} catch {
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
const connection = (nav as Navigator & { connection?: { effectiveType?: string } }).connection
|
|
516
|
+
?.effectiveType;
|
|
517
|
+
const browser = browserName(agent);
|
|
518
|
+
const os = systemName(agent);
|
|
519
|
+
file.client = {
|
|
520
|
+
...(browser ? { browser } : {}),
|
|
521
|
+
...(os ? { os } : {}),
|
|
522
|
+
device: /iPad|Tablet/i.test(agent)
|
|
523
|
+
? "tablet"
|
|
524
|
+
: /Mobi|iPhone|Android/i.test(agent)
|
|
525
|
+
? "mobile"
|
|
526
|
+
: "desktop",
|
|
527
|
+
viewport: [win.document.documentElement.clientWidth, win.innerHeight],
|
|
528
|
+
dpr: win.devicePixelRatio || 1,
|
|
529
|
+
...(win.visualViewport && win.visualViewport.scale !== 1
|
|
530
|
+
? { zoom: win.visualViewport.scale }
|
|
531
|
+
: {}),
|
|
532
|
+
locale: nav.language,
|
|
533
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
534
|
+
colorScheme: media("(prefers-color-scheme: dark)") ? "dark" : "light",
|
|
535
|
+
reducedMotion: media("(prefers-reduced-motion: reduce)"),
|
|
536
|
+
online: nav.onLine,
|
|
537
|
+
...(connection ? { connection } : {}),
|
|
538
|
+
touch: (nav.maxTouchPoints ?? 0) > 0,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (signals.target && (input.anchor || input.element)) {
|
|
543
|
+
const anchor = input.anchor;
|
|
544
|
+
file.target = {
|
|
545
|
+
...(anchor?.component?.name ? { component: anchor.component.name } : {}),
|
|
546
|
+
...(anchor?.component?.file ? { file: anchor.component.file } : {}),
|
|
547
|
+
...(anchor?.stack?.length ? { stack: anchor.stack } : {}),
|
|
548
|
+
...(anchor?.role ? { role: anchor.role } : {}),
|
|
549
|
+
...(anchor?.accName ? { name: anchor.accName } : {}),
|
|
550
|
+
...(anchor?.tag
|
|
551
|
+
? { tag: anchor.tag }
|
|
552
|
+
: input.element
|
|
553
|
+
? { tag: input.element.tagName.toLowerCase() }
|
|
554
|
+
: {}),
|
|
555
|
+
};
|
|
556
|
+
if (signals.domExcerpt && input.element?.isConnected) {
|
|
557
|
+
const element = input.element;
|
|
558
|
+
file.target.html = skeleton(element, 4096, win.location.href, fileTally);
|
|
559
|
+
const rect = element.getBoundingClientRect();
|
|
560
|
+
file.target.box = [
|
|
561
|
+
Math.round(rect.left),
|
|
562
|
+
Math.round(rect.top),
|
|
563
|
+
Math.round(rect.width),
|
|
564
|
+
Math.round(rect.height),
|
|
565
|
+
];
|
|
566
|
+
const computed = win.getComputedStyle(element);
|
|
567
|
+
file.target.styles = Object.fromEntries(
|
|
568
|
+
STYLE_NAMES.map((name) => [name, computed.getPropertyValue(name)]),
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (signals.performance) {
|
|
574
|
+
const navigation = win.performance.getEntriesByType?.("navigation")[0] as
|
|
575
|
+
| PerformanceNavigationTiming
|
|
576
|
+
| undefined;
|
|
577
|
+
const memory = (win.performance as Performance & { memory?: { usedJSHeapSize?: number } })
|
|
578
|
+
.memory;
|
|
579
|
+
file.performance = {
|
|
580
|
+
longTasks: perf.longTasks,
|
|
581
|
+
...(perf.longestTaskMs ? { longestTaskMs: Math.round(perf.longestTaskMs) } : {}),
|
|
582
|
+
...(perf.inpMs ? { inpMs: Math.round(perf.inpMs) } : {}),
|
|
583
|
+
...(perf.lcpMs ? { lcpMs: Math.round(perf.lcpMs) } : {}),
|
|
584
|
+
cls: Math.round(perf.cls * 1000) / 1000,
|
|
585
|
+
...(navigation?.loadEventEnd ? { loadMs: Math.round(navigation.loadEventEnd) } : {}),
|
|
586
|
+
...(memory?.usedJSHeapSize
|
|
587
|
+
? { heapMb: Math.round(memory.usedJSHeapSize / 1_048_576) }
|
|
588
|
+
: {}),
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
if (signals.storage) file.storage = await storage(fileTally);
|
|
592
|
+
return core.finish(file, fileTally);
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
return {
|
|
596
|
+
mark: core.mark,
|
|
597
|
+
snapshot: async (input) => {
|
|
598
|
+
try {
|
|
599
|
+
return await snapshot(input);
|
|
600
|
+
} finally {
|
|
601
|
+
core.clearMark();
|
|
602
|
+
}
|
|
603
|
+
},
|
|
604
|
+
reportError: core.reportError,
|
|
605
|
+
stats: core.stats,
|
|
606
|
+
stop() {
|
|
607
|
+
core.stop();
|
|
608
|
+
for (const restore of restores.splice(0).reverse()) restore();
|
|
609
|
+
for (const remove of listeners.splice(0)) remove();
|
|
610
|
+
for (const observer of observers.splice(0)) observer.disconnect();
|
|
611
|
+
},
|
|
612
|
+
};
|
|
613
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Role and custom field values on a comment: what is turned on, and what a post sends. DOM-free. */
|
|
2
|
+
import type { Details, Settings } from "./api.js";
|
|
3
|
+
|
|
4
|
+
export const noDetails: Details = { role: null, fields: {} };
|
|
5
|
+
|
|
6
|
+
export function detailsEnabled(settings: Settings | null) {
|
|
7
|
+
if (!settings) return false;
|
|
8
|
+
return (
|
|
9
|
+
(settings.roles.enabled && settings.roles.options.length > 0) ||
|
|
10
|
+
(settings.fields.enabled && settings.fields.items.length > 0)
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function hasFieldValues(details: Details) {
|
|
15
|
+
return Object.values(details.fields).some((value) => value.trim());
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Only sends what is turned on, so a disabled feature never rejects a post. */
|
|
19
|
+
export function detailsPayload(settings: Settings | null, details: Details): Partial<Details> {
|
|
20
|
+
if (!settings) return {};
|
|
21
|
+
const payload: Partial<Details> = {};
|
|
22
|
+
if (settings.roles.enabled && details.role) payload.role = details.role;
|
|
23
|
+
if (settings.fields.enabled) {
|
|
24
|
+
const fields = Object.fromEntries(
|
|
25
|
+
Object.entries(details.fields).filter(([, value]) => value.trim()),
|
|
26
|
+
);
|
|
27
|
+
if (Object.keys(fields).length) payload.fields = fields;
|
|
28
|
+
}
|
|
29
|
+
return payload;
|
|
30
|
+
}
|