diff --git a/web/.env.example b/web/.env.example index b1bd5bd75..a1691b4df 100644 --- a/web/.env.example +++ b/web/.env.example @@ -1 +1,5 @@ -VITE_API_URL=https://localhost:7123/api/ \ No newline at end of file +VITE_API_URL=https://localhost:7123/api/ +VITE_SENTRY_DSN= +VITE_SENTRY_ORG= +VITE_SENTRY_PROJECT= +VITE_SENTRY_AUTH_TOKEN= \ No newline at end of file diff --git a/web/package.json b/web/package.json index eae4b7e56..0c514f443 100644 --- a/web/package.json +++ b/web/package.json @@ -39,6 +39,7 @@ "@radix-ui/react-toast": "^1.2.6", "@radix-ui/react-toggle": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", + "@sentry/react": "^9.5.0", "@tailwindcss/forms": "^0.5.10", "@tanstack/react-query": "^5.66.0", "@tanstack/react-query-devtools": "^5.66.0", @@ -102,6 +103,7 @@ "@heroicons/react": "^2.2.0", "@hookform/devtools": "^4.3.3", "@playwright/test": "^1.50.1", + "@sentry/vite-plugin": "^3.2.2", "@tailwindcss/typography": "^0.5.16", "@tanstack/eslint-plugin-query": "^5.66.1", "@tanstack/react-table-devtools": "^8.21.2", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index d0d188254..96b413b00 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -71,6 +71,9 @@ importers: '@radix-ui/react-tooltip': specifier: ^1.1.8 version: 1.1.8(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@sentry/react': + specifier: ^9.5.0 + version: 9.5.0(react@18.3.1) '@tailwindcss/forms': specifier: ^0.5.10 version: 0.5.10(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@20.5.1)(typescript@5.7.3))) @@ -255,6 +258,9 @@ importers: '@playwright/test': specifier: ^1.50.1 version: 1.50.1 + '@sentry/vite-plugin': + specifier: ^3.2.2 + version: 3.2.2 '@tailwindcss/typography': specifier: ^0.5.16 version: 0.5.16(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@20.5.1)(typescript@5.7.3))) @@ -1844,6 +1850,94 @@ packages: cpu: [x64] os: [win32] + '@sentry-internal/browser-utils@9.5.0': + resolution: {integrity: sha512-AE9jgeI5+KyGvLR0vf1I6sesi0NZXZe6pDlZNXyg+pWZB2vkE9dksE8ZsoU+YiD9zjUqazgPcVyb3O0VvmaCGw==} + engines: {node: '>=18'} + + '@sentry-internal/feedback@9.5.0': + resolution: {integrity: sha512-p+yOTufEYHP1RLwkD+aZwpCNS4/2l6t4uHgphjYrEC2U/U2mtZQh+EvlBAt0wY/eiKC4/acPNrF5yFD/4A7a0A==} + engines: {node: '>=18'} + + '@sentry-internal/replay-canvas@9.5.0': + resolution: {integrity: sha512-W7MS7/9Z8uP2i0pbndxqz2VcGlFPc7Bv6gCoxRdGIWUWSBS9rsRbryO0sM0PwwuHt2mQtWMqwjYykcR441RBRA==} + engines: {node: '>=18'} + + '@sentry-internal/replay@9.5.0': + resolution: {integrity: sha512-fBBNimElAnu865HT3MJ6xH2P26KvkZvAYt+yRrWr+x5zS5KvjBYUPsSI+F0FTE14XmLW9q7DlNUl5iAZhXSy3g==} + engines: {node: '>=18'} + + '@sentry/babel-plugin-component-annotate@3.2.2': + resolution: {integrity: sha512-D+SKQ266ra/wo87s9+UI/rKQi3qhGPCR8eSCDe0VJudhjHsqyNU+JJ5lnIGCgmZaWFTXgdBP/gdr1Iz1zqGs4Q==} + engines: {node: '>= 14'} + + '@sentry/browser@9.5.0': + resolution: {integrity: sha512-HYSPW8GjknuYykJgOialKFyWg7ldmrbD1AKTIhksqdsNXLER07YeVWFAbe+xSYa1ZwwC8/s6vQJP9ZOoH1BaVg==} + engines: {node: '>=18'} + + '@sentry/bundler-plugin-core@3.2.2': + resolution: {integrity: sha512-YGrtmqQ2jMixccX2slVG/Lw7pCGJL3DGB3clmY9mO8QBEBIN3/gEANiHJVWwRidpUOS/0b7yVVGAdwZ87oPwTg==} + engines: {node: '>= 14'} + + '@sentry/cli-darwin@2.42.2': + resolution: {integrity: sha512-GtJSuxER7Vrp1IpxdUyRZzcckzMnb4N5KTW7sbTwUiwqARRo+wxS+gczYrS8tdgtmXs5XYhzhs+t4d52ITHMIg==} + engines: {node: '>=10'} + os: [darwin] + + '@sentry/cli-linux-arm64@2.42.2': + resolution: {integrity: sha512-BOxzI7sgEU5Dhq3o4SblFXdE9zScpz6EXc5Zwr1UDZvzgXZGosUtKVc7d1LmkrHP8Q2o18HcDWtF3WvJRb5Zpw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux, freebsd] + + '@sentry/cli-linux-arm@2.42.2': + resolution: {integrity: sha512-7udCw+YL9lwq+9eL3WLspvnuG+k5Icg92YE7zsteTzWLwgPVzaxeZD2f8hwhsu+wmL+jNqbpCRmktPteh3i2mg==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux, freebsd] + + '@sentry/cli-linux-i686@2.42.2': + resolution: {integrity: sha512-Sw/dQp5ZPvKnq3/y7wIJyxTUJYPGoTX/YeMbDs8BzDlu9to2LWV3K3r7hE7W1Lpbaw4tSquUHiQjP5QHCOS7aQ==} + engines: {node: '>=10'} + cpu: [x86, ia32] + os: [linux, freebsd] + + '@sentry/cli-linux-x64@2.42.2': + resolution: {integrity: sha512-mU4zUspAal6TIwlNLBV5oq6yYqiENnCWSxtSQVzWs0Jyq97wtqGNG9U+QrnwjJZ+ta/hvye9fvL2X25D/RxHQw==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux, freebsd] + + '@sentry/cli-win32-i686@2.42.2': + resolution: {integrity: sha512-iHvFHPGqgJMNqXJoQpqttfsv2GI3cGodeTq4aoVLU/BT3+hXzbV0x1VpvvEhncJkDgDicJpFLM8sEPHb3b8abw==} + engines: {node: '>=10'} + cpu: [x86, ia32] + os: [win32] + + '@sentry/cli-win32-x64@2.42.2': + resolution: {integrity: sha512-vPPGHjYoaGmfrU7xhfFxG7qlTBacroz5NdT+0FmDn6692D8IvpNXl1K+eV3Kag44ipJBBeR8g1HRJyx/F/9ACw==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@sentry/cli@2.42.2': + resolution: {integrity: sha512-spb7S/RUumCGyiSTg8DlrCX4bivCNmU/A1hcfkwuciTFGu8l5CDc2I6jJWWZw8/0enDGxuj5XujgXvU5tr4bxg==} + engines: {node: '>= 10'} + hasBin: true + + '@sentry/core@9.5.0': + resolution: {integrity: sha512-NMqyFdyg26ECAfnibAPKT8vvAt4zXp4R7dYtQnwJKhEJEVkgAshcNYeJ2D95ZLMVOqlqhTtTPnw1vqf+v9ePZg==} + engines: {node: '>=18'} + + '@sentry/react@9.5.0': + resolution: {integrity: sha512-ixOlKuMxWKSK73u41vY2wQNkQpZJo4fwRkA6r4oy745ldcwhGlOy/TMACdotbHCn4ULC86rVZN5r49mH6SV5+w==} + engines: {node: '>=18'} + peerDependencies: + react: ^16.14.0 || 17.x || 18.x || 19.x + + '@sentry/vite-plugin@3.2.2': + resolution: {integrity: sha512-WSkHOhZszMrIE9zmx2l4JhMnMlZmN/yAoHyf59pwFLIMctuZak6lNPbTbIFkFHDzIJ9Nut5RAVsw1qjmWc1PTA==} + engines: {node: '>= 14'} + '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} @@ -3104,6 +3198,10 @@ packages: resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} engines: {node: '>=8'} + dotenv@16.4.7: + resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} + engines: {node: '>=12'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -3433,6 +3531,10 @@ packages: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported + glob@9.3.5: + resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} + engines: {node: '>=16 || 14 >=14.17'} + global-dirs@0.1.1: resolution: {integrity: sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==} engines: {node: '>=4'} @@ -3957,6 +4059,10 @@ packages: magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + magic-string@0.30.8: + resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==} + engines: {node: '>=12'} + make-dir@2.1.0: resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} engines: {node: '>=6'} @@ -4033,6 +4139,10 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@8.0.4: + resolution: {integrity: sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==} + engines: {node: '>=16 || 14 >=14.17'} + minimatch@9.0.3: resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} engines: {node: '>=16 || 14 >=14.17'} @@ -4048,6 +4158,10 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass@4.2.8: + resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==} + engines: {node: '>=8'} + minipass@7.1.2: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} @@ -4080,6 +4194,15 @@ packages: no-case@3.0.4: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} @@ -4342,6 +4465,10 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -4966,6 +5093,9 @@ packages: resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} engines: {node: '>=6'} + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@4.1.1: resolution: {integrity: sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==} engines: {node: '>=14'} @@ -5087,6 +5217,9 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} + unplugin@1.0.1: + resolution: {integrity: sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA==} + unplugin@2.2.0: resolution: {integrity: sha512-m1ekpSwuOT5hxkJeZGRxO7gXbXT3gF26NjQ7GdVHoLoF8/nopLcd/QfPigpCy7i51oFHiRJg/CyHhj4vs2+KGw==} engines: {node: '>=18.12.0'} @@ -5277,10 +5410,20 @@ packages: resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} engines: {node: '>=14'} + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} + webpack-sources@3.2.3: + resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} + engines: {node: '>=10.13.0'} + + webpack-virtual-modules@0.5.0: + resolution: {integrity: sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==} + webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} @@ -5296,6 +5439,9 @@ packages: resolution: {integrity: sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==} engines: {node: '>=14'} + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -6808,6 +6954,105 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.34.7': optional: true + '@sentry-internal/browser-utils@9.5.0': + dependencies: + '@sentry/core': 9.5.0 + + '@sentry-internal/feedback@9.5.0': + dependencies: + '@sentry/core': 9.5.0 + + '@sentry-internal/replay-canvas@9.5.0': + dependencies: + '@sentry-internal/replay': 9.5.0 + '@sentry/core': 9.5.0 + + '@sentry-internal/replay@9.5.0': + dependencies: + '@sentry-internal/browser-utils': 9.5.0 + '@sentry/core': 9.5.0 + + '@sentry/babel-plugin-component-annotate@3.2.2': {} + + '@sentry/browser@9.5.0': + dependencies: + '@sentry-internal/browser-utils': 9.5.0 + '@sentry-internal/feedback': 9.5.0 + '@sentry-internal/replay': 9.5.0 + '@sentry-internal/replay-canvas': 9.5.0 + '@sentry/core': 9.5.0 + + '@sentry/bundler-plugin-core@3.2.2': + dependencies: + '@babel/core': 7.26.9 + '@sentry/babel-plugin-component-annotate': 3.2.2 + '@sentry/cli': 2.42.2 + dotenv: 16.4.7 + find-up: 5.0.0 + glob: 9.3.5 + magic-string: 0.30.8 + unplugin: 1.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + '@sentry/cli-darwin@2.42.2': + optional: true + + '@sentry/cli-linux-arm64@2.42.2': + optional: true + + '@sentry/cli-linux-arm@2.42.2': + optional: true + + '@sentry/cli-linux-i686@2.42.2': + optional: true + + '@sentry/cli-linux-x64@2.42.2': + optional: true + + '@sentry/cli-win32-i686@2.42.2': + optional: true + + '@sentry/cli-win32-x64@2.42.2': + optional: true + + '@sentry/cli@2.42.2': + dependencies: + https-proxy-agent: 5.0.1 + node-fetch: 2.7.0 + progress: 2.0.3 + proxy-from-env: 1.1.0 + which: 2.0.2 + optionalDependencies: + '@sentry/cli-darwin': 2.42.2 + '@sentry/cli-linux-arm': 2.42.2 + '@sentry/cli-linux-arm64': 2.42.2 + '@sentry/cli-linux-i686': 2.42.2 + '@sentry/cli-linux-x64': 2.42.2 + '@sentry/cli-win32-i686': 2.42.2 + '@sentry/cli-win32-x64': 2.42.2 + transitivePeerDependencies: + - encoding + - supports-color + + '@sentry/core@9.5.0': {} + + '@sentry/react@9.5.0(react@18.3.1)': + dependencies: + '@sentry/browser': 9.5.0 + '@sentry/core': 9.5.0 + hoist-non-react-statics: 3.3.2 + react: 18.3.1 + + '@sentry/vite-plugin@3.2.2': + dependencies: + '@sentry/bundler-plugin-core': 3.2.2 + unplugin: 1.0.1 + transitivePeerDependencies: + - encoding + - supports-color + '@sinclair/typebox@0.27.8': {} '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.26.9)': @@ -8212,6 +8457,8 @@ snapshots: dependencies: is-obj: 2.0.0 + dotenv@16.4.7: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -8750,6 +8997,13 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 + glob@9.3.5: + dependencies: + fs.realpath: 1.0.0 + minimatch: 8.0.4 + minipass: 4.2.8 + path-scurry: 1.11.1 + global-dirs@0.1.1: dependencies: ini: 1.3.8 @@ -9288,6 +9542,10 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + magic-string@0.30.8: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + make-dir@2.1.0: dependencies: pify: 4.0.1 @@ -9361,6 +9619,10 @@ snapshots: dependencies: brace-expansion: 1.1.11 + minimatch@8.0.4: + dependencies: + brace-expansion: 2.0.1 + minimatch@9.0.3: dependencies: brace-expansion: 2.0.1 @@ -9377,6 +9639,8 @@ snapshots: minimist@1.2.8: {} + minipass@4.2.8: {} + minipass@7.1.2: {} mlly@1.7.4: @@ -9411,6 +9675,10 @@ snapshots: lower-case: 2.0.2 tslib: 2.8.1 + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + node-releases@2.0.19: {} normalize-package-data@2.5.0: @@ -9659,6 +9927,8 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 + progress@2.0.3: {} + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 @@ -10411,6 +10681,8 @@ snapshots: universalify: 0.2.0 url-parse: 1.5.10 + tr46@0.0.3: {} + tr46@4.1.1: dependencies: punycode: 2.3.1 @@ -10534,6 +10806,13 @@ snapshots: universalify@2.0.1: {} + unplugin@1.0.1: + dependencies: + acorn: 8.14.0 + chokidar: 3.6.0 + webpack-sources: 3.2.3 + webpack-virtual-modules: 0.5.0 + unplugin@2.2.0: dependencies: acorn: 8.14.0 @@ -10707,8 +10986,14 @@ snapshots: dependencies: xml-name-validator: 4.0.0 + webidl-conversions@3.0.1: {} + webidl-conversions@7.0.0: {} + webpack-sources@3.2.3: {} + + webpack-virtual-modules@0.5.0: {} + webpack-virtual-modules@0.6.2: {} whatwg-encoding@2.0.0: @@ -10722,6 +11007,11 @@ snapshots: tr46: 4.1.1 webidl-conversions: 7.0.0 + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 diff --git a/web/src/common/types.ts b/web/src/common/types.ts index e8d79fe5a..93b132eda 100644 --- a/web/src/common/types.ts +++ b/web/src/common/types.ts @@ -1,4 +1,5 @@ import { isNilOrWhitespace, isNotNilOrWhitespace } from '@/lib/utils'; +import { AxiosError } from 'axios'; import { z } from 'zod'; export type FunctionComponent = React.ReactElement | null; @@ -194,8 +195,17 @@ export type LevelNode = { parentId: number; }; +export enum JWT_CLAIMS { + EMAIL = 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress', + ROLE = 'http://schemas.microsoft.com/ws/2008/06/identity/claims/role', + USER_ID = 'user-id', + USER_ROLE = 'user-role', +} + export type UserPayload = { - 'user-role': string; + [JWT_CLAIMS.USER_ID]: string; + [JWT_CLAIMS.USER_ROLE]: string; + [JWT_CLAIMS.EMAIL]: string; }; export enum FormSubmissionFollowUpStatus { @@ -417,3 +427,4 @@ export interface ProblemDetails { instance?: string; errors?: { name: string; reason: string }[]; // Maps field names to error messages } +export type ReportedError = Error | AxiosError; diff --git a/web/src/components/LocationsDashboard/LocationsDashboard.tsx b/web/src/components/LocationsDashboard/LocationsDashboard.tsx index 4016fb55b..b60e820a8 100644 --- a/web/src/components/LocationsDashboard/LocationsDashboard.tsx +++ b/web/src/components/LocationsDashboard/LocationsDashboard.tsx @@ -1,7 +1,7 @@ import { QueryParamsDataTable } from '@/components/ui/DataTable/QueryParamsDataTable'; import { ColumnDef } from '@tanstack/react-table'; -import { ElectionRoundStatus, type Location } from '@/common/types'; +import { ElectionRoundStatus, ReportedError, type Location } from '@/common/types'; import { LocationsFilters } from '@/components/LocationsFilters/LocationsFilters'; import { FilterBadge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; @@ -14,6 +14,7 @@ import { ExportDataButton } from '@/features/responses/components/ExportDataButt import { ExportedDataType } from '@/features/responses/models/data-export'; import { locationsKeys } from '@/hooks/locations-levels'; import i18n from '@/i18n'; +import { sendErrorToSentry } from '@/lib/sentry'; import { queryClient } from '@/main'; import { ArrowUpTrayIcon, FunnelIcon } from '@heroicons/react/24/outline'; import { Link, useNavigate, useRouter, useSearch } from '@tanstack/react-router'; @@ -49,11 +50,14 @@ export default function LocationsDashboard(): ReactElement { description: 'Location deleted', }); }, - onError: () => + onError: (error: ReportedError) => { + const title = 'Error occured when deleting location'; + sendErrorToSentry({ error, title }); toast({ - title: 'Error occured when deleting location', + title, variant: 'destructive', - }), + }); + }, }), [currentElectionRoundId, deleteLocationMutation] ); @@ -73,11 +77,14 @@ export default function LocationsDashboard(): ReactElement { description: 'Location updated', }); }, - onError: () => + onError: (error: ReportedError) => { + const title = 'Error occured when updating location'; + sendErrorToSentry({ error, title }); toast({ - title: 'Error occured when updating location', + title, variant: 'destructive', - }), + }); + }, }), [currentElectionRoundId, updateLocationMutation] ); diff --git a/web/src/components/LocationsDashboard/hooks.tsx b/web/src/components/LocationsDashboard/hooks.tsx index 29bf4db1d..3d7586c4b 100644 --- a/web/src/components/LocationsDashboard/hooks.tsx +++ b/web/src/components/LocationsDashboard/hooks.tsx @@ -1,5 +1,5 @@ import { authApi } from '@/common/auth-api'; -import { DataTableParameters, PageResponse, Location } from '@/common/types'; +import { DataTableParameters, Location, PageResponse, ReportedError } from '@/common/types'; import { locationsKeys } from '@/hooks/locations-levels'; import { buildURLSearchParams } from '@/lib/utils'; import { useMutation, useQuery, UseQueryResult } from '@tanstack/react-query'; @@ -48,14 +48,14 @@ export function useUpdateLocationMutation() { locationId: string; location: Location; onSuccess?: () => void; - onError?: () => void; + onError?: (error: ReportedError) => void; }) => { return authApi.put(`/election-rounds/${electionRoundId}/locations/${locationId}`, location); }, onSuccess: (_, { onSuccess }) => onSuccess?.(), - onError: (_, { onError }) => onError?.(), + onError: (error: ReportedError, { onError }) => onError?.(error), }); } @@ -68,13 +68,13 @@ export function useDeleteLocationMutation() { electionRoundId: string; locationId: string; onSuccess?: () => void; - onError?: () => void; + onError?: (error: ReportedError) => void; }) => { return authApi.delete(`/election-rounds/${electionRoundId}/locations/${locationId}`); }, onSuccess: (_, { onSuccess }) => onSuccess?.(), - onError: (_, { onError }) => onError?.(), + onError: (error: ReportedError, { onError }) => onError?.(error), }); } diff --git a/web/src/components/PasswordSetterDialog/usePasswordSetterDialog.ts b/web/src/components/PasswordSetterDialog/usePasswordSetterDialog.ts index 3e16b3a31..df314d076 100644 --- a/web/src/components/PasswordSetterDialog/usePasswordSetterDialog.ts +++ b/web/src/components/PasswordSetterDialog/usePasswordSetterDialog.ts @@ -1,6 +1,7 @@ import { authApi } from '@/common/auth-api'; import { addFormValidationErrorsFromBackend } from '@/common/form-backend-validation'; import { ProblemDetails } from '@/common/types'; +import { sendErrorToSentry } from '@/lib/sentry'; import { zodResolver } from '@hookform/resolvers/zod'; import { useMutation } from '@tanstack/react-query'; import { AxiosError } from 'axios'; @@ -77,9 +78,11 @@ export const usePasswordSetterDialog = () => { }, onError: (error: AxiosError) => { + const title = 'Error setting password'; addFormValidationErrorsFromBackend(form, error); + sendErrorToSentry({ error, title }); toast({ - title: 'Error setting password', + title, description: 'Please contact Platform admins', variant: 'destructive', }); diff --git a/web/src/components/PollingStationsDashboard/CreatePollingStationDialog.tsx b/web/src/components/PollingStationsDashboard/CreatePollingStationDialog.tsx index e0d7c47df..18dcf7f9f 100644 --- a/web/src/components/PollingStationsDashboard/CreatePollingStationDialog.tsx +++ b/web/src/components/PollingStationsDashboard/CreatePollingStationDialog.tsx @@ -1,5 +1,5 @@ import { authApi } from '@/common/auth-api'; -import { importPollingStationSchema } from '@/common/types'; +import { importPollingStationSchema, ReportedError } from '@/common/types'; import { Button } from '@/components/ui/button'; import { Dialog, DialogClose, DialogContent, DialogFooter, DialogTitle } from '@/components/ui/dialog'; import { Form, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; @@ -8,6 +8,7 @@ import { toast } from '@/components/ui/use-toast'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; import { ImportPollingStationRow } from '@/features/polling-stations/PollingStationsImport/PollingStationsImport'; import { pollingStationsKeys } from '@/hooks/polling-stations-levels'; +import { sendErrorToSentry } from '@/lib/sentry'; import { zodResolver } from '@hookform/resolvers/zod'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useForm } from 'react-hook-form'; @@ -44,9 +45,11 @@ function CreatePollingStationDialog({ open, onOpenChange }: CreatePollingStation form.reset({}); onOpenChange(false); }, - onError: (err) => { + onError: (error: ReportedError) => { + const title = t('addPollingStation.onError'); + sendErrorToSentry({ error, title }); toast({ - title: t('addPollingStation.onError'), + title, description: 'Please contact tech support', variant: 'destructive', }); diff --git a/web/src/components/PollingStationsDashboard/PollingStationsDashboard.tsx b/web/src/components/PollingStationsDashboard/PollingStationsDashboard.tsx index 20ecf18d0..339eeacb7 100644 --- a/web/src/components/PollingStationsDashboard/PollingStationsDashboard.tsx +++ b/web/src/components/PollingStationsDashboard/PollingStationsDashboard.tsx @@ -1,7 +1,7 @@ import { QueryParamsDataTable } from '@/components/ui/DataTable/QueryParamsDataTable'; import { ColumnDef } from '@tanstack/react-table'; -import { ElectionRoundStatus, type PollingStation } from '@/common/types'; +import { ElectionRoundStatus, ReportedError, type PollingStation } from '@/common/types'; import { PollingStationsFilters } from '@/components/PollingStationsFilters/PollingStationsFilters'; import { FilterBadge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; @@ -14,6 +14,7 @@ import { ExportDataButton } from '@/features/responses/components/ExportDataButt import { ExportedDataType } from '@/features/responses/models/data-export'; import { pollingStationsKeys } from '@/hooks/polling-stations-levels'; import i18n from '@/i18n'; +import { sendErrorToSentry } from '@/lib/sentry'; import { queryClient } from '@/main'; import { ArrowUpTrayIcon, FunnelIcon } from '@heroicons/react/24/outline'; import { Link, useNavigate, useRouter, useSearch } from '@tanstack/react-router'; @@ -52,11 +53,14 @@ export default function PollingStationsDashboard(): ReactElement { description: 'Polling station deleted', }); }, - onError: () => + onError: (error: ReportedError) => { + const title = 'Error occured when deleting polling station'; + sendErrorToSentry({ error, title }); toast({ - title: 'Error occured when deleting polling station', + title, variant: 'destructive', - }), + }); + }, }), [currentElectionRoundId, deletePollingStationMutation] ); @@ -76,11 +80,14 @@ export default function PollingStationsDashboard(): ReactElement { description: 'Polling station updated', }); }, - onError: () => + onError: (error: ReportedError) => { + const title = 'Error occured when updating polling station'; + sendErrorToSentry({ error, title }); toast({ - title: 'Error occured when updating polling station', + title, variant: 'destructive', - }), + }); + }, }), [currentElectionRoundId, updatePollingStationMutation] ); diff --git a/web/src/components/PollingStationsDashboard/hooks.tsx b/web/src/components/PollingStationsDashboard/hooks.tsx index 4571604c7..fac5dc266 100644 --- a/web/src/components/PollingStationsDashboard/hooks.tsx +++ b/web/src/components/PollingStationsDashboard/hooks.tsx @@ -1,5 +1,5 @@ import { authApi } from '@/common/auth-api'; -import { DataTableParameters, PageResponse, PollingStation } from '@/common/types'; +import { DataTableParameters, PageResponse, PollingStation, ReportedError } from '@/common/types'; import { pollingStationsKeys } from '@/hooks/polling-stations-levels'; import { buildURLSearchParams } from '@/lib/utils'; import { useMutation, useQuery, UseQueryResult } from '@tanstack/react-query'; @@ -51,7 +51,7 @@ export function useUpdatePollingStationMutation() { pollingStationId: string; pollingStation: PollingStation; onSuccess?: () => void; - onError?: () => void; + onError?: (error: ReportedError) => void; }) => { return authApi.put( `/election-rounds/${electionRoundId}/polling-stations/${pollingStationId}`, @@ -61,7 +61,7 @@ export function useUpdatePollingStationMutation() { onSuccess: (_, { onSuccess }) => onSuccess?.(), - onError: (_, { onError }) => onError?.(), + onError: (error: ReportedError, { onError }) => onError?.(error), }); } @@ -74,13 +74,13 @@ export function useDeletePollingStationMutation() { electionRoundId: string; pollingStationId: string; onSuccess?: () => void; - onError?: () => void; + onError?: (error: ReportedError) => void; }) => { return authApi.delete(`/election-rounds/${electionRoundId}/polling-stations/${pollingStationId}`); }, onSuccess: (_, { onSuccess }) => onSuccess?.(), - onError: (_, { onError }) => onError?.(), + onError: (error: ReportedError, { onError }) => onError?.(error), }); } diff --git a/web/src/context/auth.context.tsx b/web/src/context/auth.context.tsx index 2cfc44b3b..555ea5be4 100644 --- a/web/src/context/auth.context.tsx +++ b/web/src/context/auth.context.tsx @@ -1,8 +1,9 @@ import { ILoginResponse, LoginDTO, authApi } from '@/common/auth-api'; import { useToast } from '@/components/ui/use-toast'; +import { parseAndSetUserInSentry } from '@/lib/sentry'; import { parseJwt } from '@/lib/utils'; +import * as Sentry from '@sentry/react'; import { createContext, useEffect, useState } from 'react'; - export type AuthContextType = { signIn: (user: LoginDTO) => Promise; signOut: () => void; @@ -33,16 +34,19 @@ const AuthContextProvider = ({ children }: React.PropsWithChildren) => { const { toast } = useToast(); useEffect(() => { - const token = localStorage.getItem('token'); - setIsAuthenticated(!!token); - setIsLoading(false); - if (token) { - setToken(token); - const role = parseJwt(token)[`user-role`]; + try { + const token = localStorage.getItem('token'); + setIsAuthenticated(!!token); + setIsLoading(false); + if (token) { + setToken(token); + const role = parseJwt(token)[`user-role`]; - setUserRole(role); - setIsPlatformAdmin(role === 'PlatformAdmin'); - } + setUserRole(role); + setIsPlatformAdmin(role === 'PlatformAdmin'); + parseAndSetUserInSentry(token); + } + } catch (error) {} }, []); const signIn = async (user: LoginDTO): Promise => { @@ -53,6 +57,7 @@ const AuthContextProvider = ({ children }: React.PropsWithChildren) => { setToken(response.data.token); setUserRole(response.data.role); setIsPlatformAdmin(response.data.role === 'PlatformAdmin'); + parseAndSetUserInSentry(response.data.token); return true; } catch (error: any) { @@ -63,16 +68,23 @@ const AuthContextProvider = ({ children }: React.PropsWithChildren) => { variant: 'destructive', }); } + Sentry.captureException(error); return false; } }; const signOut = (): void => { - localStorage.removeItem('token'); - setIsAuthenticated(false); - setToken(''); - setUserRole(''); - setIsPlatformAdmin(false); + try { + localStorage.removeItem('token'); + setIsAuthenticated(false); + setToken(''); + setUserRole(''); + setIsPlatformAdmin(false); + Sentry.setUser(null); + } catch (error) { + Sentry.captureMessage(`Logout error`); + Sentry.captureException(error); + } }; return ( diff --git a/web/src/features/auth/AcceptInvite.tsx b/web/src/features/auth/AcceptInvite.tsx index ae69956a0..1274cd87f 100644 --- a/web/src/features/auth/AcceptInvite.tsx +++ b/web/src/features/auth/AcceptInvite.tsx @@ -2,16 +2,18 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; +import { noAuthApi } from '@/common/no-auth-api'; +import { ReportedError } from '@/common/types'; +import Logo from '@/components/layout/Header/Logo'; import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; -import { useNavigate } from '@tanstack/react-router'; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; -import Logo from '@/components/layout/Header/Logo'; +import { toast } from '@/components/ui/use-toast'; +import { sendErrorToSentry } from '@/lib/sentry'; import { Route as AcceptInviteRoute } from '@/routes/accept-invite/index'; import { useMutation } from '@tanstack/react-query'; -import { noAuthApi } from '@/common/no-auth-api'; -import { toast } from '@/components/ui/use-toast'; +import { useNavigate } from '@tanstack/react-router'; const formSchema = z .object({ @@ -60,9 +62,11 @@ function AcceptInvite() { navigate({ to: '/accept-invite/success' }); }, - onError: () => { + onError: (error: ReportedError) => { + const title = 'Error accepting invite'; + sendErrorToSentry({ error, title }); toast({ - title: 'Error accepting invite', + title, description: 'Please contact tech support', variant: 'destructive', }); diff --git a/web/src/features/election-event/components/Guides/AddGuideForm.tsx b/web/src/features/election-event/components/Guides/AddGuideForm.tsx index 2067a2bfb..ea04aa507 100644 --- a/web/src/features/election-event/components/Guides/AddGuideForm.tsx +++ b/web/src/features/election-event/components/Guides/AddGuideForm.tsx @@ -1,5 +1,5 @@ import { authApi } from '@/common/auth-api'; -import { FunctionComponent } from '@/common/types'; +import { FunctionComponent, ReportedError } from '@/common/types'; import { RichTextEditor } from '@/components/rich-text-editor'; import { useConfirm } from '@/components/ui/alert-dialog-provider'; import { FileUploader } from '@/components/ui/file-uploader'; @@ -7,7 +7,8 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from ' import { Input } from '@/components/ui/input'; import { toast } from '@/components/ui/use-toast'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; -import { isNilOrWhitespace, isNotNilOrWhitespace } from '@/lib/utils'; +import { sendErrorToSentry } from '@/lib/sentry'; +import { isNilOrWhitespace } from '@/lib/utils'; import { queryClient } from '@/main'; import { zodResolver } from '@hookform/resolvers/zod'; import { useMutation } from '@tanstack/react-query'; @@ -145,12 +146,14 @@ export default function AddGuideForm({ }); }, - onError: () => { + onError: (error: ReportedError) => { + const title = 'Error uploading citizen guide'; toast({ - title: 'Error uploading citizen guide', + title, description: 'Please contact Platform admins', variant: 'destructive', }); + sendErrorToSentry({ error, title }); onError?.(); }, diff --git a/web/src/features/election-event/components/Guides/EditGuideForm.tsx b/web/src/features/election-event/components/Guides/EditGuideForm.tsx index 4ea102932..7daf293f8 100644 --- a/web/src/features/election-event/components/Guides/EditGuideForm.tsx +++ b/web/src/features/election-event/components/Guides/EditGuideForm.tsx @@ -1,11 +1,12 @@ import { authApi } from '@/common/auth-api'; -import { FunctionComponent } from '@/common/types'; +import { FunctionComponent, ReportedError } from '@/common/types'; import { RichTextEditor } from '@/components/rich-text-editor'; import { useConfirm } from '@/components/ui/alert-dialog-provider'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; import { useToast } from '@/components/ui/use-toast'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; +import { sendErrorToSentry } from '@/lib/sentry'; import { isNilOrWhitespace, isNotNilOrWhitespace } from '@/lib/utils'; import { zodResolver } from '@hookform/resolvers/zod'; import { useMutation, useQueryClient, useSuspenseQuery } from '@tanstack/react-query'; @@ -136,11 +137,12 @@ export default function EditGuideForm({ }); }, - onError: () => { + onError: (error: ReportedError) => { onError?.(); - + const title = 'Error updating guide'; + sendErrorToSentry({ error, title }); toast({ - title: 'Error updating guide', + title, description: 'Please contact Platform admins', variant: 'destructive', }); diff --git a/web/src/features/election-event/components/Guides/GuidesDashboard.tsx b/web/src/features/election-event/components/Guides/GuidesDashboard.tsx index ee9dba0fb..c3c4a75fa 100644 --- a/web/src/features/election-event/components/Guides/GuidesDashboard.tsx +++ b/web/src/features/election-event/components/Guides/GuidesDashboard.tsx @@ -16,24 +16,25 @@ import { format } from 'date-fns'; import { authApi } from '@/common/auth-api'; import { DateTimeFormat } from '@/common/formats'; +import { ElectionRoundStatus, ReportedError } from '@/common/types'; import { useConfirm } from '@/components/ui/alert-dialog-provider'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { toast } from '@/components/ui/use-toast'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; import i18n from '@/i18n'; +import { sendErrorToSentry } from '@/lib/sentry'; import { queryClient } from '@/main'; import { useMutation } from '@tanstack/react-query'; import { Link, useNavigate } from '@tanstack/react-router'; import { ChevronDown } from 'lucide-react'; import { useMemo, useState } from 'react'; import { citizenGuidesKeys, useCitizenGuides } from '../../hooks/citizen-guides-hooks'; +import { useElectionRoundDetails } from '../../hooks/election-event-hooks'; import { observerGuidesKeys, useObserverGuides } from '../../hooks/observer-guides-hooks'; import { GuideModel, GuidePageType, GuideType } from '../../models/guide'; import AddGuideDialog from './AddGuideDialog'; -import EditGuideDialog from './EditGuideDialog'; -import { useElectionRoundDetails } from '../../hooks/election-event-hooks'; -import { ElectionRoundStatus } from '@/common/types'; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import EditGuideAccessDialog, { useEditGuideAccessDialog } from './EditGuideAccessDialog'; +import EditGuideDialog from './EditGuideDialog'; export interface GuidesDashboardProps { guidePageType: GuidePageType; @@ -95,9 +96,11 @@ export default function GuidesDashboard({ guidePageType }: GuidesDashboardProps) }); }, - onError: () => { + onError: (error: ReportedError) => { + const title = 'Error deleting guide'; + sendErrorToSentry({ error, title }); toast({ - title: 'Error deleting guide', + title, description: 'Please contact Platform admins', variant: 'destructive', }); diff --git a/web/src/features/election-rounds/components/CreateElectionRoundDialog/CreateElectionRoundDialog.tsx b/web/src/features/election-rounds/components/CreateElectionRoundDialog/CreateElectionRoundDialog.tsx index 03e49d353..1c733cb98 100644 --- a/web/src/features/election-rounds/components/CreateElectionRoundDialog/CreateElectionRoundDialog.tsx +++ b/web/src/features/election-rounds/components/CreateElectionRoundDialog/CreateElectionRoundDialog.tsx @@ -4,6 +4,7 @@ import { Button } from '@/components/ui/button'; import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Separator } from '@/components/ui/separator'; import { toast } from '@/components/ui/use-toast'; +import { sendErrorToSentry } from '@/lib/sentry'; import { queryClient } from '@/main'; import { useMutation } from '@tanstack/react-query'; import { useRouter } from '@tanstack/react-router'; @@ -11,6 +12,7 @@ import { format } from 'date-fns/format'; import { ElectionRoundModel } from '../../models/types'; import { electionRoundKeys } from '../../queries'; import ElectionRoundForm, { ElectionRoundRequest } from '../ElectionRoundForm/ElectionRoundForm'; +import { ReportedError } from '@/common/types'; export interface ElectionRoundFormProps { open: boolean; @@ -43,9 +45,11 @@ function CreateElectionRoundDialog({ open, onOpenChange }: ElectionRoundFormProp }); }, - onError: () => { + onError: (error: ReportedError) => { + const title = 'Error creating election round'; + sendErrorToSentry({ error, title }); toast({ - title: 'Error creating election round', + title, description: 'Please contact Platform admins', variant: 'destructive', }); diff --git a/web/src/features/election-rounds/components/ElectionRoundEdit/ElectionRoundEdit.tsx b/web/src/features/election-rounds/components/ElectionRoundEdit/ElectionRoundEdit.tsx index b478e3396..6a0998e34 100644 --- a/web/src/features/election-rounds/components/ElectionRoundEdit/ElectionRoundEdit.tsx +++ b/web/src/features/election-rounds/components/ElectionRoundEdit/ElectionRoundEdit.tsx @@ -1,10 +1,12 @@ import { authApi } from '@/common/auth-api'; import { DateOnlyFormat } from '@/common/formats'; +import { ReportedError } from '@/common/types'; import Layout from '@/components/layout/Layout'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Separator } from '@/components/ui/separator'; import { toast } from '@/components/ui/use-toast'; +import { sendErrorToSentry } from '@/lib/sentry'; import { queryClient } from '@/main'; import { Route } from '@/routes/election-rounds/$electionRoundId/edit'; import { useMutation, useSuspenseQuery } from '@tanstack/react-query'; @@ -48,9 +50,11 @@ function ElectionRoundEdit() { }); }, - onError: () => { + onError: (error: ReportedError) => { + const title = 'Error creating election round'; + sendErrorToSentry({ error, title }); toast({ - title: 'Error creating election round', + title, description: 'Please contact Platform admins', variant: 'destructive', }); diff --git a/web/src/features/form-templates/components/Dashboard/Dashboard.tsx b/web/src/features/form-templates/components/Dashboard/Dashboard.tsx index 1996816d7..356e271ab 100644 --- a/web/src/features/form-templates/components/Dashboard/Dashboard.tsx +++ b/web/src/features/form-templates/components/Dashboard/Dashboard.tsx @@ -1,6 +1,6 @@ import { authApi } from '@/common/auth-api'; import { DateTimeFormat } from '@/common/formats'; -import { FormBase, FormStatus } from '@/common/types'; +import { FormBase, FormStatus, ReportedError } from '@/common/types'; import AddFormTranslationsDialog, { useAddFormTranslationsDialog, } from '@/components/AddFormTranslationsDialog/AddFormTranslationsDialog'; @@ -25,6 +25,7 @@ import { toast } from '@/components/ui/use-toast'; import { useFilteringContainer } from '@/features/filtering/hooks/useFilteringContainer'; import { useLanguages } from '@/hooks/languages'; import i18n from '@/i18n'; +import { sendErrorToSentry } from '@/lib/sentry'; import { cn, mapFormType } from '@/lib/utils'; import { queryClient } from '@/main'; import { FormTemplatesSearchParams, Route } from '@/routes/form-templates/index'; @@ -434,10 +435,11 @@ export default function FormTemplatesDashboard(): ReactElement { }, onError: (error) => { + const title = 'Error publishing form template'; // @ts-ignore if (error.response.status === 400) { toast({ - title: 'Error publishing form template', + title, description: 'You are missing translations. Please translate all fields and try again', variant: 'destructive', }); @@ -445,10 +447,11 @@ export default function FormTemplatesDashboard(): ReactElement { return; } toast({ - title: 'Error publishing form template', + title, description: 'Please contact tech support', variant: 'destructive', }); + sendErrorToSentry({ error: error as ReportedError, title }); }, }); @@ -467,9 +470,11 @@ export default function FormTemplatesDashboard(): ReactElement { router.invalidate(); }, - onError: () => { + onError: (error: ReportedError) => { + const title = 'Error obsoleting form'; + sendErrorToSentry({ error, title }); toast({ - title: 'Error obsoleting form', + title, description: 'Please contact tech support', variant: 'destructive', }); @@ -491,9 +496,11 @@ export default function FormTemplatesDashboard(): ReactElement { router.invalidate(); }, - onError: () => { + onError: (error: ReportedError) => { + const title = 'Error cloning form template'; + sendErrorToSentry({ error, title }); toast({ - title: 'Error cloning form template', + title, description: 'Please contact tech support', variant: 'destructive', }); diff --git a/web/src/features/form-templates/components/FormTemplateEdit/FormTemplateEdit.tsx b/web/src/features/form-templates/components/FormTemplateEdit/FormTemplateEdit.tsx index b4534fc46..aad2bd674 100644 --- a/web/src/features/form-templates/components/FormTemplateEdit/FormTemplateEdit.tsx +++ b/web/src/features/form-templates/components/FormTemplateEdit/FormTemplateEdit.tsx @@ -1,10 +1,13 @@ import { authApi } from '@/common/auth-api'; import { mapToQuestionRequest } from '@/common/form-requests'; +import { ReportedError } from '@/common/types'; import FormEditor, { EditFormType } from '@/components/FormEditor/FormEditor'; +import { FormTemplateDetailsBreadcrumbs } from '@/components/FormTemplateDetailsBreadcrumbs/FormTemplateDetailsBreadcrumbs'; import Layout from '@/components/layout/Layout'; import { NavigateBack } from '@/components/NavigateBack/NavigateBack'; import { useConfirm } from '@/components/ui/alert-dialog-provider'; import { useToast } from '@/components/ui/use-toast'; +import { sendErrorToSentry } from '@/lib/sentry'; import { isNilOrWhitespace } from '@/lib/utils'; import { queryClient } from '@/main'; import { Route } from '@/routes/form-templates/$formTemplateId_.edit'; @@ -13,7 +16,6 @@ import { useNavigate, useRouter } from '@tanstack/react-router'; import { useCallback } from 'react'; import { UpdateFormTemplateRequest } from '../../models'; import { formTemlatesKeys, formTemplateDetailsQueryOptions } from '../../queries'; -import { FormTemplateDetailsBreadcrumbs } from '@/components/FormTemplateDetailsBreadcrumbs/FormTemplateDetailsBreadcrumbs'; function FormTemplateEdit() { const { formTemplateId } = Route.useParams(); @@ -57,9 +59,11 @@ function FormTemplateEdit() { } }, - onError: () => { + onError: (error: ReportedError) => { + const title = 'Error saving form template'; + sendErrorToSentry({ error, title }); toast({ - title: 'Error saving form template', + title, description: 'Please contact tech support', variant: 'destructive', }); diff --git a/web/src/features/form-templates/components/FormTemplateNew/FormTemplateNew.tsx b/web/src/features/form-templates/components/FormTemplateNew/FormTemplateNew.tsx index d99d90613..af2186012 100644 --- a/web/src/features/form-templates/components/FormTemplateNew/FormTemplateNew.tsx +++ b/web/src/features/form-templates/components/FormTemplateNew/FormTemplateNew.tsx @@ -1,9 +1,11 @@ import { authApi } from '@/common/auth-api'; import { mapToQuestionRequest } from '@/common/form-requests'; +import { ReportedError } from '@/common/types'; import FormEditor, { EditFormType } from '@/components/FormEditor/FormEditor'; import Layout from '@/components/layout/Layout'; import { NavigateBack } from '@/components/NavigateBack/NavigateBack'; import { useToast } from '@/components/ui/use-toast'; +import { sendErrorToSentry } from '@/lib/sentry'; import { isNilOrWhitespace } from '@/lib/utils'; import { queryClient } from '@/main'; import { useMutation } from '@tanstack/react-query'; @@ -46,9 +48,11 @@ function FormTemplateNew() { } }, - onError: () => { + onError: (error: ReportedError) => { + const title = 'Error creating form template'; + sendErrorToSentry({ error, title }); toast({ - title: 'Error creating form template', + title, description: 'Please contact tech support', variant: 'destructive', }); diff --git a/web/src/features/form-templates/components/FormTemplateTranslationEdit/FormTemplateTranslationEdit.tsx b/web/src/features/form-templates/components/FormTemplateTranslationEdit/FormTemplateTranslationEdit.tsx index 7370589b2..c2aceb33a 100644 --- a/web/src/features/form-templates/components/FormTemplateTranslationEdit/FormTemplateTranslationEdit.tsx +++ b/web/src/features/form-templates/components/FormTemplateTranslationEdit/FormTemplateTranslationEdit.tsx @@ -1,11 +1,14 @@ import { authApi } from '@/common/auth-api'; import { mapToQuestionRequest } from '@/common/form-requests'; +import { ReportedError } from '@/common/types'; import { EditFormType } from '@/components/FormEditor/FormEditor'; +import { FormTemplateDetailsBreadcrumbs } from '@/components/FormTemplateDetailsBreadcrumbs/FormTemplateDetailsBreadcrumbs'; import FormTranslationEditor from '@/components/FormTranslationEditor/FormTranslationEditor'; import Layout from '@/components/layout/Layout'; import { NavigateBack } from '@/components/NavigateBack/NavigateBack'; import { useConfirm } from '@/components/ui/alert-dialog-provider'; import { useToast } from '@/components/ui/use-toast'; +import { sendErrorToSentry } from '@/lib/sentry'; import { isNilOrWhitespace } from '@/lib/utils'; import { queryClient } from '@/main'; import { Route } from '@/routes/form-templates/$formTemplateId_.edit-translation.$languageCode'; @@ -14,7 +17,6 @@ import { useNavigate, useRouter } from '@tanstack/react-router'; import { useCallback } from 'react'; import { UpdateFormTemplateRequest } from '../../models'; import { formTemlatesKeys, formTemplateDetailsQueryOptions } from '../../queries'; -import { FormTemplateDetailsBreadcrumbs } from '@/components/FormTemplateDetailsBreadcrumbs/FormTemplateDetailsBreadcrumbs'; function FormTemplateTranslationEdit() { const { formTemplateId, languageCode } = Route.useParams(); @@ -58,9 +60,11 @@ function FormTemplateTranslationEdit() { } }, - onError: () => { + onError: (error: ReportedError) => { + const title = 'Error saving form template'; + sendErrorToSentry({ error, title }); toast({ - title: 'Error saving form template', + title, description: 'Please contact tech support', variant: 'destructive', }); diff --git a/web/src/features/forms/components/Dashboard/Dashboard.tsx b/web/src/features/forms/components/Dashboard/Dashboard.tsx index 3bc4430bd..dfe44cc0f 100644 --- a/web/src/features/forms/components/Dashboard/Dashboard.tsx +++ b/web/src/features/forms/components/Dashboard/Dashboard.tsx @@ -1,6 +1,6 @@ import { authApi } from '@/common/auth-api'; import { DateTimeFormat } from '@/common/formats'; -import { ElectionRoundStatus, FormStatus, FormType } from '@/common/types'; +import { ElectionRoundStatus, FormStatus, FormType, ReportedError } from '@/common/types'; import AddFormTranslationsDialog, { useAddFormTranslationsDialog, } from '@/components/AddFormTranslationsDialog/AddFormTranslationsDialog'; @@ -27,6 +27,7 @@ import { useElectionRoundDetails } from '@/features/election-event/hooks/electio import { useFilteringContainer } from '@/features/filtering/hooks/useFilteringContainer'; import { useLanguages } from '@/hooks/languages'; import i18n from '@/i18n'; +import { sendErrorToSentry } from '@/lib/sentry'; import { cn, isNotNilOrWhitespace, mapFormType } from '@/lib/utils'; import { queryClient } from '@/main'; import { FormsSearchParams, Route } from '@/routes/election-event/$tab'; @@ -682,11 +683,12 @@ export default function FormsDashboard(): ReactElement { router.invalidate(); }, - onError: (error) => { + onError: (error: ReportedError) => { + const title = 'Error publishing form'; // @ts-ignore if (error.response.status === 400) { toast({ - title: 'Error publishing form', + title, description: 'You are missing translations. Please translate all fields and try again', variant: 'destructive', }); @@ -694,10 +696,11 @@ export default function FormsDashboard(): ReactElement { return; } toast({ - title: 'Error publishing form', + title, description: 'Please contact tech support', variant: 'destructive', }); + sendErrorToSentry({ error, title }); }, }); @@ -716,9 +719,11 @@ export default function FormsDashboard(): ReactElement { router.invalidate(); }, - onError: () => { + onError: (error: ReportedError) => { + const title = 'Error obsoleting form'; + sendErrorToSentry({ error, title }); toast({ - title: 'Error obsoleting form', + title, description: 'Please contact tech support', variant: 'destructive', }); @@ -740,9 +745,11 @@ export default function FormsDashboard(): ReactElement { router.invalidate(); }, - onError: (error) => { + onError: (error: ReportedError) => { + const title = 'Error cloning form'; + sendErrorToSentry({ error, title }); toast({ - title: 'Error cloning form', + title, description: 'Please contact tech support', variant: 'destructive', }); diff --git a/web/src/features/forms/components/FormEdit/FormEdit.tsx b/web/src/features/forms/components/FormEdit/FormEdit.tsx index 79f35157f..c6b504845 100644 --- a/web/src/features/forms/components/FormEdit/FormEdit.tsx +++ b/web/src/features/forms/components/FormEdit/FormEdit.tsx @@ -1,5 +1,6 @@ import { authApi } from '@/common/auth-api'; import { mapToQuestionRequest } from '@/common/form-requests'; +import { ReportedError } from '@/common/types'; import { FormDetailsBreadcrumbs } from '@/components/FormDetailsBreadcrumbs/FormDetailsBreadcrumbs'; import FormEditor, { EditFormType } from '@/components/FormEditor/FormEditor'; import Layout from '@/components/layout/Layout'; @@ -8,6 +9,7 @@ import { useConfirm } from '@/components/ui/alert-dialog-provider'; import { useToast } from '@/components/ui/use-toast'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; import { useElectionRoundDetails } from '@/features/election-event/hooks/election-event-hooks'; +import { sendErrorToSentry } from '@/lib/sentry'; import { isNilOrWhitespace } from '@/lib/utils'; import { queryClient } from '@/main'; import { Route } from '@/routes/forms/$formId_.edit'; @@ -64,9 +66,11 @@ function FormEdit() { } }, - onError: () => { + onError: (error: ReportedError) => { + const title = 'Error saving form template'; + sendErrorToSentry({ error, title }); toast({ - title: 'Error saving form template', + title, description: 'Please contact tech support', variant: 'destructive', }); diff --git a/web/src/features/forms/components/FormNew/FormNew.tsx b/web/src/features/forms/components/FormNew/FormNew.tsx index 363d0bf98..d8200f6a6 100644 --- a/web/src/features/forms/components/FormNew/FormNew.tsx +++ b/web/src/features/forms/components/FormNew/FormNew.tsx @@ -1,9 +1,13 @@ import { authApi } from '@/common/auth-api'; import { mapToQuestionRequest } from '@/common/form-requests'; +import { ReportedError } from '@/common/types'; import FormEditor, { EditFormType } from '@/components/FormEditor/FormEditor'; import Layout from '@/components/layout/Layout'; import { NavigateBack } from '@/components/NavigateBack/NavigateBack'; import { useToast } from '@/components/ui/use-toast'; +import { useCurrentElectionRoundStore } from '@/context/election-round.store'; +import { useElectionRoundDetails } from '@/features/election-event/hooks/election-event-hooks'; +import { sendErrorToSentry } from '@/lib/sentry'; import { isNilOrWhitespace } from '@/lib/utils'; import { queryClient } from '@/main'; import { useMutation } from '@tanstack/react-query'; @@ -11,8 +15,6 @@ import { useNavigate, useRouter } from '@tanstack/react-router'; import { useCallback } from 'react'; import { FormFull, NewFormRequest } from '../../models'; import { formsKeys } from '../../queries'; -import { useElectionRoundDetails } from '@/features/election-event/hooks/election-event-hooks'; -import { useCurrentElectionRoundStore } from '@/context/election-round.store'; function FormNew() { const navigate = useNavigate(); @@ -56,9 +58,11 @@ function FormNew() { } }, - onError: () => { + onError: (error: ReportedError) => { + const title = 'Error creating form'; + sendErrorToSentry({ error, title }); toast({ - title: 'Error creating form', + title, description: 'Please contact tech support', variant: 'destructive', }); diff --git a/web/src/features/forms/components/FormTranslationEdit/FormTranslationEdit.tsx b/web/src/features/forms/components/FormTranslationEdit/FormTranslationEdit.tsx index bc7f4a951..5a4d3ffc5 100644 --- a/web/src/features/forms/components/FormTranslationEdit/FormTranslationEdit.tsx +++ b/web/src/features/forms/components/FormTranslationEdit/FormTranslationEdit.tsx @@ -1,22 +1,24 @@ import { authApi } from '@/common/auth-api'; import { mapToQuestionRequest } from '@/common/form-requests'; +import { ReportedError } from '@/common/types'; +import { FormDetailsBreadcrumbs } from '@/components/FormDetailsBreadcrumbs/FormDetailsBreadcrumbs'; import { EditFormType } from '@/components/FormEditor/FormEditor'; import FormTranslationEditor from '@/components/FormTranslationEditor/FormTranslationEditor'; import Layout from '@/components/layout/Layout'; import { NavigateBack } from '@/components/NavigateBack/NavigateBack'; import { useConfirm } from '@/components/ui/alert-dialog-provider'; import { useToast } from '@/components/ui/use-toast'; +import { useCurrentElectionRoundStore } from '@/context/election-round.store'; +import { useElectionRoundDetails } from '@/features/election-event/hooks/election-event-hooks'; +import { sendErrorToSentry } from '@/lib/sentry'; import { isNilOrWhitespace } from '@/lib/utils'; import { queryClient } from '@/main'; +import { Route } from '@/routes/forms/$formId_.edit-translation.$languageCode'; import { useMutation, useSuspenseQuery } from '@tanstack/react-query'; import { useNavigate, useRouter } from '@tanstack/react-router'; import { useCallback } from 'react'; import { UpdateFormRequest } from '../../models'; -import { formsKeys, formDetailsQueryOptions } from '../../queries'; -import { useCurrentElectionRoundStore } from '@/context/election-round.store'; -import { useElectionRoundDetails } from '@/features/election-event/hooks/election-event-hooks'; -import { Route } from '@/routes/forms/$formId_.edit-translation.$languageCode'; -import { FormDetailsBreadcrumbs } from '@/components/FormDetailsBreadcrumbs/FormDetailsBreadcrumbs'; +import { formDetailsQueryOptions, formsKeys } from '../../queries'; function FormTranslationEdit() { const { formId, languageCode } = Route.useParams(); @@ -65,9 +67,11 @@ function FormTranslationEdit() { } }, - onError: () => { + onError: (error: ReportedError) => { + const title = 'Error saving form'; + sendErrorToSentry({ error, title }); toast({ - title: 'Error saving form ', + title, description: 'Please contact tech support', variant: 'destructive', }); diff --git a/web/src/features/forms/hooks.ts b/web/src/features/forms/hooks.ts index eae504ee7..6e3d14814 100644 --- a/web/src/features/forms/hooks.ts +++ b/web/src/features/forms/hooks.ts @@ -1,6 +1,8 @@ import { authApi } from '@/common/auth-api'; +import { ReportedError } from '@/common/types'; import { toast } from '@/components/ui/use-toast'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; +import { sendErrorToSentry } from '@/lib/sentry'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useNavigate } from '@tanstack/react-router'; import { create } from 'zustand'; @@ -33,7 +35,7 @@ export const useCreateFormFromTemplate = () => { const currentElectionRoundId = useCurrentElectionRoundStore((s) => s.currentElectionRoundId); const navigate = useNavigate(); const queryClient = useQueryClient(); - + const createFormFromTemplateMutation = useMutation({ mutationFn: ({ templateId, languageCode }: FormFromTemplateDto) => { return authApi.post(`/election-rounds/${currentElectionRoundId}/forms:fromTemplate`, { @@ -51,12 +53,15 @@ export const useCreateFormFromTemplate = () => { navigate({ to: '/forms/$formId/edit', params: { formId: response.data.id } }); }, - onError: (err) => + onError: (error: ReportedError) => { + const title = 'Error creating form from template'; + sendErrorToSentry({ title, error }); toast({ - title: 'Error creating form from template', + title, description: 'Please contact tech support', variant: 'destructive', - }), + }); + }, onSettled: () => { if (isOpen) dismiss(); @@ -110,12 +115,15 @@ export const useCreateFormFromForm = () => { navigate({ to: '/forms/$formId/edit', params: { formId: response.data.id } }); }, - onError: (err) => + onError: (error: ReportedError) => { + const title = 'Error creating form from template'; + sendErrorToSentry({ title, error }); toast({ - title: 'Error creating form from template', + title, description: 'Please contact tech support', variant: 'destructive', - }), + }); + }, onSettled: () => { if (isOpen) dismiss(); diff --git a/web/src/features/locations/LocationsImport/LocationsImport.tsx b/web/src/features/locations/LocationsImport/LocationsImport.tsx index 46fe7e59f..c04a74848 100644 --- a/web/src/features/locations/LocationsImport/LocationsImport.tsx +++ b/web/src/features/locations/LocationsImport/LocationsImport.tsx @@ -1,4 +1,4 @@ -import { FunctionComponent, importLocationSchema } from '@/common/types'; +import { FunctionComponent, importLocationSchema, ReportedError } from '@/common/types'; import Layout from '@/components/layout/Layout'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { FileUploader } from '@/components/ui/file-uploader'; @@ -12,6 +12,7 @@ import { Button } from '@/components/ui/button'; import { useToast } from '@/components/ui/use-toast'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; import { locationsKeys } from '@/hooks/locations-levels'; +import { sendErrorToSentry } from '@/lib/sentry'; import { downloadImportExample, TemplateType } from '@/lib/utils'; import { queryClient } from '@/main'; import { ArrowDownTrayIcon } from '@heroicons/react/24/outline'; @@ -63,9 +64,11 @@ export function LocationsImport(): FunctionComponent { queryClient.invalidateQueries({ queryKey: locationsKeys.all(electionRoundId) }); navigate({ to: '/election-rounds/$electionRoundId', params: { electionRoundId } }); }, - onError: () => { + onError: (error: ReportedError) => { + const title = t('onError'); + sendErrorToSentry({ error, title }); toast({ - title: t('onError'), + title, description: 'Please contact tech support', variant: 'destructive', }); diff --git a/web/src/features/monitoring-observers/components/MonitoringObserversImport/MonitoringObserversImport.tsx b/web/src/features/monitoring-observers/components/MonitoringObserversImport/MonitoringObserversImport.tsx index 08bc6b6ff..c22836c1c 100644 --- a/web/src/features/monitoring-observers/components/MonitoringObserversImport/MonitoringObserversImport.tsx +++ b/web/src/features/monitoring-observers/components/MonitoringObserversImport/MonitoringObserversImport.tsx @@ -1,16 +1,18 @@ -import { FunctionComponent } from '@/common/types'; +import { FunctionComponent, ReportedError } from '@/common/types'; import Layout from '@/components/layout/Layout'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { FileUploader } from '@/components/ui/file-uploader'; import { Separator } from '@/components/ui/separator'; import Papa from 'papaparse'; import { useMemo, useState } from 'react'; -import { ZodIssue, ZodIssueCode, z } from 'zod'; +import { z, ZodIssue, ZodIssueCode } from 'zod'; import { authApi } from '@/common/auth-api'; import { Button } from '@/components/ui/button'; import { toast } from '@/components/ui/use-toast'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; +import { sendErrorToSentry } from '@/lib/sentry'; +import { downloadImportExample, TemplateType } from '@/lib/utils'; import { queryClient } from '@/main'; import { ArrowDownTrayIcon } from '@heroicons/react/24/outline'; import { useMutation } from '@tanstack/react-query'; @@ -19,7 +21,6 @@ import { LoaderIcon } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { monitoringObserversKeys } from '../../hooks/monitoring-observers-queries'; import { ImportedObserversDataTable } from './ImportedObserversDataTable'; -import { downloadImportExample, TemplateType } from '@/lib/utils'; export const importObserversSchema = z.object({ firstName: z @@ -80,9 +81,11 @@ export function MonitoringObserversImport(): FunctionComponent { queryClient.invalidateQueries({ queryKey: monitoringObserversKeys.all(electionRoundId) }); navigate({ to: '/monitoring-observers' }); }, - onError: () => { + onError: (error: ReportedError) => { + const title = t('onError'); + sendErrorToSentry({ error, title }); toast({ - title: t('onError'), + title, description: 'Please contact tech support', variant: 'destructive', }); diff --git a/web/src/features/monitoring-observers/components/MonitoringObserversList/CreateMonitoringObserverDialog.tsx b/web/src/features/monitoring-observers/components/MonitoringObserversList/CreateMonitoringObserverDialog.tsx index 2490914d4..dd8eca4fd 100644 --- a/web/src/features/monitoring-observers/components/MonitoringObserversList/CreateMonitoringObserverDialog.tsx +++ b/web/src/features/monitoring-observers/components/MonitoringObserversList/CreateMonitoringObserverDialog.tsx @@ -1,4 +1,5 @@ import { authApi } from '@/common/auth-api'; +import { ReportedError } from '@/common/types'; import { Button } from '@/components/ui/button'; import { Dialog, DialogClose, DialogContent, DialogFooter, DialogTitle } from '@/components/ui/dialog'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; @@ -7,6 +8,7 @@ import TagsSelectFormField from '@/components/ui/tag-selector'; import { toast } from '@/components/ui/use-toast'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; import { useMonitoringObserversTags } from '@/hooks/tags-queries'; +import { sendErrorToSentry } from '@/lib/sentry'; import { zodResolver } from '@hookform/resolvers/zod'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useForm } from 'react-hook-form'; @@ -61,9 +63,11 @@ function CreateMonitoringObserverDialog({ open, onOpenChange }: CreateMonitoring form.reset({}); onOpenChange(false); }, - onError: () => { + onError: (error: ReportedError) => { + const title = t('onError'); + sendErrorToSentry({ error, title }); toast({ - title: t('onError'), + title, description: 'Please contact tech support', variant: 'destructive', }); diff --git a/web/src/features/monitoring-observers/components/MonitoringObserversList/MonitoringObserversList.tsx b/web/src/features/monitoring-observers/components/MonitoringObserversList/MonitoringObserversList.tsx index b3edd1e17..49dd05d80 100644 --- a/web/src/features/monitoring-observers/components/MonitoringObserversList/MonitoringObserversList.tsx +++ b/web/src/features/monitoring-observers/components/MonitoringObserversList/MonitoringObserversList.tsx @@ -26,7 +26,7 @@ import { CellContext, ColumnDef } from '@tanstack/react-table'; import { useEffect, useMemo, useState } from 'react'; import { DateTimeFormat } from '@/common/formats'; -import { ElectionRoundStatus } from '@/common/types'; +import { ElectionRoundStatus, ReportedError } from '@/common/types'; import { TableCellProps } from '@/components/ui/DataTable/DataTable'; import { DataTableColumnHeader } from '@/components/ui/DataTable/DataTableColumnHeader'; import { toast } from '@/components/ui/use-toast'; @@ -35,6 +35,7 @@ import { useElectionRoundDetails } from '@/features/election-event/hooks/electio import { FILTER_KEY } from '@/features/filtering/filtering-enums'; import { useFilteringContainer } from '@/features/filtering/hooks/useFilteringContainer'; import i18n from '@/i18n'; +import { sendErrorToSentry } from '@/lib/sentry'; import { queryClient } from '@/main'; import { Route } from '@/routes/monitoring-observers/$tab'; import { useDebounce } from '@uidotdev/usehooks'; @@ -207,9 +208,11 @@ function MonitoringObserversList() { }); }, - onError: () => { + onError: (error: ReportedError) => { + const title = 'Error resending invitation'; + sendErrorToSentry({ error, title }); toast({ - title: 'Error resending invitation', + title, description: 'Please contact Platform admins', variant: 'destructive', }); diff --git a/web/src/features/ngos/components/CreateNGODialog.tsx b/web/src/features/ngos/components/CreateNGODialog.tsx index 539abf42d..7f9da9d7d 100644 --- a/web/src/features/ngos/components/CreateNGODialog.tsx +++ b/web/src/features/ngos/components/CreateNGODialog.tsx @@ -1,13 +1,17 @@ +import { addFormValidationErrorsFromBackend } from '@/common/form-backend-validation'; +import { ProblemDetails, ReportedError } from '@/common/types'; import { Button } from '@/components/ui/button'; import { Dialog, DialogClose, DialogContent, DialogFooter, DialogTitle } from '@/components/ui/dialog'; import { Form, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; import { toast } from '@/components/ui/use-toast'; +import { sendErrorToSentry } from '@/lib/sentry'; import { zodResolver } from '@hookform/resolvers/zod'; +import { AxiosError } from 'axios'; +import { useCallback, useEffect } from 'react'; import { useForm } from 'react-hook-form'; import { useCreateNgo } from '../hooks/ngos-queries'; import { newNgoSchema, NgoCreationFormData } from '../models/NGO'; -import { useCallback, useEffect } from 'react'; export interface CreateNGODialogProps { open: boolean; @@ -53,13 +57,13 @@ function CreateNGODialog({ open, onOpenChange }: CreateNGODialogProps) { description: 'New organization created', }); }, - onMutationError: (error) => { - error?.errors?.forEach((error) => { - form.setError(error.name as keyof NgoCreationFormData, { type: 'custom', message: error.reason }); - }); + onMutationError: (error: ReportedError) => { + const title = 'Error adding NGO admin'; + addFormValidationErrorsFromBackend(form, error as AxiosError); + sendErrorToSentry({ error, title }); toast({ - title: 'Error adding NGO admin', + title, description: 'Please contact Platform admins', variant: 'destructive', }); diff --git a/web/src/features/ngos/hooks/ngo-admin-queries.ts b/web/src/features/ngos/hooks/ngo-admin-queries.ts index e32cb84f3..9db8a6d70 100644 --- a/web/src/features/ngos/hooks/ngo-admin-queries.ts +++ b/web/src/features/ngos/hooks/ngo-admin-queries.ts @@ -1,8 +1,9 @@ import { authApi } from '@/common/auth-api'; -import { DataTableParameters, PageResponse, ProblemDetails } from '@/common/types'; +import { DataTableParameters, PageResponse, ProblemDetails, ReportedError } from '@/common/types'; import { useConfirm } from '@/components/ui/alert-dialog-provider'; import { buttonVariants } from '@/components/ui/button'; import { toast } from '@/components/ui/use-toast'; +import { sendErrorToSentry } from '@/lib/sentry'; import { queryOptions, useMutation, @@ -113,9 +114,11 @@ export const useNgoAdminMutations = (ngoId: string) => { router.invalidate(); navigate({ to: '/ngos/admin/$ngoId/$adminId/view', params: { ngoId, adminId } }); }, - onError: () => { + onError: (error: ReportedError) => { + const title = 'Error editing NGO admin'; + sendErrorToSentry({ error, title }); toast({ - title: 'Error editing NGO admin', + title, description: '', variant: 'destructive', }); @@ -139,9 +142,11 @@ export const useNgoAdminMutations = (ngoId: string) => { if (onMutationSuccess) onMutationSuccess(); }, - onError: () => { + onError: (error: ReportedError) => { + const title = 'Error deleting NGO admin'; + sendErrorToSentry({ error, title }); toast({ - title: 'Error deleting NGO admin', + title, description: '', variant: 'destructive', }); @@ -163,9 +168,11 @@ export const useNgoAdminMutations = (ngoId: string) => { }); }, - onError: () => { + onError: (error: ReportedError) => { + const title = 'Error deactivating the NGO admin'; + sendErrorToSentry({ error, title }); toast({ - title: 'Error deactivating the NGO admin', + title, description: '', variant: 'destructive', }); @@ -187,9 +194,11 @@ export const useNgoAdminMutations = (ngoId: string) => { }); }, - onError: () => { + onError: (error: ReportedError) => { + const title = 'Error activating the NGO admin'; + sendErrorToSentry({ error, title }); toast({ - title: 'Error activating the NGO admin', + title, description: '', variant: 'destructive', }); diff --git a/web/src/features/ngos/hooks/ngos-queries.ts b/web/src/features/ngos/hooks/ngos-queries.ts index 5e62ca0e0..a616f16c4 100644 --- a/web/src/features/ngos/hooks/ngos-queries.ts +++ b/web/src/features/ngos/hooks/ngos-queries.ts @@ -1,13 +1,14 @@ import { authApi } from '@/common/auth-api'; -import { DataTableParameters, PageResponse, ProblemDetails } from '@/common/types'; +import { DataTableParameters, PageResponse, ProblemDetails, ReportedError } from '@/common/types'; import { useConfirm } from '@/components/ui/alert-dialog-provider'; import { buttonVariants } from '@/components/ui/button'; import { toast } from '@/components/ui/use-toast'; +import { sendErrorToSentry } from '@/lib/sentry'; import { queryClient } from '@/main'; import { queryOptions, useMutation, useQuery, UseQueryResult, useSuspenseQuery } from '@tanstack/react-query'; import { useNavigate, useRouter } from '@tanstack/react-router'; -import { EditNgoFormData, NGO, NgoCreationFormData } from '../models/NGO'; import axios, { AxiosError } from 'axios'; +import { EditNgoFormData, NGO, NgoCreationFormData } from '../models/NGO'; const STALE_TIME = 1000 * 10 * 60; // 10 minutes @@ -64,7 +65,7 @@ export const useCreateNgo = () => { }: { values: NgoCreationFormData; onMutationSuccess: () => void; - onMutationError: (error?: ProblemDetails) => void; + onMutationError: (error: ReportedError) => void; }) => { return authApi.post('/ngos', { name: values.name }); }, @@ -78,14 +79,13 @@ export const useCreateNgo = () => { const axiosError = error as AxiosError; if (axiosError.response?.status === 400) { - const problemDetails = axiosError.response.data; - return onMutationError(problemDetails); + return onMutationError(axiosError); } } // Handle non-Axios or unexpected errors console.error('Unexpected error:', error); - onMutationError(); + onMutationError(error); toast({ title: 'Error creating a new NGO', description: '', @@ -114,9 +114,11 @@ export const useNgoMutations = () => { router.invalidate(); navigate({ to: '/ngos/view/$ngoId/$tab', params: { ngoId: ngoId!, tab: 'details' } }); }, - onError: () => { + onError: (error: ReportedError) => { + const title = 'Error editing NGO'; + sendErrorToSentry({ error, title }); toast({ - title: 'Error editing NGO', + title, description: '', variant: 'destructive', }); @@ -138,9 +140,11 @@ export const useNgoMutations = () => { }); }, - onError: () => { + onError: (error: ReportedError) => { + const title = 'Error deactivating NGO'; + sendErrorToSentry({ error, title }); toast({ - title: 'Error deactivating NGO', + title, description: '', variant: 'destructive', }); @@ -162,9 +166,11 @@ export const useNgoMutations = () => { }); }, - onError: () => { + onError: (error: ReportedError) => { + const title = 'Error activating NGO'; + sendErrorToSentry({ error, title }); toast({ - title: 'Error activating NGO', + title, description: '', variant: 'destructive', }); @@ -188,9 +194,11 @@ export const useNgoMutations = () => { if (onMutationSuccess) onMutationSuccess(); }, - onError: () => { + onError: (error: ReportedError) => { + const title = 'Error deleting NGO'; + sendErrorToSentry({ error, title }); toast({ - title: 'Error deleting NGO', + title, description: '', variant: 'destructive', }); diff --git a/web/src/features/observers/hooks/observers-queries.ts b/web/src/features/observers/hooks/observers-queries.ts index 5b1bed496..94e8aa25b 100644 --- a/web/src/features/observers/hooks/observers-queries.ts +++ b/web/src/features/observers/hooks/observers-queries.ts @@ -1,9 +1,10 @@ import { authApi } from '@/common/auth-api'; import { addFormValidationErrorsFromBackend } from '@/common/form-backend-validation'; -import type { DataTableParameters, PageResponse, ProblemDetails } from '@/common/types'; +import type { DataTableParameters, PageResponse, ProblemDetails, ReportedError } from '@/common/types'; import { useConfirm } from '@/components/ui/alert-dialog-provider'; import { buttonVariants } from '@/components/ui/button'; import { toast } from '@/components/ui/use-toast'; +import { sendErrorToSentry } from '@/lib/sentry'; import { type UseQueryResult, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useNavigate, useRouter } from '@tanstack/react-router'; import { AxiosError } from 'axios'; @@ -73,9 +74,11 @@ export const useObserverMutations = () => { }, onError: (error: AxiosError, { form }) => { console.error(error); + const title = 'Error adding observer'; addFormValidationErrorsFromBackend(form, error); + sendErrorToSentry({ error, title }); toast({ - title: 'Error adding observer', + title, description: '', variant: 'destructive', }); @@ -100,11 +103,11 @@ export const useObserverMutations = () => { navigate({ to: '/observers/$observerId', params: { observerId } }); }, onError: (error: AxiosError, { form }) => { - console.error(error); + const title = 'Error editing observer'; addFormValidationErrorsFromBackend(form, error); - + sendErrorToSentry({ error, title }); toast({ - title: 'Error editing observer', + title, description: '', variant: 'destructive', }); @@ -128,11 +131,12 @@ export const useObserverMutations = () => { }); }, - onError: (err, { isObserverActive }) => { - console.error(err); + onError: (error: ReportedError, { isObserverActive }) => { + const description = `Error ${isObserverActive ? 'deactivating' : 'activating'} observer`; + sendErrorToSentry({ error, title: description }); toast({ title: `Error`, - description: `Error ${isObserverActive ? 'deactivating' : 'activating'} observer`, + description, variant: 'destructive', }); }, @@ -155,9 +159,11 @@ export const useObserverMutations = () => { if (onMutationSuccess) onMutationSuccess(); }, - onError: () => { + onError: (error: ReportedError) => { + const title = 'Error deleting observer'; + sendErrorToSentry({ error, title }); toast({ - title: 'Error deleting observer', + title, description: '', variant: 'destructive', }); diff --git a/web/src/features/polling-stations/PollingStationsImport/PollingStationsImport.tsx b/web/src/features/polling-stations/PollingStationsImport/PollingStationsImport.tsx index 86dac9763..6462668b1 100644 --- a/web/src/features/polling-stations/PollingStationsImport/PollingStationsImport.tsx +++ b/web/src/features/polling-stations/PollingStationsImport/PollingStationsImport.tsx @@ -1,4 +1,4 @@ -import { FunctionComponent, importPollingStationSchema } from '@/common/types'; +import { FunctionComponent, importPollingStationSchema, ReportedError } from '@/common/types'; import Layout from '@/components/layout/Layout'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { FileUploader } from '@/components/ui/file-uploader'; @@ -12,6 +12,7 @@ import { Button } from '@/components/ui/button'; import { useToast } from '@/components/ui/use-toast'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; import { pollingStationsKeys } from '@/hooks/polling-stations-levels'; +import { sendErrorToSentry } from '@/lib/sentry'; import { downloadImportExample, TemplateType } from '@/lib/utils'; import { queryClient } from '@/main'; import { ArrowDownTrayIcon } from '@heroicons/react/24/outline'; @@ -71,12 +72,14 @@ export function PollingStationsImport(): FunctionComponent { queryClient.invalidateQueries({ queryKey: pollingStationsKeys.all(electionRoundId) }); navigate({ to: '/election-rounds/$electionRoundId', params: { electionRoundId } }); }, - onError: () => { + onError: (error: ReportedError) => { + const title = t('onError'); toast({ - title: t('onError'), + title, description: 'Please contact tech support', variant: 'destructive', }); + sendErrorToSentry({ error, title }); }, }); diff --git a/web/src/features/responses/components/CitizenReportDetails/CitizenReportDetails.tsx b/web/src/features/responses/components/CitizenReportDetails/CitizenReportDetails.tsx index 041491c2b..d332c2282 100644 --- a/web/src/features/responses/components/CitizenReportDetails/CitizenReportDetails.tsx +++ b/web/src/features/responses/components/CitizenReportDetails/CitizenReportDetails.tsx @@ -1,29 +1,31 @@ import { authApi } from '@/common/auth-api'; +import { DateTimeFormat } from '@/common/formats'; +import { usePrevSearch } from '@/common/prev-search-store'; import { CitizenReportFollowUpStatus, ElectionRoundStatus, FormSubmissionFollowUpStatus, + ReportedError, type FunctionComponent, } from '@/common/types'; import Layout from '@/components/layout/Layout'; +import { NavigateBack } from '@/components/NavigateBack/NavigateBack'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Separator } from '@/components/ui/separator'; import { toast } from '@/components/ui/use-toast'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; +import { useElectionRoundDetails } from '@/features/election-event/hooks/election-event-hooks'; +import { sendErrorToSentry } from '@/lib/sentry'; import { queryClient } from '@/main'; import { citizenReportDetailsQueryOptions, Route } from '@/routes/responses/citizen-reports/$citizenReportId'; import { useMutation, useSuspenseQuery } from '@tanstack/react-query'; import { useRouter } from '@tanstack/react-router'; +import { format } from 'date-fns'; import { citizenReportKeys } from '../../hooks/citizen-reports'; -import PreviewAnswer from '../PreviewAnswer/PreviewAnswer'; import { SubmissionType } from '../../models/common'; import { mapCitizenReportFollowUpStatus } from '../../utils/helpers'; -import { usePrevSearch } from '@/common/prev-search-store'; -import { NavigateBack } from '@/components/NavigateBack/NavigateBack'; -import { DateTimeFormat } from '@/common/formats'; -import { format } from 'date-fns'; -import { useElectionRoundDetails } from '@/features/election-event/hooks/election-event-hooks'; +import PreviewAnswer from '../PreviewAnswer/PreviewAnswer'; export default function CitizenReportDetails(): FunctionComponent { const { citizenReportId } = Route.useParams(); @@ -61,9 +63,11 @@ export default function CitizenReportDetails(): FunctionComponent { router.invalidate(); }, - onError: () => { + onError: (error: ReportedError) => { + const title = 'Error updating follow up status'; + sendErrorToSentry({ error, title }); toast({ - title: 'Error updating follow up status', + title, description: 'Please contact tech support', variant: 'destructive', }); diff --git a/web/src/features/responses/components/ExportDataButton/ExportDataButton.tsx b/web/src/features/responses/components/ExportDataButton/ExportDataButton.tsx index b7af1c1e0..d4314249f 100644 --- a/web/src/features/responses/components/ExportDataButton/ExportDataButton.tsx +++ b/web/src/features/responses/components/ExportDataButton/ExportDataButton.tsx @@ -1,12 +1,13 @@ -import { useCallback, useEffect, useState } from 'react'; import { authApi } from '@/common/auth-api'; -import type { FunctionComponent } from '@/common/types'; +import type { FunctionComponent, ReportedError } from '@/common/types'; import { CsvFileIcon } from '@/components/icons/CsvFileIcon'; import { Button } from '@/components/ui/button'; import { toast } from '@/components/ui/use-toast'; +import { useCurrentElectionRoundStore } from '@/context/election-round.store'; +import { sendErrorToSentry } from '@/lib/sentry'; +import { useCallback, useEffect, useState } from 'react'; import { useExportedDataDetails, useStartDataExport } from '../../hooks/data-export'; import { ExportStatus, type ExportedDataType } from '../../models/data-export'; -import { useCurrentElectionRoundStore } from '@/context/election-round.store'; interface ExportDataButtonProps { exportedDataType: ExportedDataType; @@ -27,8 +28,10 @@ export function ExportDataButton({ exportedDataType, filterParams }: ExportDataB onSuccess: (data) => { setExportedDataId(data.exportedDataId); }, - onError: () => { - toast({ title: 'Export failed, please try again later', variant: 'default' }); + onError: (error: ReportedError) => { + const title = 'Export failed, please try again later'; + sendErrorToSentry({ error, title }); + toast({ title, variant: 'default' }); }, } ); diff --git a/web/src/features/responses/components/FormSubmissionDetails/FormSubmissionDetails.tsx b/web/src/features/responses/components/FormSubmissionDetails/FormSubmissionDetails.tsx index af954cf9a..37f4f6d9f 100644 --- a/web/src/features/responses/components/FormSubmissionDetails/FormSubmissionDetails.tsx +++ b/web/src/features/responses/components/FormSubmissionDetails/FormSubmissionDetails.tsx @@ -1,27 +1,34 @@ import { authApi } from '@/common/auth-api'; import { DateTimeFormat } from '@/common/formats'; -import { ElectionRoundStatus, FormSubmissionFollowUpStatus, FunctionComponent, FormType } from '@/common/types'; +import { usePrevSearch } from '@/common/prev-search-store'; +import { + ElectionRoundStatus, + FormSubmissionFollowUpStatus, + FormType, + FunctionComponent, + ReportedError, +} from '@/common/types'; import Layout from '@/components/layout/Layout'; +import { NavigateBack } from '@/components/NavigateBack/NavigateBack'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { LanguageBadge } from '@/components/ui/language-badge'; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Separator } from '@/components/ui/separator'; import { toast } from '@/components/ui/use-toast'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; import { useElectionRoundDetails } from '@/features/election-event/hooks/election-event-hooks'; +import { sendErrorToSentry } from '@/lib/sentry'; import { queryClient } from '@/main'; import { Route, formSubmissionDetailsQueryOptions } from '@/routes/responses/form-submissions/$submissionId'; import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline'; import { useMutation, useSuspenseQuery } from '@tanstack/react-query'; import { Link, useRouter } from '@tanstack/react-router'; import { format } from 'date-fns'; +import { useState } from 'react'; import { formSubmissionsByEntryKeys, formSubmissionsByObserverKeys } from '../../hooks/form-submissions-queries'; import { SubmissionType } from '../../models/common'; import { mapFormSubmissionFollowUpStatus } from '../../utils/helpers'; import PreviewAnswer from '../PreviewAnswer/PreviewAnswer'; -import { usePrevSearch } from '@/common/prev-search-store'; -import { NavigateBack } from '@/components/NavigateBack/NavigateBack'; -import { useState } from 'react'; -import { LanguageBadge } from '@/components/ui/language-badge'; export default function FormSubmissionDetails(): FunctionComponent { const { submissionId } = Route.useParams(); @@ -59,9 +66,11 @@ export default function FormSubmissionDetails(): FunctionComponent { router.invalidate(); }, - onError: () => { + onError: (error: ReportedError) => { + const title = 'Error updating follow up status'; + sendErrorToSentry({ error, title }); toast({ - title: 'Error updating follow up status', + title, description: 'Please contact tech support', variant: 'destructive', }); diff --git a/web/src/features/responses/components/IncidentReportDetails/IncidentReportDetails.tsx b/web/src/features/responses/components/IncidentReportDetails/IncidentReportDetails.tsx index e85459ff5..0fd486b98 100644 --- a/web/src/features/responses/components/IncidentReportDetails/IncidentReportDetails.tsx +++ b/web/src/features/responses/components/IncidentReportDetails/IncidentReportDetails.tsx @@ -1,23 +1,29 @@ import { authApi } from '@/common/auth-api'; -import { IncidentReportFollowUpStatus, type FunctionComponent, ElectionRoundStatus } from '@/common/types'; +import { DateTimeFormat } from '@/common/formats'; +import { + ElectionRoundStatus, + IncidentReportFollowUpStatus, + ReportedError, + type FunctionComponent, +} from '@/common/types'; import Layout from '@/components/layout/Layout'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Separator } from '@/components/ui/separator'; import { toast } from '@/components/ui/use-toast'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; +import { useElectionRoundDetails } from '@/features/election-event/hooks/election-event-hooks'; +import { sendErrorToSentry } from '@/lib/sentry'; import { queryClient } from '@/main'; import { incidentReportDetailsQueryOptions, Route } from '@/routes/responses/incident-reports/$incidentReportId'; import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline'; import { useMutation, useSuspenseQuery } from '@tanstack/react-query'; import { Link, useRouter } from '@tanstack/react-router'; +import { format } from 'date-fns'; import { incidentReportsByEntryKeys, incidentReportsByObserverKeys } from '../../hooks/incident-reports-queries'; import { SubmissionType } from '../../models/common'; import { mapIncidentReportFollowUpStatus, mapIncidentReportLocationType } from '../../utils/helpers'; import PreviewAnswer from '../PreviewAnswer/PreviewAnswer'; -import { format } from 'date-fns'; -import { DateTimeFormat } from '@/common/formats'; -import { useElectionRoundDetails } from '@/features/election-event/hooks/election-event-hooks'; export default function IncidentReportDetails(): FunctionComponent { const { incidentReportId } = Route.useParams(); @@ -56,9 +62,11 @@ export default function IncidentReportDetails(): FunctionComponent { router.invalidate(); }, - onError: () => { + onError: (error: ReportedError) => { + const title = 'Error updating follow up status'; + sendErrorToSentry({ error, title }); toast({ - title: 'Error updating follow up status', + title, description: 'Please contact tech support', variant: 'destructive', }); diff --git a/web/src/features/responses/components/QuickReportDetails/QuickReportDetails.tsx b/web/src/features/responses/components/QuickReportDetails/QuickReportDetails.tsx index 70b690734..ce2f1564d 100644 --- a/web/src/features/responses/components/QuickReportDetails/QuickReportDetails.tsx +++ b/web/src/features/responses/components/QuickReportDetails/QuickReportDetails.tsx @@ -1,7 +1,7 @@ import { authApi } from '@/common/auth-api'; import { DateTimeFormat } from '@/common/formats'; import { usePrevSearch } from '@/common/prev-search-store'; -import { QuickReportFollowUpStatus, type FunctionComponent, ElectionRoundStatus } from '@/common/types'; +import { ElectionRoundStatus, QuickReportFollowUpStatus, ReportedError, type FunctionComponent } from '@/common/types'; import { NavigateBack } from '@/components/NavigateBack/NavigateBack'; import Layout from '@/components/layout/Layout'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; @@ -10,6 +10,7 @@ import { Separator } from '@/components/ui/separator'; import { toast } from '@/components/ui/use-toast'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; import { useElectionRoundDetails } from '@/features/election-event/hooks/election-event-hooks'; +import { sendErrorToSentry } from '@/lib/sentry'; import { queryClient } from '@/main'; import { Route, quickReportDetailsQueryOptions } from '@/routes/responses/quick-reports/$quickReportId'; import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline'; @@ -53,9 +54,11 @@ export default function QuickReportDetails(): FunctionComponent { void queryClient.invalidateQueries({ queryKey: quickReportKeys.all(electionRoundId) }); }, - onError: () => { + onError: (error: ReportedError) => { + const title = 'Error updating follow up status'; + sendErrorToSentry({ error, title }); toast({ - title: 'Error updating follow up status', + title, description: 'Please contact tech support', variant: 'destructive', }); diff --git a/web/src/lib/sentry.ts b/web/src/lib/sentry.ts new file mode 100644 index 000000000..675efc5a2 --- /dev/null +++ b/web/src/lib/sentry.ts @@ -0,0 +1,28 @@ +import { JWT_CLAIMS, ReportedError } from '@/common/types'; +import * as Sentry from '@sentry/react'; +import { parseJwt } from './utils'; + +export const parseAndSetUserInSentry = (token: string) => { + try { + const decodedToken = parseJwt(token); + + Sentry.setUser({ + email: decodedToken[JWT_CLAIMS.EMAIL], + role: decodedToken[JWT_CLAIMS.USER_ROLE], + userId: decodedToken[JWT_CLAIMS.USER_ID], + }); + } catch (error) { + Sentry.captureException(error); + console.error('Error decoding token:', error); + } +}; + +type SendErrorToSentry = { + error: ReportedError; + title: string; +}; + +export const sendErrorToSentry = ({ error, title }: SendErrorToSentry) => { + Sentry.captureMessage(title, 'error'); + Sentry.captureException(error); +}; diff --git a/web/src/main.tsx b/web/src/main.tsx index a63517493..e7d5e1e6c 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -1,16 +1,17 @@ +import * as Sentry from '@sentry/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { createRouter, ErrorComponent, RouterProvider } from '@tanstack/react-router'; import React, { useContext } from 'react'; import ReactDOM from 'react-dom/client'; import { I18nextProvider } from 'react-i18next'; import { AlertDialogProvider } from './components/ui/alert-dialog-provider.tsx'; +import { TanStackReactQueryDevelopmentTools } from './components/utils/development-tools/TanStackReactQueryDevelopmentTools.tsx'; +import { TanStackRouterDevelopmentTools } from './components/utils/development-tools/TanStackRouterDevelopmentTools.tsx'; import AuthContextProvider, { AuthContext } from './context/auth.context'; +import { CurrentElectionRoundContext, CurrentElectionRoundStoreProvider } from './context/election-round.store.tsx'; import i18n from './i18n'; import { routeTree } from './routeTree.gen.ts'; import './styles/tailwind.css'; -import { CurrentElectionRoundContext, CurrentElectionRoundStoreProvider } from './context/election-round.store.tsx'; -import { TanStackReactQueryDevelopmentTools } from './components/utils/development-tools/TanStackReactQueryDevelopmentTools.tsx'; -import { TanStackRouterDevelopmentTools } from './components/utils/development-tools/TanStackRouterDevelopmentTools.tsx'; export const queryClient = new QueryClient({ defaultOptions: { @@ -40,6 +41,17 @@ declare module '@tanstack/react-router' { } } +Sentry.init({ + dsn: import.meta.env['VITE_SENTRY_DSN'], + debug: import.meta.env.DEV, + environment: import.meta.env.MODE, + tracesSampleRate: import.meta.env.PROD ? 0.2 : 0, + enabled: !import.meta.env.PROD, + tracePropagationTargets: ['localhost', /^https:\/\/api\.votemonitor\.org/, /^https:\/\/votemonitor\.staging\.heroesof\.tech\/api\//], + integrations: [Sentry.browserTracingIntegration(), Sentry.tanstackRouterBrowserTracingIntegration(router)], + normalizeDepth: 5, +}); + function App() { const authContext = useContext(AuthContext); const currentElectionRoundContext = useContext(CurrentElectionRoundContext); diff --git a/web/vite.config.ts b/web/vite.config.ts index 772b2742b..b4e3c8f38 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -1,33 +1,54 @@ -import { TanStackRouterVite } from "@tanstack/router-plugin/vite" -import react from "@vitejs/plugin-react-swc"; -import path from "node:path"; -import { normalizePath } from "vite"; -import { viteStaticCopy } from "vite-plugin-static-copy"; -import { defineConfig } from "vitest/config"; - +import { sentryVitePlugin } from '@sentry/vite-plugin'; +import { TanStackRouterVite } from '@tanstack/router-plugin/vite'; +import react from '@vitejs/plugin-react-swc'; +import path from 'node:path'; +import { loadEnv, normalizePath } from 'vite'; +import { viteStaticCopy } from 'vite-plugin-static-copy'; +import { defineConfig } from 'vitest/config'; // https://vitejs.dev/config/ -export default defineConfig({ - plugins: [react(), TanStackRouterVite(), - viteStaticCopy({ - targets: [ - { - src: normalizePath(path.resolve('./src/assets/locales')), - dest: normalizePath(path.resolve('./dist')) - } - ] - })], - server: { - host: true, - strictPort: true, - }, - resolve: { - alias: { - '@': path.resolve(__dirname, './src'), + +export default defineConfig(({ mode }) => { + // Load env file based on `mode` in the current working directory. + // Set the third parameter to '' to load all env regardless of the + // `VITE_` prefix. + const env = loadEnv(mode, process.cwd(), ''); + return { + // vite config + + build: { + sourcemap: true, + }, + plugins: [ + react(), + TanStackRouterVite(), + viteStaticCopy({ + targets: [ + { + src: normalizePath(path.resolve('./src/assets/locales')), + dest: normalizePath(path.resolve('./dist')), + }, + ], + }), + sentryVitePlugin({ + org: env.VITE_SENTRY_ORG, + project: env.VITE_SENTRY_PROJECT, + authToken: env.VITE_SENTRY_AUTH_TOKEN, + sourcemaps: { filesToDeleteAfterUpload: ['dist/assets/**.js.map'] }, + }), + ], + server: { + host: true, + strictPort: true, + }, + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, + test: { + environment: 'jsdom', + setupFiles: ['./vitest.setup.ts'], + css: true, }, - }, - test: { - environment: "jsdom", - setupFiles: ["./vitest.setup.ts"], - css: true, - }, -}); \ No newline at end of file + }; +});