@posthog/react 1.10.2 → 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 +49 -4
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/slim/index.js +49 -4
- 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 +67 -22
- package/dist/umd/index.js.map +1 -1
- package/dist/umd/slim/index.js +65 -20
- 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 +3 -5
- 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 +30 -5
- package/src/hooks/useActiveFeatureFlags.ts +3 -1
- 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
|
|
|
@@ -103,6 +102,36 @@ describe('feature flag hooks', () => {
|
|
|
103
102
|
expect(result.current).toEqual(['example_feature_true', 'multivariate_feature', 'example_feature_payload'])
|
|
104
103
|
})
|
|
105
104
|
|
|
105
|
+
it.each([
|
|
106
|
+
['enabled_flag', true],
|
|
107
|
+
['disabled_flag', false],
|
|
108
|
+
['multivariate_flag', true],
|
|
109
|
+
])('should report bootstrap feature flag %s active status as %s', (flag, expected) => {
|
|
110
|
+
const client = {
|
|
111
|
+
onFeatureFlags: () => () => {},
|
|
112
|
+
config: {
|
|
113
|
+
bootstrap: {
|
|
114
|
+
featureFlags: {
|
|
115
|
+
enabled_flag: true,
|
|
116
|
+
disabled_flag: false,
|
|
117
|
+
multivariate_flag: 'variant-a',
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
featureFlags: {
|
|
122
|
+
getFlags: () => [],
|
|
123
|
+
hasLoadedFlags: false,
|
|
124
|
+
} as unknown as PostHog['featureFlags'],
|
|
125
|
+
} as unknown as PostHog
|
|
126
|
+
|
|
127
|
+
const wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
|
128
|
+
<PostHogProvider client={client}>{children}</PostHogProvider>
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
const { result } = renderHook(() => useActiveFeatureFlags(), { wrapper })
|
|
132
|
+
expect(result.current.includes(flag)).toBe(expected)
|
|
133
|
+
})
|
|
134
|
+
|
|
106
135
|
it.each([
|
|
107
136
|
['example_feature_true', true],
|
|
108
137
|
['example_feature_false', false],
|
|
@@ -166,7 +195,6 @@ describe('feature flag hooks', () => {
|
|
|
166
195
|
} as unknown as PostHog['featureFlags'],
|
|
167
196
|
} as unknown as PostHog
|
|
168
197
|
|
|
169
|
-
// eslint-disable-next-line react/display-name
|
|
170
198
|
const wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
|
171
199
|
<PostHogProvider client={client}>{children}</PostHogProvider>
|
|
172
200
|
)
|
|
@@ -196,7 +224,6 @@ describe('feature flag hooks', () => {
|
|
|
196
224
|
} as unknown as PostHog['featureFlags'],
|
|
197
225
|
} as unknown as PostHog
|
|
198
226
|
|
|
199
|
-
// eslint-disable-next-line react/display-name
|
|
200
227
|
const wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
|
201
228
|
<PostHogProvider client={client}>{children}</PostHogProvider>
|
|
202
229
|
)
|
|
@@ -275,7 +302,6 @@ describe('feature flag hooks', () => {
|
|
|
275
302
|
} as unknown as PostHog['featureFlags'],
|
|
276
303
|
} as unknown as PostHog
|
|
277
304
|
|
|
278
|
-
// eslint-disable-next-line react/display-name
|
|
279
305
|
const wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
|
280
306
|
<PostHogProvider client={client}>{children}</PostHogProvider>
|
|
281
307
|
)
|
|
@@ -318,7 +344,6 @@ describe('feature flag hooks', () => {
|
|
|
318
344
|
} as unknown as PostHog['featureFlags'],
|
|
319
345
|
} as unknown as PostHog
|
|
320
346
|
|
|
321
|
-
// eslint-disable-next-line react/display-name
|
|
322
347
|
const wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
|
323
348
|
<PostHogProvider client={client}>{children}</PostHogProvider>
|
|
324
349
|
)
|
|
@@ -14,7 +14,9 @@ export function useActiveFeatureFlags(): string[] {
|
|
|
14
14
|
|
|
15
15
|
// if the client is not loaded yet and we have a bootstrapped value, use it
|
|
16
16
|
if (!client?.featureFlags?.hasLoadedFlags && bootstrap?.featureFlags) {
|
|
17
|
-
return Object.
|
|
17
|
+
return Object.entries(bootstrap.featureFlags)
|
|
18
|
+
.filter(([, value]) => value)
|
|
19
|
+
.map(([key]) => key)
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
return featureFlags
|