Files
openhuman/src/services/analytics.ts
T
Steven Enamakel af39ee40f4 Add Sentry integration for error reporting and user analytics consent
- Introduced Sentry for error tracking, initialized in the application to capture errors while respecting user privacy.
- Added a new analytics service to manage user consent for data collection, ensuring only non-personal data is sent to Sentry.
- Updated Redux state to track user analytics preferences and modified onboarding steps to include analytics consent.
- Enhanced the PrivacyPanel component to allow users to toggle analytics sharing.
- Removed the AnalyticsStep from onboarding as its functionality is now integrated into the PrivacyPanel.

This update improves error reporting capabilities while prioritizing user privacy and consent.
2026-01-31 01:01:54 +05:30

136 lines
4.3 KiB
TypeScript

/**
* Analytics & Sentry service
*
* Manages Sentry error reporting gated behind user analytics consent.
* Designed to capture ONLY:
* - Error message & stack trace
* - Device / browser metadata (via Sentry's default user-agent parsing)
* - Source file location (via source maps)
*
* Explicitly strips:
* - All breadcrumbs (console, click, network, etc.)
* - Redux state / localStorage / sessionStorage
* - User PII (IP address, cookies)
* - Request bodies / headers
* - Session replay
*/
import * as Sentry from "@sentry/react";
import { store } from "../store";
const SENTRY_DSN = import.meta.env.VITE_SENTRY_DSN as string | undefined;
const IS_DEV = Boolean(import.meta.env.DEV) || import.meta.env.MODE === "development";
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/** Check if the current user has opted into analytics. */
export function isAnalyticsEnabled(): boolean {
const state = store.getState();
const userId = state.user?.user?._id;
if (!userId) return false;
return state.auth.isAnalyticsEnabledByUser[userId] === true;
}
// ---------------------------------------------------------------------------
// Sentry initialisation
// ---------------------------------------------------------------------------
export function initSentry(): void {
if (!SENTRY_DSN) return;
Sentry.init({
dsn: SENTRY_DSN,
environment: IS_DEV ? "development" : "production",
enabled: !IS_DEV, // disable in dev builds
// -----------------------------------------------------------------------
// Privacy: disable EVERYTHING that could leak sensitive state
// -----------------------------------------------------------------------
// No session replay
replaysSessionSampleRate: 0,
replaysOnErrorSampleRate: 0,
// No performance / tracing
tracesSampleRate: 0,
// No breadcrumbs at all (console, clicks, network, etc.)
defaultIntegrations: false,
integrations: [
// Only keep the bare-minimum integrations for stack traces
Sentry.functionToStringIntegration(),
Sentry.linkedErrorsIntegration(),
Sentry.dedupeIntegration(),
Sentry.browserApiErrorsIntegration(),
Sentry.globalHandlersIntegration(),
],
// Strip IP address
sendDefaultPii: false,
// -----------------------------------------------------------------------
// Gate every event behind the user's analytics consent flag
// -----------------------------------------------------------------------
beforeSend(event) {
if (!isAnalyticsEnabled()) return null;
// Strip any breadcrumbs that somehow snuck in
event.breadcrumbs = [];
// Strip request data (cookies, headers, body)
delete event.request;
// Strip user PII — keep only a stable anonymous ID
const userId = store.getState().user?.user?._id;
event.user = userId ? { id: userId } : undefined;
// Strip any extra/contexts that could contain Redux or localStorage data
delete event.extra;
event.contexts = {
// Keep only OS / browser / device metadata
os: event.contexts?.os,
browser: event.contexts?.browser,
device: event.contexts?.device,
};
return event;
},
beforeSendTransaction() {
// Block all transactions (performance traces)
return null;
},
// Ignore common non-actionable errors
ignoreErrors: [
"ResizeObserver loop",
"Network request failed",
"Load failed",
"AbortError",
],
});
}
// ---------------------------------------------------------------------------
// Consent sync — call when the user toggles analytics on/off
// ---------------------------------------------------------------------------
/**
* Re-sync Sentry's enabled state after the user changes their consent.
* Called from onboarding and settings.
*/
export function syncAnalyticsConsent(enabled: boolean): void {
const client = Sentry.getClient();
if (!client) return;
if (enabled) {
// Client is already initialised; events will pass through beforeSend
// because isAnalyticsEnabled() will now return true.
} else {
// Flush any pending events, then future events will be dropped by beforeSend.
void Sentry.flush(2000);
}
}