getsentry / sentry-javascript

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

Cannot find module '.prisma/client/default' #12325

Open P4sca1 opened 1 month ago

P4sca1 commented 1 month ago

Is there an existing issue for this?

How do you use Sentry?

Sentry Saas (sentry.io)

Which SDK are you using?

@sentry/node

SDK Version

8.7.0

Framework Version

No response

Link to Sentry event

No response

SDK Setup

// Preloaded using node --import flag. Do not import code from other source files here.
// https://docs.sentry.io/platforms/javascript/guides/node/install/esm/
// @ts-check

import { dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import * as Sentry from '@sentry/node'
import { nodeProfilingIntegration } from '@sentry/profiling-node'

/**
 * The root directory of the application.
 * @see {@link https://docs.sentry.io/platforms/javascript/guides/node/configuration/integrations/rewriteframes/}
 */
const root = dirname(fileURLToPath(import.meta.url))

const env =
    process.env['SENTRY_ENV'] || process.env['NODE_ENV'] || 'development'
const version = process.env['PACKAGE_VERSION']

Sentry.init({
    dsn: 'redacted',
    release: version ? `api@${version}` : undefined,
    environment: env,
    integrations: [
        nodeProfilingIntegration(),
        Sentry.dedupeIntegration(),
        Sentry.rewriteFramesIntegration({ root }),
        Sentry.prismaIntegration(),
    ],
    beforeBreadcrumb: (breadcrumb) => {
        // Filter out writes to apollographql reporting API, because they are not helpful for error tracing
        // and spam breadcrumbs.
        if (
            breadcrumb.type === 'http' &&
            typeof breadcrumb.data?.['url'] === 'string' &&
            breadcrumb.data['url'].includes('api.apollographql.com')
        ) {
            return null
        }

        return breadcrumb
    },
    tracesSampleRate: 0.1,
    profilesSampleRate: 0.1,
})

Steps to Reproduce

I migrated my code to Sentry v8 and can no longer start my application. I start my app using node --import ./instrument.js ./dist/main.js. I use pnpm as my package manager.

Expected Result

Prisma v8 should work using ESM, pnpm, and prisma.

Actual Result

Error: Cannot find module '.prisma/client/default'
Require stack:
- /Users/pascal/code/ips-hosting/api/node_modules/.pnpm/import-in-the-middle@1.7.4/node_modules/import-in-the-middle/lib/get-exports.js
- /Users/pascal/code/ips-hosting/api/node_modules/.pnpm/import-in-the-middle@1.7.4/node_modules/import-in-the-middle/hook.js
    at Module._resolveFilename (node:internal/modules/cjs/loader:1048:15)
    at Function.resolve (node:internal/modules/helpers:136:19)
    at /Users/pascal/code/ips-hosting/api/node_modules/.pnpm/import-in-the-middle@1.7.4/node_modules/import-in-the-middle/lib/get-exports.js:26:33
    at Array.map (<anonymous>)
    at getFullCjsExports (/Users/pascal/code/ips-hosting/api/node_modules/.pnpm/import-in-the-middle@1.7.4/node_modules/import-in-the-middle/lib/get-exports.js:23:40)
    at getExports (/Users/pascal/code/ips-hosting/api/node_modules/.pnpm/import-in-the-middle@1.7.4/node_modules/import-in-the-middle/lib/get-exports.js:83:12)
    at async processModule (/Users/pascal/code/ips-hosting/api/node_modules/.pnpm/import-in-the-middle@1.7.4/node_modules/import-in-the-middle/hook.js:134:23)
    at async getSource (/Users/pascal/code/ips-hosting/api/node_modules/.pnpm/import-in-the-middle@1.7.4/node_modules/import-in-the-middle/hook.js:269:60)
    at async load (/Users/pascal/code/ips-hosting/api/node_modules/.pnpm/import-in-the-middle@1.7.4/node_modules/import-in-the-middle/hook.js:335:26)
    at async nextLoad (node:internal/modules/esm/hooks:833:22) {
  code: 'MODULE_NOT_FOUND',
  requireStack: [
    '/Users/pascal/code/ips-hosting/api/node_modules/.pnpm/import-in-the-middle@1.7.4/node_modules/import-in-the-middle/lib/get-exports.js',
    '/Users/pascal/code/ips-hosting/api/node_modules/.pnpm/import-in-the-middle@1.7.4/node_modules/import-in-the-middle/hook.js'
  ]
}
AbhiPrasad commented 1 month ago

This should be fixed by our import-in-the-middle fixes. @P4sca1 could you try using the patch we have to validate this: https://github.com/getsentry/sentry-javascript/issues/12242#issuecomment-2133993135

If the patch works, fix is released with https://github.com/open-telemetry/opentelemetry-js/pull/4745

P4sca1 commented 1 month ago

I tried to apply the following patch file using ppm, but still have the same issue.

diff --git a/hook.js b/hook.js
index 79a2e5b..9539e87 100644
--- a/hook.js
+++ b/hook.js
@@ -3,6 +3,7 @@
 // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2021 Datadog, Inc.

 const { randomBytes } = require('crypto')
+const { URL } = require('url')
 const specifiers = new Map()
 const isWin = process.platform === 'win32'

@@ -91,6 +92,24 @@ function isStarExportLine (line) {
   return /^\* from /.test(line)
 }

+function isBareSpecifier (specifier) {
+  // Relative and absolute paths are not bare specifiers.
+  if (
+    specifier.startsWith('.') ||
+    specifier.startsWith('/')) {
+    return false
+  }
+
+  // Valid URLs are not bare specifiers. (file:, http:, node:, etc.)
+  try {
+    // eslint-disable-next-line no-new
+    new URL(specifier)
+    return false
+  } catch (err) {
+    return true
+  }
+}
+
 /**
  * @typedef {object} ProcessedModule
  * @property {string[]} imports A set of ESM import lines to be added to the
@@ -128,6 +147,7 @@ async function processModule ({
   srcUrl,
   context,
   parentGetSource,
+  parentResolve,
   ns = 'namespace',
   defaultAs = 'default'
 }) {
@@ -154,13 +174,22 @@ async function processModule ({
     if (isStarExportLine(n) === true) {
       const [, modFile] = n.split('* from ')
       const normalizedModName = normalizeModName(modFile)
-      const modUrl = new URL(modFile, srcUrl).toString()
       const modName = Buffer.from(modFile, 'hex') + Date.now() + randomBytes(4).toString('hex')

+      let modUrl
+      if (isBareSpecifier(modFile)) {
+        // Bare specifiers need to be resolved relative to the parent module.
+        const result = await parentResolve(modFile, { parentURL: srcUrl })
+        modUrl = result.url
+      } else {
+        modUrl = new URL(modFile, srcUrl).toString()
+      }
+
       const data = await processModule({
         srcUrl: modUrl,
         context,
         parentGetSource,
+        parentResolve,
         ns: `$${modName}`,
         defaultAs: normalizedModName
       })
@@ -180,7 +209,7 @@ async function processModule ({
       // needs to utilize that new name while being initialized from the
       // corresponding origin namespace.
       const renamedExport = matches[2]
-      setters.set(`$${renamedExport}${ns}`, `
+      setters.set(`$${renamedExport}`, `
       let $${renamedExport} = ${ns}.default
       export { $${renamedExport} as ${renamedExport} }
       set.${renamedExport} = (v) => {
@@ -191,7 +220,7 @@ async function processModule ({
       continue
     }

-    setters.set(`$${n}` + ns, `
+    setters.set(`$${n}`, `
     let $${n} = ${ns}.${n}
     export { $${n} as ${n} }
     set.${n} = (v) => {
@@ -229,7 +258,19 @@ function addIitm (url) {
 }

 function createHook (meta) {
+  let cachedResolve
+  const iitmURL = new URL('lib/register.js', meta.url).toString()
+
   async function resolve (specifier, context, parentResolve) {
+    cachedResolve = parentResolve
+    // See github.com/DataDog/import-in-the-middle/pull/76.
+    if (specifier === iitmURL) {
+      return {
+        url: specifier,
+        shortCircuit: true
+      }
+    }
+
     const { parentURL = '' } = context
     const newSpecifier = deleteIitm(specifier)
     if (isWin && parentURL.indexOf('file:node') === 0) {
@@ -253,6 +294,15 @@ function createHook (meta) {
       return url
     }

+    // If the file is referencing itself, we need to skip adding the iitm search params
+    if (url.url === parentURL) {
+      return {
+        url: url.url,
+        shortCircuit: true,
+        format: url.format
+      }
+    }
+
     specifiers.set(url.url, specifier)

     return {
@@ -262,14 +312,14 @@ function createHook (meta) {
     }
   }

-  const iitmURL = new URL('lib/register.js', meta.url).toString()
   async function getSource (url, context, parentGetSource) {
     if (hasIitm(url)) {
       const realUrl = deleteIitm(url)
       const { imports, namespaces, setters: mapSetters } = await processModule({
         srcUrl: realUrl,
         context,
-        parentGetSource
+        parentGetSource,
+        parentResolve: cachedResolve
       })
       const setters = Array.from(mapSetters.values())
     "pnpm": {
        "patchedDependencies": {
            "import-in-the-middle@1.7.4": "patches/import-in-the-middle@1.7.4.patch"
        }
    }
P4sca1 commented 1 month ago

Here is how I import prisma:

const require = createRequire(import.meta.url)
const client = require('@prisma/client').PrismaClient as typeof PrismaClient
AbhiPrasad commented 4 weeks ago

Shoot. one last thing to try then, could you override import-in-the-middle to use 1.8.0 to see if that fixes it?

package.json

{
  "pnpm": {
    "overrides": {
      "import-in-the-middle": "^1.8.0",
    }
  }
}

if this doesn't work, it's def another bug we have to look into. We'll investigate and fix asap. Sorry for the trouble.

timfish commented 4 weeks ago

It looks like this might be a different bug caused by the CJS evaluation of import-in-the-middle. I found something similar but different here with the util-deprecate package.

@P4sca1 is there any reason you're doing this:

const require = createRequire(import.meta.url)
const client = require('@prisma/client').PrismaClient as typeof PrismaClient

Rather than this?

import { PrismaClient } from '@prisma/client'
P4sca1 commented 4 weeks ago

Prisma is a CJS package and does not support ESM yet. I think you can't just import CJS packages in an ESM environment, but I will try if it works that way.

timfish commented 4 weeks ago

you can't just import CJS packages in an ESM environment

import { X } from 'some-cjs-lib' and import * as lib from 'some-cjs-lib' is supported by Node but it can sometimes be tricky with certain kinds of default exports, especially when a bundler is being used.

It is require(esm) that is currently behind an experimental flag.

timfish commented 4 weeks ago

I think this is the cause: https://github.com/DataDog/import-in-the-middle/issues/95

Working on a fix!

P4sca1 commented 4 weeks ago

Importing Prisma using import { PrismaClient } from '@prisma/client' in an ESM environment works. The workaround using createRequire is not (or no longer?) required. The issue with import-in-the-middle persists, no matter how Prisma is imported. Thank you for working on a fix! :)

timfish commented 4 weeks ago

I've just tested import { PrismaClient } from '@prisma/client' directly with import-in-the-middle@1.8.0 and it handles it correctly without error.

Are you using TypeScript and transpiling to CommonJs? Is there anything else about your setup that might help me reproduce this?

P4sca1 commented 4 weeks ago

Here's my tsconfig.json:

{
    "extends": ["@tsconfig/node-lts", "@tsconfig/strictest"],
    "compilerOptions": {
        "outDir": "./dist",
        "rootDir": "./src",

        // Use ESM
        "module": "nodenext",
        "moduleResolution": "nodenext",

        // Use helpers from tslib, instead of inlining them
        "noEmitHelpers": true,
        "importHelpers": true,

        // Generate inline source maps
        "sourceMap": true,
        "inlineSources": true,
        "sourceRoot": "/",

        // Don not require exact optional property types. The generated prisma client does not work with it.
        "exactOptionalPropertyTypes": false
    },
    "include": ["src/**/*.ts", "./types/**/*.d.ts"]
}

I run pnpm run build && pnpm run start for testing. My (stripped down) package.json looks like this:

{
    "scripts": {
        "tsc:build": "tsc --project ./tsconfig.json",
        "build": "prisma generate && pnpm run tsc:build",
        "start": "node --import ./instrument.js ./dist/main.js",
    },
    "type": "module",
    "dependencies": {
        "@prisma/client": "5.14.0",
        "@sentry/node": "8.7.0",
        "@sentry/profiling-node": "8.7.0",
        "prisma": "5.14.0",
        "tslib": "2.6.2",
    },
    "devDependencies": {
        "@sentry/cli": "2.32.1",
        "@swc-node/register": "1.9.0",
        "@tsconfig/node-lts": "20.1.3",
        "@tsconfig/strictest": "2.0.5",
        "typescript": "5.4.5"
    },
    "packageManager": "pnpm@9.1.4",
    "pnpm": {
        "overrides": {
            "import-in-the-middle": "1.8.0"
        }
    }
}
P4sca1 commented 3 weeks ago

@timfish I created a minimal reproduction here: https://codesandbox.io/p/devbox/sentry-issue-12325-z9jfx7 Run node --import ./instrument.js index.js to reproduce the error.

timfish commented 3 weeks ago

Because prisma is CJS, I think this is fixed by https://github.com/DataDog/import-in-the-middle/pull/96 but I need to confirm

timfish commented 2 weeks ago

This should hopefully have been fixed by the numerous PRs recently merged at import-in-the-middle.

While we wait for this to be released, there is a patch available that combines all the fixes. If anyone can confirm this patch fixes this issue that would be super helpful!

P4sca1 commented 1 week ago

The issue persists with import-in-the-middle@1.8.1. I updated my reproduction with the latest version.

timfish commented 1 week ago

This is still waiting for this PR to be merged.