Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ref(node): Allow node stack parser to work in browser context #5135

Merged
merged 3 commits into from
May 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not export nodeStackLineParser at the top level?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we export from the top level and then import from there, will we not hit issues with bundlers trying to target the browser?

I just have painful memories of bundlers pulling in everything and erroring before they've even had a chance to do the treeshaking magic and remove the non-compatible code.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a fair point - maybe we just stick with this for now then.

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