@envive-ai/react-hooks 0.3.45 → 0.3.47
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/api/widgetText.d.cts +2 -1
- package/dist/application/models/api/widgetText.d.ts +2 -1
- 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/index.cjs +1 -1
- package/dist/application/models/index.js +1 -1
- package/dist/application/models/validators/validateGraphQLFrontendConfig.cjs +1 -1
- package/dist/application/models/validators/validateGraphQLFrontendConfig.js +1 -1
- 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/elementObserver.d.cts +2 -2
- package/dist/application/utils/elementObserver.d.ts +2 -2
- 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/application/utils/widgetTextFromApiWidgetTextResponse.cjs +3 -2
- package/dist/application/utils/widgetTextFromApiWidgetTextResponse.js +3 -2
- 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.cjs +2 -2
- package/dist/atoms/app/variant.d.cts +7 -7
- package/dist/atoms/app/variant.d.ts +7 -7
- package/dist/atoms/app/variant.js +2 -2
- 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 +2 -2
- package/dist/atoms/chat/index.cjs +1 -1
- package/dist/atoms/chat/index.d.cts +2 -2
- package/dist/atoms/chat/index.d.ts +2 -2
- package/dist/atoms/chat/index.js +1 -1
- package/dist/atoms/chat/lastMessage.d.cts +2 -2
- package/dist/atoms/chat/lastMessage.d.ts +2 -2
- package/dist/atoms/chat/messageQueue.d.cts +6 -6
- package/dist/atoms/chat/messageQueue.d.ts +6 -6
- package/dist/atoms/chat/performanceMetrics.d.cts +6 -6
- package/dist/atoms/chat/performanceMetrics.d.ts +6 -6
- package/dist/atoms/chat/renderedWidgetRefs.d.cts +2 -2
- package/dist/atoms/chat/renderedWidgetRefs.d.ts +2 -2
- package/dist/atoms/chat/replies.d.cts +3 -3
- package/dist/atoms/chat/replies.d.ts +2 -2
- package/dist/atoms/chat/suggestions.d.cts +2 -2
- package/dist/atoms/chat/suggestions.d.ts +2 -2
- 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/index.cjs +1 -1
- package/dist/atoms/org/index.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.cjs +1 -1
- package/dist/atoms/search/chatSearch.d.cts +17 -17
- package/dist/atoms/search/chatSearch.d.ts +17 -17
- package/dist/atoms/search/chatSearch.js +1 -1
- package/dist/atoms/search/searchAPI.cjs +1 -1
- package/dist/atoms/search/searchAPI.d.cts +13 -13
- package/dist/atoms/search/searchAPI.d.ts +13 -13
- package/dist/atoms/search/searchAPI.js +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/enviveContext/types.d.cts +1 -1
- package/dist/contexts/enviveContext/types.d.ts +1 -1
- 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/pageContext/pageContext.cjs +2 -2
- package/dist/contexts/pageContext/pageContext.js +2 -2
- package/dist/contexts/salesAgentContext/chatAPI.cjs +15 -4
- package/dist/contexts/salesAgentContext/chatAPI.d.cts +4 -1
- package/dist/contexts/salesAgentContext/chatAPI.d.ts +4 -1
- package/dist/contexts/salesAgentContext/chatAPI.js +16 -5
- package/dist/contexts/salesAgentContext/salesAgentContext.cjs +15 -4
- package/dist/contexts/salesAgentContext/salesAgentContext.d.cts +2 -1
- package/dist/contexts/salesAgentContext/salesAgentContext.d.ts +2 -1
- package/dist/contexts/salesAgentContext/salesAgentContext.js +16 -5
- package/dist/contexts/salesAgentContext/salesAgentService.cjs +12 -4
- package/dist/contexts/salesAgentContext/salesAgentService.js +12 -4
- package/dist/contexts/searchContext/searchContext.cjs +1 -1
- package/dist/contexts/searchContext/searchContext.js +1 -1
- package/dist/contexts/systemSettingsContext/systemSettingsContext.d.cts +2 -2
- package/dist/contexts/systemSettingsContext/systemSettingsContext.d.ts +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/contexts/widgetConfigContext/widgetConfigContext.cjs +1 -1
- package/dist/contexts/widgetConfigContext/widgetConfigContext.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/ChatToggle/useChatToggle.cjs +1 -1
- package/dist/hooks/ChatToggle/useChatToggle.js +1 -1
- package/dist/hooks/LocalStorageOperations/useLocalStorageOperations.cjs +6 -5
- package/dist/hooks/LocalStorageOperations/useLocalStorageOperations.js +3 -2
- package/dist/hooks/PageViewedEvent/usePageViewedEvent.cjs +6 -3
- package/dist/hooks/PageViewedEvent/usePageViewedEvent.js +6 -3
- 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.cjs +3 -3
- package/dist/hooks/Search/useSearch.d.cts +1 -1
- package/dist/hooks/Search/useSearch.d.ts +1 -1
- package/dist/hooks/Search/useSearch.js +3 -3
- package/dist/hooks/SystemSettingsContext/useSystemSettingsContext.d.cts +2 -2
- 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 +13 -3
- package/dist/hooks/WidgetInteraction/types.d.cts +29 -4
- package/dist/hooks/WidgetInteraction/types.d.ts +29 -4
- package/dist/hooks/WidgetInteraction/types.js +13 -3
- package/dist/hooks/WidgetInteraction/utils.cjs +2 -2
- package/dist/hooks/WidgetInteraction/utils.js +2 -2
- package/dist/hooks/utils.d.cts +1 -1
- package/dist/hooks/utils.d.ts +1 -1
- package/dist/services/amplitudeService/amplitudeService.cjs +18 -11
- package/dist/services/amplitudeService/amplitudeService.d.cts +5 -2
- package/dist/services/amplitudeService/amplitudeService.d.ts +5 -2
- package/dist/services/amplitudeService/amplitudeService.js +16 -8
- package/dist/services/enviveConfigService/enviveConfigService.cjs +36 -4
- package/dist/services/enviveConfigService/enviveConfigService.d.cts +4 -1
- package/dist/services/enviveConfigService/enviveConfigService.d.ts +4 -1
- package/dist/services/enviveConfigService/enviveConfigService.js +35 -3
- package/dist/services/enviveConfigService/fetchGraphQLConfig.cjs +9 -8
- package/dist/services/enviveConfigService/fetchGraphQLConfig.js +9 -7
- 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.cjs +4 -3
- package/dist/services/hardcopyService/hardcopyService.d.cts +1 -1
- package/dist/services/hardcopyService/hardcopyService.d.ts +1 -1
- package/dist/services/hardcopyService/hardcopyService.js +4 -3
- 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 +18 -2
- 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/api/widgetText.ts +3 -0
- 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/application/utils/widgetTextFromApiWidgetTextResponse.ts +1 -0
- package/src/atoms/app/index.ts +1 -1
- package/src/atoms/app/variant.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/pageContext/pageContext.tsx +1 -1
- package/src/contexts/salesAgentContext/__tests__/chatAPI.test.tsx +63 -0
- package/src/contexts/salesAgentContext/chatAPI.ts +20 -1
- package/src/contexts/salesAgentContext/salesAgentContext.tsx +17 -0
- package/src/contexts/salesAgentContext/salesAgentService.ts +12 -3
- 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/__tests__/usePageViewedEvent.test.ts +4 -4
- package/src/hooks/PageViewedEvent/usePageViewedEvent.ts +5 -1
- 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 +43 -2
- package/src/hooks/WidgetInteraction/utils.ts +1 -1
- package/src/services/amplitudeService/__tests__/amplitudeService.test.ts +8 -8
- package/src/services/amplitudeService/amplitudeService.ts +43 -23
- package/src/services/enviveConfigService/__tests__/enviveConfigService.test.ts +208 -0
- package/src/services/enviveConfigService/enviveConfigService.ts +62 -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/hardcopyService/__tests__/hardcopyService.test.ts +47 -1
- package/src/services/hardcopyService/hardcopyService.ts +5 -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
|
@@ -1,9 +1,21 @@
|
|
|
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
|
+
import { configVersionOverride } from 'src/types/config-versions';
|
|
5
6
|
import { fetchGraphQLConfig, fetchUnifiedGraphQLConfig } from './fetchGraphQLConfig';
|
|
6
7
|
|
|
8
|
+
// Short-lived so config changes (gates, experiments) still propagate within
|
|
9
|
+
// a session, while sparing every page navigation from re-fetching org
|
|
10
|
+
// config + GraphQL config, both of which sit on the critical path before
|
|
11
|
+
// first paint.
|
|
12
|
+
const CONFIG_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
13
|
+
|
|
14
|
+
type CachedConfigEntry = {
|
|
15
|
+
value: EnviveServiceConfig;
|
|
16
|
+
expiresAt: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
7
19
|
type EnviveConfigServiceProps = {
|
|
8
20
|
baseUrl: string;
|
|
9
21
|
apiKey: string;
|
|
@@ -63,10 +75,57 @@ export class EnviveConfigService implements IEnviveConfigService {
|
|
|
63
75
|
this.source = source;
|
|
64
76
|
}
|
|
65
77
|
|
|
78
|
+
private getCacheKey(): string {
|
|
79
|
+
return `envive-config-cache:${this.baseUrl}:${this.apiKey}:${this.userId}:${this.namespace}:${this.source}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// `envive_config_version` / `spiffy_config_version` pin a specific config
|
|
83
|
+
// version for debugging/rollback. Any use of that override must always
|
|
84
|
+
// bypass the cache below — both reading a stale entry and writing one
|
|
85
|
+
// under the shared key would defeat the override's purpose.
|
|
86
|
+
private readCachedConfig(): EnviveServiceConfig | null {
|
|
87
|
+
if (configVersionOverride()) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const raw = sessionStorage.getItem(this.getCacheKey());
|
|
92
|
+
if (!raw) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
const entry = JSON.parse(raw) as CachedConfigEntry;
|
|
96
|
+
if (Date.now() >= entry.expiresAt) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
return entry.value;
|
|
100
|
+
} catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private writeCachedConfig(value: EnviveServiceConfig): void {
|
|
106
|
+
if (configVersionOverride()) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const entry: CachedConfigEntry = { value, expiresAt: Date.now() + CONFIG_CACHE_TTL_MS };
|
|
111
|
+
sessionStorage.setItem(this.getCacheKey(), JSON.stringify(entry));
|
|
112
|
+
} catch {
|
|
113
|
+
// Storage disabled, full, or unavailable (private browsing, etc.) -
|
|
114
|
+
// fail open and let the next call re-fetch instead of caching.
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
66
118
|
async getEnviveConfig(): Promise<EnviveServiceConfig> {
|
|
67
119
|
if (this.response !== null) {
|
|
68
120
|
return this.response;
|
|
69
121
|
}
|
|
122
|
+
|
|
123
|
+
const cached = this.readCachedConfig();
|
|
124
|
+
if (cached !== null) {
|
|
125
|
+
this.response = cached;
|
|
126
|
+
return this.response;
|
|
127
|
+
}
|
|
128
|
+
|
|
70
129
|
const queryParams = new URLSearchParams({
|
|
71
130
|
namespace: this.namespace,
|
|
72
131
|
source: this.source,
|
|
@@ -106,6 +165,7 @@ export class EnviveConfigService implements IEnviveConfigService {
|
|
|
106
165
|
: await fetchGraphQLConfig(this.baseUrl, this.apiKey);
|
|
107
166
|
|
|
108
167
|
this.response = { ...orgConfigResponse, orgConfig };
|
|
168
|
+
this.writeCachedConfig(this.response);
|
|
109
169
|
return this.response;
|
|
110
170
|
}
|
|
111
171
|
}
|
|
@@ -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> = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { WidgetTypeV3 } from 'src/contexts/typesV3';
|
|
2
2
|
import type { WidgetText } from 'src/application/models/api/widgetText';
|
|
3
3
|
import type { AppDetails } from 'src/atoms/app';
|
|
4
|
-
import { EnviveMetricsEventName } from 'src/services/amplitudeService';
|
|
4
|
+
import { EnviveMetricsEventName, TrackEventParams } from 'src/services/amplitudeService';
|
|
5
5
|
import { getAtomStore } from 'src/atoms/atomStore/atomStore';
|
|
6
6
|
import { resolvedBaseConfigVersionAtom } from 'src/atoms/envive/resolvedBaseConfigVersion';
|
|
7
7
|
import { HardcopyService, MOCK_HARDCOPY_RESPONSE, MockHardcopyService } from '../hardcopyService';
|
|
@@ -269,6 +269,52 @@ describe('HardcopyService', () => {
|
|
|
269
269
|
);
|
|
270
270
|
});
|
|
271
271
|
|
|
272
|
+
it('forwards ai_suggestions_attribution_by_slot on WidgetTextResponse when present', async () => {
|
|
273
|
+
const trackEvent = vi.fn();
|
|
274
|
+
const attribution = {
|
|
275
|
+
prompt_button_texts: {
|
|
276
|
+
served_method_id: 'variant_b',
|
|
277
|
+
served_method_type: 'bandit_pbm',
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
mockGetHardcopy.mockResolvedValueOnce(
|
|
281
|
+
makeWidgetTextResponse({ aiSuggestionsAttributionBySlot: attribution }),
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
await makeService(trackEvent).getHardcopy({
|
|
285
|
+
widgetType: WidgetTypeV3.ChatPreviewV3,
|
|
286
|
+
userEvent: {} as any,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
expect(trackEvent).toHaveBeenCalledWith(
|
|
290
|
+
expect.objectContaining({
|
|
291
|
+
eventName: EnviveMetricsEventName.WidgetTextResponse,
|
|
292
|
+
eventProps: expect.objectContaining({
|
|
293
|
+
ai_suggestions_attribution_by_slot: attribution,
|
|
294
|
+
}),
|
|
295
|
+
}),
|
|
296
|
+
);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('omits ai_suggestions_attribution_by_slot when the response carries none', async () => {
|
|
300
|
+
const trackEvent = vi.fn();
|
|
301
|
+
mockGetHardcopy.mockResolvedValueOnce(makeWidgetTextResponse());
|
|
302
|
+
|
|
303
|
+
await makeService(trackEvent).getHardcopy({
|
|
304
|
+
widgetType: WidgetTypeV3.ChatPreviewV3,
|
|
305
|
+
userEvent: {} as any,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const responseEvent = trackEvent.mock.calls
|
|
309
|
+
.map(([params]: [TrackEventParams]) => params)
|
|
310
|
+
.find(
|
|
311
|
+
(params: TrackEventParams) =>
|
|
312
|
+
params.eventName === EnviveMetricsEventName.WidgetTextResponse,
|
|
313
|
+
);
|
|
314
|
+
expect(responseEvent).toBeDefined();
|
|
315
|
+
expect(responseEvent!.eventProps).not.toHaveProperty('ai_suggestions_attribution_by_slot');
|
|
316
|
+
});
|
|
317
|
+
|
|
272
318
|
it('does not crash when trackEvent is not provided', async () => {
|
|
273
319
|
const service = new HardcopyService({
|
|
274
320
|
getAppDetails: () => mockAppDetails,
|
|
@@ -237,6 +237,11 @@ export class HardcopyService implements IHardcopyService {
|
|
|
237
237
|
response_id: response.responseId,
|
|
238
238
|
widget_text: response.values,
|
|
239
239
|
request_id: widgetTextRequest.requestId,
|
|
240
|
+
// Which method served each overridden slot — training/attribution
|
|
241
|
+
// reads this from the analytics event; passed through verbatim.
|
|
242
|
+
...(response.aiSuggestionsAttributionBySlot
|
|
243
|
+
? { ai_suggestions_attribution_by_slot: response.aiSuggestionsAttributionBySlot }
|
|
244
|
+
: {}),
|
|
240
245
|
},
|
|
241
246
|
});
|
|
242
247
|
return convertedResponse;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { createStore } from 'jotai';
|
|
2
|
+
import { getAtomStore, setAtomStoreForTesting } from 'src/atoms/atomStore/atomStore';
|
|
3
|
+
import { contextSourceAtom, envAtom, enviveConfigAtom } from 'src/atoms/envive/enviveConfig';
|
|
4
|
+
import { featureFlagServiceAtom } from 'src/atoms/org/graphqlConfig';
|
|
5
|
+
import { FeatureFlagService } from 'src/services/featureFlagService';
|
|
6
|
+
import type { EnviveServiceConfig } from 'src/services/enviveConfigService';
|
|
7
|
+
import { warmUrlResolver } from '../index';
|
|
8
|
+
|
|
9
|
+
const { mockResolveUrl } = vi.hoisted(() => ({ mockResolveUrl: vi.fn() }));
|
|
10
|
+
|
|
11
|
+
vi.mock('src/application/commerce-api', () => ({
|
|
12
|
+
default: { resolveUrl: mockResolveUrl },
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
const enviveServiceConfig = {
|
|
16
|
+
org: { org: { short_name: 'test-org', id: 'org-1' } },
|
|
17
|
+
} as EnviveServiceConfig;
|
|
18
|
+
|
|
19
|
+
const baseParams = {
|
|
20
|
+
baseUrl: 'https://commerce-api.spiffy.ai',
|
|
21
|
+
apiKey: 'test-key',
|
|
22
|
+
enviveServiceConfig,
|
|
23
|
+
show: true,
|
|
24
|
+
enviveOn: false,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
describe('warmUrlResolver', () => {
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
setAtomStoreForTesting(createStore());
|
|
30
|
+
window.sessionStorage.clear();
|
|
31
|
+
vi.clearAllMocks();
|
|
32
|
+
mockResolveUrl.mockResolvedValue({ ready: true });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('seeds env, contextSource, and the feature flag service before resolving', async () => {
|
|
36
|
+
const featureFlagService = new FeatureFlagService([]);
|
|
37
|
+
|
|
38
|
+
let envAtCallTime: string | undefined;
|
|
39
|
+
let contextSourceAtCallTime: string | undefined;
|
|
40
|
+
let flagServiceAtCallTime: FeatureFlagService | undefined;
|
|
41
|
+
mockResolveUrl.mockImplementation(async () => {
|
|
42
|
+
const store = getAtomStore();
|
|
43
|
+
envAtCallTime = store.get(envAtom);
|
|
44
|
+
contextSourceAtCallTime = store.get(contextSourceAtom);
|
|
45
|
+
flagServiceAtCallTime = store.get(featureFlagServiceAtom)?.featureFlagService;
|
|
46
|
+
return { ready: true };
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
await warmUrlResolver({
|
|
50
|
+
...baseParams,
|
|
51
|
+
env: 'prod',
|
|
52
|
+
contextSource: 'app',
|
|
53
|
+
featureFlagService,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(mockResolveUrl).toHaveBeenCalledTimes(1);
|
|
57
|
+
expect(envAtCallTime).toBe('prod');
|
|
58
|
+
expect(contextSourceAtCallTime).toBe('app');
|
|
59
|
+
expect(flagServiceAtCallTime).toBe(featureFlagService);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('does not overwrite atoms a provider has already seeded', async () => {
|
|
63
|
+
const store = getAtomStore();
|
|
64
|
+
const existingFlagService = new FeatureFlagService([]);
|
|
65
|
+
store.set(enviveConfigAtom, { identifyingPrefix: 'test', env: 'dev' });
|
|
66
|
+
store.set(featureFlagServiceAtom, { featureFlagService: existingFlagService });
|
|
67
|
+
|
|
68
|
+
await warmUrlResolver({
|
|
69
|
+
...baseParams,
|
|
70
|
+
env: 'prod',
|
|
71
|
+
contextSource: 'playground',
|
|
72
|
+
featureFlagService: new FeatureFlagService([]),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(store.get(envAtom)).toBe('dev');
|
|
76
|
+
expect(store.get(featureFlagServiceAtom)?.featureFlagService).toBe(existingFlagService);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('leaves the feature flag service atom unset when none is provided', async () => {
|
|
80
|
+
await warmUrlResolver(baseParams);
|
|
81
|
+
|
|
82
|
+
expect(getAtomStore().get(featureFlagServiceAtom)).toBeUndefined();
|
|
83
|
+
expect(mockResolveUrl).toHaveBeenCalledTimes(1);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import CommerceApiClient from 'src/application/commerce-api';
|
|
2
|
+
import Logger from 'src/application/logging/logger';
|
|
3
|
+
import { UrlResolverResponse, urlResolverAtom } from 'src/atoms/app/variant';
|
|
4
|
+
import { getAtomStore } from 'src/atoms/atomStore/atomStore';
|
|
5
|
+
import { enviveConfigAtom } from 'src/atoms/envive/enviveConfig';
|
|
6
|
+
import { resolvedBaseConfigVersionAtom } from 'src/atoms/envive/resolvedBaseConfigVersion';
|
|
7
|
+
import { featureFlagServiceAtom } from 'src/atoms/org/graphqlConfig';
|
|
8
|
+
import { EnviveServiceConfig } from 'src/services/enviveConfigService';
|
|
9
|
+
import type { FeatureFlagService } from 'src/services/featureFlagService';
|
|
10
|
+
|
|
11
|
+
const logger = new Logger('urlResolverWarmupService');
|
|
12
|
+
|
|
13
|
+
export interface WarmUrlResolverParams {
|
|
14
|
+
baseUrl: string;
|
|
15
|
+
apiKey: string;
|
|
16
|
+
enviveServiceConfig: EnviveServiceConfig;
|
|
17
|
+
show?: boolean;
|
|
18
|
+
enviveOn?: boolean;
|
|
19
|
+
env?: string;
|
|
20
|
+
contextSource?: string;
|
|
21
|
+
featureFlagService?: FeatureFlagService;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Fires the /v1/url_resolving request as soon as the org config is available,
|
|
26
|
+
* instead of waiting for the React tree to mount and PageProvider's effect to
|
|
27
|
+
* run — that removes a full network round trip from the first-widget-paint
|
|
28
|
+
* critical path.
|
|
29
|
+
*
|
|
30
|
+
* Seeds the same atoms EnviveProvider/EnviveConfigProvider populate later
|
|
31
|
+
* (with identical values) so the request context matches the one
|
|
32
|
+
* PageProvider would send, then stores the response in the
|
|
33
|
+
* sessionStorage-backed urlResolverAtom that PageProvider consults before
|
|
34
|
+
* fetching. Concurrent calls for the same URL are deduped inside
|
|
35
|
+
* CommerceApiClient.resolveUrl.
|
|
36
|
+
*/
|
|
37
|
+
export const warmUrlResolver = async ({
|
|
38
|
+
baseUrl,
|
|
39
|
+
apiKey,
|
|
40
|
+
enviveServiceConfig,
|
|
41
|
+
show,
|
|
42
|
+
enviveOn,
|
|
43
|
+
env,
|
|
44
|
+
contextSource,
|
|
45
|
+
featureFlagService,
|
|
46
|
+
}: WarmUrlResolverParams): Promise<void> => {
|
|
47
|
+
try {
|
|
48
|
+
const store = getAtomStore();
|
|
49
|
+
// Must match PageProvider's URL cleansing so the cache key lines up.
|
|
50
|
+
const cleansedUrl = window.location.href.toLowerCase().trim();
|
|
51
|
+
if (store.get(urlResolverAtom)[cleansedUrl]) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// The CommerceApiClient singleton captures baseUrl/apiKey at first
|
|
56
|
+
// instantiation, so these atoms must be seeded before any API call.
|
|
57
|
+
// Only seed when unset — providers own this atom once React mounts.
|
|
58
|
+
if (store.get(enviveConfigAtom) === undefined) {
|
|
59
|
+
store.set(enviveConfigAtom, {
|
|
60
|
+
baseUrl,
|
|
61
|
+
orgLevelApiKey: apiKey,
|
|
62
|
+
orgShortName: enviveServiceConfig.org.org.short_name,
|
|
63
|
+
orgId: enviveServiceConfig.org.org.id,
|
|
64
|
+
show,
|
|
65
|
+
enviveOn,
|
|
66
|
+
env,
|
|
67
|
+
contextSource,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
// resolveUrl reads feature gates through featureFlagServiceAtom; without
|
|
71
|
+
// this seed the warm-up request is computed with feature_gates={} and
|
|
72
|
+
// that response is what gets cached for PageProvider.
|
|
73
|
+
if (featureFlagService && store.get(featureFlagServiceAtom) === undefined) {
|
|
74
|
+
store.set(featureFlagServiceAtom, { featureFlagService });
|
|
75
|
+
}
|
|
76
|
+
// Mirrors EnviveProvider: "deployed" is the no-pin sentinel.
|
|
77
|
+
const { baseVersion, baseVersionSource } = enviveServiceConfig.orgConfig?.resolution ?? {};
|
|
78
|
+
if (baseVersion && baseVersionSource && baseVersionSource !== 'deployed') {
|
|
79
|
+
store.set(resolvedBaseConfigVersionAtom, baseVersion);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const response = (await CommerceApiClient.resolveUrl(cleansedUrl)) as UrlResolverResponse;
|
|
83
|
+
store.set(urlResolverAtom, { url: cleansedUrl, response });
|
|
84
|
+
} catch (err) {
|
|
85
|
+
// Fail open: PageProvider issues its own resolveUrl call when the cache
|
|
86
|
+
// is empty, so a warm-up failure never blocks rendering.
|
|
87
|
+
logger.logDebug('URL resolver warm-up failed', err);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
@@ -2,8 +2,11 @@ import { FrontendConfigOverrides, VariantInfo } from 'src/application/models';
|
|
|
2
2
|
import type { HardcopyResponse } from 'src/contexts/hardcopyContext/hardcopyContext';
|
|
3
3
|
import type { WidgetTypeV3 } from 'src/contexts/typesV3';
|
|
4
4
|
|
|
5
|
+
export type AddToCartCallback = (params: { product_id: string }) => void | Promise<void>;
|
|
6
|
+
|
|
5
7
|
export interface EnviveConfig {
|
|
6
8
|
publicKey?: string;
|
|
9
|
+
addToCart?: AddToCartCallback;
|
|
7
10
|
userID?: string;
|
|
8
11
|
env?: 'prod' | 'dev';
|
|
9
12
|
clientID?: string;
|