@dev.fast/whiteboard 0.0.0-stage → 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 -0
- package/README.md +146 -2
- package/THIRD_PARTY_NOTICES.md +93 -0
- package/dist/account-alias-DOHN1RSH.js +973 -0
- package/dist/agent-cli-ChOq0ZuO.js +117 -0
- package/dist/agent-cli-DzWfZr6G.js +2 -0
- package/dist/agent-client-CPQo7iTI.js +305 -0
- package/dist/authoring-tools-Cr6Kpjsx.js +234 -0
- package/dist/build-info.json +1 -0
- package/dist/cli-hVgHcdsv.js +47 -0
- package/dist/cli-runner-D8y2_luU.js +5095 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +128 -0
- package/dist/client-DFoh50W3.js +950 -0
- package/dist/desktop-discovery-DBO-PM2V.js +164 -0
- package/dist/error-message-OtiDonty.js +6 -0
- package/dist/fs-utils-BMPLt0cr.js +22 -0
- package/dist/fuzzy-match-BoAmcyak.js +30 -0
- package/dist/headless-host-BJxf4dUU.js +94 -0
- package/dist/input-error-OLgB_h31.js +11 -0
- package/dist/local-data-CxLk75rx.js +4491 -0
- package/dist/mcp-QeF8vkdf.js +140 -0
- package/dist/package-paths-B6-zxvIO.js +47 -0
- package/dist/process-error-telemetry-v9e5h6D7.js +3952 -0
- package/dist/profile-4ry2f3AM.js +76 -0
- package/dist/profile-Bh5FIrmf.js +2 -0
- package/dist/request-origin-D_4QIdoN.js +811 -0
- package/dist/review-agent-traces-DNJFYzc8.js +3866 -0
- package/dist/review-home-paths-6zZH-1c9.js +2560 -0
- package/dist/review-telemetry-DEFnQsQ2.js +1195 -0
- package/dist/runtime-CKiWhGrf.js +52 -0
- package/dist/runtime.d.ts +9 -0
- package/dist/runtime.js +2 -0
- package/dist/s3-SzLpeCG6.js +345 -0
- package/dist/s3-config-BgxkSoOy.js +2 -0
- package/dist/s3-config-CAxhO9u_.js +676 -0
- package/dist/server/desktop-host.d.ts +4 -0
- package/dist/server/desktop-host.js +1702 -0
- package/dist/server-discovery-Dbzn3w6Z.js +66 -0
- package/dist/sharing/index.d.ts +1977 -0
- package/dist/sharing/index.js +2 -0
- package/dist/src-CGP5ytbV.js +283 -0
- package/dist/src-CmdiBL20.js +3676 -0
- package/dist/src-DnwdaQ2r.js +865 -0
- package/dist/src-X9phtB2j.js +68 -0
- package/dist/stored-document-migration-DHeMpLMb.js +2671 -0
- package/dist/tool-failure-C8zB73HV.js +98 -0
- package/dist/tutorial-trace-DrBvkaeL.js +43 -0
- package/instructions/authoring.md +36 -0
- package/instructions/file-lenses.md +13 -0
- package/instructions/scratchpad.md +23 -0
- package/instructions/trace-archaeology.md +113 -0
- package/onboarding.md +8 -0
- package/package.json +106 -3
- package/src/agent-selection.ts +100 -0
- package/src/agent-session-ref.ts +80 -0
- package/src/ask/agents.ts +351 -0
- package/src/ask/checkout-files.ts +47 -0
- package/src/ask/file-refs.ts +111 -0
- package/src/ask/pi-mcp.ts +154 -0
- package/src/ask/protocol.ts +244 -0
- package/src/ask/thread-state.ts +350 -0
- package/src/ask/thread.ts +1400 -0
- package/src/ask/threads.ts +219 -0
- package/src/ask/watch.ts +79 -0
- package/src/cli-install.ts +1041 -0
- package/src/cli-runner.ts +1461 -0
- package/src/cli-runtime-info.ts +35 -0
- package/src/cli.ts +222 -0
- package/src/connect-prompts.ts +248 -0
- package/src/cursor-deeplink.ts +11 -0
- package/src/desktop-discovery.ts +355 -0
- package/src/diff-selection-migration.ts +63 -0
- package/src/embedded-posthog-key.ts +6 -0
- package/src/error-telemetry.ts +247 -0
- package/src/evidence.ts +14 -0
- package/src/exception-telemetry.ts +126 -0
- package/src/fixtures/blocks/call_stack_diff.json +27 -0
- package/src/fixtures/blocks/callout.json +11 -0
- package/src/fixtures/blocks/code.json +9 -0
- package/src/fixtures/blocks/code_peek.json +8 -0
- package/src/fixtures/blocks/database_lens.json +51 -0
- package/src/fixtures/blocks/divider.json +1 -0
- package/src/fixtures/blocks/fixtures.ts +33 -0
- package/src/fixtures/blocks/flow_diagram.json +36 -0
- package/src/fixtures/blocks/ids.ts +8 -0
- package/src/fixtures/blocks/image.json +9 -0
- package/src/fixtures/blocks/markdown.json +7 -0
- package/src/fixtures/blocks/section.json +11 -0
- package/src/fixtures/blocks/sequence.json +31 -0
- package/src/fixtures/blocks/software_map.json +7 -0
- package/src/fixtures/blocks/trace_quote.json +9 -0
- package/src/fixtures/blocks/tutorial.json +51 -0
- package/src/fs-utils.ts +32 -0
- package/src/fuzzy-match.ts +94 -0
- package/src/install.ts +29 -0
- package/src/legacy-skills.ts +133 -0
- package/src/lens-selection.ts +230 -0
- package/src/markdown-latex-math.ts +230 -0
- package/src/markdown.ts +81 -0
- package/src/package-paths.ts +53 -0
- package/src/posthog-capture-client.ts +610 -0
- package/src/review-api/README.md +295 -0
- package/src/review-api/activity.ts +340 -0
- package/src/review-api/agent-cli.ts +219 -0
- package/src/review-api/agent-client.ts +188 -0
- package/src/review-api/anchor-quotes.ts +88 -0
- package/src/review-api/ask-history.ts +242 -0
- package/src/review-api/authoring-tools.ts +211 -0
- package/src/review-api/blocks/call_stack_diff.ts +75 -0
- package/src/review-api/blocks/callout.ts +24 -0
- package/src/review-api/blocks/code.ts +9 -0
- package/src/review-api/blocks/code_peek.ts +13 -0
- package/src/review-api/blocks/database_lens.ts +150 -0
- package/src/review-api/blocks/definition.ts +40 -0
- package/src/review-api/blocks/divider.ts +6 -0
- package/src/review-api/blocks/flow_diagram.ts +111 -0
- package/src/review-api/blocks/image.ts +10 -0
- package/src/review-api/blocks/index.ts +90 -0
- package/src/review-api/blocks/markdown.ts +17 -0
- package/src/review-api/blocks/section.ts +24 -0
- package/src/review-api/blocks/sequence.ts +56 -0
- package/src/review-api/blocks/software_map.ts +9 -0
- package/src/review-api/blocks/trace_quote.ts +10 -0
- package/src/review-api/blocks/tutorial.ts +39 -0
- package/src/review-api/checkout-fs.ts +14 -0
- package/src/review-api/client.ts +1 -0
- package/src/review-api/comparison-coverage.ts +304 -0
- package/src/review-api/component-reference.ts +30 -0
- package/src/review-api/diff-lenses.ts +175 -0
- package/src/review-api/document-headings.ts +51 -0
- package/src/review-api/document-text.ts +200 -0
- package/src/review-api/document.ts +906 -0
- package/src/review-api/file-lenses.ts +100 -0
- package/src/review-api/http.ts +1877 -0
- package/src/review-api/image-decode.ts +29 -0
- package/src/review-api/input-error.ts +9 -0
- package/src/review-api/instructions.ts +89 -0
- package/src/review-api/lens-alignment.ts +52 -0
- package/src/review-api/local-data.ts +1837 -0
- package/src/review-api/map-input.ts +154 -0
- package/src/review-api/mcp-client-agent.ts +32 -0
- package/src/review-api/mcp.ts +240 -0
- package/src/review-api/origin.ts +43 -0
- package/src/review-api/profile.ts +146 -0
- package/src/review-api/public-tools.ts +102 -0
- package/src/review-api/pull-request.ts +389 -0
- package/src/review-api/read-schemas.ts +85 -0
- package/src/review-api/recovery.ts +2 -0
- package/src/review-api/request-origin.ts +33 -0
- package/src/review-api/review-progress.ts +382 -0
- package/src/review-api/status-tool.ts +10 -0
- package/src/review-api/store-schema.ts +22 -0
- package/src/review-api/store.ts +1647 -0
- package/src/review-api/tool-failure.ts +64 -0
- package/src/review-api/trace-schema.ts +13 -0
- package/src/review-api/traces.ts +119 -0
- package/src/review-api/unsupported-files.integration.ts +91 -0
- package/src/review-api/workspaces.ts +692 -0
- package/src/review-api/worktree-source.ts +170 -0
- package/src/review-api/worktree-structural.integration.ts +320 -0
- package/src/review-app-launcher.ts +432 -0
- package/src/review-app-picker.ts +168 -0
- package/src/review-app.ts +134 -0
- package/src/review-bundled-tools.ts +249 -0
- package/src/review-checkout-paths.ts +37 -0
- package/src/review-diff-files.ts +89 -0
- package/src/review-head-checkout.ts +300 -0
- package/src/review-home-paths.ts +73 -0
- package/src/review-info.ts +59 -0
- package/src/review-instances.ts +113 -0
- package/src/review-logger.ts +183 -0
- package/src/review-preferences.ts +88 -0
- package/src/review-prepare.ts +291 -0
- package/src/review-stack.ts +112 -0
- package/src/review-telemetry.ts +1066 -0
- package/src/runtime.ts +74 -0
- package/src/server/account-alias.ts +30 -0
- package/src/server/bounded-stream.ts +34 -0
- package/src/server/bug-report.ts +301 -0
- package/src/server/client-error-budget.ts +45 -0
- package/src/server/crash-report.ts +273 -0
- package/src/server/desktop-host-shutdown.ts +64 -0
- package/src/server/desktop-host.ts +190 -0
- package/src/server/desktop-server.ts +703 -0
- package/src/server/diffr-config.ts +509 -0
- package/src/server/diffr-languages.ts +97 -0
- package/src/server/global-verb-relay.ts +190 -0
- package/src/server/headless-host.ts +149 -0
- package/src/server/hono-http.ts +162 -0
- package/src/server/http-json.ts +24 -0
- package/src/server/json-review-reporting.ts +174 -0
- package/src/server/process-error-telemetry.ts +174 -0
- package/src/server/review-api-parsers.ts +90 -0
- package/src/server/review-lifecycle-telemetry.ts +73 -0
- package/src/server/review-open-watchdog.ts +46 -0
- package/src/server/review-server-core.ts +290 -0
- package/src/server/structural-comparisons.ts +153 -0
- package/src/server/structural-diff.ts +186 -0
- package/src/server/tutorial-service.ts +241 -0
- package/src/server/ui-telemetry.ts +198 -0
- package/src/server-discovery.ts +95 -0
- package/src/session-markers.ts +132 -0
- package/src/sharing/auth.ts +41 -0
- package/src/sharing/cli.ts +83 -0
- package/src/sharing/client.ts +334 -0
- package/src/sharing/export.ts +195 -0
- package/src/sharing/host.ts +316 -0
- package/src/sharing/import.ts +787 -0
- package/src/sharing/index.ts +16 -0
- package/src/sharing/repository.ts +145 -0
- package/src/sharing/routes.ts +34 -0
- package/src/slug.ts +23 -0
- package/src/software-map-diff-counts.ts +517 -0
- package/src/software-map-model.ts +1147 -0
- package/src/software-map-topology-diff.ts +260 -0
- package/src/source.ts +92 -0
- package/src/startup-trace.ts +232 -0
- package/src/stored-document-migration.ts +151 -0
- package/src/telemetry-clean-text.ts +257 -0
- package/src/telemetry-config.ts +298 -0
- package/src/telemetry-debug-sink.ts +38 -0
- package/src/telemetry.ts +13 -0
- package/src/trace-cli.ts +156 -0
- package/src/trace-storage-cli.ts +509 -0
- package/src/tutorial-conversation.ts +18 -0
- package/src/ui-telemetry-events.ts +765 -0
- package/src/unified-diff.ts +71 -0
- package/src/viewed-coverage.ts +259 -0
- package/src/windows-cli.ts +125 -0
- package/tutorial/document.json +275 -0
- package/tutorial/runtime-manifest.json +12 -0
- package/tutorial/sample-service/package.json +9 -0
- package/tutorial/sample-service/src/api/checkout-api.ts +17 -0
- package/tutorial/sample-service/src/app.ts +26 -0
- package/tutorial/sample-service/src/database/schema.ts +10 -0
- package/tutorial/sample-service/src/fulfillment/fulfillment-queue.ts +15 -0
- package/tutorial/sample-service/src/fulfillment/fulfillment-worker.ts +24 -0
- package/tutorial/sample-service/src/inventory/inventory-service.ts +11 -0
- package/tutorial/sample-service/src/orders/order-service.ts +34 -0
- package/tutorial/sample-service/src/orders/order.ts +21 -0
- package/tutorial/sample-service/src/orders/orders-repository.ts +24 -0
- package/tutorial/sample-service/src/payments/payment-gateway.ts +12 -0
- package/tutorial/sample-service/src/shipping/shipping-gateway.ts +15 -0
- package/tutorial/sample-service/tsconfig.json +12 -0
- package/tutorial/software-map.json +139 -0
- package/tutorial/trace.json +20 -0
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export async function resolveReviewRoot(
|
|
5
|
+
cwd: string,
|
|
6
|
+
execFile: typeof execFilePromise = execFilePromise,
|
|
7
|
+
): Promise<string> {
|
|
8
|
+
const initialCwd = path.resolve(cwd);
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
// cwd-based invocation: jj walks up from cwd like git does, while
|
|
12
|
+
// `-R <subdir>` fails for a subdirectory of a workspace and would
|
|
13
|
+
// silently fall back to git's cwd walk (the OUTER repo, for a
|
|
14
|
+
// non-colocated jj workspace nested in another git repo).
|
|
15
|
+
const { stdout } = await execFile("jj", ["root"], { cwd: initialCwd });
|
|
16
|
+
const root = stdout.trim();
|
|
17
|
+
|
|
18
|
+
if (root) return path.resolve(root);
|
|
19
|
+
} catch {
|
|
20
|
+
// Not a jj workspace, or jj is unavailable. Try Git below.
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const { stdout } = await execFile("git", [
|
|
25
|
+
"-C",
|
|
26
|
+
initialCwd,
|
|
27
|
+
"rev-parse",
|
|
28
|
+
"--show-toplevel",
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
const root = stdout.trim();
|
|
32
|
+
|
|
33
|
+
if (root) return path.resolve(root);
|
|
34
|
+
} catch {
|
|
35
|
+
// Non-VCS directories keep their original cwd for existing review behavior.
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return initialCwd;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function execFilePromise(
|
|
42
|
+
file: string,
|
|
43
|
+
args: string[],
|
|
44
|
+
options: { cwd?: string } = {},
|
|
45
|
+
): Promise<{ stdout: string }> {
|
|
46
|
+
return execFileUntraced(file, args, options);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function execFileUntraced(
|
|
50
|
+
file: string,
|
|
51
|
+
args: string[],
|
|
52
|
+
options: { cwd?: string } = {},
|
|
53
|
+
): Promise<{ stdout: string }> {
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
const child = spawn(file, args, {
|
|
56
|
+
cwd: options.cwd,
|
|
57
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
let stdout = "";
|
|
61
|
+
let stderr = "";
|
|
62
|
+
child.stdout?.on("data", (chunk) => {
|
|
63
|
+
stdout += String(chunk);
|
|
64
|
+
});
|
|
65
|
+
child.stderr?.on("data", (chunk) => {
|
|
66
|
+
stderr += String(chunk);
|
|
67
|
+
});
|
|
68
|
+
child.on("error", reject);
|
|
69
|
+
child.on("exit", (code) => {
|
|
70
|
+
if (code === 0) resolve({ stdout });
|
|
71
|
+
else reject(new Error(stderr.trim() || `${file} exited with ${code}`));
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { StoreClient, readStoreAuth } from "@dev.fast/trace-core";
|
|
2
|
+
import type { ReviewTelemetry } from "@review/review-telemetry";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* After a login, link this install to the signed-in account by a hash of its
|
|
6
|
+
* account id. Best effort and never throws: the login already succeeded.
|
|
7
|
+
*/
|
|
8
|
+
export async function aliasInstallationToAccount(
|
|
9
|
+
telemetry: Pick<ReviewTelemetry, "captureAccountAlias">,
|
|
10
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
11
|
+
fetchImpl?: typeof fetch,
|
|
12
|
+
): Promise<void> {
|
|
13
|
+
try {
|
|
14
|
+
const auth = await readStoreAuth(env);
|
|
15
|
+
|
|
16
|
+
if (!auth) return;
|
|
17
|
+
|
|
18
|
+
await telemetry.captureAccountAlias(async () => {
|
|
19
|
+
const session = await new StoreClient({
|
|
20
|
+
origin: auth.origin,
|
|
21
|
+
token: auth.token,
|
|
22
|
+
fetch: fetchImpl,
|
|
23
|
+
}).session();
|
|
24
|
+
|
|
25
|
+
return session.user.id;
|
|
26
|
+
});
|
|
27
|
+
} catch {
|
|
28
|
+
// No alias this time; the next login tries again.
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export class StreamLimitError extends Error {
|
|
2
|
+
constructor() {
|
|
3
|
+
super("Stream exceeds its byte limit.");
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export async function readBoundedStream(
|
|
8
|
+
stream: ReadableStream<Uint8Array>,
|
|
9
|
+
limit: number,
|
|
10
|
+
): Promise<Buffer> {
|
|
11
|
+
const reader = stream.getReader();
|
|
12
|
+
const parts: Uint8Array[] = [];
|
|
13
|
+
let size = 0;
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
for (;;) {
|
|
17
|
+
const { value, done } = await reader.read();
|
|
18
|
+
|
|
19
|
+
if (done) break;
|
|
20
|
+
size += value.byteLength;
|
|
21
|
+
|
|
22
|
+
if (size > limit) {
|
|
23
|
+
await reader.cancel().catch(() => {});
|
|
24
|
+
throw new StreamLimitError();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
parts.push(value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return Buffer.concat(parts, size);
|
|
31
|
+
} finally {
|
|
32
|
+
reader.releaseLock();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { gzipSync } from "node:zlib";
|
|
3
|
+
|
|
4
|
+
import { type JsonValue } from "@dev.fast/json";
|
|
5
|
+
import {
|
|
6
|
+
type ReviewBugReportMetaV2,
|
|
7
|
+
type ReviewBugReportRequest,
|
|
8
|
+
parseReviewBugReportResponse,
|
|
9
|
+
} from "@dev.fast/review-protocol";
|
|
10
|
+
import { readReviewPackageVersion } from "@review/package-paths";
|
|
11
|
+
import { type PostHogCaptureProperties } from "@review/posthog-capture-client";
|
|
12
|
+
import { type ReviewDiffFilesResult } from "@review/review-diff-files";
|
|
13
|
+
|
|
14
|
+
const BUG_REPORT_URL = "https://bug.dev.fast/api/v2/reports";
|
|
15
|
+
|
|
16
|
+
const MAX_PAYLOAD_BYTES = 10 * 1024 * 1024;
|
|
17
|
+
|
|
18
|
+
const UPSTREAM_TIMEOUT_MS = 20_000;
|
|
19
|
+
|
|
20
|
+
type AttachmentName = "review" | "map" | "diff";
|
|
21
|
+
|
|
22
|
+
type AttachmentError = {
|
|
23
|
+
attachment: AttachmentName;
|
|
24
|
+
error: "unavailable";
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export interface BugReportPayload {
|
|
28
|
+
schema_version: 4;
|
|
29
|
+
description: string;
|
|
30
|
+
screenshot?: { mime: "image/jpeg"; base64: string };
|
|
31
|
+
// File name to text. The document alone cannot render: every anchor lives in
|
|
32
|
+
// a sibling TypeScript module.
|
|
33
|
+
review?: Record<string, string>;
|
|
34
|
+
map?: string;
|
|
35
|
+
diff?: ReviewDiffFilesResult;
|
|
36
|
+
diagnostics: {
|
|
37
|
+
app_version: string;
|
|
38
|
+
cli_version: string;
|
|
39
|
+
platform: NodeJS.Platform;
|
|
40
|
+
app_session_id: string;
|
|
41
|
+
client_error_names: string[];
|
|
42
|
+
attachment_errors?: AttachmentError[];
|
|
43
|
+
// Review source files the report did not send, by name. Triage reads a
|
|
44
|
+
// missing module as a rendering bug unless the report says it dropped one.
|
|
45
|
+
review_omitted_files?: string[];
|
|
46
|
+
/** The telemetry envelope, so triage can tell channel, environment and surface apart. */
|
|
47
|
+
telemetry?: Record<string, JsonValue>;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class BugReportUpstreamError extends Error {
|
|
52
|
+
constructor(
|
|
53
|
+
readonly status: number,
|
|
54
|
+
message: string,
|
|
55
|
+
) {
|
|
56
|
+
super(message);
|
|
57
|
+
this.name = "BugReportUpstreamError";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface BugReportSource {
|
|
62
|
+
review(): Promise<{ files: Record<string, string>; omitted: string[] }>;
|
|
63
|
+
map(): Promise<string | null>;
|
|
64
|
+
diff(): Promise<ReviewDiffFilesResult>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function submitReviewBugReport(input: {
|
|
68
|
+
report: ReviewBugReportRequest;
|
|
69
|
+
clientErrorNames: string[];
|
|
70
|
+
fetchImpl?: typeof fetch;
|
|
71
|
+
source: BugReportSource;
|
|
72
|
+
telemetryEnvelope?: PostHogCaptureProperties;
|
|
73
|
+
}) {
|
|
74
|
+
const cliVersion = readReviewPackageVersion();
|
|
75
|
+
const attachmentErrors: AttachmentError[] = [];
|
|
76
|
+
|
|
77
|
+
const payload: BugReportPayload = {
|
|
78
|
+
schema_version: 4,
|
|
79
|
+
description: input.report.description,
|
|
80
|
+
diagnostics: {
|
|
81
|
+
app_version: input.report.app_version,
|
|
82
|
+
cli_version: cliVersion,
|
|
83
|
+
platform: process.platform,
|
|
84
|
+
app_session_id: input.report.app_session_id,
|
|
85
|
+
client_error_names: input.clientErrorNames.slice(-20),
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
if (input.telemetryEnvelope) {
|
|
90
|
+
payload.diagnostics.telemetry = Object.fromEntries(
|
|
91
|
+
Object.entries(input.telemetryEnvelope).filter(
|
|
92
|
+
(entry): entry is [string, JsonValue] => entry[1] !== undefined,
|
|
93
|
+
),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (input.report.screenshot) payload.screenshot = input.report.screenshot;
|
|
98
|
+
|
|
99
|
+
let reviewSource: Record<string, string> | undefined;
|
|
100
|
+
let omittedReviewFiles: string[] | undefined;
|
|
101
|
+
let mapSource: string | undefined;
|
|
102
|
+
let changedFileDiffs: ReviewDiffFilesResult | undefined;
|
|
103
|
+
const tasks: Array<Promise<void>> = [];
|
|
104
|
+
|
|
105
|
+
if (input.report.include_review) {
|
|
106
|
+
tasks.push(
|
|
107
|
+
input.source.review().then(
|
|
108
|
+
(result) => {
|
|
109
|
+
reviewSource = result.files;
|
|
110
|
+
|
|
111
|
+
if (result.omitted.length > 0) omittedReviewFiles = result.omitted;
|
|
112
|
+
},
|
|
113
|
+
() => {
|
|
114
|
+
attachmentErrors.push(unavailable("review"));
|
|
115
|
+
},
|
|
116
|
+
),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (input.report.include_map) {
|
|
121
|
+
tasks.push(
|
|
122
|
+
input.source.map().then(
|
|
123
|
+
// A review does not need a software map: #840 split document and map
|
|
124
|
+
// publishing, so "no map" is a normal state, not a failed read.
|
|
125
|
+
// `readSoftwareMapSourceForRef` returns null when there is nothing to
|
|
126
|
+
// send and throws when a read fails, so only the throw is an error.
|
|
127
|
+
(source) => {
|
|
128
|
+
if (source !== null) mapSource = source;
|
|
129
|
+
},
|
|
130
|
+
() => {
|
|
131
|
+
attachmentErrors.push(unavailable("map"));
|
|
132
|
+
},
|
|
133
|
+
),
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (input.report.include_diff) {
|
|
138
|
+
tasks.push(
|
|
139
|
+
input.source.diff().then(
|
|
140
|
+
(diff) => {
|
|
141
|
+
changedFileDiffs = diff;
|
|
142
|
+
},
|
|
143
|
+
() => {
|
|
144
|
+
attachmentErrors.push(unavailable("diff"));
|
|
145
|
+
},
|
|
146
|
+
),
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
await Promise.all(tasks);
|
|
151
|
+
|
|
152
|
+
if (reviewSource !== undefined) payload.review = reviewSource;
|
|
153
|
+
|
|
154
|
+
if (omittedReviewFiles !== undefined) {
|
|
155
|
+
payload.diagnostics.review_omitted_files = omittedReviewFiles;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (mapSource !== undefined) payload.map = mapSource;
|
|
159
|
+
|
|
160
|
+
if (changedFileDiffs !== undefined) payload.diff = changedFileDiffs;
|
|
161
|
+
|
|
162
|
+
if (attachmentErrors.length > 0) {
|
|
163
|
+
payload.diagnostics.attachment_errors = attachmentErrors.sort(byAttachment);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const response = await (input.fetchImpl ?? fetch)(BUG_REPORT_URL, {
|
|
167
|
+
method: "POST",
|
|
168
|
+
body: buildBugReportRequest(payload, {
|
|
169
|
+
appVersion: input.report.app_version,
|
|
170
|
+
cliVersion,
|
|
171
|
+
}),
|
|
172
|
+
signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS),
|
|
173
|
+
}).catch((error) => {
|
|
174
|
+
throw new BugReportUpstreamError(
|
|
175
|
+
502,
|
|
176
|
+
error instanceof Error ? error.message : "Bug report service failed.",
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const responseBody = await response.json().catch(() => null);
|
|
181
|
+
|
|
182
|
+
if (!response.ok) {
|
|
183
|
+
throw new BugReportUpstreamError(
|
|
184
|
+
response.status === 429 || response.status === 413
|
|
185
|
+
? response.status
|
|
186
|
+
: 502,
|
|
187
|
+
response.status === 429
|
|
188
|
+
? "Too many reports. Try again later."
|
|
189
|
+
: response.status === 413
|
|
190
|
+
? "Bug report is too large."
|
|
191
|
+
: "Bug report service failed.",
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const result = parseReviewBugReportResponse(responseBody);
|
|
196
|
+
|
|
197
|
+
if (!result.ok) throw new BugReportUpstreamError(502, result.error);
|
|
198
|
+
|
|
199
|
+
return result;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function buildBugReportRequest(
|
|
203
|
+
payload: BugReportPayload,
|
|
204
|
+
input: {
|
|
205
|
+
appVersion: string;
|
|
206
|
+
cliVersion: string;
|
|
207
|
+
maxPayloadBytes?: number;
|
|
208
|
+
},
|
|
209
|
+
): FormData {
|
|
210
|
+
const maxPayloadBytes = input.maxPayloadBytes ?? MAX_PAYLOAD_BYTES;
|
|
211
|
+
let truncatedDiff = false;
|
|
212
|
+
let truncatedMap = false;
|
|
213
|
+
let truncatedScreenshot = false;
|
|
214
|
+
let payloadBytes = gzipPayload(payload);
|
|
215
|
+
|
|
216
|
+
if (payloadBytes.byteLength > maxPayloadBytes && payload.diff) {
|
|
217
|
+
delete payload.diff;
|
|
218
|
+
truncatedDiff = true;
|
|
219
|
+
payloadBytes = gzipPayload(payload);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (payloadBytes.byteLength > maxPayloadBytes && payload.map) {
|
|
223
|
+
delete payload.map;
|
|
224
|
+
truncatedMap = true;
|
|
225
|
+
payloadBytes = gzipPayload(payload);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (payloadBytes.byteLength > maxPayloadBytes && payload.screenshot) {
|
|
229
|
+
delete payload.screenshot;
|
|
230
|
+
truncatedScreenshot = true;
|
|
231
|
+
payloadBytes = gzipPayload(payload);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (payloadBytes.byteLength > maxPayloadBytes && payload.review) {
|
|
235
|
+
delete payload.review;
|
|
236
|
+
payloadBytes = gzipPayload(payload);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (payloadBytes.byteLength > maxPayloadBytes) {
|
|
240
|
+
throw new BugReportUpstreamError(413, "Bug report is too large.");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const meta: ReviewBugReportMetaV2 = {
|
|
244
|
+
schema_version: 2,
|
|
245
|
+
description_length: Buffer.byteLength(payload.description),
|
|
246
|
+
has_review: payload.review !== undefined,
|
|
247
|
+
has_map: payload.map !== undefined,
|
|
248
|
+
has_diff: payload.diff !== undefined,
|
|
249
|
+
has_screenshot: payload.screenshot !== undefined,
|
|
250
|
+
has_trace: false,
|
|
251
|
+
payload_bytes: payloadBytes.byteLength,
|
|
252
|
+
app_version: input.appVersion,
|
|
253
|
+
cli_version: input.cliVersion,
|
|
254
|
+
platform: process.platform,
|
|
255
|
+
truncated_diff: truncatedDiff,
|
|
256
|
+
truncated_map: truncatedMap,
|
|
257
|
+
truncated_screenshot: truncatedScreenshot,
|
|
258
|
+
truncated_trace: false,
|
|
259
|
+
parts: [
|
|
260
|
+
{
|
|
261
|
+
field: "payload",
|
|
262
|
+
filename: "payload.json.gz",
|
|
263
|
+
bytes: payloadBytes.byteLength,
|
|
264
|
+
sha256: sha256Bytes(payloadBytes),
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
if (payload.description.trim().length > 0) {
|
|
270
|
+
meta.description = payload.description;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const form = new FormData();
|
|
274
|
+
form.append("meta", JSON.stringify(meta));
|
|
275
|
+
form.append(
|
|
276
|
+
"payload",
|
|
277
|
+
new Blob([Uint8Array.from(payloadBytes)], { type: "application/gzip" }),
|
|
278
|
+
"payload.json.gz",
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
return form;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function gzipPayload(payload: BugReportPayload): Buffer {
|
|
285
|
+
return gzipSync(Buffer.from(JSON.stringify(payload)), { level: 9 });
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function sha256Bytes(bytes: Uint8Array): string {
|
|
289
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// The document is required: a failure to read it makes the attachment
|
|
293
|
+
// unavailable. Each TypeScript module is optional, because a report that loses
|
|
294
|
+
// one module is still better than a report that loses the review.
|
|
295
|
+
function unavailable(attachment: AttachmentName): AttachmentError {
|
|
296
|
+
return { attachment, error: "unavailable" };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function byAttachment(left: AttachmentError, right: AttachmentError): number {
|
|
300
|
+
return left.attachment.localeCompare(right.attachment);
|
|
301
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Keeps one repeating error from counting as thousands. The budget is per app
|
|
2
|
+
// session and per message digest, so a loop on one error still lets other
|
|
3
|
+
// errors through, and the burst event says how much was withheld.
|
|
4
|
+
|
|
5
|
+
export const CLIENT_ERROR_BUDGET_PER_HASH = 5;
|
|
6
|
+
|
|
7
|
+
const BURST_EVERY = 100;
|
|
8
|
+
|
|
9
|
+
const DEFAULT_MAX_KEYS = 1_000;
|
|
10
|
+
|
|
11
|
+
export interface ClientErrorAdmission {
|
|
12
|
+
verdict: "send" | "burst" | "drop";
|
|
13
|
+
/** How many reports past the budget this key has seen, including this one. */
|
|
14
|
+
suppressed: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class ClientErrorBudget {
|
|
18
|
+
private readonly counts = new Map<string, number>();
|
|
19
|
+
|
|
20
|
+
constructor(private readonly maxKeys = DEFAULT_MAX_KEYS) {}
|
|
21
|
+
|
|
22
|
+
admit(sessionId: string, messageHash: string): ClientErrorAdmission {
|
|
23
|
+
const key = `${sessionId}\n${messageHash}`;
|
|
24
|
+
const count = (this.counts.get(key) ?? 0) + 1;
|
|
25
|
+
this.counts.delete(key);
|
|
26
|
+
this.counts.set(key, count);
|
|
27
|
+
|
|
28
|
+
while (this.counts.size > this.maxKeys) {
|
|
29
|
+
const oldest = this.counts.keys().next().value;
|
|
30
|
+
|
|
31
|
+
if (oldest === undefined) break;
|
|
32
|
+
this.counts.delete(oldest);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const suppressed = Math.max(0, count - CLIENT_ERROR_BUDGET_PER_HASH);
|
|
36
|
+
|
|
37
|
+
if (suppressed === 0) return { verdict: "send", suppressed };
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
verdict:
|
|
41
|
+
suppressed === 1 || suppressed % BURST_EVERY === 0 ? "burst" : "drop",
|
|
42
|
+
suppressed,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|