Skip to content

Commit 74abf59

Browse files
committed
chore: add capture on viewed to playground
1 parent 3a160ea commit 74abf59

File tree

6 files changed

+78
-127
lines changed

6 files changed

+78
-127
lines changed

.changeset/calm-tools-know.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@posthog/react': minor
3+
'posthog-js': minor
4+
---
5+
6+
feat: add a component that will wrap your components and capture an event when they are in view in the browser

AGENTS.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,8 @@ The recommended workflow for testing local changes uses tarballs, which most rea
132132
1. Run `pnpm turbo package` inside the root folder to generate tarballs in `./target`
133133
2. Navigate to the example/playground project: `cd examples/example-nextjs`
134134
3. Run `pnpm install` (remove `pnpm-lock.yaml` if it exists) to install local tarballs
135-
4. Run `pnpm dev` or `pnpm start` to start the project
135+
4. you might need to point package.json at the tarball like `"posthog-js": "file:../../target/posthog-js.tgz",`
136+
5. Run `pnpm dev` or `pnpm start` to start the project
136137

137138
### Development Workflow (Recommended)
138139

Lines changed: 8 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { useFeatureFlagPayload, useFeatureFlagVariantKey, usePostHog } from '../hooks'
2-
import React, { Children, ReactNode, useCallback, useEffect, useRef } from 'react'
2+
import React from 'react'
33
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'
56

