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

esbuild.serverless.js seems misleading/wrong #157

Closed
7 of 11 tasks
oliversalzburg opened this issue Jun 2, 2023 · 34 comments · Fixed by #159
Closed
7 of 11 tasks

esbuild.serverless.js seems misleading/wrong #157

oliversalzburg opened this issue Jun 2, 2023 · 34 comments · Fixed by #159
Labels
💾 💾 installation 💾 💾 package fails to install or compile from source

Comments

@oliversalzburg
Copy link

oliversalzburg commented Jun 2, 2023

Is there an existing issue for this?

SDK Version

0.3.0

What environment is your node script running in?

AWS Lambda

Are you using an alpine based docker image?

  • Yes (please specify the version)
  • No (please describe your environment)

Does the failure occur on a remote environment only?

  • Yes (please describe your environment)
  • No (please describe your local environment)

Are you using alpine based node docker image. If so, what version are you using? e.g node:16-alpine3.12

No response

If you are using alpine, have you tried using latest version of alpine or (currently alpine3.16 or alpine3.17)?

  • Yes (please specify the version)"
  • No, and I cannot upgrade

Have you tried using the latest minor version of node (currently 16.20 or 18.15)?

  • Yes 20.1.0
  • No, and I cannot upgrade

If you run npm install --verbose --foreground-scripts @sentry/profiling-node in an empty directory, what is the output?

install.log

Crash report

No response

Actual Issue Report

The example in the README suggests to use outfile: "./dist", in the esbuild configuration, which is an invalid setting for outfile.

When using the correct outdir instead, there are still no .node files being copied at all, resulting in MODULE_NOT_FOUND errors down the line.

@JonasBa
Copy link
Member

JonasBa commented Jun 4, 2023

@oliversalzburg The .node file is not there yet because we still need to release a new version that enable the changes. I'll look into validity of the esbuild config file

@oliversalzburg
Copy link
Author

Oh, that explains it then :D Should I try the alpha instead? Or did I miss anything else about how to make the 0.3.0 work with bundling?

@JonasBa
Copy link
Member

JonasBa commented Jun 5, 2023

@oliversalzburg I am targeting to release 1.0.0 today by eod so I would say best to wait :)

@oliversalzburg
Copy link
Author

Just FYI, I still wanted to prepare myself by trying the beta.7, but I couldn't get esbuild to copy the .node files there either. When I inspect the bundle, I can see it just leaves return require22("./sentry_cpu_profiler-darwin-x64-93.node") (and other similar statements) in there.

Maybe you already have this fixed in the final 1.0.0, but I wanted to bring it up just in case.

@JonasBa
Copy link
Member

JonasBa commented Jun 5, 2023

@oliversalzburg mind sharing the config? Are you testing on alpha.7? I just release a 1.0.0-beta.1 which we started using internally at Sentry before I release 1.0.0

@oliversalzburg
Copy link
Author

oliversalzburg commented Jun 5, 2023

This is the entire build script we're using to bundle our lambdas:

const argv = require("minimist")(process.argv.slice(2));
const { sentryEsbuildPlugin } = require("@sentry/esbuild-plugin");
const path = require("node:path");
const fs = require("node:fs");

if (!argv.in || !argv.out) {
  console.error(
    "Usage: node build.cjs --in packages/fn-customer-create/source/main.mts --out packages/fn-customer-create/output/bundle.cjs"
  );
  process.exit(1);
}

const entryPoint = argv.in ?? "source/main.mts";
const outfile = argv.out ?? "output/bundle.cjs";
const outdir = path.dirname(outfile);

require("esbuild")
  .build({
    bundle: true,
    entryPoints: [entryPoint],
    external: [
      // These are available through a Lambda layer.
      "@aws-lambda-powertools/commons",
      "@aws-lambda-powertools/logger",
      "@aws-lambda-powertools/tracer",
      "@aws-lambda-powertools/metrics",

      // Shipped in Lambda environment.
      "@aws-sdk/client-dynamodb",
      "@aws-sdk/client-s3",
      "@aws-sdk/client-sso",
      "@aws-sdk/client-sts",
      // Breaks bundling when using @middy/secrets-manager.
      "@aws-sdk/client-secrets-manager",
      // this breaks bundling but is not needed by auth0 - see https://auth0.github.io/node-auth0/
      "superagent-proxy",
    ],
    format: "cjs",
    loader: {
      ".node": "copy",
    },
    logLevel: "debug",
    metafile: true,
    minify: Boolean(argv.minify),
    outdir,
    platform: "node",
    plugins: process.env.SENTRY_AUTH_TOKEN
      ? [
        sentryEsbuildPlugin({
          project: "dcc-backend",
          org: "dyn-media-gmbh",
          authToken: process.env.SENTRY_AUTH_TOKEN,
          release: process.env.SENTRY_VERSION,
          telemetry: false,
        }),
      ]
      : [],
    sourcemap: true,
    target: "node18",
  })
  .then(result => {
    fs.writeFileSync(path.resolve(outdir, "meta.json"), JSON.stringify(result.metafile));
  })
  .catch(() => process.exit(1));

Given that there are explicit require statements with string literals in the code, I feel like the solution is in the esbuild config, but no luck so far with my experiments.

@JonasBa
Copy link
Member

JonasBa commented Jun 5, 2023

@oliversalzburg I just tested beta-1 with esbuild ^0.17.18 with the following config and I can see all the binary files moved. Do you mind

const { sentryEsbuildPlugin } = require("@sentry/esbuild-plugin");

require("esbuild").build({
  entryPoints: ["./index.cjs"],
  outfile: "./dist/esbuild-serverless/index.cjs",
  platform: "node",
  format: "cjs",
  bundle: true,
  minify: true,
  sourcemap: true,
  loader: {
    '.node': 'copy',
  },
  plugins: [
    // Put the Sentry esbuild plugin after all other plugins
    sentryEsbuildPlugin({
      project: "",
      org: "",
      authToken: "",
      release: process.env.SENTRY_RELEASE,
      sourcemaps: {
        // Specify the directory containing build artifacts
        assets: "./dist/**",
      },
    }),
  ],
});
CleanShot 2023-06-05 at 10 52 41@2x

Would you mind testing with the same config as I am using to see if it works for you? If it doesn't, then I would proceed by enabling the other options to see if any of them break the binary copy procedure

@oliversalzburg
Copy link
Author

I have esbuild 0.17.19 over here. It's still not behaving as it should with your config. I'll try to find out what's special on my side :(

@oliversalzburg
Copy link
Author

oliversalzburg commented Jun 5, 2023

It seems like this is related to the format of the files that are being bundled. We have .mts files. When I give those as entrypoint, I get no .node binaries.

When I create a index.cjs instead with these contents:

const ProfilingPlugin = require("@sentry/profiling-node");
new ProfilingPlugin();

then the .node files suddenly appear in the output directory.

To double-check, I created index.mjs with these contents:

import { ProfilingIntegration } from "@sentry/profiling-node";
new ProfilingIntegration();

As expected, this will then not copy the .node files.

Can't say that I understand this though :(

@JonasBa
Copy link
Member

JonasBa commented Jun 5, 2023

That is unfortunate and I do not quite understand the reasoning behind that. I think the best next step would be to mark @sentry/profiling-node as an external package and opening an issue on esbuild repository regarding the copying of binaries.

@oliversalzburg
Copy link
Author

I actually initially started with the external approach, but couldn't make it work, as then the files are also just simply missing in our AWS Lambda deployment. We only deploy our output folder with the bundled JS file and the sourcemap, nothing else. So that obviously results in a MODULE_NOT_FOUND at runtime.

I was wondering if I could just introduce the binaries into the deployment by using the Lambda layer, but I haven't looked into this option yet.

@JonasBa
Copy link
Member

JonasBa commented Jun 5, 2023

Yeah, @oliversalzburg if you mark the package as external, then you need to also ensure it's installed - I think someone else had this problem here and they managed to make it work. It's a shame .mts does not end up copying files which would make this work out the box.

