@envive-ai/react-hooks 0.3.38 → 0.3.40
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 +3 -2
- package/dist/application/commerce-api.js +3 -2
- package/dist/application/models/api/orgConfigResults.d.cts +13 -2
- package/dist/application/models/api/orgConfigResults.d.ts +13 -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/getWidgetConfigQuery.cjs +23 -0
- package/dist/application/models/graphql/queries/getWidgetConfigQuery.js +22 -0
- package/dist/application/models/index.d.cts +2 -2
- package/dist/application/models/index.d.ts +2 -2
- package/dist/application/utils/index.cjs +0 -2
- package/dist/application/utils/index.d.cts +1 -2
- package/dist/application/utils/index.d.ts +1 -2
- package/dist/application/utils/index.js +1 -2
- package/dist/atoms/app/index.d.cts +7 -7
- package/dist/atoms/app/index.d.ts +6 -6
- package/dist/atoms/chat/chatState.d.cts +18 -18
- 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.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 +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 +2 -2
- 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.d.ts +13 -13
- package/dist/atoms/envive/resolvedBaseConfigVersion.cjs +9 -0
- package/dist/atoms/envive/resolvedBaseConfigVersion.js +8 -0
- package/dist/atoms/globalSearch/globalSearch.d.cts +5 -5
- package/dist/atoms/globalSearch/globalSearch.d.ts +5 -5
- package/dist/atoms/org/customerService.d.cts +6 -6
- package/dist/atoms/org/customerService.d.ts +6 -6
- package/dist/atoms/org/graphqlConfig.d.cts +4 -4
- package/dist/atoms/org/graphqlConfig.d.ts +4 -4
- package/dist/atoms/org/newOrgConfigAtom.d.cts +2 -2
- package/dist/atoms/org/newOrgConfigAtom.d.ts +2 -2
- package/dist/atoms/org/orgAnalyticsConfig.d.cts +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/search/utils.d.ts +1 -1
- package/dist/atoms/widget/chatPreviewLoading.d.cts +2 -2
- package/dist/atoms/widget/chatPreviewLoading.d.ts +2 -2
- package/dist/contexts/enviveContext/enviveContext.cjs +11 -3
- package/dist/contexts/enviveContext/enviveContext.js +11 -3
- package/dist/contexts/graphqlContext/graphqlContext.cjs +1 -1
- package/dist/contexts/graphqlContext/graphqlContext.d.cts +3 -1
- package/dist/contexts/graphqlContext/graphqlContext.d.ts +3 -1
- package/dist/contexts/graphqlContext/graphqlContext.js +1 -1
- package/dist/contexts/salesAgentContext/salesAgentService.cjs +9 -3
- package/dist/contexts/salesAgentContext/salesAgentService.js +9 -3
- package/dist/contexts/systemSettingsContext/systemSettingsContext.d.cts +2 -2
- package/dist/contexts/types.d.cts +1 -1
- package/dist/contexts/types.d.ts +1 -1
- package/dist/contexts/typesV3.d.cts +1 -1
- package/dist/contexts/typesV3.d.ts +1 -1
- package/dist/hooks/SystemSettingsContext/useSystemSettingsContext.d.cts +2 -2
- package/dist/hooks/utils.d.cts +1 -1
- package/dist/hooks/utils.d.ts +1 -1
- package/dist/services/amplitudeService/amplitudeService.cjs +17 -2
- package/dist/services/amplitudeService/amplitudeService.d.cts +3 -1
- package/dist/services/amplitudeService/amplitudeService.d.ts +3 -1
- package/dist/services/amplitudeService/amplitudeService.js +17 -2
- package/dist/services/enviveConfigService/enviveConfigService.cjs +9 -3
- package/dist/services/enviveConfigService/enviveConfigService.d.cts +3 -2
- package/dist/services/enviveConfigService/enviveConfigService.d.ts +3 -2
- package/dist/services/enviveConfigService/enviveConfigService.js +10 -4
- package/dist/services/enviveConfigService/fetchGraphQLConfig.cjs +78 -39
- package/dist/services/enviveConfigService/fetchGraphQLConfig.js +78 -40
- package/dist/services/ga4ProjectionService/ga4ProjectionService.cjs +3 -3
- package/dist/services/ga4ProjectionService/ga4ProjectionService.js +3 -3
- package/dist/services/hardcopyService/hardcopyService.cjs +4 -2
- package/dist/services/hardcopyService/hardcopyService.js +4 -2
- package/dist/types/config-versions.cjs +3 -1
- package/dist/types/config-versions.js +3 -2
- package/dist/types/customerService.cjs +2 -1
- package/dist/types/customerService.d.cts +2 -1
- package/dist/types/customerService.d.ts +2 -1
- package/dist/types/customerService.js +2 -1
- package/package.json +1 -1
- package/src/application/commerce-api.ts +5 -1
- package/src/application/models/api/orgConfigResults.ts +27 -0
- package/src/application/models/featureGates.ts +5 -0
- package/src/application/models/graphql/queries/getWidgetConfigQuery.ts +34 -0
- package/src/application/utils/__tests__/elementObserver.test.ts +200 -0
- package/src/application/utils/index.ts +0 -1
- package/src/atoms/envive/resolvedBaseConfigVersion.ts +11 -0
- package/src/contexts/enviveContext/enviveContext.tsx +20 -0
- package/src/contexts/graphqlContext/graphqlContext.tsx +5 -0
- package/src/contexts/salesAgentContext/salesAgentService.ts +4 -1
- package/src/services/amplitudeService/__tests__/amplitudeService.test.ts +95 -0
- package/src/services/amplitudeService/amplitudeService.ts +31 -1
- package/src/services/enviveConfigService/__tests__/fetchGraphQLConfig.test.ts +107 -1
- package/src/services/enviveConfigService/enviveConfigService.ts +35 -7
- package/src/services/enviveConfigService/fetchGraphQLConfig.ts +119 -57
- package/src/services/ga4ProjectionService/ga4ProjectionService.ts +2 -2
- package/src/services/hardcopyService/__tests__/hardcopyService.test.ts +35 -1
- package/src/services/hardcopyService/hardcopyService.ts +6 -1
- package/src/types/config-versions.ts +8 -0
- package/src/types/customerService.ts +1 -0
- package/dist/application/utils/merchantUtils.cjs +0 -18
- package/dist/application/utils/merchantUtils.d.cts +0 -5
- package/dist/application/utils/merchantUtils.d.ts +0 -5
- package/dist/application/utils/merchantUtils.js +0 -17
- package/src/application/utils/merchantUtils.ts +0 -16
|
@@ -6,7 +6,6 @@ export * from './divideArray';
|
|
|
6
6
|
export * from './domObserver';
|
|
7
7
|
export * from './elementObserver';
|
|
8
8
|
export * from './imageFilter';
|
|
9
|
-
export * from './merchantUtils';
|
|
10
9
|
export * from './messageFromFormSubmittedEvent';
|
|
11
10
|
export * from './messageFromQueryEvent';
|
|
12
11
|
export * from './messageFromResponse';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { atom } from 'jotai';
|
|
2
|
+
|
|
3
|
+
// The `base_version` pymono resolved for the current user on the unified
|
|
4
|
+
// path (sourced from getWidgetConfig.resolution, not REST). Read by
|
|
5
|
+
// commerce-api, salesAgentService, and hardcopyService as a fallback after
|
|
6
|
+
// the `spiffy_config_version` / `envive_config_version` URL params, so
|
|
7
|
+
// inference requests are scoped to the same version that produced the
|
|
8
|
+
// widgets the user is rendering. Undefined until the unified-path fetch
|
|
9
|
+
// completes, on the legacy path, and when the resolution sentinel
|
|
10
|
+
// `base_version_source === "deployed"` indicates no real pin to forward.
|
|
11
|
+
export const resolvedBaseConfigVersionAtom = atom<string | undefined>(undefined);
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { useSetAtom } from 'jotai';
|
|
2
3
|
|
|
3
4
|
import { AmplitudeService } from 'src/services/amplitudeService/amplitudeService';
|
|
4
5
|
import { EnviveServiceConfig, IEnviveConfigService } from 'src/services/enviveConfigService';
|
|
6
|
+
import { resolvedBaseConfigVersionAtom } from 'src/atoms/envive/resolvedBaseConfigVersion';
|
|
5
7
|
import { IHardcopyService } from 'src/services/hardcopyService';
|
|
6
8
|
import { IUserIdentityService } from 'src/services/userIdentityService';
|
|
7
9
|
import Logger from 'src/application/logging/logger';
|
|
@@ -102,6 +104,7 @@ export const EnviveProvider: React.FC<EnviveProviderProps> = ({
|
|
|
102
104
|
}) => {
|
|
103
105
|
const { extensionService } = services;
|
|
104
106
|
const [enviveServiceConfig, setEnviveServiceConfig] = useState<EnviveServiceConfig | null>(null);
|
|
107
|
+
const setResolvedBaseConfigVersion = useSetAtom(resolvedBaseConfigVersionAtom);
|
|
105
108
|
const didReportSuccessRef = useRef(false);
|
|
106
109
|
|
|
107
110
|
const invokeExtensionCallback = useCallback((callback?: () => Promise<unknown> | void) => {
|
|
@@ -128,10 +131,26 @@ export const EnviveProvider: React.FC<EnviveProviderProps> = ({
|
|
|
128
131
|
useEffect(() => {
|
|
129
132
|
invokeExtensionCallback(extensionService?.onInjectionLoading);
|
|
130
133
|
|
|
134
|
+
// Clear any prior merchant's resolved version before the new fetch — the
|
|
135
|
+
// atom is module-level and survives EnviveProvider remounts (e.g. when
|
|
136
|
+
// Envive Hub switches merchants). Without the reset, the previous
|
|
137
|
+
// merchant's `baseVersion` would leak into the new merchant's inference
|
|
138
|
+
// requests when the new resolution falls back to the "deployed" sentinel
|
|
139
|
+
// and the conditional write below is skipped.
|
|
140
|
+
setResolvedBaseConfigVersion(undefined);
|
|
141
|
+
|
|
131
142
|
const fetchEnviveConfig = async () => {
|
|
132
143
|
try {
|
|
133
144
|
const config = await services.enviveConfigService.getEnviveConfig();
|
|
134
145
|
setEnviveServiceConfig(config);
|
|
146
|
+
// Forward only real pins to inference. baseVersionSource === "deployed"
|
|
147
|
+
// is the sentinel for "no merchant_layer, no override" — leaving the
|
|
148
|
+
// atom undefined lets inference fall through to backend's default
|
|
149
|
+
// version selection instead of pinning to the literal string "deployed".
|
|
150
|
+
const { baseVersion, baseVersionSource } = config.orgConfig?.resolution ?? {};
|
|
151
|
+
if (baseVersion && baseVersionSource && baseVersionSource !== 'deployed') {
|
|
152
|
+
setResolvedBaseConfigVersion(baseVersion);
|
|
153
|
+
}
|
|
135
154
|
} catch (error) {
|
|
136
155
|
const resolvedError =
|
|
137
156
|
error instanceof Error ? error : new Error('Failed to load envive config');
|
|
@@ -146,6 +165,7 @@ export const EnviveProvider: React.FC<EnviveProviderProps> = ({
|
|
|
146
165
|
extensionService,
|
|
147
166
|
invokeExtensionCallback,
|
|
148
167
|
invokeExtensionErrorCallback,
|
|
168
|
+
setResolvedBaseConfigVersion,
|
|
149
169
|
]);
|
|
150
170
|
|
|
151
171
|
useEffect(() => {
|
|
@@ -4,6 +4,7 @@ import { baseUrlAtom, orgLevelApiKeyAtom } from 'src/atoms/envive/enviveConfig';
|
|
|
4
4
|
import { ColorMapping } from 'src/application/models/colorsConfig';
|
|
5
5
|
import { FrontendConfig } from 'src/application/models/frontendConfig';
|
|
6
6
|
import { CamelCasedPropertiesDeep } from 'src/application/models';
|
|
7
|
+
import { ExperimentConfigResolutionMetadata } from 'src/application/models/api/orgConfigResults';
|
|
7
8
|
import { FrontendConfigV3 } from 'src/application/models/frontendConfigV3';
|
|
8
9
|
import { ColorMappingV3 } from 'src/application/models/colorsConfigV3';
|
|
9
10
|
import { OrgPageConfig } from '../types';
|
|
@@ -17,6 +18,10 @@ export type GraphQlConfigValues = {
|
|
|
17
18
|
colorsConfig?: ColorsConfigResponse | ColorsConfigV3Response;
|
|
18
19
|
frontendConfig?: CamelCasedPropertiesDeep<FrontendConfig | FrontendConfigV3>;
|
|
19
20
|
orgPageConfig?: OrgPageConfig;
|
|
21
|
+
// Populated only by the unified-path fetcher (getWidgetConfig.resolution).
|
|
22
|
+
// Legacy getColorsAndFrontendQuery does not return it. Read by EnviveProvider
|
|
23
|
+
// to thread baseVersion into inference requests as override_config_version.
|
|
24
|
+
resolution?: ExperimentConfigResolutionMetadata;
|
|
20
25
|
};
|
|
21
26
|
|
|
22
27
|
interface GraphQLContextValue {
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import { messageFromResponse } from 'src/application/utils';
|
|
16
16
|
import { appDetailsAtom } from 'src/atoms/app';
|
|
17
17
|
import { analyticsContextAtom } from 'src/atoms/app/variant';
|
|
18
|
+
import { resolvedBaseConfigVersionAtom } from 'src/atoms/envive/resolvedBaseConfigVersion';
|
|
18
19
|
import {
|
|
19
20
|
messagesAtom,
|
|
20
21
|
pendingResponseAtom,
|
|
@@ -247,6 +248,7 @@ export const useSalesAgentService: () => SalesAgentService = () => {
|
|
|
247
248
|
|
|
248
249
|
const featureFlagService = useFeatureFlagService();
|
|
249
250
|
const context = useAtomValue(appDetailsAtom);
|
|
251
|
+
const resolvedBaseConfigVersion = useAtomValue(resolvedBaseConfigVersionAtom);
|
|
250
252
|
|
|
251
253
|
const createResponsePayload = useCallback(
|
|
252
254
|
({
|
|
@@ -261,6 +263,7 @@ export const useSalesAgentService: () => SalesAgentService = () => {
|
|
|
261
263
|
const overrideConfigVersion =
|
|
262
264
|
getQueryParam('spiffy_config_version') ||
|
|
263
265
|
getQueryParam('envive_config_version') ||
|
|
266
|
+
resolvedBaseConfigVersion ||
|
|
264
267
|
undefined;
|
|
265
268
|
const overrideModelDatetime = getQueryParam('override_model_datetime') || undefined;
|
|
266
269
|
return {
|
|
@@ -273,7 +276,7 @@ export const useSalesAgentService: () => SalesAgentService = () => {
|
|
|
273
276
|
overrideModelDatetime,
|
|
274
277
|
};
|
|
275
278
|
},
|
|
276
|
-
[context, featureFlagService?.featureFlagService],
|
|
279
|
+
[context, featureFlagService?.featureFlagService, resolvedBaseConfigVersion],
|
|
277
280
|
);
|
|
278
281
|
|
|
279
282
|
const getStreamingResponses = useCallback(
|
|
@@ -698,4 +698,99 @@ describe('AmplitudeService', () => {
|
|
|
698
698
|
});
|
|
699
699
|
});
|
|
700
700
|
});
|
|
701
|
+
|
|
702
|
+
describe('Experiment-assignment props', () => {
|
|
703
|
+
it('emits experiment.{ns}.* keys as null when no assignments are configured', async () => {
|
|
704
|
+
const service = createService();
|
|
705
|
+
await service.trackEvent({
|
|
706
|
+
eventName: SpiffyMetricsEventName.ChatUserMessageInput,
|
|
707
|
+
eventProps: {},
|
|
708
|
+
});
|
|
709
|
+
const props = mockTrack.mock.calls[0]?.[1] ?? {};
|
|
710
|
+
expect(props['experiment.ml.layer_name']).toBeNull();
|
|
711
|
+
expect(props['experiment.ml.experiment_name']).toBeNull();
|
|
712
|
+
expect(props['experiment.ml.group_name']).toBeNull();
|
|
713
|
+
expect(props['experiment.ml.rule_id']).toBeNull();
|
|
714
|
+
expect(props['experiment.merchant.layer_name']).toBeNull();
|
|
715
|
+
expect(props['experiment.merchant.experiment_name']).toBeNull();
|
|
716
|
+
expect(props['experiment.merchant.group_name']).toBeNull();
|
|
717
|
+
expect(props['experiment.merchant.rule_id']).toBeNull();
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
it('populates experiment.{ns}.* keys when assignments are passed at construction', async () => {
|
|
721
|
+
const service = createService({
|
|
722
|
+
experimentAssignments: [
|
|
723
|
+
{
|
|
724
|
+
layer_name: 'ml_test_store',
|
|
725
|
+
namespace: 'ml',
|
|
726
|
+
allocated_experiment_name: 'automated_testing_ml_experiment',
|
|
727
|
+
group_name: 'ML Config Variation',
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
layer_name: 'merchant_test_store',
|
|
731
|
+
namespace: 'merchant',
|
|
732
|
+
allocated_experiment_name: 'automated_testing_merchant_experiment',
|
|
733
|
+
group_name: 'Merchant Config Variation',
|
|
734
|
+
},
|
|
735
|
+
],
|
|
736
|
+
});
|
|
737
|
+
await service.trackEvent({
|
|
738
|
+
eventName: SpiffyMetricsEventName.ChatUserMessageInput,
|
|
739
|
+
eventProps: {},
|
|
740
|
+
});
|
|
741
|
+
const props = mockTrack.mock.calls[0]?.[1] ?? {};
|
|
742
|
+
expect(props['experiment.ml.layer_name']).toBe('ml_test_store');
|
|
743
|
+
expect(props['experiment.ml.experiment_name']).toBe('automated_testing_ml_experiment');
|
|
744
|
+
expect(props['experiment.ml.group_name']).toBe('ML Config Variation');
|
|
745
|
+
expect(props['experiment.merchant.layer_name']).toBe('merchant_test_store');
|
|
746
|
+
expect(props['experiment.merchant.experiment_name']).toBe(
|
|
747
|
+
'automated_testing_merchant_experiment',
|
|
748
|
+
);
|
|
749
|
+
expect(props['experiment.merchant.group_name']).toBe('Merchant Config Variation');
|
|
750
|
+
// rule_id reserved — backend doesn't surface it.
|
|
751
|
+
expect(props['experiment.ml.rule_id']).toBeNull();
|
|
752
|
+
expect(props['experiment.merchant.rule_id']).toBeNull();
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
it('emits the unassigned namespace as null when only one is assigned', async () => {
|
|
756
|
+
const service = createService({
|
|
757
|
+
experimentAssignments: [
|
|
758
|
+
{
|
|
759
|
+
layer_name: 'ml_test_store',
|
|
760
|
+
namespace: 'ml',
|
|
761
|
+
allocated_experiment_name: 'ml_only',
|
|
762
|
+
group_name: 'ML Only',
|
|
763
|
+
},
|
|
764
|
+
],
|
|
765
|
+
});
|
|
766
|
+
await service.trackEvent({
|
|
767
|
+
eventName: SpiffyMetricsEventName.ChatUserMessageInput,
|
|
768
|
+
eventProps: {},
|
|
769
|
+
});
|
|
770
|
+
const props = mockTrack.mock.calls[0]?.[1] ?? {};
|
|
771
|
+
expect(props['experiment.ml.layer_name']).toBe('ml_test_store');
|
|
772
|
+
expect(props['experiment.merchant.layer_name']).toBeNull();
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
it('drops malformed assignments missing layer_name or namespace', async () => {
|
|
776
|
+
const service = createService({
|
|
777
|
+
experimentAssignments: [
|
|
778
|
+
// Missing layer_name → dropped silently. (Belt-and-braces: pymono's
|
|
779
|
+
// tests enforce the wire shape, but if a field ever goes null we'd
|
|
780
|
+
// rather emit `null` props than crash.)
|
|
781
|
+
{
|
|
782
|
+
namespace: 'ml',
|
|
783
|
+
allocated_experiment_name: 'orphan',
|
|
784
|
+
},
|
|
785
|
+
],
|
|
786
|
+
});
|
|
787
|
+
await service.trackEvent({
|
|
788
|
+
eventName: SpiffyMetricsEventName.ChatUserMessageInput,
|
|
789
|
+
eventProps: {},
|
|
790
|
+
});
|
|
791
|
+
const props = mockTrack.mock.calls[0]?.[1] ?? {};
|
|
792
|
+
expect(props['experiment.ml.layer_name']).toBeNull();
|
|
793
|
+
expect(props['experiment.ml.experiment_name']).toBeNull();
|
|
794
|
+
});
|
|
795
|
+
});
|
|
701
796
|
});
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
import { FeatureFlagService } from 'src/contexts/featureFlagServiceContext/featureFlagServiceContext';
|
|
12
12
|
import { WidgetTypeV3 } from 'src/contexts/typesV3';
|
|
13
13
|
import { ChatElementDisplayLocationV3 } from 'src/application/models/chatElementDisplayLocationV3';
|
|
14
|
+
import { ExperimentNamespace, OrgConfigExperimentAssignment } from 'src/application/models';
|
|
14
15
|
import { projectToGA4 } from '../ga4ProjectionService/ga4ProjectionService';
|
|
15
16
|
import { EnviveMetricsEventName, SpiffyMetricsEventName } from './eventNames';
|
|
16
17
|
|
|
@@ -39,8 +40,37 @@ export interface AmplitudeServiceConfig {
|
|
|
39
40
|
enviveOn: boolean;
|
|
40
41
|
enabledFeatures?: Record<string, boolean>;
|
|
41
42
|
getLocalStorageItem: null | ((key: string) => string | null);
|
|
43
|
+
// Captured at construction. Drives `experiment.{ns}.*` event-property
|
|
44
|
+
// enrichment. Sourced from /v1/org/config when the `use_unified_config`
|
|
45
|
+
// gate is on; otherwise omitted (props emit as null).
|
|
46
|
+
experimentAssignments?: OrgConfigExperimentAssignment[];
|
|
42
47
|
}
|
|
43
48
|
|
|
49
|
+
// Namespaces emitted on every event with constant keys, so the BigQuery
|
|
50
|
+
// schema is stable regardless of which (if any) experiment is running.
|
|
51
|
+
// Currently `ml` + `merchant`; matches `ExperimentNamespace`.
|
|
52
|
+
const EXPERIMENT_NAMESPACES: ExperimentNamespace[] = ['ml', 'merchant'];
|
|
53
|
+
|
|
54
|
+
const buildExperimentProps = (
|
|
55
|
+
assignments: OrgConfigExperimentAssignment[] | undefined,
|
|
56
|
+
): Record<string, unknown> => {
|
|
57
|
+
const byNamespace = new Map<ExperimentNamespace, OrgConfigExperimentAssignment>();
|
|
58
|
+
(assignments ?? []).forEach(a => {
|
|
59
|
+
if (a.layer_name != null && a.namespace != null) {
|
|
60
|
+
byNamespace.set(a.namespace, a);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
return EXPERIMENT_NAMESPACES.reduce<Record<string, unknown>>((acc, ns) => {
|
|
64
|
+
const a = byNamespace.get(ns);
|
|
65
|
+
acc[`experiment.${ns}.layer_name`] = a?.layer_name ?? null;
|
|
66
|
+
acc[`experiment.${ns}.experiment_name`] = a?.allocated_experiment_name ?? null;
|
|
67
|
+
acc[`experiment.${ns}.group_name`] = a?.group_name ?? null;
|
|
68
|
+
// `rule_id` reserved — backend doesn't currently surface it.
|
|
69
|
+
acc[`experiment.${ns}.rule_id`] = null;
|
|
70
|
+
return acc;
|
|
71
|
+
}, {});
|
|
72
|
+
};
|
|
73
|
+
|
|
44
74
|
export class AmplitudeService {
|
|
45
75
|
private amplitudeClient: BrowserClient | undefined;
|
|
46
76
|
|
|
@@ -91,7 +121,7 @@ export class AmplitudeService {
|
|
|
91
121
|
return acc;
|
|
92
122
|
}, {})
|
|
93
123
|
: {};
|
|
94
|
-
const experimentProps =
|
|
124
|
+
const experimentProps = buildExperimentProps(this.config.experimentAssignments);
|
|
95
125
|
|
|
96
126
|
const environmentProps = {
|
|
97
127
|
'environment.execution_context': 'bundle',
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import { fetchGraphQLConfig, DEFAULT_PAGE_VARIANTS } from '../fetchGraphQLConfig';
|
|
2
1
|
import { mockV3ColorsConfig, mockV3FrontendConfig } from 'src/contexts/graphqlContext/mockV3Config';
|
|
3
2
|
import { FloatingButtonLocation } from '@envive-ai/react-toolkit-v3/FloatingButton';
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_PAGE_VARIANTS,
|
|
5
|
+
fetchGraphQLConfig,
|
|
6
|
+
fetchUnifiedGraphQLConfig,
|
|
7
|
+
} from '../fetchGraphQLConfig';
|
|
4
8
|
|
|
5
9
|
const { MockLogger } = vi.hoisted(() => {
|
|
6
10
|
const MockLogger = vi.fn(function () {
|
|
@@ -423,3 +427,105 @@ describe('fetchGraphQLConfig', () => {
|
|
|
423
427
|
});
|
|
424
428
|
});
|
|
425
429
|
});
|
|
430
|
+
|
|
431
|
+
describe('fetchUnifiedGraphQLConfig', () => {
|
|
432
|
+
function makeUnifiedOkFetch(v3RootConfigValues: unknown, resolution?: unknown) {
|
|
433
|
+
mockFetch.mockResolvedValueOnce({
|
|
434
|
+
ok: true,
|
|
435
|
+
json: () =>
|
|
436
|
+
Promise.resolve({
|
|
437
|
+
data: {
|
|
438
|
+
me: {
|
|
439
|
+
getWidgetConfig: {
|
|
440
|
+
productsConfig: {
|
|
441
|
+
v_three_config: { values: v3RootConfigValues },
|
|
442
|
+
},
|
|
443
|
+
resolution,
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
}),
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
it('issues a getWidgetConfig query (not getProductsConfigByVersion) and forwards userId', async () => {
|
|
452
|
+
makeUnifiedOkFetch(null);
|
|
453
|
+
|
|
454
|
+
await fetchUnifiedGraphQLConfig('https://api.example.com', 'key', 'user_1');
|
|
455
|
+
|
|
456
|
+
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
457
|
+
expect(body.query).toContain('getWidgetConfig');
|
|
458
|
+
expect(body.query).not.toContain('getProductsConfigByVersion');
|
|
459
|
+
// userId is required by the schema; omitting it 400s. Regression guard.
|
|
460
|
+
// version is absent without a URL-param override so the resolver uses
|
|
461
|
+
// Statsig bucketing (passing 'deployed' here would bypass Statsig).
|
|
462
|
+
expect(body.variables).toEqual({ userId: 'user_1' });
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it('threads a spiffy_config_version URL param through as the version variable', async () => {
|
|
466
|
+
window.history.replaceState({}, '', '/?spiffy_config_version=1.2.3');
|
|
467
|
+
try {
|
|
468
|
+
makeUnifiedOkFetch(null);
|
|
469
|
+
|
|
470
|
+
await fetchUnifiedGraphQLConfig('https://api.example.com', 'key', 'user_1');
|
|
471
|
+
|
|
472
|
+
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
473
|
+
expect(body.variables).toEqual({ userId: 'user_1', version: '1.2.3' });
|
|
474
|
+
} finally {
|
|
475
|
+
window.history.replaceState({}, '', '/');
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it('parses productsConfig into the same shape as fetchGraphQLConfig', async () => {
|
|
480
|
+
makeUnifiedOkFetch(baseV3Config);
|
|
481
|
+
|
|
482
|
+
const result = await fetchUnifiedGraphQLConfig('https://api.example.com', 'key', 'user_1');
|
|
483
|
+
|
|
484
|
+
expect(result.colorsConfig).toBeDefined();
|
|
485
|
+
expect(result.frontendConfig).toBeDefined();
|
|
486
|
+
expect(result.orgPageConfig).toBeDefined();
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it('returns resolution metadata alongside the config so callers can thread baseVersion into inference', async () => {
|
|
490
|
+
makeUnifiedOkFetch(baseV3Config, {
|
|
491
|
+
baseVersion: '1.0.670',
|
|
492
|
+
baseVersionSource: 'merchant_layer',
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
const result = await fetchUnifiedGraphQLConfig('https://api.example.com', 'key', 'user_1');
|
|
496
|
+
|
|
497
|
+
expect(result.resolution).toEqual({
|
|
498
|
+
baseVersion: '1.0.670',
|
|
499
|
+
baseVersionSource: 'merchant_layer',
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
it('selects resolution fields in the query so the server can fill them', async () => {
|
|
504
|
+
makeUnifiedOkFetch(null);
|
|
505
|
+
|
|
506
|
+
await fetchUnifiedGraphQLConfig('https://api.example.com', 'key', 'user_1');
|
|
507
|
+
|
|
508
|
+
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
509
|
+
expect(body.query).toContain('resolution');
|
|
510
|
+
expect(body.query).toContain('baseVersion');
|
|
511
|
+
expect(body.query).toContain('baseVersionSource');
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it('falls back to DEFAULT_PAGE_VARIANTS when productsConfig has no page_variants array', async () => {
|
|
515
|
+
// Reuses the no-colors fallback path: when productsConfig is missing
|
|
516
|
+
// colors, DEFAULT_PAGE_VARIANTS is returned as the orgPageConfig.
|
|
517
|
+
makeUnifiedOkFetch(null);
|
|
518
|
+
|
|
519
|
+
const result = await fetchUnifiedGraphQLConfig('https://api.example.com', 'key', 'user_1');
|
|
520
|
+
|
|
521
|
+
expect(result.orgPageConfig?.pageVariants).toEqual(DEFAULT_PAGE_VARIANTS);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it('returns empty config on network failure', async () => {
|
|
525
|
+
mockFetch.mockRejectedValueOnce(new Error('boom'));
|
|
526
|
+
|
|
527
|
+
const result = await fetchUnifiedGraphQLConfig('https://api.example.com', 'key', 'user_1');
|
|
528
|
+
|
|
529
|
+
expect(result).toEqual({ colorsConfig: undefined, frontendConfig: undefined });
|
|
530
|
+
});
|
|
531
|
+
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { OrgConfigFeatureGate } from 'src/application/models';
|
|
1
|
+
import { OrgConfigExperimentAssignment, OrgConfigFeatureGate } from 'src/application/models';
|
|
2
2
|
import { FeatureGates } from 'src/application/models/featureGates';
|
|
3
|
+
import { FeatureFlagService } from 'src/contexts/featureFlagServiceContext';
|
|
3
4
|
import { GraphQlConfigValues } from 'src/contexts/graphqlContext';
|
|
4
|
-
import { fetchGraphQLConfig } from './fetchGraphQLConfig';
|
|
5
|
+
import { fetchGraphQLConfig, fetchUnifiedGraphQLConfig } from './fetchGraphQLConfig';
|
|
5
6
|
|
|
6
7
|
type EnviveConfigServiceProps = {
|
|
7
8
|
baseUrl: string;
|
|
@@ -24,6 +25,14 @@ export type EnviveServiceConfig = {
|
|
|
24
25
|
};
|
|
25
26
|
};
|
|
26
27
|
gates: OrgConfigFeatureGate[];
|
|
28
|
+
// Populated by the backend when the `use_unified_config` Statsig gate
|
|
29
|
+
// evaluates true. amplitudeService reads these to enrich events with
|
|
30
|
+
// `experiment.{ns}.*` properties.
|
|
31
|
+
experiment_assignments?: OrgConfigExperimentAssignment[];
|
|
32
|
+
// orgConfig.resolution is populated only on the unified path (sourced from
|
|
33
|
+
// getWidgetConfig.resolution, not /v1/org/config.resolution) so the version
|
|
34
|
+
// threaded into inference requests matches the version that produced the
|
|
35
|
+
// rendered widgets.
|
|
27
36
|
orgConfig?: GraphQlConfigValues;
|
|
28
37
|
};
|
|
29
38
|
|
|
@@ -67,16 +76,35 @@ export class EnviveConfigService implements IEnviveConfigService {
|
|
|
67
76
|
.map(featureGate => featureGate.toString())
|
|
68
77
|
.join(','),
|
|
69
78
|
});
|
|
70
|
-
|
|
71
|
-
|
|
79
|
+
// Serialized so we can branch the GraphQL call on the `use_unified_config`
|
|
80
|
+
// gate returned by /v1/org/config. Once the migration to getWidgetConfig
|
|
81
|
+
// completes (one endpoint returning gates + experiments + productsConfig),
|
|
82
|
+
// this collapses back to a single call.
|
|
83
|
+
const orgConfigResponse = await fetch(
|
|
84
|
+
`${this.baseUrl}/v1/org/config?${queryParams.toString()}`,
|
|
85
|
+
{
|
|
72
86
|
method: 'GET',
|
|
73
87
|
headers: {
|
|
74
88
|
'Content-Type': 'application/json',
|
|
75
89
|
Authorization: `Bearer ${this.apiKey}`,
|
|
76
90
|
},
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
91
|
+
},
|
|
92
|
+
).then(async r => {
|
|
93
|
+
if (!r.ok) {
|
|
94
|
+
throw new Error(`Org config request failed with status ${r.status}`);
|
|
95
|
+
}
|
|
96
|
+
return r.json() as Promise<EnviveServiceConfig>;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Honor URL / `window._spiffy` / localStorage overrides for
|
|
100
|
+
// `use_unified_config` the same way every other feature gate is read.
|
|
101
|
+
const useUnifiedConfig = new FeatureFlagService(
|
|
102
|
+
orgConfigResponse.gates ?? [],
|
|
103
|
+
).isFeatureGateEnabled(FeatureGates.UseUnifiedConfig);
|
|
104
|
+
const orgConfig = useUnifiedConfig
|
|
105
|
+
? await fetchUnifiedGraphQLConfig(this.baseUrl, this.apiKey, this.userId)
|
|
106
|
+
: await fetchGraphQLConfig(this.baseUrl, this.apiKey);
|
|
107
|
+
|
|
80
108
|
this.response = { ...orgConfigResponse, orgConfig };
|
|
81
109
|
return this.response;
|
|
82
110
|
}
|