sentry-javascript icon indicating copy to clipboard operation
sentry-javascript copied to clipboard

Cannot find module '.prisma/client/default'

Open P4sca1 opened this issue 1 year ago • 16 comments
trafficstars

Is there an existing issue for this?

  • [X] I have checked for existing issues https://github.com/getsentry/sentry-javascript/issues
  • [X] I have reviewed the documentation https://docs.sentry.io/
  • [X] I am using the latest SDK release https://github.com/getsentry/sentry-javascript/releases

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/[email protected]/node_modules/import-in-the-middle/lib/get-exports.js
- /Users/pascal/code/ips-hosting/api/node_modules/.pnpm/[email protected]/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/[email protected]/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/[email protected]/node_modules/import-in-the-middle/lib/get-exports.js:23:40)
    at getExports (/Users/pascal/code/ips-hosting/api/node_modules/.pnpm/[email protected]/node_modules/import-in-the-middle/lib/get-exports.js:83:12)
    at async processModule (/Users/pascal/code/ips-hosting/api/node_modules/.pnpm/[email protected]/node_modules/import-in-the-middle/hook.js:134:23)
    at async getSource (/Users/pascal/code/ips-hosting/api/node_modules/.pnpm/[email protected]/node_modules/import-in-the-middle/hook.js:269:60)
    at async load (/Users/pascal/code/ips-hosting/api/node_modules/.pnpm/[email protected]/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/[email protected]/node_modules/import-in-the-middle/lib/get-exports.js',
    '/Users/pascal/code/ips-hosting/api/node_modules/.pnpm/[email protected]/node_modules/import-in-the-middle/hook.js'
  ]
}

P4sca1 avatar Jun 03 '24 11:06 P4sca1

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

AbhiPrasad avatar Jun 03 '24 13:06 AbhiPrasad

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": {
			"[email protected]": "patches/[email protected]"
		}
	}

P4sca1 avatar Jun 03 '24 18:06 P4sca1

Here is how I import prisma:

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

P4sca1 avatar Jun 03 '24 18:06 P4sca1

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.

AbhiPrasad avatar Jun 03 '24 23:06 AbhiPrasad

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'

timfish avatar Jun 04 '24 09:06 timfish

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.

P4sca1 avatar Jun 04 '24 10:06 P4sca1

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 avatar Jun 04 '24 10:06 timfish

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

Working on a fix!

timfish avatar Jun 04 '24 12:06 timfish

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! :)

P4sca1 avatar Jun 05 '24 08:06 P4sca1

I've just tested import { PrismaClient } from '@prisma/client' directly with [email protected] 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?

timfish avatar Jun 05 '24 11:06 timfish

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": "[email protected]",
	"pnpm": {
		"overrides": {
			"import-in-the-middle": "1.8.0"
		}
	}
}

P4sca1 avatar Jun 05 '24 14:06 P4sca1

@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.

P4sca1 avatar Jun 07 '24 12:06 P4sca1

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 avatar Jun 12 '24 11:06 timfish

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!

timfish avatar Jun 15 '24 12:06 timfish

The issue persists with [email protected]. I updated my reproduction with the latest version.

P4sca1 avatar Jun 26 '24 02:06 P4sca1

This is still waiting for this PR to be merged.

timfish avatar Jun 26 '24 09:06 timfish

With the newest release of import-in-the-middle v1.9.0 this should be fixed.

If you upgrade to a fresh install of the latest version of the Node SDK it should use [email protected] by default.

AbhiPrasad avatar Jul 08 '24 18:07 AbhiPrasad