@hanzo/insights-react 1.8.1 → 1.9.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 +1 -1
- package/README.md +40 -40
- package/dist/esm/index.js +48 -48
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/surveys/index.js +12 -12
- package/dist/esm/surveys/index.js.map +1 -1
- package/dist/types/index.d.ts +29 -29
- package/dist/umd/index.js +55 -55
- package/dist/umd/index.js.map +1 -1
- package/dist/umd/surveys/index.js +16 -16
- package/dist/umd/surveys/index.js.map +1 -1
- package/package.json +38 -32
- package/src/components/{PostHogCaptureOnViewed.tsx → InsightsCaptureOnViewed.tsx} +12 -37
- package/src/components/{PostHogErrorBoundary.tsx → InsightsErrorBoundary.tsx} +13 -13
- package/src/components/{PostHogFeature.tsx → InsightsFeature.tsx} +14 -14
- package/src/components/__tests__/{PostHogCaptureOnViewed.test.tsx → InsightsCaptureOnViewed.test.tsx} +33 -33
- package/src/components/__tests__/{PostHogErrorBoundary.test.tsx → InsightsErrorBoundary.test.tsx} +17 -17
- package/src/components/__tests__/{PostHogFeature.test.tsx → InsightsFeature.test.tsx} +73 -73
- package/src/components/index.ts +6 -6
- package/src/context/InsightsContext.ts +9 -0
- package/src/context/InsightsProvider.tsx +108 -0
- package/src/context/__tests__/{PostHogContext.test.tsx → InsightsContext.test.tsx} +9 -9
- package/src/context/__tests__/{PostHogProvider.test.tsx → InsightsProvider.test.tsx} +33 -33
- package/src/context/index.ts +2 -2
- package/src/helpers/error-helpers.ts +2 -2
- package/src/hooks/__tests__/featureFlags.test.tsx +15 -15
- package/src/hooks/__tests__/useInsights.test.tsx +19 -0
- package/src/hooks/__tests__/useThumbSurvey.test.tsx +7 -7
- package/src/hooks/index.ts +1 -1
- package/src/hooks/useActiveFeatureFlags.ts +2 -2
- package/src/hooks/useFeatureFlagEnabled.ts +2 -2
- package/src/hooks/useFeatureFlagPayload.ts +2 -2
- package/src/hooks/useFeatureFlagResult.ts +2 -2
- package/src/hooks/useFeatureFlagVariantKey.ts +2 -2
- package/src/hooks/useInsights.ts +7 -0
- package/src/hooks/useThumbSurvey.ts +13 -13
- package/src/utils/type-utils.ts +2 -2
- package/surveys/package.json +1 -1
- package/src/context/PostHogContext.ts +0 -9
- package/src/context/PostHogProvider.tsx +0 -136
- package/src/hooks/__tests__/usePostHog.test.tsx +0 -19
- package/src/hooks/usePostHog.ts +0 -7
package/src/components/__tests__/{PostHogErrorBoundary.test.tsx → InsightsErrorBoundary.test.tsx}
RENAMED
|
@@ -2,20 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import { render } from '@testing-library/react'
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
5
|
+
import { __INSIGHTS_ERROR_MESSAGES, InsightsErrorBoundary } from '../InsightsErrorBoundary'
|
|
6
|
+
import insights from '@hanzo/insights'
|
|
7
7
|
|
|
8
|
-
describe('
|
|
8
|
+
describe('InsightsErrorBoundary component', () => {
|
|
9
9
|
mockFunction(console, 'error')
|
|
10
10
|
mockFunction(console, 'warn')
|
|
11
|
-
mockFunction(
|
|
11
|
+
mockFunction(insights, 'captureException')
|
|
12
12
|
|
|
13
13
|
const renderWithError = (props: any) => render(<RenderWithError {...props} />)
|
|
14
14
|
const renderWithoutError = (props?: any) => render(<RenderWithoutError {...props} />)
|
|
15
15
|
|
|
16
16
|
it('should call captureException with error message', () => {
|
|
17
17
|
const { container } = renderWithError({ message: 'Test error', fallback: <div></div> })
|
|
18
|
-
expect(
|
|
18
|
+
expect(insights.captureException).toHaveBeenCalledWith(new Error('Test error'), undefined)
|
|
19
19
|
expect(container.innerHTML).toBe('<div></div>')
|
|
20
20
|
expect(console.error).toHaveBeenCalledTimes(1)
|
|
21
21
|
expect((console.error as any).mock.calls[0][1].message).toEqual('Test error')
|
|
@@ -23,22 +23,22 @@ describe('PostHogErrorBoundary component', () => {
|
|
|
23
23
|
|
|
24
24
|
it('should warn user when fallback is null', () => {
|
|
25
25
|
const { container } = renderWithError({ fallback: null })
|
|
26
|
-
expect(
|
|
26
|
+
expect(insights.captureException).toHaveBeenCalledWith(new Error('Error'), undefined)
|
|
27
27
|
expect(container.innerHTML).toBe('')
|
|
28
|
-
expect(console.warn).toHaveBeenCalledWith(
|
|
28
|
+
expect(console.warn).toHaveBeenCalledWith(__INSIGHTS_ERROR_MESSAGES.INVALID_FALLBACK)
|
|
29
29
|
})
|
|
30
30
|
|
|
31
31
|
it('should warn user when fallback is a string', () => {
|
|
32
32
|
const { container } = renderWithError({ fallback: 'hello' })
|
|
33
|
-
expect(
|
|
33
|
+
expect(insights.captureException).toHaveBeenCalledWith(new Error('Error'), undefined)
|
|
34
34
|
expect(container.innerHTML).toBe('')
|
|
35
|
-
expect(console.warn).toHaveBeenCalledWith(
|
|
35
|
+
expect(console.warn).toHaveBeenCalledWith(__INSIGHTS_ERROR_MESSAGES.INVALID_FALLBACK)
|
|
36
36
|
})
|
|
37
37
|
|
|
38
38
|
it('should add additional properties before sending event (as object)', () => {
|
|
39
39
|
const props = { team_id: '1234' }
|
|
40
40
|
renderWithError({ message: 'Kaboom', additionalProperties: props })
|
|
41
|
-
expect(
|
|
41
|
+
expect(insights.captureException).toHaveBeenCalledWith(new Error('Kaboom'), props)
|
|
42
42
|
})
|
|
43
43
|
|
|
44
44
|
it('should add additional properties before sending event (as function)', () => {
|
|
@@ -50,7 +50,7 @@ describe('PostHogErrorBoundary component', () => {
|
|
|
50
50
|
return props
|
|
51
51
|
},
|
|
52
52
|
})
|
|
53
|
-
expect(
|
|
53
|
+
expect(insights.captureException).toHaveBeenCalledWith(new Error('Kaboom'), props)
|
|
54
54
|
})
|
|
55
55
|
|
|
56
56
|
it('should render children without errors', () => {
|
|
@@ -62,13 +62,13 @@ describe('PostHogErrorBoundary component', () => {
|
|
|
62
62
|
describe('captureException processing', () => {
|
|
63
63
|
mockFunction(console, 'error')
|
|
64
64
|
mockFunction(console, 'warn')
|
|
65
|
-
mockFunction(
|
|
65
|
+
mockFunction(insights, 'capture')
|
|
66
66
|
|
|
67
67
|
const renderWithError = (props: any) => render(<RenderWithError {...props} />)
|
|
68
68
|
|
|
69
69
|
it('should call capture with a stacktrace', () => {
|
|
70
70
|
renderWithError({ message: 'Kaboom', fallback: <div></div>, additionalProperties: {} })
|
|
71
|
-
const captureCalls = (
|
|
71
|
+
const captureCalls = (insights.capture as jest.Mock).mock.calls
|
|
72
72
|
expect(captureCalls.length).toBe(1)
|
|
73
73
|
const exceptionList = captureCalls[0][1].$exception_list
|
|
74
74
|
expect(exceptionList.length).toBe(1)
|
|
@@ -95,16 +95,16 @@ function ComponentWithError({ message }: { message: string }): React.ReactElemen
|
|
|
95
95
|
|
|
96
96
|
function RenderWithError({ message = 'Error', fallback, additionalProperties }: any) {
|
|
97
97
|
return (
|
|
98
|
-
<
|
|
98
|
+
<InsightsErrorBoundary fallback={fallback} additionalProperties={additionalProperties}>
|
|
99
99
|
<ComponentWithError message={message} />
|
|
100
|
-
</
|
|
100
|
+
</InsightsErrorBoundary>
|
|
101
101
|
)
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
function RenderWithoutError({ additionalProperties }: any) {
|
|
105
105
|
return (
|
|
106
|
-
<
|
|
106
|
+
<InsightsErrorBoundary fallback={<div></div>} additionalProperties={additionalProperties}>
|
|
107
107
|
<div>Amazing content</div>
|
|
108
|
-
</
|
|
108
|
+
</InsightsErrorBoundary>
|
|
109
109
|
)
|
|
110
110
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
2
|
import { useState } from 'react'
|
|
3
3
|
import { render, screen, fireEvent } from '@testing-library/react'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { InsightsProvider, Insights } from '../../context'
|
|
5
|
+
import { InsightsFeature } from '../'
|
|
6
6
|
import '@testing-library/jest-dom'
|
|
7
7
|
|
|
8
8
|
const FEATURE_FLAG_STATUS: Record<string, string | boolean> = {
|
|
@@ -20,16 +20,16 @@ const FEATURE_FLAG_PAYLOADS: Record<string, any> = {
|
|
|
20
20
|
},
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
describe('
|
|
24
|
-
let
|
|
23
|
+
describe('InsightsFeature component', () => {
|
|
24
|
+
let insights: Insights
|
|
25
25
|
|
|
26
|
-
const renderWith = (instance:
|
|
26
|
+
const renderWith = (instance: Insights, flag = 'test', matchValue: string | boolean | undefined = true) =>
|
|
27
27
|
render(
|
|
28
|
-
<
|
|
29
|
-
<
|
|
28
|
+
<InsightsProvider client={instance}>
|
|
29
|
+
<InsightsFeature flag={flag} match={matchValue}>
|
|
30
30
|
<div data-testid="helloDiv">Hello</div>
|
|
31
|
-
</
|
|
32
|
-
</
|
|
31
|
+
</InsightsFeature>
|
|
32
|
+
</InsightsProvider>
|
|
33
33
|
)
|
|
34
34
|
|
|
35
35
|
beforeEach(() => {
|
|
@@ -44,7 +44,7 @@ describe('PostHogFeature component', () => {
|
|
|
44
44
|
// eslint-disable-next-line compat/compat
|
|
45
45
|
window.IntersectionObserver = mockIntersectionObserver
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
insights = {
|
|
48
48
|
isFeatureEnabled: (flag: string) => !!FEATURE_FLAG_STATUS[flag],
|
|
49
49
|
getFeatureFlag: (flag: string) => FEATURE_FLAG_STATUS[flag],
|
|
50
50
|
getFeatureFlagPayload: (flag: string) => FEATURE_FLAG_PAYLOADS[flag],
|
|
@@ -62,44 +62,44 @@ describe('PostHogFeature component', () => {
|
|
|
62
62
|
featureFlags: {
|
|
63
63
|
hasLoadedFlags: true,
|
|
64
64
|
},
|
|
65
|
-
} as unknown as
|
|
65
|
+
} as unknown as Insights
|
|
66
66
|
})
|
|
67
67
|
|
|
68
68
|
it('should track interactions with the feature component', () => {
|
|
69
|
-
renderWith(
|
|
69
|
+
renderWith(insights)
|
|
70
70
|
|
|
71
71
|
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
72
|
-
expect(
|
|
72
|
+
expect(insights.capture).toHaveBeenCalledWith('$feature_interaction', {
|
|
73
73
|
feature_flag: 'test',
|
|
74
74
|
$set: { '$feature_interaction/test': true },
|
|
75
75
|
})
|
|
76
|
-
expect(
|
|
76
|
+
expect(insights.capture).toHaveBeenCalledTimes(1)
|
|
77
77
|
})
|
|
78
78
|
|
|
79
79
|
it('should not fire for every interaction with the feature component', () => {
|
|
80
|
-
renderWith(
|
|
80
|
+
renderWith(insights)
|
|
81
81
|
|
|
82
82
|
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
83
|
-
expect(
|
|
83
|
+
expect(insights.capture).toHaveBeenCalledWith('$feature_interaction', {
|
|
84
84
|
feature_flag: 'test',
|
|
85
85
|
$set: { '$feature_interaction/test': true },
|
|
86
86
|
})
|
|
87
|
-
expect(
|
|
87
|
+
expect(insights.capture).toHaveBeenCalledTimes(1)
|
|
88
88
|
|
|
89
89
|
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
90
90
|
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
91
91
|
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
92
|
-
expect(
|
|
92
|
+
expect(insights.capture).toHaveBeenCalledTimes(1)
|
|
93
93
|
})
|
|
94
94
|
|
|
95
95
|
it('should track an interaction with each child node of the feature component', () => {
|
|
96
96
|
render(
|
|
97
|
-
<
|
|
98
|
-
<
|
|
97
|
+
<InsightsProvider client={insights}>
|
|
98
|
+
<InsightsFeature flag={'test'} match={true}>
|
|
99
99
|
<div data-testid="helloDiv">Hello</div>
|
|
100
100
|
<div data-testid="worldDiv">World!</div>
|
|
101
|
-
</
|
|
102
|
-
</
|
|
101
|
+
</InsightsFeature>
|
|
102
|
+
</InsightsProvider>
|
|
103
103
|
)
|
|
104
104
|
|
|
105
105
|
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
@@ -107,29 +107,29 @@ describe('PostHogFeature component', () => {
|
|
|
107
107
|
fireEvent.click(screen.getByTestId('worldDiv'))
|
|
108
108
|
fireEvent.click(screen.getByTestId('worldDiv'))
|
|
109
109
|
fireEvent.click(screen.getByTestId('worldDiv'))
|
|
110
|
-
expect(
|
|
110
|
+
expect(insights.capture).toHaveBeenCalledWith('$feature_interaction', {
|
|
111
111
|
feature_flag: 'test',
|
|
112
112
|
$set: { '$feature_interaction/test': true },
|
|
113
113
|
})
|
|
114
|
-
expect(
|
|
114
|
+
expect(insights.capture).toHaveBeenCalledTimes(1)
|
|
115
115
|
})
|
|
116
116
|
|
|
117
117
|
it('should not fire events when interaction is disabled', () => {
|
|
118
118
|
render(
|
|
119
|
-
<
|
|
120
|
-
<
|
|
119
|
+
<InsightsProvider client={insights}>
|
|
120
|
+
<InsightsFeature flag={'test'} match={true} trackInteraction={false}>
|
|
121
121
|
<div data-testid="helloDiv">Hello</div>
|
|
122
|
-
</
|
|
123
|
-
</
|
|
122
|
+
</InsightsFeature>
|
|
123
|
+
</InsightsProvider>
|
|
124
124
|
)
|
|
125
125
|
|
|
126
126
|
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
127
|
-
expect(
|
|
127
|
+
expect(insights.capture).not.toHaveBeenCalled()
|
|
128
128
|
|
|
129
129
|
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
130
130
|
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
131
131
|
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
132
|
-
expect(
|
|
132
|
+
expect(insights.capture).not.toHaveBeenCalled()
|
|
133
133
|
})
|
|
134
134
|
|
|
135
135
|
it('should fire events when interaction is disabled but re-enabled after', () => {
|
|
@@ -146,37 +146,37 @@ describe('PostHogFeature component', () => {
|
|
|
146
146
|
>
|
|
147
147
|
Click me
|
|
148
148
|
</div>
|
|
149
|
-
<
|
|
149
|
+
<InsightsFeature flag={'test'} match={true} trackInteraction={trackInteraction}>
|
|
150
150
|
<div data-testid="helloDiv">Hello</div>
|
|
151
|
-
</
|
|
151
|
+
</InsightsFeature>
|
|
152
152
|
</>
|
|
153
153
|
)
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
render(
|
|
157
|
-
<
|
|
157
|
+
<InsightsProvider client={insights}>
|
|
158
158
|
<DynamicUpdateComponent />
|
|
159
|
-
</
|
|
159
|
+
</InsightsProvider>
|
|
160
160
|
)
|
|
161
161
|
|
|
162
162
|
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
163
|
-
expect(
|
|
163
|
+
expect(insights.capture).not.toHaveBeenCalled()
|
|
164
164
|
|
|
165
165
|
fireEvent.click(screen.getByTestId('clicker'))
|
|
166
166
|
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
167
167
|
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
168
|
-
expect(
|
|
168
|
+
expect(insights.capture).toHaveBeenCalledWith('$feature_interaction', {
|
|
169
169
|
feature_flag: 'test',
|
|
170
170
|
$set: { '$feature_interaction/test': true },
|
|
171
171
|
})
|
|
172
|
-
expect(
|
|
172
|
+
expect(insights.capture).toHaveBeenCalledTimes(1)
|
|
173
173
|
})
|
|
174
174
|
|
|
175
175
|
it('should not show the feature component if the flag is not enabled', () => {
|
|
176
|
-
renderWith(
|
|
176
|
+
renderWith(insights, 'test_value')
|
|
177
177
|
|
|
178
178
|
expect(screen.queryByTestId('helloDiv')).not.toBeInTheDocument()
|
|
179
|
-
expect(
|
|
179
|
+
expect(insights.capture).not.toHaveBeenCalled()
|
|
180
180
|
|
|
181
181
|
// check if any elements are found
|
|
182
182
|
const allTags = screen.queryAllByText(/.*/)
|
|
@@ -189,101 +189,101 @@ describe('PostHogFeature component', () => {
|
|
|
189
189
|
|
|
190
190
|
it('should fallback when provided', () => {
|
|
191
191
|
render(
|
|
192
|
-
<
|
|
193
|
-
<
|
|
192
|
+
<InsightsProvider client={insights}>
|
|
193
|
+
<InsightsFeature flag={'test_false'} match={true} fallback={<div data-testid="nope">Nope</div>}>
|
|
194
194
|
<div data-testid="helloDiv">Hello</div>
|
|
195
|
-
</
|
|
196
|
-
</
|
|
195
|
+
</InsightsFeature>
|
|
196
|
+
</InsightsProvider>
|
|
197
197
|
)
|
|
198
198
|
|
|
199
199
|
expect(screen.queryByTestId('helloDiv')).not.toBeInTheDocument()
|
|
200
|
-
expect(
|
|
200
|
+
expect(insights.capture).not.toHaveBeenCalled()
|
|
201
201
|
|
|
202
202
|
fireEvent.click(screen.getByTestId('nope'))
|
|
203
|
-
expect(
|
|
203
|
+
expect(insights.capture).not.toHaveBeenCalled()
|
|
204
204
|
})
|
|
205
205
|
|
|
206
206
|
it('should handle showing multivariate flags with bool match', () => {
|
|
207
|
-
renderWith(
|
|
207
|
+
renderWith(insights, 'multivariate_feature')
|
|
208
208
|
|
|
209
209
|
expect(screen.queryByTestId('helloDiv')).not.toBeInTheDocument()
|
|
210
|
-
expect(
|
|
210
|
+
expect(insights.capture).not.toHaveBeenCalled()
|
|
211
211
|
})
|
|
212
212
|
|
|
213
213
|
it('should handle showing multivariate flags with incorrect match', () => {
|
|
214
|
-
renderWith(
|
|
214
|
+
renderWith(insights, 'multivariate_feature', 'string-valueCXCC')
|
|
215
215
|
|
|
216
216
|
expect(screen.queryByTestId('helloDiv')).not.toBeInTheDocument()
|
|
217
|
-
expect(
|
|
217
|
+
expect(insights.capture).not.toHaveBeenCalled()
|
|
218
218
|
})
|
|
219
219
|
|
|
220
220
|
it('should handle showing multivariate flags', () => {
|
|
221
|
-
renderWith(
|
|
221
|
+
renderWith(insights, 'multivariate_feature', 'string-value')
|
|
222
222
|
|
|
223
223
|
expect(screen.queryByTestId('helloDiv')).toBeInTheDocument()
|
|
224
|
-
expect(
|
|
224
|
+
expect(insights.capture).not.toHaveBeenCalled()
|
|
225
225
|
|
|
226
226
|
fireEvent.click(screen.getByTestId('helloDiv'))
|
|
227
|
-
expect(
|
|
227
|
+
expect(insights.capture).toHaveBeenCalledWith('$feature_interaction', {
|
|
228
228
|
feature_flag: 'multivariate_feature',
|
|
229
229
|
feature_flag_variant: 'string-value',
|
|
230
230
|
$set: { '$feature_interaction/multivariate_feature': 'string-value' },
|
|
231
231
|
})
|
|
232
|
-
expect(
|
|
232
|
+
expect(insights.capture).toHaveBeenCalledTimes(1)
|
|
233
233
|
})
|
|
234
234
|
|
|
235
235
|
it('should handle payload flags', () => {
|
|
236
236
|
render(
|
|
237
|
-
<
|
|
238
|
-
<
|
|
237
|
+
<InsightsProvider client={insights}>
|
|
238
|
+
<InsightsFeature flag={'example_feature_payload'} match={'test'}>
|
|
239
239
|
{(payload: any) => {
|
|
240
240
|
return <div data-testid={`hi_${payload.name}`}>Hullo</div>
|
|
241
241
|
}}
|
|
242
|
-
</
|
|
243
|
-
</
|
|
242
|
+
</InsightsFeature>
|
|
243
|
+
</InsightsProvider>
|
|
244
244
|
)
|
|
245
245
|
|
|
246
246
|
expect(screen.queryByTestId('hi_example_feature_1_payload')).toBeInTheDocument()
|
|
247
|
-
expect(
|
|
247
|
+
expect(insights.capture).not.toHaveBeenCalled()
|
|
248
248
|
|
|
249
249
|
fireEvent.click(screen.getByTestId('hi_example_feature_1_payload'))
|
|
250
|
-
expect(
|
|
250
|
+
expect(insights.capture).toHaveBeenCalledTimes(1)
|
|
251
251
|
})
|
|
252
252
|
|
|
253
253
|
it('should not render when flag does not exist and no match is specified', () => {
|
|
254
254
|
render(
|
|
255
|
-
<
|
|
256
|
-
<
|
|
255
|
+
<InsightsProvider client={insights}>
|
|
256
|
+
<InsightsFeature flag={'nonexistent_flag'}>
|
|
257
257
|
<div data-testid="helloDiv">Hello</div>
|
|
258
|
-
</
|
|
259
|
-
</
|
|
258
|
+
</InsightsFeature>
|
|
259
|
+
</InsightsProvider>
|
|
260
260
|
)
|
|
261
261
|
|
|
262
262
|
expect(screen.queryByTestId('helloDiv')).not.toBeInTheDocument()
|
|
263
|
-
expect(
|
|
263
|
+
expect(insights.capture).not.toHaveBeenCalled()
|
|
264
264
|
})
|
|
265
265
|
|
|
266
266
|
it('should render fallback when flag does not exist (like new-cta example)', () => {
|
|
267
267
|
render(
|
|
268
|
-
<
|
|
269
|
-
<
|
|
268
|
+
<InsightsProvider client={insights}>
|
|
269
|
+
<InsightsFeature flag={'new-cta'} fallback={<div data-testid="oldButton">Old Button</div>}>
|
|
270
270
|
<div data-testid="newButton">New Button</div>
|
|
271
|
-
</
|
|
272
|
-
</
|
|
271
|
+
</InsightsFeature>
|
|
272
|
+
</InsightsProvider>
|
|
273
273
|
)
|
|
274
274
|
|
|
275
275
|
expect(screen.queryByTestId('newButton')).not.toBeInTheDocument()
|
|
276
276
|
expect(screen.queryByTestId('oldButton')).toBeInTheDocument()
|
|
277
|
-
expect(
|
|
277
|
+
expect(insights.capture).not.toHaveBeenCalled()
|
|
278
278
|
})
|
|
279
279
|
|
|
280
280
|
it('should render content when match=false and flag variant is false', () => {
|
|
281
281
|
render(
|
|
282
|
-
<
|
|
283
|
-
<
|
|
282
|
+
<InsightsProvider client={insights}>
|
|
283
|
+
<InsightsFeature flag={'test_false'} match={false}>
|
|
284
284
|
<div data-testid="disabledUI">Show when disabled</div>
|
|
285
|
-
</
|
|
286
|
-
</
|
|
285
|
+
</InsightsFeature>
|
|
286
|
+
</InsightsProvider>
|
|
287
287
|
)
|
|
288
288
|
|
|
289
289
|
expect(screen.queryByTestId('disabledUI')).toBeInTheDocument()
|
package/src/components/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export * from './
|
|
2
|
-
export * from './
|
|
1
|
+
export * from './InsightsFeature'
|
|
2
|
+
export * from './InsightsCaptureOnViewed'
|
|
3
3
|
export {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from './
|
|
4
|
+
InsightsErrorBoundary,
|
|
5
|
+
InsightsErrorBoundaryProps,
|
|
6
|
+
InsightsErrorBoundaryFallbackProps,
|
|
7
|
+
} from './InsightsErrorBoundary'
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import insightsJs, { BootstrapConfig } from '@hanzo/insights'
|
|
2
|
+
import { createContext } from 'react'
|
|
3
|
+
|
|
4
|
+
export type Insights = typeof insightsJs
|
|
5
|
+
|
|
6
|
+
export const InsightsContext = createContext<{ client: Insights; bootstrap?: BootstrapConfig }>({
|
|
7
|
+
client: insightsJs,
|
|
8
|
+
bootstrap: undefined,
|
|
9
|
+
})
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import insightsJs, { InsightsConfig as InsightsConfig } from '@hanzo/insights'
|
|
3
|
+
|
|
4
|
+
import React, { useEffect, useMemo, useRef } from 'react'
|
|
5
|
+
import { Insights, InsightsContext } from './InsightsContext'
|
|
6
|
+
import { isDeepEqual } from '../utils/object-utils'
|
|
7
|
+
|
|
8
|
+
interface PreviousInitialization {
|
|
9
|
+
apiKey: string
|
|
10
|
+
options: Partial<InsightsConfig>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type WithOptionalChildren<T> = T & { children?: React.ReactNode | undefined }
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Props for the InsightsProvider component.
|
|
17
|
+
* This is a discriminated union type that ensures mutually exclusive props:
|
|
18
|
+
*
|
|
19
|
+
* - If `client` is provided, `apiKey` and `options` must not be provided
|
|
20
|
+
* - If `apiKey` is provided, `client` must not be provided, and `options` is optional
|
|
21
|
+
*/
|
|
22
|
+
type InsightsProviderProps =
|
|
23
|
+
| { client: Insights; apiKey?: never; options?: never }
|
|
24
|
+
| { apiKey: string; options?: Partial<InsightsConfig>; client?: never }
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* InsightsProvider is a React context provider for Hanzo Insights analytics.
|
|
28
|
+
* It can be initialized in two mutually exclusive ways:
|
|
29
|
+
*
|
|
30
|
+
* 1. By providing an existing Insights `client` instance
|
|
31
|
+
* 2. By providing an `apiKey` (and optionally `options`) to create a new client
|
|
32
|
+
*
|
|
33
|
+
* These initialization methods are mutually exclusive - you must use one or the other,
|
|
34
|
+
* but not both simultaneously.
|
|
35
|
+
*/
|
|
36
|
+
export function InsightsProvider({ children, client, apiKey, options }: WithOptionalChildren<InsightsProviderProps>) {
|
|
37
|
+
const previousInitializationRef = useRef<PreviousInitialization | null>(null)
|
|
38
|
+
|
|
39
|
+
const insights = useMemo(() => {
|
|
40
|
+
if (client) {
|
|
41
|
+
if (apiKey) {
|
|
42
|
+
console.warn(
|
|
43
|
+
'[Insights] You have provided both `client` and `apiKey` to `InsightsProvider`. `apiKey` will be ignored in favour of `client`.'
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
if (options) {
|
|
47
|
+
console.warn(
|
|
48
|
+
'[Insights] You have provided both `client` and `options` to `InsightsProvider`. `options` will be ignored in favour of `client`.'
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
return client
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (apiKey) {
|
|
55
|
+
return insightsJs
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.warn(
|
|
59
|
+
'[Insights] No `apiKey` or `client` were provided to `InsightsProvider`. Using default global instance. You must initialize it manually. This is not recommended behavior.'
|
|
60
|
+
)
|
|
61
|
+
return insightsJs
|
|
62
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
63
|
+
}, [client, apiKey, JSON.stringify(options)])
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (client) {
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
const previousInitialization = previousInitializationRef.current
|
|
70
|
+
|
|
71
|
+
if (!previousInitialization) {
|
|
72
|
+
if (insightsJs.__loaded) {
|
|
73
|
+
console.warn('[Insights] `insights` was already loaded elsewhere. This may cause issues.')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
insightsJs.init(apiKey, options)
|
|
77
|
+
|
|
78
|
+
previousInitializationRef.current = {
|
|
79
|
+
apiKey: apiKey,
|
|
80
|
+
options: options ?? {},
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
if (apiKey !== previousInitialization.apiKey) {
|
|
84
|
+
console.warn(
|
|
85
|
+
"[Insights] You have provided a different `apiKey` to `InsightsProvider` than the one that was already initialized. This is not supported by our provider and we'll keep using the previous key. If you need to toggle between API Keys you need to control the `client` yourself and pass it in as a prop rather than an `apiKey` prop."
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (options && !isDeepEqual(options, previousInitialization.options)) {
|
|
90
|
+
insightsJs.set_config(options)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
previousInitializationRef.current = {
|
|
94
|
+
apiKey: apiKey,
|
|
95
|
+
options: options ?? {},
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
99
|
+
}, [client, apiKey, JSON.stringify(options)])
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<InsightsContext.Provider
|
|
103
|
+
value={{ client: insights, bootstrap: options?.bootstrap ?? client?.config?.bootstrap }}
|
|
104
|
+
>
|
|
105
|
+
{children}
|
|
106
|
+
</InsightsContext.Provider>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
2
|
import { render } from '@testing-library/react'
|
|
3
|
-
import {
|
|
3
|
+
import { InsightsProvider, Insights } from '..'
|
|
4
4
|
|
|
5
|
-
describe('
|
|
6
|
-
const
|
|
5
|
+
describe('InsightsContext component', () => {
|
|
6
|
+
const insights = {} as unknown as Insights
|
|
7
7
|
|
|
8
8
|
it('should return a client instance from the context if available', () => {
|
|
9
9
|
render(
|
|
10
|
-
<
|
|
10
|
+
<InsightsProvider client={insights}>
|
|
11
11
|
<div>Hello</div>
|
|
12
|
-
</
|
|
12
|
+
</InsightsProvider>
|
|
13
13
|
)
|
|
14
14
|
})
|
|
15
15
|
|
|
@@ -20,16 +20,16 @@ describe('PostHogContext component', () => {
|
|
|
20
20
|
expect(() => {
|
|
21
21
|
render(
|
|
22
22
|
// we have to cast `as any` so that we can test for when
|
|
23
|
-
//
|
|
24
|
-
<
|
|
23
|
+
// insights might not exist - in SSR for example
|
|
24
|
+
<InsightsProvider client={undefined as any}>
|
|
25
25
|
<div>Hello</div>
|
|
26
|
-
</
|
|
26
|
+
</InsightsProvider>
|
|
27
27
|
)
|
|
28
28
|
}).not.toThrow()
|
|
29
29
|
|
|
30
30
|
// eslint-disable-next-line no-console
|
|
31
31
|
expect(console.warn).toHaveBeenCalledWith(
|
|
32
|
-
'[
|
|
32
|
+
'[Insights] No `apiKey` or `client` were provided to `InsightsProvider`. Using default global instance. You must initialize it manually. This is not recommended behavior.'
|
|
33
33
|
)
|
|
34
34
|
})
|
|
35
35
|
})
|