posthog-node 5.8.8 → 5.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{index.d.ts → client.d.ts} +7 -378
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +480 -0
- package/dist/client.mjs +436 -0
- package/dist/entrypoints/index.edge.d.ts +6 -0
- package/dist/entrypoints/index.edge.d.ts.map +1 -0
- package/dist/entrypoints/index.edge.js +96 -0
- package/dist/entrypoints/index.edge.mjs +19 -0
- package/dist/entrypoints/index.node.d.ts +6 -0
- package/dist/entrypoints/index.node.d.ts.map +1 -0
- package/dist/entrypoints/index.node.js +107 -0
- package/dist/entrypoints/index.node.mjs +24 -0
- package/dist/exports.d.ts +4 -0
- package/dist/exports.d.ts.map +1 -0
- package/dist/exports.js +78 -0
- package/dist/exports.mjs +3 -0
- package/dist/extensions/error-tracking/autocapture.d.ts +4 -0
- package/dist/extensions/error-tracking/autocapture.d.ts.map +1 -0
- package/dist/extensions/error-tracking/autocapture.js +68 -0
- package/dist/extensions/error-tracking/autocapture.mjs +31 -0
- package/dist/extensions/error-tracking/index.d.ts +19 -0
- package/dist/extensions/error-tracking/index.d.ts.map +1 -0
- package/dist/extensions/error-tracking/index.js +97 -0
- package/dist/extensions/error-tracking/index.mjs +63 -0
- package/dist/extensions/error-tracking/modifiers/context-lines.node.d.ts +5 -0
- package/dist/extensions/error-tracking/modifiers/context-lines.node.d.ts.map +1 -0
- package/dist/extensions/error-tracking/modifiers/context-lines.node.js +227 -0
- package/dist/extensions/error-tracking/modifiers/context-lines.node.mjs +187 -0
- package/dist/extensions/error-tracking/modifiers/module.node.d.ts +3 -0
- package/dist/extensions/error-tracking/modifiers/module.node.d.ts.map +1 -0
- package/dist/extensions/error-tracking/modifiers/module.node.js +64 -0
- package/dist/extensions/error-tracking/modifiers/module.node.mjs +30 -0
- package/dist/extensions/express.d.ts +17 -0
- package/dist/extensions/express.d.ts.map +1 -0
- package/dist/extensions/express.js +61 -0
- package/dist/extensions/express.mjs +17 -0
- package/dist/extensions/feature-flags/crypto-helpers.d.ts +3 -0
- package/dist/extensions/feature-flags/crypto-helpers.d.ts.map +1 -0
- package/dist/extensions/feature-flags/crypto-helpers.js +77 -0
- package/dist/extensions/feature-flags/crypto-helpers.mjs +22 -0
- package/dist/extensions/feature-flags/crypto.d.ts +2 -0
- package/dist/extensions/feature-flags/crypto.d.ts.map +1 -0
- package/dist/extensions/feature-flags/crypto.js +47 -0
- package/dist/extensions/feature-flags/crypto.mjs +13 -0
- package/dist/extensions/feature-flags/feature-flags.d.ts +89 -0
- package/dist/extensions/feature-flags/feature-flags.d.ts.map +1 -0
- package/dist/extensions/feature-flags/feature-flags.js +529 -0
- package/dist/extensions/feature-flags/feature-flags.mjs +483 -0
- package/dist/extensions/feature-flags/lazy.d.ts +24 -0
- package/dist/extensions/feature-flags/lazy.d.ts.map +1 -0
- package/dist/extensions/feature-flags/lazy.js +60 -0
- package/dist/extensions/feature-flags/lazy.mjs +26 -0
- package/dist/extensions/sentry-integration.d.ts +54 -0
- package/dist/extensions/sentry-integration.d.ts.map +1 -0
- package/dist/extensions/sentry-integration.js +113 -0
- package/dist/extensions/sentry-integration.mjs +73 -0
- package/dist/storage-memory.d.ts +7 -0
- package/dist/storage-memory.d.ts.map +1 -0
- package/dist/storage-memory.js +46 -0
- package/dist/storage-memory.mjs +12 -0
- package/dist/types.d.ts +253 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +18 -0
- package/dist/types.mjs +0 -0
- package/dist/utils/logger.d.ts +3 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +63 -0
- package/dist/utils/logger.mjs +29 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +36 -0
- package/dist/version.mjs +2 -0
- package/package.json +32 -31
- package/src/client.ts +1532 -0
- package/src/entrypoints/index.edge.ts +22 -0
- package/src/entrypoints/index.node.ts +26 -0
- package/src/exports.ts +3 -0
- package/src/extensions/error-tracking/autocapture.ts +67 -0
- package/src/extensions/error-tracking/index.ts +104 -0
- package/src/extensions/error-tracking/modifiers/context-lines.node.ts +404 -0
- package/src/extensions/error-tracking/modifiers/module.node.ts +68 -0
- package/src/extensions/express.ts +40 -0
- package/src/extensions/feature-flags/crypto-helpers.ts +36 -0
- package/src/extensions/feature-flags/crypto.ts +22 -0
- package/src/extensions/feature-flags/feature-flags.ts +1003 -0
- package/src/extensions/feature-flags/lazy.ts +55 -0
- package/src/extensions/sentry-integration.ts +216 -0
- package/src/storage-memory.ts +13 -0
- package/src/types.ts +294 -0
- package/src/utils/logger.ts +39 -0
- package/src/version.ts +1 -0
- package/dist/edge/index.cjs +0 -3150
- package/dist/edge/index.cjs.map +0 -1
- package/dist/edge/index.mjs +0 -3144
- package/dist/edge/index.mjs.map +0 -1
- package/dist/node/index.cjs +0 -3556
- package/dist/node/index.cjs.map +0 -1
- package/dist/node/index.mjs +0 -3550
- package/dist/node/index.mjs.map +0 -1
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export * from '../exports'
|
|
2
|
+
|
|
3
|
+
import ErrorTracking from '../extensions/error-tracking'
|
|
4
|
+
import { PostHogBackendClient } from '../client'
|
|
5
|
+
import { ErrorTracking as CoreErrorTracking } from '@posthog/core'
|
|
6
|
+
|
|
7
|
+
ErrorTracking.errorPropertiesBuilder = new CoreErrorTracking.ErrorPropertiesBuilder(
|
|
8
|
+
[
|
|
9
|
+
new CoreErrorTracking.EventCoercer(),
|
|
10
|
+
new CoreErrorTracking.ErrorCoercer(),
|
|
11
|
+
new CoreErrorTracking.ObjectCoercer(),
|
|
12
|
+
new CoreErrorTracking.StringCoercer(),
|
|
13
|
+
new CoreErrorTracking.PrimitiveCoercer(),
|
|
14
|
+
],
|
|
15
|
+
[CoreErrorTracking.nodeStackLineParser]
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
export class PostHog extends PostHogBackendClient {
|
|
19
|
+
getLibraryId(): string {
|
|
20
|
+
return 'posthog-edge'
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export * from '../exports'
|
|
2
|
+
|
|
3
|
+
import { createModulerModifier } from '../extensions/error-tracking/modifiers/module.node'
|
|
4
|
+
import { addSourceContext } from '../extensions/error-tracking/modifiers/context-lines.node'
|
|
5
|
+
import ErrorTracking from '../extensions/error-tracking'
|
|
6
|
+
|
|
7
|
+
import { PostHogBackendClient } from '../client'
|
|
8
|
+
import { ErrorTracking as CoreErrorTracking } from '@posthog/core'
|
|
9
|
+
|
|
10
|
+
ErrorTracking.errorPropertiesBuilder = new CoreErrorTracking.ErrorPropertiesBuilder(
|
|
11
|
+
[
|
|
12
|
+
new CoreErrorTracking.EventCoercer(),
|
|
13
|
+
new CoreErrorTracking.ErrorCoercer(),
|
|
14
|
+
new CoreErrorTracking.ObjectCoercer(),
|
|
15
|
+
new CoreErrorTracking.StringCoercer(),
|
|
16
|
+
new CoreErrorTracking.PrimitiveCoercer(),
|
|
17
|
+
],
|
|
18
|
+
[CoreErrorTracking.nodeStackLineParser],
|
|
19
|
+
[createModulerModifier(), addSourceContext]
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
export class PostHog extends PostHogBackendClient {
|
|
23
|
+
getLibraryId(): string {
|
|
24
|
+
return 'posthog-node'
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/exports.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Portions of this file are derived from getsentry/sentry-javascript by Software, Inc. dba Sentry
|
|
2
|
+
// Licensed under the MIT License
|
|
3
|
+
|
|
4
|
+
import { ErrorTracking as CoreErrorTracking } from '@posthog/core'
|
|
5
|
+
|
|
6
|
+
type ErrorHandler = { _posthogErrorHandler: boolean } & ((error: Error) => void)
|
|
7
|
+
|
|
8
|
+
function makeUncaughtExceptionHandler(
|
|
9
|
+
captureFn: (exception: Error, hint: CoreErrorTracking.EventHint) => void,
|
|
10
|
+
onFatalFn: (exception: Error) => void
|
|
11
|
+
): ErrorHandler {
|
|
12
|
+
let calledFatalError: boolean = false
|
|
13
|
+
|
|
14
|
+
return Object.assign(
|
|
15
|
+
(error: Error): void => {
|
|
16
|
+
// Attaching a listener to `uncaughtException` will prevent the node process from exiting. We generally do not
|
|
17
|
+
// want to alter this behaviour so we check for other listeners that users may have attached themselves and adjust
|
|
18
|
+
// exit behaviour of the SDK accordingly:
|
|
19
|
+
// - If other listeners are attached, do not exit.
|
|
20
|
+
// - If the only listener attached is ours, exit.
|
|
21
|
+
const userProvidedListenersCount = global.process.listeners('uncaughtException').filter((listener) => {
|
|
22
|
+
// There are 2 listeners we ignore:
|
|
23
|
+
return (
|
|
24
|
+
// as soon as we're using domains this listener is attached by node itself
|
|
25
|
+
listener.name !== 'domainUncaughtExceptionClear' &&
|
|
26
|
+
// the handler we register in this integration
|
|
27
|
+
(listener as ErrorHandler)._posthogErrorHandler !== true
|
|
28
|
+
)
|
|
29
|
+
}).length
|
|
30
|
+
|
|
31
|
+
const processWouldExit = userProvidedListenersCount === 0
|
|
32
|
+
|
|
33
|
+
captureFn(error, {
|
|
34
|
+
mechanism: {
|
|
35
|
+
type: 'onuncaughtexception',
|
|
36
|
+
handled: false,
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
if (!calledFatalError && processWouldExit) {
|
|
41
|
+
calledFatalError = true
|
|
42
|
+
onFatalFn(error)
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
{ _posthogErrorHandler: true }
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function addUncaughtExceptionListener(
|
|
50
|
+
captureFn: (exception: Error, hint: CoreErrorTracking.EventHint) => void,
|
|
51
|
+
onFatalFn: (exception: Error) => void
|
|
52
|
+
): void {
|
|
53
|
+
global.process.on('uncaughtException', makeUncaughtExceptionHandler(captureFn, onFatalFn))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function addUnhandledRejectionListener(
|
|
57
|
+
captureFn: (exception: unknown, hint: CoreErrorTracking.EventHint) => void
|
|
58
|
+
): void {
|
|
59
|
+
global.process.on('unhandledRejection', (reason: unknown) => {
|
|
60
|
+
return captureFn(reason, {
|
|
61
|
+
mechanism: {
|
|
62
|
+
type: 'onunhandledrejection',
|
|
63
|
+
handled: false,
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { addUncaughtExceptionListener, addUnhandledRejectionListener } from './autocapture'
|
|
2
|
+
import { PostHogBackendClient } from '@/client'
|
|
3
|
+
import { uuidv7 } from '@posthog/core'
|
|
4
|
+
import { EventMessage, PostHogOptions } from '@/types'
|
|
5
|
+
import type { Logger } from '@posthog/core'
|
|
6
|
+
import { BucketedRateLimiter } from '@posthog/core'
|
|
7
|
+
import { ErrorTracking as CoreErrorTracking } from '@posthog/core'
|
|
8
|
+
|
|
9
|
+
const SHUTDOWN_TIMEOUT = 2000
|
|
10
|
+
|
|
11
|
+
export default class ErrorTracking {
|
|
12
|
+
private client: PostHogBackendClient
|
|
13
|
+
private _exceptionAutocaptureEnabled: boolean
|
|
14
|
+
private _rateLimiter: BucketedRateLimiter<string>
|
|
15
|
+
private _logger: Logger
|
|
16
|
+
|
|
17
|
+
static errorPropertiesBuilder: CoreErrorTracking.ErrorPropertiesBuilder
|
|
18
|
+
|
|
19
|
+
constructor(client: PostHogBackendClient, options: PostHogOptions, _logger: Logger) {
|
|
20
|
+
this.client = client
|
|
21
|
+
this._exceptionAutocaptureEnabled = options.enableExceptionAutocapture || false
|
|
22
|
+
this._logger = _logger
|
|
23
|
+
|
|
24
|
+
// by default captures ten exceptions before rate limiting by exception type
|
|
25
|
+
// refills at a rate of one token / 10 second period
|
|
26
|
+
// e.g. will capture 1 exception rate limited exception every 10 seconds until burst ends
|
|
27
|
+
this._rateLimiter = new BucketedRateLimiter({
|
|
28
|
+
refillRate: 1,
|
|
29
|
+
bucketSize: 10,
|
|
30
|
+
refillInterval: 10000, // ten seconds in milliseconds
|
|
31
|
+
_logger: this._logger,
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
this.startAutocaptureIfEnabled()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
static async buildEventMessage(
|
|
38
|
+
error: unknown,
|
|
39
|
+
hint: CoreErrorTracking.EventHint,
|
|
40
|
+
distinctId?: string,
|
|
41
|
+
additionalProperties?: Record<string | number, any>
|
|
42
|
+
): Promise<EventMessage> {
|
|
43
|
+
const properties: EventMessage['properties'] = { ...additionalProperties }
|
|
44
|
+
|
|
45
|
+
// Given stateless nature of Node SDK we capture exceptions using personless processing when no
|
|
46
|
+
// user can be determined because a distinct_id is not provided e.g. exception autocapture
|
|
47
|
+
if (!distinctId) {
|
|
48
|
+
properties.$process_person_profile = false
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const exceptionProperties = this.errorPropertiesBuilder.buildFromUnknown(error, hint)
|
|
52
|
+
exceptionProperties.$exception_list = await this.errorPropertiesBuilder.modifyFrames(
|
|
53
|
+
exceptionProperties.$exception_list
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
event: '$exception',
|
|
58
|
+
distinctId: distinctId || uuidv7(),
|
|
59
|
+
properties: {
|
|
60
|
+
...exceptionProperties,
|
|
61
|
+
...properties,
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private startAutocaptureIfEnabled(): void {
|
|
67
|
+
if (this.isEnabled()) {
|
|
68
|
+
addUncaughtExceptionListener(this.onException.bind(this), this.onFatalError.bind(this))
|
|
69
|
+
addUnhandledRejectionListener(this.onException.bind(this))
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private onException(exception: unknown, hint: CoreErrorTracking.EventHint): void {
|
|
74
|
+
this.client.addPendingPromise(
|
|
75
|
+
(async () => {
|
|
76
|
+
const eventMessage = await ErrorTracking.buildEventMessage(exception, hint)
|
|
77
|
+
const exceptionProperties = eventMessage.properties
|
|
78
|
+
const exceptionType = exceptionProperties?.$exception_list[0]?.type ?? 'Exception'
|
|
79
|
+
const isRateLimited = this._rateLimiter.consumeRateLimit(exceptionType)
|
|
80
|
+
if (isRateLimited) {
|
|
81
|
+
this._logger.info('Skipping exception capture because of client rate limiting.', {
|
|
82
|
+
exception: exceptionType,
|
|
83
|
+
})
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
return this.client.capture(eventMessage)
|
|
87
|
+
})()
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private async onFatalError(exception: Error): Promise<void> {
|
|
92
|
+
console.error(exception)
|
|
93
|
+
await this.client.shutdown(SHUTDOWN_TIMEOUT)
|
|
94
|
+
process.exit(1)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
isEnabled(): boolean {
|
|
98
|
+
return !this.client.isDisabled && this._exceptionAutocaptureEnabled
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
shutdown(): void {
|
|
102
|
+
this._rateLimiter.stop()
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
// Portions of this file are derived from getsentry/sentry-javascript by Software, Inc. dba Sentry
|
|
2
|
+
// Licensed under the MIT License
|
|
3
|
+
|
|
4
|
+
import { ErrorTracking as CoreErrorTracking } from '@posthog/core'
|
|
5
|
+
import { createReadStream } from 'node:fs'
|
|
6
|
+
import { createInterface } from 'node:readline'
|
|
7
|
+
|
|
8
|
+
const LRU_FILE_CONTENTS_CACHE = new CoreErrorTracking.ReduceableCache<string, Record<number, string>>(25)
|
|
9
|
+
const LRU_FILE_CONTENTS_FS_READ_FAILED = new CoreErrorTracking.ReduceableCache<string, 1>(20)
|
|
10
|
+
const DEFAULT_LINES_OF_CONTEXT = 7
|
|
11
|
+
// Determines the upper bound of lineno/colno that we will attempt to read. Large colno values are likely to be
|
|
12
|
+
// minified code while large lineno values are likely to be bundled code.
|
|
13
|
+
// Exported for testing purposes.
|
|
14
|
+
export const MAX_CONTEXTLINES_COLNO: number = 1000
|
|
15
|
+
export const MAX_CONTEXTLINES_LINENO: number = 10000
|
|
16
|
+
|
|
17
|
+
type ReadlineRange = [start: number, end: number]
|
|
18
|
+
|
|
19
|
+
export async function addSourceContext(
|
|
20
|
+
frames: CoreErrorTracking.StackFrame[]
|
|
21
|
+
): Promise<CoreErrorTracking.StackFrame[]> {
|
|
22
|
+
// keep a lookup map of which files we've already enqueued to read,
|
|
23
|
+
// so we don't enqueue the same file multiple times which would cause multiple i/o reads
|
|
24
|
+
const filesToLines: Record<string, number[]> = {}
|
|
25
|
+
|
|
26
|
+
// Maps preserve insertion order, so we iterate in reverse, starting at the
|
|
27
|
+
// outermost frame and closer to where the exception has occurred (poor mans priority)
|
|
28
|
+
for (let i = frames.length - 1; i >= 0; i--) {
|
|
29
|
+
const frame: CoreErrorTracking.StackFrame | undefined = frames[i]
|
|
30
|
+
const filename = frame?.filename
|
|
31
|
+
|
|
32
|
+
if (
|
|
33
|
+
!frame ||
|
|
34
|
+
typeof filename !== 'string' ||
|
|
35
|
+
typeof frame.lineno !== 'number' ||
|
|
36
|
+
shouldSkipContextLinesForFile(filename) ||
|
|
37
|
+
shouldSkipContextLinesForFrame(frame)
|
|
38
|
+
) {
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const filesToLinesOutput = filesToLines[filename]
|
|
43
|
+
if (!filesToLinesOutput) {
|
|
44
|
+
filesToLines[filename] = []
|
|
45
|
+
}
|
|
46
|
+
filesToLines[filename].push(frame.lineno)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const files = Object.keys(filesToLines)
|
|
50
|
+
if (files.length == 0) {
|
|
51
|
+
return frames
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const readlinePromises: Promise<void>[] = []
|
|
55
|
+
for (const file of files) {
|
|
56
|
+
// If we failed to read this before, dont try reading it again.
|
|
57
|
+
if (LRU_FILE_CONTENTS_FS_READ_FAILED.get(file)) {
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const filesToLineRanges = filesToLines[file]
|
|
62
|
+
if (!filesToLineRanges) {
|
|
63
|
+
continue
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Sort ranges so that they are sorted by line increasing order and match how the file is read.
|
|
67
|
+
filesToLineRanges.sort((a, b) => a - b)
|
|
68
|
+
// Check if the contents are already in the cache and if we can avoid reading the file again.
|
|
69
|
+
const ranges = makeLineReaderRanges(filesToLineRanges)
|
|
70
|
+
if (ranges.every((r) => rangeExistsInContentCache(file, r))) {
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const cache = emplace(LRU_FILE_CONTENTS_CACHE, file, {})
|
|
75
|
+
readlinePromises.push(getContextLinesFromFile(file, ranges, cache))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// The promise rejections are caught in order to prevent them from short circuiting Promise.all
|
|
79
|
+
await Promise.all(readlinePromises).catch(() => {})
|
|
80
|
+
|
|
81
|
+
// Perform the same loop as above, but this time we can assume all files are in the cache
|
|
82
|
+
// and attempt to add source context to frames.
|
|
83
|
+
if (frames && frames.length > 0) {
|
|
84
|
+
addSourceContextToFrames(frames, LRU_FILE_CONTENTS_CACHE)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Once we're finished processing an exception reduce the files held in the cache
|
|
88
|
+
// so that we don't indefinetly increase the size of this map
|
|
89
|
+
LRU_FILE_CONTENTS_CACHE.reduce()
|
|
90
|
+
|
|
91
|
+
return frames
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Extracts lines from a file and stores them in a cache.
|
|
96
|
+
*/
|
|
97
|
+
function getContextLinesFromFile(path: string, ranges: ReadlineRange[], output: Record<number, string>): Promise<void> {
|
|
98
|
+
return new Promise((resolve) => {
|
|
99
|
+
// It is important *not* to have any async code between createInterface and the 'line' event listener
|
|
100
|
+
// as it will cause the 'line' event to
|
|
101
|
+
// be emitted before the listener is attached.
|
|
102
|
+
const stream = createReadStream(path)
|
|
103
|
+
const lineReaded = createInterface({
|
|
104
|
+
input: stream,
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
// We need to explicitly destroy the stream to prevent memory leaks,
|
|
108
|
+
// removing the listeners on the readline interface is not enough.
|
|
109
|
+
// See: https://github.com/nodejs/node/issues/9002 and https://github.com/getsentry/sentry-javascript/issues/14892
|
|
110
|
+
function destroyStreamAndResolve(): void {
|
|
111
|
+
stream.destroy()
|
|
112
|
+
resolve()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Init at zero and increment at the start of the loop because lines are 1 indexed.
|
|
116
|
+
let lineNumber = 0
|
|
117
|
+
let currentRangeIndex = 0
|
|
118
|
+
const range = ranges[currentRangeIndex]
|
|
119
|
+
if (range === undefined) {
|
|
120
|
+
// We should never reach this point, but if we do, we should resolve the promise to prevent it from hanging.
|
|
121
|
+
destroyStreamAndResolve()
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
let rangeStart = range[0]
|
|
125
|
+
let rangeEnd = range[1]
|
|
126
|
+
|
|
127
|
+
// We use this inside Promise.all, so we need to resolve the promise even if there is an error
|
|
128
|
+
// to prevent Promise.all from short circuiting the rest.
|
|
129
|
+
function onStreamError(): void {
|
|
130
|
+
// Mark file path as failed to read and prevent multiple read attempts.
|
|
131
|
+
LRU_FILE_CONTENTS_FS_READ_FAILED.set(path, 1)
|
|
132
|
+
lineReaded.close()
|
|
133
|
+
lineReaded.removeAllListeners()
|
|
134
|
+
destroyStreamAndResolve()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// We need to handle the error event to prevent the process from crashing in < Node 16
|
|
138
|
+
// https://github.com/nodejs/node/pull/31603
|
|
139
|
+
stream.on('error', onStreamError)
|
|
140
|
+
lineReaded.on('error', onStreamError)
|
|
141
|
+
lineReaded.on('close', destroyStreamAndResolve)
|
|
142
|
+
|
|
143
|
+
lineReaded.on('line', (line) => {
|
|
144
|
+
lineNumber++
|
|
145
|
+
if (lineNumber < rangeStart) {
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// !Warning: This mutates the cache by storing the snipped line into the cache.
|
|
150
|
+
output[lineNumber] = snipLine(line, 0)
|
|
151
|
+
|
|
152
|
+
if (lineNumber >= rangeEnd) {
|
|
153
|
+
if (currentRangeIndex === ranges.length - 1) {
|
|
154
|
+
// We need to close the file stream and remove listeners, else the reader will continue to run our listener;
|
|
155
|
+
lineReaded.close()
|
|
156
|
+
lineReaded.removeAllListeners()
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
currentRangeIndex++
|
|
160
|
+
const range = ranges[currentRangeIndex]
|
|
161
|
+
if (range === undefined) {
|
|
162
|
+
// This should never happen as it means we have a bug in the context.
|
|
163
|
+
lineReaded.close()
|
|
164
|
+
lineReaded.removeAllListeners()
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
rangeStart = range[0]
|
|
168
|
+
rangeEnd = range[1]
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Adds context lines to frames */
|
|
175
|
+
function addSourceContextToFrames(
|
|
176
|
+
frames: CoreErrorTracking.StackFrame[],
|
|
177
|
+
cache: CoreErrorTracking.ReduceableCache<string, Record<number, string>>
|
|
178
|
+
): void {
|
|
179
|
+
for (const frame of frames) {
|
|
180
|
+
// Only add context if we have a filename and it hasn't already been added
|
|
181
|
+
if (frame.filename && frame.context_line === undefined && typeof frame.lineno === 'number') {
|
|
182
|
+
const contents = cache.get(frame.filename)
|
|
183
|
+
if (contents === undefined) {
|
|
184
|
+
continue
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
addContextToFrame(frame.lineno, frame, contents)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Resolves context lines before and after the given line number and appends them to the frame;
|
|
194
|
+
*/
|
|
195
|
+
function addContextToFrame(
|
|
196
|
+
lineno: number,
|
|
197
|
+
frame: CoreErrorTracking.StackFrame,
|
|
198
|
+
contents: Record<number, string> | undefined
|
|
199
|
+
): void {
|
|
200
|
+
// When there is no line number in the frame, attaching context is nonsensical and will even break grouping.
|
|
201
|
+
// We already check for lineno before calling this, but since StackFrame lineno is optional, we check it again.
|
|
202
|
+
if (frame.lineno === undefined || contents === undefined) {
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
frame.pre_context = []
|
|
207
|
+
for (let i = makeRangeStart(lineno); i < lineno; i++) {
|
|
208
|
+
// We always expect the start context as line numbers cannot be negative. If we dont find a line, then
|
|
209
|
+
// something went wrong somewhere. Clear the context and return without adding any linecontext.
|
|
210
|
+
const line = contents[i]
|
|
211
|
+
if (line === undefined) {
|
|
212
|
+
clearLineContext(frame)
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
frame.pre_context.push(line)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// We should always have the context line. If we dont, something went wrong, so we clear the context and return
|
|
220
|
+
// without adding any linecontext.
|
|
221
|
+
if (contents[lineno] === undefined) {
|
|
222
|
+
clearLineContext(frame)
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
frame.context_line = contents[lineno]
|
|
227
|
+
|
|
228
|
+
const end = makeRangeEnd(lineno)
|
|
229
|
+
frame.post_context = []
|
|
230
|
+
for (let i = lineno + 1; i <= end; i++) {
|
|
231
|
+
// Since we dont track when the file ends, we cant clear the context if we dont find a line as it could
|
|
232
|
+
// just be that we reached the end of the file.
|
|
233
|
+
const line = contents[i]
|
|
234
|
+
if (line === undefined) {
|
|
235
|
+
break
|
|
236
|
+
}
|
|
237
|
+
frame.post_context.push(line)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Clears the context lines from a frame, used to reset a frame to its original state
|
|
243
|
+
* if we fail to resolve all context lines for it.
|
|
244
|
+
*/
|
|
245
|
+
function clearLineContext(frame: CoreErrorTracking.StackFrame): void {
|
|
246
|
+
delete frame.pre_context
|
|
247
|
+
delete frame.context_line
|
|
248
|
+
delete frame.post_context
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Determines if context lines should be skipped for a file.
|
|
253
|
+
* - .min.(mjs|cjs|js) files are and not useful since they dont point to the original source
|
|
254
|
+
* - node: prefixed modules are part of the runtime and cannot be resolved to a file
|
|
255
|
+
* - data: skip json, wasm and inline js https://nodejs.org/api/esm.html#data-imports
|
|
256
|
+
*/
|
|
257
|
+
function shouldSkipContextLinesForFile(path: string): boolean {
|
|
258
|
+
// Test the most common prefix and extension first. These are the ones we
|
|
259
|
+
// are most likely to see in user applications and are the ones we can break out of first.
|
|
260
|
+
return (
|
|
261
|
+
path.startsWith('node:') ||
|
|
262
|
+
path.endsWith('.min.js') ||
|
|
263
|
+
path.endsWith('.min.cjs') ||
|
|
264
|
+
path.endsWith('.min.mjs') ||
|
|
265
|
+
path.startsWith('data:')
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Determines if we should skip contextlines based off the max lineno and colno values.
|
|
271
|
+
*/
|
|
272
|
+
function shouldSkipContextLinesForFrame(frame: CoreErrorTracking.StackFrame): boolean {
|
|
273
|
+
if (frame.lineno !== undefined && frame.lineno > MAX_CONTEXTLINES_LINENO) {
|
|
274
|
+
return true
|
|
275
|
+
}
|
|
276
|
+
if (frame.colno !== undefined && frame.colno > MAX_CONTEXTLINES_COLNO) {
|
|
277
|
+
return true
|
|
278
|
+
}
|
|
279
|
+
return false
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Checks if we have all the contents that we need in the cache.
|
|
284
|
+
*/
|
|
285
|
+
function rangeExistsInContentCache(file: string, range: ReadlineRange): boolean {
|
|
286
|
+
const contents = LRU_FILE_CONTENTS_CACHE.get(file)
|
|
287
|
+
if (contents === undefined) {
|
|
288
|
+
return false
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
for (let i = range[0]; i <= range[1]; i++) {
|
|
292
|
+
if (contents[i] === undefined) {
|
|
293
|
+
return false
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return true
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Creates contiguous ranges of lines to read from a file. In the case where context lines overlap,
|
|
302
|
+
* the ranges are merged to create a single range.
|
|
303
|
+
*/
|
|
304
|
+
function makeLineReaderRanges(lines: number[]): ReadlineRange[] {
|
|
305
|
+
if (!lines.length) {
|
|
306
|
+
return []
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
let i = 0
|
|
310
|
+
const line = lines[0]
|
|
311
|
+
|
|
312
|
+
if (typeof line !== 'number') {
|
|
313
|
+
return []
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
let current = makeContextRange(line)
|
|
317
|
+
const out: ReadlineRange[] = []
|
|
318
|
+
while (true) {
|
|
319
|
+
if (i === lines.length - 1) {
|
|
320
|
+
out.push(current)
|
|
321
|
+
break
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// If the next line falls into the current range, extend the current range to lineno + linecontext.
|
|
325
|
+
const next = lines[i + 1]
|
|
326
|
+
if (typeof next !== 'number') {
|
|
327
|
+
break
|
|
328
|
+
}
|
|
329
|
+
if (next <= current[1]) {
|
|
330
|
+
current[1] = next + DEFAULT_LINES_OF_CONTEXT
|
|
331
|
+
} else {
|
|
332
|
+
out.push(current)
|
|
333
|
+
current = makeContextRange(next)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
i++
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return out
|
|
340
|
+
}
|
|
341
|
+
// Determine start and end indices for context range (inclusive);
|
|
342
|
+
function makeContextRange(line: number): [start: number, end: number] {
|
|
343
|
+
return [makeRangeStart(line), makeRangeEnd(line)]
|
|
344
|
+
}
|
|
345
|
+
// Compute inclusive end context range
|
|
346
|
+
function makeRangeStart(line: number): number {
|
|
347
|
+
return Math.max(1, line - DEFAULT_LINES_OF_CONTEXT)
|
|
348
|
+
}
|
|
349
|
+
// Compute inclusive start context range
|
|
350
|
+
function makeRangeEnd(line: number): number {
|
|
351
|
+
return line + DEFAULT_LINES_OF_CONTEXT
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Get or init map value
|
|
356
|
+
*/
|
|
357
|
+
function emplace<T extends CoreErrorTracking.ReduceableCache<K, V>, K extends string, V>(
|
|
358
|
+
map: T,
|
|
359
|
+
key: K,
|
|
360
|
+
contents: V
|
|
361
|
+
): V {
|
|
362
|
+
const value = map.get(key)
|
|
363
|
+
|
|
364
|
+
if (value === undefined) {
|
|
365
|
+
map.set(key, contents)
|
|
366
|
+
return contents
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return value
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function snipLine(line: string, colno: number): string {
|
|
373
|
+
let newLine = line
|
|
374
|
+
const lineLength = newLine.length
|
|
375
|
+
if (lineLength <= 150) {
|
|
376
|
+
return newLine
|
|
377
|
+
}
|
|
378
|
+
if (colno > lineLength) {
|
|
379
|
+
colno = lineLength
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
let start = Math.max(colno - 60, 0)
|
|
383
|
+
if (start < 5) {
|
|
384
|
+
start = 0
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
let end = Math.min(start + 140, lineLength)
|
|
388
|
+
if (end > lineLength - 5) {
|
|
389
|
+
end = lineLength
|
|
390
|
+
}
|
|
391
|
+
if (end === lineLength) {
|
|
392
|
+
start = Math.max(end - 140, 0)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
newLine = newLine.slice(start, end)
|
|
396
|
+
if (start > 0) {
|
|
397
|
+
newLine = `...${newLine}`
|
|
398
|
+
}
|
|
399
|
+
if (end < lineLength) {
|
|
400
|
+
newLine += '...'
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return newLine
|
|
404
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Portions of this file are derived from getsentry/sentry-javascript by Software, Inc. dba Sentry
|
|
2
|
+
// Licensed under the MIT License
|
|
3
|
+
|
|
4
|
+
import { ErrorTracking as CoreErrorTracking } from '@posthog/core'
|
|
5
|
+
import { posix, sep, dirname } from 'path'
|
|
6
|
+
|
|
7
|
+
export function createModulerModifier() {
|
|
8
|
+
const getModuleFromFileName = createGetModuleFromFilename()
|
|
9
|
+
return async (frames: CoreErrorTracking.StackFrame[]): Promise<CoreErrorTracking.StackFrame[]> => {
|
|
10
|
+
for (const frame of frames) {
|
|
11
|
+
frame.module = getModuleFromFileName(frame.filename)
|
|
12
|
+
}
|
|
13
|
+
return frames
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Creates a function that gets the module name from a filename */
|
|
18
|
+
function createGetModuleFromFilename(
|
|
19
|
+
basePath: string = process.argv[1] ? dirname(process.argv[1]) : process.cwd(),
|
|
20
|
+
isWindows: boolean = sep === '\\'
|
|
21
|
+
): (filename: string | undefined) => string | undefined {
|
|
22
|
+
const normalizedBase = isWindows ? normalizeWindowsPath(basePath) : basePath
|
|
23
|
+
|
|
24
|
+
return (filename: string | undefined) => {
|
|
25
|
+
if (!filename) {
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const normalizedFilename = isWindows ? normalizeWindowsPath(filename) : filename
|
|
30
|
+
|
|
31
|
+
// eslint-disable-next-line prefer-const
|
|
32
|
+
let { dir, base: file, ext } = posix.parse(normalizedFilename)
|
|
33
|
+
|
|
34
|
+
if (ext === '.js' || ext === '.mjs' || ext === '.cjs') {
|
|
35
|
+
file = file.slice(0, ext.length * -1)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The file name might be URI-encoded which we want to decode to
|
|
39
|
+
// the original file name.
|
|
40
|
+
const decodedFile = decodeURIComponent(file)
|
|
41
|
+
|
|
42
|
+
if (!dir) {
|
|
43
|
+
// No dirname whatsoever
|
|
44
|
+
dir = '.'
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const n = dir.lastIndexOf('/node_modules')
|
|
48
|
+
if (n > -1) {
|
|
49
|
+
return `${dir.slice(n + 14).replace(/\//g, '.')}:${decodedFile}`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Let's see if it's a part of the main module
|
|
53
|
+
// To be a part of main module, it has to share the same base
|
|
54
|
+
if (dir.startsWith(normalizedBase)) {
|
|
55
|
+
const moduleName = dir.slice(normalizedBase.length + 1).replace(/\//g, '.')
|
|
56
|
+
return moduleName ? `${moduleName}:${decodedFile}` : decodedFile
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return decodedFile
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** normalizes Windows paths */
|
|
64
|
+
function normalizeWindowsPath(path: string): string {
|
|
65
|
+
return path
|
|
66
|
+
.replace(/^[A-Z]:/, '') // remove Windows-style prefix
|
|
67
|
+
.replace(/\\/g, '/') // replace all `\` instances with `/`
|
|
68
|
+
}
|