diff --git a/.github/workflows/deploy-demo.yml b/.github/workflows/deploy-demo.yml index 66cc8a1..0197743 100644 --- a/.github/workflows/deploy-demo.yml +++ b/.github/workflows/deploy-demo.yml @@ -37,6 +37,10 @@ env: # `wrangler pages deploy` a PREVIEW deploy and demo.pcbjam.com won't update. # Direct-Upload projects default to "production". PAGES_PROD_BRANCH: production + # Better Stack error-tracking DSN. Unset ⇒ this build reports nothing. + # Keep in sync with release.yml — the demo is built from BOTH workflows, and + # setting it in only one silently ships a demo with no error reporting. + ERRORS_DSN: ${{ secrets.ERRORS_DSN }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} # The publish scripts shell wrangler; no repo dep — fetch it on demand. @@ -100,6 +104,7 @@ jobs: --cdn "$CDN" --lib-tag "$LIB_TAG" ${MODELS_TAG:+--models-tag "$MODELS_TAG"} --plausible "https://plausible.io/js/pa-KjNS9YmidydULZTstsjRg.js" + ${ERRORS_DSN:+--errors-dsn "$ERRORS_DSN" --errors-env demo} # 4) Ensure the Pages project exists (first deploy creates it; no-op after). # Its production branch must equal PAGES_PROD_BRANCH or deploys land as diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index faccde4..8610c53 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -48,6 +48,12 @@ env: # Mgmt app origin: non-editor routes on the editor host redirect here # (standalone-hardening 0006). The demo build never sets this. EDITOR_APP_BASE: https://app.pcbjam.com + # Better Stack error-tracking DSN (Sentry wire format). Unset ⇒ builds report + # nothing, so this is safe to leave empty. Held as a secret rather than a + # literal: this repo is public, and although the token becomes visible in the + # shipped bundle anyway, keeping it out of git makes it rotatable without a + # commit. Keep in sync with deploy-demo.yml. + ERRORS_DSN: ${{ secrets.ERRORS_DSN }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} WRANGLER_CMD: npx --yes wrangler@4 @@ -136,6 +142,7 @@ jobs: --cdn "$CDN" --lib-tag "$LIB_TAG" ${MODELS_TAG:+--models-tag "$MODELS_TAG"} --plausible "https://plausible.io/js/pa-KjNS9YmidydULZTstsjRg.js" + ${ERRORS_DSN:+--errors-dsn "$ERRORS_DSN" --errors-env demo} - name: Ensure Pages project exists run: > @@ -183,6 +190,7 @@ jobs: node scripts/deploy/build-editor.mjs --tag "$RELEASE_TAG" --cdn "$CDN" --api-base "$EDITOR_API_BASE" --app-base "$EDITOR_APP_BASE" --plausible "https://plausible.io/js/pa-KjNS9YmidydULZTstsjRg.js" + ${ERRORS_DSN:+--errors-dsn "$ERRORS_DSN" --errors-env production} ${MODELS_TAG:+--models-tag "$MODELS_TAG"} - name: Ensure Pages project exists diff --git a/scripts/deploy/build-demo.mjs b/scripts/deploy/build-demo.mjs index f27e35f..e355ea7 100644 --- a/scripts/deploy/build-demo.mjs +++ b/scripts/deploy/build-demo.mjs @@ -34,6 +34,12 @@ function parseArgs(argv) { // Plausible pa-*.js script URL. Off unless given (or VITE_PLAUSIBLE_SRC // is already in the environment, which passes straight through). plausible: null, + // Better Stack error-tracking DSN (Sentry wire format). Omitted ⇒ no error + // reporting. The demo reports under its own environment: anonymous traffic + // on arbitrary hardware with no backend fails differently from the signed-in + // editor, and mixing them would drown the editor's real regressions. + errorsDsn: null, + errorsEnv: "demo", }; for (let i = 2; i < argv.length; i++) { const next = () => argv[++i]; @@ -51,6 +57,8 @@ function parseArgs(argv) { case "--landing": a.landing = next(); break; case "--waitlist": a.waitlist = next(); break; case "--plausible": a.plausible = next(); break; + case "--errors-dsn": a.errorsDsn = next(); break; + case "--errors-env": a.errorsEnv = next(); break; default: throw new Error(`unknown arg: ${argv[i]}`); } } @@ -128,6 +136,10 @@ function main() { VITE_WAITLIST_URL: a.waitlist, // Plausible analytics: explicit --plausible wins, else any env-provided value. ...(a.plausible ? { VITE_PLAUSIBLE_SRC: a.plausible } : {}), + // Error tracking. The env tag rides along only when a DSN is given. + ...(a.errorsDsn + ? { VITE_ERRORS_DSN: a.errorsDsn, VITE_ERRORS_ENV: a.errorsEnv } + : {}), }; console.log(`build-demo: tag=${a.tag} cdn=${a.cdn}`); @@ -139,6 +151,7 @@ function main() { console.log(` VITE_MODELS_MANIFEST_URL=${env.VITE_MODELS_MANIFEST_URL ?? "(unset — 3D models off)"}`); console.log(` VITE_LANDING_URL=${env.VITE_LANDING_URL} VITE_WAITLIST_URL=${env.VITE_WAITLIST_URL}`); console.log(` VITE_PLAUSIBLE_SRC=${env.VITE_PLAUSIBLE_SRC || "(off)"}`); + console.log(` VITE_ERRORS_DSN=${env.VITE_ERRORS_DSN ? `(set, env=${env.VITE_ERRORS_ENV})` : "(off)"}`); // Keep the dev-only WASM symlink out of the bundle (it'd copy 100s of MB into // dist/; the CDN serves it). In CI it isn't present, so this is a no-op there. diff --git a/scripts/deploy/build-editor.mjs b/scripts/deploy/build-editor.mjs index f669055..39227e9 100644 --- a/scripts/deploy/build-editor.mjs +++ b/scripts/deploy/build-editor.mjs @@ -34,6 +34,10 @@ function parseArgs(argv) { // kicad-packages3D snapshot (libs/kicad-models//); omitted ⇒ 3D models off. modelsTag: null, plausible: null, + // Better Stack error-tracking DSN (Sentry wire format). Omitted ⇒ no error + // reporting from this build. + errorsDsn: null, + errorsEnv: "production", // Companion mgmt app origin; set ⇒ non-editor routes redirect there // (standalone-hardening 0006). Omitted ⇒ every route renders locally. appBase: null, @@ -48,6 +52,8 @@ function parseArgs(argv) { case "--yjs-endpoint": a.yjsEndpoint = next(); break; case "--models-tag": a.modelsTag = next(); break; case "--plausible": a.plausible = next(); break; + case "--errors-dsn": a.errorsDsn = next(); break; + case "--errors-env": a.errorsEnv = next(); break; case "--app-base": a.appBase = next(); break; default: throw new Error(`unknown arg: ${argv[i]}`); } @@ -109,6 +115,11 @@ function main() { VITE_GIT_SHA: gitSha(repoRoot), VITE_REPO_URL: a.repo, ...(a.plausible ? { VITE_PLAUSIBLE_SRC: a.plausible } : {}), + // Error tracking. The env tag rides along only when a DSN is given, so a + // DSN-less build cannot report under a production label. + ...(a.errorsDsn + ? { VITE_ERRORS_DSN: a.errorsDsn, VITE_ERRORS_ENV: a.errorsEnv } + : {}), // Non-editor surfaces bounce to the mgmt app (mirror of the closed repo's // VITE_STANDALONE_URL pointing the other way). ...(a.appBase ? { VITE_APP_URL: a.appBase } : {}), @@ -123,6 +134,7 @@ function main() { console.log(` VITE_MODELS_MANIFEST_URL=${env.VITE_MODELS_MANIFEST_URL ?? "(unset — 3D models off)"}`); console.log(` VITE_APP_TAG=${env.VITE_APP_TAG} VITE_GIT_SHA=${env.VITE_GIT_SHA || "(none)"}`); console.log(` VITE_PLAUSIBLE_SRC=${env.VITE_PLAUSIBLE_SRC || "(off)"}`); + console.log(` VITE_ERRORS_DSN=${env.VITE_ERRORS_DSN ? `(set, env=${env.VITE_ERRORS_ENV})` : "(off)"}`); console.log(` VITE_APP_URL=${env.VITE_APP_URL || "(unset — no non-editor redirect)"}`); // Keep the dev-only WASM symlink out of the bundle (CDN serves it). diff --git a/scripts/deploy/dev-demo.mjs b/scripts/deploy/dev-demo.mjs index 94d55b3..d5a18a0 100644 --- a/scripts/deploy/dev-demo.mjs +++ b/scripts/deploy/dev-demo.mjs @@ -194,6 +194,10 @@ function main() { env.VITE_DOC_SOURCE = "api"; env.VITE_LOCAL_PROJECTS = "idb"; + // --- Never report errors from a local demo run, even if the developer has a + // production DSN sitting in their environment. + delete env.VITE_ERRORS_DSN; + // --- Projects: the read-only example gallery (the demo.pcbjam.com experience). // Default: build it locally and serve it same-origin. --content-tag // pins the live CDN gallery instead. --no-gallery falls back to local-folder diff --git a/site/src/content/legal/cookies.md b/site/src/content/legal/cookies.md index e3df006..c0db5d9 100644 --- a/site/src/content/legal/cookies.md +++ b/site/src/content/legal/cookies.md @@ -1,7 +1,7 @@ --- title: Cookie Policy description: How PCBJam uses cookies and similar technologies, and the choices you have. -updated: 2026-07-27 +updated: 2026-08-03 --- This Cookie Policy explains how **PCBJam** ("PCBJam", "we", "us", "our") uses cookies and similar technologies — such as browser local storage, IndexedDB and cache storage — when you visit **pcbjam.com** or use the PCBJam application (the "Service"), and the choices you have. @@ -101,6 +101,7 @@ We do **not** use Google Analytics, advertising pixels, or any cross-site tracki | **Paddle** (Paddle.com Market Limited and affiliates) | Merchant of Record / payments | Checkout, security and fraud-prevention cookies; their own non-essential cookies (with consent where required, via Paddle's own controls). Independent controller. | [Paddle Privacy Policy](https://www.paddle.com/legal/privacy); Paddle's cookie controls appear within its checkout. | | **Plausible** (Plausible Insights OÜ) | Cookieless analytics | Nothing — no cookies and no device storage (see Section 5). | [Plausible Data Policy](https://plausible.io/data-policy) · [Plausible Privacy](https://plausible.io/privacy) | | **Cloudflare** (Cloudflare, Inc.) | Hosting, CDN and network security | At most strictly-necessary hosting/security cookies. | [Cloudflare Privacy](https://www.cloudflare.com/privacypolicy/) | +| **Better Stack** | Error and crash diagnostics for the editor | Nothing — no cookies. The reporting code runs in your browser only when the application errors, and sends the fault report directly; it sets no cookie and stores no identifier on your device. | [Better Stack Privacy](https://betterstack.com/legal/privacy) | We are not responsible for the privacy practices of these third parties; please review their notices. We update this list as our integrations change. diff --git a/site/src/content/legal/licenses.md b/site/src/content/legal/licenses.md index 8955fd7..e0fcf91 100644 --- a/site/src/content/legal/licenses.md +++ b/site/src/content/legal/licenses.md @@ -1,7 +1,7 @@ --- title: Open-Source Licenses & Source Code description: The open-source software PCBJam is built on, the licences that apply, and how to get the corresponding source code. -updated: 2026-06-18 +updated: 2026-08-03 --- ## The short version (summary) @@ -25,6 +25,7 @@ PCBJam is a combined work. The table below lists its principal open-source compo | **wxWidgets** (base) | Cross-platform GUI toolkit that KiCad uses | **wxWindows Library Licence v3.1** (LGPL v2+ with a binary-distribution exception) | | **wxWidgets — WebAssembly port** | The browser/WASM platform layer, derived from [ahilss/wxWidgets-wasm](https://github.com/ahilss/wxWidgets-wasm) | **GNU Lesser General Public License, version 2 (LGPL v2)** — *without* the wxWindows binary exception | | Other bundled libraries | Various supporting libraries used by KiCad | Their respective licences (Apache-2.0, MIT, BSD-3-Clause, Boost, CC0, ISC, CC-BY-SA-4.0, and others) | +| Browser application libraries | The JavaScript/TypeScript libraries the editor's own interface is built from — including React, Yjs (collaborative editing), Radix UI, and the Sentry SDK (used to report crashes to our error-tracking provider; see the [Privacy Policy](/privacy)) | Predominantly **MIT**, with some Apache-2.0 and BSD. Exact versions are pinned in `web/pnpm-lock.yaml` in the source repository | The combined application is conveyed to you under the **GPLv3**. The wxWidgets components are GPL-compatible: the base toolkit's licence is explicitly compatible with GPL'd applications, and the LGPL v2 WebAssembly-port files may be combined into a GPLv3 work under the LGPL's terms. diff --git a/site/src/content/legal/privacy.md b/site/src/content/legal/privacy.md index ef7e63a..c53568f 100644 --- a/site/src/content/legal/privacy.md +++ b/site/src/content/legal/privacy.md @@ -1,7 +1,7 @@ --- title: Privacy Policy description: How PCBJam handles your personal data. -updated: 2026-07-27 +updated: 2026-08-03 --- ## 1. A quick summary @@ -146,6 +146,7 @@ We do **not** sell your personal data. We share it only with the following categ | **Paddle** | Payments / Merchant of Record | **Independent controller** (for payment data) | [Paddle Privacy](https://www.paddle.com/legal/privacy) | | **Resend** | Sending transactional and marketing email | **Processor** | [Resend DPA](https://resend.com/legal/dpa) · [Subprocessors](https://resend.com/legal/subprocessors) | | **Cloudflare** | Hosting this website, storing your project files (R2 object storage), hosting application/account data, and network delivery & security (CDN, DNS, WAF) | **Processor** | [Cloudflare DPA](https://www.cloudflare.com/cloudflare-customer-dpa/) · [GDPR hub](https://www.cloudflare.com/trust-hub/gdpr/) | +| **Better Stack** | Error and crash diagnostics from the editor — when the application fails in your browser, the error message, technical diagnostic context (browser, device capabilities, the file type being edited, the app's own internal log), and your account identifier are sent so we can find and fix the fault. **Processed in the EU.** Not used for analytics, profiling, or marketing. | **Processor** | [Better Stack DPA](https://betterstack.com/legal/dpa) · [Privacy](https://betterstack.com/legal/privacy) | | **Google (Google Workspace)** | Our business email and support correspondence | **Processor** | [Google Cloud DPA](https://cloud.google.com/terms/data-processing-addendum) | | **Professional advisers & authorities** | Lawyers, accountants, auditors; courts, regulators, and law-enforcement where legally required | Controller / as required | — | | **A successor entity** | If we are involved in a merger, acquisition, financing, or sale of assets, your data may transfer to the successor under this policy | As required | — | diff --git a/web/.env.example b/web/.env.example index 06fd159..b0561e2 100644 --- a/web/.env.example +++ b/web/.env.example @@ -20,6 +20,9 @@ VITE_WASM_ASSET_BASE_URL=/wasm # Override the artifact source dir the dev symlink points at (default: # /tests/apps/kicad, populated by tests/scripts/setup-kicad-wasm.sh). # WASM_SRC_DIR= +# Error tracking (Better Stack) — Sentry-format DSN; unset ⇒ nothing reported. +# See standalone/.env.example for the full note. +# VITE_ERRORS_DSN= # --- example backend --- # Absolute or relative path to a single KiCad project folder to serve. diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 5fb85fd..1f1d842 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -101,6 +101,9 @@ importers: '@radix-ui/react-slot': specifier: ^1.1.1 version: 1.2.4(@types/react@18.3.29)(react@18.3.1) + '@sentry/browser': + specifier: ^10.69.0 + version: 10.69.0 '@tanstack/react-query': specifier: ^5.62.11 version: 5.100.14(react@18.3.1) @@ -997,6 +1000,34 @@ packages: cpu: [x64] os: [win32] + '@sentry/browser-utils@10.69.0': + resolution: {integrity: sha512-e/u1Abj0zRPwR/deGZAP3GOULrsx67/XXnM5Skniqs4uxTsdNtPek1Nef0tpxwaQJYxwh6pWdhswLPPbbPOgBQ==} + engines: {node: '>=18'} + + '@sentry/browser@10.69.0': + resolution: {integrity: sha512-8391tnm96YbR7b8SYfEA/NEIZuyb2r3SZrtAT0bhZtjlujcYWjo7gugQvk8sWLU9cAa/euD00eJoIoJvNfpd7Q==} + engines: {node: '>=18'} + + '@sentry/conventions@0.16.0': + resolution: {integrity: sha512-fO9PLmHdVURcSPUpWCItWAtgKiMwGdJHbovoSEyLplX5sxs2ugvI4CBPTrkkgqhObnZOD0CnWBKDzSVQYBKEyQ==} + engines: {node: '>=14'} + + '@sentry/core@10.69.0': + resolution: {integrity: sha512-+uuqVEeiDzYuAKjZLqsROKXvRTbl/QeH0gfGRtpYib1cud4rAFWRIkFmcR7Jb7JGFYwmReyQotiTj/hcDszTZg==} + engines: {node: '>=18'} + + '@sentry/feedback@10.69.0': + resolution: {integrity: sha512-qrGz5Qaw93/IhMjlFN6uIaXeHwgHDaKGa6FkTAP6PonpkvSbGGqan6xfsENxzj9HUVoli1lZ6tMRDnt2qtSPhg==} + engines: {node: '>=18'} + + '@sentry/replay-canvas@10.69.0': + resolution: {integrity: sha512-VF6nXvSninHcc7dC1Zme0RjkC7VgRMCixs6jKaQX5zTNeqTW3dZGSefSOVv+ZteRi3hJvVORq985VjUC9Z/0+A==} + engines: {node: '>=18'} + + '@sentry/replay@10.69.0': + resolution: {integrity: sha512-uRhmNhtFGPOlM0iniVmWKAX3KVXI0le41yYK/iKdPjinT9jA3ZrmykO/Fv1v/KI5znOtwa9D6eHRnDTTMRxFrg==} + engines: {node: '>=18'} + '@tanstack/query-core@5.100.14': resolution: {integrity: sha512-5X41dGpxgeaHISCRW2oYwcSycZeULZzAunaudXT9ov1KOTj9xwt0CH6hbwqP1/z74ZWF7rYFnDpyYH07XFcZew==} @@ -2589,6 +2620,40 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.61.0': optional: true + '@sentry/browser-utils@10.69.0': + dependencies: + '@sentry/conventions': 0.16.0 + '@sentry/core': 10.69.0 + + '@sentry/browser@10.69.0': + dependencies: + '@sentry/browser-utils': 10.69.0 + '@sentry/conventions': 0.16.0 + '@sentry/core': 10.69.0 + '@sentry/feedback': 10.69.0 + '@sentry/replay': 10.69.0 + '@sentry/replay-canvas': 10.69.0 + + '@sentry/conventions@0.16.0': {} + + '@sentry/core@10.69.0': + dependencies: + '@sentry/conventions': 0.16.0 + + '@sentry/feedback@10.69.0': + dependencies: + '@sentry/core': 10.69.0 + + '@sentry/replay-canvas@10.69.0': + dependencies: + '@sentry/core': 10.69.0 + '@sentry/replay': 10.69.0 + + '@sentry/replay@10.69.0': + dependencies: + '@sentry/browser-utils': 10.69.0 + '@sentry/core': 10.69.0 + '@tanstack/query-core@5.100.14': {} '@tanstack/react-query@5.100.14(react@18.3.1)': diff --git a/web/standalone/.env.example b/web/standalone/.env.example index 4739225..293505c 100644 --- a/web/standalone/.env.example +++ b/web/standalone/.env.example @@ -60,6 +60,20 @@ VITE_WASM_ROOT=/wasm # plausible.io script won't load under COEP require-corp. # VITE_PLAUSIBLE_SRC=https://plausible.io/js/pa-KjNS9YmidydULZTstsjRg.js +# Error tracking (Better Stack). Unset ⇒ nothing is reported (dev/checkout +# default). Better Stack ingests the Sentry wire protocol, so this is a Sentry- +# format DSN and the app runs the stock @sentry/browser SDK against their host: +# https://@/ +# The token is public once it ships in the bundle — that is inherent to browser +# error reporting, and it is write-only, so the exposure is quota abuse rather +# than data. Deploys pass it via scripts/deploy/build-{editor,demo}.mjs +# --errors-dsn; VITE_ERRORS_ENV tags which deploy an error came from. +# NOTE: reporting also stays off whenever VITE_ALLOW_USER_OVERRIDE=1 (dev servers +# and e2e harnesses set it), so a production DSN in a local .env still can't +# pollute the dashboard. +# VITE_ERRORS_DSN= +# VITE_ERRORS_ENV=production + # Where a backend project's DOCUMENT content lives ("api" default | "ydoc"). # Same /p/ URLs either way. "api": file bytes come from the REST # backend and a user save (File->Save in the editor) is uploaded back to it. diff --git a/web/standalone/package.json b/web/standalone/package.json index 6423d2b..813a762 100644 --- a/web/standalone/package.json +++ b/web/standalone/package.json @@ -23,6 +23,7 @@ "@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-label": "^2.1.1", "@radix-ui/react-slot": "^1.1.1", + "@sentry/browser": "^10.69.0", "@tanstack/react-query": "^5.62.11", "@ts-rest/core": "^3.52.1", "class-variance-authority": "^0.7.1", diff --git a/web/standalone/src/components/WasmTool.tsx b/web/standalone/src/components/WasmTool.tsx index 4a2ba96..0749587 100644 --- a/web/standalone/src/components/WasmTool.tsx +++ b/web/standalone/src/components/WasmTool.tsx @@ -67,6 +67,7 @@ import { type ToolFile, } from "@/wasm/kicad-runner"; import { dump as dumpTrace, mark } from "@/wasm/load-trace"; +import { errorMessage, isTerminalError } from "@/wasm/terminal-error"; import { registerSaveHook, type SaveBytes } from "@/wasm/save-flow"; import type { KicadCollabHandle, @@ -1227,14 +1228,22 @@ export function WasmTool({ // genuinely terminal signatures promote to the fatal overlay; ordinary app // errors must not hijack a working editor. React.useEffect(() => { - // NOTE: matched against `e.error.message`, which is BARE — Firefox's first - // trap is literally "index out of bounds" (no "RuntimeError", no "table") - // and slipped through the original pattern; the v0.1.19 prod log opens with - // exactly that message. - const terminal = (msg: string) => - /RuntimeError|\babort(ed)?\b|\bindex out of bounds|indirect call signature|memory access out of bounds|unreachable executed|null function or function signature/i.test( - msg, - ); + // The predicate lives in wasm/terminal-error.ts (unit-tested there) and is + // shared with the error reporter, so the overlay and Better Stack can never + // disagree about what "terminal" means. + // + // It checks the error's TYPE first — every trap in this family is a + // `WebAssembly.RuntimeError` whatever the engine calls it — with the message + // patterns kept only as a fallback for the paths that lose the Error object + // (a worker ErrorEvent crosses the realm boundary with `error: null`). + // + // That ends the per-engine spelling chase this check kept losing. Matching + // the message alone had three live holes: `RuntimeError` was listed but + // never appears IN `.message`; Chrome's bare "unreachable" and "null + // function" (the v0.1.20 prod log) matched nothing; and narrowing + // "table index is out of bounds" to `\bindex out of bounds` for Firefox's + // spelling silently stopped matching Chrome's. The type check covers all + // of them, and the fallback pattern is now a superset of the old one. // Promote to the fatal screen AND pop the console open: the log panel is // the only account of what was loading, so a fatal must never leave it // collapsed behind a mystery blue screen. @@ -1254,13 +1263,13 @@ export function WasmTool({ showFatalScreen(msg); }; const onError = (e: ErrorEvent) => { - const msg = e.error instanceof Error ? `${e.error.message}` : String(e.message ?? ""); - if (!terminal(msg)) return; + const msg = errorMessage(e.error, e.message); + if (!isTerminalError(e.error, msg)) return; promote("window error", msg); }; const onRejection = (e: PromiseRejectionEvent) => { - const msg = e.reason instanceof Error ? e.reason.message : String(e.reason ?? ""); - if (!terminal(msg)) return; + const msg = errorMessage(e.reason); + if (!isTerminalError(e.reason, msg)) return; promote("unhandled rejection", msg); }; // With PROXY_TO_PTHREAD, main()/wx/timers — and therefore every asyncify @@ -1272,8 +1281,8 @@ export function WasmTool({ // from this realm, so every one gets an error tap. const NativeWorker = window.Worker; const onWorkerError = (e: ErrorEvent) => { - const msg = String(e.message ?? ""); - if (!terminal(msg)) return; + const msg = errorMessage(e.error, e.message); + if (!isTerminalError(e.error, msg)) return; promote("worker error", msg); }; const PatchedWorker = function ( diff --git a/web/standalone/src/lib/config.ts b/web/standalone/src/lib/config.ts index 8a23f47..fc82a8f 100644 --- a/web/standalone/src/lib/config.ts +++ b/web/standalone/src/lib/config.ts @@ -82,6 +82,29 @@ export const WAITLIST_URL = */ export const PLAUSIBLE_SRC = import.meta.env.VITE_PLAUSIBLE_SRC || null; +/** + * Error tracking (Better Stack). Off unless a DSN is set, so a plain dev + * checkout and any deploy that doesn't set the var report nothing. + * + * The DSN is in SENTRY wire format because Better Stack ingests that protocol — + * we run the stock @sentry/browser SDK against their host. Named by role rather + * than by vendor precisely because the two differ: `VITE_SENTRY_DSN` would + * imply Sentry receives the data (it doesn't) and `VITE_BETTERSTACK_DSN` would + * imply a Better Stack SDK (there isn't one). See lib/error-reporting.ts. + * + * Format: https://@/ + * + * NOTE: this token ships in the client bundle and is public — that is inherent + * to browser error reporting and true of Sentry's own DSNs. It is write-only + * (it cannot read anything back), so the exposure is quota abuse, not data. + */ +export const ERRORS_DSN = import.meta.env.VITE_ERRORS_DSN || null; + +/** Environment tag on reported errors. Deploys set it explicitly; a build that + * forgot to should be obvious in the dashboard rather than silently blending + * into production. */ +export const ERRORS_ENV = import.meta.env.VITE_ERRORS_ENV || "development"; + /** * Where the standalone reads PROJECTS from (env VITE_PROJECT_SOURCE): * "remote" (default) — the @pcbjam/shared REST backend at API_BASE_URL. diff --git a/web/standalone/src/lib/error-reporting.ts b/web/standalone/src/lib/error-reporting.ts new file mode 100644 index 0000000..b8770d1 --- /dev/null +++ b/web/standalone/src/lib/error-reporting.ts @@ -0,0 +1,140 @@ +import * as Sentry from "@sentry/browser"; +import { APP_GIT_SHA, APP_TAG, ERRORS_DSN, ERRORS_ENV } from "./config"; +import { isTerminalSerializedError } from "@/wasm/terminal-error"; + +/** + * Error reporting to Better Stack. + * + * Better Stack's Error Tracking ingests the SENTRY wire protocol, so we run the + * stock @sentry/browser SDK pointed at a Better Stack DSN. No Better Stack SDK + * is involved (they don't publish one for browsers; their alternative is a + * remote