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`] = `
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`] = `
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`] = `
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`] = `
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`] = `
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`] = `
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 = "" }) => {
+
);
};
diff --git a/packages/code-du-travail-frontend/src/lib/SentryTest.tsx b/packages/code-du-travail-frontend/src/lib/SentryTest.tsx
new file mode 100644
index 0000000000..754e30253d
--- /dev/null
+++ b/packages/code-du-travail-frontend/src/lib/SentryTest.tsx
@@ -0,0 +1,84 @@
+import { useCallback, useEffect, useState } from "react";
+import { Button, Container } from "@socialgouv/cdtn-ui";
+
+export const SentryTest = () => {
+ const [isVisible, setIsVisible] = useState(false);
+ const [typedText, setTypedText] = useState("");
+
+ useEffect(() => {
+ const handleKeyDown = (event: KeyboardEvent) => {
+ const newText = typedText + event.key.toLowerCase();
+ setTypedText(newText.slice(-6)); // Keep only last 6 characters
+
+ if (newText.endsWith("sentry")) {
+ setIsVisible(true);
+ console.log("Sentry test activated!");
+ }
+ };
+
+ window.addEventListener("keydown", handleKeyDown);
+ return () => window.removeEventListener("keydown", handleKeyDown);
+ }, [typedText]);
+
+ const triggerServerError = useCallback(async () => {
+ try {
+ console.log("Making request to test endpoint...");
+ // Make a request to our test endpoint that will throw a server error
+ const response = await fetch(
+ "/api/test-sentry-error-legacy?trigger=true"
+ );
+ console.log("Response status:", response.status);
+
+ if (!response.ok) {
+ const errorText = await response
+ .text()
+ .catch(() => "No error text available");
+ console.error("Server error details:", {
+ status: response.status,
+ statusText: response.statusText,
+ errorText,
+ });
+ throw new Error(`Server error: ${response.status} - ${errorText}`);
+ }
+ } catch (error) {
+ console.error("Server-side error test:", error);
+ throw error;
+ }
+ }, []);
+
+ const triggerError = useCallback(() => {
+ // Log configuration and start of error test
+ console.log("Starting Sentry test with config:", {
+ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
+ environment: process.env.NEXT_PUBLIC_EGAPRO_ENV,
+ });
+
+ console.log(
+ "Triggering test error - this should be caught by the error boundary..."
+ );
+
+ // Create and throw an error that will be caught by the error boundary
+ try {
+ // Create an error with a stack trace by actually throwing it
+ throw new Error("Test error for Sentry integration");
+ } catch (e) {
+ const error = e as Error;
+ error.name = "SentryTestError";
+ error.cause = "Manual test trigger";
+ throw error;
+ }
+ }, []);
+
+ if (!isVisible) return null;
+
+ return (
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/code-du-travail-frontend/src/modules/config/DefaultLayout.tsx b/packages/code-du-travail-frontend/src/modules/config/DefaultLayout.tsx
index 3c96d98aa9..fc37084b6b 100644
--- a/packages/code-du-travail-frontend/src/modules/config/DefaultLayout.tsx
+++ b/packages/code-du-travail-frontend/src/modules/config/DefaultLayout.tsx
@@ -5,6 +5,8 @@ import Link from "../common/Link";
import { MatomoAnalytics } from "./MatomoAnalytics";
import { DefaultColorScheme } from "@codegouvfr/react-dsfr/next-appdir";
import { StartDsfrLight } from "./StartDsfrLight";
+import { ENV } from "../../config";
+import { SentryTest } from "../sentry";
type Props = {
children: React.ReactNode;
@@ -42,6 +44,7 @@ export default function DefaultLayout({
{children}
+ {ENV === "development" && }