@hanzo/insights-react 1.8.1
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/LICENSE +21 -0
- package/README.md +198 -0
- package/dist/esm/index.js +426 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/surveys/index.js +98 -0
- package/dist/esm/surveys/index.js.map +1 -0
- package/dist/types/index.d.ts +96 -0
- package/dist/types/surveys/index.d.ts +19 -0
- package/dist/umd/index.js +449 -0
- package/dist/umd/index.js.map +1 -0
- package/dist/umd/surveys/index.js +107 -0
- package/dist/umd/surveys/index.js.map +1 -0
- package/package.json +64 -0
- package/src/components/PostHogCaptureOnViewed.tsx +126 -0
- package/src/components/PostHogErrorBoundary.tsx +89 -0
- package/src/components/PostHogFeature.tsx +91 -0
- package/src/components/__tests__/PostHogCaptureOnViewed.test.tsx +110 -0
- package/src/components/__tests__/PostHogErrorBoundary.test.tsx +110 -0
- package/src/components/__tests__/PostHogFeature.test.tsx +291 -0
- package/src/components/index.ts +7 -0
- package/src/components/internal/VisibilityAndClickTracker.tsx +49 -0
- package/src/components/internal/VisibilityAndClickTrackers.tsx +60 -0
- package/src/context/PostHogContext.ts +9 -0
- package/src/context/PostHogProvider.tsx +136 -0
- package/src/context/__tests__/PostHogContext.test.tsx +35 -0
- package/src/context/__tests__/PostHogProvider.test.tsx +131 -0
- package/src/context/index.ts +2 -0
- package/src/helpers/error-helpers.ts +15 -0
- package/src/helpers/index.ts +1 -0
- package/src/hooks/__tests__/featureFlags.test.tsx +273 -0
- package/src/hooks/__tests__/usePostHog.test.tsx +19 -0
- package/src/hooks/__tests__/useThumbSurvey.test.tsx +105 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/useActiveFeatureFlags.ts +21 -0
- package/src/hooks/useFeatureFlagEnabled.ts +24 -0
- package/src/hooks/useFeatureFlagPayload.ts +22 -0
- package/src/hooks/useFeatureFlagResult.ts +31 -0
- package/src/hooks/useFeatureFlagVariantKey.ts +22 -0
- package/src/hooks/usePostHog.ts +7 -0
- package/src/hooks/useThumbSurvey.ts +146 -0
- package/src/index.ts +4 -0
- package/src/surveys/index.ts +1 -0
- package/src/utils/__tests__/object-utils.test.ts +42 -0
- package/src/utils/object-utils.ts +36 -0
- package/src/utils/type-utils.ts +16 -0
- package/surveys/package.json +7 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { render, act } from '@testing-library/react'
|
|
3
|
+
import { PostHogProvider, PostHog } from '..'
|
|
4
|
+
import posthogJs from '@hanzo/insights'
|
|
5
|
+
|
|
6
|
+
// Mock posthog-js
|
|
7
|
+
jest.mock('@hanzo/insights', () => ({
|
|
8
|
+
__esModule: true,
|
|
9
|
+
default: {
|
|
10
|
+
init: jest.fn(),
|
|
11
|
+
set_config: jest.fn(),
|
|
12
|
+
__loaded: false,
|
|
13
|
+
},
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
describe('PostHogProvider component', () => {
|
|
17
|
+
it('should render children components', () => {
|
|
18
|
+
const posthog = {} as unknown as PostHog
|
|
19
|
+
const { getByText } = render(
|
|
20
|
+
<PostHogProvider client={posthog}>
|
|
21
|
+
<div>Test</div>
|
|
22
|
+
</PostHogProvider>
|
|
23
|
+
)
|
|
24
|
+
expect(getByText('Test')).toBeTruthy()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('when using apiKey initialization', () => {
|
|
28
|
+
const apiKey = 'test-api-key'
|
|
29
|
+
const initialOptions = { api_host: 'https://app.posthog.com' }
|
|
30
|
+
const updatedOptions = { api_host: 'https://eu.posthog.com' }
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
jest.clearAllMocks()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should call set_config when options change', () => {
|
|
37
|
+
const { rerender } = render(
|
|
38
|
+
<PostHogProvider apiKey={apiKey} options={initialOptions}>
|
|
39
|
+
<div>Test</div>
|
|
40
|
+
</PostHogProvider>
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
// First render should initialize
|
|
44
|
+
expect(posthogJs.init).toHaveBeenCalledWith(apiKey, initialOptions)
|
|
45
|
+
|
|
46
|
+
// Rerender with new options
|
|
47
|
+
act(() => {
|
|
48
|
+
rerender(
|
|
49
|
+
<PostHogProvider apiKey={apiKey} options={updatedOptions}>
|
|
50
|
+
<div>Test</div>
|
|
51
|
+
</PostHogProvider>
|
|
52
|
+
)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// Should call set_config with new options
|
|
56
|
+
expect(posthogJs.set_config).toHaveBeenCalledWith(updatedOptions)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should NOT call set_config when we pass new options that are the same as the previous options', () => {
|
|
60
|
+
const { rerender } = render(
|
|
61
|
+
<PostHogProvider apiKey={apiKey} options={initialOptions}>
|
|
62
|
+
<div>Test</div>
|
|
63
|
+
</PostHogProvider>
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
// First render should initialize
|
|
67
|
+
expect(posthogJs.init).toHaveBeenCalledWith(apiKey, initialOptions)
|
|
68
|
+
|
|
69
|
+
// Rerender with new options
|
|
70
|
+
const sameOptionsButDifferentReference = { ...initialOptions }
|
|
71
|
+
act(() => {
|
|
72
|
+
rerender(
|
|
73
|
+
<PostHogProvider apiKey={apiKey} options={sameOptionsButDifferentReference}>
|
|
74
|
+
<div>Test</div>
|
|
75
|
+
</PostHogProvider>
|
|
76
|
+
)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// Should NOT call set_config
|
|
80
|
+
expect(posthogJs.set_config).not.toHaveBeenCalled()
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('should warn when attempting to change apiKey', () => {
|
|
84
|
+
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation()
|
|
85
|
+
const newApiKey = 'different-api-key'
|
|
86
|
+
|
|
87
|
+
const { rerender } = render(
|
|
88
|
+
<PostHogProvider apiKey={apiKey} options={initialOptions}>
|
|
89
|
+
<div>Test</div>
|
|
90
|
+
</PostHogProvider>
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
// First render should initialize
|
|
94
|
+
expect(posthogJs.init).toHaveBeenCalledWith(apiKey, initialOptions)
|
|
95
|
+
|
|
96
|
+
// Rerender with new apiKey
|
|
97
|
+
act(() => {
|
|
98
|
+
rerender(
|
|
99
|
+
<PostHogProvider apiKey={newApiKey} options={initialOptions}>
|
|
100
|
+
<div>Test</div>
|
|
101
|
+
</PostHogProvider>
|
|
102
|
+
)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
// Should warn about apiKey change
|
|
106
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
107
|
+
expect.stringContaining('You have provided a different `apiKey` to `PostHogProvider`')
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
consoleSpy.mockRestore()
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('warns if posthogJs has been loaded elsewhere', () => {
|
|
114
|
+
;(posthogJs as any).__loaded = true
|
|
115
|
+
|
|
116
|
+
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation()
|
|
117
|
+
render(
|
|
118
|
+
<PostHogProvider apiKey={apiKey} options={initialOptions}>
|
|
119
|
+
<div>Test</div>
|
|
120
|
+
</PostHogProvider>
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
124
|
+
expect.stringContaining('`posthog` was already loaded elsewhere. This may cause issues.')
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
consoleSpy.mockRestore()
|
|
128
|
+
;(posthogJs as any).__loaded = false
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ErrorInfo } from 'react'
|
|
2
|
+
import { PostHog } from '../context'
|
|
3
|
+
import { CaptureResult } from '@hanzo/insights'
|
|
4
|
+
|
|
5
|
+
export const setupReactErrorHandler = (
|
|
6
|
+
client: PostHog,
|
|
7
|
+
callback?: (event: CaptureResult | undefined, error: any, errorInfo: ErrorInfo) => void
|
|
8
|
+
) => {
|
|
9
|
+
return (error: any, errorInfo: ErrorInfo): void => {
|
|
10
|
+
const event = client.captureException(error)
|
|
11
|
+
if (callback) {
|
|
12
|
+
callback(event, error, errorInfo)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './error-helpers'
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { renderHook, act } from '@testing-library/react'
|
|
3
|
+
import { PostHogProvider, PostHog } from '../../context'
|
|
4
|
+
import { isUndefined } from '../../utils/type-utils'
|
|
5
|
+
import {
|
|
6
|
+
useFeatureFlagPayload,
|
|
7
|
+
useFeatureFlagVariantKey,
|
|
8
|
+
useFeatureFlagEnabled,
|
|
9
|
+
useFeatureFlagResult,
|
|
10
|
+
useActiveFeatureFlags,
|
|
11
|
+
} from '../index'
|
|
12
|
+
|
|
13
|
+
jest.useFakeTimers()
|
|
14
|
+
|
|
15
|
+
const ACTIVE_FEATURE_FLAGS = ['example_feature_true', 'multivariate_feature', 'example_feature_payload']
|
|
16
|
+
|
|
17
|
+
const FEATURE_FLAG_STATUS: Record<string, string | boolean> = {
|
|
18
|
+
example_feature_true: true,
|
|
19
|
+
example_feature_false: false,
|
|
20
|
+
multivariate_feature: 'string-value',
|
|
21
|
+
example_feature_payload: 'test',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const FEATURE_FLAG_PAYLOADS: Record<string, any> = {
|
|
25
|
+
example_feature_payload: {
|
|
26
|
+
id: 1,
|
|
27
|
+
name: 'example_feature_1_payload',
|
|
28
|
+
key: 'example_feature_1_payload',
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('feature flag hooks', () => {
|
|
33
|
+
let posthog: PostHog
|
|
34
|
+
let renderProvider: React.FC<{ children: React.ReactNode }>
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
posthog = {
|
|
38
|
+
isFeatureEnabled: (flag: string) => !!FEATURE_FLAG_STATUS[flag],
|
|
39
|
+
getFeatureFlag: (flag: string) => FEATURE_FLAG_STATUS[flag],
|
|
40
|
+
getFeatureFlagPayload: (flag: string) => FEATURE_FLAG_PAYLOADS[flag],
|
|
41
|
+
getFeatureFlagResult: (flag: string) => {
|
|
42
|
+
const value = FEATURE_FLAG_STATUS[flag]
|
|
43
|
+
if (isUndefined(value)) {
|
|
44
|
+
return undefined
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
key: flag,
|
|
48
|
+
enabled: !!value,
|
|
49
|
+
variant: typeof value === 'string' ? value : undefined,
|
|
50
|
+
payload: FEATURE_FLAG_PAYLOADS[flag],
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
onFeatureFlags: (callback: any) => {
|
|
54
|
+
const activeFlags: string[] = []
|
|
55
|
+
for (const flag in FEATURE_FLAG_STATUS) {
|
|
56
|
+
if (FEATURE_FLAG_STATUS[flag]) {
|
|
57
|
+
activeFlags.push(flag)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
callback(activeFlags)
|
|
61
|
+
return () => {}
|
|
62
|
+
},
|
|
63
|
+
featureFlags: {
|
|
64
|
+
getFlags: () => ACTIVE_FEATURE_FLAGS,
|
|
65
|
+
hasLoadedFlags: true,
|
|
66
|
+
} as unknown as PostHog['featureFlags'],
|
|
67
|
+
} as unknown as PostHog
|
|
68
|
+
|
|
69
|
+
// eslint-disable-next-line react/display-name
|
|
70
|
+
renderProvider = ({ children }) => <PostHogProvider client={posthog}>{children}</PostHogProvider>
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it.each([
|
|
74
|
+
['example_feature_true', true],
|
|
75
|
+
['example_feature_false', false],
|
|
76
|
+
['missing', false],
|
|
77
|
+
['multivariate_feature', true],
|
|
78
|
+
['example_feature_payload', true],
|
|
79
|
+
])('should get the boolean feature flag', (flag, expected) => {
|
|
80
|
+
const { result } = renderHook(() => useFeatureFlagEnabled(flag), {
|
|
81
|
+
wrapper: renderProvider,
|
|
82
|
+
})
|
|
83
|
+
expect(result.current).toEqual(expected)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it.each([
|
|
87
|
+
['example_feature_true', undefined],
|
|
88
|
+
['example_feature_false', undefined],
|
|
89
|
+
['missing', undefined],
|
|
90
|
+
['multivariate_feature', undefined],
|
|
91
|
+
['example_feature_payload', FEATURE_FLAG_PAYLOADS.example_feature_payload],
|
|
92
|
+
])('should get the payload feature flag', (flag, expected) => {
|
|
93
|
+
const { result } = renderHook(() => useFeatureFlagPayload(flag), {
|
|
94
|
+
wrapper: renderProvider,
|
|
95
|
+
})
|
|
96
|
+
expect(result.current).toEqual(expected)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('should return the active feature flags', () => {
|
|
100
|
+
const { result } = renderHook(() => useActiveFeatureFlags(), {
|
|
101
|
+
wrapper: renderProvider,
|
|
102
|
+
})
|
|
103
|
+
expect(result.current).toEqual(['example_feature_true', 'multivariate_feature', 'example_feature_payload'])
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it.each([
|
|
107
|
+
['example_feature_true', true],
|
|
108
|
+
['example_feature_false', false],
|
|
109
|
+
['missing', undefined],
|
|
110
|
+
['multivariate_feature', 'string-value'],
|
|
111
|
+
])('should get the feature flag variant key', (flag, expected) => {
|
|
112
|
+
const { result } = renderHook(() => useFeatureFlagVariantKey(flag), {
|
|
113
|
+
wrapper: renderProvider,
|
|
114
|
+
})
|
|
115
|
+
expect(result.current).toEqual(expected)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
describe('useFeatureFlagResult', () => {
|
|
119
|
+
describe('bootstrap fallback', () => {
|
|
120
|
+
function renderWithBootstrap(
|
|
121
|
+
bootstrapFlags: Record<string, string | boolean>,
|
|
122
|
+
bootstrapPayloads?: Record<string, any>
|
|
123
|
+
) {
|
|
124
|
+
const client = {
|
|
125
|
+
getFeatureFlagResult: () => undefined,
|
|
126
|
+
onFeatureFlags: () => () => {},
|
|
127
|
+
config: {
|
|
128
|
+
bootstrap: {
|
|
129
|
+
featureFlags: bootstrapFlags,
|
|
130
|
+
featureFlagPayloads: bootstrapPayloads,
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
featureFlags: {
|
|
134
|
+
hasLoadedFlags: false,
|
|
135
|
+
} as unknown as PostHog['featureFlags'],
|
|
136
|
+
} as unknown as PostHog
|
|
137
|
+
|
|
138
|
+
// eslint-disable-next-line react/display-name
|
|
139
|
+
const wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
|
140
|
+
<PostHogProvider client={client}>{children}</PostHogProvider>
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
return wrapper
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
it('returns result for a boolean bootstrap flag', () => {
|
|
147
|
+
const wrapper = renderWithBootstrap({ my_flag: true })
|
|
148
|
+
const { result } = renderHook(() => useFeatureFlagResult('my_flag'), { wrapper })
|
|
149
|
+
expect(result.current).toEqual({
|
|
150
|
+
key: 'my_flag',
|
|
151
|
+
enabled: true,
|
|
152
|
+
variant: undefined,
|
|
153
|
+
payload: undefined,
|
|
154
|
+
})
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('returns result for a multivariate bootstrap flag', () => {
|
|
158
|
+
const wrapper = renderWithBootstrap({ my_flag: 'variant-a' })
|
|
159
|
+
const { result } = renderHook(() => useFeatureFlagResult('my_flag'), { wrapper })
|
|
160
|
+
expect(result.current).toEqual({
|
|
161
|
+
key: 'my_flag',
|
|
162
|
+
enabled: true,
|
|
163
|
+
variant: 'variant-a',
|
|
164
|
+
payload: undefined,
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('returns result for a disabled bootstrap flag', () => {
|
|
169
|
+
const wrapper = renderWithBootstrap({ my_flag: false })
|
|
170
|
+
const { result } = renderHook(() => useFeatureFlagResult('my_flag'), { wrapper })
|
|
171
|
+
expect(result.current).toEqual({
|
|
172
|
+
key: 'my_flag',
|
|
173
|
+
enabled: false,
|
|
174
|
+
variant: undefined,
|
|
175
|
+
payload: undefined,
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('includes payload from bootstrap data', () => {
|
|
180
|
+
const payload = { color: 'blue' }
|
|
181
|
+
const wrapper = renderWithBootstrap({ my_flag: true }, { my_flag: payload })
|
|
182
|
+
const { result } = renderHook(() => useFeatureFlagResult('my_flag'), { wrapper })
|
|
183
|
+
expect(result.current).toEqual({
|
|
184
|
+
key: 'my_flag',
|
|
185
|
+
enabled: true,
|
|
186
|
+
variant: undefined,
|
|
187
|
+
payload,
|
|
188
|
+
})
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('returns undefined for a missing flag', () => {
|
|
192
|
+
const wrapper = renderWithBootstrap({ other_flag: true })
|
|
193
|
+
const { result } = renderHook(() => useFeatureFlagResult('my_flag'), { wrapper })
|
|
194
|
+
expect(result.current).toBeUndefined()
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
describe('flag updates', () => {
|
|
199
|
+
it('re-renders when onFeatureFlags fires', () => {
|
|
200
|
+
let capturedCallback: (() => void) | undefined
|
|
201
|
+
const client = {
|
|
202
|
+
getFeatureFlagResult: jest.fn().mockReturnValue({
|
|
203
|
+
key: 'flag',
|
|
204
|
+
enabled: true,
|
|
205
|
+
variant: undefined,
|
|
206
|
+
payload: undefined,
|
|
207
|
+
}),
|
|
208
|
+
onFeatureFlags: (cb: () => void) => {
|
|
209
|
+
capturedCallback = cb
|
|
210
|
+
return () => {}
|
|
211
|
+
},
|
|
212
|
+
featureFlags: {
|
|
213
|
+
hasLoadedFlags: true,
|
|
214
|
+
} as unknown as PostHog['featureFlags'],
|
|
215
|
+
} as unknown as PostHog
|
|
216
|
+
|
|
217
|
+
// eslint-disable-next-line react/display-name
|
|
218
|
+
const wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
|
219
|
+
<PostHogProvider client={client}>{children}</PostHogProvider>
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
const { result } = renderHook(() => useFeatureFlagResult('flag'), { wrapper })
|
|
223
|
+
expect(result.current).toEqual({
|
|
224
|
+
key: 'flag',
|
|
225
|
+
enabled: true,
|
|
226
|
+
variant: undefined,
|
|
227
|
+
payload: undefined,
|
|
228
|
+
})
|
|
229
|
+
;(client.getFeatureFlagResult as jest.Mock).mockReturnValue({
|
|
230
|
+
key: 'flag',
|
|
231
|
+
enabled: true,
|
|
232
|
+
variant: 'new-variant',
|
|
233
|
+
payload: undefined,
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
act(() => {
|
|
237
|
+
capturedCallback!()
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
expect(result.current).toEqual({
|
|
241
|
+
key: 'flag',
|
|
242
|
+
enabled: true,
|
|
243
|
+
variant: 'new-variant',
|
|
244
|
+
payload: undefined,
|
|
245
|
+
})
|
|
246
|
+
})
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
describe('cleanup', () => {
|
|
250
|
+
it('unsubscribes from onFeatureFlags on unmount', () => {
|
|
251
|
+
const unsubscribe = jest.fn()
|
|
252
|
+
const client = {
|
|
253
|
+
getFeatureFlagResult: () => undefined,
|
|
254
|
+
onFeatureFlags: () => unsubscribe,
|
|
255
|
+
featureFlags: {
|
|
256
|
+
hasLoadedFlags: true,
|
|
257
|
+
} as unknown as PostHog['featureFlags'],
|
|
258
|
+
} as unknown as PostHog
|
|
259
|
+
|
|
260
|
+
// eslint-disable-next-line react/display-name
|
|
261
|
+
const wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
|
262
|
+
<PostHogProvider client={client}>{children}</PostHogProvider>
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
const { unmount } = renderHook(() => useFeatureFlagResult('flag'), { wrapper })
|
|
266
|
+
expect(unsubscribe).not.toHaveBeenCalled()
|
|
267
|
+
|
|
268
|
+
unmount()
|
|
269
|
+
expect(unsubscribe).toHaveBeenCalledTimes(1)
|
|
270
|
+
})
|
|
271
|
+
})
|
|
272
|
+
})
|
|
273
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { renderHook } from '@testing-library/react'
|
|
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
|
+
})
|
|
@@ -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 '@hanzo/insights'
|
|
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
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useContext, useEffect, useState } from 'react'
|
|
2
|
+
import { PostHogContext } from '../context'
|
|
3
|
+
|
|
4
|
+
export function useActiveFeatureFlags(): string[] {
|
|
5
|
+
const { client, bootstrap } = useContext(PostHogContext)
|
|
6
|
+
|
|
7
|
+
const [featureFlags, setFeatureFlags] = useState<string[]>(() => client.featureFlags.getFlags())
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
return client.onFeatureFlags((flags) => {
|
|
11
|
+
setFeatureFlags(flags)
|
|
12
|
+
})
|
|
13
|
+
}, [client])
|
|
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
|
+
|
|
20
|
+
return featureFlags
|
|
21
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useContext, useEffect, useState } from 'react'
|
|
2
|
+
import { PostHogContext } from '../context'
|
|
3
|
+
import { isUndefined } from '../utils/type-utils'
|
|
4
|
+
|
|
5
|
+
export function useFeatureFlagEnabled(flag: string): boolean | undefined {
|
|
6
|
+
const { client, bootstrap } = useContext(PostHogContext)
|
|
7
|
+
|
|
8
|
+
const [featureEnabled, setFeatureEnabled] = useState<boolean | undefined>(() => client.isFeatureEnabled(flag))
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
return client.onFeatureFlags(() => {
|
|
12
|
+
setFeatureEnabled(client.isFeatureEnabled(flag))
|
|
13
|
+
})
|
|
14
|
+
}, [client, flag])
|
|
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
|
+
|
|
23
|
+
return featureEnabled
|
|
24
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { JsonType } from '@hanzo/insights'
|
|
2
|
+
import { useContext, useEffect, useState } from 'react'
|
|
3
|
+
import { PostHogContext } from '../context'
|
|
4
|
+
|
|
5
|
+
export function useFeatureFlagPayload(flag: string): JsonType {
|
|
6
|
+
const { client, bootstrap } = useContext(PostHogContext)
|
|
7
|
+
|
|
8
|
+
const [featureFlagPayload, setFeatureFlagPayload] = useState<JsonType>(() => client.getFeatureFlagPayload(flag))
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
return client.onFeatureFlags(() => {
|
|
12
|
+
setFeatureFlagPayload(client.getFeatureFlagPayload(flag))
|
|
13
|
+
})
|
|
14
|
+
}, [client, flag])
|
|
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
|
+
|
|
21
|
+
return featureFlagPayload
|
|
22
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { FeatureFlagResult } from '@hanzo/insights'
|
|
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
|
+
}
|