How to Include a JavaScript Library via CDN Without a Hard-coded Version

Sentry prefers their JavaScript SDK to be included via CDN. Here's how to do that while still allowing automated updates.

If you’ve ever worked with us at Chromatic, you know we are borderline fanatical about keeping our dependencies up-to-date. In fact, we are so serious about this that we routinely go out of our way to include client-side libraries as dependencies in our package.json files so that we can use Dependabot to keep those dependencies up-to-date. The result is that at any given moment, most of the client-side dependencies on any of our active projects are up-to-date, and the ones that aren’t are not far behind. Compare this to including a library via a hard-coded CDN URL that includes the version number, where version upgrades can only happen manually and thus tend to get stale for long periods of time.

This practice of pulling in third-party libraries as dependencies installed with a package manager like Yarn or npm relies on package maintainers publishing production-ready files alongside source files to the npm registry, and I’ve yet to find a project that explicitly chooses not to do so. Until now.

The Problem

With Chromatic’s recent re-platforming to Eleventy, we used this very technique to include Sentry’s SDK on every page to track errors that may occur on our site. At the time we started building our new site, the latest version of Sentry’s SDK (v6.19.7) included a build/ directory with the same bundle files that they distribute via their CDN. So we followed our usual workflow: pull the package in as a dependency with Yarn, use symdeps to link the bundled files we needed to our assets directory, and call it a day.

Sadly, the latest major version of Sentry’s SDK, v7, removes built bundles from the npm registry, effectively forcing customers who relied on those bundles to either switch to using Sentry’s CDN or build the bundles themselves. Neither option felt great, so I put my idea-guy hat on.

Making the CDN Work For Us

I wanted to avoid building Sentry’s bundles as part of our site build partly because it feels like, however incremental it may be, it will slow down every PR preview, site deployment, local environment startup, and any other build of our site. That said, if I’m honest my biggest motivation is that I just don’t feel like it should be our responsibility to build Sentry’s bundle files. After all, if I build it, I own it, and I really don’t want to own this.

So I turned my attention to Sentry’s CDN documentation. The URL for the bundle we need is as follows:

https://browser.sentry-cdn.com/7.5.1/bundle.tracing.min.js

So what if we replaced that version number with a variable? I added this in our templates:

<script src="https://browser.sentry-cdn.com/{{ sentry.version(pkg) }}/bundle.tracing.min.js"
crossorigin="anonymous"></script>

As you can see, I’ve replaced the version number with a variable that invokes the sentry.version() method, passing the Eleventy-supplied pkg object as an argument. In order for this to work, I declared a version method in our source/_data/sentry.js global data file which looks like this:

/**
* @file
* Global data related to Sentry. Available in all templates within the
* `sentry` namespace.
*/


module.exports = {
dsn: /* REDACTED */,
/**
* Return the version number of the @sentry/browser library in package.json,
* stripping any semver operators from the beginning of the string.
*/

version: (pkg) => {
return pkg.dependencies['@sentry/browser'].replace(/^[=<>~^]/g, '');
},
};

The generated HTML looks like this:

<script src="https://browser.sentry-cdn.com/7.5.1/bundle.tracing.min.js"
crossorigin="anonymous">
</script>

Success! We use Sentry’s CDN to load their client-side bundle, and maintain its version via our dependency manifest in package.json, so Dependabot can bump the version as soon as Sentry publishes a new release.

Subresource Integrity

The one bit about this strategy, as it has been laid out so far, is that Sentry’s documentation includes an integrity attribute for the script element used to load the SDK from their CDN. The value passed to that integrity attribute is a hash that is unique to the file being served and it is used by browsers to validate that the contents of the remote file have not been tampered with. This is a security measure that helps mitigate some of the risk associated with loading JavaScript from a server you don’t control, such as a CDN.

Our strategy so far omits this attribute in the interest of simplicity and ease of maintenance, but let’s face it: we care about security about as much as we care about keeping our dependencies up-to-date, so we should take steps to include this attribute in our markup. Sadly, there is no reasonable way to automatically fill in this value the way we do the version. Having said that, maintaining this manually is simple enough.

First, we modify our source/_data/sentry.js file I mentioned above to add an integrity key, like so:

/**
* @file
* Global data related to Sentry. Available in all templates within
* the `sentry` namespace.
*/


module.exports = {
dsn: /* REDACTED */,
// When the Sentry SDK is upgraded, update this value with the
// integrity checksum for `bundle.tracing.min.js` in
// https://docs.sentry.io/platforms/javascript/install/cdn/#available-bundles
integrity: 'sha384-W/DkFvcLhW1KTVZV/SQYHttd5Dl8QIMR1kY7kjDyo9gALIvM+TdFpg2jGvJQ4x80',
/**
* Return the version number of the @sentry/browser library in
* package.json, stripping any semver operators from the beginning
* of the string.
*/

version: (pkg) => {
return pkg.dependencies['@sentry/browser'].replace(/^[=<>~^]/g, '');
},
};

Then, we modify our template like so:

{#
When the Sentry SDK version is bumped, we need to update the integrity
key in source/_data/sentry.js.
#}
<script src="https://browser.sentry-cdn.com/{{ sentry.version(pkg) }}/bundle.tracing.min.js"
integrity="{{ sentry.integrity }}"
crossorigin="anonymous"></script>

Notice we now have a comment instructing maintainers on where to update the integrity checksum. When Dependabot bumps the version of the Sentry SDK, the URL will get updated and the result may be an updated bundle. When that happens the file will no longer match the hash provided in the integrity attribute, the browser will fail to load the SDK due to this mismatch, and we should see errors in the browser console when reviewing the PR preview environment spun up for us in Netlify. This error should alert developers reviewing the Dependabot PR that something has broken and a cursory look at the console will lead them to the integrity attribute.

This is by no means a perfect solution, but it conserves the —ahem— integrity of our CDN link and makes it obvious when it needs to be updated.

The Result

Here’s the resulting HTML:

<script src="https://browser.sentry-cdn.com/7.5.1/bundle.tracing.min.js"
integrity="sha384-W/DkFvcLhW1KTVZV/SQYHttd5Dl8QIMR1kY7kjDyo9gALIvM+TdFpg2jGvJQ4x80"
crossorigin="anonymous">
</script>

I like this solution because it is the best of both worlds: We use Sentry’s recommended solution for our needs (loading the SDK from their CDN), and we also get to keep the version up-to-date using Dependabot. So we eat our cake and we have it too! The integrity checksum adds a bit of maintenance overhead, but it also gives us peace of mind from a security standpoint and makes our implementation match exactly what the Sentry docs specify. That’s a win in my book!

This strategy can be used for any CDN-hosted dependencies that specify a version number in the URL. It is a great way to reap the benefits of CDN-hosted libraries while greatly reducing the risk of letting the version number fall woefully out of date. Give this a shot in your projects and give us a shout @ChromaticHQ on Twitter to share your thoughts.