Skip to content

feat: add sentry#2516

Open
finnar-bin wants to merge 14 commits into
stagefrom
feat/2480-sentry-config
Open

feat: add sentry#2516
finnar-bin wants to merge 14 commits into
stagefrom
feat/2480-sentry-config

Conversation

@finnar-bin

@finnar-bin finnar-bin commented Nov 14, 2025

Copy link
Copy Markdown
Contributor

Description

Add Sentry error tracking to the website/accounts app.
Resolves #2480

Changes

Sentry initialization

  • Client, server, and edge Sentry configs with prod-only init guard
  • Custom beforeBreadcrumb on the client to enrich UI click/input events with element type, data-cy, ID, name, and button text
  • User context set via Sentry.setUser on login; cleared with Sentry.setUser(null) on logout
  • _error.js updated to capture errors via captureUnderscoreErrorException (fire-and-forget, no await)
  • SENTRY_AUTH_TOKEN wired into the CI build step for source map uploads

Bug fixes (from code review)

  • setuserInfo(null) now correctly calls Sentry.setUser(null) instead of sending { username: "undefined undefined" } to Sentry
  • Sentry.setUser gated behind NODE_ENV === 'production' to match the Sentry.init guard and avoid no-op churn on every setuserInfo call in dev
  • onRouterTransitionStart initialized as undefined instead of {} to avoid TypeError when Next.js calls it in non-production
  • onRequestError gated behind NODE_ENV === 'production' to match the init guard in server/edge configs
  • beforeBreadcrumb guarded with instanceof Element before reading tagNameEventTarget includes Text, Document, and Window which have no tagName; the throw would silently swallow the entire breadcrumb pipeline
  • Removed dead beforeBreadcrumb from server and edge configs (DOM events never fire in Node.js/edge runtimes)
  • next.config.js: extracted nextConfig variable so module.exports is assigned once, eliminating the fragile double-assignment left by the Sentry wizard

Docker / CI fixes

  • Sentry config files renamed from .ts to .js to avoid requiring typescript as a production dependency during the Docker build
  • Dockerfile base image bumped from node:18.18.2-alpine3.17 to node:18.20.4-alpine3.20@sentry/nextjs pulls in @opentelemetry/context-async-hooks@2.2.0 which requires node ^18.19.0 || >=20.6.0
  • SENTRY_AUTH_TOKEN passed as a Docker build arg in cloudbuild.yaml and cloudbuildProduction.yaml so source maps are uploaded during production builds

Type of change

  • New feature (non-breaking change which adds functionality)

⚠️ Verify before merging — client-side Sentry instrumentation

instrumentation-client.js is an officially supported convention in Next.js 15.3+, but this project is on Next.js ^14.1.4. @sentry/nextjs@10's webpack plugin does inject this file into the client bundle for older Next.js versions, but this is undocumented behavior — if it doesn't work silently, client-side errors will go uncaptured with no build warning.

Please verify on staging before merging:

  1. Run a production build and open the browser console
  2. Check window.__SENTRY__ — should return the Sentry client object, not undefined
  3. Run Sentry.captureException(new Error('sentry test')) and confirm the event appears in the Sentry UI under website-accounts

If window.__SENTRY__ is undefined, fall back to the sentry.client.config.js convention which @sentry/nextjs officially supports for Next.js 14.

⚠️ GCP setup required before source maps will upload (needs infra access)

cloudbuild.yaml and cloudbuildProduction.yaml now pass SENTRY_AUTH_TOKEN as a Docker build arg via Cloud Build's Secret Manager integration. This will not work until the following is set up by someone with GCP access:

  1. Create the secret in Secret Manager:

    echo -n "YOUR_SENTRY_AUTH_TOKEN" | gcloud secrets create sentry-auth-token \
      --data-file=- \
      --project=YOUR_PROJECT_ID
  2. Grant the Cloud Build service account access to the secret:

    gcloud secrets add-iam-policy-binding sentry-auth-token \
      --member="serviceAccount:YOUR_PROJECT_NUMBER@cloudbuild.gserviceaccount.com" \
      --role="roles/secretmanager.secretAccessor" \
      --project=YOUR_PROJECT_ID

Until this is done, production builds will fail at the Build step. The SENTRY_AUTH_TOKEN value is the same token already stored in GitHub Actions secrets.

Note

Requires SENTRY_AUTH_TOKEN to be added to repository secrets (already added for CI).

