@posthog/core 1.1.0 → 1.2.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/error-tracking/chunk-ids.js +1 -1
- package/dist/error-tracking/chunk-ids.mjs +1 -1
- package/dist/error-tracking/coercers/error-event-coercer.js +4 -5
- package/dist/error-tracking/coercers/error-event-coercer.mjs +4 -5
- package/dist/error-tracking/coercers/event-coercer.js +1 -2
- package/dist/error-tracking/coercers/event-coercer.mjs +1 -2
- package/dist/error-tracking/coercers/object-coercer.js +1 -2
- package/dist/error-tracking/coercers/object-coercer.mjs +1 -2
- package/dist/error-tracking/coercers/primitive-coercer.js +1 -2
- package/dist/error-tracking/coercers/primitive-coercer.mjs +1 -2
- package/dist/error-tracking/coercers/promise-rejection-event.js +4 -5
- package/dist/error-tracking/coercers/promise-rejection-event.mjs +4 -5
- package/dist/error-tracking/coercers/string-coercer.js +3 -4
- package/dist/error-tracking/coercers/string-coercer.mjs +3 -4
- package/dist/error-tracking/coercers/utils.js +2 -4
- package/dist/error-tracking/coercers/utils.mjs +2 -4
- package/dist/error-tracking/error-properties-builder.js +11 -15
- package/dist/error-tracking/error-properties-builder.mjs +11 -15
- package/dist/error-tracking/parsers/index.js +2 -4
- package/dist/error-tracking/parsers/index.mjs +2 -4
- package/dist/error-tracking/parsers/node.js +3 -5
- package/dist/error-tracking/parsers/node.mjs +3 -5
- package/dist/error-tracking/utils.js +4 -4
- package/dist/error-tracking/utils.mjs +4 -4
- package/dist/eventemitter.js +4 -4
- package/dist/eventemitter.mjs +4 -4
- package/dist/featureFlagUtils.js +20 -45
- package/dist/featureFlagUtils.mjs +20 -45
- package/dist/gzip.js +1 -2
- package/dist/gzip.mjs +1 -2
- package/dist/index.d.ts +4 -366
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +54 -1225
- package/dist/index.mjs +5 -1190
- package/dist/posthog-core-stateless.d.ts +204 -0
- package/dist/posthog-core-stateless.d.ts.map +1 -0
- package/dist/posthog-core-stateless.js +675 -0
- package/dist/posthog-core-stateless.mjs +632 -0
- package/dist/posthog-core.d.ts +171 -0
- package/dist/posthog-core.d.ts.map +1 -0
- package/dist/posthog-core.js +554 -0
- package/dist/posthog-core.mjs +520 -0
- package/dist/testing/PostHogCoreTestClient.d.ts +2 -1
- package/dist/testing/PostHogCoreTestClient.d.ts.map +1 -1
- package/dist/testing/PostHogCoreTestClient.js +9 -11
- package/dist/testing/PostHogCoreTestClient.mjs +8 -10
- package/dist/testing/test-utils.js +1 -1
- package/dist/testing/test-utils.mjs +1 -1
- package/dist/utils/bucketed-rate-limiter.js +8 -12
- package/dist/utils/bucketed-rate-limiter.mjs +8 -12
- package/dist/utils/index.js +3 -3
- package/dist/utils/index.mjs +3 -3
- package/dist/utils/type-utils.js +1 -1
- package/dist/utils/type-utils.mjs +1 -1
- package/dist/vendor/uuidv7.js +12 -16
- package/dist/vendor/uuidv7.mjs +12 -16
- package/package.json +3 -2
- package/src/__tests__/featureFlagUtils.spec.ts +427 -0
- package/src/__tests__/gzip.spec.ts +69 -0
- package/src/__tests__/posthog.ai.spec.ts +110 -0
- package/src/__tests__/posthog.capture.spec.ts +91 -0
- package/src/__tests__/posthog.core.spec.ts +135 -0
- package/src/__tests__/posthog.debug.spec.ts +36 -0
- package/src/__tests__/posthog.enqueue.spec.ts +93 -0
- package/src/__tests__/posthog.featureflags.spec.ts +1106 -0
- package/src/__tests__/posthog.featureflags.v1.spec.ts +922 -0
- package/src/__tests__/posthog.flush.spec.ts +237 -0
- package/src/__tests__/posthog.gdpr.spec.ts +50 -0
- package/src/__tests__/posthog.groups.spec.ts +96 -0
- package/src/__tests__/posthog.identify.spec.ts +194 -0
- package/src/__tests__/posthog.init.spec.ts +110 -0
- package/src/__tests__/posthog.listeners.spec.ts +51 -0
- package/src/__tests__/posthog.register.spec.ts +47 -0
- package/src/__tests__/posthog.reset.spec.ts +76 -0
- package/src/__tests__/posthog.sessions.spec.ts +63 -0
- package/src/__tests__/posthog.setProperties.spec.ts +102 -0
- package/src/__tests__/posthog.shutdown.spec.ts +88 -0
- package/src/__tests__/utils.spec.ts +36 -0
- package/src/error-tracking/chunk-ids.ts +58 -0
- package/src/error-tracking/coercers/dom-exception-coercer.ts +38 -0
- package/src/error-tracking/coercers/error-coercer.ts +36 -0
- package/src/error-tracking/coercers/error-event-coercer.ts +24 -0
- package/src/error-tracking/coercers/event-coercer.ts +19 -0
- package/src/error-tracking/coercers/index.ts +8 -0
- package/src/error-tracking/coercers/object-coercer.ts +76 -0
- package/src/error-tracking/coercers/primitive-coercer.ts +19 -0
- package/src/error-tracking/coercers/promise-rejection-event.spec.ts +77 -0
- package/src/error-tracking/coercers/promise-rejection-event.ts +53 -0
- package/src/error-tracking/coercers/string-coercer.spec.ts +26 -0
- package/src/error-tracking/coercers/string-coercer.ts +31 -0
- package/src/error-tracking/coercers/utils.ts +33 -0
- package/src/error-tracking/error-properties-builder.coerce.spec.ts +202 -0
- package/src/error-tracking/error-properties-builder.parse.spec.ts +30 -0
- package/src/error-tracking/error-properties-builder.ts +169 -0
- package/src/error-tracking/index.ts +5 -0
- package/src/error-tracking/parsers/base.ts +29 -0
- package/src/error-tracking/parsers/chrome.ts +53 -0
- package/src/error-tracking/parsers/gecko.ts +38 -0
- package/src/error-tracking/parsers/index.ts +104 -0
- package/src/error-tracking/parsers/node.ts +111 -0
- package/src/error-tracking/parsers/opera.ts +18 -0
- package/src/error-tracking/parsers/react-native.ts +0 -0
- package/src/error-tracking/parsers/safari.ts +33 -0
- package/src/error-tracking/parsers/winjs.ts +12 -0
- package/src/error-tracking/types.ts +107 -0
- package/src/error-tracking/utils.ts +39 -0
- package/src/eventemitter.ts +27 -0
- package/src/featureFlagUtils.ts +192 -0
- package/src/gzip.ts +29 -0
- package/src/index.ts +8 -0
- package/src/posthog-core-stateless.ts +1226 -0
- package/src/posthog-core.ts +958 -0
- package/src/testing/PostHogCoreTestClient.ts +91 -0
- package/src/testing/index.ts +2 -0
- package/src/testing/test-utils.ts +47 -0
- package/src/types.ts +544 -0
- package/src/utils/bucketed-rate-limiter.spec.ts +33 -0
- package/src/utils/bucketed-rate-limiter.ts +85 -0
- package/src/utils/index.ts +98 -0
- package/src/utils/number-utils.spec.ts +89 -0
- package/src/utils/number-utils.ts +30 -0
- package/src/utils/promise-queue.spec.ts +55 -0
- package/src/utils/promise-queue.ts +30 -0
- package/src/utils/string-utils.ts +23 -0
- package/src/utils/type-utils.ts +134 -0
- package/src/vendor/uuidv7.ts +479 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { getFilenameToChunkIdMap } from './chunk-ids'
|
|
2
|
+
import { createStackParser } from './parsers'
|
|
3
|
+
import {
|
|
4
|
+
ErrorProperties,
|
|
5
|
+
ExceptionLike,
|
|
6
|
+
ExceptionList,
|
|
7
|
+
CoercingContext,
|
|
8
|
+
StackFrame,
|
|
9
|
+
StackFrameModifierFn,
|
|
10
|
+
StackParser,
|
|
11
|
+
ErrorTrackingCoercer,
|
|
12
|
+
EventHint,
|
|
13
|
+
StackLineParser,
|
|
14
|
+
ParsingContext,
|
|
15
|
+
ChunkIdMapType,
|
|
16
|
+
Mechanism,
|
|
17
|
+
ParsedException,
|
|
18
|
+
Exception,
|
|
19
|
+
} from './types'
|
|
20
|
+
|
|
21
|
+
const MAX_CAUSE_RECURSION = 4
|
|
22
|
+
|
|
23
|
+
export class ErrorPropertiesBuilder {
|
|
24
|
+
stackParser: StackParser
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
private coercers: ErrorTrackingCoercer<any>[] = [],
|
|
28
|
+
parsers: StackLineParser[] = [],
|
|
29
|
+
private modifiers: StackFrameModifierFn[] = []
|
|
30
|
+
) {
|
|
31
|
+
this.stackParser = createStackParser(...parsers)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
buildFromUnknown(input: unknown, hint: EventHint = {}): ErrorProperties {
|
|
35
|
+
const providedMechanism = hint && hint.mechanism
|
|
36
|
+
const mechanism = providedMechanism || {
|
|
37
|
+
handled: true,
|
|
38
|
+
type: 'generic',
|
|
39
|
+
}
|
|
40
|
+
const coercingContext: CoercingContext = this.buildCoercingContext(mechanism, hint, 0)
|
|
41
|
+
const exceptionWithCause = coercingContext.apply(input)
|
|
42
|
+
const parsingContext: ParsingContext = this.buildParsingContext()
|
|
43
|
+
const exceptionWithStack = this.parseStacktrace(exceptionWithCause, parsingContext)
|
|
44
|
+
const exceptionList = this.convertToExceptionList(exceptionWithStack, mechanism)
|
|
45
|
+
return {
|
|
46
|
+
$exception_list: exceptionList,
|
|
47
|
+
$exception_level: 'error',
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
coerceFallback(ctx: CoercingContext): ExceptionLike {
|
|
52
|
+
return {
|
|
53
|
+
type: 'Error',
|
|
54
|
+
value: 'Unknown error',
|
|
55
|
+
stack: ctx.syntheticException?.stack,
|
|
56
|
+
synthetic: true,
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
parseStacktrace(err: ExceptionLike, ctx: ParsingContext): ParsedException {
|
|
61
|
+
let cause: ParsedException | undefined = undefined
|
|
62
|
+
if (err.cause != null) {
|
|
63
|
+
cause = this.parseStacktrace(err.cause, ctx)
|
|
64
|
+
}
|
|
65
|
+
let stack: StackFrame[] | undefined = undefined
|
|
66
|
+
if (err.stack != '' && err.stack != null) {
|
|
67
|
+
stack = this.applyChunkIds(this.stackParser(err.stack, err.synthetic ? 1 : 0), ctx.chunkIdMap)
|
|
68
|
+
}
|
|
69
|
+
return { ...err, cause, stack }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private applyChunkIds(frames: StackFrame[], chunkIdMap?: ChunkIdMapType): StackFrame[] {
|
|
73
|
+
return frames.map((frame) => {
|
|
74
|
+
if (frame.filename && chunkIdMap) {
|
|
75
|
+
frame.chunk_id = chunkIdMap[frame.filename]
|
|
76
|
+
}
|
|
77
|
+
return frame
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private applyCoercers(input: unknown, ctx: CoercingContext): ExceptionLike | undefined {
|
|
82
|
+
for (const adapter of this.coercers) {
|
|
83
|
+
if (adapter.match(input)) {
|
|
84
|
+
return adapter.coerce(input, ctx)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return this.coerceFallback(ctx)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private async applyModifiers(frames: StackFrame[]): Promise<StackFrame[]> {
|
|
91
|
+
let newFrames = frames
|
|
92
|
+
for (const modifier of this.modifiers) {
|
|
93
|
+
newFrames = await modifier(newFrames)
|
|
94
|
+
}
|
|
95
|
+
return newFrames
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private async modifyFrames(exceptionWithStack: ParsedException): Promise<ParsedException> {
|
|
99
|
+
let cause: ParsedException | undefined = undefined
|
|
100
|
+
if (exceptionWithStack.cause != null) {
|
|
101
|
+
cause = await this.modifyFrames(exceptionWithStack.cause)
|
|
102
|
+
}
|
|
103
|
+
let stack: StackFrame[] = []
|
|
104
|
+
if (exceptionWithStack.stack != null) {
|
|
105
|
+
stack = await this.applyModifiers(exceptionWithStack.stack)
|
|
106
|
+
}
|
|
107
|
+
return { ...exceptionWithStack, cause, stack }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private convertToExceptionList(exceptionWithStack: ParsedException, mechanism: Mechanism): ExceptionList {
|
|
111
|
+
const currentException: Exception = {
|
|
112
|
+
type: exceptionWithStack.type,
|
|
113
|
+
value: exceptionWithStack.value,
|
|
114
|
+
mechanism: {
|
|
115
|
+
type: mechanism.type ?? 'generic',
|
|
116
|
+
handled: mechanism.handled ?? true,
|
|
117
|
+
synthetic: exceptionWithStack.synthetic ?? false,
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
if (exceptionWithStack.stack) {
|
|
121
|
+
currentException.stacktrace = {
|
|
122
|
+
type: 'raw',
|
|
123
|
+
frames: exceptionWithStack.stack,
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const exceptionList: ExceptionList = [currentException]
|
|
127
|
+
if (exceptionWithStack.cause != null) {
|
|
128
|
+
// Cause errors are necessarily handled
|
|
129
|
+
exceptionList.push(
|
|
130
|
+
...this.convertToExceptionList(exceptionWithStack.cause, {
|
|
131
|
+
...mechanism,
|
|
132
|
+
handled: true,
|
|
133
|
+
})
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
return exceptionList
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
buildParsingContext(): ParsingContext {
|
|
140
|
+
const context = {
|
|
141
|
+
chunkIdMap: getFilenameToChunkIdMap(this.stackParser),
|
|
142
|
+
} as ParsingContext
|
|
143
|
+
return context
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
buildCoercingContext(mechanism: Mechanism, hint: EventHint, depth: number = 0): CoercingContext {
|
|
147
|
+
const coerce = (input: unknown, depth: number) => {
|
|
148
|
+
if (depth <= MAX_CAUSE_RECURSION) {
|
|
149
|
+
const ctx = this.buildCoercingContext(mechanism, hint, depth)
|
|
150
|
+
return this.applyCoercers(input, ctx)
|
|
151
|
+
} else {
|
|
152
|
+
return undefined
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const context = {
|
|
156
|
+
...hint,
|
|
157
|
+
// Do not propagate synthetic exception as it doesn't make sense
|
|
158
|
+
syntheticException: depth == 0 ? hint.syntheticException : undefined,
|
|
159
|
+
mechanism,
|
|
160
|
+
apply: (input: unknown) => {
|
|
161
|
+
return coerce(input, depth)
|
|
162
|
+
},
|
|
163
|
+
next: (input: unknown) => {
|
|
164
|
+
return coerce(input, depth + 1)
|
|
165
|
+
},
|
|
166
|
+
} as CoercingContext
|
|
167
|
+
return context
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { isUndefined } from '@/utils'
|
|
2
|
+
import { StackFrame } from '../types'
|
|
3
|
+
|
|
4
|
+
export const UNKNOWN_FUNCTION = '?'
|
|
5
|
+
export const OPERA10_PRIORITY = 10
|
|
6
|
+
export const OPERA11_PRIORITY = 20
|
|
7
|
+
export const CHROME_PRIORITY = 30
|
|
8
|
+
export const WINJS_PRIORITY = 40
|
|
9
|
+
export const GECKO_PRIORITY = 50
|
|
10
|
+
|
|
11
|
+
export function createFrame(filename: string, func: string, lineno?: number, colno?: number): StackFrame {
|
|
12
|
+
const frame: StackFrame = {
|
|
13
|
+
// TODO: should be a variable here
|
|
14
|
+
platform: 'web:javascript',
|
|
15
|
+
filename,
|
|
16
|
+
function: func === '<anonymous>' ? UNKNOWN_FUNCTION : func,
|
|
17
|
+
in_app: true, // All browser frames are considered in_app
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!isUndefined(lineno)) {
|
|
21
|
+
frame.lineno = lineno
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!isUndefined(colno)) {
|
|
25
|
+
frame.colno = colno
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return frame
|
|
29
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// This regex matches frames that have no function name (ie. are at the top level of a module).
|
|
2
|
+
// For example "at http://localhost:5000//script.js:1:126"
|
|
3
|
+
|
|
4
|
+
import { StackLineParser } from '../types'
|
|
5
|
+
import { createFrame, UNKNOWN_FUNCTION } from './base'
|
|
6
|
+
import { extractSafariExtensionDetails } from './safari'
|
|
7
|
+
|
|
8
|
+
// Frames _with_ function names usually look as follows: "at commitLayoutEffects (react-dom.development.js:23426:1)"
|
|
9
|
+
const chromeRegexNoFnName = /^\s*at (\S+?)(?::(\d+))(?::(\d+))\s*$/i
|
|
10
|
+
|
|
11
|
+
// This regex matches all the frames that have a function name.
|
|
12
|
+
const chromeRegex =
|
|
13
|
+
/^\s*at (?:(.+?\)(?: \[.+\])?|.*?) ?\((?:address at )?)?(?:async )?((?:<anonymous>|[-a-z]+:|.*bundle|\/)?.*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i
|
|
14
|
+
|
|
15
|
+
const chromeEvalRegex = /\((\S*)(?::(\d+))(?::(\d+))\)/
|
|
16
|
+
|
|
17
|
+
// Chromium based browsers: Chrome, Brave, new Opera, new Edge
|
|
18
|
+
// We cannot call this variable `chrome` because it can conflict with global `chrome` variable in certain environments
|
|
19
|
+
// See: https://github.com/getsentry/sentry-javascript/issues/6880
|
|
20
|
+
export const chromeStackLineParser: StackLineParser = (line) => {
|
|
21
|
+
// If the stack line has no function name, we need to parse it differently
|
|
22
|
+
const noFnParts = chromeRegexNoFnName.exec(line) as null | [string, string, string, string]
|
|
23
|
+
|
|
24
|
+
if (noFnParts) {
|
|
25
|
+
const [, filename, line, col] = noFnParts
|
|
26
|
+
return createFrame(filename, UNKNOWN_FUNCTION, +line, +col)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const parts = chromeRegex.exec(line) as null | [string, string, string, string, string]
|
|
30
|
+
|
|
31
|
+
if (parts) {
|
|
32
|
+
const isEval = parts[2] && parts[2].indexOf('eval') === 0 // start of line
|
|
33
|
+
|
|
34
|
+
if (isEval) {
|
|
35
|
+
const subMatch = chromeEvalRegex.exec(parts[2]) as null | [string, string, string, string]
|
|
36
|
+
|
|
37
|
+
if (subMatch) {
|
|
38
|
+
// throw out eval line/column and use top-most line/column number
|
|
39
|
+
parts[2] = subMatch[1] // url
|
|
40
|
+
parts[3] = subMatch[2] // line
|
|
41
|
+
parts[4] = subMatch[3] // column
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Kamil: One more hack won't hurt us right? Understanding and adding more rules on top of these regexps right now
|
|
46
|
+
// would be way too time consuming. (TODO: Rewrite whole RegExp to be more readable)
|
|
47
|
+
const [func, filename] = extractSafariExtensionDetails(parts[1] || UNKNOWN_FUNCTION, parts[2])
|
|
48
|
+
|
|
49
|
+
return createFrame(filename, func, parts[3] ? +parts[3] : undefined, parts[4] ? +parts[4] : undefined)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return
|
|
53
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// gecko regex: `(?:bundle|\d+\.js)`: `bundle` is for react native, `\d+\.js` also but specifically for ram bundles because it
|
|
2
|
+
// generates filenames without a prefix like `file://` the filenames in the stacktrace are just 42.js
|
|
3
|
+
|
|
4
|
+
import { StackLineParser } from '../types'
|
|
5
|
+
import { createFrame, UNKNOWN_FUNCTION } from './base'
|
|
6
|
+
import { extractSafariExtensionDetails } from './safari'
|
|
7
|
+
|
|
8
|
+
// We need this specific case for now because we want no other regex to match.
|
|
9
|
+
const geckoREgex =
|
|
10
|
+
/^\s*(.*?)(?:\((.*?)\))?(?:^|@)?((?:[-a-z]+)?:\/.*?|\[native code\]|[^@]*(?:bundle|\d+\.js)|\/[\w\-. /=]+)(?::(\d+))?(?::(\d+))?\s*$/i
|
|
11
|
+
const geckoEvalRegex = /(\S+) line (\d+)(?: > eval line \d+)* > eval/i
|
|
12
|
+
|
|
13
|
+
export const geckoStackLineParser: StackLineParser = (line) => {
|
|
14
|
+
const parts = geckoREgex.exec(line) as null | [string, string, string, string, string, string]
|
|
15
|
+
|
|
16
|
+
if (parts) {
|
|
17
|
+
const isEval = parts[3] && parts[3].indexOf(' > eval') > -1
|
|
18
|
+
if (isEval) {
|
|
19
|
+
const subMatch = geckoEvalRegex.exec(parts[3]) as null | [string, string, string]
|
|
20
|
+
|
|
21
|
+
if (subMatch) {
|
|
22
|
+
// throw out eval line/column and use top-most line number
|
|
23
|
+
parts[1] = parts[1] || 'eval'
|
|
24
|
+
parts[3] = subMatch[1]
|
|
25
|
+
parts[4] = subMatch[2]
|
|
26
|
+
parts[5] = '' // no column when eval
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let filename = parts[3]
|
|
31
|
+
let func = parts[1] || UNKNOWN_FUNCTION
|
|
32
|
+
;[func, filename] = extractSafariExtensionDetails(func, filename)
|
|
33
|
+
|
|
34
|
+
return createFrame(filename, func, parts[4] ? +parts[4] : undefined, parts[5] ? +parts[5] : undefined)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return
|
|
38
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Portions of this file are derived from getsentry/sentry-javascript by Software, Inc. dba Sentry
|
|
2
|
+
// Licensed under the MIT License
|
|
3
|
+
|
|
4
|
+
// 💖 open source
|
|
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
|
+
import { StackFrame, StackLineParser, StackParser } from '../types'
|
|
32
|
+
import { UNKNOWN_FUNCTION } from './base'
|
|
33
|
+
export { chromeStackLineParser } from './chrome'
|
|
34
|
+
export { winjsStackLineParser } from './winjs'
|
|
35
|
+
export { geckoStackLineParser } from './gecko'
|
|
36
|
+
export { opera10StackLineParser, opera11StackLineParser } from './opera'
|
|
37
|
+
export { nodeStackLineParser } from './node'
|
|
38
|
+
|
|
39
|
+
const WEBPACK_ERROR_REGEXP = /\(error: (.*)\)/
|
|
40
|
+
const STACKTRACE_FRAME_LIMIT = 50
|
|
41
|
+
|
|
42
|
+
export function reverseAndStripFrames(stack: ReadonlyArray<StackFrame>): StackFrame[] {
|
|
43
|
+
if (!stack.length) {
|
|
44
|
+
return []
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const localStack = Array.from(stack)
|
|
48
|
+
|
|
49
|
+
localStack.reverse()
|
|
50
|
+
|
|
51
|
+
return localStack.slice(0, STACKTRACE_FRAME_LIMIT).map((frame) => ({
|
|
52
|
+
...frame,
|
|
53
|
+
filename: frame.filename || getLastStackFrame(localStack).filename,
|
|
54
|
+
function: frame.function || UNKNOWN_FUNCTION,
|
|
55
|
+
}))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getLastStackFrame(arr: StackFrame[]): StackFrame {
|
|
59
|
+
return arr[arr.length - 1] || {}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createStackParser(...parsers: StackLineParser[]): StackParser {
|
|
63
|
+
// const sortedParsers = parsers.sort((a, b) => a[0] - b[0]).map((p) => p[1])
|
|
64
|
+
|
|
65
|
+
return (stack: string, skipFirstLines: number = 0): StackFrame[] => {
|
|
66
|
+
const frames: StackFrame[] = []
|
|
67
|
+
const lines = stack.split('\n')
|
|
68
|
+
|
|
69
|
+
for (let i = skipFirstLines; i < lines.length; i++) {
|
|
70
|
+
const line = lines[i] as string
|
|
71
|
+
// Ignore lines over 1kb as they are unlikely to be stack frames.
|
|
72
|
+
// Many of the regular expressions use backtracking which results in run time that increases exponentially with
|
|
73
|
+
// input size. Huge strings can result in hangs/Denial of Service:
|
|
74
|
+
// https://github.com/getsentry/sentry-javascript/issues/2286
|
|
75
|
+
if (line.length > 1024) {
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// https://github.com/getsentry/sentry-javascript/issues/5459
|
|
80
|
+
// Remove webpack (error: *) wrappers
|
|
81
|
+
const cleanedLine = WEBPACK_ERROR_REGEXP.test(line) ? line.replace(WEBPACK_ERROR_REGEXP, '$1') : line
|
|
82
|
+
|
|
83
|
+
// https://github.com/getsentry/sentry-javascript/issues/7813
|
|
84
|
+
// Skip Error: lines
|
|
85
|
+
if (cleanedLine.match(/\S*Error: /)) {
|
|
86
|
+
continue
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const parser of parsers) {
|
|
90
|
+
const frame = parser(cleanedLine)
|
|
91
|
+
if (frame) {
|
|
92
|
+
frames.push(frame)
|
|
93
|
+
break
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (frames.length >= STACKTRACE_FRAME_LIMIT) {
|
|
98
|
+
break
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return reverseAndStripFrames(frames)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { StackLineParser } from '../types'
|
|
2
|
+
import { UNKNOWN_FUNCTION } from './base'
|
|
3
|
+
|
|
4
|
+
/** Node Stack line parser */
|
|
5
|
+
const FILENAME_MATCH = /^\s*[-]{4,}$/
|
|
6
|
+
const FULL_MATCH = /at (?:async )?(?:(.+?)\s+\()?(?:(.+):(\d+):(\d+)?|([^)]+))\)?/
|
|
7
|
+
|
|
8
|
+
export const nodeStackLineParser: StackLineParser = (line: string) => {
|
|
9
|
+
const lineMatch = line.match(FULL_MATCH)
|
|
10
|
+
|
|
11
|
+
if (lineMatch) {
|
|
12
|
+
let object: string | undefined
|
|
13
|
+
let method: string | undefined
|
|
14
|
+
let functionName: string | undefined
|
|
15
|
+
let typeName: string | undefined
|
|
16
|
+
let methodName: string | undefined
|
|
17
|
+
|
|
18
|
+
if (lineMatch[1]) {
|
|
19
|
+
functionName = lineMatch[1]
|
|
20
|
+
|
|
21
|
+
let methodStart = functionName.lastIndexOf('.')
|
|
22
|
+
if (functionName[methodStart - 1] === '.') {
|
|
23
|
+
methodStart--
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (methodStart > 0) {
|
|
27
|
+
object = functionName.slice(0, methodStart)
|
|
28
|
+
method = functionName.slice(methodStart + 1)
|
|
29
|
+
const objectEnd = object.indexOf('.Module')
|
|
30
|
+
if (objectEnd > 0) {
|
|
31
|
+
functionName = functionName.slice(objectEnd + 1)
|
|
32
|
+
object = object.slice(0, objectEnd)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
typeName = undefined
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (method) {
|
|
39
|
+
typeName = object
|
|
40
|
+
methodName = method
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (method === '<anonymous>') {
|
|
44
|
+
methodName = undefined
|
|
45
|
+
functionName = undefined
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (functionName === undefined) {
|
|
49
|
+
methodName = methodName || UNKNOWN_FUNCTION
|
|
50
|
+
functionName = typeName ? `${typeName}.${methodName}` : methodName
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let filename = lineMatch[2]?.startsWith('file://') ? lineMatch[2].slice(7) : lineMatch[2]
|
|
54
|
+
const isNative = lineMatch[5] === 'native'
|
|
55
|
+
|
|
56
|
+
// If it's a Windows path, trim the leading slash so that `/C:/foo` becomes `C:/foo`
|
|
57
|
+
if (filename?.match(/\/[A-Z]:/)) {
|
|
58
|
+
filename = filename.slice(1)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!filename && lineMatch[5] && !isNative) {
|
|
62
|
+
filename = lineMatch[5]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
filename: filename ? decodeURI(filename) : undefined,
|
|
67
|
+
module: undefined,
|
|
68
|
+
function: functionName,
|
|
69
|
+
lineno: _parseIntOrUndefined(lineMatch[3]),
|
|
70
|
+
colno: _parseIntOrUndefined(lineMatch[4]),
|
|
71
|
+
in_app: filenameIsInApp(filename || '', isNative),
|
|
72
|
+
platform: 'node:javascript',
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (line.match(FILENAME_MATCH)) {
|
|
77
|
+
return {
|
|
78
|
+
filename: line,
|
|
79
|
+
platform: 'node:javascript',
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return undefined
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Does this filename look like it's part of the app code?
|
|
88
|
+
*/
|
|
89
|
+
function filenameIsInApp(filename: string, isNative: boolean = false): boolean {
|
|
90
|
+
const isInternal =
|
|
91
|
+
isNative ||
|
|
92
|
+
(filename &&
|
|
93
|
+
// It's not internal if it's an absolute linux path
|
|
94
|
+
!filename.startsWith('/') &&
|
|
95
|
+
// It's not internal if it's an absolute windows path
|
|
96
|
+
!filename.match(/^[A-Z]:/) &&
|
|
97
|
+
// It's not internal if the path is starting with a dot
|
|
98
|
+
!filename.startsWith('.') &&
|
|
99
|
+
// 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
|
|
100
|
+
!filename.match(/^[a-zA-Z]([a-zA-Z0-9.\-+])*:\/\//)) // Schema from: https://stackoverflow.com/a/3641782
|
|
101
|
+
|
|
102
|
+
// in_app is all that's not an internal Node function or a module within node_modules
|
|
103
|
+
// note that isNative appears to return true even for node core libraries
|
|
104
|
+
// see https://github.com/getsentry/raven-node/issues/176
|
|
105
|
+
|
|
106
|
+
return !isInternal && filename !== undefined && !filename.includes('node_modules/')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function _parseIntOrUndefined(input: string | undefined): number | undefined {
|
|
110
|
+
return parseInt(input || '', 10) || undefined
|
|
111
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { StackLineParser } from '../types'
|
|
2
|
+
import { createFrame, UNKNOWN_FUNCTION } from './base'
|
|
3
|
+
|
|
4
|
+
const opera10Regex = / line (\d+).*script (?:in )?(\S+)(?:: in function (\S+))?$/i
|
|
5
|
+
|
|
6
|
+
export const opera10StackLineParser: StackLineParser = (line) => {
|
|
7
|
+
const parts = opera10Regex.exec(line) as null | [string, string, string, string]
|
|
8
|
+
return parts ? createFrame(parts[2], parts[3] || UNKNOWN_FUNCTION, +parts[1]) : undefined
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// export const opera10StackLineParser: StackLineParser = [OPERA10_PRIORITY, opera10]
|
|
12
|
+
|
|
13
|
+
const opera11Regex = / line (\d+), column (\d+)\s*(?:in (?:<anonymous function: ([^>]+)>|([^)]+))\(.*\))? in (.*):\s*$/i
|
|
14
|
+
|
|
15
|
+
export const opera11StackLineParser: StackLineParser = (line) => {
|
|
16
|
+
const parts = opera11Regex.exec(line) as null | [string, string, string, string, string, string]
|
|
17
|
+
return parts ? createFrame(parts[5], parts[3] || parts[4] || UNKNOWN_FUNCTION, +parts[1], +parts[2]) : undefined
|
|
18
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { UNKNOWN_FUNCTION } from './base'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Safari web extensions, starting version unknown, can produce "frames-only" stacktraces.
|
|
5
|
+
* What it means, is that instead of format like:
|
|
6
|
+
*
|
|
7
|
+
* Error: wat
|
|
8
|
+
* at function@url:row:col
|
|
9
|
+
* at function@url:row:col
|
|
10
|
+
* at function@url:row:col
|
|
11
|
+
*
|
|
12
|
+
* it produces something like:
|
|
13
|
+
*
|
|
14
|
+
* function@url:row:col
|
|
15
|
+
* function@url:row:col
|
|
16
|
+
* function@url:row:col
|
|
17
|
+
*
|
|
18
|
+
* Because of that, it won't be captured by `chrome` RegExp and will fall into `Gecko` branch.
|
|
19
|
+
* This function is extracted so that we can use it in both places without duplicating the logic.
|
|
20
|
+
* Unfortunately "just" changing RegExp is too complicated now and making it pass all tests
|
|
21
|
+
* and fix this case seems like an impossible, or at least way too time-consuming task.
|
|
22
|
+
*/
|
|
23
|
+
export const extractSafariExtensionDetails = (func: string, filename: string): [string, string] => {
|
|
24
|
+
const isSafariExtension = func.indexOf('safari-extension') !== -1
|
|
25
|
+
const isSafariWebExtension = func.indexOf('safari-web-extension') !== -1
|
|
26
|
+
|
|
27
|
+
return isSafariExtension || isSafariWebExtension
|
|
28
|
+
? [
|
|
29
|
+
func.indexOf('@') !== -1 ? (func.split('@')[0] as string) : UNKNOWN_FUNCTION,
|
|
30
|
+
isSafariExtension ? `safari-extension:${filename}` : `safari-web-extension:${filename}`,
|
|
31
|
+
]
|
|
32
|
+
: [func, filename]
|
|
33
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { StackLineParser } from '../types'
|
|
2
|
+
import { createFrame, UNKNOWN_FUNCTION } from './base'
|
|
3
|
+
|
|
4
|
+
const winjsRegex = /^\s*at (?:((?:\[object object\])?.+) )?\(?((?:[-a-z]+):.*?):(\d+)(?::(\d+))?\)?\s*$/i
|
|
5
|
+
|
|
6
|
+
export const winjsStackLineParser: StackLineParser = (line) => {
|
|
7
|
+
const parts = winjsRegex.exec(line) as null | [string, string, string, string, string]
|
|
8
|
+
|
|
9
|
+
return parts
|
|
10
|
+
? createFrame(parts[2], parts[1] || UNKNOWN_FUNCTION, +parts[3], parts[4] ? +parts[4] : undefined)
|
|
11
|
+
: undefined
|
|
12
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
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 type { JsonType } from '../types'
|
|
5
|
+
|
|
6
|
+
// levels originally copied from Sentry to work with the sentry integration
|
|
7
|
+
// and to avoid relying on a frequently changing @sentry/types dependency
|
|
8
|
+
// but provided as an array of literal types, so we can constrain the level below
|
|
9
|
+
export const severityLevels = ['fatal', 'error', 'warning', 'log', 'info', 'debug'] as const
|
|
10
|
+
export declare type SeverityLevel = (typeof severityLevels)[number]
|
|
11
|
+
|
|
12
|
+
export interface PolymorphicEvent {
|
|
13
|
+
[key: string]: unknown
|
|
14
|
+
readonly type: string
|
|
15
|
+
readonly target?: unknown
|
|
16
|
+
readonly currentTarget?: unknown
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface EventHint {
|
|
20
|
+
mechanism?: Partial<Mechanism>
|
|
21
|
+
syntheticException?: Error | null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ErrorProperties {
|
|
25
|
+
$exception_list: Exception[]
|
|
26
|
+
$exception_level?: SeverityLevel
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface Exception {
|
|
30
|
+
type?: string
|
|
31
|
+
value?: string
|
|
32
|
+
mechanism?: Mechanism
|
|
33
|
+
module?: string
|
|
34
|
+
thread_id?: number
|
|
35
|
+
stacktrace?: { frames?: StackFrame[]; type: 'raw' }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type ExceptionList = Exception[]
|
|
39
|
+
|
|
40
|
+
export interface Mechanism {
|
|
41
|
+
handled?: boolean
|
|
42
|
+
type?: 'generic' | 'onunhandledrejection' | 'onuncaughtexception' | 'middleware'
|
|
43
|
+
source?: string
|
|
44
|
+
synthetic?: boolean
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type GetModuleFn = (filename: string | undefined) => string | undefined
|
|
48
|
+
|
|
49
|
+
export type StackParser = (stack: string, skipFirstLines?: number) => StackFrame[]
|
|
50
|
+
export type StackLineParser = (line: string) => StackFrame | undefined
|
|
51
|
+
|
|
52
|
+
export type StackFrameModifierFn = (frames: StackFrame[]) => Promise<StackFrame[]>
|
|
53
|
+
|
|
54
|
+
export interface StackFrame {
|
|
55
|
+
platform: string
|
|
56
|
+
filename?: string
|
|
57
|
+
function?: string
|
|
58
|
+
module?: string
|
|
59
|
+
lineno?: number
|
|
60
|
+
colno?: number
|
|
61
|
+
abs_path?: string
|
|
62
|
+
context_line?: string
|
|
63
|
+
pre_context?: string[]
|
|
64
|
+
post_context?: string[]
|
|
65
|
+
in_app?: boolean
|
|
66
|
+
instruction_addr?: string
|
|
67
|
+
addr_mode?: string
|
|
68
|
+
vars?: { [key: string]: JsonType }
|
|
69
|
+
chunk_id?: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface CoercingContext extends EventHint {
|
|
73
|
+
// Used to forward to other types
|
|
74
|
+
apply: (input: unknown) => ExceptionLike
|
|
75
|
+
// Used to coerce nested exceptions
|
|
76
|
+
next: (input: unknown) => ExceptionLike | undefined
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type ChunkIdMapType = Record<string, string>
|
|
80
|
+
|
|
81
|
+
export interface ParsingContext {
|
|
82
|
+
chunkIdMap?: ChunkIdMapType
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface Coercer<T, U, C> {
|
|
86
|
+
match(input: unknown): input is T
|
|
87
|
+
coerce(input: T, ctx: C): U
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type ErrorTrackingCoercer<T> = Coercer<T, ExceptionLike | undefined, CoercingContext>
|
|
91
|
+
|
|
92
|
+
interface BaseException {
|
|
93
|
+
type: string
|
|
94
|
+
value: string
|
|
95
|
+
synthetic: boolean
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface ExceptionLike extends BaseException {
|
|
99
|
+
stack?: string
|
|
100
|
+
cause?: ExceptionLike
|
|
101
|
+
level?: SeverityLevel
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface ParsedException extends BaseException {
|
|
105
|
+
stack?: StackFrame[]
|
|
106
|
+
cause?: ParsedException
|
|
107
|
+
}
|