@posthog/react 1.7.0 → 1.8.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,105 @@
1
+ import * as React from 'react'
2
+ import { renderHook, act } from '@testing-library/react'
3
+ import { PostHogProvider, PostHog } from '../../context'
4
+ import { useThumbSurvey } from '../useThumbSurvey'
5
+ import { SurveyEventName, SurveyEventProperties } from 'posthog-js'
6
+ import { isUndefined } from '../../utils/type-utils'
7
+
8
+ jest.useFakeTimers()
9
+
10
+ describe('useThumbSurvey hook', () => {
11
+ let posthog: PostHog
12
+ let captureMock: jest.Mock
13
+ let displaySurveyMock: jest.Mock
14
+ let wrapper: React.FC<{ children: React.ReactNode }>
15
+
16
+ beforeEach(() => {
17
+ captureMock = jest.fn()
18
+ displaySurveyMock = jest.fn()
19
+
20
+ posthog = {
21
+ capture: captureMock,
22
+ get_session_replay_url: () => 'https://app.posthog.com/replay/123',
23
+ surveys: { displaySurvey: displaySurveyMock },
24
+ } as unknown as PostHog
25
+
26
+ wrapper = ({ children }) => <PostHogProvider client={posthog}>{children}</PostHogProvider>
27
+ })
28
+
29
+ describe('survey shown tracking', () => {
30
+ it.each([
31
+ [false, true, false], // disableAutoShownTracking, shouldAutoTrack, shouldExposeTrackShown
32
+ [true, false, true],
33
+ ])(
34
+ 'disableAutoShownTracking=%s: auto-tracks=%s, exposes trackShown=%s',
35
+ (disableAutoShownTracking, shouldAutoTrack, shouldExposeTrackShown) => {
36
+ const { result } = renderHook(
37
+ () => useThumbSurvey({ surveyId: 'test-survey', disableAutoShownTracking }),
38
+ { wrapper }
39
+ )
40
+
41
+ expect(captureMock).toHaveBeenCalledTimes(shouldAutoTrack ? 1 : 0)
42
+ expect(!isUndefined(result.current.trackShown)).toBe(shouldExposeTrackShown)
43
+ }
44
+ )
45
+
46
+ it('should only emit survey shown once when trackShown is called multiple times', () => {
47
+ const { result } = renderHook(
48
+ () => useThumbSurvey({ surveyId: 'test-survey', disableAutoShownTracking: true }),
49
+ { wrapper }
50
+ )
51
+
52
+ act(() => {
53
+ result.current.trackShown?.()
54
+ result.current.trackShown?.()
55
+ })
56
+
57
+ expect(captureMock).toHaveBeenCalledTimes(1)
58
+ expect(captureMock).toHaveBeenCalledWith(SurveyEventName.SHOWN, {
59
+ [SurveyEventProperties.SURVEY_ID]: 'test-survey',
60
+ sessionRecordingUrl: 'https://app.posthog.com/replay/123',
61
+ })
62
+ })
63
+ })
64
+
65
+ describe('respond', () => {
66
+ it.each([
67
+ ['up', 1],
68
+ ['down', 2],
69
+ ] as const)('respond("%s") calls displaySurvey with initialResponses: { 0: %d }', (value, expectedResponse) => {
70
+ const { result } = renderHook(() => useThumbSurvey({ surveyId: 'test-survey' }), { wrapper })
71
+
72
+ act(() => {
73
+ result.current.respond(value)
74
+ })
75
+
76
+ expect(displaySurveyMock).toHaveBeenCalledWith(
77
+ 'test-survey',
78
+ expect.objectContaining({ initialResponses: { 0: expectedResponse } })
79
+ )
80
+ })
81
+
82
+ it('should only allow one response', () => {
83
+ const { result } = renderHook(() => useThumbSurvey({ surveyId: 'test-survey' }), { wrapper })
84
+
85
+ act(() => {
86
+ result.current.respond('up')
87
+ result.current.respond('down')
88
+ })
89
+
90
+ expect(displaySurveyMock).toHaveBeenCalledTimes(1)
91
+ expect(result.current.response).toBe('up')
92
+ })
93
+
94
+ it('should call onResponse callback', () => {
95
+ const onResponse = jest.fn()
96
+ const { result } = renderHook(() => useThumbSurvey({ surveyId: 'test-survey', onResponse }), { wrapper })
97
+
98
+ act(() => {
99
+ result.current.respond('down')
100
+ })
101
+
102
+ expect(onResponse).toHaveBeenCalledWith('down')
103
+ })
104
+ })
105
+ })
@@ -1,5 +1,6 @@
1
1
  export * from './useFeatureFlagEnabled'
2
2
  export * from './useFeatureFlagPayload'
3
+ export * from './useFeatureFlagResult'
3
4
  export * from './useActiveFeatureFlags'
4
5
  export * from './useFeatureFlagVariantKey'
5
6
  export * from './usePostHog'
@@ -0,0 +1,31 @@
1
+ import { FeatureFlagResult } from 'posthog-js'
2
+ import { useContext, useEffect, useState } from 'react'
3
+ import { PostHogContext } from '../context'
4
+ import { isUndefined } from '../utils/type-utils'
5
+
6
+ export function useFeatureFlagResult(flag: string): FeatureFlagResult | undefined {
7
+ const { client, bootstrap } = useContext(PostHogContext)
8
+
9
+ const [result, setResult] = useState<FeatureFlagResult | undefined>(() => client.getFeatureFlagResult(flag))
10
+
11
+ useEffect(() => {
12
+ return client.onFeatureFlags(() => {
13
+ setResult(client.getFeatureFlagResult(flag))
14
+ })
15
+ }, [client, flag])
16
+
17
+ if (!client?.featureFlags?.hasLoadedFlags && bootstrap?.featureFlags) {
18
+ const bootstrappedValue = bootstrap.featureFlags[flag]
19
+ if (isUndefined(bootstrappedValue)) {
20
+ return undefined
21
+ }
22
+ return {
23
+ key: flag,
24
+ enabled: typeof bootstrappedValue === 'string' ? true : !!bootstrappedValue,
25
+ variant: typeof bootstrappedValue === 'string' ? bootstrappedValue : undefined,
26
+ payload: bootstrap.featureFlagPayloads?.[flag],
27
+ }
28
+ }
29
+
30
+ return result
31
+ }
@@ -1,6 +1,6 @@
1
- import { useState, useCallback, useRef, useMemo, type RefCallback } from 'react'
1
+ import { useState, useCallback, useRef, useMemo, type RefCallback, useEffect } from 'react'
2
2
  import { usePostHog } from './usePostHog'
3
- import { DisplaySurveyType, SurveyPosition } from 'posthog-js'
3
+ import { DisplaySurveyType, SurveyEventName, SurveyEventProperties, SurveyPosition } from 'posthog-js'
4
4
 
5
5
  export interface UseThumbSurveyOptions {
6
6
  /** ID of the target PostHog survey */
@@ -11,6 +11,8 @@ export interface UseThumbSurveyOptions {
11
11
  properties?: Record<string, any>
12
12
  /** Callback on thumb button click */
13
13
  onResponse?: (response: 'up' | 'down') => void
14
+ /** Disable automatically emitting `survey shown` on hook mount. Defaults to false. */
15
+ disableAutoShownTracking?: boolean
14
16
  }
15
17
 
16
18
  export interface UseThumbSurveyResult {
@@ -20,6 +22,8 @@ export interface UseThumbSurveyResult {
20
22
  response: 'up' | 'down' | null
21
23
  /** Ref to attach to the trigger element for positioning the followup survey popup */
22
24
  triggerRef: RefCallback<HTMLElement>
25
+ /** Method to manually trigger a `survey shown` event. Only available when disableAutoShownTracking is true. */
26
+ trackShown?: () => void
23
27
  }
24
28
 
25
29
  const TRIGGER_ATTR = 'data-ph-thumb-survey-trigger'
@@ -60,6 +64,8 @@ const TRIGGER_ATTR = 'data-ph-thumb-survey-trigger'
60
64
  * Notes:
61
65
  * - The thumbs up/down response will ALWAYS be recorded, whether your survey is set to collect partial responses or not.
62
66
  * - By default, followup questions will be displayed as a pop-up next to the triggerRef. Use options.position to change the position.
67
+ * - By default, `survey shown` is emitted automatically on hook mount. To prevent this behavior, set `disableAutoShownTracking: true`,
68
+ * and manually call `trackShown()` when you want to emit this event.
63
69
  *
64
70
  * @param options UseThumbSurveyOptions
65
71
  * @returns UseThumbSurveyResult
@@ -69,6 +75,7 @@ export function useThumbSurvey({
69
75
  displayPosition = SurveyPosition.NextToTrigger,
70
76
  properties,
71
77
  onResponse,
78
+ disableAutoShownTracking,
72
79
  }: UseThumbSurveyOptions): UseThumbSurveyResult {
73
80
  const posthog = usePostHog()
74
81
  const [responded, setResponded] = useState<'up' | 'down' | null>(null)
@@ -89,9 +96,29 @@ export function useThumbSurvey({
89
96
  [triggerValue]
90
97
  )
91
98
 
99
+ const shownRef = useRef(false)
100
+ const respondedRef = useRef(false)
101
+
102
+ const trackShown = useCallback(() => {
103
+ if (shownRef.current || !posthog) return
104
+ shownRef.current = true
105
+ posthog.capture(SurveyEventName.SHOWN, {
106
+ [SurveyEventProperties.SURVEY_ID]: surveyId,
107
+ sessionRecordingUrl: posthog.get_session_replay_url?.(),
108
+ ...properties,
109
+ })
110
+ }, [posthog, surveyId, properties])
111
+
112
+ useEffect(() => {
113
+ if (!disableAutoShownTracking) {
114
+ trackShown()
115
+ }
116
+ }, [trackShown, disableAutoShownTracking])
117
+
92
118
  const respond = useCallback(
93
119
  (value: 'up' | 'down') => {
94
- if (!posthog?.surveys || responded) return
120
+ if (!posthog?.surveys || respondedRef.current) return
121
+ respondedRef.current = true
95
122
 
96
123
  setResponded(value)
97
124
  onResponse?.(value)
@@ -104,10 +131,16 @@ export function useThumbSurvey({
104
131
  initialResponses: { 0: value === 'up' ? 1 : 2 },
105
132
  position: displayPosition,
106
133
  selector: `[${TRIGGER_ATTR}="${triggerValue}"]`,
134
+ skipShownEvent: true,
107
135
  })
108
136
  },
109
- [posthog, surveyId, displayPosition, properties, responded, onResponse, triggerValue]
137
+ [posthog, surveyId, displayPosition, properties, onResponse, triggerValue]
110
138
  )
111
139
 
112
- return { respond, response: responded, triggerRef }
140
+ return {
141
+ respond,
142
+ response: responded,
143
+ triggerRef,
144
+ ...(disableAutoShownTracking && { trackShown }),
145
+ }
113
146
  }