@envive-ai/react-hooks 0.3.22 → 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/index.d.ts +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 +5 -2
- package/dist/atoms/chat/chatState.js +3 -2
- package/dist/atoms/chat/form.d.cts +2 -2
- package/dist/atoms/chat/form.d.ts +2 -2
- package/dist/atoms/chat/index.cjs +1 -0
- package/dist/atoms/chat/index.d.cts +3 -3
- 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 +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/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.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/enviveContext/enviveContext.cjs +3 -3
- package/dist/contexts/enviveContext/enviveContext.js +3 -3
- 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.ts +2 -2
- package/dist/contexts/types.d.cts +1 -1
- package/dist/contexts/types.d.ts +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/GrabAndScroll/useGrabAndScroll.d.ts +2 -2
- package/dist/hooks/Search/useSearchInput.cjs +1 -1
- package/dist/hooks/Search/useSearchInput.js +1 -1
- package/dist/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.cjs +8 -2
- package/dist/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.js +8 -2
- package/dist/hooks/WidgetInteraction/types.cjs +6 -2
- package/dist/hooks/WidgetInteraction/types.d.cts +8 -3
- package/dist/hooks/WidgetInteraction/types.d.ts +8 -3
- package/dist/hooks/WidgetInteraction/types.js +6 -2
- package/dist/hooks/utils.d.cts +1 -1
- package/dist/hooks/utils.d.ts +1 -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/hardcopyContext/hardcopyContext.tsx +10 -2
- package/src/contexts/salesAgentContext/chatAPI.ts +6 -2
- package/src/contexts/typesV3.ts +1 -0
- package/src/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.ts +10 -2
- package/src/hooks/WidgetInteraction/types.ts +10 -1
- 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
package/package.json
CHANGED
|
@@ -27,4 +27,5 @@ export enum FeatureGates {
|
|
|
27
27
|
IsAiSuggestionsVariantEnabled = 'is_ai_suggestions_variant_enabled',
|
|
28
28
|
IsAiSuggestionsVariantAEnabled = 'is_ai_suggestions_variant_a_enabled',
|
|
29
29
|
IsAiSuggestionsVariantBEnabled = 'is_ai_suggestions_variant_b_enabled',
|
|
30
|
+
IsVoiceInputEnabled = 'is_voice_input_enabled',
|
|
30
31
|
}
|
|
@@ -26,6 +26,7 @@ export const initializedAtom = atom<boolean>(false);
|
|
|
26
26
|
export const chatIsOpenAtom = atom<boolean>(false);
|
|
27
27
|
export const requestFailureAtom = atom<boolean>(false);
|
|
28
28
|
export const formSubmitAtom = atom<FormSubmittedAttributes>();
|
|
29
|
+
export const listeningToSpeechAtom = atom<boolean>(false);
|
|
29
30
|
export const chatOnToggleAtom = atom(
|
|
30
31
|
null,
|
|
31
32
|
(
|
|
@@ -219,10 +219,18 @@ export const HardcopyProvider: React.FC<HardcopyProviderProps> = ({
|
|
|
219
219
|
const { featureFlagService } = useFeatureFlagService();
|
|
220
220
|
const getHardcopyFromBackend = useCallback(
|
|
221
221
|
async (request: HardcopyRequest): Promise<HardcopyResponse> => {
|
|
222
|
-
|
|
222
|
+
const fallbackWidgetType =
|
|
223
|
+
request.widgetType === WidgetTypeV3.ProductCardV3
|
|
224
|
+
? WidgetTypeV3.ImagePromptCardV3
|
|
225
|
+
: request.widgetType;
|
|
226
|
+
|
|
227
|
+
const overrideEntry =
|
|
228
|
+
hardcopyOverride?.[request.widgetType] ?? hardcopyOverride?.[fallbackWidgetType];
|
|
229
|
+
if (overrideEntry) {
|
|
223
230
|
logger.logDebug('using hardcopy override', request.widgetType);
|
|
224
|
-
return
|
|
231
|
+
return overrideEntry;
|
|
225
232
|
}
|
|
233
|
+
|
|
226
234
|
const overrideConfigVersion =
|
|
227
235
|
getQueryParam('spiffy_config_version') ||
|
|
228
236
|
getQueryParam('envive_config_version') ||
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { hasParsedVariantInfoAtom } from 'src/atoms/app';
|
|
15
15
|
import { analyticsContextAtom } from 'src/atoms/app/variant';
|
|
16
16
|
import { formSubmitAtom, replyEventCategoryAtom } from 'src/atoms/chat';
|
|
17
|
+
import { suggestionsAtom } from 'src/atoms/chat/chatState';
|
|
17
18
|
import { queueUserEventAtom } from 'src/atoms/chat/messageQueue';
|
|
18
19
|
import { useWidgetInteraction } from 'src/hooks/WidgetInteraction';
|
|
19
20
|
import {
|
|
@@ -56,6 +57,7 @@ export const useSalesAgentChatAPI = (widget?: WidgetInteractionComponent) => {
|
|
|
56
57
|
const context = useAtomValue(analyticsContextAtom);
|
|
57
58
|
const hasParsedVariantInfo = useAtomValue(hasParsedVariantInfoAtom);
|
|
58
59
|
const queueUserEvent = useSetAtom(queueUserEventAtom);
|
|
60
|
+
const setSuggestions = useSetAtom(suggestionsAtom);
|
|
59
61
|
const setReplyEventCategory = useSetAtom(replyEventCategoryAtom);
|
|
60
62
|
const setFormSubmit = useSetAtom(formSubmitAtom);
|
|
61
63
|
const { trackEvent } = useAmplitude();
|
|
@@ -164,9 +166,10 @@ export const useSalesAgentChatAPI = (widget?: WidgetInteractionComponent) => {
|
|
|
164
166
|
content: suggestion.content,
|
|
165
167
|
},
|
|
166
168
|
};
|
|
169
|
+
setSuggestions([]);
|
|
167
170
|
queueUserEvent(event);
|
|
168
171
|
},
|
|
169
|
-
[queueUserEvent, trackEvent, context],
|
|
172
|
+
[queueUserEvent, setSuggestions, trackEvent, context],
|
|
170
173
|
);
|
|
171
174
|
const onTypedMessageSubmitted = useCallback(
|
|
172
175
|
({
|
|
@@ -229,9 +232,10 @@ export const useSalesAgentChatAPI = (widget?: WidgetInteractionComponent) => {
|
|
|
229
232
|
userTyped,
|
|
230
233
|
},
|
|
231
234
|
};
|
|
235
|
+
setSuggestions([]);
|
|
232
236
|
queueUserEvent(event);
|
|
233
237
|
},
|
|
234
|
-
[queueUserEvent, trackEvent, context],
|
|
238
|
+
[queueUserEvent, setSuggestions, trackEvent, context],
|
|
235
239
|
);
|
|
236
240
|
const onFormResponseSubmitted = useCallback(
|
|
237
241
|
(form: FormSubmittedAttributes) => {
|
package/src/contexts/typesV3.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { useAtomValue } from 'jotai';
|
|
1
2
|
import { RefObject, useEffect, useRef } from 'react';
|
|
2
|
-
import {
|
|
3
|
+
import { pageVariantInfoAtom } from 'src/atoms/app';
|
|
3
4
|
import { useAmplitude } from 'src/contexts/amplitudeContext/amplitudeContext';
|
|
5
|
+
import { PageVariantInfo } from 'src/contexts/pageContext/types';
|
|
6
|
+
import { useIntersection } from 'src/hooks/Intersection/useIntersection';
|
|
7
|
+
import { extractPageContext } from 'src/hooks/WidgetInteraction/utils';
|
|
4
8
|
import {
|
|
5
9
|
EnviveMetricsEventName,
|
|
6
10
|
SpiffyMetricsEventName,
|
|
@@ -23,6 +27,7 @@ export const useTrackComponentVisibleEvent = (
|
|
|
23
27
|
const isVisible = useIntersection(element, rootMargin);
|
|
24
28
|
const hasTrackedEvent = useRef(false);
|
|
25
29
|
const { trackEvent } = useAmplitude();
|
|
30
|
+
const variantInfo = useAtomValue(pageVariantInfoAtom);
|
|
26
31
|
|
|
27
32
|
useEffect(() => {
|
|
28
33
|
if (!enabled || hasTrackedEvent.current) {
|
|
@@ -37,7 +42,10 @@ export const useTrackComponentVisibleEvent = (
|
|
|
37
42
|
eventName: EnviveMetricsEventName.WidgetRendered,
|
|
38
43
|
eventProps: {
|
|
39
44
|
...eventProps,
|
|
40
|
-
|
|
45
|
+
trigger: {
|
|
46
|
+
widget: eventProps?.widget_type,
|
|
47
|
+
},
|
|
48
|
+
context: variantInfo ? extractPageContext(variantInfo as PageVariantInfo) : null,
|
|
41
49
|
},
|
|
42
50
|
});
|
|
43
51
|
hasTrackedEvent.current = true;
|
|
@@ -62,6 +62,8 @@ export enum WidgetInteractionType {
|
|
|
62
62
|
REVIEW_CARD_CLICKED = 'review_card_clicked',
|
|
63
63
|
MESSAGE_SUBMITTED = 'message_submitted',
|
|
64
64
|
MANUAL_SCROLL_TO_BOTTOM = 'manual_scroll_to_bottom',
|
|
65
|
+
VOICE_TRANSCRIPTION_STARTED = 'voice_transcription_started',
|
|
66
|
+
VOICE_TRANSCRIPTION_COMPLETED = 'voice_transcription_completed',
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
export enum InteractionClass {
|
|
@@ -85,6 +87,8 @@ export const INTERACTION_TYPE_CLASS: Record<WidgetInteractionType, InteractionCl
|
|
|
85
87
|
[WidgetInteractionType.REVIEW_CARD_CLICKED]: InteractionClass.INTENTIONAL,
|
|
86
88
|
[WidgetInteractionType.MESSAGE_SUBMITTED]: InteractionClass.INTENTIONAL,
|
|
87
89
|
[WidgetInteractionType.MANUAL_SCROLL_TO_BOTTOM]: InteractionClass.PASSIVE,
|
|
90
|
+
[WidgetInteractionType.VOICE_TRANSCRIPTION_STARTED]: InteractionClass.INTENTIONAL,
|
|
91
|
+
[WidgetInteractionType.VOICE_TRANSCRIPTION_COMPLETED]: InteractionClass.INTENTIONAL,
|
|
88
92
|
};
|
|
89
93
|
|
|
90
94
|
export type URL = {
|
|
@@ -151,6 +155,10 @@ export type MessageSubmitted = {
|
|
|
151
155
|
message_submitted: Request;
|
|
152
156
|
};
|
|
153
157
|
|
|
158
|
+
export type VoiceTranscription = {
|
|
159
|
+
transcription: string;
|
|
160
|
+
};
|
|
161
|
+
|
|
154
162
|
export type WidgetInteractionData =
|
|
155
163
|
| SuggestionScrolled
|
|
156
164
|
| SuggestionClicked
|
|
@@ -161,4 +169,5 @@ export type WidgetInteractionData =
|
|
|
161
169
|
| ArticleLinkClicked
|
|
162
170
|
| ProductCardClicked
|
|
163
171
|
| ReviewCardClicked
|
|
164
|
-
| MessageSubmitted
|
|
172
|
+
| MessageSubmitted
|
|
173
|
+
| VoiceTranscription;
|
|
@@ -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
|
|
|
@@ -34,6 +34,11 @@ export type WidgetInteractionDataProjection = Record<string, Record<string, stri
|
|
|
34
34
|
export interface GA4ProjectedEventConfig {
|
|
35
35
|
gaEventName: string;
|
|
36
36
|
allowedFields: readonly string[];
|
|
37
|
+
/**
|
|
38
|
+
* When present, used instead of `allowedFields` to both filter and rename fields.
|
|
39
|
+
* Key: the desired GA4 parameter name. Value: the source dot-notation key in eventProps.
|
|
40
|
+
*/
|
|
41
|
+
fieldProjections?: Record<string, string>;
|
|
37
42
|
widgetInteractionDataProjections?: WidgetInteractionDataProjection;
|
|
38
43
|
}
|
|
39
44
|
|
|
@@ -44,34 +49,36 @@ export interface GA4ExcludedEventConfig {
|
|
|
44
49
|
export type GA4EventSchemaEntry = GA4ProjectedEventConfig | GA4ExcludedEventConfig;
|
|
45
50
|
|
|
46
51
|
const WIDGET_INTERACTION_DATA_PROJECTIONS: WidgetInteractionDataProjection = {
|
|
47
|
-
widget_collapsed: {
|
|
48
|
-
product_card_clicked: {
|
|
49
|
-
suggestion_scrolled: {
|
|
52
|
+
widget_collapsed: { interaction_collapse_source: 'widget_collapsed.collapse_source' },
|
|
53
|
+
product_card_clicked: { interaction_product_id: 'product_card_clicked.product_id' },
|
|
54
|
+
suggestion_scrolled: { interaction_suggestion_id: 'suggestion_scrolled.suggestion_id' },
|
|
50
55
|
};
|
|
51
56
|
|
|
52
57
|
export const GA4_EVENT_SCHEMA: Record<EnviveMetricsEventName, GA4EventSchemaEntry> = {
|
|
53
58
|
[EnviveMetricsEventName.WidgetRendered]: {
|
|
54
59
|
gaEventName: 'envive_widget_rendered',
|
|
55
|
-
allowedFields: [
|
|
56
|
-
|
|
57
|
-
'context.
|
|
58
|
-
'
|
|
59
|
-
'
|
|
60
|
-
'
|
|
61
|
-
|
|
60
|
+
allowedFields: [],
|
|
61
|
+
fieldProjections: {
|
|
62
|
+
page_type: 'context.page_type',
|
|
63
|
+
page_id: 'context.page_id',
|
|
64
|
+
surface: 'context.surface',
|
|
65
|
+
widget: 'trigger.widget',
|
|
66
|
+
widget_role: 'trigger.widget_role',
|
|
67
|
+
},
|
|
62
68
|
},
|
|
63
69
|
|
|
64
|
-
// This event is not currently implemented
|
|
65
70
|
[EnviveMetricsEventName.WidgetInteraction]: {
|
|
66
71
|
gaEventName: 'envive_widget_interaction',
|
|
67
|
-
allowedFields: [
|
|
68
|
-
|
|
69
|
-
'context.
|
|
70
|
-
'
|
|
71
|
-
'
|
|
72
|
-
'trigger.
|
|
73
|
-
'
|
|
74
|
-
|
|
72
|
+
allowedFields: [],
|
|
73
|
+
fieldProjections: {
|
|
74
|
+
page_type: 'context.page_type',
|
|
75
|
+
page_id: 'context.page_id',
|
|
76
|
+
surface: 'context.surface',
|
|
77
|
+
widget: 'trigger.widget',
|
|
78
|
+
interaction_type: 'trigger.widget_interaction',
|
|
79
|
+
interaction_class: 'trigger.interaction_class',
|
|
80
|
+
widget_role: 'trigger.widget_role',
|
|
81
|
+
},
|
|
75
82
|
widgetInteractionDataProjections: WIDGET_INTERACTION_DATA_PROJECTIONS,
|
|
76
83
|
},
|
|
77
84
|
|
|
@@ -120,14 +127,15 @@ export const GA4_EVENT_SCHEMA: Record<EnviveMetricsEventName, GA4EventSchemaEntr
|
|
|
120
127
|
// This event is not currently implemented
|
|
121
128
|
[EnviveMetricsEventName.PageViewed]: {
|
|
122
129
|
gaEventName: 'envive_page_context_evaluated',
|
|
123
|
-
allowedFields: [
|
|
124
|
-
|
|
125
|
-
'context.
|
|
126
|
-
'context.
|
|
127
|
-
'context.
|
|
128
|
-
'context.
|
|
129
|
-
'
|
|
130
|
-
|
|
130
|
+
allowedFields: [],
|
|
131
|
+
fieldProjections: {
|
|
132
|
+
page_type: 'context.page_type',
|
|
133
|
+
page_id: 'context.page_id',
|
|
134
|
+
context_supported: 'context.supported',
|
|
135
|
+
context_ready: 'context.ready',
|
|
136
|
+
context_page_variant_id: 'context.page_variant_id',
|
|
137
|
+
environment_envive_enabled: 'environment.envive_enabled',
|
|
138
|
+
},
|
|
131
139
|
},
|
|
132
140
|
|
|
133
141
|
[EnviveMetricsEventName.WidgetTextRequest]: {
|
|
@@ -17,6 +17,42 @@ const filterToSchema = (
|
|
|
17
17
|
return result;
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
+
// Flatten only true top-level nested objects (keys with no dots) one level deep.
|
|
21
|
+
// Keys that already contain dots are left intact so that e.g.
|
|
22
|
+
// `trigger.widget_interaction_data: { ... }` is preserved for downstream extraction.
|
|
23
|
+
const flattenOneLevel = (obj: Record<string, unknown>): Record<string, unknown> => {
|
|
24
|
+
const result: Record<string, unknown> = {};
|
|
25
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
26
|
+
if (
|
|
27
|
+
!key.includes('.') &&
|
|
28
|
+
value !== null &&
|
|
29
|
+
typeof value === 'object' &&
|
|
30
|
+
!Array.isArray(value)
|
|
31
|
+
) {
|
|
32
|
+
for (const [subKey, subValue] of Object.entries(value as Record<string, unknown>)) {
|
|
33
|
+
result[`${key}.${subKey}`] = subValue;
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
result[key] = value;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Apply a GA4 projection map { ga4Key: sourceKey } to extract and rename fields.
|
|
43
|
+
const filterWithProjections = (
|
|
44
|
+
eventProps: Record<string, unknown>,
|
|
45
|
+
projections: Record<string, string>,
|
|
46
|
+
): Record<string, unknown> => {
|
|
47
|
+
const result: Record<string, unknown> = {};
|
|
48
|
+
for (const [ga4Key, sourceKey] of Object.entries(projections)) {
|
|
49
|
+
if (sourceKey in eventProps) {
|
|
50
|
+
result[ga4Key] = eventProps[sourceKey];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
};
|
|
55
|
+
|
|
20
56
|
// "context.page_type" → "context_page_type"
|
|
21
57
|
const flattenDotKeys = (obj: Record<string, unknown>): Record<string, unknown> => {
|
|
22
58
|
const result: Record<string, unknown> = {};
|
|
@@ -32,16 +68,28 @@ const flattenDotKeys = (obj: Record<string, unknown>): Record<string, unknown> =
|
|
|
32
68
|
// Search: search query
|
|
33
69
|
// Other: page url
|
|
34
70
|
// We want to omit all but pdp and plp page types to provide a clear, consistent interface for merchants.
|
|
71
|
+
// Handles both dot-notation keys (allowedFields path) and renamed GA4 keys (fieldProjections path).
|
|
35
72
|
const sanitizePageId = (filtered: Record<string, unknown>): Record<string, unknown> => {
|
|
36
|
-
const pageType = filtered['context.page_type'];
|
|
73
|
+
const pageType = filtered['page_type'] ?? filtered['context.page_type'];
|
|
37
74
|
if (pageType === 'pdp' || pageType === 'plp') {
|
|
38
75
|
return filtered;
|
|
39
76
|
}
|
|
40
77
|
const rest = { ...filtered };
|
|
78
|
+
delete rest['page_id'];
|
|
41
79
|
delete rest['context.page_id'];
|
|
42
80
|
return rest;
|
|
43
81
|
};
|
|
44
82
|
|
|
83
|
+
const getNestedValue = (obj: Record<string, unknown>, path: string): unknown => {
|
|
84
|
+
const parts = path.split('.');
|
|
85
|
+
let current: unknown = obj;
|
|
86
|
+
for (const part of parts) {
|
|
87
|
+
if (current === null || typeof current !== 'object') return undefined;
|
|
88
|
+
current = (current as Record<string, unknown>)[part];
|
|
89
|
+
}
|
|
90
|
+
return current;
|
|
91
|
+
};
|
|
92
|
+
|
|
45
93
|
// Extract whitelisted sub-fields from trigger.widget_interaction_data
|
|
46
94
|
const projectWidgetInteractionData = (
|
|
47
95
|
eventProps: Record<string, unknown>,
|
|
@@ -72,9 +120,10 @@ const projectWidgetInteractionData = (
|
|
|
72
120
|
|
|
73
121
|
const data = interactionData as Record<string, unknown>;
|
|
74
122
|
const result: Record<string, unknown> = {};
|
|
75
|
-
for (const [gaKey,
|
|
76
|
-
|
|
77
|
-
|
|
123
|
+
for (const [gaKey, sourcePath] of Object.entries(projectionMap)) {
|
|
124
|
+
const value = getNestedValue(data, sourcePath);
|
|
125
|
+
if (value !== undefined) {
|
|
126
|
+
result[gaKey] = value;
|
|
78
127
|
}
|
|
79
128
|
}
|
|
80
129
|
return result;
|
|
@@ -113,9 +162,14 @@ export const projectToGA4 = (
|
|
|
113
162
|
}
|
|
114
163
|
|
|
115
164
|
const config = schemaEntry;
|
|
116
|
-
const props = eventProps ?? {};
|
|
165
|
+
const props = flattenOneLevel(eventProps ?? {});
|
|
117
166
|
|
|
118
|
-
let filtered
|
|
167
|
+
let filtered: Record<string, unknown>;
|
|
168
|
+
if (config.fieldProjections) {
|
|
169
|
+
filtered = filterWithProjections(props, config.fieldProjections);
|
|
170
|
+
} else {
|
|
171
|
+
filtered = filterToSchema(props, config.allowedFields);
|
|
172
|
+
}
|
|
119
173
|
filtered = sanitizePageId(filtered);
|
|
120
174
|
|
|
121
175
|
const interactionFields = projectWidgetInteractionData(props, config);
|