@envive-ai/react-hooks 0.3.21 → 0.3.23
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/featureGates.cjs +2 -1
- package/dist/application/models/featureGates.d.cts +2 -1
- package/dist/application/models/featureGates.d.ts +2 -1
- package/dist/application/models/featureGates.js +2 -1
- package/dist/atoms/app/index.d.cts +7 -7
- package/dist/atoms/app/variant.d.cts +6 -6
- package/dist/atoms/app/variant.d.ts +6 -6
- package/dist/atoms/chat/chatState.cjs +3 -1
- package/dist/atoms/chat/chatState.d.cts +22 -19
- package/dist/atoms/chat/chatState.d.ts +22 -19
- package/dist/atoms/chat/chatState.js +3 -2
- package/dist/atoms/chat/form.d.cts +3 -3
- package/dist/atoms/chat/form.d.ts +2 -2
- package/dist/atoms/chat/index.cjs +1 -0
- package/dist/atoms/chat/index.d.cts +4 -4
- package/dist/atoms/chat/index.d.ts +4 -4
- package/dist/atoms/chat/index.js +2 -2
- 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 +7 -7
- 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 +3 -3
- 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 +2 -2
- package/dist/atoms/chat/suggestions.d.cts +3 -3
- 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 +1 -1
- 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/types.d.cts +1 -1
- package/dist/atoms/search/types.d.ts +1 -1
- package/dist/atoms/search/utils.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/amplitudeContext/amplitudeContext.cjs +9 -3
- package/dist/contexts/amplitudeContext/amplitudeContext.d.cts +2 -1
- package/dist/contexts/amplitudeContext/amplitudeContext.d.ts +2 -1
- package/dist/contexts/amplitudeContext/amplitudeContext.js +9 -3
- package/dist/contexts/enviveContext/enviveContext.cjs +3 -3
- package/dist/contexts/enviveContext/enviveContext.js +3 -3
- package/dist/contexts/enviveContext/types.d.ts +1 -1
- package/dist/contexts/hardcopyContext/hardcopyContext.cjs +5 -3
- package/dist/contexts/hardcopyContext/hardcopyContext.js +5 -3
- package/dist/contexts/salesAgentContext/chatAPI.cjs +12 -5
- package/dist/contexts/salesAgentContext/chatAPI.js +13 -6
- package/dist/contexts/systemSettingsContext/systemSettingsContext.d.cts +2 -2
- package/dist/contexts/types.d.cts +1 -1
- package/dist/contexts/typesV3.cjs +1 -1
- package/dist/contexts/typesV3.d.cts +2 -1
- package/dist/contexts/typesV3.d.ts +2 -1
- package/dist/contexts/typesV3.js +1 -1
- package/dist/hooks/GrabAndScroll/useGrabAndScroll.d.cts +2 -2
- package/dist/hooks/Search/useSearch.cjs +12 -4
- package/dist/hooks/Search/useSearch.js +12 -4
- package/dist/hooks/Search/useSearchInput.cjs +1 -1
- package/dist/hooks/Search/useSearchInput.js +1 -1
- package/dist/hooks/SystemSettingsContext/useSystemSettingsContext.d.cts +2 -2
- package/dist/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.cjs +26 -27
- package/dist/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.d.cts +8 -8
- package/dist/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.d.ts +8 -8
- package/dist/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.js +27 -28
- package/dist/hooks/WidgetInteraction/types.cjs +29 -1
- package/dist/hooks/WidgetInteraction/types.d.cts +17 -3
- package/dist/hooks/WidgetInteraction/types.d.ts +17 -3
- package/dist/hooks/WidgetInteraction/types.js +28 -2
- package/dist/hooks/WidgetInteraction/useWidgetInteraction.cjs +6 -2
- package/dist/hooks/WidgetInteraction/useWidgetInteraction.js +6 -2
- package/dist/hooks/utils.d.cts +1 -1
- package/dist/hooks/utils.d.ts +1 -1
- package/dist/services/amplitudeService/amplitudeService.cjs +9 -1
- package/dist/services/amplitudeService/amplitudeService.d.cts +2 -1
- package/dist/services/amplitudeService/amplitudeService.d.ts +2 -1
- package/dist/services/amplitudeService/amplitudeService.js +9 -1
- package/dist/services/ga4ProjectionService/ga4EventSchema.cjs +31 -27
- package/dist/services/ga4ProjectionService/ga4EventSchema.js +31 -27
- package/dist/services/ga4ProjectionService/ga4ProjectionService.cjs +31 -5
- package/dist/services/ga4ProjectionService/ga4ProjectionService.js +31 -5
- package/package.json +1 -1
- package/src/application/models/featureGates.ts +1 -0
- package/src/atoms/chat/chatState.ts +1 -0
- package/src/contexts/amplitudeContext/__tests__/amplitudeContext.test.tsx +31 -27
- package/src/contexts/amplitudeContext/amplitudeContext.tsx +5 -2
- package/src/contexts/hardcopyContext/hardcopyContext.tsx +10 -2
- package/src/contexts/pageContext/__tests__/pageContext.test.tsx +10 -0
- package/src/contexts/salesAgentContext/chatAPI.ts +6 -2
- package/src/contexts/typesV3.ts +1 -0
- package/src/hooks/Search/__tests__/useSearch.test.tsx +0 -4
- package/src/hooks/Search/useSearch.tsx +14 -8
- package/src/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.ts +36 -35
- package/src/hooks/WidgetInteraction/types.ts +35 -2
- package/src/hooks/WidgetInteraction/useWidgetInteraction.ts +3 -1
- package/src/services/amplitudeService/__tests__/amplitudeService.test.ts +69 -6
- package/src/services/amplitudeService/amplitudeService.ts +13 -0
- package/src/services/ga4ProjectionService/__tests__/ga4ProjectionService.test.ts +110 -49
- package/src/services/ga4ProjectionService/ga4EventSchema.ts +35 -27
- package/src/services/ga4ProjectionService/ga4ProjectionService.ts +60 -6
|
@@ -27,8 +27,8 @@ import { SearchResponseProduct } from '@spiffy-ai/commerce-api-client';
|
|
|
27
27
|
import { SearchFilterDatum, SelectFilterItem } from 'src/types/search-filter-types';
|
|
28
28
|
import { orgShortNameAtom } from 'src/atoms/envive/enviveConfig';
|
|
29
29
|
import { FrontendConfig, SearchResponseProductAttributes } from 'src/application/models';
|
|
30
|
+
import { useIntersection } from 'src/hooks/Intersection/useIntersection';
|
|
30
31
|
import { SearchResultsState, getSearchResultsState } from '../utils';
|
|
31
|
-
import { useTrackComponentVisibleEvent } from '../TrackComponentVisibleEvent';
|
|
32
32
|
import { useSearchInput } from './useSearchInput';
|
|
33
33
|
import { useNewOrgConfig } from '../NewOrgConfig';
|
|
34
34
|
import { useRecommendedProducts } from './useRecommendedProducts';
|
|
@@ -292,13 +292,19 @@ export const useSearch = ({
|
|
|
292
292
|
scrollToTop();
|
|
293
293
|
}, [setProductSorting, clearFilters, scrollToTop]);
|
|
294
294
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
{
|
|
300
|
-
|
|
301
|
-
|
|
295
|
+
const searchResultsVisible = useIntersection(searchResultsRef as RefObject<HTMLElement>, '0px');
|
|
296
|
+
const hasTrackedSearchComponentVisible = useRef(false);
|
|
297
|
+
|
|
298
|
+
useEffect(() => {
|
|
299
|
+
if (!searchResultsVisible || hasTrackedSearchComponentVisible.current) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
hasTrackedSearchComponentVisible.current = true;
|
|
303
|
+
trackEvent({
|
|
304
|
+
eventName: SpiffyMetricsEventName.SearchComponentVisible,
|
|
305
|
+
eventProps: {},
|
|
306
|
+
});
|
|
307
|
+
}, [searchResultsVisible, trackEvent]);
|
|
302
308
|
|
|
303
309
|
useEffect(() => {
|
|
304
310
|
if (
|
|
@@ -1,54 +1,55 @@
|
|
|
1
|
+
import { useAtomValue } from 'jotai';
|
|
1
2
|
import { RefObject, useEffect, useRef } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
import { useIntersection } from 'src/hooks/Intersection/useIntersection';
|
|
3
|
+
import { pageVariantInfoAtom } from 'src/atoms/app';
|
|
4
4
|
import { useAmplitude } from 'src/contexts/amplitudeContext/amplitudeContext';
|
|
5
|
-
import {
|
|
5
|
+
import { PageVariantInfo } from 'src/contexts/pageContext/types';
|
|
6
|
+
import { useIntersection } from 'src/hooks/Intersection/useIntersection';
|
|
7
|
+
import { extractPageContext } from 'src/hooks/WidgetInteraction/utils';
|
|
8
|
+
import {
|
|
9
|
+
EnviveMetricsEventName,
|
|
10
|
+
SpiffyMetricsEventName,
|
|
11
|
+
} from 'src/services/amplitudeService/amplitudeService';
|
|
6
12
|
|
|
7
13
|
/**
|
|
8
|
-
* Tracks a component and logs
|
|
14
|
+
* Tracks a component and logs `SpiffyMetricsEventName.ChatComponentVisible` when visible.
|
|
9
15
|
*
|
|
10
|
-
* @param component - The component to track.
|
|
11
16
|
* @param element - The element to track visibility of.
|
|
12
|
-
* @param eventProps -
|
|
13
|
-
* @param
|
|
17
|
+
* @param eventProps - Properties to include with the event.
|
|
18
|
+
* @param rootMargin - Root margin for the intersection observer (defaults to 0px).
|
|
19
|
+
* @param enabled - Whether tracking is enabled (defaults to true).
|
|
14
20
|
*/
|
|
15
21
|
export const useTrackComponentVisibleEvent = (
|
|
16
|
-
component: SpiffyWidgets,
|
|
17
22
|
element: RefObject<HTMLElement>,
|
|
18
23
|
eventProps?: Record<string, unknown>,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
rootMargin: string = '0px',
|
|
25
|
+
enabled: boolean = true,
|
|
26
|
+
): { isVisible: boolean } => {
|
|
27
|
+
const isVisible = useIntersection(element, rootMargin);
|
|
22
28
|
const hasTrackedEvent = useRef(false);
|
|
23
29
|
const { trackEvent } = useAmplitude();
|
|
24
|
-
|
|
25
|
-
const componentProps = (() => {
|
|
26
|
-
if (eventName === SpiffyMetricsEventName.ChatComponentVisible) {
|
|
27
|
-
return {
|
|
28
|
-
chat_component: component,
|
|
29
|
-
...eventProps,
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
if (eventName === SpiffyMetricsEventName.SearchComponentVisible) {
|
|
33
|
-
return {
|
|
34
|
-
search_component: component,
|
|
35
|
-
...eventProps,
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
// Default case for other event types
|
|
39
|
-
return {
|
|
40
|
-
component,
|
|
41
|
-
...eventProps,
|
|
42
|
-
};
|
|
43
|
-
})();
|
|
30
|
+
const variantInfo = useAtomValue(pageVariantInfoAtom);
|
|
44
31
|
|
|
45
32
|
useEffect(() => {
|
|
46
|
-
if (
|
|
33
|
+
if (!enabled || hasTrackedEvent.current) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (isVisible) {
|
|
37
|
+
trackEvent({
|
|
38
|
+
eventName: SpiffyMetricsEventName.ChatComponentVisible,
|
|
39
|
+
eventProps,
|
|
40
|
+
});
|
|
47
41
|
trackEvent({
|
|
48
|
-
eventName,
|
|
49
|
-
eventProps:
|
|
42
|
+
eventName: EnviveMetricsEventName.WidgetRendered,
|
|
43
|
+
eventProps: {
|
|
44
|
+
...eventProps,
|
|
45
|
+
trigger: {
|
|
46
|
+
widget: eventProps?.widget_type,
|
|
47
|
+
},
|
|
48
|
+
context: variantInfo ? extractPageContext(variantInfo as PageVariantInfo) : null,
|
|
49
|
+
},
|
|
50
50
|
});
|
|
51
51
|
hasTrackedEvent.current = true;
|
|
52
52
|
}
|
|
53
|
-
}, [
|
|
53
|
+
}, [enabled, isVisible, eventProps, trackEvent]);
|
|
54
|
+
return { isVisible };
|
|
54
55
|
};
|
|
@@ -5,11 +5,12 @@ export interface WidgetInteractionContext {
|
|
|
5
5
|
page_id: string;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
interface WidgetInteractionTrigger {
|
|
8
|
+
export interface WidgetInteractionTrigger {
|
|
9
9
|
interaction_id?: string;
|
|
10
10
|
widget: WidgetInteractionComponent;
|
|
11
11
|
widget_interaction: WidgetInteractionType;
|
|
12
12
|
widget_interaction_data?: WidgetInteractionData | null;
|
|
13
|
+
interaction_class?: InteractionClass;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export interface InternalWidgetInteraction {
|
|
@@ -61,8 +62,35 @@ export enum WidgetInteractionType {
|
|
|
61
62
|
REVIEW_CARD_CLICKED = 'review_card_clicked',
|
|
62
63
|
MESSAGE_SUBMITTED = 'message_submitted',
|
|
63
64
|
MANUAL_SCROLL_TO_BOTTOM = 'manual_scroll_to_bottom',
|
|
65
|
+
VOICE_TRANSCRIPTION_STARTED = 'voice_transcription_started',
|
|
66
|
+
VOICE_TRANSCRIPTION_COMPLETED = 'voice_transcription_completed',
|
|
64
67
|
}
|
|
65
68
|
|
|
69
|
+
export enum InteractionClass {
|
|
70
|
+
PASSIVE = 'passive', // No user commitment — hover, scroll, visibility triggers
|
|
71
|
+
NAVIGATIONAL = 'navigational', // Opens/closes something — often a side effect of intent
|
|
72
|
+
INTENTIONAL = 'intentional', // Deliberate user action — clicks, submits, typed queries
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const INTERACTION_TYPE_CLASS: Record<WidgetInteractionType, InteractionClass> = {
|
|
76
|
+
[WidgetInteractionType.QUERY_INPUT_CLICKED]: InteractionClass.INTENTIONAL,
|
|
77
|
+
[WidgetInteractionType.SUGGESTION_CLICKED]: InteractionClass.INTENTIONAL,
|
|
78
|
+
[WidgetInteractionType.WIDGET_CLICKED]: InteractionClass.INTENTIONAL,
|
|
79
|
+
[WidgetInteractionType.WIDGET_HOVERED]: InteractionClass.PASSIVE,
|
|
80
|
+
[WidgetInteractionType.WIDGET_EXPANDED]: InteractionClass.NAVIGATIONAL,
|
|
81
|
+
[WidgetInteractionType.WIDGET_COLLAPSED]: InteractionClass.NAVIGATIONAL,
|
|
82
|
+
[WidgetInteractionType.SUGGESTION_SCROLLED]: InteractionClass.PASSIVE,
|
|
83
|
+
[WidgetInteractionType.LINK_CLICKED]: InteractionClass.INTENTIONAL,
|
|
84
|
+
[WidgetInteractionType.PRODUCT_CARD_CLICKED]: InteractionClass.INTENTIONAL,
|
|
85
|
+
[WidgetInteractionType.TEXT_LINK_CLICKED]: InteractionClass.INTENTIONAL,
|
|
86
|
+
[WidgetInteractionType.ARTICLE_LINK_CLICKED]: InteractionClass.INTENTIONAL,
|
|
87
|
+
[WidgetInteractionType.REVIEW_CARD_CLICKED]: InteractionClass.INTENTIONAL,
|
|
88
|
+
[WidgetInteractionType.MESSAGE_SUBMITTED]: InteractionClass.INTENTIONAL,
|
|
89
|
+
[WidgetInteractionType.MANUAL_SCROLL_TO_BOTTOM]: InteractionClass.PASSIVE,
|
|
90
|
+
[WidgetInteractionType.VOICE_TRANSCRIPTION_STARTED]: InteractionClass.INTENTIONAL,
|
|
91
|
+
[WidgetInteractionType.VOICE_TRANSCRIPTION_COMPLETED]: InteractionClass.INTENTIONAL,
|
|
92
|
+
};
|
|
93
|
+
|
|
66
94
|
export type URL = {
|
|
67
95
|
url: string;
|
|
68
96
|
};
|
|
@@ -127,6 +155,10 @@ export type MessageSubmitted = {
|
|
|
127
155
|
message_submitted: Request;
|
|
128
156
|
};
|
|
129
157
|
|
|
158
|
+
export type VoiceTranscription = {
|
|
159
|
+
transcription: string;
|
|
160
|
+
};
|
|
161
|
+
|
|
130
162
|
export type WidgetInteractionData =
|
|
131
163
|
| SuggestionScrolled
|
|
132
164
|
| SuggestionClicked
|
|
@@ -137,4 +169,5 @@ export type WidgetInteractionData =
|
|
|
137
169
|
| ArticleLinkClicked
|
|
138
170
|
| ProductCardClicked
|
|
139
171
|
| ReviewCardClicked
|
|
140
|
-
| MessageSubmitted
|
|
172
|
+
| MessageSubmitted
|
|
173
|
+
| VoiceTranscription;
|
|
@@ -3,7 +3,7 @@ import { pageVariantInfoAtom } from 'src/atoms/app';
|
|
|
3
3
|
import { useAmplitude } from 'src/contexts/amplitudeContext';
|
|
4
4
|
import { PageVariantInfo } from 'src/contexts/pageContext/types';
|
|
5
5
|
import { v4 as uuid } from 'uuid';
|
|
6
|
-
import { InternalWidgetInteraction, WidgetInteraction } from './types';
|
|
6
|
+
import { INTERACTION_TYPE_CLASS, InternalWidgetInteraction, WidgetInteraction } from './types';
|
|
7
7
|
import { extractPageContext } from './utils';
|
|
8
8
|
|
|
9
9
|
const getInteractionId = () => uuid();
|
|
@@ -16,6 +16,8 @@ export const useWidgetInteraction = () => {
|
|
|
16
16
|
if (props.trigger) {
|
|
17
17
|
// eslint-disable-next-line no-param-reassign
|
|
18
18
|
props.trigger.interaction_id = props.trigger.interaction_id || getInteractionId();
|
|
19
|
+
props.trigger.interaction_class =
|
|
20
|
+
props.trigger.interaction_class || INTERACTION_TYPE_CLASS[props.trigger.widget_interaction];
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
const eventProps = {
|
|
@@ -85,10 +85,12 @@ describe('AmplitudeService', () => {
|
|
|
85
85
|
});
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
+
const validApiKey = 'abcdef1234567890abcdef1234567890';
|
|
89
|
+
|
|
88
90
|
const createService = (overrides?: Partial<AmplitudeServiceConfig>) => {
|
|
89
91
|
return new AmplitudeService({
|
|
90
92
|
userId: 'test-user-id',
|
|
91
|
-
amplitudeApiKey:
|
|
93
|
+
amplitudeApiKey: validApiKey,
|
|
92
94
|
dataResidency: 'US',
|
|
93
95
|
env: 'test',
|
|
94
96
|
orgId: 'test-org-id',
|
|
@@ -107,7 +109,7 @@ describe('AmplitudeService', () => {
|
|
|
107
109
|
createService();
|
|
108
110
|
|
|
109
111
|
expect(mockInit).toHaveBeenCalledWith(
|
|
110
|
-
|
|
112
|
+
validApiKey,
|
|
111
113
|
'test-user-id',
|
|
112
114
|
expect.objectContaining({
|
|
113
115
|
serverZone: 'US',
|
|
@@ -121,7 +123,7 @@ describe('AmplitudeService', () => {
|
|
|
121
123
|
it('should not be ready if userId is missing', () => {
|
|
122
124
|
const service = new AmplitudeService({
|
|
123
125
|
userId: '',
|
|
124
|
-
amplitudeApiKey:
|
|
126
|
+
amplitudeApiKey: validApiKey,
|
|
125
127
|
dataResidency: 'US',
|
|
126
128
|
env: 'test',
|
|
127
129
|
orgId: 'test-org-id',
|
|
@@ -157,7 +159,7 @@ describe('AmplitudeService', () => {
|
|
|
157
159
|
it('should not be ready if featureFlagService is missing', () => {
|
|
158
160
|
const service = new AmplitudeService({
|
|
159
161
|
userId: 'test-user-id',
|
|
160
|
-
amplitudeApiKey:
|
|
162
|
+
amplitudeApiKey: validApiKey,
|
|
161
163
|
dataResidency: 'US',
|
|
162
164
|
env: 'test',
|
|
163
165
|
orgId: 'test-org-id',
|
|
@@ -269,7 +271,7 @@ describe('AmplitudeService', () => {
|
|
|
269
271
|
it('should not track if client is not initialized', async () => {
|
|
270
272
|
const service = new AmplitudeService({
|
|
271
273
|
userId: '',
|
|
272
|
-
amplitudeApiKey:
|
|
274
|
+
amplitudeApiKey: validApiKey,
|
|
273
275
|
dataResidency: 'US',
|
|
274
276
|
env: 'test',
|
|
275
277
|
orgId: 'test-org-id',
|
|
@@ -419,7 +421,7 @@ describe('AmplitudeService', () => {
|
|
|
419
421
|
it('should fall back to window.localStorage when getLocalStorageItem is not provided', async () => {
|
|
420
422
|
const service = new AmplitudeService({
|
|
421
423
|
userId: 'test-user-id',
|
|
422
|
-
amplitudeApiKey:
|
|
424
|
+
amplitudeApiKey: validApiKey,
|
|
423
425
|
dataResidency: 'US',
|
|
424
426
|
env: 'test',
|
|
425
427
|
orgId: 'test-org-id',
|
|
@@ -597,6 +599,67 @@ describe('AmplitudeService', () => {
|
|
|
597
599
|
});
|
|
598
600
|
});
|
|
599
601
|
|
|
602
|
+
describe('isMockApiKey', () => {
|
|
603
|
+
it('should return true when amplitudeApiKey is "mock-amplitude-key"', () => {
|
|
604
|
+
const service = createService({ amplitudeApiKey: 'mock-amplitude-key' });
|
|
605
|
+
|
|
606
|
+
expect(service.isMockApiKey).toBe(true);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
it('should return false for a real API key', () => {
|
|
610
|
+
const service = createService();
|
|
611
|
+
|
|
612
|
+
expect(service.isMockApiKey).toBe(false);
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
describe('Mock API key behavior', () => {
|
|
617
|
+
it('should not initialize the Amplitude client when using mock API key', () => {
|
|
618
|
+
createService({ amplitudeApiKey: 'mock-amplitude-key' });
|
|
619
|
+
|
|
620
|
+
expect(mockInit).not.toHaveBeenCalled();
|
|
621
|
+
expect(mockAdd).not.toHaveBeenCalled();
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
it('should log a warning when mock API key is detected during initialization', () => {
|
|
625
|
+
const logWarnSpy = vi.spyOn(Logger.prototype, 'logWarn');
|
|
626
|
+
|
|
627
|
+
createService({ amplitudeApiKey: 'mock-amplitude-key' });
|
|
628
|
+
|
|
629
|
+
expect(logWarnSpy).toHaveBeenCalledWith(
|
|
630
|
+
'Mock API key detected — running in mock mode, no events will be sent to Amplitude.',
|
|
631
|
+
'mock-amplitude-key',
|
|
632
|
+
);
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it('should report isReady as false when using mock API key', () => {
|
|
636
|
+
const service = createService({ amplitudeApiKey: 'mock-amplitude-key' });
|
|
637
|
+
|
|
638
|
+
expect(service.isReady).toBe(false);
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
it('should not track events when using mock API key', async () => {
|
|
642
|
+
const service = createService({ amplitudeApiKey: 'mock-amplitude-key' });
|
|
643
|
+
|
|
644
|
+
await service.trackEvent({
|
|
645
|
+
eventName: SpiffyMetricsEventName.ChatUserMessageInput,
|
|
646
|
+
eventProps: { message: 'test' },
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
expect(mockTrack).not.toHaveBeenCalled();
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
it('should not call crypto.subtle.digest when using mock API key', async () => {
|
|
653
|
+
const service = createService({ amplitudeApiKey: 'mock-amplitude-key' });
|
|
654
|
+
|
|
655
|
+
await service.trackEvent({
|
|
656
|
+
eventName: SpiffyMetricsEventName.ChatUserMessageInput,
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
expect(mockDigest).not.toHaveBeenCalled();
|
|
660
|
+
});
|
|
661
|
+
});
|
|
662
|
+
|
|
600
663
|
describe('Session replay initialization', () => {
|
|
601
664
|
it('should disable session replay by default', () => {
|
|
602
665
|
createService();
|
|
@@ -56,6 +56,10 @@ export class AmplitudeService {
|
|
|
56
56
|
this.initialize();
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
get isMockApiKey(): boolean {
|
|
60
|
+
return this.config.amplitudeApiKey === 'mock-amplitude-key';
|
|
61
|
+
}
|
|
62
|
+
|
|
59
63
|
get isReady(): boolean {
|
|
60
64
|
return Boolean(
|
|
61
65
|
this.config.userId &&
|
|
@@ -228,6 +232,14 @@ export class AmplitudeService {
|
|
|
228
232
|
return;
|
|
229
233
|
}
|
|
230
234
|
|
|
235
|
+
if (this.isMockApiKey) {
|
|
236
|
+
logger.logWarn(
|
|
237
|
+
'Mock API key detected — running in mock mode, no events will be sent to Amplitude.',
|
|
238
|
+
this.config.amplitudeApiKey,
|
|
239
|
+
);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
231
243
|
if (!this.amplitudeClient) {
|
|
232
244
|
const currentAmplitudeInstance: BrowserClient = createInstance();
|
|
233
245
|
|
|
@@ -353,6 +365,7 @@ export class AmplitudeService {
|
|
|
353
365
|
eventGroups,
|
|
354
366
|
alsoSendToGoogleAnalytics = false,
|
|
355
367
|
}: TrackEventParams): Promise<void> {
|
|
368
|
+
if (this.isMockApiKey) return;
|
|
356
369
|
logger.logDebug('Submitting event', eventName);
|
|
357
370
|
try {
|
|
358
371
|
const decoratedEventName = AmplitudeService.decorateEventName(eventName);
|
|
@@ -28,34 +28,46 @@ describe('projectToGA4', () => {
|
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
describe('Widget Rendered', () => {
|
|
31
|
-
it('should push
|
|
31
|
+
it('should push renamed GA4 fields from nested input', () => {
|
|
32
32
|
projectToGA4(EnviveMetricsEventName.WidgetRendered, {
|
|
33
|
-
|
|
34
|
-
'
|
|
35
|
-
'trigger.widget': 'floating_button',
|
|
36
|
-
'trigger.interaction_id': 'abc-123',
|
|
33
|
+
trigger: { widget: 'floating_button' },
|
|
34
|
+
context: { page_type: 'pdp', page_id: 'product-123' },
|
|
37
35
|
});
|
|
38
36
|
|
|
39
37
|
expect(window.dataLayer).toHaveLength(1);
|
|
40
38
|
expect(window.dataLayer[0]).toEqual({
|
|
41
39
|
event: 'envive_widget_rendered',
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
page_type: 'pdp',
|
|
41
|
+
page_id: 'product-123',
|
|
42
|
+
widget: 'floating_button',
|
|
45
43
|
});
|
|
46
44
|
});
|
|
47
45
|
|
|
48
|
-
it('should
|
|
46
|
+
it('should also accept flat dot-notation input', () => {
|
|
49
47
|
projectToGA4(EnviveMetricsEventName.WidgetRendered, {
|
|
50
48
|
'context.page_type': 'pdp',
|
|
51
49
|
'context.page_id': 'product-123',
|
|
52
50
|
'trigger.widget': 'floating_button',
|
|
53
|
-
'trigger.interaction_id': '
|
|
51
|
+
'trigger.interaction_id': 'abc-123',
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
expect(window.dataLayer[0]).toEqual({
|
|
55
|
+
event: 'envive_widget_rendered',
|
|
56
|
+
page_type: 'pdp',
|
|
57
|
+
page_id: 'product-123',
|
|
58
|
+
widget: 'floating_button',
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should exclude fields not in fieldProjections', () => {
|
|
63
|
+
projectToGA4(EnviveMetricsEventName.WidgetRendered, {
|
|
64
|
+
trigger: { widget: 'floating_button', interaction_id: 'should-be-dropped' },
|
|
65
|
+
context: { page_type: 'pdp', page_id: 'product-123' },
|
|
54
66
|
'some.random_field': 'also-dropped',
|
|
55
67
|
});
|
|
56
68
|
|
|
57
69
|
const pushed = window.dataLayer[0];
|
|
58
|
-
expect(pushed).not.toHaveProperty('
|
|
70
|
+
expect(pushed).not.toHaveProperty('interaction_id');
|
|
59
71
|
expect(pushed).not.toHaveProperty('some_random_field');
|
|
60
72
|
});
|
|
61
73
|
});
|
|
@@ -63,97 +75,146 @@ describe('projectToGA4', () => {
|
|
|
63
75
|
describe('page_id sanitization', () => {
|
|
64
76
|
it('should keep page_id for pdp page type', () => {
|
|
65
77
|
projectToGA4(EnviveMetricsEventName.WidgetRendered, {
|
|
66
|
-
|
|
67
|
-
'
|
|
68
|
-
'trigger.widget': 'floating_button',
|
|
78
|
+
trigger: { widget: 'floating_button' },
|
|
79
|
+
context: { page_type: 'pdp', page_id: 'product-123' },
|
|
69
80
|
});
|
|
70
81
|
|
|
71
|
-
expect(window.dataLayer[0]).toHaveProperty('
|
|
82
|
+
expect(window.dataLayer[0]).toHaveProperty('page_id', 'product-123');
|
|
72
83
|
});
|
|
73
84
|
|
|
74
85
|
it('should keep page_id for plp page type', () => {
|
|
75
86
|
projectToGA4(EnviveMetricsEventName.WidgetRendered, {
|
|
76
|
-
|
|
77
|
-
'
|
|
78
|
-
'trigger.widget': 'floating_button',
|
|
87
|
+
trigger: { widget: 'floating_button' },
|
|
88
|
+
context: { page_type: 'plp', page_id: 'category-456' },
|
|
79
89
|
});
|
|
80
90
|
|
|
81
|
-
expect(window.dataLayer[0]).toHaveProperty('
|
|
91
|
+
expect(window.dataLayer[0]).toHaveProperty('page_id', 'category-456');
|
|
82
92
|
});
|
|
83
93
|
|
|
84
94
|
it('should omit page_id for search page type', () => {
|
|
85
95
|
projectToGA4(EnviveMetricsEventName.WidgetRendered, {
|
|
86
|
-
|
|
87
|
-
'
|
|
88
|
-
'trigger.widget': 'floating_button',
|
|
96
|
+
trigger: { widget: 'floating_button' },
|
|
97
|
+
context: { page_type: 'search', page_id: 'some search query' },
|
|
89
98
|
});
|
|
90
99
|
|
|
91
|
-
expect(window.dataLayer[0]).not.toHaveProperty('
|
|
100
|
+
expect(window.dataLayer[0]).not.toHaveProperty('page_id');
|
|
92
101
|
});
|
|
93
102
|
|
|
94
103
|
it('should omit page_id for homepage page type', () => {
|
|
95
104
|
projectToGA4(EnviveMetricsEventName.WidgetRendered, {
|
|
96
|
-
|
|
97
|
-
'
|
|
98
|
-
'trigger.widget': 'floating_button',
|
|
105
|
+
trigger: { widget: 'floating_button' },
|
|
106
|
+
context: { page_type: 'homepage', page_id: 'https://example.com' },
|
|
99
107
|
});
|
|
100
108
|
|
|
101
|
-
expect(window.dataLayer[0]).not.toHaveProperty('
|
|
109
|
+
expect(window.dataLayer[0]).not.toHaveProperty('page_id');
|
|
102
110
|
});
|
|
103
111
|
|
|
104
112
|
it('should omit page_id for other page type', () => {
|
|
105
113
|
projectToGA4(EnviveMetricsEventName.WidgetRendered, {
|
|
106
|
-
|
|
107
|
-
'
|
|
108
|
-
'trigger.widget': 'floating_button',
|
|
114
|
+
trigger: { widget: 'floating_button' },
|
|
115
|
+
context: { page_type: 'other', page_id: 'https://example.com/about' },
|
|
109
116
|
});
|
|
110
117
|
|
|
111
|
-
expect(window.dataLayer[0]).not.toHaveProperty('
|
|
118
|
+
expect(window.dataLayer[0]).not.toHaveProperty('page_id');
|
|
112
119
|
});
|
|
113
120
|
});
|
|
114
121
|
|
|
115
122
|
describe('Widget Interaction', () => {
|
|
116
|
-
it('should push
|
|
123
|
+
it('should push renamed GA4 fields (flat dot-notation input)', () => {
|
|
117
124
|
projectToGA4(EnviveMetricsEventName.WidgetInteraction, {
|
|
118
125
|
'context.page_type': 'pdp',
|
|
119
126
|
'context.page_id': 'product-123',
|
|
120
127
|
'trigger.widget': 'chat_overlay',
|
|
121
128
|
'trigger.widget_interaction': 'link_clicked',
|
|
129
|
+
'trigger.interaction_class': 'intentional',
|
|
122
130
|
});
|
|
123
131
|
|
|
124
132
|
expect(window.dataLayer[0]).toEqual({
|
|
125
133
|
event: 'envive_widget_interaction',
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
134
|
+
page_type: 'pdp',
|
|
135
|
+
page_id: 'product-123',
|
|
136
|
+
widget: 'chat_overlay',
|
|
137
|
+
interaction_type: 'link_clicked',
|
|
138
|
+
interaction_class: 'intentional',
|
|
130
139
|
});
|
|
131
140
|
});
|
|
132
141
|
|
|
133
|
-
it('should
|
|
142
|
+
it('should project from real Amplitude nested-object props', () => {
|
|
143
|
+
projectToGA4(EnviveMetricsEventName.WidgetInteraction, {
|
|
144
|
+
trigger: {
|
|
145
|
+
widget: 'floating_button',
|
|
146
|
+
widget_interaction: 'widget_clicked',
|
|
147
|
+
interaction_id: '604a2e2c-9848-41dd-a2b1-cb5b8612ec08',
|
|
148
|
+
interaction_class: 'intentional',
|
|
149
|
+
},
|
|
150
|
+
context: {
|
|
151
|
+
page_id: 'hsa-fsa-eligible',
|
|
152
|
+
page_type: 'plp',
|
|
153
|
+
},
|
|
154
|
+
'environment.execution_context': 'bundle',
|
|
155
|
+
'org.short_name': 'dermalogica',
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(window.dataLayer[0]).toEqual({
|
|
159
|
+
event: 'envive_widget_interaction',
|
|
160
|
+
page_type: 'plp',
|
|
161
|
+
page_id: 'hsa-fsa-eligible',
|
|
162
|
+
widget: 'floating_button',
|
|
163
|
+
interaction_type: 'widget_clicked',
|
|
164
|
+
interaction_class: 'intentional',
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should omit page_id for non-pdp/plp page types (nested input)', () => {
|
|
169
|
+
projectToGA4(EnviveMetricsEventName.WidgetInteraction, {
|
|
170
|
+
trigger: { widget: 'floating_button', widget_interaction: 'widget_clicked' },
|
|
171
|
+
context: { page_type: 'homepage', page_id: 'https://example.com' },
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
expect(window.dataLayer[0]).not.toHaveProperty('page_id');
|
|
175
|
+
expect(window.dataLayer[0]).toHaveProperty('page_type', 'homepage');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should extract interaction_collapse_source from widget_interaction_data for widget_collapsed', () => {
|
|
134
179
|
projectToGA4(EnviveMetricsEventName.WidgetInteraction, {
|
|
135
180
|
'context.page_type': 'pdp',
|
|
136
181
|
'context.page_id': 'product-123',
|
|
137
182
|
'trigger.widget': 'chat_overlay',
|
|
138
183
|
'trigger.widget_interaction': 'widget_collapsed',
|
|
139
|
-
'trigger.widget_interaction_data': {
|
|
184
|
+
'trigger.widget_interaction_data': {
|
|
185
|
+
widget_collapsed: { collapse_source: 'close_button' },
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
expect(window.dataLayer[0]).toHaveProperty('interaction_collapse_source', 'close_button');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should extract interaction_collapse_source from nested real-data trigger object', () => {
|
|
193
|
+
projectToGA4(EnviveMetricsEventName.WidgetInteraction, {
|
|
194
|
+
trigger: {
|
|
195
|
+
widget: 'floating_button',
|
|
196
|
+
widget_interaction: 'widget_collapsed',
|
|
197
|
+
interaction_class: 'navigational',
|
|
198
|
+
widget_interaction_data: { widget_collapsed: { collapse_source: 'swipe' } },
|
|
199
|
+
},
|
|
200
|
+
context: { page_type: 'pdp', page_id: 'product-123' },
|
|
140
201
|
});
|
|
141
202
|
|
|
142
|
-
expect(window.dataLayer[0]).toHaveProperty('
|
|
203
|
+
expect(window.dataLayer[0]).toHaveProperty('interaction_collapse_source', 'swipe');
|
|
204
|
+
expect(window.dataLayer[0]).toHaveProperty('interaction_class', 'navigational');
|
|
143
205
|
});
|
|
144
206
|
|
|
145
|
-
it('should extract
|
|
207
|
+
it('should extract interaction_product_id from widget_interaction_data for product_card_clicked', () => {
|
|
146
208
|
projectToGA4(EnviveMetricsEventName.WidgetInteraction, {
|
|
147
209
|
'context.page_type': 'pdp',
|
|
148
210
|
'context.page_id': 'product-123',
|
|
149
211
|
'trigger.widget': 'chat_overlay',
|
|
150
212
|
'trigger.widget_interaction': 'product_card_clicked',
|
|
151
|
-
'trigger.widget_interaction_data': { product_id: 'sku-789'
|
|
213
|
+
'trigger.widget_interaction_data': { product_card_clicked: { product_id: 'sku-789' } },
|
|
152
214
|
});
|
|
153
215
|
|
|
154
216
|
const pushed = window.dataLayer[0];
|
|
155
|
-
expect(pushed).toHaveProperty('
|
|
156
|
-
expect(pushed).not.toHaveProperty('url');
|
|
217
|
+
expect(pushed).toHaveProperty('interaction_product_id', 'sku-789');
|
|
157
218
|
});
|
|
158
219
|
|
|
159
220
|
it('should extract suggestion_id from widget_interaction_data for suggestion_scrolled', () => {
|
|
@@ -162,10 +223,10 @@ describe('projectToGA4', () => {
|
|
|
162
223
|
'context.page_id': 'product-123',
|
|
163
224
|
'trigger.widget': 'chat_overlay',
|
|
164
225
|
'trigger.widget_interaction': 'suggestion_scrolled',
|
|
165
|
-
'trigger.widget_interaction_data': { suggestion_id: 'sug-456' },
|
|
226
|
+
'trigger.widget_interaction_data': { suggestion_scrolled: { suggestion_id: 'sug-456' } },
|
|
166
227
|
});
|
|
167
228
|
|
|
168
|
-
expect(window.dataLayer[0]).toHaveProperty('
|
|
229
|
+
expect(window.dataLayer[0]).toHaveProperty('interaction_suggestion_id', 'sug-456');
|
|
169
230
|
});
|
|
170
231
|
|
|
171
232
|
it('should drop widget_interaction_data for unwhitelisted interactions', () => {
|
|
@@ -281,8 +342,8 @@ describe('projectToGA4', () => {
|
|
|
281
342
|
|
|
282
343
|
expect(window.dataLayer[0]).toEqual({
|
|
283
344
|
event: 'envive_page_context_evaluated',
|
|
284
|
-
|
|
285
|
-
|
|
345
|
+
page_type: 'pdp',
|
|
346
|
+
page_id: 'product-123',
|
|
286
347
|
context_supported: true,
|
|
287
348
|
context_ready: true,
|
|
288
349
|
context_page_variant_id: 'variant-A',
|
|
@@ -300,8 +361,8 @@ describe('projectToGA4', () => {
|
|
|
300
361
|
});
|
|
301
362
|
|
|
302
363
|
const pushed = window.dataLayer[0];
|
|
303
|
-
expect(pushed).not.toHaveProperty('
|
|
304
|
-
expect(pushed).toHaveProperty('
|
|
364
|
+
expect(pushed).not.toHaveProperty('page_id');
|
|
365
|
+
expect(pushed).toHaveProperty('page_type', 'search');
|
|
305
366
|
});
|
|
306
367
|
});
|
|
307
368
|
|