|  | 
|  | 1 | +import { existsSync, readFileSync } from 'node:fs'; | 
|  | 2 | +import { dirname, join } from 'node:path'; | 
|  | 3 | +import type { IntegrationFn } from '@sentry/core'; | 
|  | 4 | +import { isCjs } from '../utils/commonjs'; | 
|  | 5 | + | 
|  | 6 | +type ModuleInfo = Record<string, string>; | 
|  | 7 | + | 
|  | 8 | +let moduleCache: ModuleInfo | undefined; | 
|  | 9 | + | 
|  | 10 | +const INTEGRATION_NAME = 'Modules'; | 
|  | 11 | + | 
|  | 12 | +declare const __SENTRY_SERVER_MODULES__: Record<string, string>; | 
|  | 13 | + | 
|  | 14 | +/** | 
|  | 15 | + * `__SENTRY_SERVER_MODULES__` can be replaced at build time with the modules loaded by the server. | 
|  | 16 | + * Right now, we leverage this in Next.js to circumvent the problem that we do not get access to these things at runtime. | 
|  | 17 | + */ | 
|  | 18 | +const SERVER_MODULES = typeof __SENTRY_SERVER_MODULES__ === 'undefined' ? {} : __SENTRY_SERVER_MODULES__; | 
|  | 19 | + | 
|  | 20 | +const _modulesIntegration = (() => { | 
|  | 21 | +  return { | 
|  | 22 | +    name: INTEGRATION_NAME, | 
|  | 23 | +    processEvent(event) { | 
|  | 24 | +      event.modules = { | 
|  | 25 | +        ...event.modules, | 
|  | 26 | +        ..._getModules(), | 
|  | 27 | +      }; | 
|  | 28 | + | 
|  | 29 | +      return event; | 
|  | 30 | +    }, | 
|  | 31 | +    getModules: _getModules, | 
|  | 32 | +  }; | 
|  | 33 | +}) satisfies IntegrationFn; | 
|  | 34 | + | 
|  | 35 | +/** | 
|  | 36 | + * Add node modules / packages to the event. | 
|  | 37 | + * For this, multiple sources are used: | 
|  | 38 | + * - They can be injected at build time into the __SENTRY_SERVER_MODULES__ variable (e.g. in Next.js) | 
|  | 39 | + * - They are extracted from the dependencies & devDependencies in the package.json file | 
|  | 40 | + * - They are extracted from the require.cache (CJS only) | 
|  | 41 | + */ | 
|  | 42 | +export const modulesIntegration = _modulesIntegration; | 
|  | 43 | + | 
|  | 44 | +function getRequireCachePaths(): string[] { | 
|  | 45 | +  try { | 
|  | 46 | +    return require.cache ? Object.keys(require.cache as Record<string, unknown>) : []; | 
|  | 47 | +  } catch (e) { | 
|  | 48 | +    return []; | 
|  | 49 | +  } | 
|  | 50 | +} | 
|  | 51 | + | 
|  | 52 | +/** Extract information about package.json modules */ | 
|  | 53 | +function collectModules(): ModuleInfo { | 
|  | 54 | +  return { | 
|  | 55 | +    ...SERVER_MODULES, | 
|  | 56 | +    ...getModulesFromPackageJson(), | 
|  | 57 | +    ...(isCjs() ? collectRequireModules() : {}), | 
|  | 58 | +  }; | 
|  | 59 | +} | 
|  | 60 | + | 
|  | 61 | +/** Extract information about package.json modules from require.cache */ | 
|  | 62 | +function collectRequireModules(): ModuleInfo { | 
|  | 63 | +  const mainPaths = require.main?.paths || []; | 
|  | 64 | +  const paths = getRequireCachePaths(); | 
|  | 65 | + | 
|  | 66 | +  // We start with the modules from package.json (if possible) | 
|  | 67 | +  // These may be overwritten by more specific versions from the require.cache | 
|  | 68 | +  const infos: ModuleInfo = {}; | 
|  | 69 | +  const seen = new Set<string>(); | 
|  | 70 | + | 
|  | 71 | +  paths.forEach(path => { | 
|  | 72 | +    let dir = path; | 
|  | 73 | + | 
|  | 74 | +    /** Traverse directories upward in the search of package.json file */ | 
|  | 75 | +    const updir = (): void | (() => void) => { | 
|  | 76 | +      const orig = dir; | 
|  | 77 | +      dir = dirname(orig); | 
|  | 78 | + | 
|  | 79 | +      if (!dir || orig === dir || seen.has(orig)) { | 
|  | 80 | +        return undefined; | 
|  | 81 | +      } | 
|  | 82 | +      if (mainPaths.indexOf(dir) < 0) { | 
|  | 83 | +        return updir(); | 
|  | 84 | +      } | 
|  | 85 | + | 
|  | 86 | +      const pkgfile = join(orig, 'package.json'); | 
|  | 87 | +      seen.add(orig); | 
|  | 88 | + | 
|  | 89 | +      if (!existsSync(pkgfile)) { | 
|  | 90 | +        return updir(); | 
|  | 91 | +      } | 
|  | 92 | + | 
|  | 93 | +      try { | 
|  | 94 | +        const info = JSON.parse(readFileSync(pkgfile, 'utf8')) as { | 
|  | 95 | +          name: string; | 
|  | 96 | +          version: string; | 
|  | 97 | +        }; | 
|  | 98 | +        infos[info.name] = info.version; | 
|  | 99 | +      } catch (_oO) { | 
|  | 100 | +        // no-empty | 
|  | 101 | +      } | 
|  | 102 | +    }; | 
|  | 103 | + | 
|  | 104 | +    updir(); | 
|  | 105 | +  }); | 
|  | 106 | + | 
|  | 107 | +  return infos; | 
|  | 108 | +} | 
|  | 109 | + | 
|  | 110 | +/** Fetches the list of modules and the versions loaded by the entry file for your node.js app. */ | 
|  | 111 | +function _getModules(): ModuleInfo { | 
|  | 112 | +  if (!moduleCache) { | 
|  | 113 | +    moduleCache = collectModules(); | 
|  | 114 | +  } | 
|  | 115 | +  return moduleCache; | 
|  | 116 | +} | 
|  | 117 | + | 
|  | 118 | +interface PackageJson { | 
|  | 119 | +  dependencies?: Record<string, string>; | 
|  | 120 | +  devDependencies?: Record<string, string>; | 
|  | 121 | +} | 
|  | 122 | + | 
|  | 123 | +function getPackageJson(): PackageJson { | 
|  | 124 | +  try { | 
|  | 125 | +    const filePath = join(process.cwd(), 'package.json'); | 
|  | 126 | +    const packageJson = JSON.parse(readFileSync(filePath, 'utf8')) as PackageJson; | 
|  | 127 | + | 
|  | 128 | +    return packageJson; | 
|  | 129 | +  } catch (e) { | 
|  | 130 | +    return {}; | 
|  | 131 | +  } | 
|  | 132 | +} | 
|  | 133 | + | 
|  | 134 | +function getModulesFromPackageJson(): ModuleInfo { | 
|  | 135 | +  const packageJson = getPackageJson(); | 
|  | 136 | + | 
|  | 137 | +  return { | 
|  | 138 | +    ...packageJson.dependencies, | 
|  | 139 | +    ...packageJson.devDependencies, | 
|  | 140 | +  }; | 
|  | 141 | +} | 
0 commit comments