getsentry / sentry-javascript

Official Sentry SDKs for JavaScript
https://sentry.io
MIT License
7.95k stars 1.57k forks source link

Adding import map support #6141

Open shanet opened 1 year ago

shanet commented 1 year ago

Problem Statement

Short version: I want to load the Sentry JavaScript library via an Import Map, but it doesn't seem to be supported.


Longer version: I'm working on a new Rails project where I'm trying to do away with Webpack and crowd entirely. This has so far been possible with the use of the importmap-rails gem which adds support for JavaScript import maps to Rails. See for more details, but in short import maps allow the use of ES6 modules directly in the browser without the need for a backend Webpack-style compilation which greatly simplifies the amount of backend work needed to maintain a difficult to work with build system like that (I'm not a big fan of those if you can't tell!). Native browser support for import maps is nearly available with Firefox and Safari being the last major holdouts: https://caniuse.com/import-maps.

JSPM has import-map compatible versions of all NPM packages available for use to varying degrees of compatibility depending on the package. In theory these should be able to be served from one's website either by linking directly to their CDN or by downloading the packages locally and serving them yourself.

In regards to Sentry, I tried to do this a few different ways but was unsuccessful with all, except sort of one.

  1. For Sentry, the CDN-hosted version from JSPM works decently well (which gave me encouragement that it could work!).
  2. However, I don't want to rely on a third-party CDN so I tried to download it's files for the Sentry package and serve them myself. This failed with a bunch of 404 errors. After digging into this, I found out that the problem was that the files generated by JSPM were not complete. When loading from their CDN the missing modules would load from their CDN. However, when served locally these modules were truly missing and would result in 404 errors. This may be more of an issue with JSPM than Sentry, I'm not sure.
  3. I also tried to use the NPM package directly. It's somewhat hacky with Importmaps, but for other packages I've used Yarn to download the package locally, then create a symlink to the relevant node_modules directory and tell the import map to load all modules from that directory. I got decently far with this approach but it fell down when working with Sentry's browserPolyfill modules as I didn't have a way to get the load path correct for being loaded via an import map.
  4. From here I tried downloading the built bundle files from Sentry's CDN and serving those. However, this failed because I couldn't get the bundle to have the correct exports available to my own Sentry module which was attempting to import from @sentry/browser. I also tried importing directly from a URL like import * as Sentry from https://browser.sentry-cdn.com/7.17.4/bundle.tracing.min.js but this failed with the same issue.
  5. Finally, since Sentry no longer publishes built bundles in the NPM package I tried building one myself. I didn't think it would be any different from the CDN version but I gave it a shot anyway if I could have more control over the exports. This too failed with the same issue as the CDN version above. There might be more here to explore but at this point it was nearing 3am and I was about tapped out for the day.

What I ended up doing was using Sentry-javascript outside of my importmaps system. I downloaded the CDN bundle and am serving that directly as a standalone JS file with its own <script src=... tag in my head. This works fine for me since I had to set the user ID context in my view templates anyway. But it would be nice to have Sentry served like the rest of my third-party JS libraries via an import map.

Solution Brainstorm

So... possible solutions: I'm not much of a JavaScript person so really I'm not sure what it would take to make Sentry compatible with an import map. From my attempts yesterday it would seem the issue lies with getting the proper import paths set so they'll load in a browser instead of in a backend build system like Webpack. Exactly what needs to happen for that, I'm not sure.

It's also possible that what JSPM is doing to build an import map compatible version is 95% of the way there and the paths just need adjusted so the downloaded version for self-hosting has all of the necessary files.

Anyway, it would be great to have support here since in general I've really enjoyed using import maps and not having to deal with Webpack/other build tools anymore and the more of that adoption we can drive, the better in my opinion.

AbhiPrasad commented 1 year ago

Hey @shanet - thanks for writing in! Backlogging this for now, but PRs are welcome for anyone who would like to take a stab at this!

Extra note, using a bundler allows you to tree shake your code, which you can do with the Sentry SDK to reduce bundle size. I assume you'll get similar tree shaking benefits from your other libraries in use as well.

adipasquale commented 10 months ago

👋 Hi @shanet thank you for the very detailed writeup and sharing the options you’ve explored 🙇

I’m in the process of migrating a Rails 7 app to use importmaps and am also running into this problem now with Sentry not fitting in nicely. I don’t want / cannot use CDNs in this specific project.

Before I go exploring again, have you found a better way to do this than using a manually downloaded bundle and exposing it ?

shanet commented 10 months ago

@adipasquale Negative, I'm still serving a static copy of the Sentry JS file outside of an import-map with a dedicated script tag in the head. It's a bit frustrating to have this one JS file managed separately from the rest of my JS dependencies but it works so I'm in the "if it's not broken, don't fix it" camp now.

xdmx commented 9 months ago

I'm also using rails with importmaps, so far I've been using the package loaded from jspm and it's working fine. But I'd love to serve it from my own domain instead.

As a side note, I think this issue will become even more frequent since the next version of importmap-rails will download packages by default and not use the cdn directly anymore (I'd expect that passing the url it might still work, for now): https://github.com/rails/importmap-rails/pull/217

aprescott commented 6 months ago

However, when served locally these modules were truly missing and would result in 404 errors.

I have run into this problem with other packages. I'm not an expert here, but I believe importmap-rails relies on JSPM to determine dependencies, and JSPM's own generator at https://generator.jspm.io/ has interesting behavior with missing files. Try adding @sentry/browser as a dependency on the generator, then toggle the "Preload" setting, and you'll see that the output will change from this with "Preload" off (ignoring the ES shim):

<script type="importmap">
{
  "imports": {
    "@sentry/browser": "https://ga.jspm.io/npm:@sentry/browser@7.111.0/esm/index.js"
  },
  "scopes": {
    "https://ga.jspm.io/": {
      "@sentry-internal/feedback": "https://ga.jspm.io/npm:@sentry-internal/feedback@7.111.0/esm/index.js",
      "@sentry-internal/replay-canvas": "https://ga.jspm.io/npm:@sentry-internal/replay-canvas@7.111.0/esm/index.js",
      "@sentry-internal/tracing": "https://ga.jspm.io/npm:@sentry-internal/tracing@7.111.0/esm/index.js",
      "@sentry/core": "https://ga.jspm.io/npm:@sentry/core@7.111.0/esm/index.js",
      "@sentry/replay": "https://ga.jspm.io/npm:@sentry/replay@7.111.0/esm/index.js",
      "@sentry/utils": "https://ga.jspm.io/npm:@sentry/utils@7.111.0/esm/index.js"
    }
  }
}
</script>

<!-- no modulepreload <link> elements -->

To this, with "Preload" on:

<script type="importmap">
{
  "imports": {
    "@sentry/browser": "https://ga.jspm.io/npm:@sentry/browser@7.111.0/esm/index.js"
  },
  "scopes": {
    "https://ga.jspm.io/": {
      "@sentry-internal/feedback": "https://ga.jspm.io/npm:@sentry-internal/feedback@7.111.0/esm/index.js",
      "@sentry-internal/replay-canvas": "https://ga.jspm.io/npm:@sentry-internal/replay-canvas@7.111.0/esm/index.js",
      "@sentry-internal/tracing": "https://ga.jspm.io/npm:@sentry-internal/tracing@7.111.0/esm/index.js",
      "@sentry/core": "https://ga.jspm.io/npm:@sentry/core@7.111.0/esm/index.js",
      "@sentry/replay": "https://ga.jspm.io/npm:@sentry/replay@7.111.0/esm/index.js",
      "@sentry/utils": "https://ga.jspm.io/npm:@sentry/utils@7.111.0/esm/index.js"
    }
  }
}
</script>

<link rel="modulepreload" href="https://ga.jspm.io/npm:@sentry-internal/feedback@7.111.0/esm/index.js"/>
<link rel="modulepreload" href="https://ga.jspm.io/npm:@sentry-internal/replay-canvas@7.111.0/esm/index.js"/>
<link rel="modulepreload" href="https://ga.jspm.io/npm:@sentry-internal/tracing@7.111.0/esm/index.js"/>
<link rel="modulepreload" href="https://ga.jspm.io/npm:@sentry/browser@7.111.0/_/C5x5174P.js"/>
<link rel="modulepreload" href="https://ga.jspm.io/npm:@sentry/browser@7.111.0/_/dUCw-WH4.js"/>
<link rel="modulepreload" href="https://ga.jspm.io/npm:@sentry/browser@7.111.0/esm/eventbuilder.js"/>
<link rel="modulepreload" href="https://ga.jspm.io/npm:@sentry/browser@7.111.0/esm/helpers.js"/>
<link rel="modulepreload" href="https://ga.jspm.io/npm:@sentry/browser@7.111.0/esm/index.js"/>
<link rel="modulepreload" href="https://ga.jspm.io/npm:@sentry/browser@7.111.0/esm/integrations/breadcrumbs.js"/>
<link rel="modulepreload" href="https://ga.jspm.io/npm:@sentry/browser@7.111.0/esm/integrations/globalhandlers.js"/>
<link rel="modulepreload" href="https://ga.jspm.io/npm:@sentry/browser@7.111.0/esm/integrations/linkederrors.js"/>
<link rel="modulepreload" href="https://ga.jspm.io/npm:@sentry/core@7.111.0/_/EGioHq0U.js"/>
<link rel="modulepreload" href="https://ga.jspm.io/npm:@sentry/core@7.111.0/_/zINxQu1p.js"/>
<link rel="modulepreload" href="https://ga.jspm.io/npm:@sentry/core@7.111.0/esm/index.js"/>
<link rel="modulepreload" href="https://ga.jspm.io/npm:@sentry/core@7.111.0/esm/integrations/functiontostring.js"/>
<link rel="modulepreload" href="https://ga.jspm.io/npm:@sentry/core@7.111.0/esm/integrations/inboundfilters.js"/>
<link rel="modulepreload" href="https://ga.jspm.io/npm:@sentry/core@7.111.0/esm/sdk.js"/>
<link rel="modulepreload" href="https://ga.jspm.io/npm:@sentry/replay@7.111.0/esm/index.js"/>
<link rel="modulepreload" href="https://ga.jspm.io/npm:@sentry/utils@7.111.0/_/XYduJafA.js"/>
<link rel="modulepreload" href="https://ga.jspm.io/npm:@sentry/utils@7.111.0/_/nhV-bW4G.js"/>
<link rel="modulepreload" href="https://ga.jspm.io/npm:@sentry/utils@7.111.0/esm/index.js"/>
<link rel="modulepreload" href="https://ga.jspm.io/npm:@sentry/utils@7.111.0/esm/is.js"/>
<link rel="modulepreload" href="https://ga.jspm.io/npm:@sentry/utils@7.111.0/esm/misc.js"/>
<link rel="modulepreload" href="https://ga.jspm.io/npm:@sentry/utils@7.111.0/esm/path.js"/>
<link rel="modulepreload" href="https://ga.jspm.io/npm:@sentry/utils@7.111.0/esm/string.js"/>
<link rel="modulepreload" href="https://ga.jspm.io/npm:@sentry/utils@7.111.0/esm/supports.js"/>

The missing files appears to suggest there's some awareness that there are other dependencies.

(I don't think this actually functions as you'd expect though, since not every file appears in the importmap. But I could be wrong.)

In any case, there seems to be some awareness of this category of problem within the importmap-rails GitHub repo. See https://github.com/rails/importmap-rails/issues/153 which refers to missing files when using --download.

You may have some luck with using jsdelivr as an alternative source. The above PR links to https://github.com/rails/importmap-rails/issues/65#issuecomment-1803613257 which may be helpful.