Skip to content

Server-side Sentry events carry no release — COMMIT_SHA is dropped at the end of the Docker build stage #15

Closed

Description

@arpitdalal

Server-side Sentry events carry no release at all. The client bundle gets one injected at build time; the server has no equivalent, and COMMIT_SHA is not present in the runtime container. Since ~99% of this project's error volume is server-side, almost none of it is attributable to a commit.

Consequences:

  • Sentry's Resolve → Resolved In dropdown offers a stale release (8a7cb2c7f28a, deployed 03:25Z) instead of the current one, because the events it would be resolving have no release to reason about.
  • Triaging "did this start after deploy X?" is manual — the Sentry UI cannot answer it.
  • Regressions cannot be correlated to a commit via Sentry's suspect-commit/commit-context features.

The build-time release machinery itself is working correctly and is not part of this issue. Do not go looking for a bug in vite.config.ts — there isn't one.

The client half works, and here is the proof

The deployed client bundle contains the injected release global. From https://arpitdalal.dev/assets/index-CuGSVhxJ.js:

n.SENTRY_RELEASE={id:"4b7771cbfcdc6db7909936f67d81133b0f00defc"};
n._sentryDebugIds[t]="1e87b83c-659c-447e-a627-51411afb9c77";

_sentryDebugIds is only emitted when the plugin uploads source maps, which only happens after a release is created. So COMMIT_SHA reaches the build, the auth token is mounted, the plugin runs, source maps upload, and the release exists in Sentry.

Confirmed from the deployment side too:

flyctl image show --app arpitdalal-dev-256c
  → Labels: {"GH_EVENT_NAME":"push","GH_SHA":"4b7771cbfcdc6db7909936f67d81133b0f00defc", ...}

flyctl releases --app arpitdalal-dev-256c
  → v86, created 2026-09-27T15:19:06Z, status complete

You can reproduce the first check at any time:

curl -s https://arpitdalal.dev/ -o /tmp/prod.html
grep -oE '/assets/index-[A-Za-z0-9_-]+\.js' /tmp/prod.html | sort -u
curl -s "https://arpitdalal.dev/assets/<that-file>" | grep -o 'SENTRY_RELEASE={id:"[^"]*"}'

The server half has no release

server/utils/monitoring.ts:5:

Sentry.init({
	dsn: process.env.SENTRY_DSN,
	environment: process.env.NODE_ENV,
	tracesSampleRate: process.env.NODE_ENV === 'production' ? 1 : 0,
	denyUrls: [...],
	integrations: [Sentry.httpIntegration(), nodeProfilingIntegration()],
	tracesSampler(samplingContext) { ... },
})

No release. The SDK reads SENTRY_RELEASE from the global scope, but there is no global scope in Node, and the Vite plugin's injection is a client-build concern — the SSR bundle gets no equivalent. Nothing else supplies it.

Root cause: ARG scope in the Dockerfile

other/Dockerfile:29-32, the build stage:

FROM base as build

ARG COMMIT_SHA
ENV COMMIT_SHA=$COMMIT_SHA

and other/Dockerfile:46-51, the final stage that actually runs in production:

FROM base

ENV FLY="true"
ENV INTERNAL_PORT="8080"
ENV PORT="8081"
ENV NODE_ENV="production"

COMMIT_SHA stops existing at the end of the build stage. The image that runs in production has never had it. The value reaches vite.config.ts:89-91 — which is why the client is fine — and then is thrown away:

release: {
	name: process.env.COMMIT_SHA,
	setCommits: { auto: true },
},

So even the obvious one-line fix does not compile today: release: process.env.COMMIT_SHA in server/utils/monitoring.ts would evaluate to undefined at runtime. Both halves are needed.

How much is actually unattributed

app/utils/monitoring.client.tsx:47 sets replaysOnErrorSampleRate: 1.0, so every browser session containing an error should burn a replay. The Aug 30 – Sep 29 2026 billing period consumed 9 / 50 replays against 4,153 / 5,000 error events. Client-side errors therefore happened in single-digit sessions, and roughly 4,150 of 4,153 events are server-side — all of them currently released as nothing.

That is also the set of events the Sentry issue list is dominated by, which is why the Resolve dropdown looked wrong.

Solution

1. Carry COMMIT_SHA into the runtime image

In other/Dockerfile, add to the final stage (after the FROM base at line 46, alongside the other ENVs):

ARG COMMIT_SHA
ENV COMMIT_SHA=$COMMIT_SHA

An ARG is required to re-declare the build argument in a new stage — it does not inherit across FROM. Without the ARG, ENV COMMIT_SHA=$COMMIT_SHA resolves to empty.

Note the value flows in from .github/workflows/deploy.yml, which already passes it correctly:

