@envive-ai/react-hooks 0.3.19-alpha-marlo-2 → 0.3.19-alpha-marlo-4
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/dist/application/utils/elementObserver.d.cts +2 -2
- package/dist/application/utils/elementObserver.d.ts +2 -2
- package/dist/atoms/app/index.d.cts +7 -7
- package/dist/atoms/app/index.d.ts +7 -7
- package/dist/atoms/app/variant.d.ts +6 -6
- package/dist/atoms/chat/chatState.d.cts +17 -17
- package/dist/atoms/chat/chatState.d.ts +18 -18
- package/dist/atoms/chat/form.d.cts +2 -2
- package/dist/atoms/chat/form.d.ts +3 -3
- package/dist/atoms/chat/index.d.cts +2 -2
- package/dist/atoms/chat/index.d.ts +3 -3
- package/dist/atoms/chat/lastMessage.d.cts +2 -2
- package/dist/atoms/chat/lastMessage.d.ts +2 -2
- package/dist/atoms/chat/messageQueue.d.cts +6 -6
- package/dist/atoms/chat/messageQueue.d.ts +7 -7
- package/dist/atoms/chat/performanceMetrics.d.cts +6 -6
- package/dist/atoms/chat/performanceMetrics.d.ts +6 -6
- package/dist/atoms/chat/renderedWidgetRefs.d.cts +2 -2
- package/dist/atoms/chat/renderedWidgetRefs.d.ts +2 -2
- package/dist/atoms/chat/replies.d.cts +3 -3
- package/dist/atoms/chat/replies.d.ts +3 -3
- package/dist/atoms/chat/suggestions.d.cts +2 -2
- package/dist/atoms/chat/suggestions.d.ts +3 -3
- package/dist/atoms/envive/enviveConfig.d.cts +12 -12
- package/dist/atoms/envive/enviveConfig.d.ts +13 -13
- package/dist/atoms/globalSearch/globalSearch.d.cts +5 -5
- package/dist/atoms/globalSearch/globalSearch.d.ts +5 -5
- package/dist/atoms/org/customerService.d.cts +6 -6
- package/dist/atoms/org/customerService.d.ts +6 -6
- package/dist/atoms/org/graphqlConfig.d.cts +4 -4
- package/dist/atoms/org/graphqlConfig.d.ts +4 -4
- package/dist/atoms/org/newOrgConfigAtom.d.cts +2 -2
- package/dist/atoms/org/newOrgConfigAtom.d.ts +2 -2
- package/dist/atoms/org/orgAnalyticsConfig.d.cts +5 -5
- package/dist/atoms/org/orgAnalyticsConfig.d.ts +5 -5
- package/dist/atoms/search/chatSearch.d.cts +17 -17
- package/dist/atoms/search/chatSearch.d.ts +17 -17
- package/dist/atoms/search/searchAPI.d.cts +13 -13
- package/dist/atoms/search/searchAPI.d.ts +13 -13
- package/dist/atoms/search/types.d.cts +1 -1
- package/dist/atoms/search/types.d.ts +1 -1
- package/dist/atoms/search/utils.d.cts +1 -1
- package/dist/atoms/widget/chatPreviewLoading.d.cts +2 -2
- package/dist/atoms/widget/chatPreviewLoading.d.ts +2 -2
- package/dist/contexts/amplitudeContext/amplitudeContext.cjs +9 -6
- package/dist/contexts/amplitudeContext/amplitudeContext.js +9 -6
- package/dist/contexts/enviveContext/enviveContext.cjs +3 -3
- package/dist/contexts/enviveContext/enviveContext.js +3 -3
- package/dist/contexts/enviveContext/types.d.cts +1 -1
- package/dist/contexts/pageContext/pageContext.cjs +58 -1
- package/dist/contexts/pageContext/pageContext.js +60 -3
- package/dist/contexts/pageContext/types.d.ts +1 -1
- package/dist/contexts/systemSettingsContext/systemSettingsContext.d.cts +2 -2
- package/dist/contexts/systemSettingsContext/systemSettingsContext.d.ts +2 -2
- package/dist/contexts/types.d.cts +1 -1
- package/dist/contexts/types.d.ts +1 -1
- package/dist/contexts/typesV3.d.ts +1 -1
- package/dist/hooks/GrabAndScroll/useGrabAndScroll.d.ts +2 -2
- package/dist/hooks/PageViewedEvent/index.cjs +4 -0
- package/dist/hooks/PageViewedEvent/index.d.cts +2 -0
- package/dist/hooks/PageViewedEvent/index.d.ts +2 -0
- package/dist/hooks/PageViewedEvent/index.js +3 -0
- package/dist/hooks/PageViewedEvent/usePageViewedEvent.cjs +84 -0
- package/dist/hooks/PageViewedEvent/usePageViewedEvent.d.cts +18 -0
- package/dist/hooks/PageViewedEvent/usePageViewedEvent.d.ts +18 -0
- package/dist/hooks/PageViewedEvent/usePageViewedEvent.js +82 -0
- package/dist/hooks/SystemSettingsContext/useSystemSettingsContext.d.cts +2 -2
- package/dist/hooks/SystemSettingsContext/useSystemSettingsContext.d.ts +2 -2
- package/dist/hooks/WidgetInteraction/types.d.ts +1 -1
- package/dist/hooks/WidgetInteraction/utils.d.ts +1 -1
- package/dist/hooks/utils.d.ts +1 -1
- package/dist/services/amplitudeService/amplitudeService.cjs +5 -10
- package/dist/services/amplitudeService/amplitudeService.d.cts +2 -4
- package/dist/services/amplitudeService/amplitudeService.d.ts +2 -4
- package/dist/services/amplitudeService/amplitudeService.js +5 -10
- package/package.json +5 -1
- package/src/contexts/amplitudeContext/__tests__/amplitudeContext.test.tsx +5 -6
- package/src/contexts/amplitudeContext/amplitudeContext.tsx +7 -3
- package/src/contexts/pageContext/__tests__/pageContext.test.tsx +6 -0
- package/src/contexts/pageContext/pageContext.tsx +60 -1
- package/src/hooks/PageViewedEvent/__tests__/usePageViewedEvent.test.ts +297 -0
- package/src/hooks/PageViewedEvent/index.ts +1 -0
- package/src/hooks/PageViewedEvent/usePageViewedEvent.ts +103 -0
- package/src/services/amplitudeService/__tests__/amplitudeService.test.ts +6 -56
- package/src/services/amplitudeService/amplitudeService.ts +4 -14
|
@@ -10,15 +10,18 @@ import {
|
|
|
10
10
|
} from 'react';
|
|
11
11
|
import {
|
|
12
12
|
UrlResolverResponse,
|
|
13
|
+
hasParsedVariantInfoAtom,
|
|
13
14
|
pageUserEventAtom,
|
|
14
15
|
pageVariantInfoAtom,
|
|
15
16
|
urlResolverAtom,
|
|
17
|
+
variantInfoAtom,
|
|
16
18
|
} from 'src/atoms/app/variant';
|
|
17
|
-
import { useAtom } from 'jotai';
|
|
19
|
+
import { useAtom, useAtomValue } from 'jotai';
|
|
18
20
|
import CommerceApiClient from 'src/application/commerce-api';
|
|
19
21
|
import Logger from 'src/application/logging/logger';
|
|
20
22
|
import { PageVisitCategory, UserEventCategory } from '@spiffy-ai/commerce-api-client';
|
|
21
23
|
import { VariantTypeEnum } from 'src/application/models';
|
|
24
|
+
import { EnviveMetricsEventName, useAmplitude } from 'src/contexts/amplitudeContext';
|
|
22
25
|
import { mapApiUserEventToUserEvent, mapUrlResolverResponseToVariantInfo } from './mapping';
|
|
23
26
|
import { PageDetails } from './types';
|
|
24
27
|
|
|
@@ -138,6 +141,62 @@ export const PageProvider: React.FC<{
|
|
|
138
141
|
onUrlResolverNotReady,
|
|
139
142
|
]);
|
|
140
143
|
|
|
144
|
+
const { trackEvent } = useAmplitude();
|
|
145
|
+
const resolvedVariantInfo = useAtomValue(variantInfoAtom);
|
|
146
|
+
const hasParsedVariantInfo = useAtomValue(hasParsedVariantInfoAtom);
|
|
147
|
+
const pageViewedFired = useRef(false);
|
|
148
|
+
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (pageViewedFired.current || isLoading || !hasParsedVariantInfo || !variantInfo) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let pageType: string;
|
|
155
|
+
let pageId: string;
|
|
156
|
+
|
|
157
|
+
switch (variantInfo.variantType) {
|
|
158
|
+
case VariantTypeEnum.Pdp:
|
|
159
|
+
pageType = 'pdp';
|
|
160
|
+
pageId = variantInfo.productId;
|
|
161
|
+
break;
|
|
162
|
+
case VariantTypeEnum.Plp:
|
|
163
|
+
pageType = 'plp';
|
|
164
|
+
pageId = variantInfo.plpId;
|
|
165
|
+
break;
|
|
166
|
+
case VariantTypeEnum.Home:
|
|
167
|
+
pageType = 'homepage';
|
|
168
|
+
pageId = variantInfo.url;
|
|
169
|
+
break;
|
|
170
|
+
case VariantTypeEnum.Other:
|
|
171
|
+
pageType = 'other';
|
|
172
|
+
pageId = variantInfo.url;
|
|
173
|
+
break;
|
|
174
|
+
case VariantTypeEnum.PageVisit:
|
|
175
|
+
pageType = 'other';
|
|
176
|
+
pageId = variantInfo.url;
|
|
177
|
+
break;
|
|
178
|
+
case VariantTypeEnum.FullPageSalesAgent:
|
|
179
|
+
pageType = 'full_page_sales_agent';
|
|
180
|
+
pageId = variantInfo.url;
|
|
181
|
+
break;
|
|
182
|
+
default:
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
pageViewedFired.current = true;
|
|
187
|
+
|
|
188
|
+
trackEvent({
|
|
189
|
+
eventName: EnviveMetricsEventName.PageViewed,
|
|
190
|
+
eventProps: {
|
|
191
|
+
'context.page_type': pageType,
|
|
192
|
+
'context.page_id': pageId,
|
|
193
|
+
'context.supported': isSupported,
|
|
194
|
+
'context.ready': isSupported,
|
|
195
|
+
'context.page_variant_id': resolvedVariantInfo.variantId,
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
}, [isLoading, isSupported, hasParsedVariantInfo, variantInfo, resolvedVariantInfo, trackEvent]);
|
|
199
|
+
|
|
141
200
|
const setPageUrlStable = useCallback((url: string) => {
|
|
142
201
|
setPageUrl(url);
|
|
143
202
|
}, []);
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react';
|
|
2
|
+
import { VariantTypeEnum } from 'src/application/models';
|
|
3
|
+
import { EnviveMetricsEventName } from 'src/contexts/amplitudeContext';
|
|
4
|
+
import {
|
|
5
|
+
FullPageSalesAgentVariantInfo,
|
|
6
|
+
HomeVariantInfo,
|
|
7
|
+
OtherVariantInfo,
|
|
8
|
+
PDPPageVariantInfo,
|
|
9
|
+
PLPPageVariantInfo,
|
|
10
|
+
PageVariantInfo,
|
|
11
|
+
VisitPageVariantInfo,
|
|
12
|
+
} from 'src/contexts/pageContext/types';
|
|
13
|
+
import { extractPageViewedContext, usePageViewedEvent } from '../usePageViewedEvent';
|
|
14
|
+
|
|
15
|
+
const mockTrackEvent = vi.fn();
|
|
16
|
+
|
|
17
|
+
vi.mock('src/contexts/amplitudeContext', async () => {
|
|
18
|
+
const actual = await vi.importActual('src/contexts/amplitudeContext');
|
|
19
|
+
return {
|
|
20
|
+
...actual,
|
|
21
|
+
useAmplitude: () => ({
|
|
22
|
+
trackEvent: mockTrackEvent,
|
|
23
|
+
}),
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const mockUsePage = vi.fn();
|
|
28
|
+
vi.mock('src/contexts/pageContext', () => ({
|
|
29
|
+
usePage: () => mockUsePage(),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
const mockUseAtomValue = vi.fn();
|
|
33
|
+
vi.mock('jotai', async () => {
|
|
34
|
+
const actual = await vi.importActual('jotai');
|
|
35
|
+
return {
|
|
36
|
+
...actual,
|
|
37
|
+
useAtomValue: () => mockUseAtomValue(),
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('extractPageViewedContext', () => {
|
|
42
|
+
it('should return pdp context', () => {
|
|
43
|
+
const variant: PDPPageVariantInfo = {
|
|
44
|
+
variantType: VariantTypeEnum.Pdp,
|
|
45
|
+
productId: 'product-123',
|
|
46
|
+
collections: [],
|
|
47
|
+
numberOfReviews: 0,
|
|
48
|
+
merchantTags: [],
|
|
49
|
+
url: 'https://example.com/products/product-123',
|
|
50
|
+
};
|
|
51
|
+
expect(extractPageViewedContext(variant)).toEqual({
|
|
52
|
+
page_type: 'pdp',
|
|
53
|
+
page_id: 'product-123',
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should return plp context', () => {
|
|
58
|
+
const variant: PLPPageVariantInfo = {
|
|
59
|
+
variantType: VariantTypeEnum.Plp,
|
|
60
|
+
plpId: 'collection-456',
|
|
61
|
+
topCategory: 'shoes',
|
|
62
|
+
url: 'https://example.com/collections/shoes',
|
|
63
|
+
};
|
|
64
|
+
expect(extractPageViewedContext(variant)).toEqual({
|
|
65
|
+
page_type: 'plp',
|
|
66
|
+
page_id: 'collection-456',
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should return homepage context', () => {
|
|
71
|
+
const variant: HomeVariantInfo = {
|
|
72
|
+
variantType: VariantTypeEnum.Home,
|
|
73
|
+
url: 'https://example.com/',
|
|
74
|
+
};
|
|
75
|
+
expect(extractPageViewedContext(variant)).toEqual({
|
|
76
|
+
page_type: 'homepage',
|
|
77
|
+
page_id: 'https://example.com/',
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should return other context for Other variant', () => {
|
|
82
|
+
const variant: OtherVariantInfo = {
|
|
83
|
+
variantType: VariantTypeEnum.Other,
|
|
84
|
+
url: 'https://example.com/about',
|
|
85
|
+
};
|
|
86
|
+
expect(extractPageViewedContext(variant)).toEqual({
|
|
87
|
+
page_type: 'other',
|
|
88
|
+
page_id: 'https://example.com/about',
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should return other context for PageVisit variant', () => {
|
|
93
|
+
const variant: VisitPageVariantInfo = {
|
|
94
|
+
variantType: VariantTypeEnum.PageVisit,
|
|
95
|
+
url: 'https://example.com/contact',
|
|
96
|
+
};
|
|
97
|
+
expect(extractPageViewedContext(variant)).toEqual({
|
|
98
|
+
page_type: 'other',
|
|
99
|
+
page_id: 'https://example.com/contact',
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should return full_page_sales_agent context', () => {
|
|
104
|
+
const variant: FullPageSalesAgentVariantInfo = {
|
|
105
|
+
variantType: VariantTypeEnum.FullPageSalesAgent,
|
|
106
|
+
url: 'https://example.com/agent',
|
|
107
|
+
};
|
|
108
|
+
expect(extractPageViewedContext(variant)).toEqual({
|
|
109
|
+
page_type: 'full_page_sales_agent',
|
|
110
|
+
page_id: 'https://example.com/agent',
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should return null for unknown variant type', () => {
|
|
115
|
+
const variant = { variantType: 'unknown' } as unknown as PageVariantInfo;
|
|
116
|
+
expect(extractPageViewedContext(variant)).toBeNull();
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('usePageViewedEvent', () => {
|
|
121
|
+
beforeEach(() => {
|
|
122
|
+
vi.clearAllMocks();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should not fire when still loading', () => {
|
|
126
|
+
mockUsePage.mockReturnValue({
|
|
127
|
+
variantInfo: {
|
|
128
|
+
variantType: VariantTypeEnum.Pdp,
|
|
129
|
+
productId: 'p1',
|
|
130
|
+
collections: [],
|
|
131
|
+
numberOfReviews: 0,
|
|
132
|
+
merchantTags: [],
|
|
133
|
+
url: '',
|
|
134
|
+
},
|
|
135
|
+
isSupported: true,
|
|
136
|
+
isLoading: true,
|
|
137
|
+
});
|
|
138
|
+
mockUseAtomValue
|
|
139
|
+
.mockReturnValueOnce({ variantId: 'v1', variant: 'pdp' })
|
|
140
|
+
.mockReturnValueOnce(true);
|
|
141
|
+
|
|
142
|
+
renderHook(() => usePageViewedEvent());
|
|
143
|
+
expect(mockTrackEvent).not.toHaveBeenCalled();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should not fire when hasParsedVariantInfo is false', () => {
|
|
147
|
+
mockUsePage.mockReturnValue({
|
|
148
|
+
variantInfo: {
|
|
149
|
+
variantType: VariantTypeEnum.Pdp,
|
|
150
|
+
productId: 'p1',
|
|
151
|
+
collections: [],
|
|
152
|
+
numberOfReviews: 0,
|
|
153
|
+
merchantTags: [],
|
|
154
|
+
url: '',
|
|
155
|
+
},
|
|
156
|
+
isSupported: true,
|
|
157
|
+
isLoading: false,
|
|
158
|
+
});
|
|
159
|
+
mockUseAtomValue
|
|
160
|
+
.mockReturnValueOnce({ variantId: 'v1', variant: 'pdp' })
|
|
161
|
+
.mockReturnValueOnce(false);
|
|
162
|
+
|
|
163
|
+
renderHook(() => usePageViewedEvent());
|
|
164
|
+
expect(mockTrackEvent).not.toHaveBeenCalled();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should not fire when pageVariantInfo is undefined', () => {
|
|
168
|
+
mockUsePage.mockReturnValue({
|
|
169
|
+
variantInfo: undefined,
|
|
170
|
+
isSupported: false,
|
|
171
|
+
isLoading: false,
|
|
172
|
+
});
|
|
173
|
+
mockUseAtomValue
|
|
174
|
+
.mockReturnValueOnce({ variantId: 'v1', variant: 'pdp' })
|
|
175
|
+
.mockReturnValueOnce(true);
|
|
176
|
+
|
|
177
|
+
renderHook(() => usePageViewedEvent());
|
|
178
|
+
expect(mockTrackEvent).not.toHaveBeenCalled();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should fire with correct props for pdp page', () => {
|
|
182
|
+
mockUsePage.mockReturnValue({
|
|
183
|
+
variantInfo: {
|
|
184
|
+
variantType: VariantTypeEnum.Pdp,
|
|
185
|
+
productId: 'product-123',
|
|
186
|
+
collections: [],
|
|
187
|
+
numberOfReviews: 5,
|
|
188
|
+
merchantTags: [],
|
|
189
|
+
url: 'https://example.com/products/product-123',
|
|
190
|
+
},
|
|
191
|
+
isSupported: true,
|
|
192
|
+
isLoading: false,
|
|
193
|
+
});
|
|
194
|
+
mockUseAtomValue
|
|
195
|
+
.mockReturnValueOnce({ variantId: 'variant-A', variant: 'pdp' })
|
|
196
|
+
.mockReturnValueOnce(true);
|
|
197
|
+
|
|
198
|
+
renderHook(() => usePageViewedEvent());
|
|
199
|
+
|
|
200
|
+
expect(mockTrackEvent).toHaveBeenCalledTimes(1);
|
|
201
|
+
expect(mockTrackEvent).toHaveBeenCalledWith({
|
|
202
|
+
eventName: EnviveMetricsEventName.PageViewed,
|
|
203
|
+
eventProps: {
|
|
204
|
+
'context.page_type': 'pdp',
|
|
205
|
+
'context.page_id': 'product-123',
|
|
206
|
+
'context.supported': true,
|
|
207
|
+
'context.ready': true,
|
|
208
|
+
'context.page_variant_id': 'variant-A',
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should fire only once even if dependencies change', () => {
|
|
214
|
+
mockUsePage.mockReturnValue({
|
|
215
|
+
variantInfo: {
|
|
216
|
+
variantType: VariantTypeEnum.Pdp,
|
|
217
|
+
productId: 'product-123',
|
|
218
|
+
collections: [],
|
|
219
|
+
numberOfReviews: 0,
|
|
220
|
+
merchantTags: [],
|
|
221
|
+
url: '',
|
|
222
|
+
},
|
|
223
|
+
isSupported: true,
|
|
224
|
+
isLoading: false,
|
|
225
|
+
});
|
|
226
|
+
mockUseAtomValue
|
|
227
|
+
.mockReturnValueOnce({ variantId: 'v1', variant: 'pdp' })
|
|
228
|
+
.mockReturnValueOnce(true);
|
|
229
|
+
|
|
230
|
+
const { rerender } = renderHook(() => usePageViewedEvent());
|
|
231
|
+
|
|
232
|
+
mockUseAtomValue
|
|
233
|
+
.mockReturnValueOnce({ variantId: 'v1', variant: 'pdp' })
|
|
234
|
+
.mockReturnValueOnce(true);
|
|
235
|
+
|
|
236
|
+
rerender();
|
|
237
|
+
|
|
238
|
+
expect(mockTrackEvent).toHaveBeenCalledTimes(1);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should fire with full_page_sales_agent page type', () => {
|
|
242
|
+
mockUsePage.mockReturnValue({
|
|
243
|
+
variantInfo: {
|
|
244
|
+
variantType: VariantTypeEnum.FullPageSalesAgent,
|
|
245
|
+
url: 'https://example.com/agent',
|
|
246
|
+
},
|
|
247
|
+
isSupported: true,
|
|
248
|
+
isLoading: false,
|
|
249
|
+
});
|
|
250
|
+
mockUseAtomValue
|
|
251
|
+
.mockReturnValueOnce({ variantId: 'fpsa-variant', variant: 'full_page' })
|
|
252
|
+
.mockReturnValueOnce(true);
|
|
253
|
+
|
|
254
|
+
renderHook(() => usePageViewedEvent());
|
|
255
|
+
|
|
256
|
+
expect(mockTrackEvent).toHaveBeenCalledTimes(1);
|
|
257
|
+
expect(mockTrackEvent).toHaveBeenCalledWith({
|
|
258
|
+
eventName: EnviveMetricsEventName.PageViewed,
|
|
259
|
+
eventProps: {
|
|
260
|
+
'context.page_type': 'full_page_sales_agent',
|
|
261
|
+
'context.page_id': 'https://example.com/agent',
|
|
262
|
+
'context.supported': true,
|
|
263
|
+
'context.ready': true,
|
|
264
|
+
'context.page_variant_id': 'fpsa-variant',
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('should set supported and ready to false when isSupported is false', () => {
|
|
270
|
+
mockUsePage.mockReturnValue({
|
|
271
|
+
variantInfo: {
|
|
272
|
+
variantType: VariantTypeEnum.Pdp,
|
|
273
|
+
productId: 'product-456',
|
|
274
|
+
collections: [],
|
|
275
|
+
numberOfReviews: 0,
|
|
276
|
+
merchantTags: [],
|
|
277
|
+
url: '',
|
|
278
|
+
},
|
|
279
|
+
isSupported: false,
|
|
280
|
+
isLoading: false,
|
|
281
|
+
});
|
|
282
|
+
mockUseAtomValue
|
|
283
|
+
.mockReturnValueOnce({ variantId: 'v2', variant: 'pdp' })
|
|
284
|
+
.mockReturnValueOnce(true);
|
|
285
|
+
|
|
286
|
+
renderHook(() => usePageViewedEvent());
|
|
287
|
+
|
|
288
|
+
expect(mockTrackEvent).toHaveBeenCalledWith(
|
|
289
|
+
expect.objectContaining({
|
|
290
|
+
eventProps: expect.objectContaining({
|
|
291
|
+
'context.supported': false,
|
|
292
|
+
'context.ready': false,
|
|
293
|
+
}),
|
|
294
|
+
}),
|
|
295
|
+
);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './usePageViewedEvent';
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { useAtomValue } from 'jotai';
|
|
2
|
+
import { useEffect, useRef } from 'react';
|
|
3
|
+
import { VariantTypeEnum } from 'src/application/models';
|
|
4
|
+
import { hasParsedVariantInfoAtom, variantInfoAtom } from 'src/atoms/app/variant';
|
|
5
|
+
import { EnviveMetricsEventName, useAmplitude } from 'src/contexts/amplitudeContext';
|
|
6
|
+
import { usePage } from 'src/contexts/pageContext';
|
|
7
|
+
import {
|
|
8
|
+
FullPageSalesAgentVariantInfo,
|
|
9
|
+
HomeVariantInfo,
|
|
10
|
+
OtherVariantInfo,
|
|
11
|
+
PDPPageVariantInfo,
|
|
12
|
+
PLPPageVariantInfo,
|
|
13
|
+
PageVariantInfo,
|
|
14
|
+
VisitPageVariantInfo,
|
|
15
|
+
} from 'src/contexts/pageContext/types';
|
|
16
|
+
|
|
17
|
+
interface PageViewedContext {
|
|
18
|
+
page_type: string;
|
|
19
|
+
page_id: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const extractPageViewedContext = (
|
|
23
|
+
pageVariant: PageVariantInfo,
|
|
24
|
+
): PageViewedContext | null => {
|
|
25
|
+
switch (pageVariant.variantType) {
|
|
26
|
+
case VariantTypeEnum.Pdp:
|
|
27
|
+
return {
|
|
28
|
+
page_type: 'pdp',
|
|
29
|
+
page_id: (pageVariant as PDPPageVariantInfo).productId,
|
|
30
|
+
};
|
|
31
|
+
case VariantTypeEnum.Plp:
|
|
32
|
+
return {
|
|
33
|
+
page_type: 'plp',
|
|
34
|
+
page_id: (pageVariant as PLPPageVariantInfo).plpId,
|
|
35
|
+
};
|
|
36
|
+
case VariantTypeEnum.Home:
|
|
37
|
+
return {
|
|
38
|
+
page_type: 'homepage',
|
|
39
|
+
page_id: (pageVariant as HomeVariantInfo).url,
|
|
40
|
+
};
|
|
41
|
+
case VariantTypeEnum.Other:
|
|
42
|
+
return {
|
|
43
|
+
page_type: 'other',
|
|
44
|
+
page_id: (pageVariant as OtherVariantInfo).url,
|
|
45
|
+
};
|
|
46
|
+
case VariantTypeEnum.PageVisit:
|
|
47
|
+
return {
|
|
48
|
+
page_type: 'other',
|
|
49
|
+
page_id: (pageVariant as VisitPageVariantInfo).url,
|
|
50
|
+
};
|
|
51
|
+
case VariantTypeEnum.FullPageSalesAgent:
|
|
52
|
+
return {
|
|
53
|
+
page_type: 'full_page_sales_agent',
|
|
54
|
+
page_id: (pageVariant as FullPageSalesAgentVariantInfo).url,
|
|
55
|
+
};
|
|
56
|
+
default:
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Fires the [Envive] Page Viewed event once per page load when page context data is ready.
|
|
63
|
+
*
|
|
64
|
+
* This event captures page context information and is intended to eventually replace
|
|
65
|
+
* the [Amplitude] Page Viewed event.
|
|
66
|
+
*/
|
|
67
|
+
export const usePageViewedEvent = () => {
|
|
68
|
+
const hasFired = useRef(false);
|
|
69
|
+
const { trackEvent } = useAmplitude();
|
|
70
|
+
const { variantInfo: pageVariantInfo, isSupported, isLoading } = usePage();
|
|
71
|
+
const variantInfo = useAtomValue(variantInfoAtom);
|
|
72
|
+
const hasParsedVariantInfo = useAtomValue(hasParsedVariantInfoAtom);
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (hasFired.current) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (isLoading || !hasParsedVariantInfo || !pageVariantInfo) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const pageContext = extractPageViewedContext(pageVariantInfo);
|
|
84
|
+
if (!pageContext) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
hasFired.current = true;
|
|
89
|
+
|
|
90
|
+
trackEvent({
|
|
91
|
+
eventName: EnviveMetricsEventName.PageViewed,
|
|
92
|
+
eventProps: {
|
|
93
|
+
'context.page_type': pageContext.page_type,
|
|
94
|
+
'context.page_id': pageContext.page_id,
|
|
95
|
+
// context.supported and context.ready both derive from isSupported (response.ready)
|
|
96
|
+
// because the supportedEventAtom is not currently populated.
|
|
97
|
+
'context.supported': isSupported,
|
|
98
|
+
'context.ready': isSupported,
|
|
99
|
+
'context.page_variant_id': variantInfo.variantId,
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
}, [isLoading, isSupported, hasParsedVariantInfo, pageVariantInfo, variantInfo, trackEvent]);
|
|
103
|
+
};
|
|
@@ -85,12 +85,10 @@ describe('AmplitudeService', () => {
|
|
|
85
85
|
});
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
-
const validApiKey = 'abcdef1234567890abcdef1234567890';
|
|
89
|
-
|
|
90
88
|
const createService = (overrides?: Partial<AmplitudeServiceConfig>) => {
|
|
91
89
|
return new AmplitudeService({
|
|
92
90
|
userId: 'test-user-id',
|
|
93
|
-
amplitudeApiKey:
|
|
91
|
+
amplitudeApiKey: 'test-api-key',
|
|
94
92
|
dataResidency: 'US',
|
|
95
93
|
env: 'test',
|
|
96
94
|
orgId: 'test-org-id',
|
|
@@ -109,7 +107,7 @@ describe('AmplitudeService', () => {
|
|
|
109
107
|
createService();
|
|
110
108
|
|
|
111
109
|
expect(mockInit).toHaveBeenCalledWith(
|
|
112
|
-
|
|
110
|
+
'test-api-key',
|
|
113
111
|
'test-user-id',
|
|
114
112
|
expect.objectContaining({
|
|
115
113
|
serverZone: 'US',
|
|
@@ -123,7 +121,7 @@ describe('AmplitudeService', () => {
|
|
|
123
121
|
it('should not be ready if userId is missing', () => {
|
|
124
122
|
const service = new AmplitudeService({
|
|
125
123
|
userId: '',
|
|
126
|
-
amplitudeApiKey:
|
|
124
|
+
amplitudeApiKey: 'test-api-key',
|
|
127
125
|
dataResidency: 'US',
|
|
128
126
|
env: 'test',
|
|
129
127
|
orgId: 'test-org-id',
|
|
@@ -159,7 +157,7 @@ describe('AmplitudeService', () => {
|
|
|
159
157
|
it('should not be ready if featureFlagService is missing', () => {
|
|
160
158
|
const service = new AmplitudeService({
|
|
161
159
|
userId: 'test-user-id',
|
|
162
|
-
amplitudeApiKey:
|
|
160
|
+
amplitudeApiKey: 'test-api-key',
|
|
163
161
|
dataResidency: 'US',
|
|
164
162
|
env: 'test',
|
|
165
163
|
orgId: 'test-org-id',
|
|
@@ -179,54 +177,6 @@ describe('AmplitudeService', () => {
|
|
|
179
177
|
|
|
180
178
|
expect(service.isReady).toBe(true);
|
|
181
179
|
});
|
|
182
|
-
|
|
183
|
-
it('should not set isMockMode when all required config is provided', () => {
|
|
184
|
-
const service = createService();
|
|
185
|
-
|
|
186
|
-
expect(service.isMockMode).toBe(false);
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
describe('Mock mode', () => {
|
|
191
|
-
it('should set isMockMode and not initialize the client when API key is not a valid hex string', () => {
|
|
192
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
193
|
-
|
|
194
|
-
const service = createService({ amplitudeApiKey: 'not-a-valid-key' });
|
|
195
|
-
|
|
196
|
-
expect(service.isReady).toBe(false);
|
|
197
|
-
expect(service.isMockMode).toBe(true);
|
|
198
|
-
expect(mockInit).not.toHaveBeenCalled();
|
|
199
|
-
|
|
200
|
-
consoleSpy.mockRestore();
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it('should print a console.log message when a mock API key is detected', () => {
|
|
204
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
205
|
-
|
|
206
|
-
createService({ amplitudeApiKey: 'not-a-valid-key' });
|
|
207
|
-
|
|
208
|
-
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Mock API key detected'));
|
|
209
|
-
|
|
210
|
-
consoleSpy.mockRestore();
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it('should not set isMockMode when API key is empty — service is simply not ready', () => {
|
|
214
|
-
const service = createService({ amplitudeApiKey: '' });
|
|
215
|
-
|
|
216
|
-
expect(service.isReady).toBe(false);
|
|
217
|
-
expect(service.isMockMode).toBe(false);
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
it('should not call the Amplitude client when trackEvent is called in mock mode', async () => {
|
|
221
|
-
const service = createService({ amplitudeApiKey: 'not-a-valid-key' });
|
|
222
|
-
|
|
223
|
-
await service.trackEvent({
|
|
224
|
-
eventName: SpiffyMetricsEventName.ChatUserMessageInput,
|
|
225
|
-
eventProps: { message: 'test' },
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
expect(mockTrack).not.toHaveBeenCalled();
|
|
229
|
-
});
|
|
230
180
|
});
|
|
231
181
|
|
|
232
182
|
describe('trackEvent', () => {
|
|
@@ -319,7 +269,7 @@ describe('AmplitudeService', () => {
|
|
|
319
269
|
it('should not track if client is not initialized', async () => {
|
|
320
270
|
const service = new AmplitudeService({
|
|
321
271
|
userId: '',
|
|
322
|
-
amplitudeApiKey:
|
|
272
|
+
amplitudeApiKey: 'test-api-key',
|
|
323
273
|
dataResidency: 'US',
|
|
324
274
|
env: 'test',
|
|
325
275
|
orgId: 'test-org-id',
|
|
@@ -469,7 +419,7 @@ describe('AmplitudeService', () => {
|
|
|
469
419
|
it('should fall back to window.localStorage when getLocalStorageItem is not provided', async () => {
|
|
470
420
|
const service = new AmplitudeService({
|
|
471
421
|
userId: 'test-user-id',
|
|
472
|
-
amplitudeApiKey:
|
|
422
|
+
amplitudeApiKey: 'test-api-key',
|
|
473
423
|
dataResidency: 'US',
|
|
474
424
|
env: 'test',
|
|
475
425
|
orgId: 'test-org-id',
|
|
@@ -51,8 +51,6 @@ export class AmplitudeService {
|
|
|
51
51
|
|
|
52
52
|
private config: AmplitudeServiceConfig;
|
|
53
53
|
|
|
54
|
-
private _isMockMode: boolean = false;
|
|
55
|
-
|
|
56
54
|
constructor(config: AmplitudeServiceConfig) {
|
|
57
55
|
this.config = config;
|
|
58
56
|
this.initialize();
|
|
@@ -67,10 +65,6 @@ export class AmplitudeService {
|
|
|
67
65
|
);
|
|
68
66
|
}
|
|
69
67
|
|
|
70
|
-
get isMockMode(): boolean {
|
|
71
|
-
return this._isMockMode;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
68
|
private getLocalStorageItem(key: string): string | null {
|
|
75
69
|
if (this.config.getLocalStorageItem) {
|
|
76
70
|
return this.config.getLocalStorageItem(key);
|
|
@@ -219,8 +213,8 @@ export class AmplitudeService {
|
|
|
219
213
|
return enrichment;
|
|
220
214
|
}
|
|
221
215
|
|
|
222
|
-
|
|
223
|
-
return
|
|
216
|
+
static isDummyApiKey(apiKey: string): boolean {
|
|
217
|
+
return !/^[0-9a-f]{32}$/i.test(apiKey);
|
|
224
218
|
}
|
|
225
219
|
|
|
226
220
|
private initialize(): void {
|
|
@@ -238,12 +232,8 @@ export class AmplitudeService {
|
|
|
238
232
|
return;
|
|
239
233
|
}
|
|
240
234
|
|
|
241
|
-
if (
|
|
242
|
-
|
|
243
|
-
// eslint-disable-next-line no-console
|
|
244
|
-
console.log(
|
|
245
|
-
'[AmplitudeService] Mock API key detected — running in mock mode, no events will be sent to Amplitude.',
|
|
246
|
-
);
|
|
235
|
+
if (AmplitudeService.isDummyApiKey(this.config.amplitudeApiKey)) {
|
|
236
|
+
logger.logDebug('AmplitudeService is using a dummy API key — analytics will not be tracked');
|
|
247
237
|
return;
|
|
248
238
|
}
|
|
249
239
|
|