@posthog/react 1.2.3 → 1.4.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 +109 -59
- package/dist/esm/index.js.map +1 -1
- package/dist/types/index.d.ts +24 -3
- package/dist/umd/index.js +111 -58
- 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 +252 -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/PostHogContext.ts +5 -2
- package/src/context/PostHogProvider.tsx +7 -1
- 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} +36 -31
- package/src/hooks/__tests__/usePostHog.test.tsx +19 -0
- package/src/hooks/useActiveFeatureFlags.ts +8 -3
- package/src/hooks/useFeatureFlagEnabled.ts +11 -3
- package/src/hooks/useFeatureFlagPayload.ts +8 -3
- package/src/hooks/useFeatureFlagVariantKey.ts +7 -3
- package/src/components/__tests__/PostHogFeature.test.jsx +0 -298
- package/src/hooks/__tests__/usePostHog.test.jsx +0 -23
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { useFeatureFlagPayload, useFeatureFlagVariantKey, usePostHog } from '../hooks'
|
|
2
|
-
import React
|
|
2
|
+
import React from 'react'
|
|
3
3
|
import { PostHog } from '../context'
|
|
4
|
-
import { isFunction,
|
|
4
|
+
import { isFunction, isUndefined } from '../utils/type-utils'
|
|
5
|
+
import { VisibilityAndClickTrackers } from './internal/VisibilityAndClickTrackers'
|
|
5
6
|
|
|
6
7
|
export type PostHogFeatureProps = React.HTMLProps<HTMLDivElement> & {
|
|
7
8
|
flag: string
|
|
@@ -25,6 +26,7 @@ export function PostHogFeature({
|
|
|
25
26
|
}: PostHogFeatureProps): JSX.Element | null {
|
|
26
27
|
const payload = useFeatureFlagPayload(flag)
|
|
27
28
|
const variant = useFeatureFlagVariantKey(flag)
|
|
29
|
+
const posthog = usePostHog()
|
|
28
30
|
|
|
29
31
|
const shouldTrackInteraction = trackInteraction ?? true
|
|
30
32
|
const shouldTrackView = trackView ?? true
|
|
@@ -37,6 +39,8 @@ export function PostHogFeature({
|
|
|
37
39
|
options={visibilityObserverOptions}
|
|
38
40
|
trackInteraction={shouldTrackInteraction}
|
|
39
41
|
trackView={shouldTrackView}
|
|
42
|
+
onInteract={() => captureFeatureInteraction({ flag, posthog, flagVariant: variant })}
|
|
43
|
+
onView={() => captureFeatureView({ flag, posthog, flagVariant: variant })}
|
|
40
44
|
{...props}
|
|
41
45
|
>
|
|
42
46
|
{childNode}
|
|
@@ -46,7 +50,7 @@ export function PostHogFeature({
|
|
|
46
50
|
return <>{fallback}</>
|
|
47
51
|
}
|
|
48
52
|
|
|
49
|
-
function captureFeatureInteraction({
|
|
53
|
+
export function captureFeatureInteraction({
|
|
50
54
|
flag,
|
|
51
55
|
posthog,
|
|
52
56
|
flagVariant,
|
|
@@ -65,7 +69,7 @@ function captureFeatureInteraction({
|
|
|
65
69
|
posthog.capture('$feature_interaction', properties)
|
|
66
70
|
}
|
|
67
71
|
|
|
68
|
-
function captureFeatureView({
|
|
72
|
+
export function captureFeatureView({
|
|
69
73
|
flag,
|
|
70
74
|
posthog,
|
|
71
75
|
flagVariant,
|
|
@@ -83,92 +87,3 @@ function captureFeatureView({
|
|
|
83
87
|
}
|
|
84
88
|
posthog.capture('$feature_view', properties)
|
|
85
89
|
}
|
|
86
|
-
|
|
87
|
-
function VisibilityAndClickTracker({
|
|
88
|
-
flag,
|
|
89
|
-
children,
|
|
90
|
-
onIntersect,
|
|
91
|
-
onClick,
|
|
92
|
-
trackView,
|
|
93
|
-
options,
|
|
94
|
-
...props
|
|
95
|
-
}: {
|
|
96
|
-
flag: string
|
|
97
|
-
children: React.ReactNode
|
|
98
|
-
onIntersect: (entry: IntersectionObserverEntry) => void
|
|
99
|
-
onClick: () => void
|
|
100
|
-
trackView: boolean
|
|
101
|
-
options?: IntersectionObserverInit
|
|
102
|
-
}): JSX.Element {
|
|
103
|
-
const ref = useRef<HTMLDivElement>(null)
|
|
104
|
-
const posthog = usePostHog()
|
|
105
|
-
|
|
106
|
-
useEffect(() => {
|
|
107
|
-
if (isNull(ref.current) || !trackView) return
|
|
108
|
-
|
|
109
|
-
// eslint-disable-next-line compat/compat
|
|
110
|
-
const observer = new IntersectionObserver(([entry]) => onIntersect(entry), {
|
|
111
|
-
threshold: 0.1,
|
|
112
|
-
...options,
|
|
113
|
-
})
|
|
114
|
-
observer.observe(ref.current)
|
|
115
|
-
return () => observer.disconnect()
|
|
116
|
-
}, [flag, options, posthog, ref, trackView, onIntersect])
|
|
117
|
-
|
|
118
|
-
return (
|
|
119
|
-
<div ref={ref} {...props} onClick={onClick}>
|
|
120
|
-
{children}
|
|
121
|
-
</div>
|
|
122
|
-
)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function VisibilityAndClickTrackers({
|
|
126
|
-
flag,
|
|
127
|
-
children,
|
|
128
|
-
trackInteraction,
|
|
129
|
-
trackView,
|
|
130
|
-
options,
|
|
131
|
-
...props
|
|
132
|
-
}: {
|
|
133
|
-
flag: string
|
|
134
|
-
children: React.ReactNode
|
|
135
|
-
trackInteraction: boolean
|
|
136
|
-
trackView: boolean
|
|
137
|
-
options?: IntersectionObserverInit
|
|
138
|
-
}): JSX.Element {
|
|
139
|
-
const clickTrackedRef = useRef(false)
|
|
140
|
-
const visibilityTrackedRef = useRef(false)
|
|
141
|
-
const posthog = usePostHog()
|
|
142
|
-
const variant = useFeatureFlagVariantKey(flag)
|
|
143
|
-
|
|
144
|
-
const cachedOnClick = useCallback(() => {
|
|
145
|
-
if (!clickTrackedRef.current && trackInteraction) {
|
|
146
|
-
captureFeatureInteraction({ flag, posthog, flagVariant: variant })
|
|
147
|
-
clickTrackedRef.current = true
|
|
148
|
-
}
|
|
149
|
-
}, [flag, posthog, trackInteraction, variant])
|
|
150
|
-
|
|
151
|
-
const onIntersect = (entry: IntersectionObserverEntry) => {
|
|
152
|
-
if (!visibilityTrackedRef.current && entry.isIntersecting) {
|
|
153
|
-
captureFeatureView({ flag, posthog, flagVariant: variant })
|
|
154
|
-
visibilityTrackedRef.current = true
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const trackedChildren = Children.map(children, (child: ReactNode) => {
|
|
159
|
-
return (
|
|
160
|
-
<VisibilityAndClickTracker
|
|
161
|
-
flag={flag}
|
|
162
|
-
onClick={cachedOnClick}
|
|
163
|
-
onIntersect={onIntersect}
|
|
164
|
-
trackView={trackView}
|
|
165
|
-
options={options}
|
|
166
|
-
{...props}
|
|
167
|
-
>
|
|
168
|
-
{child}
|
|
169
|
-
</VisibilityAndClickTracker>
|
|
170
|
-
)
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
return <>{trackedChildren}</>
|
|
174
|
-
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { render, screen } from '@testing-library/react'
|
|
3
|
+
import { PostHog, PostHogProvider } from '../../context'
|
|
4
|
+
import { PostHogCaptureOnViewed } from '../'
|
|
5
|
+
import '@testing-library/jest-dom'
|
|
6
|
+
|
|
7
|
+
describe('PostHogCaptureOnViewed component', () => {
|
|
8
|
+
let mockObserverCallback: any = null
|
|
9
|
+
|
|
10
|
+
let fakePosthog: PostHog
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
fakePosthog = {
|
|
14
|
+
capture: jest.fn(),
|
|
15
|
+
} as unknown as PostHog
|
|
16
|
+
|
|
17
|
+
const mockIntersectionObserver = jest.fn((callback) => {
|
|
18
|
+
mockObserverCallback = callback
|
|
19
|
+
return {
|
|
20
|
+
observe: jest.fn(),
|
|
21
|
+
unobserve: jest.fn(),
|
|
22
|
+
disconnect: jest.fn(),
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
mockIntersectionObserver.prototype = {}
|
|
27
|
+
// eslint-disable-next-line compat/compat
|
|
28
|
+
window.IntersectionObserver = mockIntersectionObserver as unknown as typeof IntersectionObserver
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should render children', () => {
|
|
32
|
+
render(
|
|
33
|
+
<PostHogProvider client={fakePosthog}>
|
|
34
|
+
<PostHogCaptureOnViewed name="test-element">
|
|
35
|
+
<div data-testid="child">Hello</div>
|
|
36
|
+
</PostHogCaptureOnViewed>
|
|
37
|
+
</PostHogProvider>
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
expect(screen.getByTestId('child')).toBeInTheDocument()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('should track when element comes into view', () => {
|
|
44
|
+
render(
|
|
45
|
+
<PostHogProvider client={fakePosthog}>
|
|
46
|
+
<PostHogCaptureOnViewed name="test-element">
|
|
47
|
+
<div data-testid="child">Hello</div>
|
|
48
|
+
</PostHogCaptureOnViewed>
|
|
49
|
+
</PostHogProvider>
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
expect(fakePosthog.capture).not.toHaveBeenCalled()
|
|
53
|
+
|
|
54
|
+
mockObserverCallback([{ isIntersecting: true }])
|
|
55
|
+
|
|
56
|
+
expect(fakePosthog.capture).toHaveBeenCalledWith('$element_viewed', {
|
|
57
|
+
element_name: 'test-element',
|
|
58
|
+
})
|
|
59
|
+
expect(fakePosthog.capture).toHaveBeenCalledTimes(1)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('should only track visibility once', () => {
|
|
63
|
+
render(
|
|
64
|
+
<PostHogProvider client={fakePosthog}>
|
|
65
|
+
<PostHogCaptureOnViewed name="test-element">
|
|
66
|
+
<div data-testid="child">Hello</div>
|
|
67
|
+
</PostHogCaptureOnViewed>
|
|
68
|
+
</PostHogProvider>
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
mockObserverCallback([{ isIntersecting: true }])
|
|
72
|
+
expect(fakePosthog.capture).toHaveBeenCalledTimes(1)
|
|
73
|
+
|
|
74
|
+
mockObserverCallback([{ isIntersecting: true }])
|
|
75
|
+
mockObserverCallback([{ isIntersecting: true }])
|
|
76
|
+
expect(fakePosthog.capture).toHaveBeenCalledTimes(1)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('should include custom properties', () => {
|
|
80
|
+
render(
|
|
81
|
+
<PostHogProvider client={fakePosthog}>
|
|
82
|
+
<PostHogCaptureOnViewed name="test-element" properties={{ category: 'hero', priority: 'high' }}>
|
|
83
|
+
<div data-testid="child">Hello</div>
|
|
84
|
+
</PostHogCaptureOnViewed>
|
|
85
|
+
</PostHogProvider>
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
mockObserverCallback([{ isIntersecting: true }])
|
|
89
|
+
|
|
90
|
+
expect(fakePosthog.capture).toHaveBeenCalledWith('$element_viewed', {
|
|
91
|
+
element_name: 'test-element',
|
|
92
|
+
category: 'hero',
|
|
93
|
+
priority: 'high',
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('should not track when element is not intersecting', () => {
|
|
98
|
+
render(
|
|
99
|
+
<PostHogProvider client={fakePosthog}>
|
|
100
|
+
<PostHogCaptureOnViewed name="test-element">
|
|
101
|
+
<div data-testid="child">Hello</div>
|
|
102
|
+
</PostHogCaptureOnViewed>
|
|
103
|
+
</PostHogProvider>
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
mockObserverCallback([{ isIntersecting: false }])
|
|
107
|
+
|
|
108
|
+
expect(fakePosthog.capture).not.toHaveBeenCalled()
|
|
109
|
+
})
|
|
110
|
+
})
|
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,252 @@
|
|
|
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
|
+
featureFlags: {
|
|
63
|
+
hasLoadedFlags: true,
|
|
64
|
+
},
|
|
65
|
+
} as unknown as PostHog
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('should track interactions with the feature component', () => {
|
|
69
|
+
renderWith(posthog)
|
|
70
|
+
|
|
71
|
+
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
72
|
+
expect(posthog.capture).toHaveBeenCalledWith('$feature_interaction', {
|
|
73
|
+
feature_flag: 'test',
|
|
74
|
+
$set: { '$feature_interaction/test': true },
|
|
75
|
+
})
|
|
76
|
+
expect(posthog.capture).toHaveBeenCalledTimes(1)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('should not fire for every interaction with the feature component', () => {
|
|
80
|
+
renderWith(posthog)
|
|
81
|
+
|
|
82
|
+
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
83
|
+
expect(posthog.capture).toHaveBeenCalledWith('$feature_interaction', {
|
|
84
|
+
feature_flag: 'test',
|
|
85
|
+
$set: { '$feature_interaction/test': true },
|
|
86
|
+
})
|
|
87
|
+
expect(posthog.capture).toHaveBeenCalledTimes(1)
|
|
88
|
+
|
|
89
|
+
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
90
|
+
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
91
|
+
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
92
|
+
expect(posthog.capture).toHaveBeenCalledTimes(1)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('should track an interaction with each child node of the feature component', () => {
|
|
96
|
+
render(
|
|
97
|
+
<PostHogProvider client={posthog}>
|
|
98
|
+
<PostHogFeature flag={'test'} match={true}>
|
|
99
|
+
<div data-testid="helloDiv">Hello</div>
|
|
100
|
+
<div data-testid="worldDiv">World!</div>
|
|
101
|
+
</PostHogFeature>
|
|
102
|
+
</PostHogProvider>
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
106
|
+
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
107
|
+
fireEvent.click(screen.getByTestId('worldDiv'))
|
|
108
|
+
fireEvent.click(screen.getByTestId('worldDiv'))
|
|
109
|
+
fireEvent.click(screen.getByTestId('worldDiv'))
|
|
110
|
+
expect(posthog.capture).toHaveBeenCalledWith('$feature_interaction', {
|
|
111
|
+
feature_flag: 'test',
|
|
112
|
+
$set: { '$feature_interaction/test': true },
|
|
113
|
+
})
|
|
114
|
+
expect(posthog.capture).toHaveBeenCalledTimes(1)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('should not fire events when interaction is disabled', () => {
|
|
118
|
+
render(
|
|
119
|
+
<PostHogProvider client={posthog}>
|
|
120
|
+
<PostHogFeature flag={'test'} match={true} trackInteraction={false}>
|
|
121
|
+
<div data-testid="helloDiv">Hello</div>
|
|
122
|
+
</PostHogFeature>
|
|
123
|
+
</PostHogProvider>
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
127
|
+
expect(posthog.capture).not.toHaveBeenCalled()
|
|
128
|
+
|
|
129
|
+
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
130
|
+
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
131
|
+
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
132
|
+
expect(posthog.capture).not.toHaveBeenCalled()
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('should fire events when interaction is disabled but re-enabled after', () => {
|
|
136
|
+
const DynamicUpdateComponent = () => {
|
|
137
|
+
const [trackInteraction, setTrackInteraction] = useState(false)
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<>
|
|
141
|
+
<div
|
|
142
|
+
data-testid="clicker"
|
|
143
|
+
onClick={() => {
|
|
144
|
+
setTrackInteraction(true)
|
|
145
|
+
}}
|
|
146
|
+
>
|
|
147
|
+
Click me
|
|
148
|
+
</div>
|
|
149
|
+
<PostHogFeature flag={'test'} match={true} trackInteraction={trackInteraction}>
|
|
150
|
+
<div data-testid="helloDiv">Hello</div>
|
|
151
|
+
</PostHogFeature>
|
|
152
|
+
</>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
render(
|
|
157
|
+
<PostHogProvider client={posthog}>
|
|
158
|
+
<DynamicUpdateComponent />
|
|
159
|
+
</PostHogProvider>
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
163
|
+
expect(posthog.capture).not.toHaveBeenCalled()
|
|
164
|
+
|
|
165
|
+
fireEvent.click(screen.getByTestId('clicker'))
|
|
166
|
+
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
167
|
+
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
168
|
+
expect(posthog.capture).toHaveBeenCalledWith('$feature_interaction', {
|
|
169
|
+
feature_flag: 'test',
|
|
170
|
+
$set: { '$feature_interaction/test': true },
|
|
171
|
+
})
|
|
172
|
+
expect(posthog.capture).toHaveBeenCalledTimes(1)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('should not show the feature component if the flag is not enabled', () => {
|
|
176
|
+
renderWith(posthog, 'test_value')
|
|
177
|
+
|
|
178
|
+
expect(screen.queryByTestId('helloDiv')).not.toBeInTheDocument()
|
|
179
|
+
expect(posthog.capture).not.toHaveBeenCalled()
|
|
180
|
+
|
|
181
|
+
// check if any elements are found
|
|
182
|
+
const allTags = screen.queryAllByText(/.*/)
|
|
183
|
+
|
|
184
|
+
// Assert that no random elements are found
|
|
185
|
+
expect(allTags.length).toEqual(2)
|
|
186
|
+
expect(allTags[0].tagName).toEqual('BODY')
|
|
187
|
+
expect(allTags[1].tagName).toEqual('DIV')
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('should fallback when provided', () => {
|
|
191
|
+
render(
|
|
192
|
+
<PostHogProvider client={posthog}>
|
|
193
|
+
<PostHogFeature flag={'test_false'} match={true} fallback={<div data-testid="nope">Nope</div>}>
|
|
194
|
+
<div data-testid="helloDiv">Hello</div>
|
|
195
|
+
</PostHogFeature>
|
|
196
|
+
</PostHogProvider>
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
expect(screen.queryByTestId('helloDiv')).not.toBeInTheDocument()
|
|
200
|
+
expect(posthog.capture).not.toHaveBeenCalled()
|
|
201
|
+
|
|
202
|
+
fireEvent.click(screen.getByTestId('nope'))
|
|
203
|
+
expect(posthog.capture).not.toHaveBeenCalled()
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('should handle showing multivariate flags with bool match', () => {
|
|
207
|
+
renderWith(posthog, 'multivariate_feature')
|
|
208
|
+
|
|
209
|
+
expect(screen.queryByTestId('helloDiv')).not.toBeInTheDocument()
|
|
210
|
+
expect(posthog.capture).not.toHaveBeenCalled()
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('should handle showing multivariate flags with incorrect match', () => {
|
|
214
|
+
renderWith(posthog, 'multivariate_feature', 'string-valueCXCC')
|
|
215
|
+
|
|
216
|
+
expect(screen.queryByTestId('helloDiv')).not.toBeInTheDocument()
|
|
217
|
+
expect(posthog.capture).not.toHaveBeenCalled()
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('should handle showing multivariate flags', () => {
|
|
221
|
+
renderWith(posthog, 'multivariate_feature', 'string-value')
|
|
222
|
+
|
|
223
|
+
expect(screen.queryByTestId('helloDiv')).toBeInTheDocument()
|
|
224
|
+
expect(posthog.capture).not.toHaveBeenCalled()
|
|
225
|
+
|
|
226
|
+
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
227
|
+
expect(posthog.capture).toHaveBeenCalledWith('$feature_interaction', {
|
|
228
|
+
feature_flag: 'multivariate_feature',
|
|
229
|
+
feature_flag_variant: 'string-value',
|
|
230
|
+
$set: { '$feature_interaction/multivariate_feature': 'string-value' },
|
|
231
|
+
})
|
|
232
|
+
expect(posthog.capture).toHaveBeenCalledTimes(1)
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('should handle payload flags', () => {
|
|
236
|
+
render(
|
|
237
|
+
<PostHogProvider client={posthog}>
|
|
238
|
+
<PostHogFeature flag={'example_feature_payload'} match={'test'}>
|
|
239
|
+
{(payload: any) => {
|
|
240
|
+
return <div data-testid={`hi_${payload.name}`}>Hullo</div>
|
|
241
|
+
}}
|
|
242
|
+
</PostHogFeature>
|
|
243
|
+
</PostHogProvider>
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
expect(screen.queryByTestId('hi_example_feature_1_payload')).toBeInTheDocument()
|
|
247
|
+
expect(posthog.capture).not.toHaveBeenCalled()
|
|
248
|
+
|
|
249
|
+
fireEvent.click(screen.getByTestId('hi_example_feature_1_payload'))
|
|
250
|
+
expect(posthog.capture).toHaveBeenCalledTimes(1)
|
|
251
|
+
})
|
|
252
|
+
})
|
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
|
+
}
|