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.
Files changed (129) hide show
  1. package/dist/{index.d.ts → client.d.ts} +7 -378
  2. package/dist/client.d.ts.map +1 -0
  3. package/dist/client.js +481 -0
  4. package/dist/client.mjs +437 -0
  5. package/dist/entrypoints/index.edge.d.ts +6 -0
  6. package/dist/entrypoints/index.edge.d.ts.map +1 -0
  7. package/dist/entrypoints/index.edge.js +89 -0
  8. package/dist/entrypoints/index.edge.mjs +12 -0
  9. package/dist/entrypoints/index.node.d.ts +6 -0
  10. package/dist/entrypoints/index.node.d.ts.map +1 -0
  11. package/dist/entrypoints/index.node.js +99 -0
  12. package/dist/entrypoints/index.node.mjs +16 -0
  13. package/dist/exports.d.ts +4 -0
  14. package/dist/exports.d.ts.map +1 -0
  15. package/dist/exports.js +78 -0
  16. package/dist/exports.mjs +3 -0
  17. package/dist/extensions/error-tracking/autocapture.d.ts +4 -0
  18. package/dist/extensions/error-tracking/autocapture.d.ts.map +1 -0
  19. package/dist/extensions/error-tracking/autocapture.js +68 -0
  20. package/dist/extensions/error-tracking/autocapture.mjs +31 -0
  21. package/dist/extensions/error-tracking/chunk-ids.d.ts +5 -0
  22. package/dist/extensions/error-tracking/chunk-ids.d.ts.map +1 -0
  23. package/dist/extensions/error-tracking/chunk-ids.js +68 -0
  24. package/dist/extensions/error-tracking/chunk-ids.mjs +34 -0
  25. package/dist/extensions/error-tracking/context-lines.node.d.ts +5 -0
  26. package/dist/extensions/error-tracking/context-lines.node.d.ts.map +1 -0
  27. package/dist/extensions/error-tracking/context-lines.node.js +227 -0
  28. package/dist/extensions/error-tracking/context-lines.node.mjs +187 -0
  29. package/dist/extensions/error-tracking/error-conversion.d.ts +4 -0
  30. package/dist/extensions/error-tracking/error-conversion.d.ts.map +1 -0
  31. package/dist/extensions/error-tracking/error-conversion.js +183 -0
  32. package/dist/extensions/error-tracking/error-conversion.mjs +146 -0
  33. package/dist/extensions/error-tracking/get-module.node.d.ts +3 -0
  34. package/dist/extensions/error-tracking/get-module.node.d.ts.map +1 -0
  35. package/dist/extensions/error-tracking/get-module.node.js +57 -0
  36. package/dist/extensions/error-tracking/get-module.node.mjs +23 -0
  37. package/dist/extensions/error-tracking/index.d.ts +20 -0
  38. package/dist/extensions/error-tracking/index.d.ts.map +1 -0
  39. package/dist/extensions/error-tracking/index.js +97 -0
  40. package/dist/extensions/error-tracking/index.mjs +63 -0
  41. package/dist/extensions/error-tracking/reduceable-cache.d.ts +13 -0
  42. package/dist/extensions/error-tracking/reduceable-cache.d.ts.map +1 -0
  43. package/dist/extensions/error-tracking/reduceable-cache.js +57 -0
  44. package/dist/extensions/error-tracking/reduceable-cache.mjs +23 -0
  45. package/dist/extensions/error-tracking/stack-parser.d.ts +3 -0
  46. package/dist/extensions/error-tracking/stack-parser.d.ts.map +1 -0
  47. package/dist/extensions/error-tracking/stack-parser.js +148 -0
  48. package/dist/extensions/error-tracking/stack-parser.mjs +114 -0
  49. package/dist/extensions/error-tracking/type-checking.d.ts +8 -0
  50. package/dist/extensions/error-tracking/type-checking.d.ts.map +1 -0
  51. package/dist/extensions/error-tracking/type-checking.js +80 -0
  52. package/dist/extensions/error-tracking/type-checking.mjs +31 -0
  53. package/dist/extensions/error-tracking/types.d.ts +61 -0
  54. package/dist/extensions/error-tracking/types.d.ts.map +1 -0
  55. package/dist/extensions/error-tracking/types.js +43 -0
  56. package/dist/extensions/error-tracking/types.mjs +9 -0
  57. package/dist/extensions/express.d.ts +17 -0
  58. package/dist/extensions/express.d.ts.map +1 -0
  59. package/dist/extensions/express.js +61 -0
  60. package/dist/extensions/express.mjs +17 -0
  61. package/dist/extensions/feature-flags/crypto-helpers.d.ts +3 -0
  62. package/dist/extensions/feature-flags/crypto-helpers.d.ts.map +1 -0
  63. package/dist/extensions/feature-flags/crypto-helpers.js +77 -0
  64. package/dist/extensions/feature-flags/crypto-helpers.mjs +22 -0
  65. package/dist/extensions/feature-flags/crypto.d.ts +2 -0
  66. package/dist/extensions/feature-flags/crypto.d.ts.map +1 -0
  67. package/dist/extensions/feature-flags/crypto.js +47 -0
  68. package/dist/extensions/feature-flags/crypto.mjs +13 -0
  69. package/dist/extensions/feature-flags/feature-flags.d.ts +89 -0
  70. package/dist/extensions/feature-flags/feature-flags.d.ts.map +1 -0
  71. package/dist/extensions/feature-flags/feature-flags.js +529 -0
  72. package/dist/extensions/feature-flags/feature-flags.mjs +483 -0
  73. package/dist/extensions/feature-flags/lazy.d.ts +24 -0
  74. package/dist/extensions/feature-flags/lazy.d.ts.map +1 -0
  75. package/dist/extensions/feature-flags/lazy.js +60 -0
  76. package/dist/extensions/feature-flags/lazy.mjs +26 -0
  77. package/dist/extensions/sentry-integration.d.ts +54 -0
  78. package/dist/extensions/sentry-integration.d.ts.map +1 -0
  79. package/dist/extensions/sentry-integration.js +113 -0
  80. package/dist/extensions/sentry-integration.mjs +73 -0
  81. package/dist/storage-memory.d.ts +7 -0
  82. package/dist/storage-memory.d.ts.map +1 -0
  83. package/dist/storage-memory.js +46 -0
  84. package/dist/storage-memory.mjs +12 -0
  85. package/dist/types.d.ts +253 -0
  86. package/dist/types.d.ts.map +1 -0
  87. package/dist/types.js +18 -0
  88. package/dist/types.mjs +0 -0
  89. package/dist/utils/logger.d.ts +3 -0
  90. package/dist/utils/logger.d.ts.map +1 -0
  91. package/dist/utils/logger.js +63 -0
  92. package/dist/utils/logger.mjs +29 -0
  93. package/dist/version.d.ts +2 -0
  94. package/dist/version.d.ts.map +1 -0
  95. package/dist/version.js +36 -0
  96. package/dist/version.mjs +2 -0
  97. package/package.json +32 -31
  98. package/src/client.ts +1532 -0
  99. package/src/entrypoints/index.edge.ts +15 -0
  100. package/src/entrypoints/index.node.ts +17 -0
  101. package/src/exports.ts +3 -0
  102. package/src/extensions/error-tracking/autocapture.ts +65 -0
  103. package/src/extensions/error-tracking/chunk-ids.ts +58 -0
  104. package/src/extensions/error-tracking/context-lines.node.ts +392 -0
  105. package/src/extensions/error-tracking/error-conversion.ts +291 -0
  106. package/src/extensions/error-tracking/get-module.node.ts +57 -0
  107. package/src/extensions/error-tracking/index.ts +103 -0
  108. package/src/extensions/error-tracking/reduceable-cache.ts +39 -0
  109. package/src/extensions/error-tracking/stack-parser.ts +212 -0
  110. package/src/extensions/error-tracking/type-checking.ts +40 -0
  111. package/src/extensions/error-tracking/types.ts +71 -0
  112. package/src/extensions/express.ts +39 -0
  113. package/src/extensions/feature-flags/crypto-helpers.ts +36 -0
  114. package/src/extensions/feature-flags/crypto.ts +22 -0
  115. package/src/extensions/feature-flags/feature-flags.ts +1003 -0
  116. package/src/extensions/feature-flags/lazy.ts +55 -0
  117. package/src/extensions/sentry-integration.ts +216 -0
  118. package/src/storage-memory.ts +13 -0
  119. package/src/types.ts +294 -0
  120. package/src/utils/logger.ts +39 -0
  121. package/src/version.ts +1 -0
  122. package/dist/edge/index.cjs +0 -3150
  123. package/dist/edge/index.cjs.map +0 -1
  124. package/dist/edge/index.mjs +0 -3144
  125. package/dist/edge/index.mjs.map +0 -1
  126. package/dist/node/index.cjs +0 -3556
  127. package/dist/node/index.cjs.map +0 -1
  128. package/dist/node/index.mjs +0 -3550
  129. 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
+ }