@envive-ai/react-hooks 0.3.46 → 0.3.48
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/commerce-api.cjs +73 -3
- package/dist/application/commerce-api.d.cts +16 -1
- package/dist/application/commerce-api.d.ts +16 -1
- package/dist/application/commerce-api.js +73 -3
- package/dist/application/models/api/userEvent.d.cts +9 -2
- package/dist/application/models/api/userEvent.d.ts +9 -2
- 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/application/models/graphql/queries/getProductOptionsQuery.cjs +33 -0
- package/dist/application/models/graphql/queries/getProductOptionsQuery.d.cts +28 -0
- package/dist/application/models/graphql/queries/getProductOptionsQuery.d.ts +28 -0
- package/dist/application/models/graphql/queries/getProductOptionsQuery.js +32 -0
- package/dist/application/models/validators/validateResponse.cjs +2 -1
- package/dist/application/models/validators/validateResponse.js +2 -1
- package/dist/application/models/validators/validateUserEvent.cjs +3 -3
- package/dist/application/models/validators/validateUserEvent.js +3 -3
- package/dist/application/utils/index.cjs +2 -0
- package/dist/application/utils/index.d.cts +2 -1
- package/dist/application/utils/index.d.ts +2 -1
- package/dist/application/utils/index.js +2 -1
- package/dist/application/utils/messageFromAddToCartEvent.cjs +32 -0
- package/dist/application/utils/messageFromAddToCartEvent.d.cts +19 -0
- package/dist/application/utils/messageFromAddToCartEvent.d.ts +19 -0
- package/dist/application/utils/messageFromAddToCartEvent.js +31 -0
- package/dist/atoms/app/index.cjs +1 -1
- package/dist/atoms/app/index.d.cts +7 -7
- package/dist/atoms/app/index.d.ts +7 -7
- package/dist/atoms/app/index.js +1 -1
- package/dist/atoms/app/variant.d.cts +6 -6
- package/dist/atoms/app/variant.d.ts +6 -6
- package/dist/atoms/chat/chatState.d.cts +19 -19
- package/dist/atoms/chat/chatState.d.ts +19 -19
- package/dist/atoms/chat/form.d.cts +2 -2
- package/dist/atoms/chat/form.d.ts +3 -3
- package/dist/atoms/chat/index.d.cts +2 -2
- package/dist/atoms/chat/index.d.ts +3 -3
- package/dist/atoms/chat/lastMessage.d.cts +2 -2
- package/dist/atoms/chat/lastMessage.d.ts +2 -2
- package/dist/atoms/chat/messageQueue.d.cts +6 -6
- package/dist/atoms/chat/messageQueue.d.ts +7 -7
- package/dist/atoms/chat/performanceMetrics.d.cts +6 -6
- package/dist/atoms/chat/performanceMetrics.d.ts +6 -6
- package/dist/atoms/chat/renderedWidgetRefs.d.cts +2 -2
- package/dist/atoms/chat/renderedWidgetRefs.d.ts +3 -3
- package/dist/atoms/chat/replies.d.cts +1 -1
- package/dist/atoms/chat/replies.d.ts +3 -3
- package/dist/atoms/chat/suggestions.d.cts +2 -2
- package/dist/atoms/chat/suggestions.d.ts +3 -3
- package/dist/atoms/envive/enviveConfig.cjs +7 -4
- package/dist/atoms/envive/enviveConfig.d.cts +16 -14
- package/dist/atoms/envive/enviveConfig.d.ts +16 -14
- package/dist/atoms/envive/enviveConfig.js +6 -4
- 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.cjs +1 -1
- package/dist/atoms/org/graphqlConfig.d.cts +6 -6
- package/dist/atoms/org/graphqlConfig.d.ts +6 -6
- package/dist/atoms/org/graphqlConfig.js +1 -1
- 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 +4 -4
- package/dist/atoms/org/orgAnalyticsConfig.d.ts +4 -4
- 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/enviveConfigContext/enviveConfigContext.cjs +6 -4
- package/dist/contexts/enviveConfigContext/enviveConfigContext.d.cts +3 -1
- package/dist/contexts/enviveConfigContext/enviveConfigContext.d.ts +3 -1
- package/dist/contexts/enviveConfigContext/enviveConfigContext.js +6 -4
- package/dist/contexts/enviveContext/enviveContext.cjs +7 -3
- package/dist/contexts/enviveContext/enviveContext.d.cts +5 -1
- package/dist/contexts/enviveContext/enviveContext.d.ts +5 -1
- package/dist/contexts/enviveContext/enviveContext.js +7 -3
- package/dist/contexts/featureFlagServiceContext/featureFlagServiceContext.cjs +2 -97
- package/dist/contexts/featureFlagServiceContext/featureFlagServiceContext.d.cts +3 -25
- package/dist/contexts/featureFlagServiceContext/featureFlagServiceContext.d.ts +3 -25
- package/dist/contexts/featureFlagServiceContext/featureFlagServiceContext.js +3 -97
- package/dist/contexts/featureFlagServiceContext/index.cjs +2 -1
- package/dist/contexts/featureFlagServiceContext/index.d.cts +2 -1
- package/dist/contexts/featureFlagServiceContext/index.d.ts +2 -1
- package/dist/contexts/featureFlagServiceContext/index.js +2 -1
- package/dist/contexts/graphqlContext/mockV3Config.cjs +1 -1
- package/dist/contexts/graphqlContext/mockV3Config.js +1 -1
- package/dist/contexts/localStorageContext/index.cjs +2 -1
- package/dist/contexts/localStorageContext/index.d.cts +2 -1
- package/dist/contexts/localStorageContext/index.d.ts +2 -1
- package/dist/contexts/localStorageContext/index.js +2 -1
- package/dist/contexts/localStorageContext/localStorageContext.cjs +5 -11
- package/dist/contexts/localStorageContext/localStorageContext.d.cts +3 -7
- package/dist/contexts/localStorageContext/localStorageContext.d.ts +3 -7
- package/dist/contexts/localStorageContext/localStorageContext.js +3 -8
- package/dist/contexts/localStorageContext/localStorageKeys.cjs +12 -0
- package/dist/contexts/localStorageContext/localStorageKeys.d.cts +9 -0
- package/dist/contexts/localStorageContext/localStorageKeys.d.ts +9 -0
- package/dist/contexts/localStorageContext/localStorageKeys.js +11 -0
- package/dist/contexts/salesAgentContext/chatAPI.cjs +13 -2
- package/dist/contexts/salesAgentContext/chatAPI.d.cts +4 -1
- package/dist/contexts/salesAgentContext/chatAPI.d.ts +4 -1
- package/dist/contexts/salesAgentContext/chatAPI.js +14 -3
- package/dist/contexts/salesAgentContext/salesAgentContext.cjs +12 -4
- package/dist/contexts/salesAgentContext/salesAgentContext.js +12 -4
- package/dist/contexts/systemSettingsContext/systemSettingsContext.d.cts +2 -2
- package/dist/contexts/types.cjs +1 -1
- package/dist/contexts/types.d.cts +3 -1
- package/dist/contexts/types.d.ts +3 -1
- package/dist/contexts/types.js +1 -1
- package/dist/contexts/typesV3.cjs +1 -1
- package/dist/contexts/typesV3.d.cts +11 -2
- package/dist/contexts/typesV3.d.ts +12 -3
- package/dist/contexts/typesV3.js +1 -1
- package/dist/hooks/AddToCart/addToCartErrorMessage.cjs +7 -0
- package/dist/hooks/AddToCart/addToCartErrorMessage.d.cts +5 -0
- package/dist/hooks/AddToCart/addToCartErrorMessage.d.ts +5 -0
- package/dist/hooks/AddToCart/addToCartErrorMessage.js +6 -0
- package/dist/hooks/AddToCart/createAddToCartUserEvent.cjs +20 -0
- package/dist/hooks/AddToCart/createAddToCartUserEvent.d.cts +17 -0
- package/dist/hooks/AddToCart/createAddToCartUserEvent.d.ts +17 -0
- package/dist/hooks/AddToCart/createAddToCartUserEvent.js +19 -0
- package/dist/hooks/AddToCart/index.cjs +9 -0
- package/dist/hooks/AddToCart/index.d.cts +5 -0
- package/dist/hooks/AddToCart/index.d.ts +5 -0
- package/dist/hooks/AddToCart/index.js +6 -0
- package/dist/hooks/AddToCart/parseAddToCartFunction.cjs +27 -0
- package/dist/hooks/AddToCart/parseAddToCartFunction.d.cts +13 -0
- package/dist/hooks/AddToCart/parseAddToCartFunction.d.ts +13 -0
- package/dist/hooks/AddToCart/parseAddToCartFunction.js +27 -0
- package/dist/hooks/AddToCart/useAddToCart.cjs +24 -0
- package/dist/hooks/AddToCart/useAddToCart.d.cts +14 -0
- package/dist/hooks/AddToCart/useAddToCart.d.ts +14 -0
- package/dist/hooks/AddToCart/useAddToCart.js +23 -0
- package/dist/hooks/GrabAndScroll/useGrabAndScroll.d.cts +2 -2
- package/dist/hooks/LocalStorageOperations/useLocalStorageOperations.cjs +6 -5
- package/dist/hooks/LocalStorageOperations/useLocalStorageOperations.js +3 -2
- package/dist/hooks/PageViewedEvent/usePageViewedEvent.cjs +5 -2
- package/dist/hooks/PageViewedEvent/usePageViewedEvent.js +5 -2
- package/dist/hooks/ProductDetails/index.cjs +9 -0
- package/dist/hooks/ProductDetails/index.d.cts +4 -0
- package/dist/hooks/ProductDetails/index.d.ts +4 -0
- package/dist/hooks/ProductDetails/index.js +4 -0
- package/dist/hooks/ProductDetails/useProductDetails.cjs +53 -0
- package/dist/hooks/ProductDetails/useProductDetails.d.cts +20 -0
- package/dist/hooks/ProductDetails/useProductDetails.d.ts +20 -0
- package/dist/hooks/ProductDetails/useProductDetails.js +52 -0
- package/dist/hooks/ProductDetails/variantSelection.cjs +42 -0
- package/dist/hooks/ProductDetails/variantSelection.d.cts +35 -0
- package/dist/hooks/ProductDetails/variantSelection.d.ts +35 -0
- package/dist/hooks/ProductDetails/variantSelection.js +38 -0
- package/dist/hooks/Search/useSearch.d.cts +1 -1
- package/dist/hooks/Search/useSearch.d.ts +1 -1
- package/dist/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.cjs +11 -2
- package/dist/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.d.cts +1 -10
- package/dist/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.d.ts +1 -10
- package/dist/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.js +11 -2
- package/dist/hooks/WidgetInteraction/types.cjs +12 -2
- package/dist/hooks/WidgetInteraction/types.d.cts +28 -3
- package/dist/hooks/WidgetInteraction/types.d.ts +28 -3
- package/dist/hooks/WidgetInteraction/types.js +12 -2
- package/dist/hooks/utils.d.cts +1 -1
- package/dist/hooks/utils.d.ts +1 -1
- package/dist/services/amplitudeService/amplitudeService.cjs +16 -10
- package/dist/services/amplitudeService/amplitudeService.d.cts +5 -2
- package/dist/services/amplitudeService/amplitudeService.d.ts +5 -2
- package/dist/services/amplitudeService/amplitudeService.js +14 -7
- package/dist/services/enviveConfigService/enviveConfigService.cjs +3 -4
- package/dist/services/enviveConfigService/enviveConfigService.d.cts +1 -1
- package/dist/services/enviveConfigService/enviveConfigService.d.ts +1 -1
- package/dist/services/enviveConfigService/enviveConfigService.js +2 -3
- package/dist/services/enviveConfigService/fetchGraphQLConfig.cjs +8 -7
- package/dist/services/enviveConfigService/fetchGraphQLConfig.js +8 -6
- package/dist/services/featureFlagService/index.cjs +101 -0
- package/dist/services/featureFlagService/index.d.cts +28 -0
- package/dist/services/featureFlagService/index.d.ts +28 -0
- package/dist/services/featureFlagService/index.js +101 -0
- package/dist/services/ga4ProjectionService/ga4EventSchema.cjs +8 -2
- package/dist/services/ga4ProjectionService/ga4EventSchema.js +8 -2
- package/dist/services/hardcopyService/hardcopyService.d.cts +1 -1
- package/dist/services/hardcopyService/hardcopyService.d.ts +1 -1
- package/dist/services/urlResolverWarmupService/index.cjs +54 -0
- package/dist/services/urlResolverWarmupService/index.d.cts +40 -0
- package/dist/services/urlResolverWarmupService/index.d.ts +40 -0
- package/dist/services/urlResolverWarmupService/index.js +54 -0
- package/dist/types/enviveConfig.d.cts +6 -2
- package/dist/types/enviveConfig.d.ts +6 -2
- package/dist/types/index.d.cts +2 -2
- package/dist/types/index.d.ts +2 -2
- package/package.json +17 -1
- package/src/application/__tests__/commerceApiAddToCartRehydration.test.ts +126 -0
- package/src/application/commerce-api.ts +142 -23
- package/src/application/models/api/userEvent.ts +8 -1
- package/src/application/models/featureGates.ts +5 -0
- package/src/application/models/graphql/queries/getProductOptionsQuery.ts +59 -0
- package/src/application/models/validators/__tests__/validateUserEvent.test.ts +41 -0
- package/src/application/models/validators/validateResponse.ts +1 -0
- package/src/application/models/validators/validateUserEvent.ts +7 -2
- package/src/application/utils/__tests__/messageFromAddToCartEvent.test.ts +109 -0
- package/src/application/utils/index.ts +1 -0
- package/src/application/utils/messageFromAddToCartEvent.ts +32 -0
- package/src/atoms/app/index.ts +1 -1
- package/src/atoms/envive/enviveConfig.ts +6 -1
- package/src/atoms/org/graphqlConfig.ts +1 -1
- package/src/contexts/enviveConfigContext/enviveConfigContext.tsx +5 -0
- package/src/contexts/enviveContext/enviveContext.tsx +10 -0
- package/src/contexts/featureFlagServiceContext/featureFlagServiceContext.tsx +6 -173
- package/src/contexts/localStorageContext/localStorageContext.tsx +2 -5
- package/src/contexts/localStorageContext/localStorageKeys.ts +8 -0
- package/src/contexts/salesAgentContext/__tests__/chatAPI.test.tsx +63 -0
- package/src/contexts/salesAgentContext/chatAPI.ts +20 -1
- package/src/contexts/salesAgentContext/salesAgentContext.tsx +10 -0
- package/src/contexts/types.ts +2 -0
- package/src/contexts/typesV3.ts +14 -0
- package/src/hooks/AddToCart/__tests__/parseAddToCartFunction.test.ts +31 -0
- package/src/hooks/AddToCart/__tests__/useAddToCart.test.tsx +204 -0
- package/src/hooks/AddToCart/addToCartErrorMessage.ts +2 -0
- package/src/hooks/AddToCart/createAddToCartUserEvent.ts +25 -0
- package/src/hooks/AddToCart/index.ts +4 -0
- package/src/hooks/AddToCart/parseAddToCartFunction.ts +31 -0
- package/src/hooks/AddToCart/useAddToCart.ts +35 -0
- package/src/hooks/PageViewedEvent/usePageViewedEvent.ts +4 -0
- package/src/hooks/ProductDetails/__tests__/useProductDetails.test.tsx +129 -0
- package/src/hooks/ProductDetails/__tests__/variantSelection.test.ts +145 -0
- package/src/hooks/ProductDetails/index.ts +2 -0
- package/src/hooks/ProductDetails/useProductDetails.ts +62 -0
- package/src/hooks/ProductDetails/variantSelection.ts +61 -0
- package/src/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.ts +15 -0
- package/src/hooks/WidgetInteraction/types.ts +42 -1
- package/src/services/amplitudeService/__tests__/amplitudeService.test.ts +8 -8
- package/src/services/amplitudeService/amplitudeService.ts +42 -23
- package/src/services/enviveConfigService/enviveConfigService.ts +2 -2
- package/src/services/enviveConfigService/fetchGraphQLConfig.ts +17 -6
- package/src/services/featureFlagService/index.ts +176 -0
- package/src/services/ga4ProjectionService/__tests__/ga4ProjectionService.test.ts +36 -0
- package/src/services/ga4ProjectionService/ga4EventSchema.ts +7 -0
- package/src/services/urlResolverWarmupService/__tests__/urlResolverWarmupService.test.ts +85 -0
- package/src/services/urlResolverWarmupService/index.ts +89 -0
- package/src/types/enviveConfig.ts +3 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import CommerceApiClient from 'src/application/commerce-api';
|
|
2
|
+
import {
|
|
3
|
+
ProductDetails,
|
|
4
|
+
ProductVariant,
|
|
5
|
+
} from 'src/application/models/graphql/queries/getProductOptionsQuery';
|
|
6
|
+
|
|
7
|
+
/** A dropdown selection state: option name → chosen value. */
|
|
8
|
+
export type VariantSelection = Record<string, string>;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Imperative variant-details fetch for click-driven flows (the mount-driven
|
|
12
|
+
* useProductDetails hook is a poor fit when the fetch should ride a button's
|
|
13
|
+
* pending state). Resolves undefined when the product has no options data.
|
|
14
|
+
*
|
|
15
|
+
* @throws on network/HTTP failure or GraphQL errors
|
|
16
|
+
*/
|
|
17
|
+
export const fetchProductDetails = (productId: string): Promise<ProductDetails | undefined> =>
|
|
18
|
+
CommerceApiClient.retrieveProductDetails(productId);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A product needs the variant selector when there is more than one variant to
|
|
22
|
+
* choose between and at least one option actually offers a choice.
|
|
23
|
+
*/
|
|
24
|
+
export const needsVariantSelection = (
|
|
25
|
+
details: ProductDetails | undefined,
|
|
26
|
+
): details is ProductDetails =>
|
|
27
|
+
!!details && details.variants.length > 1 && details.options.some(o => o.values.length > 1);
|
|
28
|
+
|
|
29
|
+
/** The variantId to add directly when the product has exactly one variant. */
|
|
30
|
+
export const getSingleVariantId = (details: ProductDetails | undefined): string | undefined =>
|
|
31
|
+
details && details.variants.length === 1 ? details.variants[0].variantId : undefined;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Initial dropdown state: the options of the first available variant, falling
|
|
35
|
+
* back to the first variant, falling back to the first value of each option —
|
|
36
|
+
* so the selector opens in an addable state whenever possible.
|
|
37
|
+
*/
|
|
38
|
+
export const getDefaultVariantSelection = (details: ProductDetails): VariantSelection => {
|
|
39
|
+
const defaultVariant = details.variants.find(v => v.available) ?? details.variants[0];
|
|
40
|
+
if (defaultVariant) {
|
|
41
|
+
return Object.fromEntries(defaultVariant.options.map(({ name, value }) => [name, value]));
|
|
42
|
+
}
|
|
43
|
+
return Object.fromEntries(
|
|
44
|
+
details.options.filter(o => o.values.length > 0).map(o => [o.name, o.values[0]]),
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The variant whose options exactly match the selection (every option name in
|
|
50
|
+
* the variant agrees with the selected value, and counts match), or undefined
|
|
51
|
+
* when the combination does not exist.
|
|
52
|
+
*/
|
|
53
|
+
export const findVariantForSelection = (
|
|
54
|
+
details: ProductDetails,
|
|
55
|
+
selection: VariantSelection,
|
|
56
|
+
): ProductVariant | undefined =>
|
|
57
|
+
details.variants.find(
|
|
58
|
+
variant =>
|
|
59
|
+
variant.options.length === Object.keys(selection).length &&
|
|
60
|
+
variant.options.every(({ name, value }) => selection[name] === value),
|
|
61
|
+
);
|
|
@@ -18,6 +18,14 @@ import {
|
|
|
18
18
|
* @param rootMargin - Root margin for the intersection observer (defaults to 0px).
|
|
19
19
|
* @param enabled - Whether tracking is enabled (defaults to true).
|
|
20
20
|
*/
|
|
21
|
+
const safeNow = (): number | undefined => {
|
|
22
|
+
try {
|
|
23
|
+
return typeof window !== 'undefined' ? window.performance?.now?.() : undefined;
|
|
24
|
+
} catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
21
29
|
export const useTrackComponentVisibleEvent = (
|
|
22
30
|
element: RefObject<HTMLElement>,
|
|
23
31
|
eventProps?: Record<string, unknown>,
|
|
@@ -28,6 +36,12 @@ export const useTrackComponentVisibleEvent = (
|
|
|
28
36
|
const hasTrackedEvent = useRef(false);
|
|
29
37
|
const { trackEvent } = useAmplitude();
|
|
30
38
|
const variantInfo = useAtomValue(pageVariantInfoAtom);
|
|
39
|
+
// Captured on first render, not when the event fires: Widget Rendered is
|
|
40
|
+
// emitted at visibility, which for below-the-fold widgets can be long
|
|
41
|
+
// after the widget actually rendered. performance.now() is relative to
|
|
42
|
+
// navigation start, so this reads as "ms from initial page load to
|
|
43
|
+
// widget render".
|
|
44
|
+
const renderOffsetMs = useRef(safeNow()).current;
|
|
31
45
|
|
|
32
46
|
useEffect(() => {
|
|
33
47
|
console.debug('[useTrackComponentVisibleEvent] effect ran', {
|
|
@@ -63,6 +77,7 @@ export const useTrackComponentVisibleEvent = (
|
|
|
63
77
|
widget: eventProps?.widget_type,
|
|
64
78
|
},
|
|
65
79
|
context: variantInfo ? extractPageContext(variantInfo as PageVariantInfo) : null,
|
|
80
|
+
'performance.render_offset_ms': renderOffsetMs,
|
|
66
81
|
},
|
|
67
82
|
});
|
|
68
83
|
hasTrackedEvent.current = true;
|
|
@@ -61,6 +61,8 @@ export enum WidgetInteractionType {
|
|
|
61
61
|
SUGGESTION_SCROLLED = 'suggestion_scrolled',
|
|
62
62
|
LINK_CLICKED = 'link_clicked',
|
|
63
63
|
PRODUCT_CARD_CLICKED = 'product_card_clicked',
|
|
64
|
+
ADD_TO_CART_CLICKED = 'add_to_cart_clicked',
|
|
65
|
+
ADD_TO_CART_FAILED = 'add_to_cart_failed',
|
|
64
66
|
TEXT_LINK_CLICKED = 'text_link_clicked',
|
|
65
67
|
ARTICLE_LINK_CLICKED = 'article_link_clicked',
|
|
66
68
|
REVIEW_CARD_CLICKED = 'review_card_clicked',
|
|
@@ -69,6 +71,9 @@ export enum WidgetInteractionType {
|
|
|
69
71
|
VOICE_TRANSCRIPTION_STARTED = 'voice_transcription_started',
|
|
70
72
|
VOICE_TRANSCRIPTION_COMPLETED = 'voice_transcription_completed',
|
|
71
73
|
OVERLAY_DISMISSED = 'overlay_dismissed',
|
|
74
|
+
VARIANT_SELECTOR_OPENED = 'variant_selector_opened',
|
|
75
|
+
VARIANT_OPTION_SELECTED = 'variant_option_selected',
|
|
76
|
+
VARIANT_SELECTOR_DISMISSED = 'variant_selector_dismissed',
|
|
72
77
|
}
|
|
73
78
|
|
|
74
79
|
export enum InteractionClass {
|
|
@@ -87,6 +92,8 @@ export const INTERACTION_TYPE_CLASS: Record<WidgetInteractionType, InteractionCl
|
|
|
87
92
|
[WidgetInteractionType.SUGGESTION_SCROLLED]: InteractionClass.PASSIVE,
|
|
88
93
|
[WidgetInteractionType.LINK_CLICKED]: InteractionClass.INTENTIONAL,
|
|
89
94
|
[WidgetInteractionType.PRODUCT_CARD_CLICKED]: InteractionClass.INTENTIONAL,
|
|
95
|
+
[WidgetInteractionType.ADD_TO_CART_CLICKED]: InteractionClass.INTENTIONAL,
|
|
96
|
+
[WidgetInteractionType.ADD_TO_CART_FAILED]: InteractionClass.PASSIVE,
|
|
90
97
|
[WidgetInteractionType.TEXT_LINK_CLICKED]: InteractionClass.INTENTIONAL,
|
|
91
98
|
[WidgetInteractionType.ARTICLE_LINK_CLICKED]: InteractionClass.INTENTIONAL,
|
|
92
99
|
[WidgetInteractionType.REVIEW_CARD_CLICKED]: InteractionClass.INTENTIONAL,
|
|
@@ -95,6 +102,9 @@ export const INTERACTION_TYPE_CLASS: Record<WidgetInteractionType, InteractionCl
|
|
|
95
102
|
[WidgetInteractionType.VOICE_TRANSCRIPTION_STARTED]: InteractionClass.INTENTIONAL,
|
|
96
103
|
[WidgetInteractionType.VOICE_TRANSCRIPTION_COMPLETED]: InteractionClass.INTENTIONAL,
|
|
97
104
|
[WidgetInteractionType.OVERLAY_DISMISSED]: InteractionClass.NAVIGATIONAL,
|
|
105
|
+
[WidgetInteractionType.VARIANT_SELECTOR_OPENED]: InteractionClass.NAVIGATIONAL,
|
|
106
|
+
[WidgetInteractionType.VARIANT_OPTION_SELECTED]: InteractionClass.INTENTIONAL,
|
|
107
|
+
[WidgetInteractionType.VARIANT_SELECTOR_DISMISSED]: InteractionClass.NAVIGATIONAL,
|
|
98
108
|
};
|
|
99
109
|
|
|
100
110
|
export type URL = {
|
|
@@ -153,6 +163,14 @@ export type ProductCardClicked = {
|
|
|
153
163
|
product_card_clicked: Product;
|
|
154
164
|
};
|
|
155
165
|
|
|
166
|
+
export type AddToCartClicked = {
|
|
167
|
+
add_to_cart_clicked: Product;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export type AddToCartFailed = {
|
|
171
|
+
add_to_cart_failed: Product;
|
|
172
|
+
};
|
|
173
|
+
|
|
156
174
|
export type ReviewCardClicked = {
|
|
157
175
|
review_card_clicked: Review;
|
|
158
176
|
};
|
|
@@ -165,6 +183,24 @@ export type VoiceTranscription = {
|
|
|
165
183
|
transcription: string;
|
|
166
184
|
};
|
|
167
185
|
|
|
186
|
+
export type SelectedVariantOption = {
|
|
187
|
+
product_id: string;
|
|
188
|
+
option_name: string;
|
|
189
|
+
option_value: string;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
export type VariantSelectorOpened = {
|
|
193
|
+
variant_selector_opened: Product;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export type VariantOptionSelected = {
|
|
197
|
+
variant_option_selected: SelectedVariantOption;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export type VariantSelectorDismissed = {
|
|
201
|
+
variant_selector_dismissed: Product;
|
|
202
|
+
};
|
|
203
|
+
|
|
168
204
|
export type WidgetInteractionData =
|
|
169
205
|
| SuggestionScrolled
|
|
170
206
|
| SuggestionClicked
|
|
@@ -174,6 +210,11 @@ export type WidgetInteractionData =
|
|
|
174
210
|
| TextLinkClicked
|
|
175
211
|
| ArticleLinkClicked
|
|
176
212
|
| ProductCardClicked
|
|
213
|
+
| AddToCartClicked
|
|
214
|
+
| AddToCartFailed
|
|
177
215
|
| ReviewCardClicked
|
|
178
216
|
| MessageSubmitted
|
|
179
|
-
| VoiceTranscription
|
|
217
|
+
| VoiceTranscription
|
|
218
|
+
| VariantSelectorOpened
|
|
219
|
+
| VariantOptionSelected
|
|
220
|
+
| VariantSelectorDismissed;
|
|
@@ -105,8 +105,8 @@ describe('AmplitudeService', () => {
|
|
|
105
105
|
};
|
|
106
106
|
|
|
107
107
|
describe('Initialization', () => {
|
|
108
|
-
it('should initialize Amplitude client when all required config is provided', () => {
|
|
109
|
-
createService();
|
|
108
|
+
it('should initialize Amplitude client when all required config is provided', async () => {
|
|
109
|
+
await createService().initialized();
|
|
110
110
|
|
|
111
111
|
expect(mockInit).toHaveBeenCalledWith(
|
|
112
112
|
validApiKey,
|
|
@@ -516,9 +516,9 @@ describe('AmplitudeService', () => {
|
|
|
516
516
|
configurable: true,
|
|
517
517
|
});
|
|
518
518
|
|
|
519
|
-
createService({
|
|
519
|
+
await createService({
|
|
520
520
|
show: true,
|
|
521
|
-
});
|
|
521
|
+
}).initialized();
|
|
522
522
|
|
|
523
523
|
// Simulate a Page Viewed event by calling the enrichment plugin
|
|
524
524
|
// The enrichment is added during initialization
|
|
@@ -564,7 +564,7 @@ describe('AmplitudeService', () => {
|
|
|
564
564
|
configurable: true,
|
|
565
565
|
});
|
|
566
566
|
|
|
567
|
-
createService();
|
|
567
|
+
await createService().initialized();
|
|
568
568
|
|
|
569
569
|
const enrichmentCall = mockAdd.mock.calls[0];
|
|
570
570
|
const enrichment = enrichmentCall[0];
|
|
@@ -580,7 +580,7 @@ describe('AmplitudeService', () => {
|
|
|
580
580
|
});
|
|
581
581
|
|
|
582
582
|
it('should not enrich non-specified events', async () => {
|
|
583
|
-
createService();
|
|
583
|
+
await createService().initialized();
|
|
584
584
|
|
|
585
585
|
const enrichmentCall = mockAdd.mock.calls[0];
|
|
586
586
|
const enrichment = enrichmentCall[0];
|
|
@@ -659,8 +659,8 @@ describe('AmplitudeService', () => {
|
|
|
659
659
|
});
|
|
660
660
|
|
|
661
661
|
describe('Session replay initialization', () => {
|
|
662
|
-
it('should disable session replay by default', () => {
|
|
663
|
-
createService();
|
|
662
|
+
it('should disable session replay by default', async () => {
|
|
663
|
+
await createService().initialized();
|
|
664
664
|
|
|
665
665
|
// Session replay is always disabled in the current implementation
|
|
666
666
|
expect(mockInit).toHaveBeenCalledWith(
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { createInstance } from '@amplitude/analytics-browser';
|
|
2
1
|
import { EventsDispatcher, SpiffyEvent } from 'src/events';
|
|
3
2
|
import { LocalStorageKeys } from 'src/contexts/localStorageContext';
|
|
4
3
|
import Logger from 'src/application/logging/logger';
|
|
@@ -8,7 +7,7 @@ import type {
|
|
|
8
7
|
Event,
|
|
9
8
|
ServerZoneType,
|
|
10
9
|
} from '@amplitude/analytics-types';
|
|
11
|
-
import { FeatureFlagService } from 'src/
|
|
10
|
+
import { FeatureFlagService } from 'src/services/featureFlagService';
|
|
12
11
|
import { WidgetTypeV3 } from 'src/contexts/typesV3';
|
|
13
12
|
import { ChatElementDisplayLocationV3 } from 'src/application/models/chatElementDisplayLocationV3';
|
|
14
13
|
import { ExperimentNamespace, OrgConfigExperimentAssignment } from 'src/application/models';
|
|
@@ -80,24 +79,36 @@ export class AmplitudeService {
|
|
|
80
79
|
|
|
81
80
|
private config: AmplitudeServiceConfig;
|
|
82
81
|
|
|
82
|
+
private initPromise: Promise<void>;
|
|
83
|
+
|
|
83
84
|
constructor(config: AmplitudeServiceConfig) {
|
|
84
85
|
this.config = config;
|
|
85
|
-
this.initialize();
|
|
86
|
+
this.initPromise = this.initialize();
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
get isMockApiKey(): boolean {
|
|
89
90
|
return this.config.amplitudeApiKey === 'mock-amplitude-key';
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
// Must not depend on `amplitudeClient`: the SDK now loads via dynamic
|
|
94
|
+
// import, and AmplitudeProvider unmounts its entire subtree while
|
|
95
|
+
// isReady is false — gating on the client would block widget rendering
|
|
96
|
+
// on the analytics chunk download. Events awaited in trackEvent queue
|
|
97
|
+
// behind `initPromise` instead.
|
|
92
98
|
get isReady(): boolean {
|
|
93
99
|
return Boolean(
|
|
94
100
|
this.config.userId &&
|
|
95
101
|
this.config.featureFlagService &&
|
|
96
102
|
this.config.amplitudeApiKey &&
|
|
97
|
-
this.
|
|
103
|
+
!this.isMockApiKey,
|
|
98
104
|
);
|
|
99
105
|
}
|
|
100
106
|
|
|
107
|
+
/** Resolves once SDK initialization has settled (never rejects). */
|
|
108
|
+
initialized(): Promise<void> {
|
|
109
|
+
return this.initPromise;
|
|
110
|
+
}
|
|
111
|
+
|
|
101
112
|
private getLocalStorageItem(key: string): string | null {
|
|
102
113
|
if (this.config.getLocalStorageItem) {
|
|
103
114
|
return this.config.getLocalStorageItem(key);
|
|
@@ -246,7 +257,7 @@ export class AmplitudeService {
|
|
|
246
257
|
return enrichment;
|
|
247
258
|
}
|
|
248
259
|
|
|
249
|
-
private initialize(): void {
|
|
260
|
+
private async initialize(): Promise<void> {
|
|
250
261
|
const isReady = Boolean(
|
|
251
262
|
this.config.userId && this.config.featureFlagService && this.config.amplitudeApiKey,
|
|
252
263
|
);
|
|
@@ -270,25 +281,32 @@ export class AmplitudeService {
|
|
|
270
281
|
}
|
|
271
282
|
|
|
272
283
|
if (!this.amplitudeClient) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
trackHistoryChanges: 'pathOnly',
|
|
284
|
+
try {
|
|
285
|
+
// Dynamic import keeps the Amplitude SDK out of the eager
|
|
286
|
+
// injection-bundle chunk; trackEvent queues behind initPromise.
|
|
287
|
+
const { createInstance } = await import('@amplitude/analytics-browser');
|
|
288
|
+
const currentAmplitudeInstance: BrowserClient = createInstance();
|
|
289
|
+
|
|
290
|
+
currentAmplitudeInstance.add(this.getEventTrackingEnrichment());
|
|
291
|
+
currentAmplitudeInstance.init(this.config.amplitudeApiKey, this.config.userId, {
|
|
292
|
+
serverZone: this.config.dataResidency as ServerZoneType,
|
|
293
|
+
trackingOptions: {
|
|
294
|
+
ipAddress: true,
|
|
285
295
|
},
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
296
|
+
autocapture: {
|
|
297
|
+
attribution: true,
|
|
298
|
+
pageViews: {
|
|
299
|
+
trackHistoryChanges: 'pathOnly',
|
|
300
|
+
},
|
|
301
|
+
sessions: false,
|
|
302
|
+
formInteractions: false,
|
|
303
|
+
fileDownloads: false,
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
this.amplitudeClient = currentAmplitudeInstance;
|
|
307
|
+
} catch (err) {
|
|
308
|
+
logger.logError('Failed to load or initialize the Amplitude SDK', err);
|
|
309
|
+
}
|
|
292
310
|
}
|
|
293
311
|
}
|
|
294
312
|
|
|
@@ -396,6 +414,7 @@ export class AmplitudeService {
|
|
|
396
414
|
alsoSendToGoogleAnalytics = false,
|
|
397
415
|
}: TrackEventParams): Promise<void> {
|
|
398
416
|
if (this.isMockApiKey) return;
|
|
417
|
+
await this.initPromise;
|
|
399
418
|
logger.logDebug('Submitting event', eventName);
|
|
400
419
|
try {
|
|
401
420
|
const decoratedEventName = AmplitudeService.decorateEventName(eventName);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { OrgConfigExperimentAssignment, OrgConfigFeatureGate } from 'src/application/models';
|
|
2
2
|
import { FeatureGates } from 'src/application/models/featureGates';
|
|
3
|
-
import { FeatureFlagService } from 'src/
|
|
4
|
-
import { GraphQlConfigValues } from 'src/contexts/graphqlContext';
|
|
3
|
+
import { FeatureFlagService } from 'src/services/featureFlagService';
|
|
4
|
+
import type { GraphQlConfigValues } from 'src/contexts/graphqlContext';
|
|
5
5
|
import { configVersionOverride } from 'src/types/config-versions';
|
|
6
6
|
import { fetchGraphQLConfig, fetchUnifiedGraphQLConfig } from './fetchGraphQLConfig';
|
|
7
7
|
|
|
@@ -7,13 +7,16 @@ import {
|
|
|
7
7
|
} from 'src/application/models';
|
|
8
8
|
import { FrontendConfigV3 } from 'src/application/models/frontendConfigV3';
|
|
9
9
|
import { ColorMappingV3 } from 'src/application/models/colorsConfigV3';
|
|
10
|
-
import
|
|
10
|
+
// Type-only: a value import would pull the FloatingButton component (and
|
|
11
|
+
// React) through the package's component index into the injection bundle's
|
|
12
|
+
// pre-config chunk.
|
|
13
|
+
import type { FloatingButtonLocation } from '@envive-ai/react-toolkit-v3/FloatingButton';
|
|
11
14
|
import { getColorsAndFrontendQuery } from 'src/application/models/graphql/queries/getColorsAndFrontendQuery';
|
|
12
15
|
import { getWidgetConfigQuery } from 'src/application/models/graphql/queries/getWidgetConfigQuery';
|
|
13
16
|
import { mockV3ColorsConfig, mockV3FrontendConfig } from 'src/contexts/graphqlContext/mockV3Config';
|
|
14
17
|
import { WidgetConfigV3 } from 'src/contexts/typesV3';
|
|
15
18
|
import { PageVariantConfig, PageVariantTestType, WidgetMountingConfig } from 'src/contexts/types';
|
|
16
|
-
import { GraphQlConfigValues } from 'src/contexts/graphqlContext';
|
|
19
|
+
import type { GraphQlConfigValues } from 'src/contexts/graphqlContext';
|
|
17
20
|
import { configVersionOverride } from 'src/types/config-versions';
|
|
18
21
|
import { ExperimentConfigResolutionMetadata } from 'src/application/models/api/orgConfigResults';
|
|
19
22
|
|
|
@@ -39,11 +42,19 @@ export const DEFAULT_PAGE_VARIANTS: PageVariantConfig[] = [
|
|
|
39
42
|
},
|
|
40
43
|
];
|
|
41
44
|
|
|
45
|
+
// String-literal values of the FloatingButtonLocation enum; keep in sync
|
|
46
|
+
// with @envive-ai/react-toolkit-v3/FloatingButton/types/types.ts.
|
|
47
|
+
const FLOATING_BUTTON_POSITIONS = [
|
|
48
|
+
'bottom-left',
|
|
49
|
+
'middle-left',
|
|
50
|
+
'middle-right',
|
|
51
|
+
'bottom-right',
|
|
52
|
+
] as const;
|
|
53
|
+
|
|
42
54
|
const mapPositionToCamelCase = (position: string): FloatingButtonLocation => {
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (position === 'bottom-right') return FloatingButtonLocation.BOTTOM_RIGHT;
|
|
55
|
+
if ((FLOATING_BUTTON_POSITIONS as readonly string[]).includes(position)) {
|
|
56
|
+
return position as FloatingButtonLocation;
|
|
57
|
+
}
|
|
47
58
|
throw new Error(`Invalid position: ${position}`);
|
|
48
59
|
};
|
|
49
60
|
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { OrgConfigFeatureGate } from 'src/application/models/api/orgConfigResults';
|
|
2
|
+
import { FeatureGates } from 'src/application/models/featureGates';
|
|
3
|
+
import Logger from 'src/application/logging/logger';
|
|
4
|
+
|
|
5
|
+
const FEATURE_FLAGS_STORAGE_KEY = 'spiffy-feature-flags';
|
|
6
|
+
|
|
7
|
+
const logger = new Logger('featureFlagService');
|
|
8
|
+
|
|
9
|
+
export interface FullFlagValue {
|
|
10
|
+
resolved: boolean;
|
|
11
|
+
initial: boolean | undefined;
|
|
12
|
+
query_param_override: boolean | undefined;
|
|
13
|
+
window_param_override: boolean | undefined;
|
|
14
|
+
stored_override: boolean | undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Kept free of React imports: the injection bundle constructs this service
|
|
18
|
+
// before deciding whether to load the React app, and pulling React into
|
|
19
|
+
// that pre-config code path would put it on the eager critical chunk.
|
|
20
|
+
export class FeatureFlagService {
|
|
21
|
+
private featureGates: OrgConfigFeatureGate[];
|
|
22
|
+
|
|
23
|
+
constructor(featureGates: OrgConfigFeatureGate[]) {
|
|
24
|
+
this.featureGates = featureGates;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static getQueryParamFeatureGateOverrides(featureGate: FeatureGates): boolean | undefined {
|
|
28
|
+
const url = window.location?.href ?? '';
|
|
29
|
+
if (url.includes('?')) {
|
|
30
|
+
const urlObj = new URL(url);
|
|
31
|
+
const params = new URLSearchParams(urlObj.search);
|
|
32
|
+
const paramsObj = Object.fromEntries(params.entries());
|
|
33
|
+
const value = paramsObj[featureGate];
|
|
34
|
+
|
|
35
|
+
if (value != null) {
|
|
36
|
+
return value === 'true';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static getWindowFeatureGateOverrides(featureGate: FeatureGates): boolean | undefined {
|
|
44
|
+
if (typeof window === 'undefined') {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
const windowOverride = window as Window & {
|
|
48
|
+
_envive?: { featureOverrides?: Record<string, boolean> };
|
|
49
|
+
_spiffy?: { featureOverrides?: Record<string, boolean> };
|
|
50
|
+
};
|
|
51
|
+
const featureOverrides =
|
|
52
|
+
windowOverride?._envive?.featureOverrides ?? windowOverride?._spiffy?.featureOverrides;
|
|
53
|
+
const value = featureOverrides?.[String(featureGate)];
|
|
54
|
+
if (value != null) {
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
static getStoredFeatureGateOverrides(featureGate: FeatureGates): boolean | undefined {
|
|
61
|
+
if (typeof window === 'undefined' || typeof window.localStorage === 'undefined') {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const featureFlags = window.localStorage.getItem(FEATURE_FLAGS_STORAGE_KEY);
|
|
66
|
+
if (!featureFlags) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
const featureFlagsObj = JSON.parse(featureFlags) as Record<string, boolean>;
|
|
70
|
+
return featureFlagsObj[featureGate];
|
|
71
|
+
} catch (err) {
|
|
72
|
+
logger.logError('getStoredFeatureGateOverrides: error parsing feature flags', err);
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
static getFeatureFlagOverride(featureGate: FeatureGates): boolean | undefined {
|
|
78
|
+
const queryOverride = FeatureFlagService.getQueryParamFeatureGateOverrides(featureGate);
|
|
79
|
+
if (queryOverride != null) {
|
|
80
|
+
logger.logDebug(
|
|
81
|
+
`isFeatureGateEnabled featureGate:${featureGate} overridden by URL param. New value: ${queryOverride}`,
|
|
82
|
+
);
|
|
83
|
+
return queryOverride;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const windowOverride = FeatureFlagService.getWindowFeatureGateOverrides(featureGate);
|
|
87
|
+
if (windowOverride != null) {
|
|
88
|
+
logger.logDebug(
|
|
89
|
+
`isFeatureGateEnabled featureGate:${featureGate} overridden by window param. New value: ${windowOverride}`,
|
|
90
|
+
);
|
|
91
|
+
return windowOverride;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const storedOverride = FeatureFlagService.getStoredFeatureGateOverrides(featureGate);
|
|
95
|
+
if (storedOverride != null) {
|
|
96
|
+
logger.logDebug(
|
|
97
|
+
`isFeatureGateEnabled featureGate:${featureGate} overridden by stored value. New value: ${storedOverride}`,
|
|
98
|
+
);
|
|
99
|
+
return storedOverride;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
isFeatureGateEnabled = (featureGate: FeatureGates): boolean => {
|
|
106
|
+
const gateValue = this.featureGates.find(gate => gate.name === featureGate);
|
|
107
|
+
|
|
108
|
+
const featureFlagOverride = FeatureFlagService.getFeatureFlagOverride(featureGate);
|
|
109
|
+
if (featureFlagOverride !== undefined) {
|
|
110
|
+
return featureFlagOverride;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (gateValue == null || gateValue.value == null) {
|
|
114
|
+
logger.logDebug(
|
|
115
|
+
`isFeatureGateEnabled featureGate:${featureGate} value is undefined - returning false`,
|
|
116
|
+
);
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
return gateValue.value;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// Ensure that standard overrides work as expected
|
|
123
|
+
isClientSessionEnabled = (): boolean =>
|
|
124
|
+
this.isFeatureGateEnabled(FeatureGates.IsClientSessionEnabled);
|
|
125
|
+
|
|
126
|
+
getFeatureFlags = (): Record<string, boolean> => {
|
|
127
|
+
return Object.fromEntries(
|
|
128
|
+
Object.values(FeatureGates).map((featureGate: FeatureGates) => [
|
|
129
|
+
featureGate,
|
|
130
|
+
this.isFeatureGateEnabled(featureGate),
|
|
131
|
+
]),
|
|
132
|
+
);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
getFullFlagValue = (featureGate: FeatureGates): FullFlagValue => {
|
|
136
|
+
const gateValue = this.featureGates.find(gate => gate.name === featureGate);
|
|
137
|
+
return {
|
|
138
|
+
resolved: this.isFeatureGateEnabled(featureGate),
|
|
139
|
+
initial: gateValue?.value,
|
|
140
|
+
query_param_override: FeatureFlagService.getQueryParamFeatureGateOverrides(featureGate),
|
|
141
|
+
window_param_override: FeatureFlagService.getWindowFeatureGateOverrides(featureGate),
|
|
142
|
+
stored_override: FeatureFlagService.getStoredFeatureGateOverrides(featureGate),
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
getFullFlagValues = (): Record<string, FullFlagValue> => {
|
|
147
|
+
const featureGates = Object.values(FeatureGates).map((featureGate: FeatureGates) => [
|
|
148
|
+
featureGate,
|
|
149
|
+
this.getFullFlagValue(featureGate),
|
|
150
|
+
]);
|
|
151
|
+
|
|
152
|
+
return featureGates.length > 0
|
|
153
|
+
? featureGates.reduce<Record<string, FullFlagValue>>((acc, entry) => {
|
|
154
|
+
const [name, value] = entry as [FeatureGates, FullFlagValue];
|
|
155
|
+
if (name && value != null) {
|
|
156
|
+
return { ...acc, [`feature_gate.${name}`]: value };
|
|
157
|
+
}
|
|
158
|
+
return acc;
|
|
159
|
+
}, {})
|
|
160
|
+
: {};
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
static persistFeatureGateOverrides(): void {
|
|
164
|
+
if (typeof window === 'undefined' || typeof window.localStorage === 'undefined') {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const overrides = Object.values(FeatureGates)
|
|
168
|
+
.map(
|
|
169
|
+
featureGate =>
|
|
170
|
+
[featureGate, FeatureFlagService.getFeatureFlagOverride(featureGate)] as const,
|
|
171
|
+
)
|
|
172
|
+
.filter(([, value]) => value !== undefined)
|
|
173
|
+
.reduce<Record<string, boolean>>((acc, [key, value]) => ({ ...acc, [key]: value! }), {});
|
|
174
|
+
window.localStorage.setItem(FEATURE_FLAGS_STORAGE_KEY, JSON.stringify(overrides));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -229,6 +229,42 @@ describe('projectToGA4', () => {
|
|
|
229
229
|
expect(window.dataLayer[0]).toHaveProperty('interaction_suggestion_id', 'sug-456');
|
|
230
230
|
});
|
|
231
231
|
|
|
232
|
+
it('should extract product_id and option_name but not option_value for variant_option_selected', () => {
|
|
233
|
+
projectToGA4(EnviveMetricsEventName.WidgetInteraction, {
|
|
234
|
+
'context.page_type': 'pdp',
|
|
235
|
+
'context.page_id': 'product-123',
|
|
236
|
+
'trigger.widget': 'floating_chat',
|
|
237
|
+
'trigger.widget_interaction': 'variant_option_selected',
|
|
238
|
+
'trigger.widget_interaction_data': {
|
|
239
|
+
variant_option_selected: {
|
|
240
|
+
product_id: 'sku-789',
|
|
241
|
+
option_name: 'Color',
|
|
242
|
+
option_value: 'Grey',
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const pushed = window.dataLayer[0];
|
|
248
|
+
expect(pushed).toHaveProperty('interaction_product_id', 'sku-789');
|
|
249
|
+
expect(pushed).toHaveProperty('interaction_option_name', 'Color');
|
|
250
|
+
expect(pushed).not.toHaveProperty('interaction_option_value');
|
|
251
|
+
expect(pushed).not.toHaveProperty('option_value');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('should extract interaction_product_id for variant selector open/dismiss', () => {
|
|
255
|
+
projectToGA4(EnviveMetricsEventName.WidgetInteraction, {
|
|
256
|
+
'context.page_type': 'pdp',
|
|
257
|
+
'context.page_id': 'product-123',
|
|
258
|
+
'trigger.widget': 'floating_chat',
|
|
259
|
+
'trigger.widget_interaction': 'variant_selector_dismissed',
|
|
260
|
+
'trigger.widget_interaction_data': {
|
|
261
|
+
variant_selector_dismissed: { product_id: 'sku-789' },
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
expect(window.dataLayer[0]).toHaveProperty('interaction_product_id', 'sku-789');
|
|
266
|
+
});
|
|
267
|
+
|
|
232
268
|
it('should drop widget_interaction_data for unwhitelisted interactions', () => {
|
|
233
269
|
projectToGA4(EnviveMetricsEventName.WidgetInteraction, {
|
|
234
270
|
'context.page_type': 'pdp',
|
|
@@ -52,6 +52,13 @@ const WIDGET_INTERACTION_DATA_PROJECTIONS: WidgetInteractionDataProjection = {
|
|
|
52
52
|
widget_collapsed: { interaction_collapse_source: 'widget_collapsed.collapse_source' },
|
|
53
53
|
product_card_clicked: { interaction_product_id: 'product_card_clicked.product_id' },
|
|
54
54
|
suggestion_scrolled: { interaction_suggestion_id: 'suggestion_scrolled.suggestion_id' },
|
|
55
|
+
variant_selector_opened: { interaction_product_id: 'variant_selector_opened.product_id' },
|
|
56
|
+
// option_value is intentionally excluded — merchant-defined values are high cardinality
|
|
57
|
+
variant_option_selected: {
|
|
58
|
+
interaction_product_id: 'variant_option_selected.product_id',
|
|
59
|
+
interaction_option_name: 'variant_option_selected.option_name',
|
|
60
|
+
},
|
|
61
|
+
variant_selector_dismissed: { interaction_product_id: 'variant_selector_dismissed.product_id' },
|
|
55
62
|
};
|
|
56
63
|
|
|
57
64
|
export const GA4_EVENT_SCHEMA: Record<EnviveMetricsEventName, GA4EventSchemaEntry> = {
|