import * as Sentry from "@sentry/react";
const environment = import.meta.env.VITE_ENVIRONMENT ?? "development";
const isProd = environment === "production";
export function initSentry(): void {
const dsn = import.meta.env.VITE_SENTRY_DSN;
if (!dsn) return; // no-op in local/preview builds without a DSN
Sentry.init({
dsn,
environment,
release: import.meta.env.VITE_RELEASE,
integrations: [
Sentry.browserTracingIntegration(),
Sentry.replayIntegration({ maskAllText: true, blockAllMedia: true }),
],
tracesSampleRate: isProd ? 0.15 : 1.0,
replaysSessionSampleRate: isProd ? 0.05 : 0,
replaysOnErrorSampleRate: 1.0,
ignoreErrors: [
"ResizeObserver loop limit exceeded",
"Non-Error promise rejection captured",
/extensions\//i,
/Failed to fetch/i,
],
beforeSend(event) {
if (event.request?.cookies) delete event.request.cookies;
const headers = event.request?.headers;
if (headers?.Authorization) headers.Authorization = "[redacted]";
event.breadcrumbs = event.breadcrumbs?.map((crumb) => {
if (crumb.data?.url) {
crumb.data.url = String(crumb.data.url).split("?")[0];
}
return crumb;
});
return event;
},
});
Sentry.setTag("deploy.environment", environment);
}
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { sentryVitePlugin } from "@sentry/vite-plugin";
const release = process.env.RELEASE ?? process.env.VITE_RELEASE;
export default defineConfig({
build: {
// produce maps for upload but do not reference them from shipped JS
sourcemap: "hidden",
},
define: {
"import.meta.env.VITE_RELEASE": JSON.stringify(release),
},
plugins: [
react(),
sentryVitePlugin({
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
release: { name: release },
sourcemaps: {
assets: "./dist/**/*.js",
filesToDeleteAfterUpload: "./dist/**/*.js.map",
},
disable: !process.env.SENTRY_AUTH_TOKEN,
}),
],
});
import * as Sentry from "@sentry/react";
import { Component, ErrorInfo, ReactNode } from "react";
interface Props {
fallback: ReactNode;
children: ReactNode;
}
interface State {
hasError: boolean;
eventId?: string;
}
export class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false };
static getDerivedStateFromError(): State {
return { hasError: true };
}
componentDidCatch(error: Error, info: ErrorInfo): void {
const eventId = Sentry.captureException(error, (scope) => {
scope.setContext("react", { componentStack: info.componentStack });
scope.setLevel("fatal");
return scope;
});
this.setState({ eventId });
}
render(): ReactNode {
if (!this.state.hasError) return this.props.children;
return (
<div role="alert">
{this.props.fallback}
<button
type="button"
onClick={() => Sentry.showReportDialog({ eventId: this.state.eventId })}
>
Report this problem
</button>
</div>
);
}
}
This snippet shows how a production frontend wires up Sentry so that errors are grouped by release, filtered by environment, and stripped of sensitive data before they leave the browser. The core idea of sentry.ts is that a Sentry release is only useful if it exactly matches the version embedded in the shipped bundle and the version used to upload source maps; otherwise stack traces stay minified and errors from different deploys pile into the same issue.
In sentry.ts, import.meta.env.VITE_RELEASE and VITE_ENVIRONMENT are injected at build time so the exact commit SHA travels with the bundle. initSentry bails out early when there is no DSN, which keeps local development and preview builds quiet instead of spamming the dashboard. tracesSampleRate and replaysSessionSampleRate are tuned per environment so production pays for a small sampled slice of performance and replay data while other environments sample everything or nothing. The beforeSend hook is where privacy lives: it runs on every event and drops request.cookies, redacts Authorization headers, and scrubs query strings from breadcrumbs so tokens and emails never reach Sentry. The ignoreErrors list suppresses noisy browser-extension and network chatter that would otherwise drown real signal.
The vite.config.ts tab closes the loop between the runtime release and the build. sentryVitePlugin reads the same RELEASE value, uploads source maps for the matching release, and then deletes them from the output so minified maps are not served publicly. Setting build.sourcemap to hidden produces maps for upload without referencing them in the shipped JS.
Finally, ErrorBoundary.tsx demonstrates capturing render-time errors with Sentry.captureException and attaching component context via scope.setContext, plus a Sentry.showReportDialog escape hatch for user feedback. The trade-off across these files is operational discipline: the release string must be identical in all three places, which is why it is derived from a single environment variable rather than hand-typed. Reach for this pattern whenever deploys are frequent and untagged errors become impossible to attribute to a specific version.
Related snips
package api
import (
"net/http"
"runtime/debug"
)
Expose build metadata for debugging deploys
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
use tracing::{info, instrument};
#[instrument]
fn process_request(user_id: u64) {
info!(user_id, "Processing request");
// Work happens here
tracing for structured logging and distributed tracing
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["form"]
static values = { delay: { type: Number, default: 250 } }
Debounced live search with Stimulus + Turbo Streams
import axios, { AxiosError } from 'axios'
import { v4 as uuidv4 } from 'uuid'
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1',
timeout: 15000,
Axios API client with interceptors
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface FilterState {
search: string
category: string | null
Zustand for lightweight state management
Share this code
Here's the card — post it anywhere.