From 0ec9a6ec0719ee8d7b3fd14906ab2f4da1b42d20 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 19 Nov 2024 00:05:13 +0100 Subject: [PATCH 1/4] feat(node): Add a function to get the current execution context --- packages/node/src/utils/execution-context.ts | 69 ++++++++++++ .../node/test/utils/execution-context.test.ts | 103 ++++++++++++++++++ packages/node/tsconfig.json | 2 +- 3 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 packages/node/src/utils/execution-context.ts create mode 100644 packages/node/test/utils/execution-context.test.ts diff --git a/packages/node/src/utils/execution-context.ts b/packages/node/src/utils/execution-context.ts new file mode 100644 index 000000000000..c9c1f7602caa --- /dev/null +++ b/packages/node/src/utils/execution-context.ts @@ -0,0 +1,69 @@ +import type { StackParser } from '@sentry/types'; +import { resolve } from 'node:path'; + +export interface ProcessInterface { + execArgv: string[]; + argv: string[]; + cwd: () => string; +} + +export interface ProcessArgs { + appPath: string; + importPaths: string[]; + requirePaths: string[]; +} + +/** + * Parses the process arguments to determine the app path, import paths, and require paths. + */ +export function parseProcessPaths(proc: ProcessInterface): ProcessArgs { + const { execArgv, argv, cwd: getCwd } = proc; + const cwd = getCwd(); + const appPath = resolve(cwd, argv[1] || ''); + + const joinedArgs = execArgv.join(' '); + const importPaths = Array.from(joinedArgs.matchAll(/--import[ =](\S+)/g)).map(e => resolve(cwd, e[1] || '')); + const requirePaths = Array.from(joinedArgs.matchAll(/--require[ =](\S+)/g)).map(e => resolve(cwd, e[1] || '')); + + return { appPath, importPaths, requirePaths }; +} + +/** + * Gets the current execution context. + * + * `app` means this function was most likely called via the app entry point. + * `import` means this function was most likely called from an --import cli arg. + * `require` means this function was most likely called from a --require cli arg. + * `unknown` means we couldn't determine for sure. + */ +export function getExecutionContext( + stackParser: StackParser, + proc: ProcessInterface = process, +): 'import' | 'require' | 'app' | 'unknown' { + const filenames = stackParser(new Error().stack || '') + .map(f => f.filename) + .filter(Boolean) as string[]; + + const { appPath, importPaths, requirePaths } = parseProcessPaths(proc); + + const output = []; + + if (appPath && filenames.includes(appPath)) { + output.push('app'); + } + + if (importPaths.some(p => filenames.includes(p))) { + output.push('import'); + } + + if (requirePaths.some(p => filenames.includes(p))) { + output.push('require'); + } + + // We only only return anything other than 'unknown' if we only got one match. + if (output.length === 1) { + return output[0] as 'import' | 'require' | 'app'; + } + + return 'unknown'; +} diff --git a/packages/node/test/utils/execution-context.test.ts b/packages/node/test/utils/execution-context.test.ts new file mode 100644 index 000000000000..97be03fd5c60 --- /dev/null +++ b/packages/node/test/utils/execution-context.test.ts @@ -0,0 +1,103 @@ +import type { ProcessInterface, ProcessArgs } from '../../src/utils/execution-context'; +import { getExecutionContext, parseProcessPaths } from '../../src/utils/execution-context'; +import { defaultStackParser } from '../../src'; + +const PROCESS_ARG_TESTS: [ProcessInterface, ProcessArgs][] = [ + [ + { cwd: () => '/user/tim/docs', argv: ['/bin/node', 'app.js'], execArgv: ['--import', './something.js'] }, + { appPath: '/user/tim/docs/app.js', importPaths: ['/user/tim/docs/something.js'], requirePaths: [] }, + ], + [ + { + cwd: () => '/user/tim/docs', + argv: ['/bin/node', 'app.js'], + execArgv: ['--import', './something.js', '--import=./else.js'], + }, + { + appPath: '/user/tim/docs/app.js', + importPaths: ['/user/tim/docs/something.js', '/user/tim/docs/else.js'], + requirePaths: [], + }, + ], + [ + { + cwd: () => '/user/tim/docs', + argv: ['/bin/node', 'app.js'], + execArgv: ['--require', './something.js', '--import=else.js'], + }, + { + appPath: '/user/tim/docs/app.js', + importPaths: ['/user/tim/docs/else.js'], + requirePaths: ['/user/tim/docs/something.js'], + }, + ], + [ + { + cwd: () => '/user/tim/docs', + argv: ['/bin/node', 'app.js'], + execArgv: ['--require=here/something.js'], + }, + { + appPath: '/user/tim/docs/app.js', + importPaths: [], + requirePaths: ['/user/tim/docs/here/something.js'], + }, + ], +]; + +describe('getExecutionContext', () => { + it.each(PROCESS_ARG_TESTS)('parseProcessArgs', (input, output) => { + const result = parseProcessPaths(input); + expect(result).toStrictEqual(output); + }); + + it('app absolute', () => { + const ctx = getExecutionContext(defaultStackParser, { + cwd: () => __dirname, + argv: ['/bin/node', __filename], + execArgv: [], + }); + + expect(ctx).toEqual('app'); + }); + + it('app relative', () => { + const ctx = getExecutionContext(defaultStackParser, { + cwd: () => __dirname, + argv: ['/bin/node', 'execution-context.test.ts'], + execArgv: [], + }); + + expect(ctx).toEqual('app'); + }); + + it('import absolute', () => { + const ctx = getExecutionContext(defaultStackParser, { + cwd: () => __dirname, + argv: ['/bin/node', 'app.ts'], + execArgv: ['--import', __filename], + }); + + expect(ctx).toEqual('import'); + }); + + it('import relative', () => { + const ctx = getExecutionContext(defaultStackParser, { + cwd: () => __dirname, + argv: ['/bin/node', 'app.ts'], + execArgv: ['--import', './execution-context.test.ts'], + }); + + expect(ctx).toEqual('import'); + }); + + it('require relative', () => { + const ctx = getExecutionContext(defaultStackParser, { + cwd: () => __dirname, + argv: ['/bin/node', 'app.ts'], + execArgv: ['--require', './execution-context.test.ts'], + }); + + expect(ctx).toEqual('require'); + }); +}); diff --git a/packages/node/tsconfig.json b/packages/node/tsconfig.json index 8f38d240197e..b9683a850600 100644 --- a/packages/node/tsconfig.json +++ b/packages/node/tsconfig.json @@ -4,7 +4,7 @@ "include": ["src/**/*"], "compilerOptions": { - "lib": ["es2018"], + "lib": ["es2018", "es2020.string"], "module": "Node16" } } From 14d251254571d379fceb6175bb07d35cb8de0b49 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 19 Nov 2024 00:17:13 +0100 Subject: [PATCH 2/4] Rename --- .../{execution-context.ts => entry-point.ts} | 6 ++--- ...on-context.test.ts => entry-point.test.ts} | 22 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) rename packages/node/src/utils/{execution-context.ts => entry-point.ts} (96%) rename packages/node/test/utils/{execution-context.test.ts => entry-point.test.ts} (76%) diff --git a/packages/node/src/utils/execution-context.ts b/packages/node/src/utils/entry-point.ts similarity index 96% rename from packages/node/src/utils/execution-context.ts rename to packages/node/src/utils/entry-point.ts index c9c1f7602caa..597be697f96d 100644 --- a/packages/node/src/utils/execution-context.ts +++ b/packages/node/src/utils/entry-point.ts @@ -1,5 +1,5 @@ -import type { StackParser } from '@sentry/types'; import { resolve } from 'node:path'; +import type { StackParser } from '@sentry/types'; export interface ProcessInterface { execArgv: string[]; @@ -29,14 +29,14 @@ export function parseProcessPaths(proc: ProcessInterface): ProcessArgs { } /** - * Gets the current execution context. + * Gets the current entry point type. * * `app` means this function was most likely called via the app entry point. * `import` means this function was most likely called from an --import cli arg. * `require` means this function was most likely called from a --require cli arg. * `unknown` means we couldn't determine for sure. */ -export function getExecutionContext( +export function getEntryPointType( stackParser: StackParser, proc: ProcessInterface = process, ): 'import' | 'require' | 'app' | 'unknown' { diff --git a/packages/node/test/utils/execution-context.test.ts b/packages/node/test/utils/entry-point.test.ts similarity index 76% rename from packages/node/test/utils/execution-context.test.ts rename to packages/node/test/utils/entry-point.test.ts index 97be03fd5c60..ec1cc76d0f56 100644 --- a/packages/node/test/utils/execution-context.test.ts +++ b/packages/node/test/utils/entry-point.test.ts @@ -1,6 +1,6 @@ -import type { ProcessInterface, ProcessArgs } from '../../src/utils/execution-context'; -import { getExecutionContext, parseProcessPaths } from '../../src/utils/execution-context'; import { defaultStackParser } from '../../src'; +import type { ProcessArgs, ProcessInterface } from '../../src/utils/entry-point'; +import { getEntryPointType, parseProcessPaths } from '../../src/utils/entry-point'; const PROCESS_ARG_TESTS: [ProcessInterface, ProcessArgs][] = [ [ @@ -45,14 +45,14 @@ const PROCESS_ARG_TESTS: [ProcessInterface, ProcessArgs][] = [ ], ]; -describe('getExecutionContext', () => { +describe('getEntryPointType', () => { it.each(PROCESS_ARG_TESTS)('parseProcessArgs', (input, output) => { const result = parseProcessPaths(input); expect(result).toStrictEqual(output); }); it('app absolute', () => { - const ctx = getExecutionContext(defaultStackParser, { + const ctx = getEntryPointType(defaultStackParser, { cwd: () => __dirname, argv: ['/bin/node', __filename], execArgv: [], @@ -62,9 +62,9 @@ describe('getExecutionContext', () => { }); it('app relative', () => { - const ctx = getExecutionContext(defaultStackParser, { + const ctx = getEntryPointType(defaultStackParser, { cwd: () => __dirname, - argv: ['/bin/node', 'execution-context.test.ts'], + argv: ['/bin/node', 'entry-point.test.ts'], execArgv: [], }); @@ -72,7 +72,7 @@ describe('getExecutionContext', () => { }); it('import absolute', () => { - const ctx = getExecutionContext(defaultStackParser, { + const ctx = getEntryPointType(defaultStackParser, { cwd: () => __dirname, argv: ['/bin/node', 'app.ts'], execArgv: ['--import', __filename], @@ -82,20 +82,20 @@ describe('getExecutionContext', () => { }); it('import relative', () => { - const ctx = getExecutionContext(defaultStackParser, { + const ctx = getEntryPointType(defaultStackParser, { cwd: () => __dirname, argv: ['/bin/node', 'app.ts'], - execArgv: ['--import', './execution-context.test.ts'], + execArgv: ['--import', './entry-point.test.ts'], }); expect(ctx).toEqual('import'); }); it('require relative', () => { - const ctx = getExecutionContext(defaultStackParser, { + const ctx = getEntryPointType(defaultStackParser, { cwd: () => __dirname, argv: ['/bin/node', 'app.ts'], - execArgv: ['--require', './execution-context.test.ts'], + execArgv: ['--require', './entry-point.test.ts'], }); expect(ctx).toEqual('require'); From 1a1795952efab2c1b87a7821be0c14fe3fd5ad5b Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 19 Nov 2024 00:22:48 +0100 Subject: [PATCH 3/4] Stack parser doesn't need to be configurable --- packages/node/src/utils/entry-point.ts | 9 +++------ packages/node/test/utils/entry-point.test.ts | 10 +++++----- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/packages/node/src/utils/entry-point.ts b/packages/node/src/utils/entry-point.ts index 597be697f96d..e73130f1efff 100644 --- a/packages/node/src/utils/entry-point.ts +++ b/packages/node/src/utils/entry-point.ts @@ -1,5 +1,5 @@ import { resolve } from 'node:path'; -import type { StackParser } from '@sentry/types'; +import { defaultStackParser } from '../sdk/api'; export interface ProcessInterface { execArgv: string[]; @@ -36,11 +36,8 @@ export function parseProcessPaths(proc: ProcessInterface): ProcessArgs { * `require` means this function was most likely called from a --require cli arg. * `unknown` means we couldn't determine for sure. */ -export function getEntryPointType( - stackParser: StackParser, - proc: ProcessInterface = process, -): 'import' | 'require' | 'app' | 'unknown' { - const filenames = stackParser(new Error().stack || '') +export function getEntryPointType(proc: ProcessInterface = process): 'import' | 'require' | 'app' | 'unknown' { + const filenames = defaultStackParser(new Error().stack || '') .map(f => f.filename) .filter(Boolean) as string[]; diff --git a/packages/node/test/utils/entry-point.test.ts b/packages/node/test/utils/entry-point.test.ts index ec1cc76d0f56..7d19e31464ba 100644 --- a/packages/node/test/utils/entry-point.test.ts +++ b/packages/node/test/utils/entry-point.test.ts @@ -52,7 +52,7 @@ describe('getEntryPointType', () => { }); it('app absolute', () => { - const ctx = getEntryPointType(defaultStackParser, { + const ctx = getEntryPointType({ cwd: () => __dirname, argv: ['/bin/node', __filename], execArgv: [], @@ -62,7 +62,7 @@ describe('getEntryPointType', () => { }); it('app relative', () => { - const ctx = getEntryPointType(defaultStackParser, { + const ctx = getEntryPointType({ cwd: () => __dirname, argv: ['/bin/node', 'entry-point.test.ts'], execArgv: [], @@ -72,7 +72,7 @@ describe('getEntryPointType', () => { }); it('import absolute', () => { - const ctx = getEntryPointType(defaultStackParser, { + const ctx = getEntryPointType({ cwd: () => __dirname, argv: ['/bin/node', 'app.ts'], execArgv: ['--import', __filename], @@ -82,7 +82,7 @@ describe('getEntryPointType', () => { }); it('import relative', () => { - const ctx = getEntryPointType(defaultStackParser, { + const ctx = getEntryPointType({ cwd: () => __dirname, argv: ['/bin/node', 'app.ts'], execArgv: ['--import', './entry-point.test.ts'], @@ -92,7 +92,7 @@ describe('getEntryPointType', () => { }); it('require relative', () => { - const ctx = getEntryPointType(defaultStackParser, { + const ctx = getEntryPointType({ cwd: () => __dirname, argv: ['/bin/node', 'app.ts'], execArgv: ['--require', './entry-point.test.ts'], From 5fd5c94093e94f977426e9e02d6e1d0ef11f4798 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 19 Nov 2024 00:33:29 +0100 Subject: [PATCH 4/4] Lint --- packages/node/test/utils/entry-point.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/node/test/utils/entry-point.test.ts b/packages/node/test/utils/entry-point.test.ts index 7d19e31464ba..5013482bca46 100644 --- a/packages/node/test/utils/entry-point.test.ts +++ b/packages/node/test/utils/entry-point.test.ts @@ -1,4 +1,3 @@ -import { defaultStackParser } from '../../src'; import type { ProcessArgs, ProcessInterface } from '../../src/utils/entry-point'; import { getEntryPointType, parseProcessPaths } from '../../src/utils/entry-point';