flyctl deploy --remote-only --build-arg COMMIT_SHA=${{ github.sha }} \
  --build-secret SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}

No workflow change is needed.

2. Set release explicitly in the server SDK

server/utils/monitoring.ts:6:

Sentry.init({
	dsn: process.env.SENTRY_DSN,
	environment: process.env.NODE_ENV,
	release: process.env.COMMIT_SHA,
	...
})

Being explicit is better than relying on the global here even after the env var exists: it keeps the association working if the bundler integration is ever removed, and it makes the dependency visible to anyone reading the file.

3. Leave the client as-is

app/utils/monitoring.client.tsx needs no change. The injected SENTRY_RELEASE global already does the right thing, and adding release there would mean threading COMMIT_SHA through getEnv() in app/utils/env.server.ts and envSchema in app/utils/schemas.ts — more surface for no gain. If you would rather have it explicit and symmetric, that is a reasonable alternative, but it is not required and it is not cheaper.

Acceptance criteria

  • A server-side error surfaces in Sentry tagged with the release of the commit that is deployed. Verify by triggering one and checking the release shown on the issue.

  • A direct check that the variable survives into the runtime image:

    flyctl ssh -C 'printenv COMMIT_SHA' --app arpitdalal-dev-256c

    must print the full 40-char SHA of the deployed commit, and it must match flyctl image show's GH_SHA label.

  • The Sentry Resolve → Resolved In dropdown offers the currently-deployed release rather than a stale one.

  • Server stack traces resolve to original source, not minified frames. This is the part most likely to still be broken after the release is tagged — see the note below.

  • npm run validate and npm run test:run pass.

A caveat worth checking before you close this

vite.config.ts:96 sets filesToDeleteAfterUpload: ['./build/**/*.map']. Following #13, the server bundle is produced by the Vite SSR build into build/server/ rather than by esbuild into server-build/, so server maps are now inside the glob and should upload for the first time. That is a genuine improvement from #13 and worth confirming, but it also means server stack-trace symbolication is newly in play and may surface mapping issues that were previously invisible because no server maps were ever uploaded.

Do not treat a good-looking release tag as proof that server symbolication works. Trigger a real server error and check the stack trace reads as TypeScript. If it does not, that is a separate problem — file it separately rather than folding it in here.

Tests

tests/ has no Sentry init coverage today. A cheap regression guard is worth adding: assert that the built server bundle contains the release value, so a future Dockerfile edit that drops the ARG fails loudly instead of silently regressing. The repo already has a Docker-adjacent smoke test in tests/server.smoke.test.ts — check how it provisions the server and extend it if it can read the runtime env cheaply.

A unit test is not practical here: server/utils/monitoring.ts calls Sentry.init at module scope with no seam for injection, and the value under test is a build-time constant. Assert on the artifact, not the function.

Related

Activity

  1. arpitdalal commented on Sep 27, 2026

    @arpitdalal
    OwnerAuthor

    Correction: the release is probably not being created

    While investigating credential handling for #16, I found the mechanism that actually explains the missing release — and it invalidates the evidence section above.

    #16 root cause: SENTRY_ORG and SENTRY_PROJECT are read at vite.config.ts:87-88 and have never been available during a remote build. Not in GitHub Actions secrets (gh secret list shows only FLY_API_TOKEN and SENTRY_AUTH_TOKEN), not passed as --build-secret, not visible to the build from Fly's runtime secret store, and .env is excluded by other/Dockerfile.dockerignore. So both evaluate to undefined on every deploy.

    The plugin then hits this guard in @sentry/bundler-plugin and returns before creating anything:

    } else if (!options.org && !options.authToken.startsWith("sntrys_")) {
      logger.warn(`No organization slug provided. Will not create release. ...`)
      return
    }

    What this invalidates: the _sentryDebugIds line I cited above is not proof the release exists. Debug-ID injection happens during the bundle phase; release creation and source map upload happen later in closeBundle. The guard returns from the release step only and never un-injects anything. The bundle looks correct whether or not the upload ever ran. My statement that "the release is in Sentry — go look at Releases" was wrong.

    The SENTRY_RELEASE={id:"4b7771c..."} value is injected locally from COMMIT_SHA and does not require a round trip, so it proves only that COMMIT_SHA reached the build.

    What still stands in this issue:

    These two must be fixed together for server events to be attributed at all: #16 makes the build-time value available, this issue makes it available at runtime. Fixing only this one would tag the server with a correctly-named release that still has no source maps uploaded under it.

    To confirm the diagnosis, read the remote build log (dashboard → your app → Builds → the build for the commit → Build log) and search for No organization slug provided. flyctl has no builds subcommand. If the warning is absent, the token must be sntrys_-prefixed, the guard passed, and the cause is something else — file that separately.

  2. added a commit that references this issue on Sep 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions