feat(observability): browser Sentry SDK (gated on VITE_SENTRY_DSN)
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Sase frontend has Faro (RUM) and PostHog (product analytics) but no browser error monitoring. A recent panel session investigation (panel insight cmpv9q3ms004dfcphluw0z8bh — silent rage-clicks after parts_panel_viewed with no API call) couldn't be confirmed or refuted from JS error data because there was no JS error channel at all. This adds one. - @sentry/react ^9 (dynamic import in lib/sentry.ts so the bundle only pays the SDK cost when DSN is configured) - Init is gated on VITE_SENTRY_DSN — unset = no-op, no behaviour change - Replay only fires on error (sessionSampleRate 0, onErrorSampleRate 1) - KVKK: sendDefaultPii false, maskAllText + blockAllMedia on replay - Builds emit hidden source maps so Sentry can de-minify traces while end users don't fetch the maps in the browser - docker-compose.coolify.yml threads VITE_SENTRY_* through as build args (VITE_* must be build-time; runtime env never reaches a Vite bundle) Wiring on the Coolify side is a separate manual step — set VITE_SENTRY_DSN on the prod (ro48g…) and/or dev (jwgwkg…) app and redeploy. Backend Sentry (NestJS) is unchanged. Refs: Süper Panel docs/ARCHITECTURE.md, panel sentry-archive job.
This commit is contained in:
@@ -22,6 +22,7 @@
|
||||
"@remotion/player": "^4.0.422",
|
||||
"@sase/shared": "workspace:*",
|
||||
"@sase/ui": "workspace:*",
|
||||
"@sentry/react": "^9.0.0",
|
||||
"@tanstack/react-query": "^5.62.0",
|
||||
"@tanstack/react-router": "^1.120.0",
|
||||
"better-auth": "^1.2.0",
|
||||
|
||||
57
apps/web/src/lib/sentry.ts
Normal file
57
apps/web/src/lib/sentry.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Sentry browser SDK init — gated on VITE_SENTRY_DSN.
|
||||
*
|
||||
* Co-exists with Faro (Grafana) and PostHog: Sentry handles uncaught JS errors
|
||||
* and on-error session replay; Faro handles RUM/traces; PostHog handles product
|
||||
* analytics. No conflict between the three at the global error-handler layer.
|
||||
*
|
||||
* KVKK note: `sendDefaultPii: false` and `maskAllText: true` on replay.
|
||||
*/
|
||||
import type { init as SentryInit } from "@sentry/react";
|
||||
|
||||
let initialized = false;
|
||||
|
||||
export async function initSentry() {
|
||||
if (initialized) return;
|
||||
const dsn = import.meta.env.VITE_SENTRY_DSN;
|
||||
if (!dsn) {
|
||||
if (import.meta.env.DEV) {
|
||||
console.info("[sentry] VITE_SENTRY_DSN unset — skipping init");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const environment =
|
||||
import.meta.env.VITE_SENTRY_ENVIRONMENT ?? import.meta.env.MODE ?? "production";
|
||||
const release = import.meta.env.VITE_SENTRY_RELEASE;
|
||||
|
||||
try {
|
||||
const Sentry = await import("@sentry/react");
|
||||
(Sentry.init as typeof SentryInit)({
|
||||
dsn,
|
||||
environment,
|
||||
release,
|
||||
integrations: [
|
||||
Sentry.browserTracingIntegration(),
|
||||
Sentry.replayIntegration({ maskAllText: true, blockAllMedia: true }),
|
||||
],
|
||||
// Performance: 10% trace sample rate (low volume site, can raise later).
|
||||
tracesSampleRate: 0.1,
|
||||
// Replay: only when an error fires; no session-replay otherwise (cost + privacy).
|
||||
replaysSessionSampleRate: 0.0,
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
// KVKK: do not auto-attach IP / cookies / user agent fingerprints.
|
||||
sendDefaultPii: false,
|
||||
// Drop framework noise that isn't actionable.
|
||||
ignoreErrors: [
|
||||
"ResizeObserver loop limit exceeded",
|
||||
"ResizeObserver loop completed with undelivered notifications.",
|
||||
"Non-Error promise rejection captured",
|
||||
],
|
||||
});
|
||||
initialized = true;
|
||||
console.log("[sentry] browser SDK initialized");
|
||||
} catch (err) {
|
||||
console.warn("[sentry] init failed:", (err as Error).message);
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,13 @@ import { initFaro } from "./lib/faro";
|
||||
import { initLocale } from "./lib/i18n";
|
||||
import { initMetaPixel } from "./lib/meta-pixel";
|
||||
import { initPostHog } from "./lib/posthog";
|
||||
import { initSentry } from "./lib/sentry";
|
||||
import { routeTree } from "./routeTree.gen";
|
||||
import "./globals.css";
|
||||
|
||||
// Sentry first so it can capture errors thrown by other init code.
|
||||
initSentry();
|
||||
|
||||
// Read persisted locale from localStorage before first render — without this
|
||||
// a reload always falls back to the TR default even if the user picked EN.
|
||||
initLocale();
|
||||
|
||||
@@ -30,6 +30,9 @@ export default defineConfig({
|
||||
},
|
||||
build: {
|
||||
outDir: "dist",
|
||||
sourcemap: false,
|
||||
// 'hidden' emits source maps but strips the //# sourceMappingURL comment
|
||||
// from JS bundles. Sentry can still consume them (via release upload), but
|
||||
// browsers won't fetch them, so end-user stack traces stay minified.
|
||||
sourcemap: "hidden",
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user