Deploy private source maps with Netlify
Tags: devops, webpack, googlecloud, ci
Published: 2019-10-11 11:00:07

*Originally posted on [Medium](https://medium.com/@tibotiber/deploy-private-source-maps-with-netlify-72865adb2f8) on Mar 10, 2019.*
*Photo by [Markus Spiske](https://unsplash.com/photos/SH98nuc1-Xc) on Unsplash.*

Source maps are great, they let you ship minified, optimised code to your users while enabling you to map things back to your original source code in error logs and while debugging. You will typically use a build system to generate your minifed code along with the source maps and deploy them together on your host. You can also use this build step to “uglify” your code in order to protect valuable intellectual property present in your frontend code. When that happens, you can no longer afford to deploy the source maps publicly. This post details how I deployed our source maps with team-only access.

---

## Private hosting of source maps

We chose to deploy our source maps on [Google Cloud Storage](https://cloud.google.com/storage/), in a bucket configured for access to authenticated users from our domain only. This is really convenient as we are using G Suite for our emails and can therefore leverage an authentication system that’s already in place. To do this, set permissions using [Access Control Lists (ACLs)](https://cloud.google.com/storage/docs/access-control/lists) on your bucket.

For example, head to *Permissions* for your bucket, then *Add members*, enter `yourdomain.com` as the member and add the role *Storage Object Viewer* to let anyone in your organization access source maps when their browser is logged in.

---

## Pointing the source maps to your bucket

Source maps are automatically fetched by the browser whenever needed, to do this the browser relies on an address indicated at the end of the minified file. It usually looks like this:

```
//# sourceMappingURL=index.e0d98431.js.map
```

By default, most build systems will co-locate source maps with the minified files and link them using their relative path. We will need to change the URL to the one of the bucket we created.

Here, your mileage may vary depending on the tools you use, but the concept remain the same. We use [React](https://reactjs.org/) for our frontend code, with [create-react-app](https://facebook.github.io/create-react-app/) for an easy setup. We had to modify the [Webpack](https://webpack.js.org/) configuration to use the [SourceMapDevToolPlugin](https://webpack.js.org/plugins/source-map-dev-tool-plugin/) to set a custom URL. We use [react-app-rewired](https://github.com/timarney/react-app-rewired) to modify the Webpack configuration without ejecting.

```js
// config-overrides.js
const webpack = require('webpack')

function setSourceMaps (config, env) {
  config.devtool = false
  if (!config.plugins) {
    config.plugins = []
  }
  config.plugins.push(
    new webpack.SourceMapDevToolPlugin({
      append:
        env === 'development'
          ? '\n//# sourceMappingURL=[url]'
          : '\n//# sourceMappingURL=https://storage.cloud.google.com/<bucket-name>/[url]',
      filename: '[file].map'
    })
  )
  return config
}

module.exports = {
  webpack: (config, env) => {
    config = setSourceMaps(config, env)
    return config
  }
}
```

From there, your build will inject the new URL at the end of the minified file:

```
//# sourceMappingURL=https://storage.cloud.google.com/<bucket-name>/index.e0d98431.js.map
```

---

## Deploying the source maps with Netlify

We use [Netlify](https://www.netlify.com/) to deploy and host our frontend code behind their [Application Delivery Network (ADN)](https://www.netlify.com/features/adn/). We previously built the code and let Netlify handle the rest. Our build script was something like:

```
“build”: “react-app-rewired build”
```

Now we call a shell script that handles the build and upload the source maps:

```
“build”: “./build.sh”
```

```sh
# build.sh

#!/bin/bash
# build
yarn react-app-rewired build
# install gcloud cli
echo "Installing gcloud sdk"
curl -O https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-sdk-237.0.0-linux-x86_64.tar.gz
tar -xzf google-cloud-sdk-237.0.0-linux-x86_64.tar.gz
./google-cloud-sdk/bin/gcloud --version
# authenticate to gcloud
echo "Authenticating to gcloud"
echo $GCLOUD_KEY_FILE > /tmp/gcloud_keyfile.json
./google-cloud-sdk/bin/gcloud auth activate-service-account --key-file /tmp/gcloud_keyfile.json
# upload sourcemaps
echo "Uploading sourcemaps to gcs"
./google-cloud-sdk/bin/gsutil cp build/**/**/*.map gs://<bucket-name>/
# delete sourcemaps
echo "Deleting sourcemaps from netlify deploy"
rm build/**/**/*.map
```

You may be using another Continuous Integration (CI) tool like [TravisCI](https://travis-ci.com/) but the concept will be exactly the same.

1. Build your code as usual
2. You must install the GCloud SDK (which includes `gsutil`, the cloud storage CLI) in a non-interactive fashion by getting it from archived versions. This also ensures you use a fix version of the SDK and future updates don’t break your CI pipeline.
3. You must authenticate to Google Cloud. They recommend using a [service account](https://cloud.google.com/iam/docs/service-accounts) for CI jobs. See how to create one from [the docs](https://cloud.google.com/iam/docs/creating-managing-service-accounts#creating_a_service_account). This will generate a JSON file which you need to provide during your deploy job. As we do not want to put this file in the repository for security reasons, I suggest storing its content as en environment variable in Netlify (or the CI tool you use) and building the file from the environment variable. (that’s what `$GCLOUD_KEY_FILE` is all about)
4. From there the hardest is done. Use `gsutil` to upload the source maps and do not forget to delete them from the build folder before letting Netlify deploy that folder.

---

## BONUS: Upload code and source maps to Sentry

If you’re using [Sentry](https://sentry.io) as error reporting solution, you may also want to upload both the minified code and private source maps to Sentry to get the best error stack traces you can. To do that, we’ll simply add a section to create a Sentry “release” in our `build.sh` file. Below are the extras to add right before deleting the source maps.

```sh
# build.sh

# ... build and push to gcloud ... #

# install sentry cli
echo "Installing and configuring sentry cli"
curl -L -o sentry-cli https://github.com/getsentry/sentry-cli/releases/download/1.40.0/sentry-cli-Linux-x86_64
chmod u+x sentry-cli
./sentry-cli --version
export SENTRY_ORG=<sentry-org>
export SENTRY_PROJECT=<sentry-project>
# create sentry release and upload source maps
echo "Creating release"
./sentry-cli releases new ${COMMIT_REF::7}
echo "Setting release commits"
./sentry-cli releases set-commits --commit "<github-org>/<github-repo>@$COMMIT_REF" ${COMMIT_REF::7}
echo "Link deploy to release"
./sentry-cli releases deploys ${COMMIT_REF::7} new -e $SENTRY_ENV
echo "Uploading source maps"
./sentry-cli releases files ${COMMIT_REF::7} upload-sourcemaps build/static/js/ --rewrite --url-prefix '~/static/js'
echo "Finalizing release"
./sentry-cli releases finalize ${COMMIT_REF::7}

# ... delete source maps ... #

```

Notes:

1. The name of the release is your choice, I use the short SHA of the commit `${COMMIT_REF::7}` but you can use anything, just make sure to adapt it in every `sentry-cli` command.
2. `$SENTRY_ENV` is an environment variable I use to name my release (dev, staging, prod). It is set by branch in the `netlify.toml` file as described [in the docs](https://www.netlify.com/docs/netlify-toml-reference/).
3. `upload-sourcemaps` will look for `.js` and `.js.map` files in the provided directory, here `build/static/js/` where our JS assets are bundled by webpack. The `rewrite` and `url-prefix` options ensure that sentry uses its own uploaded artifacts instead of trying to get the one from cloud storage.

---

Hope this has been a useful post. Happy devops ;).