@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,76 @@
|
|
|
1
|
+
import { isEmptyString, isError, isEvent, isString } from '@/utils'
|
|
2
|
+
import { CoercingContext, ErrorTrackingCoercer, ExceptionLike, SeverityLevel, severityLevels } from '../types'
|
|
3
|
+
import { extractExceptionKeysForMessage } from './utils'
|
|
4
|
+
|
|
5
|
+
type ObjectLike = Record<string, unknown>
|
|
6
|
+
|
|
7
|
+
export class ObjectCoercer implements ErrorTrackingCoercer<ObjectLike> {
|
|
8
|
+
match(candidate: unknown): candidate is ObjectLike {
|
|
9
|
+
return typeof candidate === 'object' && candidate !== null
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
coerce(candidate: ObjectLike, ctx: CoercingContext): ExceptionLike | undefined {
|
|
13
|
+
const errorProperty = this.getErrorPropertyFromObject(candidate)
|
|
14
|
+
if (errorProperty) {
|
|
15
|
+
return ctx.apply(errorProperty)
|
|
16
|
+
} else {
|
|
17
|
+
return {
|
|
18
|
+
type: this.getType(candidate),
|
|
19
|
+
value: this.getValue(candidate),
|
|
20
|
+
stack: ctx.syntheticException?.stack,
|
|
21
|
+
level: this.isSeverityLevel(candidate.level) ? candidate.level : 'error',
|
|
22
|
+
synthetic: true,
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
getType(err: Record<string, unknown>): string {
|
|
28
|
+
return isEvent(err) ? err.constructor.name : 'Error'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getValue(err: object) {
|
|
32
|
+
if ('name' in err && typeof err.name === 'string') {
|
|
33
|
+
let message = `'${err.name}' captured as exception`
|
|
34
|
+
|
|
35
|
+
if ('message' in err && typeof err.message === 'string') {
|
|
36
|
+
message += ` with message: '${err.message}'`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return message
|
|
40
|
+
} else if ('message' in err && typeof err.message === 'string') {
|
|
41
|
+
return err.message
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const className = this.getObjectClassName(err)
|
|
45
|
+
const keys = extractExceptionKeysForMessage(err)
|
|
46
|
+
|
|
47
|
+
return `${className && className !== 'Object' ? `'${className}'` : 'Object'} captured as exception with keys: ${keys}`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private isSeverityLevel(x: unknown): x is SeverityLevel {
|
|
51
|
+
return isString(x) && !isEmptyString(x) && severityLevels.indexOf(x as SeverityLevel) >= 0
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** If a plain object has a property that is an `Error`, return this error. */
|
|
55
|
+
private getErrorPropertyFromObject(obj: Record<string, unknown>): Error | undefined {
|
|
56
|
+
for (const prop in obj) {
|
|
57
|
+
if (Object.prototype.hasOwnProperty.call(obj, prop)) {
|
|
58
|
+
const value = obj[prop]
|
|
59
|
+
if (isError(value)) {
|
|
60
|
+
return value
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return undefined
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private getObjectClassName(obj: unknown): string | undefined {
|
|
69
|
+
try {
|
|
70
|
+
const prototype: unknown | null = Object.getPrototypeOf(obj)
|
|
71
|
+
return prototype ? prototype.constructor.name : undefined
|
|
72
|
+
} catch (e) {
|
|
73
|
+
return undefined
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { isPrimitive } from '@/utils'
|
|
2
|
+
import { CoercingContext, ErrorTrackingCoercer, ExceptionLike } from '../types'
|
|
3
|
+
|
|
4
|
+
export type PrimitiveType = null | undefined | boolean | number | string | symbol | bigint
|
|
5
|
+
|
|
6
|
+
export class PrimitiveCoercer implements ErrorTrackingCoercer<PrimitiveType> {
|
|
7
|
+
match(candidate: unknown): candidate is PrimitiveType {
|
|
8
|
+
return isPrimitive(candidate)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
coerce(value: PrimitiveType, ctx: CoercingContext): ExceptionLike | undefined {
|
|
12
|
+
return {
|
|
13
|
+
type: 'Error',
|
|
14
|
+
value: `Primitive value captured as exception: ${String(value)}`,
|
|
15
|
+
stack: ctx.syntheticException?.stack,
|
|
16
|
+
synthetic: true,
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { CoercingContext } from '../types'
|
|
2
|
+
import { PromiseRejectionEventCoercer } from './promise-rejection-event'
|
|
3
|
+
|
|
4
|
+
type PromiseRejectionEventTypes = 'rejectionhandled' | 'unhandledrejection'
|
|
5
|
+
|
|
6
|
+
type PromiseRejectionEventInit = {
|
|
7
|
+
promise: Promise<any>
|
|
8
|
+
reason: any
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
class PromiseRejectionEvent extends Event {
|
|
12
|
+
public readonly promise: Promise<any>
|
|
13
|
+
public readonly reason: any
|
|
14
|
+
|
|
15
|
+
public constructor(type: PromiseRejectionEventTypes, options: PromiseRejectionEventInit) {
|
|
16
|
+
super(type)
|
|
17
|
+
|
|
18
|
+
this.promise = options.promise
|
|
19
|
+
this.reason = options.reason
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('PromiseRejectionEventCoercer', () => {
|
|
24
|
+
const coercer = new PromiseRejectionEventCoercer()
|
|
25
|
+
|
|
26
|
+
it('should coerce event with reason is a primitive', () => {
|
|
27
|
+
const pre = new PromiseRejectionEvent('unhandledrejection', {
|
|
28
|
+
promise: Promise.resolve('wat'),
|
|
29
|
+
reason: 'My house is on fire',
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const ctx = {
|
|
33
|
+
apply: jest.fn(() => ({
|
|
34
|
+
type: 'MockType',
|
|
35
|
+
value: 'MockValue',
|
|
36
|
+
synthetic: true,
|
|
37
|
+
})),
|
|
38
|
+
next: jest.fn(),
|
|
39
|
+
} as CoercingContext
|
|
40
|
+
|
|
41
|
+
expect(coercer.coerce(pre, ctx)).toMatchObject({
|
|
42
|
+
type: 'UnhandledRejection',
|
|
43
|
+
value: 'Non-Error promise rejection captured with value: My house is on fire',
|
|
44
|
+
synthetic: true,
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should coerce event with reason is an error', () => {
|
|
49
|
+
class CustomTestError extends Error {
|
|
50
|
+
constructor(message: string) {
|
|
51
|
+
super(message)
|
|
52
|
+
this.name = 'CustomTestError'
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const pre = new PromiseRejectionEvent('unhandledrejection', {
|
|
57
|
+
promise: Promise.resolve('wat'),
|
|
58
|
+
reason: new CustomTestError('My house is on fire'),
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const ctx = {
|
|
62
|
+
apply: jest.fn((err: Error) => ({
|
|
63
|
+
type: err.name,
|
|
64
|
+
value: err.message,
|
|
65
|
+
stack: err.stack,
|
|
66
|
+
synthetic: false,
|
|
67
|
+
})),
|
|
68
|
+
next: jest.fn(),
|
|
69
|
+
} as CoercingContext
|
|
70
|
+
|
|
71
|
+
expect(coercer.coerce(pre, ctx)).toMatchObject({
|
|
72
|
+
type: 'CustomTestError',
|
|
73
|
+
value: 'My house is on fire',
|
|
74
|
+
synthetic: false,
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { isBuiltin, isPrimitive } from '@/utils'
|
|
2
|
+
import { CoercingContext, ErrorTrackingCoercer, ExceptionLike } from '../types'
|
|
3
|
+
|
|
4
|
+
// Web only
|
|
5
|
+
export class PromiseRejectionEventCoercer implements ErrorTrackingCoercer<PromiseRejectionEvent> {
|
|
6
|
+
match(err: unknown): err is PromiseRejectionEvent {
|
|
7
|
+
return isBuiltin(err, 'PromiseRejectionEvent')
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
coerce(err: PromiseRejectionEvent, ctx: CoercingContext): ExceptionLike | undefined {
|
|
11
|
+
const reason = this.getUnhandledRejectionReason(err)
|
|
12
|
+
if (isPrimitive(reason)) {
|
|
13
|
+
return {
|
|
14
|
+
type: 'UnhandledRejection',
|
|
15
|
+
value: `Non-Error promise rejection captured with value: ${String(reason)}`,
|
|
16
|
+
stack: ctx.syntheticException?.stack,
|
|
17
|
+
synthetic: true,
|
|
18
|
+
}
|
|
19
|
+
} else {
|
|
20
|
+
return ctx.apply(reason)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private getUnhandledRejectionReason(error: unknown): unknown {
|
|
25
|
+
if (isPrimitive(error)) {
|
|
26
|
+
return error
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// dig the object of the rejection out of known event types
|
|
30
|
+
try {
|
|
31
|
+
type ErrorWithReason = { reason: unknown }
|
|
32
|
+
// PromiseRejectionEvents store the object of the rejection under 'reason'
|
|
33
|
+
// see https://developer.mozilla.org/en-US/docs/Web/API/PromiseRejectionEvent
|
|
34
|
+
if ('reason' in (error as ErrorWithReason)) {
|
|
35
|
+
return (error as ErrorWithReason).reason
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type CustomEventWithDetail = { detail: { reason: unknown } }
|
|
39
|
+
// something, somewhere, (likely a browser extension) effectively casts PromiseRejectionEvents
|
|
40
|
+
// to CustomEvents, moving the `promise` and `reason` attributes of the PRE into
|
|
41
|
+
// the CustomEvent's `detail` attribute, since they're not part of CustomEvent's spec
|
|
42
|
+
// see https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent and
|
|
43
|
+
// https://github.com/getsentry/sentry-javascript/issues/2380
|
|
44
|
+
if ('detail' in (error as CustomEventWithDetail) && 'reason' in (error as CustomEventWithDetail).detail) {
|
|
45
|
+
return (error as CustomEventWithDetail).detail.reason
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// no-empty
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return error
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { CoercingContext } from '../types'
|
|
2
|
+
import { StringCoercer } from './string-coercer'
|
|
3
|
+
|
|
4
|
+
describe('PromiseRejectionEventCoercer', () => {
|
|
5
|
+
const coercer = new StringCoercer()
|
|
6
|
+
|
|
7
|
+
it('should parse string', () => {
|
|
8
|
+
const infos = coercer.getInfos('My house is on fire')
|
|
9
|
+
expect(infos).toMatchObject([undefined, 'My house is on fire'])
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('should parse errors', () => {
|
|
13
|
+
const infos = coercer.getInfos('ReferenceError: My house is on fire')
|
|
14
|
+
expect(infos).toMatchObject(['ReferenceError', 'My house is on fire'])
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('should discard prefix', () => {
|
|
18
|
+
const infos = coercer.getInfos('Uncaught exception: ReferenceError: My house is on fire')
|
|
19
|
+
expect(infos).toMatchObject(['ReferenceError', 'My house is on fire'])
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('should not match other patterns', () => {
|
|
23
|
+
const infos = coercer.getInfos('ValueError: ReferenceError: My house is on fire')
|
|
24
|
+
expect(infos).toMatchObject([undefined, 'ValueError: ReferenceError: My house is on fire'])
|
|
25
|
+
})
|
|
26
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ExceptionLike, ErrorTrackingCoercer, CoercingContext } from '../types'
|
|
2
|
+
|
|
3
|
+
const ERROR_TYPES_PATTERN =
|
|
4
|
+
/^(?:[Uu]ncaught (?:exception: )?)?(?:((?:Eval|Internal|Range|Reference|Syntax|Type|URI|)Error): )?(.*)$/i
|
|
5
|
+
|
|
6
|
+
export class StringCoercer implements ErrorTrackingCoercer<string> {
|
|
7
|
+
match(input: unknown): input is string {
|
|
8
|
+
return typeof input === 'string'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
coerce(input: string, ctx: CoercingContext): ExceptionLike {
|
|
12
|
+
const [type, value] = this.getInfos(input)
|
|
13
|
+
return {
|
|
14
|
+
type: type ?? 'Error',
|
|
15
|
+
value: value ?? input,
|
|
16
|
+
stack: ctx.syntheticException?.stack,
|
|
17
|
+
synthetic: true,
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
getInfos(candidate: string): [string, string] {
|
|
22
|
+
let type = 'Error'
|
|
23
|
+
let value = candidate
|
|
24
|
+
const groups = candidate.match(ERROR_TYPES_PATTERN)
|
|
25
|
+
if (groups) {
|
|
26
|
+
type = groups[1]
|
|
27
|
+
value = groups[2]
|
|
28
|
+
}
|
|
29
|
+
return [type, value]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export function truncate(str: string, max: number = 0): string {
|
|
2
|
+
if (typeof str !== 'string' || max === 0) {
|
|
3
|
+
return str
|
|
4
|
+
}
|
|
5
|
+
return str.length <= max ? str : `${str.slice(0, max)}...`
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Given any captured exception, extract its keys and create a sorted
|
|
10
|
+
* and truncated list that will be used inside the event message.
|
|
11
|
+
* eg. `Non-error exception captured with keys: foo, bar, baz`
|
|
12
|
+
*/
|
|
13
|
+
export function extractExceptionKeysForMessage(err: object, maxLength = 40): string {
|
|
14
|
+
const keys = Object.keys(err)
|
|
15
|
+
keys.sort()
|
|
16
|
+
|
|
17
|
+
if (!keys.length) {
|
|
18
|
+
return '[object has no keys]'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
for (let i = keys.length; i > 0; i--) {
|
|
22
|
+
const serialized = keys.slice(0, i).join(', ')
|
|
23
|
+
if (serialized.length > maxLength) {
|
|
24
|
+
continue
|
|
25
|
+
}
|
|
26
|
+
if (i === keys.length) {
|
|
27
|
+
return serialized
|
|
28
|
+
}
|
|
29
|
+
return serialized.length <= maxLength ? serialized : `${serialized.slice(0, maxLength)}...`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return ''
|
|
33
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { DOMExceptionCoercer, ErrorEventCoercer, ErrorCoercer, ObjectCoercer, StringCoercer } from './coercers'
|
|
2
|
+
import { PrimitiveCoercer } from './coercers/primitive-coercer'
|
|
3
|
+
import { PromiseRejectionEventCoercer } from './coercers/promise-rejection-event'
|
|
4
|
+
import { ErrorPropertiesBuilder } from './error-properties-builder'
|
|
5
|
+
import { ExceptionLike } from './types'
|
|
6
|
+
|
|
7
|
+
describe('ErrorPropertiesBuilder', () => {
|
|
8
|
+
describe('coerceUnknown', () => {
|
|
9
|
+
class CustomTestError extends Error {
|
|
10
|
+
constructor(message: string, cause?: unknown) {
|
|
11
|
+
super(message)
|
|
12
|
+
this.name = 'CustomTestError'
|
|
13
|
+
this.cause = cause
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const errorPropertiesBuilder = new ErrorPropertiesBuilder(
|
|
18
|
+
[
|
|
19
|
+
new DOMExceptionCoercer(),
|
|
20
|
+
new ErrorEventCoercer(),
|
|
21
|
+
new ErrorCoercer(),
|
|
22
|
+
new PromiseRejectionEventCoercer(),
|
|
23
|
+
new ObjectCoercer(),
|
|
24
|
+
new StringCoercer(),
|
|
25
|
+
new PrimitiveCoercer(),
|
|
26
|
+
],
|
|
27
|
+
[],
|
|
28
|
+
[]
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
function coerceInput(input: unknown, error: Error = new Error()): ExceptionLike | undefined {
|
|
32
|
+
const coercingContext = errorPropertiesBuilder.buildCoercingContext(
|
|
33
|
+
{ handled: false },
|
|
34
|
+
{
|
|
35
|
+
syntheticException: error,
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
return coercingContext.apply(input)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
it('should handle null values', async () => {
|
|
42
|
+
const syntheticError = new Error()
|
|
43
|
+
const exception = coerceInput(null, syntheticError)
|
|
44
|
+
expect(exception).toMatchObject({
|
|
45
|
+
type: 'Error',
|
|
46
|
+
value: 'Primitive value captured as exception: null',
|
|
47
|
+
stack: syntheticError.stack,
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('should handle string', () => {
|
|
52
|
+
const syntheticError = new Error()
|
|
53
|
+
const exception = coerceInput('test', syntheticError)
|
|
54
|
+
expect(exception).toMatchObject({
|
|
55
|
+
type: 'Error',
|
|
56
|
+
value: 'test',
|
|
57
|
+
stack: syntheticError.stack,
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('should handle exception string', () => {
|
|
62
|
+
const syntheticError = new Error()
|
|
63
|
+
const exception = coerceInput('Uncaught exception: InternalError: but somehow still a string', syntheticError)
|
|
64
|
+
expect(exception).toMatchObject({
|
|
65
|
+
type: 'InternalError',
|
|
66
|
+
value: 'but somehow still a string',
|
|
67
|
+
stack: syntheticError.stack,
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should use keys in objects', async () => {
|
|
72
|
+
const syntheticError = new Error()
|
|
73
|
+
const errorObject = { foo: 'Foo value', bar: 'Bar value' }
|
|
74
|
+
const exception = coerceInput(errorObject, syntheticError)
|
|
75
|
+
expect(exception).toMatchObject({
|
|
76
|
+
type: 'Error',
|
|
77
|
+
value: 'Object captured as exception with keys: bar, foo',
|
|
78
|
+
stack: syntheticError.stack,
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('should handle object with an error property', () => {
|
|
83
|
+
const nestedError = new CustomTestError('My special error')
|
|
84
|
+
const errorObject = { error: nestedError }
|
|
85
|
+
const syntheticError = new Error()
|
|
86
|
+
const exception = coerceInput(errorObject, syntheticError)
|
|
87
|
+
expect(exception).toMatchObject({
|
|
88
|
+
type: 'CustomTestError',
|
|
89
|
+
value: 'My special error',
|
|
90
|
+
stack: nestedError.stack,
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('should handle error', () => {
|
|
95
|
+
const errorObject = new CustomTestError('My special error')
|
|
96
|
+
const exception = coerceInput(errorObject)
|
|
97
|
+
expect(exception).toMatchObject({
|
|
98
|
+
type: 'CustomTestError',
|
|
99
|
+
value: 'My special error',
|
|
100
|
+
stack: errorObject.stack,
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('should handle error with error cause', () => {
|
|
105
|
+
const secondError = new CustomTestError('My original error')
|
|
106
|
+
const firstError = new CustomTestError('My wrapped error', secondError)
|
|
107
|
+
const exception = coerceInput(firstError)
|
|
108
|
+
expect(exception).toMatchObject({
|
|
109
|
+
type: 'CustomTestError',
|
|
110
|
+
value: 'My wrapped error',
|
|
111
|
+
stack: firstError.stack,
|
|
112
|
+
cause: {
|
|
113
|
+
type: 'CustomTestError',
|
|
114
|
+
value: 'My original error',
|
|
115
|
+
stack: secondError.stack,
|
|
116
|
+
},
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('should handle error with object cause', () => {
|
|
121
|
+
const originalCause = { foo: 'bar', test: 'test' }
|
|
122
|
+
const kaboomError = new CustomTestError('Front error', originalCause)
|
|
123
|
+
const syntheticError = new Error()
|
|
124
|
+
const exception = coerceInput(kaboomError, syntheticError)
|
|
125
|
+
expect(exception).toMatchObject({
|
|
126
|
+
type: 'CustomTestError',
|
|
127
|
+
value: 'Front error',
|
|
128
|
+
stack: kaboomError.stack,
|
|
129
|
+
cause: {
|
|
130
|
+
type: 'Error',
|
|
131
|
+
value: 'Object captured as exception with keys: foo, test',
|
|
132
|
+
// Do we want to use the stack from the synthetic error?
|
|
133
|
+
stack: undefined,
|
|
134
|
+
},
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('should handle error with string cause', () => {
|
|
139
|
+
const originalCause = 'My original error'
|
|
140
|
+
const kaboomError = new CustomTestError('Front error', originalCause)
|
|
141
|
+
const syntheticError = new Error()
|
|
142
|
+
const exception = coerceInput(kaboomError, syntheticError)
|
|
143
|
+
expect(exception).toMatchObject({
|
|
144
|
+
type: 'CustomTestError',
|
|
145
|
+
value: 'Front error',
|
|
146
|
+
stack: kaboomError.stack,
|
|
147
|
+
cause: {
|
|
148
|
+
type: 'Error',
|
|
149
|
+
value: 'My original error',
|
|
150
|
+
// Do we want to use the stack from the synthetic error?
|
|
151
|
+
stack: undefined,
|
|
152
|
+
},
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('should convert a plain Event to an error', () => {
|
|
157
|
+
class MouseEvent extends Event {
|
|
158
|
+
constructor(type: string, eventInitDict?: EventInit) {
|
|
159
|
+
super(type, eventInitDict)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const event = new MouseEvent('click', { bubbles: true, cancelable: true, composed: true })
|
|
163
|
+
const syntheticError = new Error()
|
|
164
|
+
const exception = coerceInput(event, syntheticError)
|
|
165
|
+
expect(exception).toMatchObject({
|
|
166
|
+
type: 'MouseEvent',
|
|
167
|
+
value: "'MouseEvent' captured as exception with keys: [object has no keys]",
|
|
168
|
+
stack: syntheticError.stack,
|
|
169
|
+
synthetic: true,
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('should convert a DOM Error to an error', () => {
|
|
174
|
+
class FakeDomError {
|
|
175
|
+
constructor(
|
|
176
|
+
public name: string,
|
|
177
|
+
public message: string
|
|
178
|
+
) {}
|
|
179
|
+
[Symbol.toStringTag] = 'DOMError'
|
|
180
|
+
}
|
|
181
|
+
const event = new FakeDomError('click', 'foo')
|
|
182
|
+
const exception = coerceInput(event)
|
|
183
|
+
expect(exception).toMatchObject({
|
|
184
|
+
type: 'DOMError',
|
|
185
|
+
value: 'click: foo',
|
|
186
|
+
stack: undefined,
|
|
187
|
+
synthetic: false,
|
|
188
|
+
})
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('should convert a DOM Exception to an error', () => {
|
|
192
|
+
const event = new DOMException('oh no disaster', 'dom-exception')
|
|
193
|
+
const exception = coerceInput(event)
|
|
194
|
+
expect(exception).toBeDefined()
|
|
195
|
+
expect(exception).toMatchObject({
|
|
196
|
+
type: 'DOMException',
|
|
197
|
+
value: 'dom-exception: oh no disaster',
|
|
198
|
+
synthetic: false,
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ErrorPropertiesBuilder } from './error-properties-builder'
|
|
2
|
+
import { chromeStackLineParser } from './parsers'
|
|
3
|
+
import { StackFrame } from './types'
|
|
4
|
+
|
|
5
|
+
describe('ErrorPropertiesBuilder', () => {
|
|
6
|
+
describe('coerceUnknown', () => {
|
|
7
|
+
const errorPropertiesBuilder = new ErrorPropertiesBuilder([], [chromeStackLineParser], [])
|
|
8
|
+
|
|
9
|
+
function parseStack(error: Error): StackFrame[] | undefined {
|
|
10
|
+
const ctx = {}
|
|
11
|
+
const exception = errorPropertiesBuilder.parseStacktrace(
|
|
12
|
+
{
|
|
13
|
+
type: 'Error',
|
|
14
|
+
value: 'Whatever',
|
|
15
|
+
stack: error.stack,
|
|
16
|
+
synthetic: false,
|
|
17
|
+
},
|
|
18
|
+
ctx
|
|
19
|
+
)
|
|
20
|
+
return exception.stack
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
it('should parse stacktraces', () => {
|
|
24
|
+
const syntheticError = new Error()
|
|
25
|
+
const frames = parseStack(syntheticError)
|
|
26
|
+
expect(frames).toBeDefined()
|
|
27
|
+
expect(frames).toHaveLength(16)
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
})
|