posthog-node
Version:
PostHog Node.js integration
270 lines (217 loc) • 9.16 kB
text/typescript
// 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] || {}
}