@envive-ai/react-hooks 0.3.19 → 0.3.21
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/models/guards/api/isApiPDPEventAttributes.cjs +2 -2
- package/dist/application/models/guards/api/isApiPDPEventAttributes.js +2 -2
- package/dist/atoms/app/index.d.ts +1 -1
- package/dist/atoms/chat/chatState.d.cts +18 -18
- package/dist/atoms/chat/chatState.d.ts +1 -1
- package/dist/atoms/chat/form.d.cts +2 -2
- package/dist/atoms/chat/form.d.ts +2 -2
- package/dist/atoms/chat/index.d.cts +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 +6 -6
- 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 +2 -2
- package/dist/atoms/envive/enviveConfig.d.cts +13 -13
- 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.ts +1 -1
- package/dist/atoms/widget/chatPreviewLoading.d.cts +2 -2
- package/dist/atoms/widget/chatPreviewLoading.d.ts +2 -2
- package/dist/contexts/enviveContext/enviveContext.cjs +41 -7
- package/dist/contexts/enviveContext/enviveContext.d.cts +8 -2
- package/dist/contexts/enviveContext/enviveContext.d.ts +8 -2
- package/dist/contexts/enviveContext/enviveContext.js +42 -8
- package/dist/contexts/enviveContext/index.d.cts +2 -2
- package/dist/contexts/enviveContext/index.d.ts +2 -2
- package/dist/contexts/enviveContext/types.d.ts +1 -1
- package/dist/contexts/pageContext/pageContext.cjs +78 -3
- package/dist/contexts/pageContext/pageContext.d.cts +3 -1
- package/dist/contexts/pageContext/pageContext.d.ts +3 -1
- package/dist/contexts/pageContext/pageContext.js +81 -6
- 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/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/utils.d.cts +1 -1
- package/package.json +5 -1
- package/src/application/models/guards/api/isApiPDPEventAttributes.ts +1 -1
- package/src/contexts/enviveContext/enviveContext.tsx +71 -7
- package/src/contexts/pageContext/__tests__/pageContext.test.tsx +6 -0
- package/src/contexts/pageContext/pageContext.tsx +79 -2
- 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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ReactNode, useEffect, useMemo, useState } from 'react';
|
|
1
|
+
import { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
|
|
3
3
|
import { OrgConfigFeatureGate } from 'src/application/models/api/orgConfigResults';
|
|
4
4
|
import { EnviveConfigService, EnviveServiceConfig } from 'src/services/enviveConfigService';
|
|
@@ -30,6 +30,12 @@ interface AgentWrapperProps {
|
|
|
30
30
|
enabledAgents: EnviveAgent[];
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
export interface ExtensionStatusCallbacks {
|
|
34
|
+
onInjectionLoading?: () => Promise<unknown> | void;
|
|
35
|
+
onInjectionSuccess?: () => Promise<unknown> | void;
|
|
36
|
+
onInjectionError?: (error: Error) => Promise<unknown> | void;
|
|
37
|
+
}
|
|
38
|
+
|
|
33
39
|
interface EnviveProviderProps extends AgentWrapperProps {
|
|
34
40
|
amplitudeApiKey?: string;
|
|
35
41
|
dataResidency?: string;
|
|
@@ -58,6 +64,7 @@ interface EnviveProviderProps extends AgentWrapperProps {
|
|
|
58
64
|
overrideConfig?: GraphQlConfigValues;
|
|
59
65
|
mockSalesAgentData?: any;
|
|
60
66
|
hardcopyOverride?: Partial<Record<WidgetTypeV3, HardcopyResponse>>;
|
|
67
|
+
extensionService?: ExtensionStatusCallbacks;
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
const SearchAgentWrapper: React.FC<AgentWrapperProps> = ({ children, enabledAgents }) => {
|
|
@@ -108,9 +115,34 @@ export const EnviveProvider: React.FC<EnviveProviderProps> = ({
|
|
|
108
115
|
requestV3Config = false,
|
|
109
116
|
mockSalesAgentData,
|
|
110
117
|
hardcopyOverride,
|
|
118
|
+
extensionService,
|
|
111
119
|
...config
|
|
112
120
|
}) => {
|
|
113
121
|
const [userId, setUserId] = useState<string>('');
|
|
122
|
+
const didReportSuccessRef = useRef(false);
|
|
123
|
+
|
|
124
|
+
const invokeExtensionCallback = useCallback((callback?: () => Promise<unknown> | void) => {
|
|
125
|
+
if (!callback) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
Promise.resolve(callback()).catch(error => {
|
|
130
|
+
logger.logDebug('Extension status callback failed', error);
|
|
131
|
+
});
|
|
132
|
+
}, []);
|
|
133
|
+
|
|
134
|
+
const invokeExtensionErrorCallback = useCallback(
|
|
135
|
+
(error: Error) => {
|
|
136
|
+
if (!extensionService?.onInjectionError) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
Promise.resolve(extensionService.onInjectionError(error)).catch(callbackError => {
|
|
141
|
+
logger.logDebug('Extension error callback failed', callbackError);
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
[extensionService],
|
|
145
|
+
);
|
|
114
146
|
|
|
115
147
|
const userIdService = useMemo(
|
|
116
148
|
() =>
|
|
@@ -133,6 +165,8 @@ export const EnviveProvider: React.FC<EnviveProviderProps> = ({
|
|
|
133
165
|
const [enviveServiceConfig, setEnviveServiceConfig] = useState<EnviveServiceConfig | null>(null);
|
|
134
166
|
|
|
135
167
|
useEffect(() => {
|
|
168
|
+
invokeExtensionCallback(extensionService?.onInjectionLoading);
|
|
169
|
+
|
|
136
170
|
// Really not happy with this approach, but I'm seeing 429 errors in the tests
|
|
137
171
|
// because of the rate limiting on the API.
|
|
138
172
|
const enviveConfigService =
|
|
@@ -144,11 +178,38 @@ export const EnviveProvider: React.FC<EnviveProviderProps> = ({
|
|
|
144
178
|
namespace: namespace || '',
|
|
145
179
|
source: source || '',
|
|
146
180
|
});
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
181
|
+
const fetchEnviveConfig = async () => {
|
|
182
|
+
try {
|
|
183
|
+
const newConfig = await enviveConfigService.getEnviveConfig();
|
|
184
|
+
setEnviveServiceConfig(newConfig);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
const resolvedError =
|
|
187
|
+
error instanceof Error ? error : new Error('Failed to load envive config');
|
|
188
|
+
logger.logError('Failed to initialize EnviveProvider', resolvedError);
|
|
189
|
+
invokeExtensionErrorCallback(resolvedError);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
fetchEnviveConfig();
|
|
194
|
+
}, [
|
|
195
|
+
config,
|
|
196
|
+
userId,
|
|
197
|
+
namespace,
|
|
198
|
+
source,
|
|
199
|
+
inputEnviveConfigService,
|
|
200
|
+
extensionService,
|
|
201
|
+
invokeExtensionCallback,
|
|
202
|
+
invokeExtensionErrorCallback,
|
|
203
|
+
]);
|
|
204
|
+
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
if (!enviveServiceConfig || didReportSuccessRef.current) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
didReportSuccessRef.current = true;
|
|
211
|
+
invokeExtensionCallback(extensionService?.onInjectionSuccess);
|
|
212
|
+
}, [enviveServiceConfig, extensionService, invokeExtensionCallback]);
|
|
152
213
|
|
|
153
214
|
const enviveConfig = useMemo(
|
|
154
215
|
() => ({
|
|
@@ -182,7 +243,10 @@ export const EnviveProvider: React.FC<EnviveProviderProps> = ({
|
|
|
182
243
|
<FeatureFlagServiceProvider featureFlagService={featureFlagService}>
|
|
183
244
|
<UserIdentityProvider userIdService={userIdService}>
|
|
184
245
|
<AmplitudeProvider externalAmplitudeService={inputAmplitudeService}>
|
|
185
|
-
<PageProvider
|
|
246
|
+
<PageProvider
|
|
247
|
+
previewMode={previewMode}
|
|
248
|
+
onUrlResolverNotReady={extensionService?.onInjectionError}
|
|
249
|
+
>
|
|
186
250
|
<WidgetConfigProvider>
|
|
187
251
|
<HardcopyProvider hardcopyOverride={hardcopyOverride}>
|
|
188
252
|
<EnviveCssProvider>
|
|
@@ -27,6 +27,12 @@ vi.mock('src/application/commerce-api', () => ({
|
|
|
27
27
|
},
|
|
28
28
|
}));
|
|
29
29
|
|
|
30
|
+
// Mock useAmplitude (PageProvider dispatches page viewed event internally)
|
|
31
|
+
vi.mock('src/contexts/amplitudeContext', () => ({
|
|
32
|
+
EnviveMetricsEventName: { PageViewed: 'Page Viewed' },
|
|
33
|
+
useAmplitude: () => ({ trackEvent: vi.fn() }),
|
|
34
|
+
}));
|
|
35
|
+
|
|
30
36
|
// Component that uses the usePage hook
|
|
31
37
|
const MockComponent: React.FC = () => {
|
|
32
38
|
const context = usePage();
|
|
@@ -5,19 +5,23 @@ import {
|
|
|
5
5
|
useContext,
|
|
6
6
|
useEffect,
|
|
7
7
|
useMemo,
|
|
8
|
+
useRef,
|
|
8
9
|
useState,
|
|
9
10
|
} from 'react';
|
|
10
11
|
import {
|
|
11
12
|
UrlResolverResponse,
|
|
13
|
+
hasParsedVariantInfoAtom,
|
|
12
14
|
pageUserEventAtom,
|
|
13
15
|
pageVariantInfoAtom,
|
|
14
16
|
urlResolverAtom,
|
|
17
|
+
variantInfoAtom,
|
|
15
18
|
} from 'src/atoms/app/variant';
|
|
16
|
-
import { useAtom } from 'jotai';
|
|
19
|
+
import { useAtom, useAtomValue } from 'jotai';
|
|
17
20
|
import CommerceApiClient from 'src/application/commerce-api';
|
|
18
21
|
import Logger from 'src/application/logging/logger';
|
|
19
22
|
import { PageVisitCategory, UserEventCategory } from '@spiffy-ai/commerce-api-client';
|
|
20
23
|
import { VariantTypeEnum } from 'src/application/models';
|
|
24
|
+
import { EnviveMetricsEventName, useAmplitude } from 'src/contexts/amplitudeContext';
|
|
21
25
|
import { mapApiUserEventToUserEvent, mapUrlResolverResponseToVariantInfo } from './mapping';
|
|
22
26
|
import { PageDetails } from './types';
|
|
23
27
|
|
|
@@ -35,13 +39,17 @@ const PageContext = createContext<
|
|
|
35
39
|
export const PageProvider: React.FC<{
|
|
36
40
|
children: ReactNode;
|
|
37
41
|
previewMode: boolean;
|
|
38
|
-
|
|
42
|
+
/** When URL resolver returns `ready: false`, invoked for extension/injection failure reporting. */
|
|
43
|
+
onUrlResolverNotReady?: (error: Error) => Promise<unknown> | void;
|
|
44
|
+
}> = ({ children, previewMode = false, onUrlResolverNotReady }) => {
|
|
39
45
|
const [pageUrl, setPageUrl] = useState<string | undefined>(undefined);
|
|
40
46
|
const [urlResolverResponse, setUrlResolverResponse] = useAtom(urlResolverAtom);
|
|
41
47
|
const [isLoading, setIsLoading] = useState(false);
|
|
42
48
|
const [variantInfo, setVariantInfo] = useAtom(pageVariantInfoAtom);
|
|
43
49
|
const [userEvent, setUserEvent] = useAtom(pageUserEventAtom);
|
|
44
50
|
const [isSupported, setIsSupported] = useState(false);
|
|
51
|
+
const lastNotReadyUrlRef = useRef<string | null>(null);
|
|
52
|
+
|
|
45
53
|
useEffect(() => {
|
|
46
54
|
setPageUrl(window.location.href);
|
|
47
55
|
}, []);
|
|
@@ -72,11 +80,13 @@ export const PageProvider: React.FC<{
|
|
|
72
80
|
if (response.ready) {
|
|
73
81
|
const newVariantInfo = mapUrlResolverResponseToVariantInfo(url, response);
|
|
74
82
|
const newUserEvent = mapApiUserEventToUserEvent(response.user_event ?? undefined);
|
|
83
|
+
logger.logDebug('setting variant info and user event', { newVariantInfo, newUserEvent });
|
|
75
84
|
setVariantInfo(newVariantInfo);
|
|
76
85
|
setUserEvent(newUserEvent);
|
|
77
86
|
} else {
|
|
78
87
|
setUserEvent(undefined);
|
|
79
88
|
setVariantInfo(undefined);
|
|
89
|
+
logger.logDebug('set variant info and user event to undefined', { variantInfo, userEvent });
|
|
80
90
|
}
|
|
81
91
|
},
|
|
82
92
|
[setVariantInfo, setUserEvent],
|
|
@@ -104,6 +114,16 @@ export const PageProvider: React.FC<{
|
|
|
104
114
|
setVariantFromUrlResolver(cleansedUrl, response);
|
|
105
115
|
setIsLoading(false);
|
|
106
116
|
setIsSupported(response.ready);
|
|
117
|
+
|
|
118
|
+
if (response.ready) {
|
|
119
|
+
lastNotReadyUrlRef.current = null;
|
|
120
|
+
} else if (onUrlResolverNotReady && lastNotReadyUrlRef.current !== cleansedUrl) {
|
|
121
|
+
lastNotReadyUrlRef.current = cleansedUrl;
|
|
122
|
+
const err = new Error(`URL resolver returned ready=false for ${cleansedUrl}`);
|
|
123
|
+
Promise.resolve(onUrlResolverNotReady(err)).catch(callbackError => {
|
|
124
|
+
logger.logDebug('onUrlResolverNotReady callback failed', callbackError);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
107
127
|
} catch (e) {
|
|
108
128
|
setIsLoading(false);
|
|
109
129
|
logger.logError('Failed to resolve page URL', e, { pageUrl });
|
|
@@ -118,8 +138,65 @@ export const PageProvider: React.FC<{
|
|
|
118
138
|
userEvent,
|
|
119
139
|
variantInfo,
|
|
120
140
|
previewMode,
|
|
141
|
+
onUrlResolverNotReady,
|
|
121
142
|
]);
|
|
122
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
|
+
|
|
123
200
|
const setPageUrlStable = useCallback((url: string) => {
|
|
124
201
|
setPageUrl(url);
|
|
125
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
|
+
};
|