Published on

JavaScript source maps with Phoenix 1.7

If you are setting up error tracking for the JavaScript assets in your Phoenix app, you’ll need a source map to enable readable stack traces. In the following, I’m setting this up with Sentry for error tracking and GitHub Actions for CI/CD, but you can easily adapt this to any vendor.

Generating the source maps

With Phoenix 1.7 you generate the source maps by setting the --sourcemap flag for esbuild in config/config.exs:

config :esbuild,
  version: "0.17.11",
  my_app_web: [
      ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/* --sourcemap=external),
    cd: Path.expand("../assets", __DIR__),
    env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}

In the above, I use --sourcemap=external flag to generate the source map but importantly not include the reference in our .js file. If you are fine with exposing the source map to the public you can just use --sourcemap and be done with it; the browser can show the full stack trace and your error tracker might be able to automatically pick it up.

In my case, I want to keep the source map to myself, so I have to move it out of the assets directory in my build stage so it will not be included in my release build.

Add the following commands in the Dockerfile right after the assets are compiled with mix assets.deploy:

# ...

# Compile assets
RUN mix assets.deploy

# Move source maps out of assets
RUN mkdir ./sourcemaps
RUN mv apps/my_app_web/priv/static/assets/* ./sourcemaps
# ...

With these commands I’m moving the source map files out of priv/static/assets to a place that will not be included in my release.

Upload the source maps

I’m using Github Actions for my CI/CD and will be using the Sentry Release Github Action task to upload the source map. This means that I first need to extract the source maps from the build.

Here we add a new stage named sourcemaps to our Dockerfile just before our final release stage:

# ...
# sourcemaps is a target used as remote context to push source maps to sentry
FROM scratch AS sourcemaps
COPY --from=builder /app/sourcemaps/ /

# Start a new build stage so that the final image will only contain
# the compiled release and other runtime necessities
# ...

The only thing we do in this stage is to copy the generated source maps so we can pull them out of the build to the host. We can pull them out of the build by running docker build . --output ./sourcemaps --target sourcemaps.

Now we can set up Github Actions to upload the source maps when we build the release:

# ...
- name: Build and push image
  id: docker-build
  uses: docker/build-push-action@v4
    cache-from: type=gha
    cache-to: type=gha,mode=max
    # push: true
    # tags: ...
    # ...

- name: Output source maps
  uses: docker/build-push-action@v4
    target: sourcemaps
    cache-from: type=gha
    cache-to: type=gha,mode=max
    outputs: type=local,dest=./sourcemaps

- name: Create Sentry release
  uses: getsentry/action-release@v1
    SENTRY_ORG: ${{ vars.SENTRY_ORG }}
    version: ${{ github.sha }}
    sourcemaps: ./sourcemaps

We are using the Build and Push docker images GHA task to first build and push our release image. After that, we output the source maps to the ./sourcemaps directory on our GHA host. Now the Sentry Release Github Actions task can pick them up and upload them.

Note that we are caching the layers so we won’t rebuild the image when running the ouput source maps task after the build and push.

Hi, I'm Dan Schultzer, I write in this blog, work a lot in Elixir, maintain several open source projects, and help companies streamline their development process