UNPKG

posthog-node

Version:
270 lines (217 loc) 9.16 kB
// Portions of this file are derived from getsentry/sentry-javascript by Software, Inc. dba Sentry // Licensed under the MIT License import { posix, sep, dirname } from 'path' import { StackFrame, StackLineParser, StackLineParserFn, StackParser } from './types' type GetModuleFn = (filename: string | undefined) => string | undefined // This was originally forked from https://github.com/csnover/TraceKit, and was largely // re-written as part of raven - js. // // This code was later copied to the JavaScript mono - repo and further modified and // refactored over the years. // Copyright (c) 2013 Onur Can Cakmak onur.cakmak@gmail.com and all TraceKit contributors. // // Permission is hereby granted, free of charge, to any person obtaining a copy of this // software and associated documentation files(the 'Software'), to deal in the Software // without restriction, including without limitation the rights to use, copy, modify, // merge, publish, distribute, sublicense, and / or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following // conditions: // // The above copyright notice and this permission notice shall be included in all copies // or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A // PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. const WEBPACK_ERROR_REGEXP = /\(error: (.*)\)/ const STACKTRACE_FRAME_LIMIT = 50 const UNKNOWN_FUNCTION = '?' /** Node Stack line parser */ export function node(getModule?: GetModuleFn): StackLineParserFn { const FILENAME_MATCH = /^\s*[-]{4,}$/ const FULL_MATCH = /at (?:async )?(?:(.+?)\s+\()?(?:(.+):(\d+):(\d+)?|([^)]+))\)?/ return (line: string) => { const lineMatch = line.match(FULL_MATCH) if (lineMatch) { 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] let methodStart = functionName.lastIndexOf('.') if (functionName[methodStart - 1] === '.') { methodStart-- } if (methodStart > 0) { object = functionName.slice(0, methodStart) method = functionName.slice(methodStart + 1) const objectEnd = object.indexOf('.Module') if (objectEnd > 0) { functionName = functionName.slice(objectEnd + 1) object = object.slice(0, objectEnd) } } typeName = undefined } if (method) { typeName = object methodName = method } if (method === '<anonymous>') { methodName = undefined functionName = undefined } if (functionName === undefined) { methodName = methodName || UNKNOWN_FUNCTION functionName = typeName ? `${typeName}.${methodName}` : methodName } let filename = lineMatch[2]?.startsWith('file://') ? lineMatch[2].slice(7) : lineMatch[2] const isNative = lineMatch[5] === 'native' // If it's a Windows path, trim the leading slash so that `/C:/foo` becomes `C:/foo` if (filename?.match(/\/[A-Z]:/)) { filename = filename.slice(1) } if (!filename && lineMatch[5] && !isNative) { filename = lineMatch[5] } return { filename: filename ? decodeURI(filename) : undefined, module: getModule ? getModule(filename) : undefined, function: functionName, lineno: _parseIntOrUndefined(lineMatch[3]), colno: _parseIntOrUndefined(lineMatch[4]), in_app: filenameIsInApp(filename || '', isNative), platform: 'node:javascript', } } if (line.match(FILENAME_MATCH)) { return { filename: line, platform: 'node:javascript', } } return undefined } } /** * Does this filename look like it's part of the app code? */ export function filenameIsInApp(filename: string, isNative: boolean = false): boolean { const isInternal = isNative || (filename && // It's not internal if it's an absolute linux path !filename.startsWith('/') && // It's not internal if it's an absolute windows path !filename.match(/^[A-Z]:/) && // It's not internal if the path is starting with a dot !filename.startsWith('.') && // It's not internal if the frame has a protocol. In node, this is usually the case if the file got pre-processed with a bundler like webpack !filename.match(/^[a-zA-Z]([a-zA-Z0-9.\-+])*:\/\//)) // Schema from: https://stackoverflow.com/a/3641782 // 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 return !isInternal && filename !== undefined && !filename.includes('node_modules/') } function _parseIntOrUndefined(input: string | undefined): number | undefined { return parseInt(input || '', 10) || undefined } export function nodeStackLineParser(getModule?: GetModuleFn): StackLineParser { return [90, node(getModule)] } export const defaultStackParser: StackParser = createStackParser(nodeStackLineParser(createGetModuleFromFilename())) /** Creates a function that gets the module name from a filename */ export function createGetModuleFromFilename( basePath: string = process.argv[1] ? dirname(process.argv[1]) : process.cwd(), isWindows: boolean = sep === '\\' ): (filename: string | undefined) => string | undefined { const normalizedBase = isWindows ? normalizeWindowsPath(basePath) : basePath return (filename: string | undefined) => { if (!filename) { return } const normalizedFilename = isWindows ? normalizeWindowsPath(filename) : filename // eslint-disable-next-line prefer-const let { dir, base: file, ext } = posix.parse(normalizedFilename) if (ext === '.js' || ext === '.mjs' || ext === '.cjs') { file = file.slice(0, ext.length * -1) } // The file name might be URI-encoded which we want to decode to // the original file name. const decodedFile = decodeURIComponent(file) if (!dir) { // No dirname whatsoever dir = '.' } const n = dir.lastIndexOf('/node_modules') if (n > -1) { return `${dir.slice(n + 14).replace(/\//g, '.')}:${decodedFile}` } // 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 if (dir.startsWith(normalizedBase)) { const moduleName = dir.slice(normalizedBase.length + 1).replace(/\//g, '.') return moduleName ? `${moduleName}:${decodedFile}` : decodedFile } return decodedFile } } /** normalizes Windows paths */ function normalizeWindowsPath(path: string): string { return path .replace(/^[A-Z]:/, '') // remove Windows-style prefix .replace(/\\/g, '/') // replace all `\` instances with `/` } export function createStackParser(...parsers: StackLineParser[]): StackParser { const sortedParsers = parsers.sort((a, b) => a[0] - b[0]).map((p) => p[1]) return (stack: string, skipFirstLines: number = 0): StackFrame[] => { const frames: StackFrame[] = [] const lines = stack.split('\n') for (let i = skipFirstLines; i < lines.length; i++) { const line = lines[i] as string // Ignore lines over 1kb as they are unlikely to be stack frames. if (line.length > 1024) { continue } // https://github.com/getsentry/sentry-javascript/issues/5459 // Remove webpack (error: *) wrappers const cleanedLine = WEBPACK_ERROR_REGEXP.test(line) ? line.replace(WEBPACK_ERROR_REGEXP, '$1') : line // https://github.com/getsentry/sentry-javascript/issues/7813 // Skip Error: lines if (cleanedLine.match(/\S*Error: /)) { continue } for (const parser of sortedParsers) { const frame = parser(cleanedLine) if (frame) { frames.push(frame) break } } if (frames.length >= STACKTRACE_FRAME_LIMIT) { break } } return reverseAndStripFrames(frames) } } export function reverseAndStripFrames(stack: ReadonlyArray<StackFrame>): StackFrame[] { if (!stack.length) { return [] } const localStack = Array.from(stack) localStack.reverse() return localStack.slice(0, STACKTRACE_FRAME_LIMIT).map((frame) => ({ ...frame, filename: frame.filename || getLastStackFrame(localStack).filename, function: frame.function || UNKNOWN_FUNCTION, })) } function getLastStackFrame(arr: StackFrame[]): StackFrame { return arr[arr.length - 1] || {} }