feat: optout sentry reporting

This commit is contained in:
devthejo 2025-07-08 18:45:12 +02:00
parent 754e14946c
commit c1b220f007
Signed by: devthejo
GPG key ID: 00CCA7A92B1D5351
6 changed files with 276 additions and 53 deletions

View file

@ -6,7 +6,7 @@ import { ErrorUtils } from "react-native";
import { createLogger } from "~/lib/logger";
import { SYSTEM_SCOPES } from "~/lib/logger/scopes";
import { authActions, permissionWizardActions } from "~/stores";
import { authActions, permissionWizardActions, paramsActions } from "~/stores";
import { secureStore } from "~/storage/memorySecureStore";
import memoryAsyncStorage from "~/storage/memoryAsyncStorage";
@ -62,6 +62,7 @@ const initializeStores = () => {
// Then initialize other stores sequentially
initializeStore("authActions", authActions.init);
initializeStore("permissionWizard", permissionWizardActions.init);
initializeStore("paramsActions", paramsActions.init);
initializeStore("storeSubscriptions", storeSubscriptions.init);
appLogger.info("Core initialization complete");

View file

@ -0,0 +1,78 @@
import React, { useCallback } from "react";
import { View } from "react-native";
import { Title, Switch } from "react-native-paper";
import { createStyles } from "~/theme";
import { useParamsState, paramsActions } from "~/stores";
import Text from "~/components/Text";
import { setSentryEnabled } from "~/sentry";
function SentryOptOut() {
const styles = useStyles();
const { sentryEnabled } = useParamsState(["sentryEnabled"]);
const handleToggle = useCallback(async () => {
const newValue = !sentryEnabled;
await paramsActions.setSentryEnabled(newValue);
// Dynamically enable/disable Sentry
setSentryEnabled(newValue);
}, [sentryEnabled]);
return (
<View style={styles.container}>
<Title style={styles.title}>Rapport d'erreurs</Title>
<View style={styles.content}>
<View style={styles.switchContainer}>
<Text style={styles.label}>Envoyer les rapports d'erreurs</Text>
<Switch
value={sentryEnabled}
onValueChange={handleToggle}
style={styles.switch}
/>
</View>
<Text style={styles.description}>
Les rapports d'erreurs nous aident à améliorer l'application en nous
permettant de mieux identifier et corriger les problèmes techniques.
</Text>
</View>
</View>
);
}
const useStyles = createStyles(({ theme: { colors } }) => ({
container: {
width: "100%",
alignItems: "center",
},
title: {
fontSize: 20,
fontWeight: "bold",
marginVertical: 15,
},
content: {
width: "100%",
},
switchContainer: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
marginBottom: 5,
paddingVertical: 5,
},
label: {
fontSize: 16,
flex: 1,
marginRight: 15,
},
switch: {
flexShrink: 0,
},
description: {
fontSize: 14,
color: colors.onSurfaceVariant,
textAlign: "left",
lineHeight: 20,
},
}));
export default SentryOptOut;

View file

