Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add loader file to node-based SDKs to support ESM monkeypatching #11338

Merged
merged 10 commits into from
Apr 3, 2024

Conversation

lforst
Copy link
Member

@lforst lforst commented Mar 28, 2024

Ref: #10046

Adds a loader script that can be used like node --loader=@sentry/[sdk-of-your-choice] your-script.js to allow for monkey-patching modules even in ESM.

Note

Currently, this is broken for upstream reasons. See #11338 (comment)

@lforst
Copy link
Member Author

lforst commented Mar 28, 2024

I am having a problem where this loader simply doesn't work with our SDK. I am doing the following but no spans are created for incoming requests (in fact otel doesn't seem to path the http module):

// test.mjs
import * as Sentry from '@sentry/node';

Sentry.init({
  dsn: 'https://7cea2b6e298f4fcc86bb28d22ceaeac4@o447951.ingest.sentry.io/4505391490007040',
  tracesSampleRate: 1,
  debug: true,
});

import('node:http').then(http => {
  const PORT = 3000;

  const server = http.createServer((req, res) => {
    res.writeHead(200, { 'Content-Type': 'text/html' });
    res.end('<html>response</html>');
  });

  server.listen(PORT, () => {
    console.log(`Server is running on http://localhost:${PORT}`);
  });
});
node --import=@sentry/node/register test.mjs

If I do

node --loader=@sentry/node/register test.mjs

it crashes with

(node:615243) ExperimentalWarning: `--experimental-loader` may be removed in the future; instead use `register()`:
--import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register("%40sentry/node/register", pathToFileURL("./"));'
(Use `node --trace-warnings ...` to show where the warning was created)
file:///workspaces/sentry-nextjs-vercel-testproject/node_modules/@sentry/opentelemetry/esm/index.js:9
import { SamplingDecision } from '@opentelemetry/sdk-trace-base';
         ^^^^^^^^^^^^^^^^
SyntaxError: The requested module '@opentelemetry/sdk-trace-base' does not provide an export named 'SamplingDecision'
    at ModuleJob._instantiate (node:internal/modules/esm/module_job:123:21)
    at async ModuleJob.run (node:internal/modules/esm/module_job:191:5)
    at async ModuleLoader.import (node:internal/modules/esm/loader:336:24)
    at async loadESM (node:internal/process/esm_loader:34:7)
    at async handleMainPromise (node:internal/modules/run_main:106:12)

Copy link

codecov bot commented Mar 28, 2024

Bundle Report

Changes will decrease total bundle size by 381.72kB ⬇️

Bundle name Size Change
@sentry/types-cjs 35 bytes 0 bytes
@sentry/types-esm 35 bytes 0 bytes
@sentry/utils-cjs 178.75kB 0 bytes
@sentry/node-cjs 336.97kB 0 bytes
@sentry/utils-esm 174.17kB 0 bytes
@sentry-internal/replay-cjs 306.35kB 0 bytes
@sentry/core-cjs 240.44kB 0 bytes
@sentry/core-esm 236.82kB 0 bytes
@sentry-internal/integration-shims-cjs 3.65kB 0 bytes
@sentry-internal/replay-esm 306.46kB 0 bytes
@sentry/aws-serverless-cjs 14.62kB 0 bytes
@sentry-internal/replay-canvas-cjs 29.51kB 0 bytes
@sentry-internal/node-integration-tests-cjs 1.04kB 0 bytes
@sentry-internal/node-integration-tests-esm 888 bytes 0 bytes
@sentry-internal/replay-canvas-esm 29.43kB 0 bytes
@sentry/bun-cjs 13.5kB 0 bytes
@sentry/google-cloud-serverless-esm 19.16kB 0 bytes
@sentry/browser-esm 104.53kB 13 bytes ⬆️
@sentry/browser-cjs 107.36kB 44 bytes ⬆️
@sentry/bun-esm 10.05kB 0 bytes
@sentry/google-cloud-serverless-cjs 23.0kB 0 bytes
@sentry/wasm-cjs 5.2kB 0 bytes
@sentry/node-esm 333.56kB 0 bytes
@sentry/vue-cjs 20.19kB 0 bytes
@sentry/svelte-cjs 13.84kB 0 bytes
@sentry/astro-cjs 27.13kB 0 bytes
@sentry/wasm-esm 4.85kB 0 bytes
@sentry/astro-esm 23.39kB 0 bytes
@sentry/react-cjs 45.04kB 0 bytes
@sentry/vue-esm 18.85kB 0 bytes
@sentry/svelte-esm 12.72kB 0 bytes
@sentry/sveltekit-cjs 69.31kB 0 bytes
@sentry/sveltekit-esm 61.08kB 0 bytes
@sentry/remix-esm 48.23kB 0 bytes
@sentry/react-esm 41.18kB 0 bytes
@sentry/remix-cjs 53.62kB 0 bytes
@sentry/gatsby-cjs 905 bytes 0 bytes
@sentry/nextjs-cjs 20.52kB 0 bytes
@sentry/nextjs-esm 150.44kB 137.91kB ⬆️
@sentry/profiling-node-cjs 25.5kB 0 bytes
@sentry/profiling-node-esm 25.52kB 0 bytes
@sentry/vercel-edge-cjs (removed) 18.23kB ⬇️
@sentry-internal/integration-shims-esm (removed) 2.99kB ⬇️
@sentry-internal/feedback-cjs (removed) 65.56kB ⬇️
@sentry/vercel-edge-esm (removed) 16.13kB ⬇️
@sentry/gatsby-esm (removed) 385 bytes ⬇️
@sentry/opentelemetry-cjs (removed) 68.45kB ⬇️
@sentry-internal/tracing-cjs (removed) 108.01kB ⬇️
@sentry-internal/feedback-esm (removed) 65.28kB ⬇️
@sentry/opentelemetry-esm (removed) 67.4kB ⬇️
@sentry-internal/tracing-esm (removed) 107.26kB ⬇️

@lforst
Copy link
Member Author

lforst commented Mar 28, 2024

Talked with @timfish (🙏).

So apparently, @opentelemetry/instrumentation + ESM + our loader (or even @opentelemetry/instrumentation/hook.mjs) is broken on certain Node.js versions (like 18.19.0, 19, 20, 21), because import-in-the-middle only added support for these Node.js versions in version >=1.7.2, however import-in-the-middle@>=1.7.2 breaks @opentelemetry/instrumentation's instrumentation.

All of the above is outlined and tracked here: open-telemetry/opentelemetry-js#4547 (comment)

Copy link
Collaborator

@timfish timfish left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--loader is deprecated and marked for removal at some point in the future so we should be targeting --import.

https://nodejs.org/api/cli.html#--experimental-loadermodule

This flag is discouraged and may be removed in a future version of Node.js. Please use --import with register() instead.

The only difference is that the code in the register export should pass the loader to register. Then it can be used in the same way but wth --import:

import { register } from 'node:module';
register('../build/loader-hook.mjs', import.meta.url););

Docs are here:
https://nodejs.org/api/module.html#enabling

@lforst
Copy link
Member Author

lforst commented Apr 2, 2024

@timfish This is funny. I tried to use register but it was only added in Node 18.19.0, which is incompatible with the currently used version of import-in-the-middle. 😭 😂

@lforst
Copy link
Member Author

lforst commented Apr 2, 2024

It looks like we need import.meta.url, otherwise node throws with

[WebServer] 
node:internal/process/esm_loader:40
      internalBinding('errors').triggerUncaughtException(
                                ^

[WebServer] TypeError [ERR_INVALID_URL]: Invalid URL
    at new NodeError (node:internal/errors:405:5)
    at new URL (node:internal/url:676:13)
    at getPackageScopeConfig (node:internal/modules/esm/package_config:29:24)
    at packageResolve (node:internal/modules/esm/resolve:870:25)
    at moduleResolve (node:internal/modules/esm/resolve:973:20)
    at defaultResolve (node:internal/modules/esm/resolve:1193:11)
    at nextResolve (node:internal/modules/esm/hooks:864:28)
    at Hooks.resolve (node:internal/modules/esm/hooks:302:30)
    at ModuleLoader.resolve (node:internal/modules/esm/loader:365:35)
    at ModuleLoader.getModuleJob (node:internal/modules/esm/loader:249:38) {
  input: './package.json',
  code: 'ERR_INVALID_URL'
}

I don't fully understand yet why, but I assume it doesn't know how to resolve relative imports without a base url.

@lforst lforst requested a review from timfish April 2, 2024 10:07
Copy link
Collaborator

@timfish timfish left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

We might also want to export the loader hook too, to support the < Node v18 case?

@lforst
Copy link
Member Author

lforst commented Apr 2, 2024

We might also want to export the loader hook too, to support the < Node v18 case?

Fair point, it would need to be in a separate hook because simply importing register from module will crash on Node < 18.

@timfish
Copy link
Collaborator

timfish commented Apr 2, 2024

it would need to be in a separate hook because simply importing register from module will crash on Node < 18.

Yeah, there would need to be another export. Maybe @sentry/node/hook?

@lforst
Copy link
Member Author

lforst commented Apr 3, 2024

I changed it so we have two files. Users would have to do the following:

  • Node <=18.18.2: node --experimental-loader=@sentry/node/hook app.js
  • Node >=18.19.0: node --import=@sentry/node/register app.js

@lforst lforst changed the title feat: Add loader file to node-based SDKs to support ESM feat: Add loader file to node-based SDKs to support ESM monkeypatching Apr 3, 2024
@lforst lforst merged commit 4085728 into develop Apr 3, 2024
94 checks passed
@lforst lforst deleted the lforst-loader-file branch April 3, 2024 11:36
@lforst
Copy link
Member Author

lforst commented Apr 3, 2024

An important todo for us here is gonna be documenting this properly when import-in-the-middle and @opentelemetry/instrumentation is fixed.

@wojtekmaj
Copy link

Perhaps we could rename hook to loader and register to import? I forgot these names in the middle of writing this comment. 😅 Or is there a naming convention I'm not aware of?

@lforst
Copy link
Member Author

lforst commented Apr 9, 2024

@wojtekmaj That's a very fair suggestion. Otel and import-in-the-middle call the hook "hook", so that's why we named it like that. It's like a semi-convention. But I see the appeal of naming them loader and import.

@wojtekmaj
Copy link

https://github.com/tapjs/tsimp uses what I proposed and I love I never need to dig through the docs to configure it properly :)

@lforst
Copy link
Member Author

lforst commented Apr 9, 2024

Ok that looks actually pretty clean. Let me change this :D

@lforst
Copy link
Member Author

lforst commented Apr 9, 2024

@wojtekmaj What do you think about import-hook and loader-hook? I am worried that just @sentry/node/import is not descriptive enough..

@wojtekmaj
Copy link

wojtekmaj commented Apr 9, 2024

Makes sense to me! I'd go for a more descriptive version if you think it's gonna be used as one-time thing you put in your scripts section, and a shorter version if you expect people to type in commands in their terminals more often.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants