@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.
@@ -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 }>({ client: posthogJs })
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 <PostHogContext.Provider value={{ client: posthog }}>{children}</PostHogContext.Provider>
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
- given(
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
- given.render()
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(() => given.render()).not.toThrow()
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 // Pretend it's initialized
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
- given('renderProvider', () => ({ children }) => (
27
- <PostHogProvider client={given.posthog}>{children}</PostHogProvider>
28
- ))
26
+ let posthog: PostHog
27
+ let renderProvider: React.FC<{ children: React.ReactNode }>
29
28
 
30
- given('posthog', () => ({
31
- isFeatureEnabled: (flag) => !!FEATURE_FLAG_STATUS[flag],
32
- getFeatureFlag: (flag) => FEATURE_FLAG_STATUS[flag],
33
- getFeatureFlagPayload: (flag) => FEATURE_FLAG_PAYLOADS[flag],
34
- onFeatureFlags: (callback) => {
35
- const activeFlags = []
36
- for (const flag in FEATURE_FLAG_STATUS) {
37
- if (FEATURE_FLAG_STATUS[flag]) {
38
- activeFlags.push(flag)
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
- callback(activeFlags)
42
- return () => {}
43
- },
44
- featureFlags: {
45
- getFlags: () => ACTIVE_FEATURE_FLAGS,
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
- let { result } = renderHook(() => useFeatureFlagEnabled(flag), {
57
- wrapper: given.renderProvider,
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
- let { result } = renderHook(() => useFeatureFlagPayload(flag), {
70
- wrapper: given.renderProvider,
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
- let { result } = renderHook(() => useActiveFeatureFlags(), {
77
- wrapper: given.renderProvider,
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
- let { result } = renderHook(() => useFeatureFlagVariantKey(flag), {
89
- wrapper: given.renderProvider,
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 { usePostHog } from './usePostHog'
1
+ import { useContext, useEffect, useState } from 'react'
2
+ import { PostHogContext } from '../context'
3
3
 
4
4
  export function useActiveFeatureFlags(): string[] {
5
- const client = usePostHog()
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 { usePostHog } from './usePostHog'
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 = usePostHog()
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 { usePostHog } from './usePostHog'
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 = usePostHog()
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 { usePostHog } from './usePostHog'
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 = usePostHog()
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
  }