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 8d41127..674ce7a 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": "~7.11.0", "expo": "^56.0.12", "expo-blur": "~56.0.3", "expo-build-properties": "~56.0.19", @@ -685,6 +686,44 @@ "@scure/base": ["@scure/base@2.2.0", "", {}, "sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg=="], + "@sentry-internal/browser-utils": ["@sentry-internal/browser-utils@10.37.0", "", { "dependencies": { "@sentry/core": "10.37.0" } }, "sha512-rqdESYaVio9Ktz55lhUhtBsBUCF3wvvJuWia5YqoHDd+egyIfwWxITTAa0TSEyZl7283A4WNHNl0hyeEMblmfA=="], + + "@sentry-internal/feedback": ["@sentry-internal/feedback@10.37.0", "", { "dependencies": { "@sentry/core": "10.37.0" } }, "sha512-P0PVlfrDvfvCYg2KPIS7YUG/4i6ZPf8z1MicXx09C9Cz9W9UhSBh/nii13eBdDtLav2BFMKhvaFMcghXHX03Hw=="], + + "@sentry-internal/replay": ["@sentry-internal/replay@10.37.0", "", { "dependencies": { "@sentry-internal/browser-utils": "10.37.0", "@sentry/core": "10.37.0" } }, "sha512-snuk12ZaDerxesSnetNIwKoth/51R0y/h3eXD/bGtXp+hnSkeXN5HanI/RJl297llRjn4zJYRShW9Nx86Ay0Dw=="], + + "@sentry-internal/replay-canvas": ["@sentry-internal/replay-canvas@10.37.0", "", { "dependencies": { "@sentry-internal/replay": "10.37.0", "@sentry/core": "10.37.0" } }, "sha512-PyIYSbjLs+L5essYV0MyIsh4n5xfv2eV7l0nhUoPJv9Bak3kattQY3tholOj0EP3SgKgb+8HSZnmazgF++Hbog=="], + + "@sentry/babel-plugin-component-annotate": ["@sentry/babel-plugin-component-annotate@4.8.0", "", {}, "sha512-cy/9Eipkv23MsEJ4IuB4dNlVwS9UqOzI3Eu+QPake5BVFgPYCX0uP0Tr3Z43Ime6Rb+BiDnWC51AJK9i9afHYw=="], + + "@sentry/browser": ["@sentry/browser@10.37.0", "", { "dependencies": { "@sentry-internal/browser-utils": "10.37.0", "@sentry-internal/feedback": "10.37.0", "@sentry-internal/replay": "10.37.0", "@sentry-internal/replay-canvas": "10.37.0", "@sentry/core": "10.37.0" } }, "sha512-kheqJNqGZP5TSBCPv4Vienv1sfZwXKHQDYR+xrdHHYdZqwWuZMJJW/cLO9XjYAe+B9NnJ4UwJOoY4fPvU+HQ1Q=="], + + "@sentry/cli": ["@sentry/cli@2.58.4", "", { "dependencies": { "https-proxy-agent": "^5.0.0", "node-fetch": "^2.6.7", "progress": "^2.0.3", "proxy-from-env": "^1.1.0", "which": "^2.0.2" }, "optionalDependencies": { "@sentry/cli-darwin": "2.58.4", "@sentry/cli-linux-arm": "2.58.4", "@sentry/cli-linux-arm64": "2.58.4", "@sentry/cli-linux-i686": "2.58.4", "@sentry/cli-linux-x64": "2.58.4", "@sentry/cli-win32-arm64": "2.58.4", "@sentry/cli-win32-i686": "2.58.4", "@sentry/cli-win32-x64": "2.58.4" }, "bin": { "sentry-cli": "bin/sentry-cli" } }, "sha512-ArDrpuS8JtDYEvwGleVE+FgR+qHaOp77IgdGSacz6SZy6Lv90uX0Nu4UrHCQJz8/xwIcNxSqnN22lq0dH4IqTg=="], + + "@sentry/cli-darwin": ["@sentry/cli-darwin@2.58.4", "", { "os": "darwin" }, "sha512-kbTD+P4X8O+nsNwPxCywtj3q22ecyRHWff98rdcmtRrvwz8CKi/T4Jxn/fnn2i4VEchy08OWBuZAqaA5Kh2hRQ=="], + + "@sentry/cli-linux-arm": ["@sentry/cli-linux-arm@2.58.4", "", { "os": [ "linux", "android", "freebsd", ], "cpu": "arm" }, "sha512-rdQ8beTwnN48hv7iV7e7ZKucPec5NJkRdrrycMJMZlzGBPi56LqnclgsHySJ6Kfq506A2MNuQnKGaf/sBC9REA=="], + + "@sentry/cli-linux-arm64": ["@sentry/cli-linux-arm64@2.58.4", "", { "os": [ "linux", "android", "freebsd", ], "cpu": "arm64" }, "sha512-0g0KwsOozkLtzN8/0+oMZoOuQ0o7W6O+hx+ydVU1bktaMGKEJLMAWxOQNjsh1TcBbNIXVOKM/I8l0ROhaAb8Ig=="], + + "@sentry/cli-linux-i686": ["@sentry/cli-linux-i686@2.58.4", "", { "os": [ "linux", "android", "freebsd", ], "cpu": "ia32" }, "sha512-NseoIQAFtkziHyjZNPTu1Gm1opeQHt7Wm1LbLrGWVIRvUOzlslO9/8i6wETUZ6TjlQxBVRgd3Q0lRBG2A8rFYA=="], + + "@sentry/cli-linux-x64": ["@sentry/cli-linux-x64@2.58.4", "", { "os": [ "linux", "android", "freebsd", ], "cpu": "x64" }, "sha512-d3Arz+OO/wJYTqCYlSN3Ktm+W8rynQ/IMtSZLK8nu0ryh5mJOh+9XlXY6oDXw4YlsM8qCRrNquR8iEI1Y/IH+Q=="], + + "@sentry/cli-win32-arm64": ["@sentry/cli-win32-arm64@2.58.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-bqYrF43+jXdDBh0f8HIJU3tbvlOFtGyRjHB8AoRuMQv9TEDUfENZyCelhdjA+KwDKYl48R1Yasb4EHNzsoO83w=="], + + "@sentry/cli-win32-i686": ["@sentry/cli-win32-i686@2.58.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-3triFD6jyvhVcXOmGyttf+deKZcC1tURdhnmDUIBkiDPJKGT/N5xa4qAtHJlAB/h8L9jgYih9bvJnvvFVM7yug=="], + + "@sentry/cli-win32-x64": ["@sentry/cli-win32-x64@2.58.4", "", { "os": "win32", "cpu": "x64" }, "sha512-cSzN4PjM1RsCZ4pxMjI0VI7yNCkxiJ5jmWncyiwHXGiXrV1eXYdQ3n1LhUYLZ91CafyprR0OhDcE+RVZ26Qb5w=="], + + "@sentry/core": ["@sentry/core@10.37.0", "", {}, "sha512-hkRz7S4gkKLgPf+p3XgVjVm7tAfvcEPZxeACCC6jmoeKhGkzN44nXwLiqqshJ25RMcSrhfFvJa/FlBg6zupz7g=="], + + "@sentry/react": ["@sentry/react@10.37.0", "", { "dependencies": { "@sentry/browser": "10.37.0", "@sentry/core": "10.37.0" }, "peerDependencies": { "react": "^16.14.0 || 17.x || 18.x || 19.x" } }, "sha512-XLnXJOHgsCeVAVBbO+9AuGlZWnCxLQHLOmKxpIr8wjE3g7dHibtug6cv8JLx78O4dd7aoCqv2TTyyKY9FLJ2EQ=="], + + "@sentry/react-native": ["@sentry/react-native@7.11.0", "", { "dependencies": { "@sentry/babel-plugin-component-annotate": "4.8.0", "@sentry/browser": "10.37.0", "@sentry/cli": "2.58.4", "@sentry/core": "10.37.0", "@sentry/react": "10.37.0", "@sentry/types": "10.37.0" }, "peerDependencies": { "expo": ">=49.0.0", "react": ">=17.0.0", "react-native": ">=0.65.0" }, "optionalPeers": ["expo"], "bin": { "sentry-expo-upload-sourcemaps": "scripts/expo-upload-sourcemaps.js" } }, "sha512-OiDaLCAGpRN18YG/o7IIwLhU0Xpb0tYKQ5QxkGHiwb+L3VHn+MqGCGfITYNdhqr06HHMvu9Lysm+UJxaNmGaJg=="], + + "@sentry/types": ["@sentry/types@10.37.0", "", { "dependencies": { "@sentry/core": "10.37.0" } }, "sha512-umpnUKRC0AAbJrADg6SlFtqN2yzf7NHciCF9lkHau+ax2PIZ/NDmoG4RQujFVflVaVoD60Ly2t+CcPnYIWMPlw=="], + "@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=="], @@ -811,7 +850,7 @@ "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], - "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], @@ -1303,7 +1342,7 @@ "http-parser-js": ["http-parser-js@0.5.10", "", {}, "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA=="], - "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], "human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], @@ -1631,6 +1670,8 @@ "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + "node-forge": ["node-forge@1.3.3", "", {}, "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg=="], "node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="], @@ -1741,6 +1782,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=="], @@ -1977,6 +2020,8 @@ "toqr": ["toqr@0.1.1", "", {}, "sha512-FWAPzCIHZHnrE/5/w9MPk0kK25hSQSH2IKhYh9PyjS3SG/+IEMvlwIHbhz+oF7xl54I+ueZlVnMjyzdSwLmAwA=="], + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], "tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="], @@ -2049,12 +2094,16 @@ "web-vitals": ["web-vitals@4.2.4", "", {}, "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw=="], + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + "websocket-driver": ["websocket-driver@0.7.4", "", { "dependencies": { "http-parser-js": ">=0.5.1", "safe-buffer": ">=5.1.0", "websocket-extensions": ">=0.1.1" } }, "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg=="], "websocket-extensions": ["websocket-extensions@0.1.4", "", {}, "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg=="], "whatwg-fetch": ["whatwg-fetch@3.6.20", "", {}, "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="], + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + "whatwg-url-minimum": ["whatwg-url-minimum@0.1.2", "", {}, "sha512-XPEm0XFQWNVG292lII1PrRRJl3sItrs7CettZ4ncYxuDVpLyy+NwlGyut2hXI0JswcJUxeCH+CyOJK0ZzAXD6A=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], @@ -2245,6 +2294,8 @@ "metro-babel-transformer/hermes-parser": ["hermes-parser@0.35.0", "", { "dependencies": { "hermes-estree": "0.35.0" } }, "sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA=="], + "metro-cache/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "metro-source-map/source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="], "metro-symbolicate/source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="], @@ -2341,6 +2392,8 @@ "metro-babel-transformer/hermes-parser/hermes-estree": ["hermes-estree@0.35.0", "", {}, "sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg=="], + "metro-cache/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "metro/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], "metro/hermes-parser/hermes-estree": ["hermes-estree@0.35.0", "", {}, "sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg=="], 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 1a0d0d7..2b6adae 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@react-native-firebase/app": "^24.1.1", "@react-native-firebase/messaging": "^24.1.1", "@scure/base": "^2.2.0", + "@sentry/react-native": "~7.11.0", "expo": "^56.0.12", "expo-blur": "~56.0.3", "expo-build-properties": "~56.0.19", @@ -76,7 +77,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 5f6a8c1..1e0c9cc 100644 --- a/src/app/_layout.tsx +++ b/src/app/_layout.tsx @@ -9,6 +9,7 @@ import { useTheme } from "@/hooks/use-theme"; import { useSettingsStore } from "@/stores/settings"; import { useTokenStore } from "@/stores/token"; import { activateCurrentLocale } from "@/utils/locale"; +import { withSentryRoot } from "@/utils/sentry"; import { isTokenEnrollmentUri } from "@/utils/token"; import { i18n } from "@lingui/core"; import { I18nProvider } from "@lingui/react"; @@ -26,7 +27,7 @@ import { activateCurrentLocale(); -export default function RootLayout() { +function RootLayout() { const [fontsLoaded] = useInterFonts(); const colorScheme = useColorScheme(); @@ -205,3 +206,5 @@ function RootLayoutContent() { ); } + +export default withSentryRoot(RootLayout); diff --git a/src/stores/settings.ts b/src/stores/settings.ts index 19f7914..ec428ad 100644 --- a/src/stores/settings.ts +++ b/src/stores/settings.ts @@ -1,4 +1,5 @@ import AsyncStorage from "@react-native-async-storage/async-storage"; +import { setSentryTrackingEnabled } from "@/utils/sentry"; import { create } from "zustand"; import { createJSONStorage, persist } from "zustand/middleware"; @@ -30,8 +31,10 @@ export const useSettingsStore = create()( hasHydrated: false, completeOnboarding: () => set({ hasCompletedOnboarding: true }), resetOnboarding: () => set({ hasCompletedOnboarding: false }), - setCrashReportsEnabled: (enabled) => - set({ crashReportsEnabled: enabled }), + setCrashReportsEnabled: (enabled) => { + set({ crashReportsEnabled: enabled }); + setSentryTrackingEnabled(enabled); + }, setHasHydrated: (hasHydrated) => set({ hasHydrated }), }), { @@ -43,6 +46,7 @@ export const useSettingsStore = create()( }), onRehydrateStorage: () => (state) => { state?.setHasHydrated(true); + setSentryTrackingEnabled(state?.crashReportsEnabled ?? false); }, }, ), diff --git a/src/utils/sentry.ts b/src/utils/sentry.ts new file mode 100644 index 0000000..8e4778c --- /dev/null +++ b/src/utils/sentry.ts @@ -0,0 +1,184 @@ +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 setSentryTrackingEnabled(enabled: boolean): void { + if (enabled) { + initSentry(); + return; + } + + if (!sentryInitialized) { + return; + } + + sentryInitialized = false; + + void Sentry.close().catch((error: unknown) => { + if (__DEV__) { + console.warn("Failed to close Sentry:", error); + } + }); +} + +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 | undefined>( + record: T, +): T { + if (!record) { + return record; + } + + return sanitizeValue(record, undefined, new WeakSet()) as T; +} + +function sanitizeValue( + value: unknown, + key: string | undefined, + seen: WeakSet, +): unknown { + if (key && SENSITIVE_FIELD_NAME_PATTERN.test(key)) { + return "[Filtered]"; + } + + if (typeof value === "string") { + return sanitizeText(value); + } + + if (typeof value !== "object" || value === null) { + return value; + } + + if (seen.has(value)) { + return "[Circular]"; + } + + seen.add(value); + + if (Array.isArray(value)) { + return value.map((item) => sanitizeValue(item, undefined, seen)); + } + + const record = value as Record; + + return Object.fromEntries( + Object.entries(record).map(([recordKey, recordValue]) => [ + recordKey, + sanitizeValue(recordValue, recordKey, seen), + ]), + ); +} + +function sanitizeOptionalText(value: string | undefined): string | undefined { + return value ? sanitizeText(value) : value; +} + +function sanitizeText(value: string): string { + return value.replace(OTP_AUTH_URI_PATTERN, "otpauth://[Filtered]"); +}