Cloudflare Wrangler Sourcemaps on Sentry

June 22, 2025

I just spent way too much time figuring out how to get proper sourcemaps working for a Cloudflare Workers project that uses Sentry for error monitoring. The default Wrangler setup uses esbuild internally, and while you can get sourcemaps working with esbuild, Sentry's Vite plugin makes this trivially easy.

The Problem

Cloudflare Workers deployed via wrangler deploy use esbuild by default. This is fast and works great, but when errors happen in production, Sentry shows you minified stack traces that are basically useless:

Error: Something went wrong
  at a (index.js:1:234)
  at b (index.js:1:567)

Not helpful when you're trying to debug a production issue at 2am.

The Solution

Wrangler supports Vite as an alternative bundler, and Sentry has excellent Vite plugin support. Here's my complete config:

vite.config.ts:

import { resolve } from "node:path";
import { cloudflare } from "@cloudflare/vite-plugin";
import { sentryVitePlugin } from "@sentry/vite-plugin";
import { defineConfig, loadEnv } from "vite";

export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd(), "");
  const envValue =
    env["NODE_ENV"] === "development" ? "development" : "production";

  return {
    define: {
      __ENV__: JSON.stringify(envValue),
    },
    resolve: {
      alias: {
        "~": resolve(__dirname, "./src"),
      },
    },
    plugins: [
      cloudflare(),
      sentryVitePlugin({
        org: process.env["SENTRY_ORG"] ?? "your-org",
        project: process.env["SENTRY_PROJECT"] ?? "api",
        authToken: process.env["SENTRY_AUTH_TOKEN"],
        debug: envValue === "development",
        sourcemaps: {
          assets: [".vite/your_worker/**/*.js", ".vite/your_worker/**/*.map"],
        },
      }),
    ],
    build: {
      sourcemap: true,
      outDir: resolve(__dirname, "./.vite"),
    },
    server: {
      port: 8787,
      host: "0.0.0.0",
      allowedHosts: true,
    },
  };
});

Injecting Build-Time Variables

One nice feature of Vite is the define option, which lets you inject constants at build time. I use this for environment detection:

define: {
  __ENV__: JSON.stringify(envValue),
}

Then in your worker code:

declare const __ENV__: string;

This gets replaced at build time, so there's no runtime overhead. Much cleaner than checking process.env or other runtime environment detection.

Environment Variables Gotcha

Here's something that tripped me up: Cloudflare Workers bindings (like secrets) don't work in CI environments. You can't use wrangler secrets put SENTRY_AUTH_TOKEN and expect it to be available during your build process.

Instead, you need to set SENTRY_AUTH_TOKEN as a regular environment variable if you're using Worker Builds.

The Sentry auth token is only needed at build time for uploading sourcemaps - it's not a runtime dependency of your worker.

Package Scripts

package.json:

{
  "scripts": {
    "deploy": "vite build && wrangler deploy --outdir=.vite/your_worker",
    "dev": "vite dev"
  }
}

wrangler.jsonc

{
  "name": "your-worker",
  "compatibility_date": "2025-06-17"
  "main": "./src/index.ts",
}

What This Gets You

  1. Automatic sourcemap upload: The Sentry Vite plugin handles uploading sourcemaps during build
  2. Proper stack traces: Production errors now show actual function names and line numbers
  3. Release tracking: Each deploy gets tagged with a release ID for better error correlation
  4. Build-time constants: Use define to inject environment-specific values
  5. Hot reloading: Development server with proper alias resolution

The Critical Detail

Make sure your wrangler deploy --outdir matches your Vite build.outDir. In my case, Vite outputs to .vite/ and Wrangler deploys from .vite/your_worker/. This took me way too long to debug when they were mismatched.

Development vs Production

For development, wrangler dev automatically detects and uses your Vite config. For production, the two-step vite build && wrangler deploy ensures sourcemaps are generated and uploaded before the worker goes live.

Sentry Integration with Release Tracking

Here's how I wire up Sentry in the actual worker code to use the deployment metadata for release tracking:

interface Env {
  Variables: {};
  Bindings: Readonly<Cloudflare.Env>;
}

const app = new Hono<Env>();

app.get("/_error", () => {
  throw new Error(`😵‍💫 im an error on ${__ENV__}`);
});

app.get("/_error/sentry", () => {
  Sentry.withScope((scope) => {
    scope.setExtra("foo", "bar");
    Sentry.captureException(new Error("foobar"));
  });
  return new Response("😖 im a sentry error");
});

const handler: ExportedHandler<Cloudflare.Env> = {
  fetch: app.fetch,
}

export default Sentry.withSentry((env) => {
  const typedEnv = env;
  const { id: versionId } = typedEnv.CF_VERSION_METADATA;
  return {
    dsn: typedEnv.SENTRY_DSN,
    release: versionId,
    environment: __ENV__,
    sendDefaultPii: true,
  };
}, handler);

The magic here is CF_VERSION_METADATA - this gives you access to Cloudflare's deployment metadata, including a unique version ID for each deployment. This becomes your Sentry release ID, which links errors to specific deployments and sourcemaps.

To enable this binding, add it to your wrangler.jsonc:

{
  "version_metadata": {
    "binding": "CF_VERSION_METADATA"
  }
}

Now when errors occur, they're automatically tagged with the exact deployment version, making it easy to correlate issues with specific releases.

The Complete Picture

With this setup:

  1. Vite builds your worker and generates sourcemaps
  2. Sentry plugin uploads sourcemaps tagged with version metadata
  3. Wrangler deploys with the version metadata binding
  4. Runtime errors get tagged with the deployment version
  5. Sentry shows proper stack traces with source file/line numbers

This setup has made debugging production issues so much easier. No more guessing which minified function threw an error. The combination of Vite's modern tooling and Sentry's excellent plugin support really does make a difference.

Useful Resources