Comment thread src/instrumentation-client.ts Outdated
Comment thread sentry.server.config.js
Comment thread sentry.server.config.js
Comment thread sentry.edge.config.ts Outdated
Comment thread sentry.server.config.js
Comment thread next.config.js
Comment thread src/instrumentation-client.js
Comment thread src/store/index.js
@github-actions

Copy link
Copy Markdown

Review Summary

Nice to see Sentry getting wired up. The wizard-generated scaffold is solid, but there are a handful of issues worth addressing before merge — flagged inline. Grouping them here by priority:

Higher priority

  • Possible broken client-side init. src/instrumentation-client.ts is the Next 15.3+ convention; this project is on Next 14.1.4. Worth confirming that this file is actually being loaded — otherwise client errors silently won't reach Sentry. The Next-14 equivalent is typically sentry.client.config.ts.
  • onRouterTransitionStart = {} placeholder in non-prod is a latent TypeError if Next ever calls it. Make it a no-op function.
  • tracesSampleRate: 1 will sample 100% of transactions — likely to blow through quota fast for a marketing site + dashboard. Start much lower (e.g. 0.1).
  • PII surface is broad. sendDefaultPii: true + breadcrumb capture of button text / input names + setUser({ email, username, id }) together send a lot of identifying data. Add a beforeSend scrubber, truncate textContent, and reconsider sendDefaultPii.
  • No env distinction between prod and stage. Both run with NODE_ENV=production (the project uses a separate PRODUCTION env var to tell them apart per CLAUDE.md). Without an environment field, stage and prod errors will be indistinguishable in Sentry, and stage will also get the full PII + sampling treatment.

Medium priority

  • Three near-identical Sentry config files. The 60+ line beforeBreadcrumb block is duplicated across edge / server / client, but it only does meaningful work in the browser (DOM events). Extract shared options into one helper to keep tuning in a single place.
  • Hardcoded DSN in three files. Move to NEXT_PUBLIC_SENTRY_DSN.
  • Sentry.setUser lives inside the generic setuserInfo setter. Easy to call inadvertently. Also ${firstName} ${lastName} produces "undefined undefined" when those are missing.

Low priority / nits

  • automaticVercelMonitors: true is a no-op on GCP Cloud Run (this project's deploy target). Remove or set false.
  • widenClientFileUpload: true is intentional? It increases build time noticeably.
  • Redundant early return breadcrumb inside the if (messages.length > 1) branch (both paths return the same value).

Looks good

  • CI wiring of SENTRY_AUTH_TOKEN via secrets, .env.sentry-build-plugin added to .gitignore, .env.example updated, and captureUnderscoreErrorException correctly added to _error.js.

- Guard Sentry.setUser in setuserInfo: call setUser(null) on logout/null
  data, fix "undefined undefined" username via nullish coalescing
- Initialize onRouterTransitionStart as undefined instead of {} to avoid
  TypeError when Next.js calls it as a function in non-production
- Gate onRequestError behind NODE_ENV production check to match the
  init guard in server/edge configs
- Remove dead beforeBreadcrumb from server and edge configs — DOM events
  (ui.click/ui.input) never fire in Node.js or V8 edge runtimes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread src/instrumentation-client.js
Comment thread src/instrumentation-client.js
Comment thread src/instrumentation-client.js
Comment thread src/instrumentation-client.js
Comment thread src/pages/_error.js Outdated
Comment thread src/instrumentation.js
Comment thread .github/workflows/ci.yml
- next.config.js: extract nextConfig variable so module.exports is
  assigned once; eliminates fragile double-assignment left by Sentry wizard
- beforeBreadcrumb: guard with instanceof Element before reading tagName;
  EventTarget includes Text nodes, Document, and Window — tagName is
  undefined on these and .toLowerCase() throws, swallowing the breadcrumb
- Sentry.setUser: gate behind NODE_ENV === 'production' to match the
  Sentry.init guard and avoid no-op churn on every setuserInfo call in dev
- _error.js: drop await on captureUnderscoreErrorException; fire-and-forget
  so a slow network doesn't block the error page from rendering
- Dockerfile: bump base image from 18.18.2-alpine3.17 to 18.20.4-alpine3.20
  to satisfy @opentelemetry/context-async-hooks@2.2.0 (requires >=18.19.0)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread sentry.server.config.js
Comment thread sentry.server.config.js
Comment thread sentry.server.config.js
Comment thread src/store/index.js
Comment thread Dockerfile
Comment thread src/instrumentation-client.js
Comment thread src/instrumentation-client.js
Comment thread src/pages/_error.js
Comment thread next.config.js
@github-actions

Copy link
Copy Markdown

Review summary

Solid Sentry integration overall — the structure follows the SDK's expected layout and the bug-fix list in the description shows the obvious foot-guns were already caught. A few things worth resolving before merging, plus some smaller cleanups left inline.

Blocking-ish (worth fixing before this lands)

  1. Source map upload won't run in production builds. SENTRY_AUTH_TOKEN is wired into .github/workflows/ci.yml, but production builds happen via cloudbuild.yaml / cloudbuildProduction.yamldocker buildRUN npm run build inside the container. The token never reaches that build, so uploaded source maps will be missing/skipped and Sentry stack traces will be minified. See inline note on Dockerfile:1.

  2. tracesSampleRate: 1 (100%) in all three configs. This will be expensive and noisy on a public site. Sentry's own default for prod is 0.1. Worth lowering, or setting to 0 if you don't need performance traces yet.

  3. sendDefaultPii: true + Sentry.setUser({ email, … }). Combined, this captures IPs, cookies, request headers, and user email/name on every event. For a site with EU/CA traffic this likely needs a privacy-policy update and/or consent gating. If you only need user attribution on errors, dropping sendDefaultPii to false still gives you that via setUser.

Worth doing

  1. DSN hardcoded in 3 files. Move to NEXT_PUBLIC_SENTRY_DSN / SENTRY_DSN env vars so stage and prod can point at separate projects without editing source.

  2. setuserInfo({}) would call Sentry.setUser({ email: undefined, id: undefined, … }) rather than clearing the user, because if (data) is truthy for {}. Tighten to if (data?.ZUID). (src/store/index.js)

  3. instrumentation-client.js is officially a Next 15.3+ convention — repo is on next@^14.1.4. @sentry/nextjs@10's webpack plugin does still inject it for older Next, but worth confirming with a real prod build that client-side Sentry actually initializes (e.g. throw a test error from the browser).

  4. PR description nit: says Dockerfile bumped to node:20-alpine, actual change is node:18.20.4-alpine3.20. The new pin does satisfy @opentelemetry/context-async-hooks's engines, just update the description.

Smaller / optional

  • onRouterTransitionStart is App Router only — dead code in this Pages Router project.
  • target.textContent in beforeBreadcrumb is unbounded; consider .slice(0, 200).
  • automaticVercelMonitors: true does nothing off Vercel; safe to drop.
  • Slack + Sentry + console error pipelines now all fire for the same prod error — consider retiring or narrowing the Slack one once Sentry alerting is set up.

Nice work on _error.js, the beforeBreadcrumb enrichment, and catching the original "undefined undefined" username bug in code review.

Without the token, withSentryConfig silently skips source map uploads
during npm run build, leaving Sentry with minified stack traces in prod.

- Dockerfile: accept SENTRY_AUTH_TOKEN as a build arg and set it as an
  env var so the Sentry webpack plugin can read it during the build
- cloudbuild.yaml / cloudbuildProduction.yaml: pull the token from GCP
  Secret Manager and forward it as --build-arg to docker build

Requires sentry-auth-token to be created in GCP Secret Manager and the
Cloud Build service account granted roles/secretmanager.secretAccessor.
See PR description for setup steps.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread src/instrumentation.js
Comment thread src/instrumentation-client.js
Comment thread next.config.js
Comment thread sentry.server.config.js
Comment thread src/instrumentation-client.js
Comment thread sentry.server.config.js
Comment thread src/instrumentation-client.js
Comment thread src/instrumentation-client.js
Comment thread src/store/index.js
Comment thread src/pages/_error.js
@github-actions

Copy link
Copy Markdown

Review summary

Solid first pass at Sentry integration — the wizard-generated boilerplate has been cleaned up and the bugs called out in the PR description (the setuserInfo(null) username bug, the onRouterTransitionStart = {} TypeError, the beforeBreadcrumb instanceof Element guard) are all real wins over what the wizard ships by default. Inline comments cover the specifics; the highlights below are the ones I think matter most before merge.

Worth addressing before merge

  • onRequestError on Next 14 is dead code — that hook is a Next 15+ addition. Server-side request errors won't be auto-captured on this stack, which is probably contrary to the team's expectations after seeing this PR land. Either remove or document the limitation.
  • tracesSampleRate: 1 (100%) in all three configs — this is a quota/perf footgun. 10% is plenty for routine perf monitoring.
  • No environment tag + hardcoded DSN — without environment: 'production' | 'staging' | ..., the staging-verification step in the PR description will produce events indistinguishable from real prod errors in the Sentry UI.
  • PII risk via target.textContent button-text capture combined with sendDefaultPii: true — buttons in account UIs frequently embed user-provided strings (emails, names) in their labels. Worth at least truncating, and ideally preferring aria-label.

Smaller cleanups

  • automaticVercelMonitors: true is dead config (this app deploys to GCP Cloud Run, not Vercel).
  • captureRouterTransitionStart is App Router-only; this is a Pages Router project, so the onRouterTransitionStart export is a no-op.
  • normalizeDepth: 10 is much higher than the Sentry default (3); raises payload size and PII-leak surface area in nested objects.
  • The two return breadcrumb branches in beforeBreadcrumb collapse — see inline suggestion.
  • Sentry.setUser inside the NODE_ENV === 'production' guard is defensive but redundant (it's a no-op without init), and creates a second flag to flip if you ever want to enable staging.

Things I'd flag but aren't this PR's problem

  • The hardcoded Slack webhook in src/pages/_error.js predates this PR but is now sitting next to a Sentry capture call — worth moving to an env var the next time _error.js is touched.

Things I checked and they look good

  • instanceof Element guard in beforeBreadcrumb correctly handles the Text / Document / Window cases the PR description mentions.
  • Renaming .ts.js for the Sentry config files is the right call to avoid pulling typescript into the prod Docker image.
  • Cloud Build wiring (availableSecrets + secretEnv + --build-arg) is standard and correct, assuming the secret + IAM binding from the PR description get set up.
  • Node base image bump from 18.18.218.20.4 is necessary for @opentelemetry/context-async-hooks@2.2.0.
  • next.config.js module.exports is now single-assignment — good cleanup over the wizard default.

Comment thread Dockerfile
Comment on lines +17 to +18
ARG SENTRY_AUTH_TOKEN
ENV SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Secret persists in the runtime image.

ENV SENTRY_AUTH_TOKEN=… writes the token into the image's environment layer, so it remains:

  1. Visible via docker inspect / docker history on anyone who can pull the image
  2. Set as process.env.SENTRY_AUTH_TOKEN inside the running container at npm start

The token only needs to exist for the npm run build step. Either inline it on the RUN line, or (better) use a BuildKit secret mount so it never lands in any layer:

Suggested change
ARG SENTRY_AUTH_TOKEN
ENV SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN
ARG SENTRY_AUTH_TOKEN
RUN --mount=type=secret,id=sentry_auth_token,env=SENTRY_AUTH_TOKEN npm run build

(cloudbuild.yaml would then mount the secret via --secret id=sentry_auth_token,env=SENTRY_AUTH_TOKEN instead of --build-arg.)

Lower-effort alternative if you'd rather not touch the cloudbuild files:

ARG SENTRY_AUTH_TOKEN
RUN SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN npm run build

This keeps the token scoped to that single RUN, so it isn't baked into the image config.

dsn: 'https://4d93b3a97e10da5bc8817c46c1c79e9b@o162121.ingest.us.sentry.io/4510349072728064',

// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
tracesSampleRate: 1,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tracesSampleRate: 1 sends a performance transaction for every page load / route transition on the client. The same value is set in sentry.server.config.js and sentry.edge.config.js, so server-side requests are also fully sampled.

For a public marketing site this can burn through the Sentry transaction quota fast and adds visible network overhead for users. Sentry's own guidance (docs) is to start much lower in production (e.g. 0.1 or even 0.01) and tune up if needed.

Consider either lowering the constant or reading it from an env var so it can be tuned per-environment without a redeploy:

tracesSampleRate: Number(process.env.NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE ?? 0.1),

(Same change would apply to the server + edge configs.)

Comment on lines +52 to +53
if (elementType === 'button' && target.textContent) {
messages.push(`Button Text: "${target.textContent.trim()}"`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two concerns with using target.textContent here:

  1. PII leakagesendDefaultPii: true is already on, but button text that contains user-specific info (e.g. Delete invoice INV-2024-…, Sign out as alice@example.com) will land in breadcrumbs and from there into the events shown to whoever is on-call. Worth deciding whether that's intended.

  2. Unbounded lengthtextContent returns the concatenated text of all descendants. A button containing an icon + label is fine, but a click captured on an outer wrapper (or a button whose Sentry-resolved target is a parent due to event bubbling) can produce a multi-KB string. Sentry's payload limits will eventually truncate, but it's still wasted breadcrumb space.

A small cap is usually enough:

if (elementType === 'button' && target.textContent) {
  messages.push(`Button Text: "${target.textContent.trim().slice(0, 100)}"`);
}

Comment thread sentry.edge.config.js
Comment on lines +1 to +22
// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on).
// The config you add here will be used whenever one of the edge features is loaded.
// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/

import * as Sentry from '@sentry/nextjs';

if (process.env.NODE_ENV === 'production') {
Sentry.init({
dsn: 'https://4d93b3a97e10da5bc8817c46c1c79e9b@o162121.ingest.us.sentry.io/4510349072728064',

// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
tracesSampleRate: 1,

// Enable sending user PII (Personally Identifiable Information)
// https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/options/#sendDefaultPii
sendDefaultPii: true,

maxBreadcrumbs: 50,
normalizeDepth: 10,
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sentry.edge.config.js and sentry.server.config.js are byte-for-byte identical except for the leading comment. Since they'll have to be kept in sync every time the init options change (DSN rotation, sampling tweaks, integrations), consider extracting the shared options into a small helper and importing them in both files — or, if the Sentry SDK allows, re-exporting one from the other:

// sentry.edge.config.js
import './sentry.server.config';

Not a blocker — just flagging that the duplication will quietly drift.

Comment thread src/pages/_error.js
Error.getInitialProps = ({ res, err }) => {
const statusCode = res ? res.statusCode : err ? err.statusCode : 404;
Error.getInitialProps = (contextData) => {
Sentry.captureUnderscoreErrorException(contextData);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: Sentry.captureUnderscoreErrorException runs unconditionally, but Sentry.init is gated on NODE_ENV === 'production' in instrumentation-client.js. The SDK no-ops when uninitialized so this is harmless, but it's inconsistent with the gating pattern used everywhere else in this PR (setuserInfo, onRequestError, onRouterTransitionStart). Either gate it the same way or drop the gate from the other call sites — pick one convention so future readers don't have to wonder which is right.

@github-actions

github-actions Bot commented Jun 7, 2026

Copy link
Copy Markdown

Review summary

Overall the Sentry wiring is clean and the bug fixes called out in the PR description (the setUser(null) fix, the instanceof Element guard, the onRouterTransitionStart = undefined default, and the nextConfig extraction) all look correct. Init is consistently gated on NODE_ENV === 'production', which matches how GTM_ID and the Slack webhook are already handled in this repo.

A few things worth addressing before merge — inline comments have details:

Security

  • Dockerfile: ENV SENTRY_AUTH_TOKEN=… bakes the token into the final image's environment layer. It only needs to exist for the build step. Suggested fix uses a BuildKit secret mount (or, lower-effort, scopes it to a single RUN line).

Tuning

  • tracesSampleRate: 1 in all three configs means 100% transaction sampling on both client and server. For a public marketing site this will eat through the Sentry quota quickly and adds visible network overhead for users. Recommend lowering and ideally reading from an env var so it can be tuned without a redeploy.

Quality / consistency

  • beforeBreadcrumb: button textContent is read without a length cap and may pull in user-identifying strings (PII is already opted-in via sendDefaultPii: true, but worth deciding intentionally). A .slice(0, 100) is a cheap guard.
  • sentry.edge.config.js and sentry.server.config.js are identical — they'll drift the moment one is updated and the other isn't.
  • _error.js calls Sentry.captureUnderscoreErrorException unconditionally, while every other Sentry call in this PR is gated on NODE_ENV === 'production'. SDK no-ops when uninitialized so it's harmless, but inconsistent.

Re: the two verification gates already called out in the PR description — both are well-flagged. The instrumentation-client.js-on-Next-14 concern is real; the staging smoke test you described (window.__SENTRY__ + a manual captureException) is exactly the right check before merging.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🚀 [Feature]: Setup Sentry

1 participant