chore(sentry): make sampling config environment-driven

This commit is contained in:
2026-07-02 13:04:13 +08:00
parent d8a28bb438
commit bf7d47050d
8 changed files with 173 additions and 38 deletions
+7
View File
@@ -18,6 +18,13 @@ NEXT_PUBLIC_API_CONNECT_TIMEOUT=30000
NEXT_PUBLIC_API_RECEIVE_TIMEOUT=60000
NEXT_PUBLIC_API_SEND_TIMEOUT=30000
# Sentry observability
NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE=1
NEXT_PUBLIC_SENTRY_REPLAYS_SESSION_SAMPLE_RATE=0.1
NEXT_PUBLIC_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE=1
NEXT_PUBLIC_SENTRY_SEND_DEFAULT_PII=false
NEXT_PUBLIC_SENTRY_ENABLE_LOGS=true
# next-auth v4Auth.js)—— JWT 签名密钥
# dev 环境独立密钥(**不**与 test / prod 共用)
AUTH_SECRET=PinzhujxR/qMPrY7bAor8qQlwmmy1tJJCex9Tomhk2c=
+7
View File
@@ -11,6 +11,13 @@ NEXT_PUBLIC_API_CONNECT_TIMEOUT=30000
NEXT_PUBLIC_API_RECEIVE_TIMEOUT=60000
NEXT_PUBLIC_API_SEND_TIMEOUT=30000
# Sentry observability
NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE=1
NEXT_PUBLIC_SENTRY_REPLAYS_SESSION_SAMPLE_RATE=0.1
NEXT_PUBLIC_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE=1
NEXT_PUBLIC_SENTRY_SEND_DEFAULT_PII=false
NEXT_PUBLIC_SENTRY_ENABLE_LOGS=true
# next-auth v4Auth.js)—— JWT 签名密钥
# test 环境独立密钥(**不**与 dev / prod 共用)
AUTH_SECRET=rKGeHQ/8tKPoh12Kiedil5zHuj9ZfPA0NwY9bgwRqkE=
+7
View File
@@ -19,6 +19,13 @@ NEXT_PUBLIC_API_CONNECT_TIMEOUT=30000
NEXT_PUBLIC_API_RECEIVE_TIMEOUT=60000
NEXT_PUBLIC_API_SEND_TIMEOUT=30000
# Sentry observability
NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE=0.1
NEXT_PUBLIC_SENTRY_REPLAYS_SESSION_SAMPLE_RATE=0.01
NEXT_PUBLIC_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE=1
NEXT_PUBLIC_SENTRY_SEND_DEFAULT_PII=false
NEXT_PUBLIC_SENTRY_ENABLE_LOGS=true
# next-auth v4Auth.js)—— JWT 签名密钥
# production 环境独立密钥(**最**重要,**签**发**用**户 session cookie
AUTH_SECRET=XypsCWoLCNmwOaq4taIMJHe4+cU9YZBaYN4VMTtmSik=
+2 -12
View File
@@ -5,16 +5,6 @@
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: "https://a9390ae6d4b06c851854d48b070bb4cf@o4511511166713856.ingest.us.sentry.io/4511612532228096",
import { getSharedSentryOptions } from "@/utils/sentry-config";
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
tracesSampleRate: 1,
// Enable logs to be sent to Sentry
enableLogs: true,
// Enable sending user PII (Personally Identifiable Information)
// https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/options/#sendDefaultPii
sendDefaultPii: true,
});
Sentry.init(getSharedSentryOptions());
+2 -12
View File
@@ -4,16 +4,6 @@
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: "https://a9390ae6d4b06c851854d48b070bb4cf@o4511511166713856.ingest.us.sentry.io/4511612532228096",
import { getSharedSentryOptions } from "@/utils/sentry-config";
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
tracesSampleRate: 1,
// Enable logs to be sent to Sentry
enableLogs: true,
// Enable sending user PII (Personally Identifiable Information)
// https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/options/#sendDefaultPii
sendDefaultPii: true,
});
Sentry.init(getSharedSentryOptions());
+8 -14
View File
@@ -3,29 +3,23 @@
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
import {
getSentryReplayOnErrorSampleRate,
getSentryReplaySessionSampleRate,
getSharedSentryOptions,
} from "@/utils/sentry-config";
Sentry.init({
dsn: "https://a9390ae6d4b06c851854d48b070bb4cf@o4511511166713856.ingest.us.sentry.io/4511612532228096",
...getSharedSentryOptions(),
// Add optional integrations for additional features
integrations: [Sentry.replayIntegration()],
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
tracesSampleRate: 1,
// Enable logs to be sent to Sentry
enableLogs: true,
// Define how likely Replay events are sampled.
// This sets the sample rate to be 10%. You may want this to be 100% while
// in development and sample at a lower rate in production
replaysSessionSampleRate: 0.1,
replaysSessionSampleRate: getSentryReplaySessionSampleRate(),
// Define how likely Replay events are sampled when an error occurs.
replaysOnErrorSampleRate: 1.0,
// Enable sending user PII (Personally Identifiable Information)
// https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/options/#sendDefaultPii
sendDefaultPii: true,
replaysOnErrorSampleRate: getSentryReplayOnErrorSampleRate(),
});
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
+58
View File
@@ -0,0 +1,58 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import {
getSentryReplaySessionSampleRate,
getSharedSentryOptions,
readBoolean,
readSampleRate,
} from "../sentry-config";
describe("sentry config", () => {
afterEach(() => {
vi.unstubAllEnvs();
});
it("uses conservative production defaults", () => {
vi.stubEnv("NEXT_PUBLIC_APP_ENV", "production");
expect(getSharedSentryOptions()).toMatchObject({
tracesSampleRate: 0.1,
enableLogs: true,
sendDefaultPii: false,
});
expect(getSentryReplaySessionSampleRate()).toBe(0.01);
});
it("keeps non-production sampling high for local debugging", () => {
vi.stubEnv("NEXT_PUBLIC_APP_ENV", "development");
expect(getSharedSentryOptions().tracesSampleRate).toBe(1);
expect(getSentryReplaySessionSampleRate()).toBe(0.1);
});
it("allows valid environment overrides", () => {
vi.stubEnv("NEXT_PUBLIC_APP_ENV", "production");
vi.stubEnv("NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE", "0.25");
vi.stubEnv("NEXT_PUBLIC_SENTRY_ENABLE_LOGS", "false");
vi.stubEnv("NEXT_PUBLIC_SENTRY_SEND_DEFAULT_PII", "true");
vi.stubEnv("NEXT_PUBLIC_SENTRY_REPLAYS_SESSION_SAMPLE_RATE", "0.05");
expect(getSharedSentryOptions()).toMatchObject({
tracesSampleRate: 0.25,
enableLogs: false,
sendDefaultPii: true,
});
expect(getSentryReplaySessionSampleRate()).toBe(0.05);
});
it("falls back for invalid sample rates and booleans", () => {
expect(readSampleRate("1.2", 0.1)).toBe(0.1);
expect(readSampleRate("-0.1", 0.1)).toBe(0.1);
expect(readSampleRate("abc", 0.1)).toBe(0.1);
expect(readSampleRate("0.5", 0.1)).toBe(0.5);
expect(readBoolean("maybe", false)).toBe(false);
expect(readBoolean("yes", false)).toBe(true);
expect(readBoolean("off", true)).toBe(false);
});
});
+82
View File
@@ -0,0 +1,82 @@
const SENTRY_DSN =
"https://a9390ae6d4b06c851854d48b070bb4cf@o4511511166713856.ingest.us.sentry.io/4511612532228096";
const PRODUCTION_TRACE_SAMPLE_RATE = 0.1;
const NON_PRODUCTION_TRACE_SAMPLE_RATE = 1;
const PRODUCTION_REPLAY_SESSION_SAMPLE_RATE = 0.01;
const NON_PRODUCTION_REPLAY_SESSION_SAMPLE_RATE = 0.1;
const DEFAULT_REPLAY_ON_ERROR_SAMPLE_RATE = 1;
export interface SharedSentryOptions {
dsn: string;
tracesSampleRate: number;
enableLogs: boolean;
sendDefaultPii: boolean;
}
export function getSharedSentryOptions(): SharedSentryOptions {
const isProduction = process.env.NEXT_PUBLIC_APP_ENV === "production";
return {
dsn: SENTRY_DSN,
tracesSampleRate: readSampleRate(
process.env.NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE ??
process.env.SENTRY_TRACES_SAMPLE_RATE,
isProduction
? PRODUCTION_TRACE_SAMPLE_RATE
: NON_PRODUCTION_TRACE_SAMPLE_RATE,
),
enableLogs: readBoolean(
process.env.NEXT_PUBLIC_SENTRY_ENABLE_LOGS ??
process.env.SENTRY_ENABLE_LOGS,
true,
),
sendDefaultPii: readBoolean(
process.env.NEXT_PUBLIC_SENTRY_SEND_DEFAULT_PII ??
process.env.SENTRY_SEND_DEFAULT_PII,
false,
),
};
}
export function getSentryReplaySessionSampleRate(): number {
const isProduction = process.env.NEXT_PUBLIC_APP_ENV === "production";
return readSampleRate(
process.env.NEXT_PUBLIC_SENTRY_REPLAYS_SESSION_SAMPLE_RATE,
isProduction
? PRODUCTION_REPLAY_SESSION_SAMPLE_RATE
: NON_PRODUCTION_REPLAY_SESSION_SAMPLE_RATE,
);
}
export function getSentryReplayOnErrorSampleRate(): number {
return readSampleRate(
process.env.NEXT_PUBLIC_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE,
DEFAULT_REPLAY_ON_ERROR_SAMPLE_RATE,
);
}
export function readSampleRate(
value: string | undefined,
fallback: number,
): number {
if (value === undefined || value.trim().length === 0) return fallback;
const parsed = Number(value);
if (!Number.isFinite(parsed)) return fallback;
if (parsed < 0 || parsed > 1) return fallback;
return parsed;
}
export function readBoolean(
value: string | undefined,
fallback: boolean,
): boolean {
if (value === undefined || value.trim().length === 0) return fallback;
const normalized = value.trim().toLowerCase();
if (["1", "true", "yes", "on"].includes(normalized)) return true;
if (["0", "false", "no", "off"].includes(normalized)) return false;
return fallback;
}