@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.
@@ -1,7 +1,8 @@
1
1
  import { useFeatureFlagPayload, useFeatureFlagVariantKey, usePostHog } from '../hooks'
2
- import React, { Children, ReactNode, useCallback, useEffect, useRef } from 'react'
2
+ import React from 'react'
3
3
  import { PostHog } from '../context'
4
- import { isFunction, isNull, isUndefined } from '../utils/type-utils'
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
+ })
@@ -10,25 +10,25 @@ describe('PostHogErrorBoundary component', () => {
10
10
  mockFunction(console, 'warn')
11
11
  mockFunction(posthog, 'captureException')
12
12
 
13
- given('render_with_error', () => (props) => render(<RenderWithError {...props} />))
14
- given('render_without_error', () => (props) => render(<RenderWithoutError {...props} />))
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 } = given.render_with_error({ message: 'Test error', fallback: <div></div> })
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 } = given.render_with_error({ fallback: null })
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 } = given.render_with_error({ fallback: 'hello' })
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
- given.render_with_error({ message: 'Kaboom', additionalProperties: props })
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
- given.render_with_error({
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 } = given.render_without_error()
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
- given('render_with_error', () => (props) => render(<RenderWithError {...props} />))
66
+ const renderWithError = (props: any) => render(<RenderWithError {...props} />)
67
67
 
68
68
  it('should call capture with a stacktrace', () => {
69
- given.render_with_error({ message: 'Kaboom', fallback: <div></div>, additionalProperties: {} })
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
+ })
@@ -1,4 +1,5 @@
1
1
  export * from './PostHogFeature'
2
+ export * from './PostHogCaptureOnViewed'
2
3
  export {
3
4
  PostHogErrorBoundary,
4
5
  PostHogErrorBoundaryProps,
@@ -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
+ }