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:
@@ -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");
|
||||||
|
|||||||
Reference in New Issue
Block a user