@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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +198 -0
  3. package/dist/esm/index.js +426 -0
  4. package/dist/esm/index.js.map +1 -0
  5. package/dist/esm/surveys/index.js +98 -0
  6. package/dist/esm/surveys/index.js.map +1 -0
  7. package/dist/types/index.d.ts +96 -0
  8. package/dist/types/surveys/index.d.ts +19 -0
  9. package/dist/umd/index.js +449 -0
  10. package/dist/umd/index.js.map +1 -0
  11. package/dist/umd/surveys/index.js +107 -0
  12. package/dist/umd/surveys/index.js.map +1 -0
  13. package/package.json +64 -0
  14. package/src/components/PostHogCaptureOnViewed.tsx +126 -0
  15. package/src/components/PostHogErrorBoundary.tsx +89 -0
  16. package/src/components/PostHogFeature.tsx +91 -0
  17. package/src/components/__tests__/PostHogCaptureOnViewed.test.tsx +110 -0
  18. package/src/components/__tests__/PostHogErrorBoundary.test.tsx +110 -0
  19. package/src/components/__tests__/PostHogFeature.test.tsx +291 -0
  20. package/src/components/index.ts +7 -0
  21. package/src/components/internal/VisibilityAndClickTracker.tsx +49 -0
  22. package/src/components/internal/VisibilityAndClickTrackers.tsx +60 -0
  23. package/src/context/PostHogContext.ts +9 -0
  24. package/src/context/PostHogProvider.tsx +136 -0
  25. package/src/context/__tests__/PostHogContext.test.tsx +35 -0
  26. package/src/context/__tests__/PostHogProvider.test.tsx +131 -0
  27. package/src/context/index.ts +2 -0
  28. package/src/helpers/error-helpers.ts +15 -0
  29. package/src/helpers/index.ts +1 -0
  30. package/src/hooks/__tests__/featureFlags.test.tsx +273 -0
  31. package/src/hooks/__tests__/usePostHog.test.tsx +19 -0
  32. package/src/hooks/__tests__/useThumbSurvey.test.tsx +105 -0
  33. package/src/hooks/index.ts +6 -0
  34. package/src/hooks/useActiveFeatureFlags.ts +21 -0
  35. package/src/hooks/useFeatureFlagEnabled.ts +24 -0
  36. package/src/hooks/useFeatureFlagPayload.ts +22 -0
  37. package/src/hooks/useFeatureFlagResult.ts +31 -0
  38. package/src/hooks/useFeatureFlagVariantKey.ts +22 -0
  39. package/src/hooks/usePostHog.ts +7 -0
  40. package/src/hooks/useThumbSurvey.ts +146 -0
  41. package/src/index.ts +4 -0
  42. package/src/surveys/index.ts +1 -0
  43. package/src/utils/__tests__/object-utils.test.ts +42 -0
  44. package/src/utils/object-utils.ts +36 -0
  45. package/src/utils/type-utils.ts +16 -0
  46. 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,2 @@
1
+ export * from './PostHogContext'
2
+ export * from './PostHogProvider'
@@ -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,6 @@
1
+ export * from './useFeatureFlagEnabled'
2
+ export * from './useFeatureFlagPayload'
3
+ export * from './useFeatureFlagResult'
4
+ export * from './useActiveFeatureFlags'
5
+ export * from './useFeatureFlagVariantKey'
6
+ export * from './usePostHog'
@@ -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
+ }