67
export type PostHogFeatureProps = React.HTMLProps<HTMLDivElement> & {
78
flag: string
@@ -25,6 +26,7 @@ export function PostHogFeature({
2526
}: PostHogFeatureProps): JSX.Element | null {
2627
const payload = useFeatureFlagPayload(flag)
2728
const variant = useFeatureFlagVariantKey(flag)
29+
const posthog = usePostHog()
2830

2931
const shouldTrackInteraction = trackInteraction ?? true
3032
const shouldTrackView = trackView ?? true
@@ -37,6 +39,8 @@ export function PostHogFeature({
3739
options={visibilityObserverOptions}
3840
trackInteraction={shouldTrackInteraction}
3941
trackView={shouldTrackView}
42+
onInteract={() => captureFeatureInteraction({ flag, posthog, flagVariant: variant })}
43+
onView={() => captureFeatureView({ flag, posthog, flagVariant: variant })}
4044
{...props}
4145
>
4246
{childNode}
@@ -46,7 +50,7 @@ export function PostHogFeature({
4650
return <>{fallback}</>
4751
}
4852

49-
function captureFeatureInteraction({
53+
export function captureFeatureInteraction({
5054
flag,
5155
posthog,
5256
flagVariant,
@@ -65,7 +69,7 @@ function captureFeatureInteraction({
6569
posthog.capture('$feature_interaction', properties)
6670
}
6771

68-
function captureFeatureView({
72+
export function captureFeatureView({
6973
flag,
7074
posthog,
7175
flagVariant,
@@ -83,92 +87,3 @@ function captureFeatureView({
8387
}
8488
posthog.capture('$feature_view', properties)
8589
}
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-
}

packages/react/src/components/internal/VisibilityAndClickTracker.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { isNull } from '../../utils/type-utils'
55
* VisibilityAndClickTracker is an internal component,
66
* its API might change without warning and without being signalled as a breaking change
77
*
8+
* Wraps the provided children in a div, and tracks visibility of and clicks on that div
89
*/
910
export function VisibilityAndClickTracker({
1011
children,
Lines changed: 41 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,60 @@
1-
import React, { MouseEventHandler, useEffect, useMemo, useRef } from 'react'
2-
import { isNull } from '../../utils/type-utils'
1+
import React, { Children, ReactNode, useCallback, useRef } from 'react'
2+
import { VisibilityAndClickTracker } from './VisibilityAndClickTracker'
33

44
/**
5-
* VisibilityAndClickTracker is an internal component,
5+
* VisibilityAndClickTrackers is an internal component,
66
* its API might change without warning and without being signalled as a breaking change
77
*
8+
* Wraps each of the children passed to it for visiblity and click tracking
9+
*
810
*/
9-
export function VisibilityAndClickTracker({
11+
export function VisibilityAndClickTrackers({
1012
children,
11-
onIntersect,
12-
onClick,
13+
trackInteraction,
1314
trackView,
1415
options,
16+
onInteract,
17+
onView,
1518
...props
1619
}: {
20+
flag: string
1721
children: React.ReactNode
18-
onIntersect: (entry: IntersectionObserverEntry) => void
19-
onClick?: MouseEventHandler<HTMLDivElement>
22+
trackInteraction: boolean
2023
trackView: boolean
2124
options?: IntersectionObserverInit
25+
onInteract?: () => void
26+
onView?: () => void
2227
}): JSX.Element {
23-
const ref = useRef<HTMLDivElement>(null)
28+
const clickTrackedRef = useRef(false)
29+
const visibilityTrackedRef = useRef(false)
2430

25-
const observerOptions = useMemo(
26-
() => ({
27-
threshold: 0.1,
28-
...options,
29-
}),
30-
// eslint-disable-next-line react-hooks/exhaustive-deps
31-
[options?.threshold, options?.root, options?.rootMargin]
32-
)
31+
const cachedOnClick = useCallback(() => {
32+
if (!clickTrackedRef.current && trackInteraction && onInteract) {
33+
onInteract()
34+
clickTrackedRef.current = true
35+
}
36+
}, [trackInteraction, onInteract])
3337

34-
useEffect(() => {
35-
if (isNull(ref.current) || !trackView) return
38+
const onIntersect = (entry: IntersectionObserverEntry) => {
39+
if (!visibilityTrackedRef.current && entry.isIntersecting && onView) {
40+
onView()
41+
visibilityTrackedRef.current = true
42+
}
43+
}
3644

37-
// eslint-disable-next-line compat/compat
38-
const observer = new IntersectionObserver(([entry]) => onIntersect(entry), observerOptions)
39-
observer.observe(ref.current)
40-
return () => observer.disconnect()
41-
}, [observerOptions, trackView, onIntersect])
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+
})
4258

43-
return (
44-
<div ref={ref} {...props} onClick={onClick}>
45-
{children}
46-
</div>
47-
)
59+
return <>{trackedChildren}</>
4860
}

playground/react-nextjs/app/page.tsx

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client'
22

33
import Image from 'next/image'
4+
import { PostHogCaptureOnViewed } from '@posthog/react'
45

56
const catImages = Array.from({ length: 120 }, (_, i) => ({
67
id: i + 1,
@@ -31,7 +32,11 @@ export default function Home() {
3132
<h2 style={{ fontSize: '1.5rem', color: '#555' }}>Scroll down to see the gallery...</h2>
3233
</div>
3334

34-
<div
35+
<PostHogCaptureOnViewed
36+
name="cat-gallery"
37+
properties={{ gallery_size: catImages.length, gallery_type: 'cats' }}
38+
trackAllChildren
39+
observerOptions={{ threshold: 0.1 }}
3540
style={{
3641
display: 'grid',
3742
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
@@ -63,11 +68,22 @@ export default function Home() {
6368
/>
6469
</div>
6570
))}
66-
</div>
71+
</PostHogCaptureOnViewed>
6772

68-
<div style={{ height: '50vh', marginTop: '2rem' }}>
73+
<PostHogCaptureOnViewed
74+
name="test-element"
75+
properties={{ test: true }}
76+
observerOptions={{ threshold: 0.1 }}
77+
style={{
78+
padding: '2rem',
79+
backgroundColor: '#00b894',
80+
color: 'white',
81+
height: '50vh',
82+
marginTop: '2rem',
83+
}}
84+
>
6985
<p style={{ color: '#666' }}>End of page</p>
70-
</div>
86+
</PostHogCaptureOnViewed>
7187
</div>
7288
</main>
7389
)

0 commit comments

Comments
 (0)