diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9860820 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +SENTRY_AUTH_TOKEN= \ No newline at end of file diff --git a/app.json b/app.json index c83b7d2..8a1f0aa 100644 --- a/app.json +++ b/app.json @@ -43,6 +43,14 @@ "expo-dev-client", "@react-native-firebase/app", "@react-native-firebase/messaging", + [ + "@sentry/react-native/expo", + { + "url": "https://edumfa.edumfa.io/", + "project": "app", + "organization": "edumfa" + } + ], [ "expo-splash-screen", { diff --git a/bun.lock b/bun.lock index 929506d..2a381ff 100644 --- a/bun.lock +++ b/bun.lock @@ -16,6 +16,7 @@ "@react-native-firebase/app": "^24.1.1", "@react-native-firebase/messaging": "^24.1.1", "@scure/base": "^2.2.0", + "@sentry/react-native": "^8.14.0", "expo": "^56.0.11", "expo-blur": "~56.0.3", "expo-build-properties": "~56.0.18", @@ -684,6 +685,44 @@ "@scure/base": ["@scure/base@2.2.0", "", {}, "sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg=="], + "@sentry-internal/browser-utils": ["@sentry-internal/browser-utils@10.57.0", "", { "dependencies": { "@sentry/core": "10.57.0" } }, "sha512-tXObp954rMTSYKlbftjVXHtNl4t/6ssks3jkqyzmKb+PDPWzabGQO7sWwqVuTjT8Kx/8A3FmriS1bGmqxiJy3A=="], + + "@sentry-internal/feedback": ["@sentry-internal/feedback@10.57.0", "", { "dependencies": { "@sentry/core": "10.57.0" } }, "sha512-ZcF4QhkqGX3iiQSXB2N0N3Awp+j5iqnDRu6PA/qyLFrWqH5ZiiAAgu59OLD9E6XAdg6iFtLYw19MAMZVK8qNOQ=="], + + "@sentry-internal/replay": ["@sentry-internal/replay@10.57.0", "", { "dependencies": { "@sentry-internal/browser-utils": "10.57.0", "@sentry/core": "10.57.0" } }, "sha512-Wmnx/6ABynVH1iwuoNUqJNyjIUqsqoGML7qsyivBRKb5Wo2YQtPOQlQYfxfZSvWzGpcoSVdInkRjDssUQxQEQg=="], + + "@sentry-internal/replay-canvas": ["@sentry-internal/replay-canvas@10.57.0", "", { "dependencies": { "@sentry-internal/replay": "10.57.0", "@sentry/core": "10.57.0" } }, "sha512-zsfa4JcfV0AEc9YhNxNabd5lSZL2Av84saAyexGAqcHs+67m9Gd0cGStOzMb/nCl7UAtmdP0aI+G7a3rcxxN/A=="], + + "@sentry/babel-plugin-component-annotate": ["@sentry/babel-plugin-component-annotate@5.3.0", "", {}, "sha512-p4q8gn8wcFqZGP/s2MnJCAAd8fTikaU6A0mM97RDHQgStcrYiaS0Sc5zUNfb1V+UOLPuvdEdL6MwyxfzjYJQTA=="], + + "@sentry/browser": ["@sentry/browser@10.57.0", "", { "dependencies": { "@sentry-internal/browser-utils": "10.57.0", "@sentry-internal/feedback": "10.57.0", "@sentry-internal/replay": "10.57.0", "@sentry-internal/replay-canvas": "10.57.0", "@sentry/core": "10.57.0" } }, "sha512-s36AQy/CKXTfyY9Z+qUhzNomntZXgfs0rbaK7q9ffnFkqcPwzE8qQtVs58y3Suut56u+AhwSztgQtERcuZ5VIA=="], + + "@sentry/cli": ["@sentry/cli@3.5.0", "", { "dependencies": { "progress": "^2.0.3", "proxy-from-env": "^1.1.0", "undici": "^6.22.0", "which": "^2.0.2" }, "optionalDependencies": { "@sentry/cli-darwin": "3.5.0", "@sentry/cli-linux-arm": "3.5.0", "@sentry/cli-linux-arm64": "3.5.0", "@sentry/cli-linux-i686": "3.5.0", "@sentry/cli-linux-x64": "3.5.0", "@sentry/cli-win32-arm64": "3.5.0", "@sentry/cli-win32-i686": "3.5.0", "@sentry/cli-win32-x64": "3.5.0" }, "bin": { "sentry-cli": "bin/sentry-cli" } }, "sha512-D3N3kULOnAClRkhHKqOAcOTtDnRe7hFacdHLlSsQXAZB11Qu6VAy8mGmM3654Lq/3LHl5h8CW590JGR9E0yahQ=="], + + "@sentry/cli-darwin": ["@sentry/cli-darwin@3.5.0", "", { "os": "darwin" }, "sha512-Q7WIq+SNMh/7gddovgcHUBlgzsH2jASdkxinwxoDZVNcF96gqr35QllRHkxZTt1+nLM3JytL2A+Mh9JdvMWzsA=="], + + "@sentry/cli-linux-arm": ["@sentry/cli-linux-arm@3.5.0", "", { "os": [ "linux", "android", "freebsd", ], "cpu": "arm" }, "sha512-d4U64gQikZiqr63XL3suWhd28e0NQg63GX+JumRiDYCDCAF3gmdUS6BRR43lyoZm2wqhHRQSms2bMArAPTDH/w=="], + + "@sentry/cli-linux-arm64": ["@sentry/cli-linux-arm64@3.5.0", "", { "os": [ "linux", "android", "freebsd", ], "cpu": "arm64" }, "sha512-nWlWo0qw1OqcOFR6GgcJ3KT+bMOlE3BFBtbMCxyURb4dTcT+NPy1Oe9Kk+JyWgRk1xm4CTaPXJNd2JD5V36FQA=="], + + "@sentry/cli-linux-i686": ["@sentry/cli-linux-i686@3.5.0", "", { "os": [ "linux", "android", "freebsd", ], "cpu": "ia32" }, "sha512-HE8zFNvg/YWudGp+pBeJSNgF/siyarnlBkWbRxz6WApsFq7uvajXPk7EqwJ0onKnzPS14OPlTtTejA3iSUuCNg=="], + + "@sentry/cli-linux-x64": ["@sentry/cli-linux-x64@3.5.0", "", { "os": [ "linux", "android", "freebsd", ], "cpu": "x64" }, "sha512-uU6h/b3SIoWysn0U6PoyUx/R5z9kU9+VcuvydjlqkZGDbbcRIxqp0T/wgB/jhXeFaSSPSFlmmkQCVYi8PXCrSw=="], + + "@sentry/cli-win32-arm64": ["@sentry/cli-win32-arm64@3.5.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-P13w9Vc/ur6rnwtiBe4wzu4M81ODhdOGc8iWpkN51gzjldjz9FF4F2UEdS9UUYBxesWJU+UF62ZVQ4H73rhS4g=="], + + "@sentry/cli-win32-i686": ["@sentry/cli-win32-i686@3.5.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-LZIEsJ5zMd0HAYlnG7G7OM6/AOvFaWkuBPdePZqYvtMLOUsfY7zeW8ubyZ9uCOWsj5Xc7UiDF4v0gZ45Strlkg=="], + + "@sentry/cli-win32-x64": ["@sentry/cli-win32-x64@3.5.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Dgkn60xoF7csXcKk+gRlmOA7s//7w0R3QlaXCo5RbX/c5VTbcV74r6ON7A+SKTFO9i4fEGblLlzQ0MLqrIo2sw=="], + + "@sentry/core": ["@sentry/core@10.57.0", "", {}, "sha512-kntItTA2kiT0YpL7encXaF6mkdZMB+y48lwj8w1wkfBpfJAC7sifdgrzLQZqmsqVNE3crg9VfufaAGA+78uFMg=="], + + "@sentry/expo-upload-sourcemaps": ["@sentry/expo-upload-sourcemaps@8.14.0", "", { "dependencies": { "@sentry/cli": "3.5.0" }, "peerDependencies": { "@expo/env": "*", "dotenv": "*" }, "optionalPeers": ["@expo/env", "dotenv"], "bin": { "expo-upload-sourcemaps": "cli.js" } }, "sha512-caPp11nVIAHtCTaM2kTqf3wG5aAui09EbeqoBEA9//NnRkuOVuRqapfX+ABTFlnnimbBSvn3MKrQt6HTMp/ELQ=="], + + "@sentry/react": ["@sentry/react@10.57.0", "", { "dependencies": { "@sentry/browser": "10.57.0", "@sentry/core": "10.57.0" }, "peerDependencies": { "react": "^16.14.0 || 17.x || 18.x || 19.x" } }, "sha512-6QThwQ4XWQ2rwKZEVQ9P9WKl7JlowC7S5LpAvmMdrwlfJBpLDFOsM7tycnIvbXTXf0ZOOuLFPa4L4YYbdyNGmA=="], + + "@sentry/react-native": ["@sentry/react-native@8.14.0", "", { "dependencies": { "@sentry/babel-plugin-component-annotate": "5.3.0", "@sentry/browser": "10.57.0", "@sentry/cli": "3.5.0", "@sentry/core": "10.57.0", "@sentry/expo-upload-sourcemaps": "8.14.0", "@sentry/react": "10.57.0" }, "peerDependencies": { "expo": ">=49.0.0", "react": ">=17.0.0", "react-native": ">=0.65.0" }, "optionalPeers": ["expo"], "bin": { "sentry-eas-build-on-complete": "scripts/eas-build-hook.js", "sentry-eas-build-on-error": "scripts/eas-build-hook.js", "sentry-eas-build-on-success": "scripts/eas-build-hook.js", "sentry-expo-upload-sourcemaps": "scripts/expo-upload-sourcemaps.js" } }, "sha512-2OF5M1Easgo25qX5360I/zb/P8IPhu6YXTgvCQCA6AYHxZvLf/9w/Rzaa+BYl6wBnnUvserjtua1xXdSrQhQXA=="], + "@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], "@sinonjs/commons": ["@sinonjs/commons@3.0.1", "", { "dependencies": { "type-detect": "4.0.8" } }, "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ=="], @@ -1738,6 +1777,8 @@ "protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], + "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + "pseudolocale": ["pseudolocale@2.2.0", "", { "dependencies": { "commander": "^10.0.0" }, "bin": { "pseudolocale": "dist/cli.mjs" } }, "sha512-O+D2eU7fO9wVLqrohvt9V/9fwMadnJQ4jxwiK+LeNEqhMx8JYx4xQHkArDCJFAdPPOp/pQq6z5L37eBvAoc8jw=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], @@ -2000,6 +2041,8 @@ "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], + "undici": ["undici@6.26.0", "", {}, "sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A=="], + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "unicode-canonical-property-names-ecmascript": ["unicode-canonical-property-names-ecmascript@2.0.1", "", {}, "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg=="], diff --git a/metro.config.js b/metro.config.js new file mode 100644 index 0000000..eddbc90 --- /dev/null +++ b/metro.config.js @@ -0,0 +1,5 @@ +const { getSentryExpoConfig } = require("@sentry/react-native/metro"); + +const config = getSentryExpoConfig(__dirname); + +module.exports = config; diff --git a/package.json b/package.json index 6e18bb8..624046d 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@react-native-firebase/app": "^24.1.1", "@react-native-firebase/messaging": "^24.1.1", "@scure/base": "^2.2.0", + "@sentry/react-native": "^8.14.0", "expo": "^56.0.11", "expo-blur": "~56.0.3", "expo-build-properties": "~56.0.18", @@ -74,7 +75,7 @@ "jest": { "preset": "jest-expo", "transformIgnorePatterns": [ - "node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg)" + "node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|@sentry/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg)" ] } } diff --git a/src/app/_layout.tsx b/src/app/_layout.tsx index 8e7e1e2..8f95121 100644 --- a/src/app/_layout.tsx +++ b/src/app/_layout.tsx @@ -6,6 +6,7 @@ import { useTheme } from "@/hooks/use-theme"; import { useNotificationStore } from "@/store/notification-store"; import { useTokenStore } from "@/store/token-store"; import { activateCurrentLocale } from "@/utils/locale"; +import { initSentry, withSentryRoot } from "@/utils/sentry"; import { isTokenEnrollmentUri } from "@/utils/token-utils"; import { i18n } from "@lingui/core"; import { I18nProvider } from "@lingui/react"; @@ -21,9 +22,10 @@ import { useColorScheme, } from "react-native"; +initSentry(); activateCurrentLocale(); -export default function RootLayout() { +function RootLayout() { const [fontsLoaded] = useInterFonts(); const colorScheme = useColorScheme(); @@ -178,3 +180,5 @@ function RootLayoutContent() { ); } + +export default withSentryRoot(RootLayout); diff --git a/src/utils/sentry.ts b/src/utils/sentry.ts new file mode 100644 index 0000000..5c5774e --- /dev/null +++ b/src/utils/sentry.ts @@ -0,0 +1,165 @@ +import type { Breadcrumb, ErrorEvent, Exception } from "@sentry/react-native"; +import * as Sentry from "@sentry/react-native"; +import { isRunningInExpoGo } from "expo"; +import type { ComponentType } from "react"; + +const SENTRY_DSN = + "https://c75bcaf61c8d79c8bcc0896d3598179a@sentry.edumfa.io/31"; +const SENTRY_ENVIRONMENT = + process.env.EXPO_PUBLIC_SENTRY_ENVIRONMENT ?? + (__DEV__ ? "development" : "production"); + +const SENSITIVE_FIELD_NAME_PATTERN = + /(authorization|credential|password|pin|secret|token|otp|uri|url)/i; +const OTP_AUTH_URI_PATTERN = /otpauth:\/\/[^\s"'<>)]*/gi; + +let sentryInitialized = false; + +export function initSentry(): void { + if (sentryInitialized) { + return; + } + + const enableNative = !isRunningInExpoGo(); + + Sentry.init({ + dsn: SENTRY_DSN, + environment: SENTRY_ENVIRONMENT, + debug: false, + sendDefaultPii: false, + sampleRate: 1, + maxBreadcrumbs: 30, + maxCacheItems: 20, + attachScreenshot: false, + attachViewHierarchy: false, + attachThreads: false, + enableAutoSessionTracking: false, + enableCaptureFailedRequests: false, + enableLogs: false, + enableNative, + enableNativeCrashHandling: enableNative, + enableNdk: enableNative, + enableNdkScopeSync: enableNative, + enableAppHangTracking: enableNative, + enableWatchdogTerminationTracking: enableNative, + enableAutoPerformanceTracing: false, + enableAppStartTracking: false, + enableNativeFramesTracking: false, + enableStallTracking: false, + enableUserInteractionTracing: false, + tracesSampleRate: 0, + tracePropagationTargets: [], + profilesSampleRate: 0, + replaysSessionSampleRate: 0, + replaysOnErrorSampleRate: 0, + beforeBreadcrumb: sanitizeBreadcrumb, + beforeSend: sanitizeEvent, + }); + + sentryInitialized = true; +} + +export function withSentryRoot
>( + component: ComponentType
, +): ComponentType
{
+ return Sentry.wrap(component);
+}
+
+function sanitizeEvent(event: ErrorEvent): ErrorEvent {
+ const sanitizedEvent: ErrorEvent = {
+ ...event,
+ message: sanitizeOptionalText(event.message),
+ logentry: event.logentry
+ ? {
+ ...event.logentry,
+ message: sanitizeOptionalText(event.logentry.message),
+ params: event.logentry.params?.map((param) =>
+ sanitizeValue(param, undefined, new WeakSet()),
+ ),
+ }
+ : event.logentry,
+ exception: event.exception
+ ? {
+ ...event.exception,
+ values: event.exception.values?.map(sanitizeException),
+ }
+ : event.exception,
+ breadcrumbs: event.breadcrumbs?.map(sanitizeBreadcrumb),
+ extra: sanitizeRecord(event.extra),
+ };
+
+ delete sanitizedEvent.request;
+ delete sanitizedEvent.user;
+
+ return sanitizedEvent;
+}
+
+function sanitizeException(exception: Exception): Exception {
+ return {
+ ...exception,
+ value: sanitizeOptionalText(exception.value),
+ };
+}
+
+function sanitizeBreadcrumb(breadcrumb: Breadcrumb): Breadcrumb {
+ return {
+ ...breadcrumb,
+ message: sanitizeOptionalText(breadcrumb.message),
+ data: sanitizeRecord(breadcrumb.data),
+ };
+}
+
+function sanitizeRecord