From 34f4573c87971867b3bd78a7be636c6853fd0bd9 Mon Sep 17 00:00:00 2001 From: Martial Maillot Date: Wed, 12 Feb 2025 15:05:15 +0100 Subject: [PATCH] feat(sentry): migration sur la nouvelle version de Sentry # Conflicts: # .kontinuous/env/dev/values.yaml --- .github/workflows/review-auto.yaml | 2 + .github/workflows/review.yaml | 2 + .../dev/templates/sentry.sealed-secret.yaml | 16 + .../env/dev/templates/www.sealed-secret.yaml | 1 - .kontinuous/env/dev/values.yaml | 24 +- .kontinuous/values.yaml | 11 + Dockerfile | 49 +- .../droit-du-travail.test.js.snap | 6 +- .../glossaire-[slug].test.tsx.snap | 6 +- .../__snapshots__/glossaire.test.tsx.snap | 6 +- .../modeles-de-courriers.test.tsx.snap | 6 +- .../__snapshots__/recherche.test.js.snap | 6 +- .../__snapshots__/themes.test.tsx.snap | 6 +- .../app/api/monitoring/envelope/route.ts | 262 +++++ .../app/api/test-sentry-error/route.ts | 39 + .../app/global-error.tsx | 8 +- .../code-du-travail-frontend/next.config.mjs | 104 +- .../code-du-travail-frontend/package.json | 2 +- .../pages/api/test-sentry-error-legacy.ts | 43 + .../sentry.client.config.ts | 97 +- .../sentry.edge.config.ts | 52 + .../sentry.server.config.ts | 73 +- .../code-du-travail-frontend/src/config.ts | 3 + .../src/layout/Layout.tsx | 2 + .../src/lib/SentryTest.tsx | 84 ++ .../src/modules/config/DefaultLayout.tsx | 3 + .../src/modules/sentry/SentryTest.tsx | 88 ++ .../src/modules/sentry/error.ts | 76 ++ .../src/modules/sentry/index.ts | 2 + yarn.lock | 944 +++++++++++------- 30 files changed, 1552 insertions(+), 471 deletions(-) create mode 100644 .kontinuous/env/dev/templates/sentry.sealed-secret.yaml create mode 100644 packages/code-du-travail-frontend/app/api/monitoring/envelope/route.ts create mode 100644 packages/code-du-travail-frontend/app/api/test-sentry-error/route.ts create mode 100644 packages/code-du-travail-frontend/pages/api/test-sentry-error-legacy.ts create mode 100644 packages/code-du-travail-frontend/sentry.edge.config.ts create mode 100644 packages/code-du-travail-frontend/src/lib/SentryTest.tsx create mode 100644 packages/code-du-travail-frontend/src/modules/sentry/SentryTest.tsx create mode 100644 packages/code-du-travail-frontend/src/modules/sentry/error.ts create mode 100644 packages/code-du-travail-frontend/src/modules/sentry/index.ts diff --git a/.github/workflows/review-auto.yaml b/.github/workflows/review-auto.yaml index f6bc970608..e1bdd78110 100644 --- a/.github/workflows/review-auto.yaml +++ b/.github/workflows/review-auto.yaml @@ -40,6 +40,8 @@ jobs: run: | echo "SITE_URL=https://${{ steps.env.outputs.subdomain }}.ovh.fabrique.social.gouv.fr" >> $GITHUB_ENV - name: Run test e2e + env: + CYPRESS: "true" run: | TEST_BASEURL=${{ env.SITE_URL }} TEST_MODE=light yarn test:e2e - name: Archive generated screenshots diff --git a/.github/workflows/review.yaml b/.github/workflows/review.yaml index e8a0a17839..e2e14eb2d9 100644 --- a/.github/workflows/review.yaml +++ b/.github/workflows/review.yaml @@ -37,6 +37,8 @@ jobs: run: | echo "SITE_URL=https://${{ steps.env.outputs.subdomain }}.ovh.fabrique.social.gouv.fr" >> $GITHUB_ENV - name: Run test e2e + env: + CYPRESS: "true" run: | TEST_BASEURL=${{ env.SITE_URL }} TEST_MODE=light yarn test:e2e - name: Archive generated screenshots diff --git a/.kontinuous/env/dev/templates/sentry.sealed-secret.yaml b/.kontinuous/env/dev/templates/sentry.sealed-secret.yaml new file mode 100644 index 0000000000..8bc347ad3d --- /dev/null +++ b/.kontinuous/env/dev/templates/sentry.sealed-secret.yaml @@ -0,0 +1,16 @@ +apiVersion: bitnami.com/v1alpha1 +kind: SealedSecret +metadata: + annotations: + sealedsecrets.bitnami.com/cluster-wide: 'true' + name: sentry +spec: + encryptedData: + SENTRY_DSN: AgDCMaihNlpRZTWAxW87fBaiGPbNLugKZnWAeLI8Tux2IKwn1xpIkeL/5tqHX3hwUJf1UFbanz2u1NzBAW1l/hQ0CANRGDQcqX06q9/Rbwr+s9aa8yflYJIQM+E0XHVYaauCyJBvfb00Ep8cSdPwUjA8aUug0asfNcz1iPC25bEB8HX+9Nz8L4MPeArsGiirjVNSWqojk+odLKydSatIpO81STTCF8/pCX0+Rs/6Zg2eFZU17sS1E6eyjOAec7WFF1S6l8D0o+B6WOroopSflnmY9+QWHf2UBVRb5y3PLbpR8K8jrn9ZGjjXKT77RHu6/pVR2AklGSk/ri0XAXT+TvFzFFKoy5sloZrDPdvA1JQnVgxZV5syaIQcu+BOKIN9xvUWop/hsFwpmbxFgfyDxgFV5UmGgF014RdhEGpWc6FuQc3kRXbTQerxF/nLH1OTFqnhA+jsHYNIsfv8Q24DIFE17yM7RI7W9CkFy//hqHES1AayRwx2njZxdUSwNyyzQcQIhvGQP5KcLXU9GIOr3UxCdJBswzYqTTt1VGdBMkM0I+xZgdXIWaL4WHrfe+eQhr6pwJtIJZNtpijvFuz2Ot8Ewm1PILCl0kbq9IuJb5FkDqMuzskMa/AYesfFEMUdLo7ybIHaag2i/DYdzw2U1sqT/M8tc7yZW/ztcG5rHQ3vK1twhmMznl05bpuzSozTz2X68qYwC6otGlNSlPxntOwzpL+irlJYE3JXqz8nds064EOJf7w7lffzMKpPWiyNEL3j39/ma7k6E4EwWFUwbqbSvJju6ajB8YD3e6U= + SENTRY_AUTH_TOKEN: AgAaGu/aJjM6cEx4sEKuLC3bjJrE4AnaTQKTmcG57OcJUonD6zo2ytoNx8dKwEhp6SNO8mq1ZHXJuzEDc/0vqVinTYfg3ZYmHpLMmK6ofclVcgqT/za45gDJCgt/3x3hUGrNiUz1c1hYjNje2FQ4gIvUYUYGaMSEsWGPi2cZrIsCoeaYP2l2IpVoohmAj/lfCboJD2A3d0hFJDbArqTYoBECuPf2QpidNWi14vSwZTtugGbz9wplR+Vc/P0pzkz/M/p9dQB/XwMXiPyrUHNC2bWmBfs4MctoMiG/mRRAAnUyssHLJt615IAZuRpXN0ZGi2yw+vQDG5ggq2VKHbWzPnXR2ggZI19iFFLGvdjvYdu3uQiPin3PWzqo6pbAkFCjg8h7gTaaMBtXd5zFa4xrSQ+UfQfJ7wKMABZ0Ry+XpqkW2ZkRuQdOWQaOFKy13j/+3PLg1hQBd7iEyoYIUYjR1PyIQG1fp7jG9FRvglvJimzlW8PsjSw19p6wvsRscK+u3jlHdQJ0+pZTCe7AyZvM91T9LPwW5mj9s+RM5jtJ+/b6WKrDWt9cAk4YR0Vjdja+I3yueLs4CM8a68kcLY1vsjfATWUMh1keMaMngn39UMhPOMdz76gcFGIwcn63v7wi//jIe1ZhY97566JezPZ9AFx4Iw44/T5fF/gA6SGLw0+UGuQbaPpLf1w6Dmz9KaOnQzsZSgWyTNBEX7+9LnuRL1UL4RRCah4MlFdRigvP7H1nay56QKSaZREPCisXp/h3t0XPlbjdNr529Sqwj674LC039JWYRhy+6r11rjuPz7V45FnNSoACozkNlGoFkXkP4fesBsyLiUR89n0Qs2k5ku/JTrosLLl/aRDDPXvuP0f6xXuMiFSt66MSXR/US2JGaokuOC0NL/rdPWcULGWMK8JlAtjZyimqO/KAlneLnIBa3EV1kbpSHbOdvaKf4M0RQBu/AUPT4NiDLrg9ddGkh2e2lF4wRNACysuNjb00R4YuKOe8c+VCJX2HUEvgXxsnrWL76UZi9wiQ + template: + metadata: + annotations: + sealedsecrets.bitnami.com/cluster-wide: 'true' + name: sentry + type: Opaque diff --git a/.kontinuous/env/dev/templates/www.sealed-secret.yaml b/.kontinuous/env/dev/templates/www.sealed-secret.yaml index 5879411503..66c3a01085 100644 --- a/.kontinuous/env/dev/templates/www.sealed-secret.yaml +++ b/.kontinuous/env/dev/templates/www.sealed-secret.yaml @@ -8,7 +8,6 @@ spec: encryptedData: ELASTICSEARCH_URL: AgAUJaPLaiM5gyJSFwbPwfZauAtxMMmW85PhhA85jAmz1z2k3MS/tgc0hzdwLJ6psJcivAONP11BcJ3uXuz7MvOsdXKhkKbU03nb4lB/jDhYh3dAj2Keuz0TMKgZSZUxj8rETucDRgusJ6I35tVFshXG3tbqlP/K290CrmlqA2hWVAHoR9HtdBdbRjW3vBxiOjjSvcUo7bWCU/9/TmMhWskkgVLG6bQOSUWZBS+9ab2yzg/2aqNN7ZwzZc5yzurLjdkk/vJhPTSMbmdCy3DIxTaDMt4fcJ6ONxQZtOW5MZqlhdMEmYbW+/OuHQlDt8kYR5hIn35WIRWOXa5109GyyI9UXFOHtRn4AxrxuWlyzzwiuQsWk147ozqU1bV1j80HFDJtFmopP2jDCRK9ijmLsBThBBqyPzFrfPZWQsLEj+xAb+jc2fuwKtfeNYnzoY9p0/IKwrUFkBjzjrggCSkm87bVPZGFUBK0tnL1l4OYB3qJhyWEVlL1nPFDCxIXOO3nIwC9XH6wUKe5VIIR/XZDWPnYFI+kktHFgPgGCqsHEujwp8w+aVpQy+L3r5kSuWonUguUIUDuvGYc/I5nXzpy25yKpSXjq/1d1vvx66pD8AngyDSGowGVX3nfvf2dAq1K/UQQUvWq2Aaf5Bh03tA0RVPczgtAQZ7iwueoMVWzBddGAywcIJOW1lJQJ6kQZ6flt1dnDosLSU7Ra6KdRAElopLjqNzEUFJpGzv+Di72mcrOWDkUxqz2qFssfwDuPaQ/HLS0+mgt8qHFJLgQxCOCuhyz2GWv ELASTICSEARCH_TOKEN_API: AgC4tRnPP5Rq9G12mvlWjt/m8w4Z6AAZPQwbLxK2sYHEfF7KuH+QVeknaX6x861lGHSZq4r1bJJJUBInCG1gwV8j8ndt6g4GfAul+fouRoYVCoyCyRZ063qXm+OsTvaihlbi8hAPoTt+iDWkX6PRkMMrWrBTgxGHpoTizeqLYyu2EXz5nSmF1UfzIgXfcArD7k8ESbQ9rJEPdEWSoH9xbami3FnPltID21g0JViXjHTm3nuIO7NHH6/gkQdXYL7mxGh+wy6bDo0+98WV5eFzUc22D9rqsd/q2RaW5s/fUCnWnU0WDknSJR4JWAOh3RFWIJ+NiWMpa4oiYs/sTp560GdJC4aZoWI7oGEzjVdHRjC8IbQIiPCLj+3KKFVFsgGLub6/ILRVtZwynYLv1V+D9GvjNMQJAsLVk/98+6GneZKHAEObEbuCw9mij+cgJzYog/aKMHF4JAnM+LAZVYJVWyn2Te0fqq6Z4NjjP6XIOGNMNQFro9+d335u1qAb50L/Rpkvzi1Qon0EBVGgtj9/VZOLk7MsJstKtB3nMI95ObsVfRCh5BxNEbC0FHMmRdUOcx204F2Lteb51LaVqL/ARFZPYRPw9Z1Cgi4focKH3J+oEAKp1N8jvymtobhIqoM+xLfmSmVvBbq5F3VfwdXEXm3WYC5gAZW7Fgkx3qForfw/lBShk/aXhs3FzAAWnXgZao8bHEsbGxGpRtwBwG5Y/WnzlVy/Z1VKI6klxK7Vn63AgrwQjsQ8izdK6EtzhJhjG/keor2Ye5Hp3MOA16A= - SENTRY_AUTH_TOKEN: AgCKID/es7BZx/js5TA1A0OXpHRvTMzxsKYdfrwXEtM+sUj9NAjPWnNMw+x/ZrxJ+927XkzvOc9bCnMMEHylXUwuz3zf1u3XYFBaGd3vG0VCDeHshH5vokLEhEy3d44qdJmIMWqXYFGWjuWj/3yW1x3+4DyBQGO1zxYleiPppYrnheF3yrdwFUM/v4PnHsBPxqL8Tqr+tdi4J3o3POanNglbF4/AsgNJbpGOFxkhZXCewWhEr9lF+sX7bxbo7Pl5tc/RwdTn9xtQiEvO8Sqxkl/cPwXcEvylTWc2eaGg76deD4Le1LEyFpNDQ5AzTmt+LnC9tGmBvpkYVZz5uJev+ybhAcj0OcOOZq1Cg0p5hZl8zD5JmHES78mMxpGvc9799wIfNx26ASNtHj+dFBRrHrgfqIrGoI9MDaSSEB9w4PBLTZsuLdaFZJgiWtzVPud1cnYSaYC4tsfG3qhHbKOUJXOA+BvJR2TZEdzHZCWMGUVg0slUjGCCbkvD7HDEswXYATDj3RzhkNbwhvbWp2HSfS8awlzTbOOhP3SQGr3EiuD0OVgBW/fRBAY77cq6NnNqpiK4STutN9uC801Zxd9uPsWiY5nv0XtMzS6No4ZutyKR6gI10ZSQqvRrdcYbAPoYbOBIUR2zXyzd1bx+wCnojRofTf/gHptKq9FopKDlTmxj040rTW42CIQzXgf5kVRUxKZrDB0LICLwRNU+YYdK8kuBIvisOQ9dx2P6Ez4nAfWdnO93z87tt7306a9kIyuw/diN75t6ai6uJhF0ac+FiIEDJTNexVg7CQg2kPzgpH4M5H7RgsxnKxPm1qnbDMJ7KG3Vzu4BG4vc/fa6fgihpN+t0b5PqHbuWaskhqUSEPsKdx8Q2cRgL0+27JCpdWRDQDX8wF3MTgdWWxE3TYZmjGq42u9TNfHBBmr/PCDfhPI3PFV+WZkwayV3K3LIjQf9H8GcpZD7w5OyL44oAYcL6Iye6Y1420A6mq5b1a9rayO6wWlLQQwtX78haGVcMv7RXo+bK/CXu+IR template: metadata: annotations: diff --git a/.kontinuous/env/dev/values.yaml b/.kontinuous/env/dev/values.yaml index 4af3653797..0c664e32ae 100644 --- a/.kontinuous/env/dev/values.yaml +++ b/.kontinuous/env/dev/values.yaml @@ -2,14 +2,16 @@ jobs: runs: build-app: use: build + env: + - name: NEXT_PUBLIC_SENTRY_DSN + valueFrom: + secretKeyRef: + name: sentry + key: SENTRY_DSN with: buildArgs: - NEXT_PUBLIC_SENTRY_DSN: https://81b6e3d265cf736588f894040d265705@sentry.fabrique.social.gouv.fr/107 - NEXT_PUBLIC_SENTRY_PUBLIC_KEY: 81b6e3d265cf736588f894040d265705 - NEXT_PUBLIC_SENTRY_PROJECT_ID: 107 - NEXT_PUBLIC_SENTRY_BASE_URL: https://sentry.fabrique.social.gouv.fr - NEXT_PUBLIC_SENTRY_ENV: dev - NEXT_PUBLIC_SENTRY_RELEASE: "{{.Values.global.branchSlug32}}" + + NEXT_PUBLIC_CDTN_ENV: development NEXT_PUBLIC_BUCKET_FOLDER: "preview" NEXT_PUBLIC_BUCKET_SITEMAP_FOLDER: "sitemap" NEXT_PUBLIC_BUCKET_URL: https://cdtn-dev-public.s3.gra.io.cloud.ovh.net @@ -17,14 +19,16 @@ jobs: NEXT_PUBLIC_PIWIK_URL: https://matomo.fabrique.social.gouv.fr NEXT_PUBLIC_COMMIT: "{{.Values.global.sha}}" NEXT_PUBLIC_SITE_URL: "https://{{.Values.global.host}}" - NEXT_PUBLIC_SENTRY_ORG: "incubateur" - NEXT_PUBLIC_SENTRY_PROJECT: "code-du-travail-numerique-v2" - NEXT_PUBLIC_SENTRY_URL: "https://sentry.fabrique.social.gouv.fr" + NEXT_PUBLIC_SENTRY_DSN: "$NEXT_PUBLIC_SENTRY_DSN" + SENTRY_RELEASE: "{{.Values.global.branchSlug32}}" + SENTRY_ORG: incubateur + SENTRY_PROJECT: fabnum-code-du-travail-numerique + SENTRY_URL: https://sentry2.fabrique.social.gouv.fr NEXT_PUBLIC_ES_INDEX_PREFIX: "cdtn-preprod" NEXT_PUBLIC_BRANCH_NAME_SLUG: "{{.Values.global.branchSlug32}}" secrets: sentry_auth_token: - secretName: www-secret + secretName: sentry secretKey: SENTRY_AUTH_TOKEN elasticsearch_token_api: secretName: www-secret diff --git a/.kontinuous/values.yaml b/.kontinuous/values.yaml index a21f161e90..25066f906d 100644 --- a/.kontinuous/values.yaml +++ b/.kontinuous/values.yaml @@ -29,6 +29,17 @@ app: name: www-configmap - secretRef: name: www-secret + env: + - name: SENTRY_AUTH_TOKEN + valueFrom: + secretKeyRef: + name: sentry + key: SENTRY_AUTH_TOKEN + - name: NEXT_PUBLIC_SENTRY_DSN + valueFrom: + secretKeyRef: + name: sentry + key: SENTRY_DSN resources: limits: cpu: 400m diff --git a/Dockerfile b/Dockerfile index a423c83d12..63b9734a75 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,33 +29,33 @@ ARG NEXT_PUBLIC_PIWIK_SITE_ID ENV NEXT_PUBLIC_PIWIK_SITE_ID=$NEXT_PUBLIC_PIWIK_SITE_ID ARG NEXT_PUBLIC_PIWIK_URL ENV NEXT_PUBLIC_PIWIK_URL=$NEXT_PUBLIC_PIWIK_URL -ARG NEXT_PUBLIC_SENTRY_DSN -ENV NEXT_PUBLIC_SENTRY_DSN=$NEXT_PUBLIC_SENTRY_DSN -ARG NEXT_PUBLIC_SENTRY_ENV -ENV NEXT_PUBLIC_SENTRY_ENV=$NEXT_PUBLIC_SENTRY_ENV -ARG NEXT_PUBLIC_SENTRY_PUBLIC_KEY -ENV NEXT_PUBLIC_SENTRY_PUBLIC_KEY=$NEXT_PUBLIC_SENTRY_PUBLIC_KEY -ARG NEXT_PUBLIC_SENTRY_PROJECT_ID -ENV NEXT_PUBLIC_SENTRY_PROJECT_ID=$NEXT_PUBLIC_SENTRY_PROJECT_ID -ARG NEXT_PUBLIC_SENTRY_BASE_URL -ENV NEXT_PUBLIC_SENTRY_BASE_URL=$NEXT_PUBLIC_SENTRY_BASE_URL ARG NEXT_PUBLIC_COMMIT ENV NEXT_PUBLIC_COMMIT=$NEXT_PUBLIC_COMMIT ARG NEXT_PUBLIC_SITE_URL ENV NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL -ARG NEXT_PUBLIC_SENTRY_RELEASE -ENV NEXT_PUBLIC_SENTRY_RELEASE=$NEXT_PUBLIC_SENTRY_RELEASE -ARG NEXT_PUBLIC_SENTRY_ORG -ENV NEXT_PUBLIC_SENTRY_ORG=$NEXT_PUBLIC_SENTRY_ORG -ARG NEXT_PUBLIC_SENTRY_PROJECT -ENV NEXT_PUBLIC_SENTRY_PROJECT=$NEXT_PUBLIC_SENTRY_PROJECT -ARG NEXT_PUBLIC_SENTRY_URL -ENV NEXT_PUBLIC_SENTRY_URL=$NEXT_PUBLIC_SENTRY_URL + +ARG NEXT_PUBLIC_SENTRY_ENV +ENV NEXT_PUBLIC_SENTRY_ENV=$NEXT_PUBLIC_SENTRY_ENV +ARG SENTRY_URL +ARG SENTRY_ORG +ARG SENTRY_PROJECT +ARG SENTRY_RELEASE +ARG NEXT_PUBLIC_SENTRY_DSN +ENV SENTRY_URL=$SENTRY_URL +ENV SENTRY_ORG=$SENTRY_ORG +ENV SENTRY_PROJECT=$SENTRY_PROJECT +ENV NEXT_PUBLIC_SENTRY_DSN=$NEXT_PUBLIC_SENTRY_DSN +ENV SENTRY_RELEASE=$SENTRY_RELEASE + ARG NEXT_PUBLIC_ES_INDEX_PREFIX ENV NEXT_PUBLIC_ES_INDEX_PREFIX=$NEXT_PUBLIC_ES_INDEX_PREFIX ARG NEXT_PUBLIC_BRANCH_NAME_SLUG ENV NEXT_PUBLIC_BRANCH_NAME_SLUG=$NEXT_PUBLIC_BRANCH_NAME_SLUG +# Enable source map generation during build +ENV GENERATE_SOURCEMAP=true \ + NODE_ENV=production + # hadolint ignore=SC2046 RUN --mount=type=secret,id=sentry_auth_token \ --mount=type=secret,id=elasticsearch_token_api \ @@ -63,6 +63,7 @@ RUN --mount=type=secret,id=sentry_auth_token \ export SENTRY_AUTH_TOKEN=$(cat /run/secrets/sentry_auth_token) && \ export ELASTICSEARCH_TOKEN_API=$(cat /run/secrets/elasticsearch_token_api) && \ export ELASTICSEARCH_URL=$(cat /run/secrets/elasticsearch_url) && \ + export GENERATE_SOURCEMAP=true && \ yarn build && \ yarn workspaces focus --production --all && \ yarn cache clean @@ -86,6 +87,7 @@ COPY --from=dist --chown=1000:1000 /dep/packages/code-du-travail-frontend/next.c COPY --from=dist --chown=1000:1000 /dep/packages/code-du-travail-frontend/instrumentation.ts /app/packages/code-du-travail-frontend/instrumentation.ts COPY --from=dist --chown=1000:1000 /dep/packages/code-du-travail-frontend/sentry.client.config.ts /app/packages/code-du-travail-frontend/sentry.client.config.ts COPY --from=dist --chown=1000:1000 /dep/packages/code-du-travail-frontend/sentry.server.config.ts /app/packages/code-du-travail-frontend/sentry.server.config.ts +COPY --from=dist --chown=1000:1000 /dep/packages/code-du-travail-frontend/sentry.edge.config.ts /app/packages/code-du-travail-frontend/sentry.edge.config.ts COPY --from=dist --chown=1000:1000 /dep/packages/code-du-travail-frontend/redirects.json /app/packages/code-du-travail-frontend/redirects.json COPY --from=dist --chown=1000:1000 /dep/packages/code-du-travail-frontend/scripts /app/packages/code-du-travail-frontend/scripts COPY --from=dist --chown=1000:1000 /dep/package.json /app/package.json @@ -94,3 +96,14 @@ COPY --from=dist --chown=1000:1000 /dep/node_modules /app/node_modules RUN mkdir -p /app/packages/code-du-travail-frontend/.next/cache/images && chown -R 1000:1000 /app/packages/code-du-travail-frontend/.next CMD [ "yarn", "workspace", "@cdt/frontend", "start"] + +ARG SENTRY_URL +ARG SENTRY_ORG +ARG SENTRY_PROJECT +ARG SENTRY_RELEASE +ARG NEXT_PUBLIC_SENTRY_DSN +ENV SENTRY_URL=$SENTRY_URL +ENV SENTRY_ORG=$SENTRY_ORG +ENV SENTRY_PROJECT=$SENTRY_PROJECT +ENV NEXT_PUBLIC_SENTRY_DSN=$NEXT_PUBLIC_SENTRY_DSN +ENV SENTRY_RELEASE=$SENTRY_RELEASE diff --git a/packages/code-du-travail-frontend/__tests__/__snapshots__/droit-du-travail.test.js.snap b/packages/code-du-travail-frontend/__tests__/__snapshots__/droit-du-travail.test.js.snap index baf0c6d833..1b3a23638f 100644 --- a/packages/code-du-travail-frontend/__tests__/__snapshots__/droit-du-travail.test.js.snap +++ b/packages/code-du-travail-frontend/__tests__/__snapshots__/droit-du-travail.test.js.snap @@ -3,10 +3,10 @@ exports[` should render 1`] = `
should render 1`] = `
diff --git a/packages/code-du-travail-frontend/__tests__/__snapshots__/glossaire-[slug].test.tsx.snap b/packages/code-du-travail-frontend/__tests__/__snapshots__/glossaire-[slug].test.tsx.snap index e36d836ce4..cd14ff908f 100644 --- a/packages/code-du-travail-frontend/__tests__/__snapshots__/glossaire-[slug].test.tsx.snap +++ b/packages/code-du-travail-frontend/__tests__/__snapshots__/glossaire-[slug].test.tsx.snap @@ -3,10 +3,10 @@ exports[` should render 1`] = `
should render 1`] = `
diff --git a/packages/code-du-travail-frontend/__tests__/__snapshots__/glossaire.test.tsx.snap b/packages/code-du-travail-frontend/__tests__/__snapshots__/glossaire.test.tsx.snap index 99c8859f78..7f02249229 100644 --- a/packages/code-du-travail-frontend/__tests__/__snapshots__/glossaire.test.tsx.snap +++ b/packages/code-du-travail-frontend/__tests__/__snapshots__/glossaire.test.tsx.snap @@ -3,10 +3,10 @@ exports[` should render 1`] = `
should render 1`] = `
diff --git a/packages/code-du-travail-frontend/__tests__/__snapshots__/modeles-de-courriers.test.tsx.snap b/packages/code-du-travail-frontend/__tests__/__snapshots__/modeles-de-courriers.test.tsx.snap index 1586e6d16d..5c0abff98d 100644 --- a/packages/code-du-travail-frontend/__tests__/__snapshots__/modeles-de-courriers.test.tsx.snap +++ b/packages/code-du-travail-frontend/__tests__/__snapshots__/modeles-de-courriers.test.tsx.snap @@ -3,10 +3,10 @@ exports[` should render 1`] = `
should render 1`] = `
diff --git a/packages/code-du-travail-frontend/__tests__/__snapshots__/recherche.test.js.snap b/packages/code-du-travail-frontend/__tests__/__snapshots__/recherche.test.js.snap index 2b67754851..b2a19c4e64 100644 --- a/packages/code-du-travail-frontend/__tests__/__snapshots__/recherche.test.js.snap +++ b/packages/code-du-travail-frontend/__tests__/__snapshots__/recherche.test.js.snap @@ -3,10 +3,10 @@ exports[` should render 1`] = `
should render 1`] = `
diff --git a/packages/code-du-travail-frontend/__tests__/__snapshots__/themes.test.tsx.snap b/packages/code-du-travail-frontend/__tests__/__snapshots__/themes.test.tsx.snap index 2a991fc2ad..7be5b99405 100644 --- a/packages/code-du-travail-frontend/__tests__/__snapshots__/themes.test.tsx.snap +++ b/packages/code-du-travail-frontend/__tests__/__snapshots__/themes.test.tsx.snap @@ -3,10 +3,10 @@ exports[` should render 1`] = `
should render 1`] = `
diff --git a/packages/code-du-travail-frontend/app/api/monitoring/envelope/route.ts b/packages/code-du-travail-frontend/app/api/monitoring/envelope/route.ts new file mode 100644 index 0000000000..b3791365ba --- /dev/null +++ b/packages/code-du-travail-frontend/app/api/monitoring/envelope/route.ts @@ -0,0 +1,262 @@ +import { type NextRequest } from "next/server"; + +export const dynamic = "force-dynamic"; + +export async function POST(request: NextRequest) { + const sentryUrl = process.env.SENTRY_URL; + if (!sentryUrl) { + console.error("Sentry URL not configured"); + return new Response("Sentry URL not configured", { status: 500 }); + } + + try { + // Get the raw body + const body = await request.text(); + // console.log("Received envelope body:", body); + // console.log("Envelope body length:", body.length); + // console.log("Envelope newlines count:", (body.match(/\n/g) || []).length); + // Parse envelope format (newline-delimited JSON) + let projectId: string | undefined; + let publicKey: string | undefined; + + // Split into lines + const lines = body.split("\n"); + if (lines.length < 2) { + console.error("Invalid envelope format: missing parts"); + return new Response("Invalid envelope format", { status: 400 }); + } + + const headerRaw = lines[0]; + const items: string[] = []; + + // Process lines after header to build items with proper newlines + let i = 1; + while (i < lines.length) { + const itemHeaderLine = lines[i]; + if (!itemHeaderLine || itemHeaderLine.trim() === "") { + i++; + continue; + } + + try { + // Try to parse item header + const itemHeader = JSON.parse(itemHeaderLine); + if (!itemHeader.type) { + i++; + continue; + } + + // Found an item header, collect payload until next item header or end + const itemPayloadLines: string[] = []; + i++; + while (i < lines.length) { + const payloadLine = lines[i]; + // Stop if we hit an empty line or another item header + if (payloadLine.trim() === "") { + i++; + continue; + } + try { + const parsed = JSON.parse(payloadLine); + if (parsed.type) { + break; + } + } catch (e) { + // Not a header, add to payload + } + itemPayloadLines.push(payloadLine); + i++; + } + + // Add item with its payload, ensuring proper newlines + if (itemPayloadLines.length > 0) { + // For items with payload: header + \n + payload + items.push(itemHeaderLine + "\n" + itemPayloadLines.join("\n")); + } else { + // For header-only items: just the header + items.push(itemHeaderLine); + } + + // Log item structure for debugging + // console.log("Added item:", { + // type: itemHeader.type, + // hasPayload: itemPayloadLines.length > 0, + // payloadLines: itemPayloadLines.length, + // }); + } catch (e) { + // Not a valid item header, skip + i++; + } + } + + // Log parsed items with detailed structure + // items.forEach((item, index) => { + // console.log(`Item ${index} structure:`, { + // content: item, + // length: item.length, + // newlines: (item.match(/\n/g) || []).length, + // endsWithNewline: item.endsWith("\n"), + // }); + // }); + + // Reconstruct envelope with proper format: + // header + \n\n + item1 + \n\n + item2 + \n\n + const envelope = headerRaw + "\n\n" + items.join("\n"); + + // Log detailed envelope structure + // console.log("Envelope structure:", { + // headerLength: headerRaw.length, + // itemsCount: items.length, + // totalLength: envelope.length, + // newlines: (envelope.match(/\n/g) || []).length, + // firstNewlineAt: envelope.indexOf("\n"), + // headerAndFirstItem: envelope.split("\n").slice(0, 3), + // }); + + try { + // Parse header + const header = JSON.parse(headerRaw); + + // Try to get DSN from envelope header + if (header.dsn) { + const dsnUrl = new URL(header.dsn); + projectId = dsnUrl.pathname.split("/")[1]; + publicKey = dsnUrl.username; + // console.log("Parsed DSN from envelope:", { projectId, publicKey }); + } + } catch (e) { + console.error("Failed to parse envelope header:", e); + return new Response("Invalid envelope header", { status: 400 }); + } + + // Fallback to environment DSN if needed + if (!projectId || !publicKey) { + const dsn = process.env.NEXT_PUBLIC_SENTRY_DSN; + if (dsn) { + try { + const dsnUrl = new URL(dsn); + projectId = dsnUrl.pathname.split("/")[1]; + publicKey = dsnUrl.username; + // console.log("Parsed DSN from environment:", { projectId, publicKey }); + } catch (e) { + console.warn("Could not parse environment DSN:", e); + } + } + } + + if (!projectId || !publicKey) { + console.warn("Could not extract project details from DSN"); + return new Response("Could not parse Sentry DSN", { status: 500 }); + } + + // Get origin for CORS headers + const origin = request.headers.get("origin"); + + // Forward the request to Sentry's envelope endpoint + const sentryResponse = await fetch( + `${sentryUrl}/api/${projectId}/envelope/`, + { + method: "POST", + credentials: "omit", // Don't send cookies for client-side error reporting + headers: { + // Forward original headers needed for client error reporting + "Content-Type": "text/plain;charset=UTF-8", + Accept: "*/*", + // Forward original auth header from client request + "X-Sentry-Auth": + request.headers.get("X-Sentry-Auth") || + `Sentry sentry_key=${publicKey},sentry_version=7,sentry_client=sentry.javascript.nextjs/8.0.0`, + }, + // Use reconstructed envelope with proper newlines + body: envelope, + } + ); + + if (sentryResponse.status === 403) { + console.error("Sentry authentication failed:", { + responseStatus: sentryResponse.status, + responseStatusText: sentryResponse.statusText, + sentryError: sentryResponse.headers.get("X-Sentry-Error"), + url: `${sentryUrl}/api/${projectId}/envelope/`, + }); + + // Try to get response body for more error details + try { + const errorBody = await sentryResponse.clone().text(); + console.error("Sentry error response body:", errorBody); + } catch (e) { + console.error("Could not read error response body"); + } + } + + // console.log("Sentry response:", { + // status: sentryResponse.status, + // statusText: sentryResponse.statusText, + // error: sentryResponse.headers.get("X-Sentry-Error"), + // }); + + // Get Sentry response headers we want to forward + const sentryHeaders = [ + "X-Sentry-Error", + "X-Sentry-Rate-Limits", + "Retry-After", + ]; + + // Get the request origin or default to * + const requestOrigin = request.headers.get("origin") || "*"; + + const responseHeaders: Record = { + "Content-Type": "text/plain;charset=UTF-8", + "Access-Control-Allow-Origin": requestOrigin, + "Access-Control-Allow-Credentials": "true", + "Access-Control-Expose-Headers": + "X-Sentry-Error, X-Sentry-Rate-Limits, Retry-After", + // Add Vary header when using dynamic origin + ...(requestOrigin !== "*" ? { Vary: "Origin" } : {}), + }; + + // Forward specific Sentry headers if they exist + for (const header of sentryHeaders) { + const value = sentryResponse.headers.get(header); + if (value) { + responseHeaders[header] = value; + // console.log(`Forwarding header ${header}:`, value); + } + } + + // Return the response from Sentry with forwarded headers + return new Response(await sentryResponse.text(), { + status: sentryResponse.status, + headers: responseHeaders, + }); + } catch (error) { + console.error("Error forwarding to Sentry:", error); + return new Response("Error forwarding to Sentry", { status: 500 }); + } +} + +// Handle OPTIONS requests for CORS +export async function OPTIONS(request: NextRequest) { + // Get the request origin or default to * + const requestOrigin = request.headers.get("origin") || "*"; + + const headers: Record = { + "Access-Control-Allow-Origin": requestOrigin, + "Access-Control-Allow-Methods": "POST, OPTIONS", + "Access-Control-Allow-Credentials": "true", + "Access-Control-Allow-Headers": "Accept, Content-Type, X-Sentry-Auth", + "Access-Control-Expose-Headers": + "X-Sentry-Error, X-Sentry-Rate-Limits, Retry-After", + "Access-Control-Max-Age": "86400", + }; + + // Add Vary header when using dynamic origin + if (requestOrigin !== "*") { + headers["Vary"] = "Origin"; + } + + return new Response(null, { + status: 200, + headers, + }); +} diff --git a/packages/code-du-travail-frontend/app/api/test-sentry-error/route.ts b/packages/code-du-travail-frontend/app/api/test-sentry-error/route.ts new file mode 100644 index 0000000000..46406e81a0 --- /dev/null +++ b/packages/code-du-travail-frontend/app/api/test-sentry-error/route.ts @@ -0,0 +1,39 @@ +import { headers } from "next/headers"; +import { NextResponse } from "next/server"; + +export const dynamic = "force-dynamic"; // Ensure the route is not statically optimized + +export async function GET(request: Request) { + try { + console.log("API route hit:", request.url); + console.log("Headers:", Object.fromEntries(headers())); + + const { searchParams } = new URL(request.url); + const shouldError = searchParams.get("trigger") === "true"; + + console.log("Should trigger error:", shouldError); + + if (shouldError) { + console.log("Throwing test error..."); + throw new Error("Test server-side error for Sentry integration"); + } + + // Return success response with proper headers + return new NextResponse( + JSON.stringify({ + message: "Test endpoint - add ?trigger=true to trigger error", + }), + { + status: 200, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + } + ); + } catch (error) { + console.error("API route error:", error); + + // Re-throw the error to be caught by Sentry + throw error; + } +} diff --git a/packages/code-du-travail-frontend/app/global-error.tsx b/packages/code-du-travail-frontend/app/global-error.tsx index 32739cfb1e..4170ea97ea 100644 --- a/packages/code-du-travail-frontend/app/global-error.tsx +++ b/packages/code-du-travail-frontend/app/global-error.tsx @@ -2,8 +2,8 @@ import { useEffect } from "react"; import { UnexpectedError } from "../src/modules/error/UnexpectedError"; -import * as Sentry from "@sentry/nextjs"; import { Metadata } from "next"; +import { captureError } from "../src/modules/sentry/error"; export const metadata: Metadata = { title: "Erreur", @@ -17,7 +17,11 @@ export default function GlobalError({ }) { useEffect(() => { console.error(error); - Sentry.captureException(error); + captureError(error, { + type: "client", + url: window.location.href, + path: window.location.pathname, + }); }, [error]); return ( diff --git a/packages/code-du-travail-frontend/next.config.mjs b/packages/code-du-travail-frontend/next.config.mjs index 5f92f9995b..bcaa5a2b14 100644 --- a/packages/code-du-travail-frontend/next.config.mjs +++ b/packages/code-du-travail-frontend/next.config.mjs @@ -16,17 +16,29 @@ const sentryConfig = { project: process.env.NEXT_PUBLIC_SENTRY_PROJECT, sentryUrl: process.env.NEXT_PUBLIC_SENTRY_URL, authToken: process.env.SENTRY_AUTH_TOKEN, - release: { - name: process.env.NEXT_PUBLIC_SENTRY_RELEASE, - setCommits: process.env.NEXT_PUBLIC_COMMIT - ? { - repo: "SocialGouv/code-du-travail-numerique", - commit: process.env.NEXT_PUBLIC_COMMIT, - } - : { auto: true }, + // Source maps configuration + sourcemaps: { + assets: ".next/**/*.{js,map}", + ignore: ["node_modules/**/*"], + rewrite: true, + stripPrefix: ["webpack://_N_E/", "webpack://", "app://"], + urlPrefix: "app:///_next", }, - hideSourceMaps: true, - widenClientFileUpload: true, + // Debug and release configuration + silent: false, + debug: true, + release: + process.env.SENTRY_RELEASE || process.env.NEXT_PUBLIC_GITHUB_SHA || "dev", + dist: process.env.NEXT_PUBLIC_GITHUB_SHA || "dev", + setCommits: { + auto: true, + ignoreMissing: true, + }, + deploy: { + env: process.env.NEXT_PUBLIC_EGAPRO_ENV || "development", + dist: process.env.NEXT_PUBLIC_GITHUB_SHA || "dev", + }, + injectBuildInformation: true, }; const nextConfig = { @@ -45,11 +57,21 @@ const nextConfig = { experimental: { instrumentationHook: true, }, - webpack: (config) => { + webpack: (config, { dev, isServer }) => { config.module.rules.push({ test: /\.woff2$/, type: "asset/resource", }); + // Configure source maps for production + if (!isServer && !dev) { + config.devtool = "source-map"; + config.optimization = { + ...config.optimization, + minimize: true, + moduleIds: "deterministic", + chunkIds: "deterministic", + }; + } return config; }, transpilePackages: ["@codegouvfr/react-dsfr"], @@ -101,4 +123,62 @@ const moduleExports = { }, }; -export default withSentryConfig(moduleExports, sentryConfig); +export default withSentryConfig( + moduleExports, + { + // Sentry webpack plugin options + org: process.env.SENTRY_ORG, + project: process.env.SENTRY_PROJECT, + url: process.env.SENTRY_URL, + authToken: process.env.SENTRY_AUTH_TOKEN, + + // Source maps configuration + sourcemaps: { + assets: ".next/**/*.{js,map}", + ignore: ["node_modules/**/*"], + rewrite: true, + stripPrefix: ["webpack://_N_E/", "webpack://", "app://"], + urlPrefix: "app:///_next", + }, + + // Debug and release configuration + silent: false, + debug: true, + release: + process.env.SENTRY_RELEASE || process.env.NEXT_PUBLIC_GITHUB_SHA || "dev", + dist: process.env.NEXT_PUBLIC_GITHUB_SHA || "dev", + setCommits: { + auto: true, + ignoreMissing: true, + }, + deploy: { + env: process.env.NEXT_PUBLIC_SENTRY_ENV || "development", + dist: process.env.NEXT_PUBLIC_GITHUB_SHA || "dev", + }, + injectBuildInformation: true, + }, + { + // Sentry Next.js SDK options + // Note: tunnelRoute option doesn't work with self-hosted instances + // Using custom tunnel implementation instead + tunnelRoute: false, + widenClientFileUpload: true, + hideSourceMaps: false, + disableLogger: true, + + // Enable component names and release injection + includeNames: true, + release: { + inject: true, + name: + process.env.SENTRY_RELEASE || + process.env.NEXT_PUBLIC_GITHUB_SHA || + "dev", + }, + + // Server instrumentation options + autoInstrumentServerFunctions: true, + autoInstrumentMiddleware: true, + automaticVercelMonitors: true, + } +); diff --git a/packages/code-du-travail-frontend/package.json b/packages/code-du-travail-frontend/package.json index 3994ab6c0b..6810ee62ab 100644 --- a/packages/code-du-travail-frontend/package.json +++ b/packages/code-du-travail-frontend/package.json @@ -41,7 +41,7 @@ "@opentelemetry/instrumentation-generic-pool": "^0.37.0", "@opentelemetry/instrumentation-http": "^0.52.0", "@opentelemetry/instrumentation-net": "^0.37.0", - "@sentry/nextjs": "^8.24.0", + "@sentry/nextjs": "8.51.0", "@socialgouv/cdtn-elasticsearch": "^2.44.2", "@socialgouv/cdtn-logger": "^2.0.0", "@socialgouv/cdtn-types": "^2.51.0", diff --git a/packages/code-du-travail-frontend/pages/api/test-sentry-error-legacy.ts b/packages/code-du-travail-frontend/pages/api/test-sentry-error-legacy.ts new file mode 100644 index 0000000000..67f3b4b3d3 --- /dev/null +++ b/packages/code-du-travail-frontend/pages/api/test-sentry-error-legacy.ts @@ -0,0 +1,43 @@ +import { headers } from "next/headers"; +import { NextResponse } from "next/server"; +import { NextApiRequest, NextApiResponse } from "next"; +import { runMiddleware } from "../../src/api"; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + await runMiddleware(req, res); + try { + console.log("API route hit:", req.url); + console.log("Headers:", Object.fromEntries(headers())); + + const { searchParams } = new URL(req.url!); + const shouldError = searchParams.get("trigger") === "true"; + + console.log("Should trigger error:", shouldError); + + if (shouldError) { + console.log("Throwing test error..."); + throw new Error("Test server-side error for Sentry integration"); + } + + // Return success response with proper headers + return new NextResponse( + JSON.stringify({ + message: "Test endpoint - add ?trigger=true to trigger error", + }), + { + status: 200, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + } + ); + } catch (error) { + console.error("API route error:", error); + + // Re-throw the error to be caught by Sentry + throw error; + } +} diff --git a/packages/code-du-travail-frontend/sentry.client.config.ts b/packages/code-du-travail-frontend/sentry.client.config.ts index 7c90c2d0d6..fde68cb325 100644 --- a/packages/code-du-travail-frontend/sentry.client.config.ts +++ b/packages/code-du-travail-frontend/sentry.client.config.ts @@ -1,23 +1,92 @@ -// This file configures the initialization of Sentry on the browser. -// The config you add here will be used whenever a page is visited. +// This file configures the initialization of Sentry on the client. +// The config you add here will be used whenever a users loads a page in their browser. // https://docs.sentry.io/platforms/javascript/guides/nextjs/ import * as Sentry from "@sentry/nextjs"; +import { replayIntegration } from "@sentry/nextjs"; + +const ENVIRONMENT = process.env.NEXT_PUBLIC_SENTRY_ENV || "dev"; +const IS_PRODUCTION = ENVIRONMENT === "production"; + +// Declare Cypress on window for TypeScript +declare global { + interface Window { + Cypress?: unknown; + } +} + +// Disable Sentry during Cypress tests +const isCypressTest = + typeof window !== "undefined" && window.Cypress !== undefined; Sentry.init({ - dsn: - process.env.NEXT_PUBLIC_SENTRY_DSN || process.env.SENTRY_PUBLIC_DSN || "", - environment: - process.env.NEXT_PUBLIC_SENTRY_ENV || process.env.SENTRY_ENV || "dev", - tracesSampleRate: 0.05, - release: process.env.NEXT_PUBLIC_SENTRY_RELEASE || process.env.SENTRY_RELEASE, + // Basic configuration + dsn: isCypressTest ? undefined : process.env.NEXT_PUBLIC_SENTRY_DSN, // Disable Sentry in Cypress by setting DSN to undefined + environment: ENVIRONMENT, + debug: true, // Temporarily enable debug mode to troubleshoot + dist: process.env.NEXT_PUBLIC_GITHUB_SHA || "dev", + + // Enable tunneling to avoid ad-blockers (using custom implementation for self-hosted instance) + tunnel: "/api/monitoring/envelope", + + // Performance monitoring and source maps + enableTracing: true, + attachStacktrace: true, // Attach stack traces to all messages + normalizeDepth: 10, // Increase stack trace depth for better context + tracesSampleRate: IS_PRODUCTION ? 0.1 : 1.0, // Sample 10% of traces in prod, all in dev + maxBreadcrumbs: 100, // Increase from default 100 to capture more context + + // Session replay configuration + replaysSessionSampleRate: IS_PRODUCTION ? 0.1 : 0.5, // Sample 10% of sessions in prod, 50% in dev + replaysOnErrorSampleRate: 1.0, // Always capture sessions with errors + + // Error tracking configuration + sampleRate: 1.0, // Capture all errors + autoSessionTracking: true, // Enable automatic session tracking + sendClientReports: true, // Enable immediate client reports + + beforeSend(event) { + console.log("Sentry beforeSend called with event:", { + eventId: event.event_id, + type: event.type, + exception: event.exception?.values?.[0], + environment: event.environment, + }); + + // Filter out non-error events in production + if (IS_PRODUCTION && !event.exception) { + console.log("Filtering out non-error event in production"); + return null; + } + + // Filter out known unnecessary errors + const ignoreErrors = [ + "ResizeObserver loop limit exceeded", + "Network request failed", + /^Loading chunk .* failed/, + /^Loading CSS chunk .* failed/, + ]; + + if ( + event.exception && + ignoreErrors.some((pattern) => { + if (typeof pattern === "string") { + return event.exception?.values?.[0]?.value?.includes(pattern); + } + return pattern.test(event.exception?.values?.[0]?.value || ""); + }) + ) { + return null; + } + + return event; + }, + integrations: [ - Sentry.replayIntegration({ - maskAllText: false, - blockAllMedia: false, - maskAllInputs: false, + replayIntegration({ + maskAllText: true, + blockAllMedia: true, + useCompression: false, // https://github.com/nuxt-community/sentry-module/issues/562#issuecomment-1516338000 , see also https://github.com/getsentry/sentry-javascript/issues/7302 (but not evocated the problem of selfhost instance on this issue) }), ], - replaysSessionSampleRate: 0, - replaysOnErrorSampleRate: 1.0, }); diff --git a/packages/code-du-travail-frontend/sentry.edge.config.ts b/packages/code-du-travail-frontend/sentry.edge.config.ts new file mode 100644 index 0000000000..1f3ce0da9b --- /dev/null +++ b/packages/code-du-travail-frontend/sentry.edge.config.ts @@ -0,0 +1,52 @@ +// This file configures the initialization of Sentry for edge runtimes +// The config you add here will be used whenever your app runs on the edge + +import * as Sentry from "@sentry/nextjs"; + +const ENVIRONMENT = process.env.NEXT_PUBLIC_SENTRY_ENV || "dev"; +const IS_PRODUCTION = ENVIRONMENT === "production"; + +Sentry.init({ + // Basic configuration + dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, + environment: ENVIRONMENT, + debug: true, // Temporarily enable debug mode to troubleshoot + dist: process.env.NEXT_PUBLIC_GITHUB_SHA || "dev", + + // Performance monitoring and source maps + enableTracing: true, + attachStacktrace: true, // Attach stack traces to all messages + normalizeDepth: 10, // Increase stack trace depth for better context + tracesSampleRate: IS_PRODUCTION ? 0.1 : 1.0, // Sample 10% of traces in prod, all in dev + maxBreadcrumbs: 100, // Increase from default 100 to capture more context + + // Error tracking configuration + sampleRate: 1.0, // Capture all errors + + beforeSend(event) { + // Filter out non-error events in production + if (IS_PRODUCTION && !event.exception) return null; + + // Filter out known unnecessary errors + const ignoreErrors = [ + "ResizeObserver loop limit exceeded", + "Network request failed", + /^Loading chunk .* failed/, + /^Loading CSS chunk .* failed/, + ]; + + if ( + event.exception && + ignoreErrors.some((pattern) => { + if (typeof pattern === "string") { + return event.exception?.values?.[0]?.value?.includes(pattern); + } + return pattern.test(event.exception?.values?.[0]?.value || ""); + }) + ) { + return null; + } + + return event; + }, +}); diff --git a/packages/code-du-travail-frontend/sentry.server.config.ts b/packages/code-du-travail-frontend/sentry.server.config.ts index bc7c40f8c9..54c9155924 100644 --- a/packages/code-du-travail-frontend/sentry.server.config.ts +++ b/packages/code-du-travail-frontend/sentry.server.config.ts @@ -1,19 +1,62 @@ +// This file configures the initialization of Sentry on the server. +// The config you add here will be used whenever the server handles a request. +// https://docs.sentry.io/platforms/javascript/guides/nextjs/ + import * as Sentry from "@sentry/nextjs"; -import { HttpInstrumentation } from "@opentelemetry/instrumentation-http"; -import { NetInstrumentation } from "@opentelemetry/instrumentation-net"; -import { GenericPoolInstrumentation } from "@opentelemetry/instrumentation-generic-pool"; + +const ENVIRONMENT = process.env.NEXT_PUBLIC_SENTRY_ENV || "dev"; +const IS_PRODUCTION = ENVIRONMENT === "production"; + +// Check for Cypress test environment +const isCypressTest = process.env.CYPRESS === "true"; Sentry.init({ - dsn: - process.env.NEXT_PUBLIC_SENTRY_DSN || process.env.SENTRY_PUBLIC_DSN || "", - environment: - process.env.NEXT_PUBLIC_SENTRY_ENV || process.env.SENTRY_ENV || "dev", - tracesSampleRate: 0.2, - release: process.env.NEXT_PUBLIC_SENTRY_RELEASE || process.env.SENTRY_RELEASE, -}); + // Basic configuration + dsn: isCypressTest ? undefined : process.env.NEXT_PUBLIC_SENTRY_DSN, // Disable Sentry in Cypress + environment: ENVIRONMENT, + debug: true, // Temporarily enable debug mode to troubleshoot + dist: process.env.NEXT_PUBLIC_GITHUB_SHA || "dev", + + // Performance monitoring and source maps + enableTracing: true, + attachStacktrace: true, // Attach stack traces to all messages + normalizeDepth: 10, // Increase stack trace depth for better context + tracesSampleRate: IS_PRODUCTION ? 0.1 : 1.0, // Sample 10% of traces in prod, all in dev + maxBreadcrumbs: 100, // Increase from default 100 to capture more context -Sentry.addOpenTelemetryInstrumentation( - new GenericPoolInstrumentation(), - new HttpInstrumentation(), - new NetInstrumentation() -); + // Error tracking configuration + sampleRate: 1.0, // Capture all errors + autoSessionTracking: true, // Enable automatic session tracking + sendClientReports: true, // Enable immediate client reports + + beforeSend(event) { + // Filter out non-error events in production + if (IS_PRODUCTION && !event.exception) return null; + + // Filter out known unnecessary errors + const ignoreErrors = [ + "ResizeObserver loop limit exceeded", + "Network request failed", + /^Loading chunk .* failed/, + /^Loading CSS chunk .* failed/, + /^ECONNREFUSED/, + /^ECONNRESET/, + /^ETIMEDOUT/, + "Database connection timeout", + ]; + + if ( + event.exception && + ignoreErrors.some((pattern) => { + if (typeof pattern === "string") { + return event.exception?.values?.[0]?.value?.includes(pattern); + } + return pattern.test(event.exception?.values?.[0]?.value || ""); + }) + ) { + return null; + } + + return event; + }, +}); diff --git a/packages/code-du-travail-frontend/src/config.ts b/packages/code-du-travail-frontend/src/config.ts index 91252c5c60..e21331d110 100644 --- a/packages/code-du-travail-frontend/src/config.ts +++ b/packages/code-du-travail-frontend/src/config.ts @@ -1,5 +1,6 @@ const { version } = require("../package.json"); +type EnvironmentType = "development" | "preprod" | "production"; export const BUCKET_URL = process.env.NEXT_PUBLIC_BUCKET_URL ?? "https://cdtn-dev-public.s3.gra.io.cloud.ovh.net"; @@ -17,6 +18,8 @@ export const IS_PREPROD = process.env.NEXT_PUBLIC_IS_PREPRODUCTION_DEPLOYMENT ?? false; export const IS_PROD = process.env.NEXT_PUBLIC_IS_PRODUCTION_DEPLOYMENT ?? false; +export const ENV = (process.env.NEXT_PUBLIC_CDTN_ENV ?? + "development") as EnvironmentType; export const ENTERPRISE_API_URL = "https://recherche-entreprises.api.gouv.fr"; export const API_GEO_URL = "https://geo.api.gouv.fr"; export const REVALIDATE_TIME = 1800; // 30 minutes diff --git a/packages/code-du-travail-frontend/src/layout/Layout.tsx b/packages/code-du-travail-frontend/src/layout/Layout.tsx index 68653b0b4c..ff5b3b8452 100644 --- a/packages/code-du-travail-frontend/src/layout/Layout.tsx +++ b/packages/code-du-travail-frontend/src/layout/Layout.tsx @@ -5,6 +5,7 @@ import styled, { css } from "styled-components"; import { ErrorBoundary } from "../common/ErrorBoundary"; import Footer from "./Footer"; import { Header, HEADER_HEIGHT, MOBILE_HEADER_HEIGHT } from "./Header"; +import { SentryTest } from "../lib/SentryTest"; const Layout = ({ children, currentPage = "" }) => { return ( @@ -17,6 +18,7 @@ const Layout = ({ children, currentPage = "" }) => {