mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat(analytics): upload web source maps to Sentry + tie release to build (#369)
* feat(analytics): upload web source maps to Sentry + tie release to build Web crash reports were unusable: the bundle ships minified with no source maps uploaded, and every build reported as the frozen APP_VERSION, so a Sentry error showed an unreadable stack under a single release. - Add @sentry/vite-plugin: emit hidden source maps and upload them by debug id when SENTRY_AUTH_TOKEN is present (published Docker build only), then delete the maps so they never ship. No-op for dev and the source archive. - Set the Sentry release from SENTRY_RELEASE / VITE_SENTRY_RELEASE (the Docker build passes the release version), falling back to APP_VERSION. - Relax beforeSend so app bundle frames keep a host-stripped path (Sentry needs it to match the uploaded map) while the instance hostname, error message, and PII stay stripped. Filesystem paths still collapse to the basename. - Wire the Dockerfile (sentry_auth_token build secret + SENTRY_RELEASE arg/env) and the release docker job. * fix(analytics): point source map upload at the snapotter org (project node)
This commit is contained in:
@@ -11,6 +11,16 @@ function basename(p: string): string {
|
||||
return i >= 0 ? p.slice(i + 1) : p;
|
||||
}
|
||||
|
||||
// App bundle frames are http(s) URLs. Keep the host-less pathname so Sentry can
|
||||
// match the frame to its uploaded source map, but drop the instance hostname
|
||||
// (not anonymous). Filesystem-style paths collapse to the basename so a local
|
||||
// path or username can never leave the browser.
|
||||
function scrubFramePath(p: string): string {
|
||||
const url = p.match(/^https?:\/\/[^/]+(\/[^?#]*)?/i);
|
||||
if (url) return url[1] ?? "/";
|
||||
return basename(p);
|
||||
}
|
||||
|
||||
// Only these keys may leave the browser per event, and only as primitives.
|
||||
const ALLOWED: Record<string, ReadonlySet<string>> = {
|
||||
tool_opened: new Set(["tool_id", "category", "modality"]),
|
||||
@@ -66,7 +76,8 @@ export async function initAnalytics(config: AnalyticsConfig): Promise<void> {
|
||||
const Sentry = await import("@sentry/react");
|
||||
Sentry.init({
|
||||
dsn: config.sentryDsn,
|
||||
release: (await import("@snapotter/shared")).APP_VERSION,
|
||||
release:
|
||||
import.meta.env.VITE_SENTRY_RELEASE || (await import("@snapotter/shared")).APP_VERSION,
|
||||
environment: "production",
|
||||
tracesSampleRate: config.sampleRate,
|
||||
sendDefaultPii: false,
|
||||
@@ -85,13 +96,22 @@ export async function initAnalytics(config: AnalyticsConfig): Promise<void> {
|
||||
ex.value = ex.type;
|
||||
if (ex.stacktrace?.frames) {
|
||||
for (const frame of ex.stacktrace.frames) {
|
||||
if (frame.filename) frame.filename = basename(frame.filename);
|
||||
frame.abs_path = undefined;
|
||||
if (frame.filename) frame.filename = scrubFramePath(frame.filename);
|
||||
if (frame.abs_path) frame.abs_path = scrubFramePath(frame.abs_path);
|
||||
frame.vars = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Keep debug_meta image paths consistent with the scrubbed frames so
|
||||
// debug-id source-map matching still resolves, minus the hostname.
|
||||
if (event.debug_meta?.images) {
|
||||
for (const img of event.debug_meta.images) {
|
||||
if ("code_file" in img && img.code_file) {
|
||||
img.code_file = scrubFramePath(img.code_file);
|
||||
}
|
||||
}
|
||||
}
|
||||
return event;
|
||||
},
|
||||
beforeBreadcrumb() {
|
||||
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
// Sentry release, injected at build time by the Docker web build. Unset in
|
||||
// dev and source-archive builds, where the SDK falls back to APP_VERSION.
|
||||
readonly VITE_SENTRY_RELEASE?: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
Reference in New Issue
Block a user