From c1b220f0078db8d07b3d58a7ac146d8af159a17d Mon Sep 17 00:00:00 2001 From: devthejo Date: Tue, 8 Jul 2025 18:45:12 +0200 Subject: [PATCH] feat: optout sentry reporting --- src/app/index.js | 3 +- src/scenes/Params/SentryOptOut.js | 78 ++++++++++++ src/scenes/Params/View.js | 4 + src/sentry/index.js | 192 ++++++++++++++++++++++-------- src/storage/storageKeys.js | 1 + src/stores/params.js | 51 ++++++++ 6 files changed, 276 insertions(+), 53 deletions(-) create mode 100644 src/scenes/Params/SentryOptOut.js diff --git a/src/app/index.js b/src/app/index.js index 8b2f401..a54a5c8 100644 --- a/src/app/index.js +++ b/src/app/index.js @@ -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"); diff --git a/src/scenes/Params/SentryOptOut.js b/src/scenes/Params/SentryOptOut.js new file mode 100644 index 0000000..b15bb6e --- /dev/null +++ b/src/scenes/Params/SentryOptOut.js @@ -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 ( + + Rapport d'erreurs + + + Envoyer les rapports d'erreurs + + + + Les rapports d'erreurs nous aident à améliorer l'application en nous + permettant de mieux identifier et corriger les problèmes techniques. + + + + ); +} + +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; diff --git a/src/scenes/Params/View.js b/src/scenes/Params/View.js index 4b0c0a9..3c89035 100644 --- a/src/scenes/Params/View.js +++ b/src/scenes/Params/View.js @@ -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 }) { + + + diff --git a/src/sentry/index.js b/src/sentry/index.js index 67e1e79..284155e 100644 --- a/src/sentry/index.js +++ b/src/sentry/index.js @@ -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, + }); + } +}; diff --git a/src/storage/storageKeys.js b/src/storage/storageKeys.js index f616faa..1256575 100644 --- a/src/storage/storageKeys.js +++ b/src/storage/storageKeys.js @@ -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"), }; diff --git a/src/stores/params.js b/src/stores/params.js index eedb4b2..25e61b5 100644 --- a/src/stores/params.js +++ b/src/stores/params.js @@ -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, }, }; });