mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-07-14 18:15:56 -04:00
feat(native): add Sentry native crash reporting with privacy-conscious defaults
Adds @sentry/react-native for native crash reporting (segfaults, OOM, ANRs, watchdog kills) that PostHog's JS-level tracking can't capture. Privacy measures: - sendDefaultPii: false, no tracing/replay/profiling/screenshots - beforeSend strips user/request data from JS errors - beforeBreadcrumb drops navigation/http breadcrumbs - Screenshot, ViewHierarchy, UserInteraction integrations filtered out Crash reporting has its own toggle in Settings, independent from the existing analytics toggle. Enabled by default, takes effect on restart since native crash handlers must be installed at init time. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
EXPO_PUBLIC_POSTHOG_KEY=
|
||||
EXPO_PUBLIC_POSTHOG_HOST=https://public-api.sofa.watch/ingest
|
||||
EXPO_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
|
||||
EXPO_PUBLIC_SENTRY_DSN=
|
||||
|
||||
@@ -128,6 +128,13 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
[
|
||||
"@sentry/react-native/expo",
|
||||
{
|
||||
"organization": "jarvis-0a",
|
||||
"project": "sofa-native"
|
||||
}
|
||||
]
|
||||
],
|
||||
"experiments": {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const { getDefaultConfig } = require("expo/metro-config");
|
||||
const { withSentryConfig } = require("@sentry/react-native/metro");
|
||||
const { withUniwindConfig } = require("uniwind/metro");
|
||||
const { wrapWithReanimatedMetroConfig } = require("react-native-reanimated/metro-config");
|
||||
|
||||
@@ -26,7 +27,9 @@ config.resolver = {
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = withUniwindConfig(wrapWithReanimatedMetroConfig(config), {
|
||||
cssEntryFile: "./src/global.css",
|
||||
dtsFile: "./uniwind-types.d.ts",
|
||||
});
|
||||
module.exports = withSentryConfig(
|
||||
withUniwindConfig(wrapWithReanimatedMetroConfig(config), {
|
||||
cssEntryFile: "./src/global.css",
|
||||
dtsFile: "./uniwind-types.d.ts",
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"@react-native-menu/menu": "2.0.0",
|
||||
"@react-navigation/elements": "2.9.11",
|
||||
"@react-navigation/native": "7.1.34",
|
||||
"@sentry/react-native": "7.11.0",
|
||||
"@shopify/flash-list": "2.0.2",
|
||||
"@sofa/api": "workspace:*",
|
||||
"@sofa/i18n": "workspace:*",
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Trans, useLingui } from "@lingui/react/macro";
|
||||
import {
|
||||
IconArrowUpRight,
|
||||
IconBrandGithub,
|
||||
IconBug,
|
||||
IconCamera,
|
||||
IconChartBar,
|
||||
IconCloud,
|
||||
@@ -53,6 +54,7 @@ import { setPersistedLocale } from "@/lib/i18n";
|
||||
import { orpc } from "@/lib/orpc";
|
||||
import { isAnalyticsEnabled, setAnalyticsEnabled } from "@/lib/posthog";
|
||||
import { queryClient } from "@/lib/query-client";
|
||||
import { isCrashReportingEnabled, setCrashReportingEnabled } from "@/lib/sentry";
|
||||
import { authClient, getServerUrl, requestServerChange } from "@/lib/server";
|
||||
import { toast } from "@/lib/toast";
|
||||
import { activateLocale, isLocaleRTL, type SupportedLocale } from "@sofa/i18n";
|
||||
@@ -83,6 +85,7 @@ export default function SettingsScreen() {
|
||||
const [languageModalOpen, setLanguageModalOpen] = useState(false);
|
||||
const languageLabel = LOCALE_INFO.find((o) => o.code === i18n.locale)?.nativeName ?? i18n.locale;
|
||||
const [analyticsEnabled, setAnalyticsToggle] = useState(isAnalyticsEnabled);
|
||||
const [crashReportingEnabled, setCrashReportingToggle] = useState(isCrashReportingEnabled);
|
||||
|
||||
const isAdmin = session?.user?.role === "admin";
|
||||
const serverUrl = getServerUrl();
|
||||
@@ -404,6 +407,21 @@ export default function SettingsScreen() {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<SettingsRow
|
||||
label={t`Crash reporting`}
|
||||
icon={IconBug}
|
||||
right={
|
||||
<Switch
|
||||
value={crashReportingEnabled}
|
||||
accessibilityLabel={t`Crash reporting`}
|
||||
onValueChange={(enabled) => {
|
||||
setCrashReportingToggle(enabled);
|
||||
setCrashReportingEnabled(enabled);
|
||||
toast.info(t`Takes effect after restarting the app`);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SettingsSection>
|
||||
</Animated.View>
|
||||
|
||||
|
||||
@@ -25,13 +25,15 @@ import { useWidgetRefresh } from "@/hooks/use-widget-refresh";
|
||||
import { initLocale } from "@/lib/i18n";
|
||||
import { initAnalytics, posthog } from "@/lib/posthog";
|
||||
import { queryClient } from "@/lib/query-client";
|
||||
import { getScopeKey, initialize, onStorageScopeChange, queryPersister } from "@/lib/server";
|
||||
import { initSentry, Sentry } from "@/lib/sentry";
|
||||
import { getScopeKey, initSession, onStorageScopeChange, queryPersister } from "@/lib/server";
|
||||
import { sofaTheme } from "@/lib/theme";
|
||||
import { i18n } from "@sofa/i18n";
|
||||
|
||||
SplashScreen.preventAutoHideAsync();
|
||||
enableFreeze(true);
|
||||
initialize();
|
||||
initSession();
|
||||
initSentry();
|
||||
const localeReady = initLocale();
|
||||
|
||||
const changePasswordOptions =
|
||||
@@ -187,7 +189,7 @@ function QueryProvider({ children }: { children: React.ReactNode }) {
|
||||
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
|
||||
}
|
||||
|
||||
export default function RootLayout() {
|
||||
function RootLayout() {
|
||||
const inner = (
|
||||
<I18nProvider i18n={i18n}>
|
||||
<QueryProvider>
|
||||
@@ -210,3 +212,5 @@ export default function RootLayout() {
|
||||
</PostHogProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default Sentry.wrap(RootLayout);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { msg } from "@lingui/core/macro";
|
||||
import * as Sentry from "@sentry/react-native";
|
||||
import { QueryCache, QueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { posthog } from "@/lib/posthog";
|
||||
@@ -31,6 +32,7 @@ export const queryClient = new QueryClient({
|
||||
|
||||
toast.error(i18n._(msg`Something went wrong\u2026`));
|
||||
posthog?.captureException(error, { source: "react-query" });
|
||||
Sentry.captureException(error, { tags: { source: "react-query" } });
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import * as Sentry from "@sentry/react-native";
|
||||
|
||||
import { globalStorage } from "@/lib/mmkv";
|
||||
|
||||
const CRASH_REPORTING_ENABLED_KEY = "sofa_crash_reporting_enabled";
|
||||
const CRASH_REPORTING_EXPLICIT_KEY = "sofa_crash_reporting_explicit";
|
||||
|
||||
const sentryDsn = process.env.EXPO_PUBLIC_SENTRY_DSN ?? "";
|
||||
|
||||
/** Whether the user has explicitly set a crash reporting preference. */
|
||||
export function hasExplicitCrashPreference(): boolean {
|
||||
return globalStorage.getBoolean(CRASH_REPORTING_EXPLICIT_KEY) === true;
|
||||
}
|
||||
|
||||
/** Current crash reporting enabled state (explicit preference or default=true). */
|
||||
export function isCrashReportingEnabled(): boolean {
|
||||
return globalStorage.getBoolean(CRASH_REPORTING_ENABLED_KEY) ?? true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the settings toggle — marks the preference as explicit.
|
||||
* Takes effect on next app restart (Sentry native handlers are installed at init).
|
||||
*/
|
||||
export function setCrashReportingEnabled(enabled: boolean): void {
|
||||
globalStorage.set(CRASH_REPORTING_ENABLED_KEY, enabled);
|
||||
globalStorage.set(CRASH_REPORTING_EXPLICIT_KEY, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Sentry. Must be called at module scope in the root layout
|
||||
* (before any React renders) so native crash handlers are installed early.
|
||||
*
|
||||
* The `enabled` flag is read synchronously from MMKV. Toggling the
|
||||
* setting in Settings takes effect on the next app restart.
|
||||
*/
|
||||
export function initSentry(): void {
|
||||
if (!sentryDsn) return;
|
||||
|
||||
Sentry.init({
|
||||
dsn: sentryDsn,
|
||||
enabled: isCrashReportingEnabled(),
|
||||
|
||||
// --- Privacy ---
|
||||
sendDefaultPii: false,
|
||||
|
||||
// --- Crashes only: disable performance/session features ---
|
||||
tracesSampleRate: 0,
|
||||
profilesSampleRate: 0,
|
||||
replaysSessionSampleRate: 0,
|
||||
replaysOnErrorSampleRate: 0,
|
||||
|
||||
attachScreenshot: false,
|
||||
attachViewHierarchy: false,
|
||||
|
||||
beforeBreadcrumb(breadcrumb) {
|
||||
// Keep only console errors and sentry-internal breadcrumbs.
|
||||
// Drop navigation, http, UI tap, etc. to avoid leaking screen params.
|
||||
if (breadcrumb.category === "console" && breadcrumb.level === "error") {
|
||||
return breadcrumb;
|
||||
}
|
||||
if (breadcrumb.category === "sentry") {
|
||||
return breadcrumb;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
// PII scrubbing for JS errors (does NOT run for native crashes, but
|
||||
// native crashes only contain stack traces + device info, no PII).
|
||||
beforeSend(event) {
|
||||
delete event.user;
|
||||
delete event.request;
|
||||
|
||||
if (event.breadcrumbs) {
|
||||
for (const crumb of event.breadcrumbs) {
|
||||
if (crumb.data) {
|
||||
delete crumb.data.params;
|
||||
delete crumb.data.query;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return event;
|
||||
},
|
||||
|
||||
integrations(integrations) {
|
||||
return integrations.filter((i) => {
|
||||
if (i.name === "Screenshot") return false;
|
||||
if (i.name === "ViewHierarchy") return false;
|
||||
if (i.name === "UserInteraction") return false;
|
||||
return true;
|
||||
});
|
||||
},
|
||||
|
||||
environment: __DEV__ ? "development" : "production",
|
||||
});
|
||||
}
|
||||
|
||||
export { Sentry };
|
||||
@@ -386,7 +386,7 @@ let _cachedSessionSeeded = false;
|
||||
* Seed the Better Auth session atom from SecureStore before React renders.
|
||||
* Call at module scope in the root layout. Idempotent.
|
||||
*/
|
||||
export function initialize(): void {
|
||||
export function initSession(): void {
|
||||
const cached = getCachedSession();
|
||||
if (cached) {
|
||||
_cachedSessionSeeded = true;
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"@sofa/i18n": "workspace:*",
|
||||
"@tabler/icons-react": "3.40.0",
|
||||
"@tanstack/react-form": "catalog:",
|
||||
"@tanstack/react-hotkeys": "0.5.1",
|
||||
"@tanstack/react-hotkeys": "0.6.0",
|
||||
"@tanstack/react-query": "catalog:",
|
||||
"@tanstack/react-router": "1.168.3",
|
||||
"@tanstack/zod-adapter": "1.166.9",
|
||||
@@ -37,7 +37,7 @@
|
||||
"class-variance-authority": "0.7.1",
|
||||
"clsx": "2.1.1",
|
||||
"cmdk": "1.1.1",
|
||||
"jotai": "2.18.1",
|
||||
"jotai": "2.19.0",
|
||||
"motion": "12.38.0",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
|
||||
Reference in New Issue
Block a user