An alternative that maybe is not the most pretty, but will work is to not mark the package as external and copy the binaries yourself. Something like cp node_modules/@sentry/profiling-node/lib/binaries/*.node ./dist should make the binaries available and fix the missing module error

@JonasBa JonasBa added the 💾 💾 installation 💾 💾 package fails to install or compile from source label Jun 5, 2023
@adesso-os
Copy link

After the most recent feedback, I think I will just have to find a way to copy the binaries manually. Very interesting insights though.

I just really wanted to avoid the manual copy, because it feels so fragile. Maybe paths change in the future, or the location of packages changes in the monorepo. I'd just feel better if it was fully automated through bundling :D

@adesso-os
Copy link

adesso-os commented Jun 6, 2023

Interestingly, when I install the beta.1, the binaries are not in a binaries subfolder, but in lib. Using this plugin for now:

const pluginNodeProfiling = {
  name: "sentry-profiling-node",
  setup(build) {
    build.onEnd(result => {
      const moduleEntrypoint = require.resolve("@sentry/profiling-node");
      const moduleBasePath = path.dirname(moduleEntrypoint);
      for (const entry of fs.readdirSync(moduleBasePath)) {
        if (!entry.endsWith(".node")) {
          continue;
        }
        const fileSource = path.resolve(moduleBasePath, entry);
        const fileOutput = path.resolve(outdir, entry);
        fs.copyFileSync(fileSource, fileOutput);
      }
    });
  },
};

@adesso-os
Copy link

adesso-os commented Jun 6, 2023

After deploying this, I see new problems. My deployed code will fail on the createRequire call. I assume it's because import.meta.url doesn't exist in the resulting CJS context.

I saw your latest comment on the esbuild thread. If you're going to make any adjustments to the plugin, I'd be happy to give those a test-drive :)

{
    "errorType": "TypeError",
    "errorMessage": "The argument 'filename' must be a file URL object, file URL string, or absolute path string. Received undefined",
    "code": "ERR_INVALID_ARG_VALUE",
    "stack": [
        "TypeError [ERR_INVALID_ARG_VALUE]: The argument 'filename' must be a file URL object, file URL string, or absolute path string. Received undefined",
        "    at __node_internal_captureLargerStackTrace (node:internal/errors:490:5)",
        "    at new NodeError (node:internal/errors:399:5)",
        "    at createRequire (node:internal/modules/cjs/loader:1373:11)",
        "    at Object.<anonymous> (/node_modules/@sentry/profiling-node/lib/index.mjs:5:21)",
        "    at Module._compile (node:internal/modules/cjs/loader:1254:14)",
        "    at Module._extensions..js (node:internal/modules/cjs/loader:1308:10)",
        "    at Module.load (node:internal/modules/cjs/loader:1117:32)",
        "    at Module._load (node:internal/modules/cjs/loader:958:12)",
        "    at Module.require (node:internal/modules/cjs/loader:1141:19)",
        "    at require (node:internal/modules/cjs/helpers:110:18)",
        "    at _tryRequireFile (file:///var/runtime/index.mjs:912:37)",
        "    at _tryRequire (file:///var/runtime/index.mjs:967:136)",
        "    at async _loadUserApp (file:///var/runtime/index.mjs:991:16)",
        "    at async UserFunction.js.module.exports.load (file:///var/runtime/index.mjs:1032:21)",
        "    at async start (file:///var/runtime/index.mjs:1195:23)",
        "    at async file:///var/runtime/index.mjs:1201:1"
    ]
}

@JonasBa
Copy link
Member

JonasBa commented Jun 6, 2023

Yes @adesso-os, I made some changes and will release beta.2 soon that hopefully fixes this

@JonasBa
Copy link
Member

JonasBa commented Jun 7, 2023

@oliversalzburg @adesso-os mind testing 1.0.0-beta.2 as it should now be using the correct version of require?

@JonasBa JonasBa reopened this Jun 7, 2023
@adesso-os
Copy link

That seems to be working beautifully :) esbuild also now specifically complains if I don't specify a loader for the .node files. Thanks for the quick solution!

@JonasBa
Copy link
Member

JonasBa commented Jun 7, 2023

Perfect, sorry for the back and forth, hope you enjoy using profiling!

@JonasBa JonasBa closed this as completed Jun 7, 2023
@JonasBa
Copy link
Member

JonasBa commented Jun 7, 2023

@adesso-os worth noting that I will release a first major version in the next day or so. Feel free to upgrade to that

@oliversalzburg
Copy link
Author

I feel like something might still be off, but I will create a new ticket for it when I'm back to work. I'm now seeing new runtime errors (Error: Dynamic require of "child_process" is not supported) and I see that there is a bunch of CJS code in the plugin, even though I thought it should pick the ESM versions of its dependencies for the bundle.

image

@JonasBa
Copy link
Member

JonasBa commented Jun 8, 2023

@oliversalzburg I think there is a chance that other sentry modules are not fully esm compatible. Can you post a stack trace so I can see what is happening here?

@oliversalzburg
Copy link
Author

The stack trace is:

  1) Uncaught error outside test suite:
     Uncaught Error: Dynamic require of "child_process" is not supported
      at file:///home/runner/work/dcc-backend/dcc-backend/node_modules/@sentry/profiling-node/lib/index.mjs:12:9
      at node_modules/detect-libc/lib/detect-libc.js (file:///home/runner/work/dcc-backend/dcc-backend/node_modules/@sentry/profiling-node/lib/index.mjs:9512:24)
      at __require2 (file:///home/runner/work/dcc-backend/dcc-backend/node_modules/@sentry/profiling-node/lib/index.mjs:15:50)
      at file:///home/runner/work/dcc-backend/dcc-backend/node_modules/@sentry/profiling-node/lib/index.mjs:16572:34
      at ModuleJob.run (node:internal/modules/esm/module_job:192:25)

I looked at that, and the line in question seems to be an import in node_modules/detect-libc/lib/detect-libc.js. It looked like that is a result of the cjs variants of other @sentry modules being used.

In the bundle of of the profiling-node plugin, there are these suspicious imports:

// src/hubextensions.ts
var import_utils3 = __toESM(require_cjs());
var import_core = __toESM(require_cjs2());

Not sure why it would first wrap the CJS of @sentry/utils and @sentry/node, and then try to convert that back to ESM, when those modules already provide ESM exports. I also looked at the package.json of those modules, but couldn't make sense of it. :(

Hope you have more luck.

@JonasBa
Copy link
Member

JonasBa commented Jun 9, 2023

@oliversalzburg, mind trying to upgrade to 1.0.2 and letting me know if it works? There is a chance that our dependency here is not esm compatible...

@oliversalzburg
Copy link
Author

oliversalzburg commented Jun 9, 2023

With 1.0.2 I'm back to missing the .node files in the build output (looks like it's using require again), and there still seems to be CJS code from dependencies in the bundle too (like node_modules/@sentry/utils/cjs/worldwide.js) :(

The tests pass now. So that error is gone, but I'm surely back to MODULE_NOT_FOUND when I try to actually load the plugin at runtime.

@JonasBa
Copy link
Member

JonasBa commented Jun 9, 2023

Ok, will revert that change then, ESM with native modules is just a mine field of edge cases and it seems we wont be able to make all of them work. In this particular case, it seems that .node files just cannot be loaded via imports in esm in sveltekit. I will revert that change

@JonasBa
Copy link
Member

JonasBa commented Jun 12, 2023

Reverted this in 1.0.3

@JonasBa
Copy link
Member

JonasBa commented Jun 12, 2023

@oliversalzburg just a thought, but what happens if you import @sentry/profiling-node from @sentry/profiling-node/lib/index.js?

@oliversalzburg
Copy link
Author

@JonasBa Basically only results in other build errors:

✘ [ERROR] Could not resolve "@sentry/profiling-node/lib/index.js"

    ../packages/lib-lambda/output/sentry/SentryIntegration.mjs:2:37:
      2 │ import { ProfilingIntegration } from "@sentry/profiling-node/lib/index.js";
        ╵                                      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

  The path "./lib/index.js" is not exported by package "@sentry/profiling-node":

    ../node_modules/@sentry/profiling-node/package.json:12:13:
      12 │   "exports": {
         ╵              ^

  You can mark the path "@sentry/profiling-node/lib/index.js" as external to exclude it from the
  bundle, which will remove this error.

@oliversalzburg
Copy link
Author

So I'm looking at the index.js and index.mjs of the 1.0.3, and the code in the bundle pretty early contains // node_modules/@sentry/utils/cjs/is.js for both bundles. I wonder if this already points at a larger issue.

As @sentry/utils has ESM content, I would expect to see that in the bundle as well, right?

@oliversalzburg
Copy link
Author

I also tried going back to CJS bundle, just to see if that works, but that also leads to new errors:

TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".node" for /home/runner/work/dcc-backend/dcc-backend/node_modules/@sentry/profiling-node/lib/sentry_cpu_profiler-linux-x64-glibc-108.node
    at __node_internal_captureLargerStackTrace (node:internal/errors:490:5)
    at new NodeError (node:internal/errors:399:5)
    at Object.getFileProtocolModuleFormat [as file:] (node:internal/modules/esm/get_format:79:11)
    at defaultGetFormat (node:internal/modules/esm/get_format:121:38)
    at defaultLoad (node:internal/modules/esm/load:81:20)
    at nextLoad (node:internal/modules/esm/loader:163:28)
    at ESMLoader.load (node:internal/modules/esm/loader:605:26)
    at ESMLoader.moduleProvider (node:internal/modules/esm/loader:457:22)
    at new ModuleJob (node:internal/modules/esm/module_job:64:26)
    at #createModuleJob (node:internal/modules/esm/loader:480:17)
    at ESMLoader.getModuleJob (node:internal/modules/esm/loader:434:34) {
  code: 'ERR_UNKNOWN_FILE_EXTENSION'

When I'm building a CJS bundle, I don't understand why it will contain parts of the plugin code that uses import, but I don't even understand if that's the issue here.

Either way, I'll probably have to give up on Sentry profiling now, as this was supposed to be a quick task and I'm way over budget. Thanks for the efforts.

@JonasBa
Copy link
Member

JonasBa commented Jun 13, 2023

@oliversalzburg thanks for the contributions though. I'm considering just removing esm support from the bundle as the pain does not seem to be worth it in this case and there just seem to be edge case after edge case of issues. If you would still like to use sentry profiling, I would go with marking @sentry/profiling-node as an external library and installing it separately, that should work without any issues

@adesso-os
Copy link

I will likely come back to this at a later time, but the way our Lambda bundling/deployment works right now, setting things as external is not a full solution for us.

In general, we don't necessarily want to use ESM for the bundle, but the CJS approach also didn't work for us initially. We'll definitely come back to this, I just can't invest any more right now. Your efforts are appreciated nonetheless :)

@adesso-os
Copy link

Just FYI, with the latest release, I was also able to make profiling work on our project. I noticed something interesting though. I build all our lambdas in a single process. I just provide all the entrypoints and let esbuild write the output directory structure.

In that scenario, the .node files are only copied once to the root of the output directory, and the import statements will then reference that file with ../../../.. relative paths.

I resolved that with the plugin below, which also copies the file to the output root, but rewrites the import to be ./. Then I can copy the .node file into the root of the individual lambda output folder when the build completes.

// We want the profiling binary to reside at the root of each lambda folder.
// We ensure manually that the right plugin is bundled together with each lambda.
// This plugin ensures that the import paths in the build output refer to the
// correct locations.
const sentryProfilingImportPathPlugin = {
  name: "SentryProfilingImportPath",
  setup(build) {
    build.onResolve({ filter: /sentry_cpu_profiler.+\.node$/ }, async resolveArgs => {
      fs.mkdirSync(pathOutput, {
        recursive: true,
      });
      fs.copyFileSync(
        path.resolve(resolveArgs.resolveDir, resolveArgs.path),
        path.resolve(pathOutput, resolveArgs.path)
      );
      return { path: resolveArgs.path, external: true };
    });
  },
};

And then copying to all output directories at the end of the build:

// Try finding the right Sentry profiling plugin for our arch.
    const profilingPlugins = fs
      .readdirSync(pathOutput)
      .filter(file => file.match(/sentry_cpu_profiler/));
    const profilingPlugin = profilingPlugins.filter(file => file.match(new RegExp(arch)))[0];
    if (!profilingPlugin) {
      console.warn(
        `Cannot find matching Sentry profiling plugin for '${arch}'! Sentry profiling will not work in this build!`
      );
    } else {
      // Copy the plugin to all output directories.
      console.info(
        `Found '${profilingPlugins.length}' Sentry profiling plugin candidates in build output. Desired architecture is '${arch}'.`
      );
      console.info(
        ` + Choosing: '${profilingPlugin}' and associating with '${lambdaEntrypoints.length}' lambda outputs...`
      );
      for (const lambdaMain of lambdaEntrypoints) {
        fs.copyFileSync(
          path.resolve(pathOutput, profilingPlugin),
          path.resolve(path.dirname(lambdaMain.path), profilingPlugin)
        );
      }
    }

Maybe this helps someone else :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
💾 💾 installation 💾 💾 package fails to install or compile from source
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants