@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
|
@@ -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,6 +1,9 @@
|
|
|
1
|
-
import posthogJs from 'posthog-js'
|
|
1
|
+
import posthogJs, { BootstrapConfig } from 'posthog-js'
|
|
2
2
|
import { createContext } from 'react'
|
|
3
3
|
|
|
4
4
|
export type PostHog = typeof posthogJs
|
|
5
5
|
|
|
6
|
-
export const PostHogContext = createContext<{ client: PostHog }>({
|
|
6
|
+
export const PostHogContext = createContext<{ client: PostHog; bootstrap?: BootstrapConfig }>({
|
|
7
|
+
client: posthogJs,
|
|
8
|
+
bootstrap: undefined,
|
|
9
|
+
})
|
|
@@ -124,5 +124,11 @@ export function PostHogProvider({ children, client, apiKey, options }: WithOptio
|
|
|
124
124
|
}
|
|
125
125
|
}, [client, apiKey, JSON.stringify(options)]) // Stringify options to be a stable reference
|
|
126
126
|
|
|
127
|
-
return
|
|
127
|
+
return (
|
|
128
|
+
<PostHogContext.Provider
|
|
129
|
+
value={{ client: posthog, bootstrap: options?.bootstrap ?? client?.config?.bootstrap }}
|
|
130
|
+
>
|
|
131
|
+
{children}
|
|
132
|
+
</PostHogContext.Provider>
|
|
133
|
+
)
|
|
128
134
|
}
|
|
@@ -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,33 @@ 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
|
+
hasLoadedFlags: true,
|
|
47
|
+
} as unknown as PostHog['featureFlags'],
|
|
48
|
+
} as unknown as PostHog
|
|
49
|
+
|
|
50
|
+
// eslint-disable-next-line react/display-name
|
|
51
|
+
renderProvider = ({ children }) => <PostHogProvider client={posthog}>{children}</PostHogProvider>
|
|
52
|
+
})
|
|
48
53
|
|
|
49
54
|
it.each([
|
|
50
55
|
['example_feature_true', true],
|
|
@@ -53,8 +58,8 @@ describe('useFeatureFlagPayload hook', () => {
|
|
|
53
58
|
['multivariate_feature', true],
|
|
54
59
|
['example_feature_payload', true],
|
|
55
60
|
])('should get the boolean feature flag', (flag, expected) => {
|
|
56
|
-
|
|
57
|
-
wrapper:
|
|
61
|
+
const { result } = renderHook(() => useFeatureFlagEnabled(flag), {
|
|
62
|
+
wrapper: renderProvider,
|
|
58
63
|
})
|
|
59
64
|
expect(result.current).toEqual(expected)
|
|
60
65
|
})
|
|
@@ -66,15 +71,15 @@ describe('useFeatureFlagPayload hook', () => {
|
|
|
66
71
|
['multivariate_feature', undefined],
|
|
67
72
|
['example_feature_payload', FEATURE_FLAG_PAYLOADS.example_feature_payload],
|
|
68
73
|
])('should get the payload feature flag', (flag, expected) => {
|
|
69
|
-
|
|
70
|
-
wrapper:
|
|
74
|
+
const { result } = renderHook(() => useFeatureFlagPayload(flag), {
|
|
75
|
+
wrapper: renderProvider,
|
|
71
76
|
})
|
|
72
77
|
expect(result.current).toEqual(expected)
|
|
73
78
|
})
|
|
74
79
|
|
|
75
80
|
it('should return the active feature flags', () => {
|
|
76
|
-
|
|
77
|
-
wrapper:
|
|
81
|
+
const { result } = renderHook(() => useActiveFeatureFlags(), {
|
|
82
|
+
wrapper: renderProvider,
|
|
78
83
|
})
|
|
79
84
|
expect(result.current).toEqual(['example_feature_true', 'multivariate_feature', 'example_feature_payload'])
|
|
80
85
|
})
|
|
@@ -85,8 +90,8 @@ describe('useFeatureFlagPayload hook', () => {
|
|
|
85
90
|
['missing', undefined],
|
|
86
91
|
['multivariate_feature', 'string-value'],
|
|
87
92
|
])('should get the feature flag variant key', (flag, expected) => {
|
|
88
|
-
|
|
89
|
-
wrapper:
|
|
93
|
+
const { result } = renderHook(() => useFeatureFlagVariantKey(flag), {
|
|
94
|
+
wrapper: renderProvider,
|
|
90
95
|
})
|
|
91
96
|
expect(result.current).toEqual(expected)
|
|
92
97
|
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { renderHook } from '@testing-library/react-hooks'
|
|
3
|
+
import { PostHogProvider, PostHog } from '../../context'
|
|
4
|
+
import { usePostHog } from '..'
|
|
5
|
+
|
|
6
|
+
jest.useFakeTimers()
|
|
7
|
+
|
|
8
|
+
const posthog = { posthog_client: true } as unknown as PostHog
|
|
9
|
+
|
|
10
|
+
describe('usePostHog hook', () => {
|
|
11
|
+
it('should return the client', () => {
|
|
12
|
+
const { result } = renderHook(() => usePostHog(), {
|
|
13
|
+
wrapper: ({ children }: { children: React.ReactNode }) => (
|
|
14
|
+
<PostHogProvider client={posthog}>{children}</PostHogProvider>
|
|
15
|
+
),
|
|
16
|
+
})
|
|
17
|
+
expect(result.current).toEqual(posthog)
|
|
18
|
+
})
|
|
19
|
+
})
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react'
|
|
2
|
-
import {
|
|
1
|
+
import { useContext, useEffect, useState } from 'react'
|
|
2
|
+
import { PostHogContext } from '../context'
|
|
3
3
|
|
|
4
4
|
export function useActiveFeatureFlags(): string[] {
|
|
5
|
-
const client =
|
|
5
|
+
const { client, bootstrap } = useContext(PostHogContext)
|
|
6
6
|
|
|
7
7
|
const [featureFlags, setFeatureFlags] = useState<string[]>(() => client.featureFlags.getFlags())
|
|
8
8
|
|
|
@@ -12,5 +12,10 @@ export function useActiveFeatureFlags(): string[] {
|
|
|
12
12
|
})
|
|
13
13
|
}, [client])
|
|
14
14
|
|
|
15
|
+
// if the client is not loaded yet and we have a bootstrapped value, use it
|
|
16
|
+
if (!client.featureFlags.hasLoadedFlags && bootstrap?.featureFlags) {
|
|
17
|
+
return Object.keys(bootstrap.featureFlags)
|
|
18
|
+
}
|
|
19
|
+
|
|
15
20
|
return featureFlags
|
|
16
21
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react'
|
|
2
|
-
import {
|
|
1
|
+
import { useContext, useEffect, useState } from 'react'
|
|
2
|
+
import { PostHogContext } from '../context'
|
|
3
|
+
import { isUndefined } from '../utils/type-utils'
|
|
3
4
|
|
|
4
5
|
export function useFeatureFlagEnabled(flag: string): boolean | undefined {
|
|
5
|
-
const client =
|
|
6
|
+
const { client, bootstrap } = useContext(PostHogContext)
|
|
6
7
|
|
|
7
8
|
const [featureEnabled, setFeatureEnabled] = useState<boolean | undefined>(() => client.isFeatureEnabled(flag))
|
|
8
9
|
|
|
@@ -12,5 +13,12 @@ export function useFeatureFlagEnabled(flag: string): boolean | undefined {
|
|
|
12
13
|
})
|
|
13
14
|
}, [client, flag])
|
|
14
15
|
|
|
16
|
+
const bootstrapped = bootstrap?.featureFlags?.[flag]
|
|
17
|
+
|
|
18
|
+
// if the client is not loaded yet, check if we have a bootstrapped value and then true/false it
|
|
19
|
+
if (!client.featureFlags.hasLoadedFlags && bootstrap?.featureFlags) {
|
|
20
|
+
return isUndefined(bootstrapped) ? undefined : !!bootstrapped
|
|
21
|
+
}
|
|
22
|
+
|
|
15
23
|
return featureEnabled
|
|
16
24
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react'
|
|
2
1
|
import { JsonType } from 'posthog-js'
|
|
3
|
-
import {
|
|
2
|
+
import { useContext, useEffect, useState } from 'react'
|
|
3
|
+
import { PostHogContext } from '../context'
|
|
4
4
|
|
|
5
5
|
export function useFeatureFlagPayload(flag: string): JsonType {
|
|
6
|
-
const client =
|
|
6
|
+
const { client, bootstrap } = useContext(PostHogContext)
|
|
7
7
|
|
|
8
8
|
const [featureFlagPayload, setFeatureFlagPayload] = useState<JsonType>(() => client.getFeatureFlagPayload(flag))
|
|
9
9
|
|
|
@@ -13,5 +13,10 @@ export function useFeatureFlagPayload(flag: string): JsonType {
|
|
|
13
13
|
})
|
|
14
14
|
}, [client, flag])
|
|
15
15
|
|
|
16
|
+
// if the client is not loaded yet, use the bootstrapped value
|
|
17
|
+
if (!client.featureFlags.hasLoadedFlags && bootstrap?.featureFlagPayloads) {
|
|
18
|
+
return bootstrap.featureFlagPayloads[flag]
|
|
19
|
+
}
|
|
20
|
+
|
|
16
21
|
return featureFlagPayload
|
|
17
22
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react'
|
|
2
|
-
import {
|
|
1
|
+
import { useContext, useEffect, useState } from 'react'
|
|
2
|
+
import { PostHogContext } from '../context'
|
|
3
3
|
|
|
4
4
|
export function useFeatureFlagVariantKey(flag: string): string | boolean | undefined {
|
|
5
|
-
const client =
|
|
5
|
+
const { client, bootstrap } = useContext(PostHogContext)
|
|
6
6
|
|
|
7
7
|
const [featureFlagVariantKey, setFeatureFlagVariantKey] = useState<string | boolean | undefined>(() =>
|
|
8
8
|
client.getFeatureFlag(flag)
|
|
@@ -14,5 +14,9 @@ export function useFeatureFlagVariantKey(flag: string): string | boolean | undef
|
|
|
14
14
|
})
|
|
15
15
|
}, [client, flag])
|
|
16
16
|
|
|
17
|
+
if (!client.featureFlags.hasLoadedFlags && bootstrap?.featureFlags) {
|
|
18
|
+
return bootstrap.featureFlags[flag]
|
|
19
|
+
}
|
|
20
|
+
|
|
17
21
|
return featureFlagVariantKey
|
|
18
22
|
}
|