@posthog/react 1.2.3 → 1.3.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/esm/index.js +68 -36
- package/dist/esm/index.js.map +1 -1
- package/dist/types/index.d.ts +19 -1
- package/dist/umd/index.js +70 -35
- package/dist/umd/index.js.map +1 -1
- package/package.json +5 -7
- package/src/components/PostHogCaptureOnViewed.tsx +126 -0
- package/src/components/PostHogFeature.tsx +8 -93
- package/src/components/__tests__/PostHogCaptureOnViewed.test.tsx +110 -0
- package/src/components/__tests__/{PostHogErrorBoundary.test.jsx → PostHogErrorBoundary.test.tsx} +16 -16
- package/src/components/__tests__/PostHogFeature.test.tsx +249 -0
- package/src/components/index.ts +1 -0
- package/src/components/internal/VisibilityAndClickTracker.tsx +49 -0
- package/src/components/internal/VisibilityAndClickTrackers.tsx +60 -0
- package/src/context/__tests__/{PostHogContext.test.jsx → PostHogContext.test.tsx} +16 -15
- package/src/context/__tests__/{PostHogProvider.test.jsx → PostHogProvider.test.tsx} +4 -4
- package/src/hooks/__tests__/{featureFlags.test.jsx → featureFlags.test.tsx} +35 -31
- package/src/hooks/__tests__/usePostHog.test.tsx +19 -0
- package/src/components/__tests__/PostHogFeature.test.jsx +0 -298
- package/src/hooks/__tests__/usePostHog.test.jsx +0 -23
package/src/components/__tests__/{PostHogErrorBoundary.test.jsx → PostHogErrorBoundary.test.tsx}
RENAMED
|
@@ -10,25 +10,25 @@ describe('PostHogErrorBoundary component', () => {
|
|
|
10
10
|
mockFunction(console, 'warn')
|
|
11
11
|
mockFunction(posthog, 'captureException')
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
const renderWithError = (props: any) => render(<RenderWithError {...props} />)
|
|
14
|
+
const renderWithoutError = (props?: any) => render(<RenderWithoutError {...props} />)
|
|
15
15
|
|
|
16
16
|
it('should call captureException with error message', () => {
|
|
17
|
-
const { container } =
|
|
17
|
+
const { container } = renderWithError({ message: 'Test error', fallback: <div></div> })
|
|
18
18
|
expect(posthog.captureException).toHaveBeenCalledWith(new Error('Test error'), undefined)
|
|
19
19
|
expect(container.innerHTML).toBe('<div></div>')
|
|
20
20
|
expect(console.error).toHaveBeenCalledTimes(2)
|
|
21
21
|
})
|
|
22
22
|
|
|
23
23
|
it('should warn user when fallback is null', () => {
|
|
24
|
-
const { container } =
|
|
24
|
+
const { container } = renderWithError({ fallback: null })
|
|
25
25
|
expect(posthog.captureException).toHaveBeenCalledWith(new Error('Error'), undefined)
|
|
26
26
|
expect(container.innerHTML).toBe('')
|
|
27
27
|
expect(console.warn).toHaveBeenCalledWith(__POSTHOG_ERROR_MESSAGES.INVALID_FALLBACK)
|
|
28
28
|
})
|
|
29
29
|
|
|
30
30
|
it('should warn user when fallback is a string', () => {
|
|
31
|
-
const { container } =
|
|
31
|
+
const { container } = renderWithError({ fallback: 'hello' })
|
|
32
32
|
expect(posthog.captureException).toHaveBeenCalledWith(new Error('Error'), undefined)
|
|
33
33
|
expect(container.innerHTML).toBe('')
|
|
34
34
|
expect(console.warn).toHaveBeenCalledWith(__POSTHOG_ERROR_MESSAGES.INVALID_FALLBACK)
|
|
@@ -36,15 +36,15 @@ describe('PostHogErrorBoundary component', () => {
|
|
|
36
36
|
|
|
37
37
|
it('should add additional properties before sending event (as object)', () => {
|
|
38
38
|
const props = { team_id: '1234' }
|
|
39
|
-
|
|
39
|
+
renderWithError({ message: 'Kaboom', additionalProperties: props })
|
|
40
40
|
expect(posthog.captureException).toHaveBeenCalledWith(new Error('Kaboom'), props)
|
|
41
41
|
})
|
|
42
42
|
|
|
43
43
|
it('should add additional properties before sending event (as function)', () => {
|
|
44
44
|
const props = { team_id: '1234' }
|
|
45
|
-
|
|
45
|
+
renderWithError({
|
|
46
46
|
message: 'Kaboom',
|
|
47
|
-
additionalProperties: (err) => {
|
|
47
|
+
additionalProperties: (err: Error) => {
|
|
48
48
|
expect(err.message).toBe('Kaboom')
|
|
49
49
|
return props
|
|
50
50
|
},
|
|
@@ -53,7 +53,7 @@ describe('PostHogErrorBoundary component', () => {
|
|
|
53
53
|
})
|
|
54
54
|
|
|
55
55
|
it('should render children without errors', () => {
|
|
56
|
-
const { container } =
|
|
56
|
+
const { container } = renderWithoutError()
|
|
57
57
|
expect(container.innerHTML).toBe('<div>Amazing content</div>')
|
|
58
58
|
})
|
|
59
59
|
})
|
|
@@ -63,11 +63,11 @@ describe('captureException processing', () => {
|
|
|
63
63
|
mockFunction(console, 'warn')
|
|
64
64
|
mockFunction(posthog, 'capture')
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
const renderWithError = (props: any) => render(<RenderWithError {...props} />)
|
|
67
67
|
|
|
68
68
|
it('should call capture with a stacktrace', () => {
|
|
69
|
-
|
|
70
|
-
const captureCalls = posthog.capture.mock.calls
|
|
69
|
+
renderWithError({ message: 'Kaboom', fallback: <div></div>, additionalProperties: {} })
|
|
70
|
+
const captureCalls = (posthog.capture as jest.Mock).mock.calls
|
|
71
71
|
expect(captureCalls.length).toBe(1)
|
|
72
72
|
const exceptionList = captureCalls[0][1].$exception_list
|
|
73
73
|
expect(exceptionList.length).toBe(1)
|
|
@@ -76,7 +76,7 @@ describe('captureException processing', () => {
|
|
|
76
76
|
})
|
|
77
77
|
})
|
|
78
78
|
|
|
79
|
-
function mockFunction(object, funcName) {
|
|
79
|
+
function mockFunction(object: any, funcName: string) {
|
|
80
80
|
const originalFunc = object[funcName]
|
|
81
81
|
|
|
82
82
|
beforeEach(() => {
|
|
@@ -88,11 +88,11 @@ function mockFunction(object, funcName) {
|
|
|
88
88
|
})
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
function ComponentWithError({ message }) {
|
|
91
|
+
function ComponentWithError({ message }: { message: string }): React.ReactElement {
|
|
92
92
|
throw new Error(message)
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
function RenderWithError({ message = 'Error', fallback, additionalProperties }) {
|
|
95
|
+
function RenderWithError({ message = 'Error', fallback, additionalProperties }: any) {
|
|
96
96
|
return (
|
|
97
97
|
<PostHogErrorBoundary fallback={fallback} additionalProperties={additionalProperties}>
|
|
98
98
|
<ComponentWithError message={message} />
|
|
@@ -100,7 +100,7 @@ function RenderWithError({ message = 'Error', fallback, additionalProperties })
|
|
|
100
100
|
)
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
function RenderWithoutError({ additionalProperties }) {
|
|
103
|
+
function RenderWithoutError({ additionalProperties }: any) {
|
|
104
104
|
return (
|
|
105
105
|
<PostHogErrorBoundary fallback={<div></div>} additionalProperties={additionalProperties}>
|
|
106
106
|
<div>Amazing content</div>
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { useState } from 'react'
|
|
3
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
4
|
+
import { PostHogProvider, PostHog } from '../../context'
|
|
5
|
+
import { PostHogFeature } from '../'
|
|
6
|
+
import '@testing-library/jest-dom'
|
|
7
|
+
|
|
8
|
+
const FEATURE_FLAG_STATUS: Record<string, string | boolean> = {
|
|
9
|
+
multivariate_feature: 'string-value',
|
|
10
|
+
example_feature_payload: 'test',
|
|
11
|
+
test: true,
|
|
12
|
+
test_false: false,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const FEATURE_FLAG_PAYLOADS: Record<string, any> = {
|
|
16
|
+
example_feature_payload: {
|
|
17
|
+
id: 1,
|
|
18
|
+
name: 'example_feature_1_payload',
|
|
19
|
+
key: 'example_feature_1_payload',
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('PostHogFeature component', () => {
|
|
24
|
+
let posthog: PostHog
|
|
25
|
+
|
|
26
|
+
const renderWith = (instance: PostHog, flag = 'test', matchValue: string | boolean | undefined = true) =>
|
|
27
|
+
render(
|
|
28
|
+
<PostHogProvider client={instance}>
|
|
29
|
+
<PostHogFeature flag={flag} match={matchValue}>
|
|
30
|
+
<div data-testid="helloDiv">Hello</div>
|
|
31
|
+
</PostHogFeature>
|
|
32
|
+
</PostHogProvider>
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
// IntersectionObserver isn't available in test environment
|
|
37
|
+
const mockIntersectionObserver = jest.fn()
|
|
38
|
+
mockIntersectionObserver.mockReturnValue({
|
|
39
|
+
observe: () => null,
|
|
40
|
+
unobserve: () => null,
|
|
41
|
+
disconnect: () => null,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
// eslint-disable-next-line compat/compat
|
|
45
|
+
window.IntersectionObserver = mockIntersectionObserver
|
|
46
|
+
|
|
47
|
+
posthog = {
|
|
48
|
+
isFeatureEnabled: (flag: string) => !!FEATURE_FLAG_STATUS[flag],
|
|
49
|
+
getFeatureFlag: (flag: string) => FEATURE_FLAG_STATUS[flag],
|
|
50
|
+
getFeatureFlagPayload: (flag: string) => FEATURE_FLAG_PAYLOADS[flag],
|
|
51
|
+
onFeatureFlags: (callback: any) => {
|
|
52
|
+
const activeFlags: string[] = []
|
|
53
|
+
for (const flag in FEATURE_FLAG_STATUS) {
|
|
54
|
+
if (FEATURE_FLAG_STATUS[flag]) {
|
|
55
|
+
activeFlags.push(flag)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
callback(activeFlags)
|
|
59
|
+
return () => {}
|
|
60
|
+
},
|
|
61
|
+
capture: jest.fn(),
|
|
62
|
+
} as unknown as PostHog
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('should track interactions with the feature component', () => {
|
|
66
|
+
renderWith(posthog)
|
|
67
|
+
|
|
68
|
+
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
69
|
+
expect(posthog.capture).toHaveBeenCalledWith('$feature_interaction', {
|
|
70
|
+
feature_flag: 'test',
|
|
71
|
+
$set: { '$feature_interaction/test': true },
|
|
72
|
+
})
|
|
73
|
+
expect(posthog.capture).toHaveBeenCalledTimes(1)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('should not fire for every interaction with the feature component', () => {
|
|
77
|
+
renderWith(posthog)
|
|
78
|
+
|
|
79
|
+
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
80
|
+
expect(posthog.capture).toHaveBeenCalledWith('$feature_interaction', {
|
|
81
|
+
feature_flag: 'test',
|
|
82
|
+
$set: { '$feature_interaction/test': true },
|
|
83
|
+
})
|
|
84
|
+
expect(posthog.capture).toHaveBeenCalledTimes(1)
|
|
85
|
+
|
|
86
|
+
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
87
|
+
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
88
|
+
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
89
|
+
expect(posthog.capture).toHaveBeenCalledTimes(1)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('should track an interaction with each child node of the feature component', () => {
|
|
93
|
+
render(
|
|
94
|
+
<PostHogProvider client={posthog}>
|
|
95
|
+
<PostHogFeature flag={'test'} match={true}>
|
|
96
|
+
<div data-testid="helloDiv">Hello</div>
|
|
97
|
+
<div data-testid="worldDiv">World!</div>
|
|
98
|
+
</PostHogFeature>
|
|
99
|
+
</PostHogProvider>
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
103
|
+
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
104
|
+
fireEvent.click(screen.getByTestId('worldDiv'))
|
|
105
|
+
fireEvent.click(screen.getByTestId('worldDiv'))
|
|
106
|
+
fireEvent.click(screen.getByTestId('worldDiv'))
|
|
107
|
+
expect(posthog.capture).toHaveBeenCalledWith('$feature_interaction', {
|
|
108
|
+
feature_flag: 'test',
|
|
109
|
+
$set: { '$feature_interaction/test': true },
|
|
110
|
+
})
|
|
111
|
+
expect(posthog.capture).toHaveBeenCalledTimes(1)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('should not fire events when interaction is disabled', () => {
|
|
115
|
+
render(
|
|
116
|
+
<PostHogProvider client={posthog}>
|
|
117
|
+
<PostHogFeature flag={'test'} match={true} trackInteraction={false}>
|
|
118
|
+
<div data-testid="helloDiv">Hello</div>
|
|
119
|
+
</PostHogFeature>
|
|
120
|
+
</PostHogProvider>
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
124
|
+
expect(posthog.capture).not.toHaveBeenCalled()
|
|
125
|
+
|
|
126
|
+
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
127
|
+
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
128
|
+
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
129
|
+
expect(posthog.capture).not.toHaveBeenCalled()
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('should fire events when interaction is disabled but re-enabled after', () => {
|
|
133
|
+
const DynamicUpdateComponent = () => {
|
|
134
|
+
const [trackInteraction, setTrackInteraction] = useState(false)
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<>
|
|
138
|
+
<div
|
|
139
|
+
data-testid="clicker"
|
|
140
|
+
onClick={() => {
|
|
141
|
+
setTrackInteraction(true)
|
|
142
|
+
}}
|
|
143
|
+
>
|
|
144
|
+
Click me
|
|
145
|
+
</div>
|
|
146
|
+
<PostHogFeature flag={'test'} match={true} trackInteraction={trackInteraction}>
|
|
147
|
+
<div data-testid="helloDiv">Hello</div>
|
|
148
|
+
</PostHogFeature>
|
|
149
|
+
</>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
render(
|
|
154
|
+
<PostHogProvider client={posthog}>
|
|
155
|
+
<DynamicUpdateComponent />
|
|
156
|
+
</PostHogProvider>
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
160
|
+
expect(posthog.capture).not.toHaveBeenCalled()
|
|
161
|
+
|
|
162
|
+
fireEvent.click(screen.getByTestId('clicker'))
|
|
163
|
+
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
164
|
+
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
165
|
+
expect(posthog.capture).toHaveBeenCalledWith('$feature_interaction', {
|
|
166
|
+
feature_flag: 'test',
|
|
167
|
+
$set: { '$feature_interaction/test': true },
|
|
168
|
+
})
|
|
169
|
+
expect(posthog.capture).toHaveBeenCalledTimes(1)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('should not show the feature component if the flag is not enabled', () => {
|
|
173
|
+
renderWith(posthog, 'test_value')
|
|
174
|
+
|
|
175
|
+
expect(screen.queryByTestId('helloDiv')).not.toBeInTheDocument()
|
|
176
|
+
expect(posthog.capture).not.toHaveBeenCalled()
|
|
177
|
+
|
|
178
|
+
// check if any elements are found
|
|
179
|
+
const allTags = screen.queryAllByText(/.*/)
|
|
180
|
+
|
|
181
|
+
// Assert that no random elements are found
|
|
182
|
+
expect(allTags.length).toEqual(2)
|
|
183
|
+
expect(allTags[0].tagName).toEqual('BODY')
|
|
184
|
+
expect(allTags[1].tagName).toEqual('DIV')
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('should fallback when provided', () => {
|
|
188
|
+
render(
|
|
189
|
+
<PostHogProvider client={posthog}>
|
|
190
|
+
<PostHogFeature flag={'test_false'} match={true} fallback={<div data-testid="nope">Nope</div>}>
|
|
191
|
+
<div data-testid="helloDiv">Hello</div>
|
|
192
|
+
</PostHogFeature>
|
|
193
|
+
</PostHogProvider>
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
expect(screen.queryByTestId('helloDiv')).not.toBeInTheDocument()
|
|
197
|
+
expect(posthog.capture).not.toHaveBeenCalled()
|
|
198
|
+
|
|
199
|
+
fireEvent.click(screen.getByTestId('nope'))
|
|
200
|
+
expect(posthog.capture).not.toHaveBeenCalled()
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('should handle showing multivariate flags with bool match', () => {
|
|
204
|
+
renderWith(posthog, 'multivariate_feature')
|
|
205
|
+
|
|
206
|
+
expect(screen.queryByTestId('helloDiv')).not.toBeInTheDocument()
|
|
207
|
+
expect(posthog.capture).not.toHaveBeenCalled()
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('should handle showing multivariate flags with incorrect match', () => {
|
|
211
|
+
renderWith(posthog, 'multivariate_feature', 'string-valueCXCC')
|
|
212
|
+
|
|
213
|
+
expect(screen.queryByTestId('helloDiv')).not.toBeInTheDocument()
|
|
214
|
+
expect(posthog.capture).not.toHaveBeenCalled()
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('should handle showing multivariate flags', () => {
|
|
218
|
+
renderWith(posthog, 'multivariate_feature', 'string-value')
|
|
219
|
+
|
|
220
|
+
expect(screen.queryByTestId('helloDiv')).toBeInTheDocument()
|
|
221
|
+
expect(posthog.capture).not.toHaveBeenCalled()
|
|
222
|
+
|
|
223
|
+
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
224
|
+
expect(posthog.capture).toHaveBeenCalledWith('$feature_interaction', {
|
|
225
|
+
feature_flag: 'multivariate_feature',
|
|
226
|
+
feature_flag_variant: 'string-value',
|
|
227
|
+
$set: { '$feature_interaction/multivariate_feature': 'string-value' },
|
|
228
|
+
})
|
|
229
|
+
expect(posthog.capture).toHaveBeenCalledTimes(1)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('should handle payload flags', () => {
|
|
233
|
+
render(
|
|
234
|
+
<PostHogProvider client={posthog}>
|
|
235
|
+
<PostHogFeature flag={'example_feature_payload'} match={'test'}>
|
|
236
|
+
{(payload: any) => {
|
|
237
|
+
return <div data-testid={`hi_${payload.name}`}>Hullo</div>
|
|
238
|
+
}}
|
|
239
|
+
</PostHogFeature>
|
|
240
|
+
</PostHogProvider>
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
expect(screen.queryByTestId('hi_example_feature_1_payload')).toBeInTheDocument()
|
|
244
|
+
expect(posthog.capture).not.toHaveBeenCalled()
|
|
245
|
+
|
|
246
|
+
fireEvent.click(screen.getByTestId('hi_example_feature_1_payload'))
|
|
247
|
+
expect(posthog.capture).toHaveBeenCalledTimes(1)
|
|
248
|
+
})
|
|
249
|
+
})
|
package/src/components/index.ts
CHANGED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import React, { MouseEventHandler, useEffect, useMemo, useRef } from 'react'
|
|
2
|
+
import { isNull } from '../../utils/type-utils'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* VisibilityAndClickTracker is an internal component,
|
|
6
|
+
* its API might change without warning and without being signalled as a breaking change
|
|
7
|
+
*
|
|
8
|
+
* Wraps the provided children in a div, and tracks visibility of and clicks on that div
|
|
9
|
+
*/
|
|
10
|
+
export function VisibilityAndClickTracker({
|
|
11
|
+
children,
|
|
12
|
+
onIntersect,
|
|
13
|
+
onClick,
|
|
14
|
+
trackView,
|
|
15
|
+
options,
|
|
16
|
+
...props
|
|
17
|
+
}: {
|
|
18
|
+
children: React.ReactNode
|
|
19
|
+
onIntersect: (entry: IntersectionObserverEntry) => void
|
|
20
|
+
onClick?: MouseEventHandler<HTMLDivElement>
|
|
21
|
+
trackView: boolean
|
|
22
|
+
options?: IntersectionObserverInit
|
|
23
|
+
}): JSX.Element {
|
|
24
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
25
|
+
|
|
26
|
+
const observerOptions = useMemo(
|
|
27
|
+
() => ({
|
|
28
|
+
threshold: 0.1,
|
|
29
|
+
...options,
|
|
30
|
+
}),
|
|
31
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
32
|
+
[options?.threshold, options?.root, options?.rootMargin]
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (isNull(ref.current) || !trackView) return
|
|
37
|
+
|
|
38
|
+
// eslint-disable-next-line compat/compat
|
|
39
|
+
const observer = new IntersectionObserver(([entry]) => onIntersect(entry), observerOptions)
|
|
40
|
+
observer.observe(ref.current)
|
|
41
|
+
return () => observer.disconnect()
|
|
42
|
+
}, [observerOptions, trackView, onIntersect])
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div ref={ref} {...props} onClick={onClick}>
|
|
46
|
+
{children}
|
|
47
|
+
</div>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React, { Children, ReactNode, useCallback, useRef } from 'react'
|
|
2
|
+
import { VisibilityAndClickTracker } from './VisibilityAndClickTracker'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* VisibilityAndClickTrackers is an internal component,
|
|
6
|
+
* its API might change without warning and without being signalled as a breaking change
|
|
7
|
+
*
|
|
8
|
+
* Wraps each of the children passed to it for visiblity and click tracking
|
|
9
|
+
*
|
|
10
|
+
*/
|
|
11
|
+
export function VisibilityAndClickTrackers({
|
|
12
|
+
children,
|
|
13
|
+
trackInteraction,
|
|
14
|
+
trackView,
|
|
15
|
+
options,
|
|
16
|
+
onInteract,
|
|
17
|
+
onView,
|
|
18
|
+
...props
|
|
19
|
+
}: {
|
|
20
|
+
flag: string
|
|
21
|
+
children: React.ReactNode
|
|
22
|
+
trackInteraction: boolean
|
|
23
|
+
trackView: boolean
|
|
24
|
+
options?: IntersectionObserverInit
|
|
25
|
+
onInteract?: () => void
|
|
26
|
+
onView?: () => void
|
|
27
|
+
}): JSX.Element {
|
|
28
|
+
const clickTrackedRef = useRef(false)
|
|
29
|
+
const visibilityTrackedRef = useRef(false)
|
|
30
|
+
|
|
31
|
+
const cachedOnClick = useCallback(() => {
|
|
32
|
+
if (!clickTrackedRef.current && trackInteraction && onInteract) {
|
|
33
|
+
onInteract()
|
|
34
|
+
clickTrackedRef.current = true
|
|
35
|
+
}
|
|
36
|
+
}, [trackInteraction, onInteract])
|
|
37
|
+
|
|
38
|
+
const onIntersect = (entry: IntersectionObserverEntry) => {
|
|
39
|
+
if (!visibilityTrackedRef.current && entry.isIntersecting && onView) {
|
|
40
|
+
onView()
|
|
41
|
+
visibilityTrackedRef.current = true
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const trackedChildren = Children.map(children, (child: ReactNode) => {
|
|
46
|
+
return (
|
|
47
|
+
<VisibilityAndClickTracker
|
|
48
|
+
onClick={cachedOnClick}
|
|
49
|
+
onIntersect={onIntersect}
|
|
50
|
+
trackView={trackView}
|
|
51
|
+
options={options}
|
|
52
|
+
{...props}
|
|
53
|
+
>
|
|
54
|
+
{child}
|
|
55
|
+
</VisibilityAndClickTracker>
|
|
56
|
+
)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
return <>{trackedChildren}</>
|
|
60
|
+
}
|
|
@@ -1,30 +1,31 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
2
|
import { render } from '@testing-library/react'
|
|
3
|
-
import { PostHogProvider } from '..'
|
|
3
|
+
import { PostHogProvider, PostHog } from '..'
|
|
4
4
|
|
|
5
5
|
describe('PostHogContext component', () => {
|
|
6
|
-
|
|
7
|
-
'render',
|
|
8
|
-
() => () =>
|
|
9
|
-
render(
|
|
10
|
-
<PostHogProvider client={given.posthog}>
|
|
11
|
-
<div>Hello</div>
|
|
12
|
-
</PostHogProvider>
|
|
13
|
-
)
|
|
14
|
-
)
|
|
15
|
-
given('posthog', () => ({}))
|
|
6
|
+
const posthog = {} as unknown as PostHog
|
|
16
7
|
|
|
17
8
|
it('should return a client instance from the context if available', () => {
|
|
18
|
-
|
|
9
|
+
render(
|
|
10
|
+
<PostHogProvider client={posthog}>
|
|
11
|
+
<div>Hello</div>
|
|
12
|
+
</PostHogProvider>
|
|
13
|
+
)
|
|
19
14
|
})
|
|
20
15
|
|
|
21
16
|
it("should not throw error if a client instance can't be found in the context", () => {
|
|
22
|
-
given('posthog', () => undefined) // it might not exist in SSR for example
|
|
23
|
-
|
|
24
17
|
// eslint-disable-next-line no-console
|
|
25
18
|
console.warn = jest.fn()
|
|
26
19
|
|
|
27
|
-
expect(() =>
|
|
20
|
+
expect(() => {
|
|
21
|
+
render(
|
|
22
|
+
// we have to cast `as any` so that we can test for when
|
|
23
|
+
// posthog might not exist - in SSR for example
|
|
24
|
+
<PostHogProvider client={undefined as any}>
|
|
25
|
+
<div>Hello</div>
|
|
26
|
+
</PostHogProvider>
|
|
27
|
+
)
|
|
28
|
+
}).not.toThrow()
|
|
28
29
|
|
|
29
30
|
// eslint-disable-next-line no-console
|
|
30
31
|
expect(console.warn).toHaveBeenCalledWith(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
2
|
import { render, act } from '@testing-library/react'
|
|
3
|
-
import { PostHogProvider } from '..'
|
|
3
|
+
import { PostHogProvider, PostHog } from '..'
|
|
4
4
|
import posthogJs from 'posthog-js'
|
|
5
5
|
|
|
6
6
|
// Mock posthog-js
|
|
@@ -15,7 +15,7 @@ jest.mock('posthog-js', () => ({
|
|
|
15
15
|
|
|
16
16
|
describe('PostHogProvider component', () => {
|
|
17
17
|
it('should render children components', () => {
|
|
18
|
-
const posthog = {}
|
|
18
|
+
const posthog = {} as unknown as PostHog
|
|
19
19
|
const { getByText } = render(
|
|
20
20
|
<PostHogProvider client={posthog}>
|
|
21
21
|
<div>Test</div>
|
|
@@ -111,7 +111,7 @@ describe('PostHogProvider component', () => {
|
|
|
111
111
|
})
|
|
112
112
|
|
|
113
113
|
it('warns if posthogJs has been loaded elsewhere', () => {
|
|
114
|
-
posthogJs.__loaded = true
|
|
114
|
+
;(posthogJs as any).__loaded = true
|
|
115
115
|
|
|
116
116
|
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation()
|
|
117
117
|
render(
|
|
@@ -125,7 +125,7 @@ describe('PostHogProvider component', () => {
|
|
|
125
125
|
)
|
|
126
126
|
|
|
127
127
|
consoleSpy.mockRestore()
|
|
128
|
-
posthogJs.__loaded = false
|
|
128
|
+
;(posthogJs as any).__loaded = false
|
|
129
129
|
})
|
|
130
130
|
})
|
|
131
131
|
})
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
2
|
import { renderHook } from '@testing-library/react-hooks'
|
|
3
|
-
import { PostHogProvider } from '../../context'
|
|
3
|
+
import { PostHogProvider, PostHog } from '../../context'
|
|
4
4
|
import { useFeatureFlagPayload, useFeatureFlagVariantKey, useFeatureFlagEnabled, useActiveFeatureFlags } from '../index'
|
|
5
5
|
|
|
6
6
|
jest.useFakeTimers()
|
|
7
7
|
|
|
8
8
|
const ACTIVE_FEATURE_FLAGS = ['example_feature_true', 'multivariate_feature', 'example_feature_payload']
|
|
9
9
|
|
|
10
|
-
const FEATURE_FLAG_STATUS = {
|
|
10
|
+
const FEATURE_FLAG_STATUS: Record<string, string | boolean> = {
|
|
11
11
|
example_feature_true: true,
|
|
12
12
|
example_feature_false: false,
|
|
13
13
|
multivariate_feature: 'string-value',
|
|
14
14
|
example_feature_payload: 'test',
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
const FEATURE_FLAG_PAYLOADS = {
|
|
17
|
+
const FEATURE_FLAG_PAYLOADS: Record<string, any> = {
|
|
18
18
|
example_feature_payload: {
|
|
19
19
|
id: 1,
|
|
20
20
|
name: 'example_feature_1_payload',
|
|
@@ -23,28 +23,32 @@ const FEATURE_FLAG_PAYLOADS = {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
describe('useFeatureFlagPayload hook', () => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
))
|
|
26
|
+
let posthog: PostHog
|
|
27
|
+
let renderProvider: React.FC<{ children: React.ReactNode }>
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
posthog = {
|
|
31
|
+
isFeatureEnabled: (flag: string) => !!FEATURE_FLAG_STATUS[flag],
|
|
32
|
+
getFeatureFlag: (flag: string) => FEATURE_FLAG_STATUS[flag],
|
|
33
|
+
getFeatureFlagPayload: (flag: string) => FEATURE_FLAG_PAYLOADS[flag],
|
|
34
|
+
onFeatureFlags: (callback: any) => {
|
|
35
|
+
const activeFlags: string[] = []
|
|
36
|
+
for (const flag in FEATURE_FLAG_STATUS) {
|
|
37
|
+
if (FEATURE_FLAG_STATUS[flag]) {
|
|
38
|
+
activeFlags.push(flag)
|
|
39
|
+
}
|
|
39
40
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
41
|
+
callback(activeFlags)
|
|
42
|
+
return () => {}
|
|
43
|
+
},
|
|
44
|
+
featureFlags: {
|
|
45
|
+
getFlags: () => ACTIVE_FEATURE_FLAGS,
|
|
46
|
+
} as unknown as PostHog['featureFlags'],
|
|
47
|
+
} as unknown as PostHog
|
|
48
|
+
|
|
49
|
+
// eslint-disable-next-line react/display-name
|
|
50
|
+
renderProvider = ({ children }) => <PostHogProvider client={posthog}>{children}</PostHogProvider>
|
|
51
|
+
})
|
|
48
52
|
|
|
49
53
|
it.each([
|
|
50
54
|
['example_feature_true', true],
|
|
@@ -53,8 +57,8 @@ describe('useFeatureFlagPayload hook', () => {
|
|
|
53
57
|
['multivariate_feature', true],
|
|
54
58
|
['example_feature_payload', true],
|
|
55
59
|
])('should get the boolean feature flag', (flag, expected) => {
|
|
56
|
-
|
|
57
|
-
wrapper:
|
|
60
|
+
const { result } = renderHook(() => useFeatureFlagEnabled(flag), {
|
|
61
|
+
wrapper: renderProvider,
|
|
58
62
|
})
|
|
59
63
|
expect(result.current).toEqual(expected)
|
|
60
64
|
})
|
|
@@ -66,15 +70,15 @@ describe('useFeatureFlagPayload hook', () => {
|
|
|
66
70
|
['multivariate_feature', undefined],
|
|
67
71
|
['example_feature_payload', FEATURE_FLAG_PAYLOADS.example_feature_payload],
|
|
68
72
|
])('should get the payload feature flag', (flag, expected) => {
|
|
69
|
-
|
|
70
|
-
wrapper:
|
|
73
|
+
const { result } = renderHook(() => useFeatureFlagPayload(flag), {
|
|
74
|
+
wrapper: renderProvider,
|
|
71
75
|
})
|
|
72
76
|
expect(result.current).toEqual(expected)
|
|
73
77
|
})
|
|
74
78
|
|
|
75
79
|
it('should return the active feature flags', () => {
|
|
76
|
-
|
|
77
|
-
wrapper:
|
|
80
|
+
const { result } = renderHook(() => useActiveFeatureFlags(), {
|
|
81
|
+
wrapper: renderProvider,
|
|
78
82
|
})
|
|
79
83
|
expect(result.current).toEqual(['example_feature_true', 'multivariate_feature', 'example_feature_payload'])
|
|
80
84
|
})
|
|
@@ -85,8 +89,8 @@ describe('useFeatureFlagPayload hook', () => {
|
|
|
85
89
|
['missing', undefined],
|
|
86
90
|
['multivariate_feature', 'string-value'],
|
|
87
91
|
])('should get the feature flag variant key', (flag, expected) => {
|
|
88
|
-
|
|
89
|
-
wrapper:
|
|
92
|
+
const { result } = renderHook(() => useFeatureFlagVariantKey(flag), {
|
|
93
|
+
wrapper: renderProvider,
|
|
90
94
|
})
|
|
91
95
|
expect(result.current).toEqual(expected)
|
|
92
96
|
})
|