@ -6,6 +6,7 @@ import ParamsRadius from "./Radius";
import ParamsEmergencyCall from "./EmergencyCall";
import ThemeSwitcher from "./ThemeSwitcher";
import Permissions from "./Permissions";
import SentryOptOut from "./SentryOptOut";
export default function ParamsView({ data }) {
const styles = useStyles();
@ -25,6 +26,9 @@ export default function ParamsView({ data }) {
<View style={styles.section}>
<ParamsRadius data={data} />
</View>
<View style={styles.section}>
<SentryOptOut />
</View>
<View style={styles.section}>
<Permissions />
</View>

View file

@ -3,6 +3,15 @@ import { Platform } from "react-native";
import env from "~/env";
import packageJson from "../../package.json";
import memoryAsyncStorage from "~/storage/memoryAsyncStorage";
import { STORAGE_KEYS } from "~/storage/storageKeys";
import { createLogger } from "~/lib/logger";
import { SYSTEM_SCOPES } from "~/lib/logger/scopes";
const sentryLogger = createLogger({
module: SYSTEM_SCOPES.APP,
feature: "sentry",
});
// Get the build number from native code
const getBuildNumber = () => {
@ -23,60 +32,139 @@ const getReleaseVersion = () => {
return `com.alertesecours@${version}+${buildNumber}`;
};
Sentry.init({
dsn: env.SENTRY_DSN,
tracesSampleRate: 0.1,
debug: __DEV__,
// Configure release to match ios-archive.sh format
release: getReleaseVersion(),
// Use BUILD_TIME from env to match the value used in sourcemap upload
dist: env.BUILD_TIME,
enableNative: true,
attachStacktrace: true,
environment: __DEV__ ? "development" : "production",
normalizeDepth: 10,
maxBreadcrumbs: 100,
// Enable debug ID tracking
_experiments: {
debugIds: true,
},
beforeSend(event) {
event.extra = {
...event.extra,
jsEngine: global.HermesInternal ? "hermes" : "jsc",
hermesEnabled: !!global.HermesInternal,
version: packageJson.version,
buildNumber: getBuildNumber(),
buildTime: env.BUILD_TIME,
};
// Check if Sentry is enabled by user preference
const checkSentryEnabled = async () => {
try {
// Wait for memory storage to be initialized
let retries = 0;
const maxRetries = 10;
if (event.exception) {
event.exception.values = event.exception.values?.map((value) => ({
...value,
mechanism: {
...value.mechanism,
handled: true,
synthetic: false,
type: "hermes",
},
}));
while (retries < maxRetries) {
try {
const stored = await memoryAsyncStorage.getItem(
STORAGE_KEYS.SENTRY_ENABLED,
);
if (stored !== null) {
return JSON.parse(stored);
}
break; // Storage is ready, no preference stored
} catch (error) {
if (
error.message?.includes("not initialized") &&
retries < maxRetries - 1
) {
// Wait a bit and retry if storage not initialized
await new Promise((resolve) => setTimeout(resolve, 100));
retries++;
continue;
}
sentryLogger.warn("Failed to check Sentry preference", {
error: error.message,
});
break;
}
}
} catch (error) {
sentryLogger.warn("Failed to check Sentry preference", {
error: error.message,
});
}
// Default to enabled if no preference stored or error occurred
return true;
};
return event;
},
beforeBreadcrumb(breadcrumb) {
if (breadcrumb.category === "console") {
// Initialize Sentry with user preference check
const initializeSentry = async () => {
const isEnabled = await checkSentryEnabled();
Sentry.init({
dsn: env.SENTRY_DSN,
enabled: isEnabled,
tracesSampleRate: 0.1,
debug: __DEV__,
// Configure release to match ios-archive.sh format
release: getReleaseVersion(),
// Use BUILD_TIME from env to match the value used in sourcemap upload
dist: env.BUILD_TIME,
enableNative: true,
attachStacktrace: true,
environment: __DEV__ ? "development" : "production",
normalizeDepth: 10,
maxBreadcrumbs: 100,
// Enable debug ID tracking
_experiments: {
debugIds: true,
},
beforeSend(event) {
event.extra = {
...event.extra,
jsEngine: global.HermesInternal ? "hermes" : "jsc",
hermesEnabled: !!global.HermesInternal,
version: packageJson.version,
buildNumber: getBuildNumber(),
buildTime: env.BUILD_TIME,
};
if (event.exception) {
event.exception.values = event.exception.values?.map((value) => ({
...value,
mechanism: {
...value.mechanism,
handled: true,
synthetic: false,
type: "hermes",
},
}));
}
return event;
},
beforeBreadcrumb(breadcrumb) {
if (breadcrumb.category === "console") {
return breadcrumb;
}
return breadcrumb;
}
return breadcrumb;
},
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
integrations: [
// Sentry.mobileReplayIntegration({
// maskAllText: false,
// maskAllImages: false,
// maskAllVectors: false,
// }),
],
},
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
integrations: [
// Sentry.mobileReplayIntegration({
// maskAllText: false,
// maskAllImages: false,
// maskAllVectors: false,
// }),
],
});
};
// Initialize Sentry asynchronously
initializeSentry().catch((error) => {
sentryLogger.warn("Failed to initialize Sentry", {
error: error.message,
});
});
// Export function to dynamically control Sentry
export const setSentryEnabled = (enabled) => {
try {
// Use the newer Sentry API
const client = Sentry.getClient();
if (client) {
const options = client.getOptions();
options.enabled = enabled;
if (!enabled) {
// Clear any pending events when disabling
Sentry.withScope((scope) => {
scope.clear();
});
}
sentryLogger.info("Sentry state toggled", { enabled });
} else {
sentryLogger.warn("Sentry client not available for toggling");
}
} catch (error) {
sentryLogger.warn("Failed to toggle Sentry state", {
error: error.message,
});
}
};

View file

@ -80,4 +80,5 @@ export const STORAGE_KEYS = {
LAST_KNOWN_LOCATION: registerAsyncStorageKey("@last_known_location"),
EULA_ACCEPTED_SIMPLE: registerAsyncStorageKey("eula_accepted"),
EMULATOR_MODE_ENABLED: registerAsyncStorageKey("emulator_mode_enabled"),
SENTRY_ENABLED: registerAsyncStorageKey("@sentry_enabled"),
};

View file

@ -1,4 +1,13 @@
import { createAtom } from "~/lib/atomic-zustand";
import memoryAsyncStorage from "~/storage/memoryAsyncStorage";
import { STORAGE_KEYS } from "~/storage/storageKeys";
import { createLogger } from "~/lib/logger";
import { SYSTEM_SCOPES } from "~/lib/logger/scopes";
const paramsLogger = createLogger({
module: SYSTEM_SCOPES.APP,
feature: "params",
});
export default createAtom(({ merge, reset }) => {
const setDevModeEnabled = (b) => {
@ -37,6 +46,45 @@ export default createAtom(({ merge, reset }) => {
});
};
const setSentryEnabled = async (sentryEnabled) => {
merge({
sentryEnabled,
});
// Persist to storage
try {
await memoryAsyncStorage.setItem(
STORAGE_KEYS.SENTRY_ENABLED,
JSON.stringify(sentryEnabled),
);
} catch (error) {
paramsLogger.warn("Failed to persist Sentry preference", {
error: error.message,
});
}
};
const initSentryEnabled = async () => {
try {
const stored = await memoryAsyncStorage.getItem(
STORAGE_KEYS.SENTRY_ENABLED,
);
if (stored !== null) {
const sentryEnabled = JSON.parse(stored);
merge({ sentryEnabled });
return sentryEnabled;
}
} catch (error) {
paramsLogger.warn("Failed to load Sentry preference", {
error: error.message,
});
}
};
const init = async () => {
await initSentryEnabled();
};
return {
default: {
// devModeEnabled: false,
@ -46,6 +94,7 @@ export default createAtom(({ merge, reset }) => {
mapColorScheme: "auto",
hasRegisteredRelatives: null,
alertListSortBy: "location",
sentryEnabled: true,
},
actions: {
reset,
@ -55,6 +104,8 @@ export default createAtom(({ merge, reset }) => {
setMapColorScheme,
setHasRegisteredRelatives,
setAlertListSortBy,
setSentryEnabled,
init,
},
};
});