diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 751a4cb..3c657bd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,6 +25,8 @@ jobs: MACOS_APPLE_ID: ${{ secrets.MACOS_APPLE_ID }} # App-specific password generated at appleid.apple.com for notarization. MACOS_APPLE_PASSWORD: ${{ secrets.MACOS_APPLE_PASSWORD }} + # Sentry project DSN used for production telemetry. + SENTRY_DSN: ${{ secrets.SENTRY_DSN }} steps: - uses: actions/checkout@v6 diff --git a/.gitignore b/.gitignore index e65b59a..b306dd7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # Node dependencies. node_modules/ +# Local environment. +.env + # Build output. build/ dist/ diff --git a/README.md b/README.md index 540962c..b20fa17 100644 --- a/README.md +++ b/README.md @@ -55,5 +55,8 @@ npm install npm run build ``` +Production Sentry telemetry is included only when `SENTRY_DSN` is set during the build. +Release builds read it from the `SENTRY_DSN` GitHub Actions secret. + If you need to sign and notarize the application, see the [.github/workflows/build.yml](.github/workflows/build.yml) for a ready-to-use script that works on macOS and Windows. diff --git a/package-lock.json b/package-lock.json index 9c9a1a8..6e9ba22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,8 @@ "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-toggle-group": "^1.1.11", + "@sentry/node": "^10.64.0", + "@sentry/react": "^10.64.0", "async-mutex": "^0.5.0", "class-variance-authority": "^0.7.0", "lucide-react": "^0.503.0", @@ -65,6 +67,49 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@apm-js-collab/code-transformer": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@apm-js-collab/code-transformer/-/code-transformer-0.15.0.tgz", + "integrity": "sha512-XmXYVs8CzJ1Aj79noVbn2weUO/XWtRyURpGqx7aU7DOXlUQhR0WKOQNF0okh7PCeY37vxf7kU3v57OAkEPm3ww==", + "license": "Apache-2.0", + "dependencies": { + "@types/estree": "^1.0.8", + "astring": "^1.9.0", + "esquery": "^1.7.0", + "meriyah": "^6.1.4", + "semifies": "^1.0.0", + "source-map": "^0.6.0" + }, + "bin": { + "code-transformer": "cli.js" + } + }, + "node_modules/@apm-js-collab/code-transformer-bundler-plugins": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@apm-js-collab/code-transformer-bundler-plugins/-/code-transformer-bundler-plugins-0.5.0.tgz", + "integrity": "sha512-YxLBY5nGlurL7QeJLq6e5g0ouBpAp0pwgyA/5rHXEXwhiPLn9ZHbT+Y2LlP90GT872cSocfjWRYu/fnpuBudNQ==", + "license": "MIT", + "dependencies": { + "@apm-js-collab/code-transformer": "^0.15.0", + "es-module-lexer": "^2.1.0", + "magic-string": "^0.30.21", + "module-details-from-path": "^1.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@apm-js-collab/tracing-hooks": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@apm-js-collab/tracing-hooks/-/tracing-hooks-0.10.1.tgz", + "integrity": "sha512-w2OWXR7FWrKqSziuE9+QclaZrStxO/8+OwbXM635s/zs0Eez1Qo3ivSPdB2WsaPY/iznKTytONPx/PitD7IXcA==", + "license": "Apache-2.0", + "dependencies": { + "@apm-js-collab/code-transformer": "^0.15.0", + "debug": "^4.4.1", + "module-details-from-path": "^1.0.4" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "dev": true, @@ -1414,6 +1459,119 @@ "node": ">= 8" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.220.0.tgz", + "integrity": "sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.220.0.tgz", + "integrity": "sha512-xQx3E2WxP1mDvKzxLxX+CTCtNLa560YJZ3087qYHerl2YmiKpv7AH+dAy7vmx+eVrZ5BwhfWUAVoKOoxCNHcpw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.220.0", + "import-in-the-middle": "^3.0.0", + "require-in-the-middle": "^8.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.9.0.tgz", + "integrity": "sha512-sGA19HvtrrSKYsseHphluH6j3p6Xa3fqc7c7y8f/7mYWejc1lyDFcpSdD1kYa50HCLUeEo4zA5bW0pniaPszuw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.9.0.tgz", + "integrity": "sha512-cp9zmTl62R8PJrpvFcmc8N2JQU/xfa0S+61q511Nji+QxCfZ8Ifvg7H27G8cANe4crg4RTrWsVvanHiXjSp6ag==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/sdk-trace": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.42.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.42.0.tgz", + "integrity": "sha512-icc5xCzndZfhuJMy5oqk5AvloWquR7jtae74qzpkKkhGp8BivK+oCcEXgGnjCdTfp8hA44l+w8gE8yYJbocJJw==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/@oxc-project/types": { "version": "0.127.0", "devOptional": true, @@ -2308,6 +2466,203 @@ "dev": true, "license": "MIT" }, + "node_modules/@sentry/browser": { + "version": "10.64.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.64.0.tgz", + "integrity": "sha512-CHZ7+79evh8knBtVHjW/XqV3mRaWs2TdjX7i55zpFe9vcNngQHV++0ss8ird/xfMY1mfCDtbyAN+Z6XY2o5aJA==", + "license": "MIT", + "dependencies": { + "@sentry/browser-utils": "10.64.0", + "@sentry/core": "10.64.0", + "@sentry/feedback": "10.64.0", + "@sentry/replay": "10.64.0", + "@sentry/replay-canvas": "10.64.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/browser-utils": { + "version": "10.64.0", + "resolved": "https://registry.npmjs.org/@sentry/browser-utils/-/browser-utils-10.64.0.tgz", + "integrity": "sha512-qXvUpsZdE0+6SovhDZvjN4JkqQEpzeYsrOF5g63wMsqJWWv2vW/pQZIlJs0F6C0t4q5AHZL4fl5rNkvpVqFdKA==", + "license": "MIT", + "dependencies": { + "@sentry/core": "10.64.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/conventions": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@sentry/conventions/-/conventions-0.15.1.tgz", + "integrity": "sha512-ZLP8bRdMON3prWE2tJyImuYscCxdcJeIPIhrOs/rgyFm3C1nCh1B6gdvPj3AZ5zW08oSFFCsq7T+tYEW3h8MNA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@sentry/core": { + "version": "10.64.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.64.0.tgz", + "integrity": "sha512-HjojJcXD1l2qZ1AXje2s0XY/nYsaUt00wsM1HMBImA8vAClyPisFE/CC0/UD6pEvsGFhVgi8Dcxo7EN41uyeFw==", + "license": "MIT", + "dependencies": { + "@sentry/conventions": "^0.15.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/feedback": { + "version": "10.64.0", + "resolved": "https://registry.npmjs.org/@sentry/feedback/-/feedback-10.64.0.tgz", + "integrity": "sha512-YrmKwRoAto+nwo26DoAhnpNmhkrVkuQFIAXqlTD8ipERjhcSNUYJAkAB+nH4w1KIgm9QQZE1sTq3iNQdPl1XMQ==", + "license": "MIT", + "dependencies": { + "@sentry/core": "10.64.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node": { + "version": "10.64.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-10.64.0.tgz", + "integrity": "sha512-rhNZ3CTqwdTQHo9Zd+NsoLyW69VIzbMcGMRX9y8W1A6runT4YH/MDhmT0U9oWfNZKN8ngqR/gfVMTnlYVQliCA==", + "license": "MIT", + "dependencies": { + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/instrumentation": "^0.220.0", + "@opentelemetry/sdk-trace-base": "^2.9.0", + "@sentry/conventions": "^0.15.1", + "@sentry/core": "10.64.0", + "@sentry/node-core": "10.64.0", + "@sentry/opentelemetry": "10.64.0", + "@sentry/server-utils": "10.64.0", + "import-in-the-middle": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node-core": { + "version": "10.64.0", + "resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-10.64.0.tgz", + "integrity": "sha512-dcma5uEWl0SXywY4vzrlOwuz4NBPyPhrzqL1NjlU6Di/OVbyYAZJPCwsR8XYDz73PGc5sU3qKSRoXWX56Ip0wA==", + "license": "MIT", + "dependencies": { + "@sentry/conventions": "^0.15.1", + "@sentry/core": "10.64.0", + "@sentry/opentelemetry": "10.64.0", + "import-in-the-middle": "^3.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "^1.30.1 || ^2.1.0", + "@opentelemetry/exporter-trace-otlp-http": ">=0.57.0 <1", + "@opentelemetry/instrumentation": ">=0.57.1 <1", + "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@opentelemetry/core": { + "optional": true + }, + "@opentelemetry/exporter-trace-otlp-http": { + "optional": true + }, + "@opentelemetry/instrumentation": { + "optional": true + }, + "@opentelemetry/sdk-trace-base": { + "optional": true + } + } + }, + "node_modules/@sentry/opentelemetry": { + "version": "10.64.0", + "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-10.64.0.tgz", + "integrity": "sha512-hXw8dwSSA9/9r3VGy0PO2SJp7g5/yH2+7Bak60EkOT21AT1BwFnVEsVQyZb+UHjG7UWne/jDbwdWPSUm/rtovw==", + "license": "MIT", + "dependencies": { + "@sentry/conventions": "^0.15.1", + "@sentry/core": "10.64.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "^1.30.1 || ^2.1.0", + "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0" + } + }, + "node_modules/@sentry/react": { + "version": "10.64.0", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-10.64.0.tgz", + "integrity": "sha512-oShEKcosBKdm0kgan8suFb6Jj8poccEyDz2qiflonq/5/hUDyzdVeKfFzf0b1MmdjWSn9k3hfrLNFfPNCuEt7Q==", + "license": "MIT", + "dependencies": { + "@sentry/browser": "10.64.0", + "@sentry/core": "10.64.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.14.0 || 17.x || 18.x || 19.x" + } + }, + "node_modules/@sentry/replay": { + "version": "10.64.0", + "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-10.64.0.tgz", + "integrity": "sha512-q8nke2GDSwEiu1zYoctDDvnSNTjHWoVAKo2JXTleD0nwOO6IlAgUYmsm3qEQiSW7BVla9W1TRbW6ChFAUXYZbw==", + "license": "MIT", + "dependencies": { + "@sentry/browser-utils": "10.64.0", + "@sentry/core": "10.64.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/replay-canvas": { + "version": "10.64.0", + "resolved": "https://registry.npmjs.org/@sentry/replay-canvas/-/replay-canvas-10.64.0.tgz", + "integrity": "sha512-EioBwcnnhtPiXzTZJTbZKaMEg3qNM5YZlX1VanoXomPATqfJD62cb/M27zhgiWXXJ7e8/NGVHvwNDYGrqsN39g==", + "license": "MIT", + "dependencies": { + "@sentry/core": "10.64.0", + "@sentry/replay": "10.64.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/server-utils": { + "version": "10.64.0", + "resolved": "https://registry.npmjs.org/@sentry/server-utils/-/server-utils-10.64.0.tgz", + "integrity": "sha512-d568Jhn3WBfm07dsdPsLh0q+qRj71xZEDZFsA33kizD0UuSCR8axtc2YW6aKdPhtuqgm9tHjSX35Q9vK/XmujA==", + "license": "MIT", + "dependencies": { + "@apm-js-collab/code-transformer": "^0.15.0", + "@apm-js-collab/code-transformer-bundler-plugins": "^0.5.0", + "@apm-js-collab/tracing-hooks": "^0.10.1", + "@sentry/conventions": "^0.15.1", + "@sentry/core": "10.64.0", + "magic-string": "~0.30.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@sindresorhus/merge-streams": { "version": "4.0.0", "dev": true, @@ -3023,6 +3378,15 @@ "node": ">=8" } }, + "node_modules/astring": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "license": "MIT", + "bin": { + "astring": "bin/astring" + } + }, "node_modules/async-mutex": { "version": "0.5.0", "license": "MIT", @@ -3215,6 +3579,12 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/cjs-module-lexer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "license": "MIT" + }, "node_modules/class-variance-authority": { "version": "0.7.1", "license": "Apache-2.0", @@ -3480,6 +3850,12 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.0.tgz", + "integrity": "sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==", + "license": "MIT" + }, "node_modules/es6-error": { "version": "4.1.1", "license": "MIT" @@ -4082,6 +4458,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-in-the-middle": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", + "license": "Apache-2.0", + "dependencies": { + "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", + "module-details-from-path": "^1.0.4" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/import-meta-resolve": { "version": "4.2.0", "dev": true, @@ -4593,6 +4983,15 @@ "node": ">= 8" } }, + "node_modules/meriyah": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/meriyah/-/meriyah-6.1.4.tgz", + "integrity": "sha512-Sz8FzjzI0kN13GK/6MVEsVzMZEPvOhnmmI1lU5+/1cGOiK3QUahntrNNtdVeihrO7t9JpoH75iMNXg6R6uWflQ==", + "license": "ISC", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/micromatch": { "version": "4.0.8", "dev": true, @@ -4619,6 +5018,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/module-details-from-path": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "dev": true, @@ -5073,6 +5478,19 @@ "node": ">=0.10.0" } }, + "node_modules/require-in-the-middle": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", + "integrity": "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3" + }, + "engines": { + "node": ">=9.3.0 || >=8.10.0 <9.0.0" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "dev": true, @@ -5168,6 +5586,12 @@ "version": "0.27.0", "license": "MIT" }, + "node_modules/semifies": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semifies/-/semifies-1.0.0.tgz", + "integrity": "sha512-xXR3KGeoxTNWPD4aBvL5NUpMTT7WMANr3EWnaS190QVkY52lqqcVRD7Q05UVbBhiWDGWMlJEUam9m7uFFGVScw==", + "license": "Apache-2.0" + }, "node_modules/semver": { "version": "7.7.4", "license": "ISC", @@ -5298,6 +5722,15 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "devOptional": true, diff --git a/package.json b/package.json index 0279a17..2bb59c2 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,8 @@ "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-toggle-group": "^1.1.11", + "@sentry/node": "^10.64.0", + "@sentry/react": "^10.64.0", "async-mutex": "^0.5.0", "class-variance-authority": "^0.7.0", "lucide-react": "^0.503.0", diff --git a/src/main/Application.ts b/src/main/Application.ts index a1741c1..bf183ee 100644 --- a/src/main/Application.ts +++ b/src/main/Application.ts @@ -60,6 +60,9 @@ export class Application { */ async initialize(): Promise { await this.historyStore.initialize(); + // Register this before renderer windows are created, + // because renderer startup reads app metadata. + registerApplicationMetadataIpc(); this.recordingWorkerWindow.initialize(); this.mainMenu.create(); let initialWelcomeAutoShowHandled = false; @@ -80,7 +83,6 @@ export class Application { this.recordingController.handleShortcutTrigger(); }); - registerApplicationMetadataIpc(); registerDesktopIpc(); registerPermissionsIpc(this.permissions, () => { void this.readiness.recompute(); diff --git a/src/main/index.ts b/src/main/index.ts index 163c4ed..ce4d0fc 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,5 +1,9 @@ +import { flushSentry } from './sentry'; +import * as Sentry from '@sentry/node'; import { Application } from './Application'; -void new Application().initialize().catch((err: unknown) => { +void new Application().initialize().catch(async (err: unknown) => { console.error('[Application] Failed to initialize:', err); + Sentry.captureException(err); + await flushSentry(); }); diff --git a/src/main/sentry.ts b/src/main/sentry.ts new file mode 100644 index 0000000..a4b3075 --- /dev/null +++ b/src/main/sentry.ts @@ -0,0 +1,35 @@ +import { app } from '@mobrowser/api'; +import * as Sentry from '@sentry/node'; + +declare const SENTRY_DSN: string; + +const SENTRY_FLUSH_TIMEOUT_MS = 2000; + +if (SENTRY_DSN !== '' && app.packaged) { + Sentry.init({ + dsn: SENTRY_DSN, + environment: 'production', + release: `${app.name}@${app.version}`, + defaultIntegrations: false, + }); + Sentry.setTag('process', 'main'); + // Send only one event per production launch to avoid stats duplication. + Sentry.captureMessage('app.launch', 'info'); + + Sentry.startSession(); + Sentry.captureSession(); + + process.once('uncaughtException', captureFatalError); + process.once('unhandledRejection', captureFatalError); +} + +function captureFatalError(error: unknown): void { + Sentry.captureException(error); + void flushSentry().finally(() => { + process.exit(1); + }); +} + +export function flushSentry(): Promise { + return Sentry.flush(SENTRY_FLUSH_TIMEOUT_MS); +} diff --git a/src/main/system/ApplicationMetadataService.ts b/src/main/system/ApplicationMetadataService.ts index 60ade26..2d9fe43 100644 --- a/src/main/system/ApplicationMetadataService.ts +++ b/src/main/system/ApplicationMetadataService.ts @@ -16,6 +16,9 @@ class ApplicationMetadataService implements ApplicationMetadataServiceInterface * Returns metadata from the application shell configuration. */ GetApplicationMetadata() { - return Promise.resolve({ version: app.version }); + return Promise.resolve({ + name: app.name, + version: app.version, + }); } } diff --git a/src/renderer/index.tsx b/src/renderer/index.tsx index c75753d..288e12c 100644 --- a/src/renderer/index.tsx +++ b/src/renderer/index.tsx @@ -1,5 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; +import * as Sentry from '@sentry/react'; +import { initializeSentry } from './sentry'; import { AboutApp } from './about/AboutApp'; import { startTranscriptionRuntime } from './recording/transcription/TranscriptionRuntime'; import './index.css'; @@ -39,6 +41,8 @@ document.documentElement.dataset.rendererWindow = rendererWindowKind ?? 'unknown document.body.dataset.rendererWindow = rendererWindowKind ?? 'unknown'; document.title = rendererWindowKind === null ? 'MōVoice' : TITLE_BY_WINDOW[rendererWindowKind]; +await initializeSentry(); + initTheme(); const rootElement = document.getElementById('root'); @@ -53,7 +57,11 @@ if (rendererWindowKind === 'background') { const SelectedApp = rendererWindowKind === null ? null : APP_BY_WINDOW[rendererWindowKind]; const appElement = SelectedApp === null ? <> : React.createElement(SelectedApp); - ReactDOM.createRoot(rootElement).render( + ReactDOM.createRoot(rootElement, { + onCaughtError: Sentry.reactErrorHandler(), + onRecoverableError: Sentry.reactErrorHandler(), + onUncaughtError: Sentry.reactErrorHandler(), + }).render( {appElement} , diff --git a/src/renderer/proto/application_metadata.proto b/src/renderer/proto/application_metadata.proto index b1b728e..4b0858a 100644 --- a/src/renderer/proto/application_metadata.proto +++ b/src/renderer/proto/application_metadata.proto @@ -6,10 +6,15 @@ import "google/protobuf/empty.proto"; * Application metadata exposed by the main process. */ message ApplicationMetadata { + /** + * Current application name. + */ + string name = 1; + /** * Current application version. */ - string version = 1; + string version = 2; } /** diff --git a/src/renderer/sentry.ts b/src/renderer/sentry.ts new file mode 100644 index 0000000..a763ce3 --- /dev/null +++ b/src/renderer/sentry.ts @@ -0,0 +1,19 @@ +import * as Sentry from '@sentry/react'; +import { ipc } from './gen/ipc'; + +declare const SENTRY_DSN: string; + +export async function initializeSentry(): Promise { + if (SENTRY_DSN === '' || import.meta.env.DEV) { + return; + } + + const metadata = await ipc.applicationMetadata.GetApplicationMetadata({}); + + Sentry.init({ + dsn: SENTRY_DSN, + environment: 'production', + release: `${metadata.name}@${metadata.version}`, + }); + Sentry.setTag('process', 'renderer'); +} diff --git a/vite.config.ts b/vite.config.ts index 5ed209f..9cc22f6 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,6 +2,10 @@ import path from "path" import react from "@vitejs/plugin-react" import { defineConfig, type UserConfig } from "vite" +const buildTimeDefines = { + SENTRY_DSN: JSON.stringify(process.env.SENTRY_DSN ?? ""), +} + export default defineConfig(({ mode }) => { if (mode === "main") { return defineMainConfig() @@ -15,6 +19,7 @@ export default defineConfig(({ mode }) => { function defineMainConfig(): UserConfig { return { root: path.resolve(__dirname, "./src/main"), + define: buildTimeDefines, build: { target: "esnext", outDir: path.resolve(__dirname, "./out/main"), @@ -28,9 +33,10 @@ function defineMainConfig(): UserConfig { rollupOptions: { external: [ "mobrowser", - "node:crypto", - "node:fs", - "node:path", + "import-in-the-middle", + "module-details-from-path", + "require-in-the-middle", + /^node:.*/, ], }, }, @@ -53,6 +59,7 @@ function defineMainConfig(): UserConfig { function defineRendererConfig(): UserConfig { return { root: path.resolve(__dirname, "./src/renderer"), + define: buildTimeDefines, plugins: [react()], build: { outDir: path.resolve(__dirname, "./out/renderer"),