posthog-node 5.8.8 → 5.9.0
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 +481 -0
- package/dist/client.mjs +437 -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 +89 -0
- package/dist/entrypoints/index.edge.mjs +12 -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 +99 -0
- package/dist/entrypoints/index.node.mjs +16 -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/chunk-ids.d.ts +5 -0
- package/dist/extensions/error-tracking/chunk-ids.d.ts.map +1 -0
- package/dist/extensions/error-tracking/chunk-ids.js +68 -0
- package/dist/extensions/error-tracking/chunk-ids.mjs +34 -0
- package/dist/extensions/error-tracking/context-lines.node.d.ts +5 -0
- package/dist/extensions/error-tracking/context-lines.node.d.ts.map +1 -0
- package/dist/extensions/error-tracking/context-lines.node.js +227 -0
- package/dist/extensions/error-tracking/context-lines.node.mjs +187 -0
- package/dist/extensions/error-tracking/error-conversion.d.ts +4 -0
- package/dist/extensions/error-tracking/error-conversion.d.ts.map +1 -0
- package/dist/extensions/error-tracking/error-conversion.js +183 -0
- package/dist/extensions/error-tracking/error-conversion.mjs +146 -0
- package/dist/extensions/error-tracking/get-module.node.d.ts +3 -0
- package/dist/extensions/error-tracking/get-module.node.d.ts.map +1 -0
- package/dist/extensions/error-tracking/get-module.node.js +57 -0
- package/dist/extensions/error-tracking/get-module.node.mjs +23 -0
- package/dist/extensions/error-tracking/index.d.ts +20 -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/reduceable-cache.d.ts +13 -0
- package/dist/extensions/error-tracking/reduceable-cache.d.ts.map +1 -0
- package/dist/extensions/error-tracking/reduceable-cache.js +57 -0
- package/dist/extensions/error-tracking/reduceable-cache.mjs +23 -0
- package/dist/extensions/error-tracking/stack-parser.d.ts +3 -0
- package/dist/extensions/error-tracking/stack-parser.d.ts.map +1 -0
- package/dist/extensions/error-tracking/stack-parser.js +148 -0
- package/dist/extensions/error-tracking/stack-parser.mjs +114 -0
- package/dist/extensions/error-tracking/type-checking.d.ts +8 -0
- package/dist/extensions/error-tracking/type-checking.d.ts.map +1 -0
- package/dist/extensions/error-tracking/type-checking.js +80 -0
- package/dist/extensions/error-tracking/type-checking.mjs +31 -0
- package/dist/extensions/error-tracking/types.d.ts +61 -0
- package/dist/extensions/error-tracking/types.d.ts.map +1 -0
- package/dist/extensions/error-tracking/types.js +43 -0
- package/dist/extensions/error-tracking/types.mjs +9 -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 +15 -0
- package/src/entrypoints/index.node.ts +17 -0
- package/src/exports.ts +3 -0
- package/src/extensions/error-tracking/autocapture.ts +65 -0
- package/src/extensions/error-tracking/chunk-ids.ts +58 -0
- package/src/extensions/error-tracking/context-lines.node.ts +392 -0
- package/src/extensions/error-tracking/error-conversion.ts +291 -0
- package/src/extensions/error-tracking/get-module.node.ts +57 -0
- package/src/extensions/error-tracking/index.ts +103 -0
- package/src/extensions/error-tracking/reduceable-cache.ts +39 -0
- package/src/extensions/error-tracking/stack-parser.ts +212 -0
- package/src/extensions/error-tracking/type-checking.ts +40 -0
- package/src/extensions/error-tracking/types.ts +71 -0
- package/src/extensions/express.ts +39 -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,291 @@
|
|
|
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 { getFilenameToChunkIdMap } from './chunk-ids'
|
|
5
|
+
import { isError, isErrorEvent, isEvent, isPlainObject } from './type-checking'
|
|
6
|
+
import {
|
|
7
|
+
ErrorProperties,
|
|
8
|
+
EventHint,
|
|
9
|
+
Exception,
|
|
10
|
+
Mechanism,
|
|
11
|
+
StackFrame,
|
|
12
|
+
StackFrameModifierFn,
|
|
13
|
+
StackParser,
|
|
14
|
+
} from './types'
|
|
15
|
+
|
|
16
|
+
export async function propertiesFromUnknownInput(
|
|
17
|
+
stackParser: StackParser,
|
|
18
|
+
frameModifiers: StackFrameModifierFn[],
|
|
19
|
+
input: unknown,
|
|
20
|
+
hint?: EventHint
|
|
21
|
+
): Promise<ErrorProperties> {
|
|
22
|
+
const providedMechanism = hint && hint.mechanism
|
|
23
|
+
const mechanism = providedMechanism || {
|
|
24
|
+
handled: true,
|
|
25
|
+
type: 'generic',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const errorList = getErrorList(mechanism, input, hint)
|
|
29
|
+
const exceptionList = await Promise.all(
|
|
30
|
+
errorList.map(async (error) => {
|
|
31
|
+
const exception = await exceptionFromError(stackParser, frameModifiers, error)
|
|
32
|
+
exception.value = exception.value || ''
|
|
33
|
+
exception.type = exception.type || 'Error'
|
|
34
|
+
exception.mechanism = mechanism
|
|
35
|
+
return exception
|
|
36
|
+
})
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
const properties = { $exception_list: exceptionList }
|
|
40
|
+
return properties
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Flatten error causes into a list of errors
|
|
44
|
+
// See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause
|
|
45
|
+
function getErrorList(mechanism: Mechanism, input: unknown, hint?: EventHint): Error[] {
|
|
46
|
+
const error = getError(mechanism, input, hint)
|
|
47
|
+
if (error.cause) {
|
|
48
|
+
return [error, ...getErrorList(mechanism, error.cause, hint)]
|
|
49
|
+
}
|
|
50
|
+
return [error]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getError(mechanism: Mechanism, exception: unknown, hint?: EventHint): Error {
|
|
54
|
+
if (isError(exception)) {
|
|
55
|
+
return exception
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
mechanism.synthetic = true
|
|
59
|
+
|
|
60
|
+
if (isPlainObject(exception)) {
|
|
61
|
+
const errorFromProp = getErrorPropertyFromObject(exception)
|
|
62
|
+
if (errorFromProp) {
|
|
63
|
+
return errorFromProp
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const message = getMessageForObject(exception)
|
|
67
|
+
const ex = hint?.syntheticException || new Error(message)
|
|
68
|
+
ex.message = message
|
|
69
|
+
|
|
70
|
+
return ex
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// This handles when someone does: `throw "something awesome";`
|
|
74
|
+
// We use synthesized Error here so we can extract a (rough) stack trace.
|
|
75
|
+
const ex = hint?.syntheticException || new Error(exception as string)
|
|
76
|
+
ex.message = `${exception}`
|
|
77
|
+
|
|
78
|
+
return ex
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** If a plain object has a property that is an `Error`, return this error. */
|
|
82
|
+
function getErrorPropertyFromObject(obj: Record<string, unknown>): Error | undefined {
|
|
83
|
+
for (const prop in obj) {
|
|
84
|
+
if (Object.prototype.hasOwnProperty.call(obj, prop)) {
|
|
85
|
+
const value = obj[prop]
|
|
86
|
+
if (isError(value)) {
|
|
87
|
+
return value
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return undefined
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getMessageForObject(exception: Record<string, unknown>): string {
|
|
96
|
+
if ('name' in exception && typeof exception.name === 'string') {
|
|
97
|
+
let message = `'${exception.name}' captured as exception`
|
|
98
|
+
|
|
99
|
+
if ('message' in exception && typeof exception.message === 'string') {
|
|
100
|
+
message += ` with message '${exception.message}'`
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return message
|
|
104
|
+
} else if ('message' in exception && typeof exception.message === 'string') {
|
|
105
|
+
return exception.message
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const keys = extractExceptionKeysForMessage(exception)
|
|
109
|
+
|
|
110
|
+
// Some ErrorEvent instances do not have an `error` property, which is why they are not handled before
|
|
111
|
+
// We still want to try to get a decent message for these cases
|
|
112
|
+
if (isErrorEvent(exception)) {
|
|
113
|
+
return `Event \`ErrorEvent\` captured as exception with message \`${exception.message}\``
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const className = getObjectClassName(exception)
|
|
117
|
+
|
|
118
|
+
return `${className && className !== 'Object' ? `'${className}'` : 'Object'} captured as exception with keys: ${keys}`
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function getObjectClassName(obj: unknown): string | undefined | void {
|
|
122
|
+
try {
|
|
123
|
+
const prototype: unknown | null = Object.getPrototypeOf(obj)
|
|
124
|
+
return prototype ? prototype.constructor.name : undefined
|
|
125
|
+
} catch (e) {
|
|
126
|
+
// ignore errors here
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Given any captured exception, extract its keys and create a sorted
|
|
132
|
+
* and truncated list that will be used inside the event message.
|
|
133
|
+
* eg. `Non-error exception captured with keys: foo, bar, baz`
|
|
134
|
+
*/
|
|
135
|
+
function extractExceptionKeysForMessage(exception: Record<string, unknown>, maxLength: number = 40): string {
|
|
136
|
+
const keys = Object.keys(convertToPlainObject(exception))
|
|
137
|
+
keys.sort()
|
|
138
|
+
|
|
139
|
+
const firstKey = keys[0]
|
|
140
|
+
|
|
141
|
+
if (!firstKey) {
|
|
142
|
+
return '[object has no keys]'
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (firstKey.length >= maxLength) {
|
|
146
|
+
return truncate(firstKey, maxLength)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for (let includedKeys = keys.length; includedKeys > 0; includedKeys--) {
|
|
150
|
+
const serialized = keys.slice(0, includedKeys).join(', ')
|
|
151
|
+
if (serialized.length > maxLength) {
|
|
152
|
+
continue
|
|
153
|
+
}
|
|
154
|
+
if (includedKeys === keys.length) {
|
|
155
|
+
return serialized
|
|
156
|
+
}
|
|
157
|
+
return truncate(serialized, maxLength)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return ''
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function truncate(str: string, max: number = 0): string {
|
|
164
|
+
if (typeof str !== 'string' || max === 0) {
|
|
165
|
+
return str
|
|
166
|
+
}
|
|
167
|
+
return str.length <= max ? str : `${str.slice(0, max)}...`
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Transforms any `Error` or `Event` into a plain object with all of their enumerable properties, and some of their
|
|
172
|
+
* non-enumerable properties attached.
|
|
173
|
+
*
|
|
174
|
+
* @param value Initial source that we have to transform in order for it to be usable by the serializer
|
|
175
|
+
* @returns An Event or Error turned into an object - or the value argument itself, when value is neither an Event nor
|
|
176
|
+
* an Error.
|
|
177
|
+
*/
|
|
178
|
+
function convertToPlainObject<V>(value: V):
|
|
179
|
+
| {
|
|
180
|
+
[ownProps: string]: unknown
|
|
181
|
+
type: string
|
|
182
|
+
target: string
|
|
183
|
+
currentTarget: string
|
|
184
|
+
detail?: unknown
|
|
185
|
+
}
|
|
186
|
+
| {
|
|
187
|
+
[ownProps: string]: unknown
|
|
188
|
+
message: string
|
|
189
|
+
name: string
|
|
190
|
+
stack?: string
|
|
191
|
+
}
|
|
192
|
+
| V {
|
|
193
|
+
if (isError(value)) {
|
|
194
|
+
return {
|
|
195
|
+
message: value.message,
|
|
196
|
+
name: value.name,
|
|
197
|
+
stack: value.stack,
|
|
198
|
+
...getOwnProperties(value),
|
|
199
|
+
}
|
|
200
|
+
} else if (isEvent(value)) {
|
|
201
|
+
const newObj: {
|
|
202
|
+
[ownProps: string]: unknown
|
|
203
|
+
type: string
|
|
204
|
+
target: string
|
|
205
|
+
currentTarget: string
|
|
206
|
+
detail?: unknown
|
|
207
|
+
} = {
|
|
208
|
+
type: value.type,
|
|
209
|
+
target: serializeEventTarget(value.target),
|
|
210
|
+
currentTarget: serializeEventTarget(value.currentTarget),
|
|
211
|
+
...getOwnProperties(value),
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// TODO: figure out why this fails typing (I think CustomEvent is only supported in Node 19 onwards)
|
|
215
|
+
// if (typeof CustomEvent !== 'undefined' && isInstanceOf(value, CustomEvent)) {
|
|
216
|
+
// newObj.detail = (value as unknown as CustomEvent).detail
|
|
217
|
+
// }
|
|
218
|
+
|
|
219
|
+
return newObj
|
|
220
|
+
} else {
|
|
221
|
+
return value
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Filters out all but an object's own properties */
|
|
226
|
+
function getOwnProperties(obj: unknown): { [key: string]: unknown } {
|
|
227
|
+
if (typeof obj === 'object' && obj !== null) {
|
|
228
|
+
const extractedProps: { [key: string]: unknown } = {}
|
|
229
|
+
for (const property in obj) {
|
|
230
|
+
if (Object.prototype.hasOwnProperty.call(obj, property)) {
|
|
231
|
+
extractedProps[property] = (obj as Record<string, unknown>)[property]
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return extractedProps
|
|
235
|
+
} else {
|
|
236
|
+
return {}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Creates a string representation of the target of an `Event` object */
|
|
241
|
+
function serializeEventTarget(target: unknown): string {
|
|
242
|
+
try {
|
|
243
|
+
return Object.prototype.toString.call(target)
|
|
244
|
+
} catch (_oO) {
|
|
245
|
+
return '<unknown>'
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Extracts stack frames from the error and builds an Exception
|
|
251
|
+
*/
|
|
252
|
+
async function exceptionFromError(
|
|
253
|
+
stackParser: StackParser,
|
|
254
|
+
frameModifiers: StackFrameModifierFn[],
|
|
255
|
+
error: Error
|
|
256
|
+
): Promise<Exception> {
|
|
257
|
+
const exception: Exception = {
|
|
258
|
+
type: error.name || error.constructor.name,
|
|
259
|
+
value: error.message,
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
let frames = parseStackFrames(stackParser, error)
|
|
263
|
+
|
|
264
|
+
for (const modifier of frameModifiers) {
|
|
265
|
+
frames = await modifier(frames)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (frames.length) {
|
|
269
|
+
exception.stacktrace = { frames, type: 'raw' }
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return exception
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Extracts stack frames from the error.stack string
|
|
277
|
+
*/
|
|
278
|
+
function parseStackFrames(stackParser: StackParser, error: Error): StackFrame[] {
|
|
279
|
+
return applyChunkIds(stackParser(error.stack || '', 1), stackParser)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function applyChunkIds(frames: StackFrame[], parser: StackParser): StackFrame[] {
|
|
283
|
+
const filenameChunkIdMap = getFilenameToChunkIdMap(parser)
|
|
284
|
+
frames.forEach((frame) => {
|
|
285
|
+
if (frame.filename && filenameChunkIdMap) {
|
|
286
|
+
frame.chunk_id = filenameChunkIdMap[frame.filename]
|
|
287
|
+
}
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
return frames
|
|
291
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
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 { posix, sep, dirname } from 'path'
|
|
5
|
+
|
|
6
|
+
/** Creates a function that gets the module name from a filename */
|
|
7
|
+
export function createGetModuleFromFilename(
|
|
8
|
+
basePath: string = process.argv[1] ? dirname(process.argv[1]) : process.cwd(),
|
|
9
|
+
isWindows: boolean = sep === '\\'
|
|
10
|
+
): (filename: string | undefined) => string | undefined {
|
|
11
|
+
const normalizedBase = isWindows ? normalizeWindowsPath(basePath) : basePath
|
|
12
|
+
|
|
13
|
+
return (filename: string | undefined) => {
|
|
14
|
+
if (!filename) {
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const normalizedFilename = isWindows ? normalizeWindowsPath(filename) : filename
|
|
19
|
+
|
|
20
|
+
// eslint-disable-next-line prefer-const
|
|
21
|
+
let { dir, base: file, ext } = posix.parse(normalizedFilename)
|
|
22
|
+
|
|
23
|
+
if (ext === '.js' || ext === '.mjs' || ext === '.cjs') {
|
|
24
|
+
file = file.slice(0, ext.length * -1)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// The file name might be URI-encoded which we want to decode to
|
|
28
|
+
// the original file name.
|
|
29
|
+
const decodedFile = decodeURIComponent(file)
|
|
30
|
+
|
|
31
|
+
if (!dir) {
|
|
32
|
+
// No dirname whatsoever
|
|
33
|
+
dir = '.'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const n = dir.lastIndexOf('/node_modules')
|
|
37
|
+
if (n > -1) {
|
|
38
|
+
return `${dir.slice(n + 14).replace(/\//g, '.')}:${decodedFile}`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Let's see if it's a part of the main module
|
|
42
|
+
// To be a part of main module, it has to share the same base
|
|
43
|
+
if (dir.startsWith(normalizedBase)) {
|
|
44
|
+
const moduleName = dir.slice(normalizedBase.length + 1).replace(/\//g, '.')
|
|
45
|
+
return moduleName ? `${moduleName}:${decodedFile}` : decodedFile
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return decodedFile
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** normalizes Windows paths */
|
|
53
|
+
function normalizeWindowsPath(path: string): string {
|
|
54
|
+
return path
|
|
55
|
+
.replace(/^[A-Z]:/, '') // remove Windows-style prefix
|
|
56
|
+
.replace(/\\/g, '/') // replace all `\` instances with `/`
|
|
57
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { EventHint, StackFrameModifierFn, StackParser } from './types'
|
|
2
|
+
import { addUncaughtExceptionListener, addUnhandledRejectionListener } from './autocapture'
|
|
3
|
+
import { PostHogBackendClient } from '../../client'
|
|
4
|
+
import { uuidv7 } from '@posthog/core'
|
|
5
|
+
import { propertiesFromUnknownInput } from './error-conversion'
|
|
6
|
+
import { EventMessage, PostHogOptions } from '../../types'
|
|
7
|
+
import type { Logger } from '@posthog/core'
|
|
8
|
+
import { BucketedRateLimiter } from '@posthog/core'
|
|
9
|
+
|
|
10
|
+
const SHUTDOWN_TIMEOUT = 2000
|
|
11
|
+
|
|
12
|
+
export default class ErrorTracking {
|
|
13
|
+
private client: PostHogBackendClient
|
|
14
|
+
private _exceptionAutocaptureEnabled: boolean
|
|
15
|
+
private _rateLimiter: BucketedRateLimiter<string>
|
|
16
|
+
private _logger: Logger
|
|
17
|
+
|
|
18
|
+
static stackParser: StackParser
|
|
19
|
+
static frameModifiers: StackFrameModifierFn[]
|
|
20
|
+
|
|
21
|
+
constructor(client: PostHogBackendClient, options: PostHogOptions, _logger: Logger) {
|
|
22
|
+
this.client = client
|
|
23
|
+
this._exceptionAutocaptureEnabled = options.enableExceptionAutocapture || false
|
|
24
|
+
this._logger = _logger
|
|
25
|
+
|
|
26
|
+
// by default captures ten exceptions before rate limiting by exception type
|
|
27
|
+
// refills at a rate of one token / 10 second period
|
|
28
|
+
// e.g. will capture 1 exception rate limited exception every 10 seconds until burst ends
|
|
29
|
+
this._rateLimiter = new BucketedRateLimiter({
|
|
30
|
+
refillRate: 1,
|
|
31
|
+
bucketSize: 10,
|
|
32
|
+
refillInterval: 10000, // ten seconds in milliseconds
|
|
33
|
+
_logger: this._logger,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
this.startAutocaptureIfEnabled()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static async buildEventMessage(
|
|
40
|
+
error: unknown,
|
|
41
|
+
hint: EventHint,
|
|
42
|
+
distinctId?: string,
|
|
43
|
+
additionalProperties?: Record<string | number, any>
|
|
44
|
+
): Promise<EventMessage> {
|
|
45
|
+
const properties: EventMessage['properties'] = { ...additionalProperties }
|
|
46
|
+
|
|
47
|
+
// Given stateless nature of Node SDK we capture exceptions using personless processing when no
|
|
48
|
+
// user can be determined because a distinct_id is not provided e.g. exception autocapture
|
|
49
|
+
if (!distinctId) {
|
|
50
|
+
properties.$process_person_profile = false
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const exceptionProperties = await propertiesFromUnknownInput(this.stackParser, this.frameModifiers, error, hint)
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
event: '$exception',
|
|
57
|
+
distinctId: distinctId || uuidv7(),
|
|
58
|
+
properties: {
|
|
59
|
+
...exceptionProperties,
|
|
60
|
+
...properties,
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private startAutocaptureIfEnabled(): void {
|
|
66
|
+
if (this.isEnabled()) {
|
|
67
|
+
addUncaughtExceptionListener(this.onException.bind(this), this.onFatalError.bind(this))
|
|
68
|
+
addUnhandledRejectionListener(this.onException.bind(this))
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private async onException(exception: unknown, hint: EventHint): Promise<void> {
|
|
73
|
+
this.client.addPendingPromise(
|
|
74
|
+
(async () => {
|
|
75
|
+
const eventMessage = await ErrorTracking.buildEventMessage(exception, hint)
|
|
76
|
+
const exceptionProperties = eventMessage.properties
|
|
77
|
+
const exceptionType = exceptionProperties?.$exception_list[0]?.type ?? 'Exception'
|
|
78
|
+
const isRateLimited = this._rateLimiter.consumeRateLimit(exceptionType)
|
|
79
|
+
if (isRateLimited) {
|
|
80
|
+
this._logger.info('Skipping exception capture because of client rate limiting.', {
|
|
81
|
+
exception: exceptionType,
|
|
82
|
+
})
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
return this.client.capture(eventMessage)
|
|
86
|
+
})()
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private async onFatalError(exception: Error): Promise<void> {
|
|
91
|
+
console.error(exception)
|
|
92
|
+
await this.client.shutdown(SHUTDOWN_TIMEOUT)
|
|
93
|
+
process.exit(1)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
isEnabled(): boolean {
|
|
97
|
+
return !this.client.isDisabled && this._exceptionAutocaptureEnabled
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
shutdown(): void {
|
|
101
|
+
this._rateLimiter.stop()
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Portions of this file are derived from getsentry/sentry-javascript by Software, Inc. dba Sentry
|
|
2
|
+
// Licensed under the MIT License
|
|
3
|
+
|
|
4
|
+
/** A simple Least Recently Used map */
|
|
5
|
+
export class ReduceableCache<K, V> {
|
|
6
|
+
private readonly _cache: Map<K, V>
|
|
7
|
+
|
|
8
|
+
public constructor(private readonly _maxSize: number) {
|
|
9
|
+
this._cache = new Map<K, V>()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Get an entry or undefined if it was not in the cache. Re-inserts to update the recently used order */
|
|
13
|
+
public get(key: K): V | undefined {
|
|
14
|
+
const value = this._cache.get(key)
|
|
15
|
+
if (value === undefined) {
|
|
16
|
+
return undefined
|
|
17
|
+
}
|
|
18
|
+
// Remove and re-insert to update the order
|
|
19
|
+
this._cache.delete(key)
|
|
20
|
+
this._cache.set(key, value)
|
|
21
|
+
return value
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Insert an entry and evict an older entry if we've reached maxSize */
|
|
25
|
+
public set(key: K, value: V): void {
|
|
26
|
+
this._cache.set(key, value)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Remove an entry and return the entry if it was in the cache */
|
|
30
|
+
public reduce(): void {
|
|
31
|
+
while (this._cache.size >= this._maxSize) {
|
|
32
|
+
const value = this._cache.keys().next().value
|
|
33
|
+
if (value) {
|
|
34
|
+
// keys() returns an iterator in insertion order so keys().next() gives us the oldest key
|
|
35
|
+
this._cache.delete(value)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
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 { GetModuleFn, StackFrame, StackLineParser, StackLineParserFn, StackParser } from './types'
|
|
5
|
+
|
|
6
|
+
// This was originally forked from https://github.com/csnover/TraceKit, and was largely
|
|
7
|
+
// re-written as part of raven - js.
|
|
8
|
+
//
|
|
9
|
+
// This code was later copied to the JavaScript mono - repo and further modified and
|
|
10
|
+
// refactored over the years.
|
|
11
|
+
|
|
12
|
+
// Copyright (c) 2013 Onur Can Cakmak onur.cakmak@gmail.com and all TraceKit contributors.
|
|
13
|
+
//
|
|
14
|
+
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
|
15
|
+
// software and associated documentation files(the 'Software'), to deal in the Software
|
|
16
|
+
// without restriction, including without limitation the rights to use, copy, modify,
|
|
17
|
+
// merge, publish, distribute, sublicense, and / or sell copies of the Software, and to
|
|
18
|
+
// permit persons to whom the Software is furnished to do so, subject to the following
|
|
19
|
+
// conditions:
|
|
20
|
+
//
|
|
21
|
+
// The above copyright notice and this permission notice shall be included in all copies
|
|
22
|
+
// or substantial portions of the Software.
|
|
23
|
+
//
|
|
24
|
+
// THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
|
25
|
+
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
|
26
|
+
// PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
27
|
+
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
|
28
|
+
// CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
|
29
|
+
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
30
|
+
|
|
31
|
+
const WEBPACK_ERROR_REGEXP = /\(error: (.*)\)/
|
|
32
|
+
const STACKTRACE_FRAME_LIMIT = 50
|
|
33
|
+
|
|
34
|
+
const UNKNOWN_FUNCTION = '?'
|
|
35
|
+
|
|
36
|
+
/** Node Stack line parser */
|
|
37
|
+
function node(getModule?: GetModuleFn): StackLineParserFn {
|
|
38
|
+
const FILENAME_MATCH = /^\s*[-]{4,}$/
|
|
39
|
+
const FULL_MATCH = /at (?:async )?(?:(.+?)\s+\()?(?:(.+):(\d+):(\d+)?|([^)]+))\)?/
|
|
40
|
+
|
|
41
|
+
return (line: string) => {
|
|
42
|
+
const lineMatch = line.match(FULL_MATCH)
|
|
43
|
+
|
|
44
|
+
if (lineMatch) {
|
|
45
|
+
let object: string | undefined
|
|
46
|
+
let method: string | undefined
|
|
47
|
+
let functionName: string | undefined
|
|
48
|
+
let typeName: string | undefined
|
|
49
|
+
let methodName: string | undefined
|
|
50
|
+
|
|
51
|
+
if (lineMatch[1]) {
|
|
52
|
+
functionName = lineMatch[1]
|
|
53
|
+
|
|
54
|
+
let methodStart = functionName.lastIndexOf('.')
|
|
55
|
+
if (functionName[methodStart - 1] === '.') {
|
|
56
|
+
methodStart--
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (methodStart > 0) {
|
|
60
|
+
object = functionName.slice(0, methodStart)
|
|
61
|
+
method = functionName.slice(methodStart + 1)
|
|
62
|
+
const objectEnd = object.indexOf('.Module')
|
|
63
|
+
if (objectEnd > 0) {
|
|
64
|
+
functionName = functionName.slice(objectEnd + 1)
|
|
65
|
+
object = object.slice(0, objectEnd)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
typeName = undefined
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (method) {
|
|
72
|
+
typeName = object
|
|
73
|
+
methodName = method
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (method === '<anonymous>') {
|
|
77
|
+
methodName = undefined
|
|
78
|
+
functionName = undefined
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (functionName === undefined) {
|
|
82
|
+
methodName = methodName || UNKNOWN_FUNCTION
|
|
83
|
+
functionName = typeName ? `${typeName}.${methodName}` : methodName
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let filename = lineMatch[2]?.startsWith('file://') ? lineMatch[2].slice(7) : lineMatch[2]
|
|
87
|
+
const isNative = lineMatch[5] === 'native'
|
|
88
|
+
|
|
89
|
+
// If it's a Windows path, trim the leading slash so that `/C:/foo` becomes `C:/foo`
|
|
90
|
+
if (filename?.match(/\/[A-Z]:/)) {
|
|
91
|
+
filename = filename.slice(1)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!filename && lineMatch[5] && !isNative) {
|
|
95
|
+
filename = lineMatch[5]
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
filename: filename ? decodeURI(filename) : undefined,
|
|
100
|
+
module: getModule ? getModule(filename) : undefined,
|
|
101
|
+
function: functionName,
|
|
102
|
+
lineno: _parseIntOrUndefined(lineMatch[3]),
|
|
103
|
+
colno: _parseIntOrUndefined(lineMatch[4]),
|
|
104
|
+
in_app: filenameIsInApp(filename || '', isNative),
|
|
105
|
+
platform: 'node:javascript',
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (line.match(FILENAME_MATCH)) {
|
|
110
|
+
return {
|
|
111
|
+
filename: line,
|
|
112
|
+
platform: 'node:javascript',
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return undefined
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Does this filename look like it's part of the app code?
|
|
122
|
+
*/
|
|
123
|
+
function filenameIsInApp(filename: string, isNative: boolean = false): boolean {
|
|
124
|
+
const isInternal =
|
|
125
|
+
isNative ||
|
|
126
|
+
(filename &&
|
|
127
|
+
// It's not internal if it's an absolute linux path
|
|
128
|
+
!filename.startsWith('/') &&
|
|
129
|
+
// It's not internal if it's an absolute windows path
|
|
130
|
+
!filename.match(/^[A-Z]:/) &&
|
|
131
|
+
// It's not internal if the path is starting with a dot
|
|
132
|
+
!filename.startsWith('.') &&
|
|
133
|
+
// 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
|
|
134
|
+
!filename.match(/^[a-zA-Z]([a-zA-Z0-9.\-+])*:\/\//)) // Schema from: https://stackoverflow.com/a/3641782
|
|
135
|
+
|
|
136
|
+
// in_app is all that's not an internal Node function or a module within node_modules
|
|
137
|
+
// note that isNative appears to return true even for node core libraries
|
|
138
|
+
// see https://github.com/getsentry/raven-node/issues/176
|
|
139
|
+
|
|
140
|
+
return !isInternal && filename !== undefined && !filename.includes('node_modules/')
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function _parseIntOrUndefined(input: string | undefined): number | undefined {
|
|
144
|
+
return parseInt(input || '', 10) || undefined
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function nodeStackLineParser(getModule?: GetModuleFn): StackLineParser {
|
|
148
|
+
return [90, node(getModule)]
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function createStackParser(getModule?: GetModuleFn): StackParser {
|
|
152
|
+
const parsers = [nodeStackLineParser(getModule)]
|
|
153
|
+
const sortedParsers = parsers.sort((a, b) => a[0] - b[0]).map((p) => p[1])
|
|
154
|
+
|
|
155
|
+
return (stack: string, skipFirstLines: number = 0): StackFrame[] => {
|
|
156
|
+
const frames: StackFrame[] = []
|
|
157
|
+
const lines = stack.split('\n')
|
|
158
|
+
|
|
159
|
+
for (let i = skipFirstLines; i < lines.length; i++) {
|
|
160
|
+
const line = lines[i] as string
|
|
161
|
+
// Ignore lines over 1kb as they are unlikely to be stack frames.
|
|
162
|
+
if (line.length > 1024) {
|
|
163
|
+
continue
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// https://github.com/getsentry/sentry-javascript/issues/5459
|
|
167
|
+
// Remove webpack (error: *) wrappers
|
|
168
|
+
const cleanedLine = WEBPACK_ERROR_REGEXP.test(line) ? line.replace(WEBPACK_ERROR_REGEXP, '$1') : line
|
|
169
|
+
|
|
170
|
+
// https://github.com/getsentry/sentry-javascript/issues/7813
|
|
171
|
+
// Skip Error: lines
|
|
172
|
+
if (cleanedLine.match(/\S*Error: /)) {
|
|
173
|
+
continue
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (const parser of sortedParsers) {
|
|
177
|
+
const frame = parser(cleanedLine)
|
|
178
|
+
|
|
179
|
+
if (frame) {
|
|
180
|
+
frames.push(frame)
|
|
181
|
+
break
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (frames.length >= STACKTRACE_FRAME_LIMIT) {
|
|
186
|
+
break
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return reverseAndStripFrames(frames)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function reverseAndStripFrames(stack: ReadonlyArray<StackFrame>): StackFrame[] {
|
|
195
|
+
if (!stack.length) {
|
|
196
|
+
return []
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const localStack = Array.from(stack)
|
|
200
|
+
|
|
201
|
+
localStack.reverse()
|
|
202
|
+
|
|
203
|
+
return localStack.slice(0, STACKTRACE_FRAME_LIMIT).map((frame) => ({
|
|
204
|
+
...frame,
|
|
205
|
+
filename: frame.filename || getLastStackFrame(localStack).filename,
|
|
206
|
+
function: frame.function || UNKNOWN_FUNCTION,
|
|
207
|
+
}))
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function getLastStackFrame(arr: StackFrame[]): StackFrame {
|
|
211
|
+
return arr[arr.length - 1] || {}
|
|
212
|
+
}
|