Skip to content

Commit

Permalink
ref(node): Allow node stack parser to work in browser context (#5135)
Browse files Browse the repository at this point in the history
  • Loading branch information
timfish authored and AbhiPrasad committed May 30, 2022
1 parent 8fcf3ff commit 31be2be
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 107 deletions.
3 changes: 1 addition & 2 deletions packages/node/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,8 @@ export {

export { NodeClient } from './client';
export { makeNodeTransport } from './transports';
export { defaultIntegrations, init, lastEventId, flush, close, getSentryRelease } from './sdk';
export { defaultIntegrations, init, defaultStackParser, lastEventId, flush, close, getSentryRelease } from './sdk';
export { deepReadDirSync } from './utils';
export { defaultStackParser } from './stack-parser';

import { Integrations as CoreIntegrations } from '@sentry/core';
import { getMainCarrier } from '@sentry/hub';
Expand Down
36 changes: 36 additions & 0 deletions packages/node/src/module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { basename, dirname } from '@sentry/utils';

/** Gets the module from a filename */
export function getModule(filename: string | undefined): string | undefined {
if (!filename) {
return;
}

// We could use optional chaining here but webpack does like that mixed with require
const base = `${
(require && require.main && require.main.filename && dirname(require.main.filename)) || global.process.cwd()
}/`;

// It's specifically a module
const file = basename(filename, '.js');

const path = dirname(filename);
let n = path.lastIndexOf('/node_modules/');
if (n > -1) {
// /node_modules/ is 14 chars
return `${path.substr(n + 14).replace(/\//g, '.')}:${file}`;
}
// Let's see if it's a part of the main module
// To be a part of main module, it has to share the same base
n = `${path}/`.lastIndexOf(base, 0);

if (n === 0) {
let moduleName = path.substr(base.length).replace(/\//g, '.');
if (moduleName) {
moduleName += ':';
}
moduleName += file;
return moduleName;
}
return file;
}
10 changes: 7 additions & 3 deletions packages/node/src/sdk.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { getCurrentHub, getIntegrationsToSetup, initAndBind, Integrations as CoreIntegrations } from '@sentry/core';
import { getMainCarrier, setHubOnCarrier } from '@sentry/hub';
import { SessionStatus } from '@sentry/types';
import { getGlobalObject, logger, stackParserFromStackParserOptions } from '@sentry/utils';
import { SessionStatus, StackParser } from '@sentry/types';
import { createStackParser, getGlobalObject, logger, stackParserFromStackParserOptions } from '@sentry/utils';
import * as domain from 'domain';

import { NodeClient } from './client';
import { IS_DEBUG_BUILD } from './flags';
import { Console, ContextLines, Http, LinkedErrors, OnUncaughtException, OnUnhandledRejection } from './integrations';
import { defaultStackParser } from './stack-parser';
import { getModule } from './module';
import { nodeStackLineParser } from './stack-parser';
import { makeNodeTransport } from './transports';
import { NodeClientOptions, NodeOptions } from './types';

Expand Down Expand Up @@ -232,6 +233,9 @@ export function getSentryRelease(fallback?: string): string | undefined {
);
}

/** Node.js stack parser */
export const defaultStackParser: StackParser = createStackParser(nodeStackLineParser(getModule));

/**
* Enable automatic Session Tracking for the node process.
*/
Expand Down
166 changes: 68 additions & 98 deletions packages/node/src/stack-parser.ts
Original file line number Diff line number Diff line change
@@ -1,119 +1,89 @@
import { StackLineParser, StackLineParserFn } from '@sentry/types';
import { basename, createStackParser, dirname } from '@sentry/utils';

/** Gets the module */
function getModule(filename: string | undefined): string | undefined {
if (!filename) {
return;
}

// We could use optional chaining here but webpack does like that mixed with require
const base = `${
(require && require.main && require.main.filename && dirname(require.main.filename)) || global.process.cwd()
}/`;

// It's specifically a module
const file = basename(filename, '.js');

const path = dirname(filename);
let n = path.lastIndexOf('/node_modules/');
if (n > -1) {
// /node_modules/ is 14 chars
return `${path.substr(n + 14).replace(/\//g, '.')}:${file}`;
}
// Let's see if it's a part of the main module
// To be a part of main module, it has to share the same base
n = `${path}/`.lastIndexOf(base, 0);

if (n === 0) {
let moduleName = path.substr(base.length).replace(/\//g, '.');
if (moduleName) {
moduleName += ':';
}
moduleName += file;
return moduleName;
}
return file;
}

const FILENAME_MATCH = /^\s*[-]{4,}$/;
const FULL_MATCH = /at (?:async )?(?:(.+?)\s+\()?(?:(.+?):(\d+)(?::(\d+))?|([^)]+))\)?/;

type GetModuleFn = (filename: string | undefined) => string | undefined;

// eslint-disable-next-line complexity
const node: StackLineParserFn = (line: string) => {
if (line.match(FILENAME_MATCH)) {
return {
filename: line,
};
}
function node(getModule?: GetModuleFn): StackLineParserFn {
// eslint-disable-next-line complexity
return (line: string) => {
if (line.match(FILENAME_MATCH)) {
return {
filename: line,
};
}

const lineMatch = line.match(FULL_MATCH);
if (!lineMatch) {
return undefined;
}
const lineMatch = line.match(FULL_MATCH);
if (!lineMatch) {
return undefined;
}

let object: string | undefined;
let method: string | undefined;
let functionName: string | undefined;
let typeName: string | undefined;
let methodName: string | undefined;
let object: string | undefined;
let method: string | undefined;
let functionName: string | undefined;
let typeName: string | undefined;
let methodName: string | undefined;

if (lineMatch[1]) {
functionName = lineMatch[1];
if (lineMatch[1]) {
functionName = lineMatch[1];

let methodStart = functionName.lastIndexOf('.');
if (functionName[methodStart - 1] === '.') {
// eslint-disable-next-line no-plusplus
methodStart--;
}
let methodStart = functionName.lastIndexOf('.');
if (functionName[methodStart - 1] === '.') {
// eslint-disable-next-line no-plusplus
methodStart--;
}

if (methodStart > 0) {
object = functionName.substr(0, methodStart);
method = functionName.substr(methodStart + 1);
const objectEnd = object.indexOf('.Module');
if (objectEnd > 0) {
functionName = functionName.substr(objectEnd + 1);
object = object.substr(0, objectEnd);
if (methodStart > 0) {
object = functionName.substr(0, methodStart);
method = functionName.substr(methodStart + 1);
const objectEnd = object.indexOf('.Module');
if (objectEnd > 0) {
functionName = functionName.substr(objectEnd + 1);
object = object.substr(0, objectEnd);
}
}
typeName = undefined;
}
typeName = undefined;
}

if (method) {
typeName = object;
methodName = method;
}
if (method) {
typeName = object;
methodName = method;
}

if (method === '<anonymous>') {
methodName = undefined;
functionName = undefined;
}
if (method === '<anonymous>') {
methodName = undefined;
functionName = undefined;
}

if (functionName === undefined) {
methodName = methodName || '<anonymous>';
functionName = typeName ? `${typeName}.${methodName}` : methodName;
}
if (functionName === undefined) {
methodName = methodName || '<anonymous>';
functionName = typeName ? `${typeName}.${methodName}` : methodName;
}

const filename = lineMatch[2]?.startsWith('file://') ? lineMatch[2].substr(7) : lineMatch[2];
const isNative = lineMatch[5] === 'native';
const isInternal =
isNative || (filename && !filename.startsWith('/') && !filename.startsWith('.') && filename.indexOf(':\\') !== 1);
const filename = lineMatch[2]?.startsWith('file://') ? lineMatch[2].substr(7) : lineMatch[2];
const isNative = lineMatch[5] === 'native';
const isInternal =
isNative || (filename && !filename.startsWith('/') && !filename.startsWith('.') && filename.indexOf(':\\') !== 1);

// in_app is all that's not an internal Node function or a module within node_modules
// note that isNative appears to return true even for node core libraries
// see https://github.com/getsentry/raven-node/issues/176
const in_app = !isInternal && filename !== undefined && !filename.includes('node_modules/');
// in_app is all that's not an internal Node function or a module within node_modules
// note that isNative appears to return true even for node core libraries
// see https://github.com/getsentry/raven-node/issues/176
const in_app = !isInternal && filename !== undefined && !filename.includes('node_modules/');

return {
filename,
module: getModule(filename),
function: functionName,
lineno: parseInt(lineMatch[3], 10) || undefined,
colno: parseInt(lineMatch[4], 10) || undefined,
in_app,
return {
filename,
module: getModule?.(filename),
function: functionName,
lineno: parseInt(lineMatch[3], 10) || undefined,
colno: parseInt(lineMatch[4], 10) || undefined,
in_app,
};
};
};

export const nodeStackLineParser: StackLineParser = [90, node];
}

export const defaultStackParser = createStackParser(nodeStackLineParser);
/** Node.js stack line parser */
export function nodeStackLineParser(getModule?: GetModuleFn): StackLineParser {
return [90, node(getModule)];
}
2 changes: 1 addition & 1 deletion packages/node/test/context-lines.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as fs from 'fs';

import { parseStackFrames } from '../src/eventbuilder';
import { ContextLines, resetFileContentCache } from '../src/integrations/contextlines';
import { defaultStackParser } from '../src/stack-parser';
import { defaultStackParser } from '../src/sdk';
import { getError } from './helper/error';

describe('ContextLines', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/node/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
Scope,
} from '../src';
import { ContextLines, LinkedErrors } from '../src/integrations';
import { defaultStackParser } from '../src/stack-parser';
import { defaultStackParser } from '../src/sdk';
import { getDefaultNodeClientOptions } from './helper/node-client-options';

jest.mock('@sentry/core', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/node/test/integrations/linkederrors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ExtendedError } from '@sentry/types';

import { Event, NodeClient } from '../../src';
import { LinkedErrors } from '../../src/integrations/linkederrors';
import { defaultStackParser as stackParser } from '../../src/stack-parser';
import { defaultStackParser as stackParser } from '../../src/sdk';
import { getDefaultNodeClientOptions } from '../helper/node-client-options';

let linkedErrors: any;
Expand Down
2 changes: 1 addition & 1 deletion packages/node/test/stacktrace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
*/

import { parseStackFrames } from '../src/eventbuilder';
import { defaultStackParser as stackParser } from '../src/stack-parser';
import { defaultStackParser as stackParser } from '../src/sdk';

function testBasic() {
return new Error('something went wrong');
Expand Down

0 comments on commit 31be2be

Please sign in to comment.