fix(observability): harden Sentry init for catalog-degradation reporting

- initSentry() is now idempotent via a cached promise, and the degradation
  reporter / feedback opener await it — so an empty/error state that renders
  before the SDK's dynamic import resolves no longer drops the event (not missing
  the failure is the whole point of the auto-capture).
- environment now resolves hostname-first (sase.tr → production, dev.sase.tr →
  staging), so the compose-baked VITE_SENTRY_ENVIRONMENT default ("production")
  no longer makes dev browser events indistinguishable from prod.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 19:32:22 +03:00
parent f02f97f69f
commit cd54cf9443

View File

@@ -13,9 +13,17 @@ let initialized = false;
// Set once init succeeds so the catalog-degradation helpers below can use the // Set once init succeeds so the catalog-degradation helpers below can use the
// already-loaded SDK without re-awaiting the dynamic import on every call. // already-loaded SDK without re-awaiting the dynamic import on every call.
let sentryApi: typeof import("@sentry/react") | null = null; let sentryApi: typeof import("@sentry/react") | null = null;
// Cache the init promise so concurrent callers (e.g. a degradation reporter that
// fires on first paint, racing the main.tsx call) await the SAME init instead of
// double-initialising the SDK.
let initPromise: Promise<void> | null = null;
export async function initSentry() { export function initSentry(): Promise<void> {
if (initialized) return; if (!initPromise) initPromise = doInitSentry();
return initPromise;
}
async function doInitSentry(): Promise<void> {
const dsn = import.meta.env.VITE_SENTRY_DSN; const dsn = import.meta.env.VITE_SENTRY_DSN;
if (!dsn) { if (!dsn) {
if (import.meta.env.DEV) { if (import.meta.env.DEV) {
@@ -24,20 +32,18 @@ export async function initSentry() {
return; return;
} }
// Both dev.sase.tr (staging) and sase.tr (prod) ship a production Vite build // Hostname is authoritative for the two real deployments — sase.tr (prod) and
// (MODE === "production"), so MODE can't separate them. Derive from the host at // dev.sase.tr (staging) — which both ship a production Vite build baking the
// runtime instead, so dev errors stay filterable from prod in Sentry. An // SAME VITE_SENTRY_ENVIRONMENT (compose default "production"), so that var can't
// explicit VITE_SENTRY_ENVIRONMENT still wins. // tell them apart. Use it only as a fallback for local/preview hosts.
const host = typeof window !== "undefined" ? window.location.hostname : ""; const host = typeof window !== "undefined" ? window.location.hostname : "";
const environment = const environment =
(import.meta.env.VITE_SENTRY_ENVIRONMENT as string | undefined) ?? host === "sase.tr" || host === "www.sase.tr"
(host === "sase.tr" || host === "www.sase.tr"
? "production" ? "production"
: host.endsWith("dev.sase.tr") : host.endsWith("dev.sase.tr")
? "staging" ? "staging"
: import.meta.env.DEV : (import.meta.env.VITE_SENTRY_ENVIRONMENT as string | undefined) ||
? "development" (import.meta.env.DEV ? "development" : "staging");
: "staging");
const release = import.meta.env.VITE_SENTRY_RELEASE; const release = import.meta.env.VITE_SENTRY_RELEASE;
try { try {
@@ -123,11 +129,14 @@ export async function reportCatalogDegradation(
ctx: CatalogIssueContext, ctx: CatalogIssueContext,
): Promise<void> { ): Promise<void> {
try { try {
const Sentry = sentryApi;
if (!Sentry) return;
const dedupKey = `${kind}:${ctx.categoryId ?? ctx.vehicleId ?? ""}`; const dedupKey = `${kind}:${ctx.categoryId ?? ctx.vehicleId ?? ""}`;
if (reportedThisSession.has(dedupKey)) return; if (reportedThisSession.has(dedupKey)) return;
reportedThisSession.add(dedupKey); reportedThisSession.add(dedupKey);
// The empty/error state can render before the SDK's dynamic import resolves —
// wait for init so the failure is never silently dropped.
await initSentry();
const Sentry = sentryApi;
if (!Sentry) return;
const brand = brandOf(ctx.vehicleLabel); const brand = brandOf(ctx.vehicleLabel);
Sentry.captureMessage(`catalog degraded: ${kind} (browser/${brand})`, { Sentry.captureMessage(`catalog degraded: ${kind} (browser/${brand})`, {
@@ -153,6 +162,7 @@ export async function reportCatalogDegradation(
*/ */
export async function openCatalogFeedback(ctx: CatalogIssueContext): Promise<void> { export async function openCatalogFeedback(ctx: CatalogIssueContext): Promise<void> {
try { try {
await initSentry();
const Sentry = sentryApi; const Sentry = sentryApi;
if (!Sentry) return; if (!Sentry) return;
Sentry.setTag("catalog_source", ctx.source ?? "unknown"); Sentry.setTag("catalog_source", ctx.source ?? "unknown");