Originally posted on Medium on Mar 10, 2019.
Photo by Markus Spiske 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, 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) 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 for our frontend code, with create-react-app for an easy setup. We had to modify the Webpack configuration to use the SourceMapDevToolPlugin to set a custom URL. We use react-app-rewired to modify the Webpack configuration without ejecting.
// 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 to deploy and host our frontend code behind their Application Delivery Network (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”
# 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 but the concept will be exactly the same.
- Build your code as usual
- 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. - You must authenticate to Google Cloud. They recommend using a service account for CI jobs. See how to create one from the docs. 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) - 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 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.
# 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:
- 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 everysentry-cli
command. -
$SENTRY_ENV
is an environment variable I use to name my release (dev, staging, prod). It is set by branch in thenetlify.toml
file as described in the docs. -
upload-sourcemaps
will look for.js
and.js.map
files in the provided directory, herebuild/static/js/
where our JS assets are bundled by webpack. Therewrite
andurl-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 ;).
Top comments (0)