@posthog/react 1.10.3 → 1.10.4
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/esm/index.js +40 -3
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/slim/index.js +40 -3
- package/dist/esm/slim/index.js.map +1 -1
- package/dist/esm/surveys/index.js +2 -0
- package/dist/esm/surveys/index.js.map +1 -1
- package/dist/types/index.d.ts +3 -2
- package/dist/types/slim/index.d.ts +3 -2
- package/dist/types/surveys/index.d.ts +2 -1
- package/dist/umd/index.js +58 -21
- package/dist/umd/index.js.map +1 -1
- package/dist/umd/slim/index.js +56 -19
- package/dist/umd/slim/index.js.map +1 -1
- package/dist/umd/surveys/index.js +2 -0
- package/dist/umd/surveys/index.js.map +1 -1
- package/package.json +4 -6
- package/src/__tests__/umd-bundle.test.ts +23 -0
- package/src/components/PostHogErrorBoundary.tsx +4 -6
- package/src/components/__tests__/PostHogErrorBoundary.test.tsx +62 -7
- package/src/helpers/__tests__/error-helpers.test.ts +43 -0
- package/src/helpers/error-helpers.ts +2 -1
- package/src/helpers/react-component-stack.ts +53 -0
- package/src/hooks/__tests__/featureFlags.test.tsx +0 -6
- package/src/utils/type-utils.ts +0 -1
|
@@ -24,7 +24,8 @@ describe('PostHogErrorBoundary component', () => {
|
|
|
24
24
|
|
|
25
25
|
it('should call captureException with error message', () => {
|
|
26
26
|
const { container } = renderWithError({ message: 'Test error', fallback: <div></div> })
|
|
27
|
-
expect(posthog.captureException).toHaveBeenCalledWith(
|
|
27
|
+
expect(posthog.captureException).toHaveBeenCalledWith(expect.any(Error), undefined)
|
|
28
|
+
expectCapturedReactError()
|
|
28
29
|
expect(container.innerHTML).toBe('<div></div>')
|
|
29
30
|
expect(console.error).toHaveBeenCalledTimes(1)
|
|
30
31
|
expect((console.error as any).mock.calls[0][1].message).toEqual('Test error')
|
|
@@ -32,14 +33,16 @@ describe('PostHogErrorBoundary component', () => {
|
|
|
32
33
|
|
|
33
34
|
it('should warn user when fallback is null', () => {
|
|
34
35
|
const { container } = renderWithError({ fallback: null })
|
|
35
|
-
expect(posthog.captureException).toHaveBeenCalledWith(
|
|
36
|
+
expect(posthog.captureException).toHaveBeenCalledWith(expect.any(Error), undefined)
|
|
37
|
+
expectCapturedReactError()
|
|
36
38
|
expect(container.innerHTML).toBe('')
|
|
37
39
|
expect(console.warn).toHaveBeenCalledWith(__POSTHOG_ERROR_MESSAGES.INVALID_FALLBACK)
|
|
38
40
|
})
|
|
39
41
|
|
|
40
42
|
it('should warn user when fallback is a string', () => {
|
|
41
43
|
const { container } = renderWithError({ fallback: 'hello' })
|
|
42
|
-
expect(posthog.captureException).toHaveBeenCalledWith(
|
|
44
|
+
expect(posthog.captureException).toHaveBeenCalledWith(expect.any(Error), undefined)
|
|
45
|
+
expectCapturedReactError()
|
|
43
46
|
expect(container.innerHTML).toBe('')
|
|
44
47
|
expect(console.warn).toHaveBeenCalledWith(__POSTHOG_ERROR_MESSAGES.INVALID_FALLBACK)
|
|
45
48
|
})
|
|
@@ -47,19 +50,40 @@ describe('PostHogErrorBoundary component', () => {
|
|
|
47
50
|
it('should add additional properties before sending event (as object)', () => {
|
|
48
51
|
const props = { team_id: '1234' }
|
|
49
52
|
renderWithError({ message: 'Kaboom', additionalProperties: props })
|
|
50
|
-
expect(posthog.captureException).toHaveBeenCalledWith(
|
|
53
|
+
expect(posthog.captureException).toHaveBeenCalledWith(expect.any(Error), props)
|
|
54
|
+
expectCapturedReactError()
|
|
51
55
|
})
|
|
52
56
|
|
|
53
57
|
it('should add additional properties before sending event (as function)', () => {
|
|
54
58
|
const props = { team_id: '1234' }
|
|
55
59
|
renderWithError({
|
|
56
60
|
message: 'Kaboom',
|
|
57
|
-
additionalProperties: (err: Error) => {
|
|
61
|
+
additionalProperties: (err: Error, errorInfo: React.ErrorInfo) => {
|
|
58
62
|
expect(err.message).toBe('Kaboom')
|
|
63
|
+
expect(errorInfo.componentStack).toContain('PostHogErrorBoundary.test.tsx')
|
|
59
64
|
return props
|
|
60
65
|
},
|
|
61
66
|
})
|
|
62
|
-
expect(posthog.captureException).toHaveBeenCalledWith(
|
|
67
|
+
expect(posthog.captureException).toHaveBeenCalledWith(expect.any(Error), props)
|
|
68
|
+
expectCapturedReactError()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should capture the component stack for primitive exceptions', () => {
|
|
72
|
+
render(
|
|
73
|
+
<PostHogErrorBoundary fallback={<div></div>}>
|
|
74
|
+
<ComponentWithUndefinedError />
|
|
75
|
+
</PostHogErrorBoundary>
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
expect(posthog.captureException).toHaveBeenCalledWith(expect.any(Error), undefined)
|
|
79
|
+
const capturedError = (posthog.captureException as jest.Mock).mock.calls[0][0]
|
|
80
|
+
expect(capturedError).toEqual(
|
|
81
|
+
expect.objectContaining({
|
|
82
|
+
message: 'Primitive value captured as exception: undefined',
|
|
83
|
+
name: 'React ErrorBoundary Error',
|
|
84
|
+
stack: expect.stringContaining('ComponentWithUndefinedError'),
|
|
85
|
+
})
|
|
86
|
+
)
|
|
63
87
|
})
|
|
64
88
|
|
|
65
89
|
it('should render children without errors', () => {
|
|
@@ -88,12 +112,39 @@ describe('captureException processing', () => {
|
|
|
88
112
|
const captureCalls = (posthog.capture as jest.Mock).mock.calls
|
|
89
113
|
expect(captureCalls.length).toBe(1)
|
|
90
114
|
const exceptionList = captureCalls[0][1].$exception_list
|
|
91
|
-
expect(exceptionList.length).toBe(
|
|
115
|
+
expect(exceptionList.length).toBe(2)
|
|
92
116
|
const stacktrace = exceptionList[0].stacktrace
|
|
93
117
|
expect(stacktrace.frames.length).toBeGreaterThan(20)
|
|
118
|
+
expect(exceptionList[1].type).toBe('React ErrorBoundary Error')
|
|
119
|
+
expectComponentStackFrames(exceptionList[1].stacktrace.frames, 'PostHogErrorBoundary')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('should parse the component stack for primitive exceptions', () => {
|
|
123
|
+
render(
|
|
124
|
+
<PostHogErrorBoundary fallback={<div></div>}>
|
|
125
|
+
<ComponentWithUndefinedError />
|
|
126
|
+
</PostHogErrorBoundary>
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
const captureCalls = (posthog.capture as jest.Mock).mock.calls
|
|
130
|
+
const exceptionList = captureCalls[0][1].$exception_list
|
|
131
|
+
expect(exceptionList).toHaveLength(1)
|
|
132
|
+
expect(exceptionList[0].type).toBe('React ErrorBoundary Error')
|
|
133
|
+
expectComponentStackFrames(exceptionList[0].stacktrace.frames, 'ComponentWithUndefinedError')
|
|
94
134
|
})
|
|
95
135
|
})
|
|
96
136
|
|
|
137
|
+
function expectComponentStackFrames(frames: Array<{ function?: string }>, expectedFunction: string) {
|
|
138
|
+
expect(frames.length).toBeGreaterThan(0)
|
|
139
|
+
expect(frames.some((frame) => frame.function === expectedFunction)).toBe(true)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function expectCapturedReactError() {
|
|
143
|
+
const capturedError = (posthog.captureException as jest.Mock).mock.calls[0][0]
|
|
144
|
+
expect(capturedError.cause.name).toBe('React ErrorBoundary Error')
|
|
145
|
+
expect(capturedError.cause.stack).toContain('PostHogErrorBoundary.test.tsx')
|
|
146
|
+
}
|
|
147
|
+
|
|
97
148
|
function mockFunction(object: any, funcName: string) {
|
|
98
149
|
const originalFunc = object[funcName]
|
|
99
150
|
|
|
@@ -110,6 +161,10 @@ function ComponentWithError({ message }: { message: string }): React.ReactElemen
|
|
|
110
161
|
throw new Error(message)
|
|
111
162
|
}
|
|
112
163
|
|
|
164
|
+
function ComponentWithUndefinedError(): React.ReactElement {
|
|
165
|
+
throw undefined
|
|
166
|
+
}
|
|
167
|
+
|
|
113
168
|
function RenderWithError({ message = 'Error', fallback, additionalProperties }: any) {
|
|
114
169
|
return (
|
|
115
170
|
<PostHogErrorBoundary fallback={fallback} additionalProperties={additionalProperties}>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ErrorInfo } from 'react'
|
|
2
|
+
import { setupReactErrorHandler } from '../error-helpers'
|
|
3
|
+
|
|
4
|
+
describe('setupReactErrorHandler', () => {
|
|
5
|
+
it('captures the React component stack', () => {
|
|
6
|
+
const captureException = jest.fn()
|
|
7
|
+
const errorInfo: ErrorInfo = { componentStack: '\n in CrashingComponent' }
|
|
8
|
+
const handler = setupReactErrorHandler({ captureException } as any)
|
|
9
|
+
|
|
10
|
+
handler(undefined, errorInfo)
|
|
11
|
+
|
|
12
|
+
expect(captureException).toHaveBeenCalledWith(
|
|
13
|
+
expect.objectContaining({
|
|
14
|
+
message: 'Primitive value captured as exception: undefined',
|
|
15
|
+
name: 'React ErrorBoundary Error',
|
|
16
|
+
stack: errorInfo.componentStack,
|
|
17
|
+
})
|
|
18
|
+
)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('appends the React component stack after existing error causes', () => {
|
|
22
|
+
const captureException = jest.fn()
|
|
23
|
+
const errorInfo: ErrorInfo = { componentStack: '\n in CrashingComponent' }
|
|
24
|
+
const handler = setupReactErrorHandler({ captureException } as any)
|
|
25
|
+
const error = new Error('outer error') as Error & { cause?: unknown }
|
|
26
|
+
const cause = new Error('inner error') as Error & { cause?: unknown }
|
|
27
|
+
const nestedCause = new Error('nested error') as Error & { cause?: unknown }
|
|
28
|
+
error.cause = cause
|
|
29
|
+
cause.cause = nestedCause
|
|
30
|
+
|
|
31
|
+
handler(error, errorInfo)
|
|
32
|
+
|
|
33
|
+
expect(captureException).toHaveBeenCalledWith(error)
|
|
34
|
+
expect(error.cause).toBe(cause)
|
|
35
|
+
expect(cause.cause).toBe(nestedCause)
|
|
36
|
+
expect(nestedCause.cause).toEqual(
|
|
37
|
+
expect.objectContaining({
|
|
38
|
+
name: 'React ErrorBoundary Error',
|
|
39
|
+
stack: errorInfo.componentStack,
|
|
40
|
+
})
|
|
41
|
+
)
|
|
42
|
+
})
|
|
43
|
+
})
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import type { ErrorInfo } from 'react'
|
|
2
2
|
import { PostHog } from '../context'
|
|
3
3
|
import type { CaptureResult } from 'posthog-js'
|
|
4
|
+
import { addReactComponentStack } from './react-component-stack'
|
|
4
5
|
|
|
5
6
|
export const setupReactErrorHandler = (
|
|
6
7
|
client: PostHog,
|
|
7
8
|
callback?: (event: CaptureResult | undefined, error: any, errorInfo: ErrorInfo) => void
|
|
8
9
|
) => {
|
|
9
10
|
return (error: any, errorInfo: ErrorInfo): void => {
|
|
10
|
-
const event = client.captureException(error)
|
|
11
|
+
const event = client.captureException(addReactComponentStack(error, errorInfo.componentStack))
|
|
11
12
|
if (callback) {
|
|
12
13
|
callback(event, error, errorInfo)
|
|
13
14
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { isUndefined } from '../utils/type-utils'
|
|
2
|
+
|
|
3
|
+
type ErrorWithCause = Error & { cause?: unknown }
|
|
4
|
+
|
|
5
|
+
const isError = (value: unknown): value is ErrorWithCause => {
|
|
6
|
+
const tag = Object.prototype.toString.call(value)
|
|
7
|
+
return (
|
|
8
|
+
value instanceof Error ||
|
|
9
|
+
tag === '[object Error]' ||
|
|
10
|
+
tag === '[object Exception]' ||
|
|
11
|
+
tag === '[object DOMException]' ||
|
|
12
|
+
tag === '[object DOMError]'
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const setCause = (error: ErrorWithCause, cause: ErrorWithCause): void => {
|
|
17
|
+
const seenErrors = new WeakSet<ErrorWithCause>()
|
|
18
|
+
let currentError = error
|
|
19
|
+
|
|
20
|
+
while (!seenErrors.has(currentError)) {
|
|
21
|
+
seenErrors.add(currentError)
|
|
22
|
+
|
|
23
|
+
if (!isError(currentError.cause)) {
|
|
24
|
+
if (!isUndefined(currentError.cause)) {
|
|
25
|
+
cause.cause = currentError.cause
|
|
26
|
+
}
|
|
27
|
+
currentError.cause = cause
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
currentError = currentError.cause
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Model React's component stack as a linked error so existing exception parsing and rendering can handle it.
|
|
36
|
+
export const addReactComponentStack = (error: unknown, componentStack?: string | null): unknown => {
|
|
37
|
+
if (!componentStack) {
|
|
38
|
+
return error
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const componentStackError = new Error(
|
|
42
|
+
isError(error) ? error.message : `Primitive value captured as exception: ${String(error)}`
|
|
43
|
+
)
|
|
44
|
+
componentStackError.name = `React ErrorBoundary ${isError(error) ? error.name : 'Error'}`
|
|
45
|
+
componentStackError.stack = componentStack
|
|
46
|
+
|
|
47
|
+
if (isError(error)) {
|
|
48
|
+
setCause(error, componentStackError)
|
|
49
|
+
return error
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return componentStackError
|
|
53
|
+
}
|
|
@@ -66,7 +66,6 @@ describe('feature flag hooks', () => {
|
|
|
66
66
|
} as unknown as PostHog['featureFlags'],
|
|
67
67
|
} as unknown as PostHog
|
|
68
68
|
|
|
69
|
-
// eslint-disable-next-line react/display-name
|
|
70
69
|
renderProvider = ({ children }) => <PostHogProvider client={posthog}>{children}</PostHogProvider>
|
|
71
70
|
})
|
|
72
71
|
|
|
@@ -125,7 +124,6 @@ describe('feature flag hooks', () => {
|
|
|
125
124
|
} as unknown as PostHog['featureFlags'],
|
|
126
125
|
} as unknown as PostHog
|
|
127
126
|
|
|
128
|
-
// eslint-disable-next-line react/display-name
|
|
129
127
|
const wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
|
130
128
|
<PostHogProvider client={client}>{children}</PostHogProvider>
|
|
131
129
|
)
|
|
@@ -197,7 +195,6 @@ describe('feature flag hooks', () => {
|
|
|
197
195
|
} as unknown as PostHog['featureFlags'],
|
|
198
196
|
} as unknown as PostHog
|
|
199
197
|
|
|
200
|
-
// eslint-disable-next-line react/display-name
|
|
201
198
|
const wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
|
202
199
|
<PostHogProvider client={client}>{children}</PostHogProvider>
|
|
203
200
|
)
|
|
@@ -227,7 +224,6 @@ describe('feature flag hooks', () => {
|
|
|
227
224
|
} as unknown as PostHog['featureFlags'],
|
|
228
225
|
} as unknown as PostHog
|
|
229
226
|
|
|
230
|
-
// eslint-disable-next-line react/display-name
|
|
231
227
|
const wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
|
232
228
|
<PostHogProvider client={client}>{children}</PostHogProvider>
|
|
233
229
|
)
|
|
@@ -306,7 +302,6 @@ describe('feature flag hooks', () => {
|
|
|
306
302
|
} as unknown as PostHog['featureFlags'],
|
|
307
303
|
} as unknown as PostHog
|
|
308
304
|
|
|
309
|
-
// eslint-disable-next-line react/display-name
|
|
310
305
|
const wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
|
311
306
|
<PostHogProvider client={client}>{children}</PostHogProvider>
|
|
312
307
|
)
|
|
@@ -349,7 +344,6 @@ describe('feature flag hooks', () => {
|
|
|
349
344
|
} as unknown as PostHog['featureFlags'],
|
|
350
345
|
} as unknown as PostHog
|
|
351
346
|
|
|
352
|
-
// eslint-disable-next-line react/display-name
|
|
353
347
|
const wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
|
354
348
|
<PostHogProvider client={client}>{children}</PostHogProvider>
|
|
355
349
|
)
|