@envive-ai/react-hooks 0.3.63 → 0.3.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/application/models/featureGates.cjs +2 -2
- package/dist/application/models/featureGates.d.cts +2 -2
- package/dist/application/models/featureGates.d.ts +2 -2
- package/dist/application/models/featureGates.js +2 -2
- package/dist/application/utils/elementObserver.d.cts +2 -2
- package/dist/atoms/app/index.d.cts +7 -7
- package/dist/atoms/app/index.d.ts +1 -1
- 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.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.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 +3 -3
- package/dist/atoms/chat/suggestions.d.ts +3 -3
- package/dist/atoms/envive/enviveConfig.d.cts +14 -14
- package/dist/atoms/globalSearch/globalSearch.d.cts +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/types.d.cts +1 -1
- package/dist/atoms/search/utils.d.cts +1 -1
- package/dist/atoms/widget/chatPreviewLoading.d.cts +2 -2
- package/dist/atoms/widget/chatPreviewLoading.d.ts +2 -2
- package/dist/contexts/pageContext/pageContext.cjs +12 -24
- package/dist/contexts/pageContext/pageContext.js +12 -24
- 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.cjs +8 -1
- package/dist/contexts/typesV3.d.cts +23 -6
- package/dist/contexts/typesV3.d.ts +23 -6
- package/dist/contexts/typesV3.js +8 -2
- package/dist/hooks/Intersection/useIntersection.cjs +9 -2
- package/dist/hooks/Intersection/useIntersection.d.cts +2 -2
- package/dist/hooks/Intersection/useIntersection.d.ts +2 -2
- package/dist/hooks/Intersection/useIntersection.js +9 -2
- package/dist/hooks/SystemSettingsContext/useSystemSettingsContext.d.cts +2 -2
- package/dist/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.cjs +12 -3
- package/dist/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.d.cts +2 -1
- package/dist/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.d.ts +2 -1
- package/dist/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.js +13 -4
- package/dist/hooks/WidgetLoadDiagnostics/index.cjs +4 -0
- package/dist/hooks/WidgetLoadDiagnostics/index.d.cts +2 -0
- package/dist/hooks/WidgetLoadDiagnostics/index.d.ts +2 -0
- package/dist/hooks/WidgetLoadDiagnostics/index.js +3 -0
- package/dist/hooks/WidgetLoadDiagnostics/useWidgetLoadDiagnostics.cjs +122 -0
- package/dist/hooks/WidgetLoadDiagnostics/useWidgetLoadDiagnostics.d.cts +48 -0
- package/dist/hooks/WidgetLoadDiagnostics/useWidgetLoadDiagnostics.d.ts +48 -0
- package/dist/hooks/WidgetLoadDiagnostics/useWidgetLoadDiagnostics.js +120 -0
- package/dist/hooks/utils.d.cts +1 -1
- package/dist/hooks/utils.d.ts +1 -1
- package/dist/services/amplitudeService/eventNames.cjs +2 -1
- package/dist/services/amplitudeService/eventNames.d.cts +2 -1
- package/dist/services/amplitudeService/eventNames.d.ts +2 -1
- package/dist/services/amplitudeService/eventNames.js +2 -1
- package/dist/services/enviveConfigService/enviveConfigService.cjs +21 -35
- package/dist/services/enviveConfigService/enviveConfigService.d.cts +1 -2
- package/dist/services/enviveConfigService/enviveConfigService.d.ts +1 -2
- package/dist/services/enviveConfigService/enviveConfigService.js +21 -35
- package/dist/services/enviveConfigService/fetchBootstrapConfig.cjs +15 -16
- package/dist/services/enviveConfigService/fetchBootstrapConfig.js +15 -16
- package/dist/services/enviveConfigService/fetchGraphQLConfig.cjs +1 -69
- package/dist/services/enviveConfigService/fetchGraphQLConfig.js +2 -69
- package/dist/services/featureFlagService/index.cjs +7 -5
- package/dist/services/featureFlagService/index.d.cts +2 -1
- package/dist/services/featureFlagService/index.d.ts +2 -1
- package/dist/services/featureFlagService/index.js +7 -5
- package/dist/services/ga4ProjectionService/ga4EventSchema.cjs +3 -2
- package/dist/services/ga4ProjectionService/ga4EventSchema.js +3 -2
- package/dist/services/hardcopyService/hardcopyService.cjs +12 -3
- package/dist/services/hardcopyService/hardcopyService.d.cts +3 -1
- package/dist/services/hardcopyService/hardcopyService.d.ts +3 -1
- package/dist/services/hardcopyService/hardcopyService.js +12 -3
- package/package.json +5 -1
- package/src/application/models/featureGates.ts +11 -8
- package/src/contexts/pageContext/__tests__/pageContext.test.tsx +4 -59
- package/src/contexts/pageContext/pageContext.tsx +20 -41
- package/src/contexts/typesV3.ts +22 -4
- package/src/hooks/Intersection/useIntersection.ts +11 -0
- package/src/hooks/TrackComponentVisibleEvent/__tests__/useTrackComponentVisibleEvent.test.tsx +64 -0
- package/src/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.ts +23 -4
- package/src/hooks/WidgetLoadDiagnostics/__tests__/useWidgetLoadDiagnostics.test.ts +161 -0
- package/src/hooks/WidgetLoadDiagnostics/index.ts +5 -0
- package/src/hooks/WidgetLoadDiagnostics/useWidgetLoadDiagnostics.ts +178 -0
- package/src/services/amplitudeService/eventNames.ts +4 -0
- package/src/services/enviveConfigService/__tests__/enviveConfigService.test.ts +72 -158
- package/src/services/enviveConfigService/__tests__/fetchBootstrapConfig.test.ts +111 -13
- package/src/services/enviveConfigService/__tests__/fetchGraphQLConfig.test.ts +161 -490
- package/src/services/enviveConfigService/enviveConfigService.ts +51 -87
- package/src/services/enviveConfigService/fetchBootstrapConfig.ts +74 -66
- package/src/services/enviveConfigService/fetchGraphQLConfig.ts +0 -120
- package/src/services/featureFlagService/__tests__/getFeatureFlagOverrides.test.ts +54 -0
- package/src/services/featureFlagService/index.ts +19 -6
- package/src/services/ga4ProjectionService/ga4EventSchema.ts +5 -0
- package/src/services/hardcopyService/__tests__/hardcopyService.test.ts +16 -5
- package/src/services/hardcopyService/hardcopyService.ts +10 -2
- package/dist/application/models/graphql/queries/getWidgetConfigQuery.cjs +0 -42
- package/dist/application/models/graphql/queries/getWidgetConfigQuery.js +0 -41
- package/src/application/models/graphql/queries/getWidgetConfigQuery.ts +0 -55
|
@@ -3,15 +3,11 @@ import { FeatureGates } from 'src/application/models/featureGates';
|
|
|
3
3
|
import type { GraphQlConfigValues } from 'src/contexts/graphqlContext';
|
|
4
4
|
import { configVersionOverride } from 'src/types/config-versions';
|
|
5
5
|
import { FeatureFlagService } from 'src/services/featureFlagService';
|
|
6
|
-
import Logger from 'src/application/logging/logger';
|
|
7
6
|
import type { UrlResolverResponse } from 'src/atoms/app/variant';
|
|
8
7
|
import { getAtomStore } from 'src/atoms/atomStore/atomStore';
|
|
9
8
|
import { chatIdAtom } from 'src/atoms/app';
|
|
10
|
-
import { fetchUnifiedGraphQLConfig } from './fetchGraphQLConfig';
|
|
11
9
|
import { fetchBootstrapConfig } from './fetchBootstrapConfig';
|
|
12
10
|
|
|
13
|
-
const logger = new Logger('enviveConfigService');
|
|
14
|
-
|
|
15
11
|
// Short-lived so config changes (gates, experiments) still propagate within
|
|
16
12
|
// a session, while sparing every page navigation from re-fetching org
|
|
17
13
|
// config + GraphQL config, both of which sit on the critical path before
|
|
@@ -30,9 +26,9 @@ type EnviveConfigServiceProps = {
|
|
|
30
26
|
namespace: string;
|
|
31
27
|
source: string;
|
|
32
28
|
// The url_resolving context bits the hooks layer can't derive itself, supplied
|
|
33
|
-
// by the injection bundle so getEnviveConfig can
|
|
34
|
-
//
|
|
35
|
-
//
|
|
29
|
+
// by the injection bundle so getEnviveConfig can request bootstrap's
|
|
30
|
+
// url_resolving branch. Optional: when absent (mocks, or a disabled session)
|
|
31
|
+
// bootstrap is still called for config, just without the url_resolving branch.
|
|
36
32
|
env?: string;
|
|
37
33
|
contextSource?: string;
|
|
38
34
|
};
|
|
@@ -118,7 +114,8 @@ export class EnviveConfigService implements IEnviveConfigService {
|
|
|
118
114
|
// Hands off (once) the url_resolving payload that /v1/session/bootstrap
|
|
119
115
|
// returned alongside the config — paired with the URL it was resolved for — so
|
|
120
116
|
// the warm-up seeds urlResolverAtom with it instead of firing a separate
|
|
121
|
-
// /v1/url_resolving. null on the
|
|
117
|
+
// /v1/url_resolving. null on the cached path or a config-only bootstrap (a
|
|
118
|
+
// disabled session, which requests no url_resolving branch).
|
|
122
119
|
takeBootstrapUrlResolving(): BootstrapUrlResolving | null {
|
|
123
120
|
const value = this.bootstrapUrlResolving;
|
|
124
121
|
this.bootstrapUrlResolving = null;
|
|
@@ -176,91 +173,58 @@ export class EnviveConfigService implements IEnviveConfigService {
|
|
|
176
173
|
return this.response;
|
|
177
174
|
}
|
|
178
175
|
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
// list of gates the app checks, so the
|
|
176
|
+
// /v1/session/bootstrap is the single session-config call: org identity +
|
|
177
|
+
// feature gates + experiment assignments + productsConfig/resolution, PLUS
|
|
178
|
+
// the initial url_resolving — the consolidation of GET /v1/org/config,
|
|
179
|
+
// GraphQL me.getWidgetConfig, and POST /v1/url_resolving into one round trip.
|
|
180
|
+
// `gateNames` is the full list of gates the app checks, so the server
|
|
181
|
+
// evaluates and returns them. A failure throws (see fetchBootstrapConfig) and
|
|
182
|
+
// propagates to inject.ts, which aborts injection — there is no getWidgetConfig
|
|
183
|
+
// left to fall back to, the same blast radius the single /v1/org/config had.
|
|
184
184
|
const gateNames = Object.values(FeatureGates).map(featureGate => featureGate.toString());
|
|
185
185
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
186
|
+
// env + contextSource are supplied only by the injection bundle (the hooks
|
|
187
|
+
// layer can't derive them). Present → request bootstrap's url_resolving branch
|
|
188
|
+
// and stash it for the warm-up to seed. Absent (mocks, or a disabled session
|
|
189
|
+
// that still needs org config + gates for the "Envive Initialized" event) →
|
|
190
|
+
// bootstrap returns config only, no page-variant round trip.
|
|
191
|
+
const wantsUrlResolving = this.env !== undefined && this.contextSource !== undefined;
|
|
192
|
+
// Captured once and reused for the stash so the URL carried to the warm-up is
|
|
193
|
+
// provably the one bootstrap resolved. The warm-up only seeds it if this still
|
|
194
|
+
// matches the current URL (a SPA nav during the round trip would otherwise
|
|
195
|
+
// cache this result under a later URL). Same cleansing as warmUrlResolver so
|
|
196
|
+
// the urlResolverAtom key lines up.
|
|
197
|
+
const cleansedUrl = window.location.href.toLowerCase().trim();
|
|
193
198
|
|
|
194
|
-
|
|
199
|
+
const { config, urlResolving } = await fetchBootstrapConfig({
|
|
200
|
+
baseUrl: this.baseUrl,
|
|
201
|
+
apiKey: this.apiKey,
|
|
202
|
+
userId: this.userId,
|
|
203
|
+
source: this.source,
|
|
204
|
+
// Scope org_config.configs to this namespace so bootstrap doesn't return
|
|
205
|
+
// internal-only org configs to the browser (matches the legacy /v1/org/config).
|
|
206
|
+
namespace: this.namespace,
|
|
207
|
+
gateNames,
|
|
208
|
+
// The persistent chat session id (same source resolveUrl reads) so
|
|
209
|
+
// bootstrap's url_resolving carries it instead of an empty string.
|
|
210
|
+
chatId: getAtomStore().get(chatIdAtom),
|
|
211
|
+
...(wantsUrlResolving
|
|
212
|
+
? {
|
|
213
|
+
url: cleansedUrl,
|
|
214
|
+
contextSource: this.contextSource,
|
|
215
|
+
env: this.env,
|
|
216
|
+
// Client-side gate overrides only (query/window/localStorage); the
|
|
217
|
+
// server resolves the real gate values and merges these over them.
|
|
218
|
+
featureGates: FeatureFlagService.getFeatureFlagOverrides(),
|
|
219
|
+
}
|
|
220
|
+
: {}),
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
this.bootstrapUrlResolving = urlResolving ? { url: cleansedUrl, response: urlResolving } : null;
|
|
224
|
+
this.response = config;
|
|
195
225
|
this.writeCachedConfig(this.response);
|
|
196
226
|
return this.response;
|
|
197
227
|
}
|
|
198
|
-
|
|
199
|
-
// The unified-config ramp (`use_unified_config` Statsig gate). When the gate
|
|
200
|
-
// is ON for this user, swap the getWidgetConfig result for Matt's
|
|
201
|
-
// /v1/session/bootstrap — the same config PLUS the initial url_resolving in
|
|
202
|
-
// ONE round trip (the consolidation this migration is about). The gate is read
|
|
203
|
-
// off the getWidgetConfig response just fetched, because the FE has no Statsig
|
|
204
|
-
// SDK: pymono evaluates the gate server-side and returns it. `FeatureFlagService`
|
|
205
|
-
// also honors a `?use_unified_config=true` override, so the bootstrap path is
|
|
206
|
-
// testable before the gate ramps.
|
|
207
|
-
//
|
|
208
|
-
// Only runs on the cache-miss path (bootstrap's config is then cached like any
|
|
209
|
-
// other), so a cached load never re-fetches. Needs env + contextSource for the
|
|
210
|
-
// url_resolving context — absent for mocks / non-bundle callers, where the
|
|
211
|
-
// branch is skipped. A bootstrap failure falls back to the getWidgetConfig
|
|
212
|
-
// config already in hand: a bootstrap problem degrades to the proven path
|
|
213
|
-
// instead of aborting injection. At 100% the getWidgetConfig call above is
|
|
214
|
-
// dropped and bootstrap becomes the sole config call (step 8).
|
|
215
|
-
private async maybeUpgradeToBootstrap(
|
|
216
|
-
widgetConfig: EnviveServiceConfig,
|
|
217
|
-
gateNames: string[],
|
|
218
|
-
): Promise<EnviveServiceConfig> {
|
|
219
|
-
if (this.env === undefined || this.contextSource === undefined) {
|
|
220
|
-
return widgetConfig;
|
|
221
|
-
}
|
|
222
|
-
// Built once: reads the gate (override-aware) AND supplies the override-aware
|
|
223
|
-
// gate map bootstrap's url_resolving needs (matches the legacy resolveUrl path).
|
|
224
|
-
const featureFlagService = new FeatureFlagService(widgetConfig.gates);
|
|
225
|
-
if (!featureFlagService.isFeatureGateEnabled(FeatureGates.UseUnifiedConfig)) {
|
|
226
|
-
return widgetConfig;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Captured once and reused for both the bootstrap request and the stash, so
|
|
230
|
-
// the URL carried to the warm-up is provably the one bootstrap resolved. The
|
|
231
|
-
// warm-up only seeds it if this still matches the current URL (a SPA nav
|
|
232
|
-
// during the round trip would otherwise cache this result under a later URL).
|
|
233
|
-
// Same cleansing as warmUrlResolver so the urlResolverAtom key lines up.
|
|
234
|
-
const cleansedUrl = window.location.href.toLowerCase().trim();
|
|
235
|
-
try {
|
|
236
|
-
const { config, urlResolving } = await fetchBootstrapConfig({
|
|
237
|
-
baseUrl: this.baseUrl,
|
|
238
|
-
apiKey: this.apiKey,
|
|
239
|
-
userId: this.userId,
|
|
240
|
-
source: this.source,
|
|
241
|
-
// Scope org_config.configs to this namespace so bootstrap doesn't return
|
|
242
|
-
// internal-only org configs to the browser (matches the legacy /v1/org/config).
|
|
243
|
-
namespace: this.namespace,
|
|
244
|
-
gateNames,
|
|
245
|
-
url: cleansedUrl,
|
|
246
|
-
orgId: widgetConfig.org.org.id,
|
|
247
|
-
orgShortName: widgetConfig.org.org.short_name,
|
|
248
|
-
contextSource: this.contextSource,
|
|
249
|
-
env: this.env,
|
|
250
|
-
featureGates: featureFlagService.getFeatureFlags(),
|
|
251
|
-
// The persistent chat session id (same source resolveUrl reads) so
|
|
252
|
-
// bootstrap's url_resolving carries it instead of an empty string.
|
|
253
|
-
chatId: getAtomStore().get(chatIdAtom),
|
|
254
|
-
});
|
|
255
|
-
this.bootstrapUrlResolving = urlResolving
|
|
256
|
-
? { url: cleansedUrl, response: urlResolving }
|
|
257
|
-
: null;
|
|
258
|
-
return config;
|
|
259
|
-
} catch (err) {
|
|
260
|
-
logger.logError('getEnviveConfig | bootstrap upgrade failed, using getWidgetConfig', err);
|
|
261
|
-
return widgetConfig;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
228
|
}
|
|
265
229
|
|
|
266
230
|
/**
|
|
@@ -14,11 +14,6 @@ import type { EnviveServiceConfig } from './enviveConfigService';
|
|
|
14
14
|
|
|
15
15
|
const logger = new Logger('fetchBootstrapConfig');
|
|
16
16
|
|
|
17
|
-
// The url-resolving context bits the hooks layer can't derive on its own — the
|
|
18
|
-
// injection bundle supplies env (a build-time var) and contextSource. org
|
|
19
|
-
// identity is threaded from the getWidgetConfig response fetched moments earlier
|
|
20
|
-
// (see enviveConfigService: bootstrap only runs on the gate-ON path, which reads
|
|
21
|
-
// the gate off getWidgetConfig first, so org id/short_name are already in hand).
|
|
22
17
|
export interface FetchBootstrapConfigParams {
|
|
23
18
|
baseUrl: string;
|
|
24
19
|
apiKey: string;
|
|
@@ -31,22 +26,27 @@ export interface FetchBootstrapConfigParams {
|
|
|
31
26
|
// bundle receives. The legacy /v1/org/config call scoped it the same way.
|
|
32
27
|
namespace: string;
|
|
33
28
|
gateNames: string[];
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
29
|
+
chatId?: string;
|
|
30
|
+
// url_resolving inputs, all supplied together by the injection bundle when
|
|
31
|
+
// Envive is enabled. When `url` is present, bootstrap runs its page-variant
|
|
32
|
+
// branch and returns the initial url_resolving; when absent (envive_on=false —
|
|
33
|
+
// there is no page to resolve) the branch is omitted and bootstrap returns
|
|
34
|
+
// config only. org identity is NO LONGER sent: /v1/session/bootstrap fills
|
|
35
|
+
// org_id/org_short_name from the authenticated org (the API key), so the
|
|
36
|
+
// bootstrap-only client — which no longer calls getWidgetConfig — needs no way
|
|
37
|
+
// to learn its own org before the request.
|
|
38
|
+
url?: string;
|
|
38
39
|
// Context `source` for url_resolving ('app' | 'playground'); may differ from
|
|
39
40
|
// the config `source` above (playground = Envive Hub).
|
|
40
|
-
contextSource
|
|
41
|
-
env
|
|
42
|
-
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
// ship ahead of the backend.
|
|
41
|
+
contextSource?: string;
|
|
42
|
+
env?: string;
|
|
43
|
+
// Override-ONLY feature gates (FeatureFlagService.getFeatureFlagOverrides()):
|
|
44
|
+
// just the client-side overrides (query/window/localStorage), which bootstrap
|
|
45
|
+
// merges OVER its own server-resolved gates for page-variant selection — the
|
|
46
|
+
// legacy /v1/url_resolving behaviour. NOT a full resolved map: on the
|
|
47
|
+
// bootstrap-only path the client has no server gate values to resolve from, so
|
|
48
|
+
// a full map would clobber the server's gates. Empty (the common case, no
|
|
49
|
+
// overrides) leaves the server gates untouched.
|
|
50
50
|
featureGates?: Record<string, boolean>;
|
|
51
51
|
}
|
|
52
52
|
|
|
@@ -127,18 +127,17 @@ const toResolution = (
|
|
|
127
127
|
// `me.getWidgetConfig`, and `POST /v1/url_resolving` behind Matt's endpoint.
|
|
128
128
|
//
|
|
129
129
|
// Raw fetch (the npm client `@spiffy-ai/commerce-api-client` has no bootstrap
|
|
130
|
-
// method
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
130
|
+
// method). Throws on network/HTTP failure/timeout, which propagates through
|
|
131
|
+
// getEnviveConfig to inject.ts and aborts injection — bootstrap is now the SOLE
|
|
132
|
+
// config call, so there is no getWidgetConfig left to fall back to (the same
|
|
133
|
+
// blast radius the single GET /v1/org/config call always had).
|
|
134
134
|
//
|
|
135
|
-
// Bootstrap
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
// bootstraps false-abort into the fallback, defeating the consolidation).
|
|
135
|
+
// Bootstrap BLOCKS the pre-first-paint critical path, so it is bounded by a
|
|
136
|
+
// timeout: a hung /v1/session/bootstrap (server black-hole, half-open socket,
|
|
137
|
+
// slow-loris body) would otherwise block injection until the browser's
|
|
138
|
+
// multi-minute default. CALIBRATE against the endpoint's real p99 — with no
|
|
139
|
+
// fallback, a value below p99 turns normal-but-slow bootstraps into FAILED
|
|
140
|
+
// injections, so err on the generous side rather than the aggressive one.
|
|
142
141
|
const BOOTSTRAP_TIMEOUT_MS = 1500;
|
|
143
142
|
|
|
144
143
|
export const fetchBootstrapConfig = async ({
|
|
@@ -148,19 +147,43 @@ export const fetchBootstrapConfig = async ({
|
|
|
148
147
|
source,
|
|
149
148
|
namespace,
|
|
150
149
|
gateNames,
|
|
150
|
+
chatId,
|
|
151
151
|
url,
|
|
152
|
-
orgId,
|
|
153
|
-
orgShortName,
|
|
154
152
|
contextSource,
|
|
155
153
|
env,
|
|
156
|
-
chatId,
|
|
157
154
|
featureGates,
|
|
158
155
|
}: FetchBootstrapConfigParams): Promise<BootstrapResult> => {
|
|
159
156
|
const version = configVersionOverride();
|
|
157
|
+
// The url_resolving branch is requested only when the bundle supplied a URL
|
|
158
|
+
// (Envive enabled). Absent → bootstrap returns config only (envive_on=false).
|
|
159
|
+
const urlResolvingBody =
|
|
160
|
+
url !== undefined
|
|
161
|
+
? {
|
|
162
|
+
url_resolving: {
|
|
163
|
+
url,
|
|
164
|
+
context: {
|
|
165
|
+
user_id: userId,
|
|
166
|
+
// org_id / org_short_name intentionally omitted — the server fills
|
|
167
|
+
// them from the authenticated org (see SessionBootstrapService).
|
|
168
|
+
chat_id: chatId ?? '',
|
|
169
|
+
source: contextSource,
|
|
170
|
+
env,
|
|
171
|
+
},
|
|
172
|
+
// Override-only gates (empty unless the client set an override); the
|
|
173
|
+
// server merges these OVER its own resolved gates (pymono
|
|
174
|
+
// SessionBootstrapUrlResolvingRequest.feature_gates), so client-side
|
|
175
|
+
// overrides still reach page-variant selection.
|
|
176
|
+
feature_gates: featureGates,
|
|
177
|
+
// The config-version override comes ONLY from the URL query param
|
|
178
|
+
// (configVersionOverride) — the same source the config version uses.
|
|
179
|
+
override_config_version: version,
|
|
180
|
+
},
|
|
181
|
+
}
|
|
182
|
+
: {};
|
|
160
183
|
|
|
161
184
|
// Armed before fetch and kept live across response.json() (a stalled body read
|
|
162
185
|
// must also be interruptible), cleared in finally. On timeout the fetch rejects
|
|
163
|
-
// with an AbortError →
|
|
186
|
+
// with an AbortError → propagates through getEnviveConfig and aborts injection.
|
|
164
187
|
const controller = new AbortController();
|
|
165
188
|
const timeoutId = setTimeout(() => controller.abort(), BOOTSTRAP_TIMEOUT_MS);
|
|
166
189
|
let result: RawBootstrapResponse;
|
|
@@ -179,28 +202,7 @@ export const fetchBootstrapConfig = async ({
|
|
|
179
202
|
namespace,
|
|
180
203
|
include_feature_gates: gateNames,
|
|
181
204
|
version,
|
|
182
|
-
|
|
183
|
-
url,
|
|
184
|
-
context: {
|
|
185
|
-
user_id: userId,
|
|
186
|
-
org_id: orgId,
|
|
187
|
-
org_short_name: orgShortName,
|
|
188
|
-
chat_id: chatId ?? '',
|
|
189
|
-
source: contextSource,
|
|
190
|
-
env,
|
|
191
|
-
},
|
|
192
|
-
// Override-aware gates so client gate overrides reach page-variant
|
|
193
|
-
// resolution (bootstrap merges these over its server-resolved gates).
|
|
194
|
-
// Ignored by pymono until the endpoint gains the field — safe to ship early.
|
|
195
|
-
feature_gates: featureGates,
|
|
196
|
-
// The config-version override comes ONLY from the URL query param
|
|
197
|
-
// (configVersionOverride) — the same source getWidgetConfig uses. It must
|
|
198
|
-
// NOT come from getWidgetConfig's resolved version: on the gate-ON path
|
|
199
|
-
// that response is discarded, and bootstrap resolves the config version
|
|
200
|
-
// server-side (scoping url_resolving to that resolved version, if desired,
|
|
201
|
-
// belongs on the server — not a getWidgetConfig dependency here).
|
|
202
|
-
override_config_version: version,
|
|
203
|
-
},
|
|
205
|
+
...urlResolvingBody,
|
|
204
206
|
}),
|
|
205
207
|
signal: controller.signal,
|
|
206
208
|
});
|
|
@@ -213,16 +215,22 @@ export const fetchBootstrapConfig = async ({
|
|
|
213
215
|
clearTimeout(timeoutId);
|
|
214
216
|
}
|
|
215
217
|
|
|
216
|
-
// A 2xx with a missing/partial payload must NOT silently
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
// required
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
|
|
218
|
+
// A 2xx with a missing/partial payload must NOT silently become a broken config
|
|
219
|
+
// — a missing org identity or widget_config would drop products/experiments.
|
|
220
|
+
// Throw instead: with bootstrap the sole config call, the throw propagates to
|
|
221
|
+
// inject.ts and aborts injection rather than rendering a broken session. `gates`
|
|
222
|
+
// is required to be an ARRAY (present) but MAY be empty: an empty gates response
|
|
223
|
+
// is a valid "everything off" signal — FeatureFlagService then resolves every
|
|
224
|
+
// gate to false, so isClientSessionEnabled is false and the session disables
|
|
225
|
+
// gracefully (with the "Envive Initialized" event still firing). Rejecting empty
|
|
226
|
+
// gates would instead abort injection and lose that event. (A products config
|
|
227
|
+
// that is present-but-malformed still degrades to mock below — only the
|
|
228
|
+
// top-level sections are required here.)
|
|
229
|
+
if (
|
|
230
|
+
!result.org_config?.org?.org ||
|
|
231
|
+
!Array.isArray(result.org_config?.gates) ||
|
|
232
|
+
!result.widget_config
|
|
233
|
+
) {
|
|
226
234
|
throw new Error(
|
|
227
235
|
'Bootstrap config response missing required sections (org_config.org.org / org_config.gates / widget_config)',
|
|
228
236
|
);
|
|
@@ -11,20 +11,10 @@ import { ColorMappingV3 } from 'src/application/models/colorsConfigV3';
|
|
|
11
11
|
// React) through the package's component index into the injection bundle's
|
|
12
12
|
// pre-config chunk.
|
|
13
13
|
import type { FloatingButtonLocation } from '@envive-ai/react-toolkit-v3/FloatingButton';
|
|
14
|
-
import { getWidgetConfigQuery } from 'src/application/models/graphql/queries/getWidgetConfigQuery';
|
|
15
14
|
import { mockV3ColorsConfig, mockV3FrontendConfig } from 'src/contexts/graphqlContext/mockV3Config';
|
|
16
15
|
import { WidgetConfigV3 } from 'src/contexts/typesV3';
|
|
17
16
|
import { PageVariantConfig, PageVariantTestType, WidgetMountingConfig } from 'src/contexts/types';
|
|
18
17
|
import type { GraphQlConfigValues } from 'src/contexts/graphqlContext';
|
|
19
|
-
import { configVersionOverride } from 'src/types/config-versions';
|
|
20
|
-
import {
|
|
21
|
-
ExperimentConfigResolutionMetadata,
|
|
22
|
-
OrgConfigExperimentAssignment,
|
|
23
|
-
OrgConfigFeatureGate,
|
|
24
|
-
} from 'src/application/models/api/orgConfigResults';
|
|
25
|
-
// Type-only import — erased at compile time, so this does not create a runtime
|
|
26
|
-
// import cycle with enviveConfigService (which imports fetchUnifiedGraphQLConfig).
|
|
27
|
-
import type { EnviveServiceConfig } from './enviveConfigService';
|
|
28
18
|
|
|
29
19
|
const logger = new Logger('fetchGraphQLConfig');
|
|
30
20
|
|
|
@@ -238,113 +228,3 @@ export const transformV3ProductsConfig = (
|
|
|
238
228
|
},
|
|
239
229
|
};
|
|
240
230
|
};
|
|
241
|
-
|
|
242
|
-
// The camelCase `experimentAssignments` GraphQL shape → the snake_case wire
|
|
243
|
-
// shape amplitudeService reads (it previously arrived from REST /v1/org/config).
|
|
244
|
-
const toExperimentAssignments = (
|
|
245
|
-
raw:
|
|
246
|
-
| ReadonlyArray<{
|
|
247
|
-
layerName?: string;
|
|
248
|
-
namespace?: OrgConfigExperimentAssignment['namespace'];
|
|
249
|
-
allocatedExperimentName?: string | null;
|
|
250
|
-
groupName?: string | null;
|
|
251
|
-
}>
|
|
252
|
-
| undefined,
|
|
253
|
-
): OrgConfigExperimentAssignment[] =>
|
|
254
|
-
(raw ?? []).map(a => ({
|
|
255
|
-
layer_name: a.layerName,
|
|
256
|
-
namespace: a.namespace,
|
|
257
|
-
allocated_experiment_name: a.allocatedExperimentName,
|
|
258
|
-
group_name: a.groupName,
|
|
259
|
-
}));
|
|
260
|
-
|
|
261
|
-
// The unified `getWidgetConfig` config path — now the single source of session
|
|
262
|
-
// config. Returns everything the client needs in one call: org identity,
|
|
263
|
-
// feature gates, and experiment assignments (which used to come from REST
|
|
264
|
-
// `/v1/org/config`), plus the productsConfig and the resolution metadata pymono
|
|
265
|
-
// used to pick the version (callers thread `resolution.baseVersion` into
|
|
266
|
-
// downstream inference so it's scoped to the version that produced the rendered
|
|
267
|
-
// widgets). `gateNames` selects which gates to evaluate; `source` feeds Statsig
|
|
268
|
-
// user properties. A `spiffy_config_version` / `envive_config_version` URL
|
|
269
|
-
// param, when present, is threaded through as the `version` variable to pin a
|
|
270
|
-
// specific version (bypasses the merchant experiment — QA / rollback).
|
|
271
|
-
//
|
|
272
|
-
// Throws on network/GraphQL failure: with org + gates sourced here, there is no
|
|
273
|
-
// partial-render fallback (inject.ts aborts injection on the throw), matching
|
|
274
|
-
// how the old /v1/org/config call behaved.
|
|
275
|
-
export const fetchUnifiedGraphQLConfig = async (
|
|
276
|
-
baseUrl: string,
|
|
277
|
-
apiKey: string,
|
|
278
|
-
userId: string,
|
|
279
|
-
gateNames: string[],
|
|
280
|
-
source: string,
|
|
281
|
-
): Promise<EnviveServiceConfig> => {
|
|
282
|
-
const query = getWidgetConfigQuery();
|
|
283
|
-
const version = configVersionOverride();
|
|
284
|
-
const response = await fetch(`${baseUrl}/v1/graphql`, {
|
|
285
|
-
method: 'POST',
|
|
286
|
-
headers: {
|
|
287
|
-
'Content-Type': 'application/json',
|
|
288
|
-
Authorization: `Bearer ${apiKey}`,
|
|
289
|
-
},
|
|
290
|
-
body: JSON.stringify({ query, variables: { userId, version, gateNames, source } }),
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
if (!response.ok) {
|
|
294
|
-
throw new Error(`Unified config request failed: ${response.statusText}`);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const result = await response.json();
|
|
298
|
-
if (result.errors) {
|
|
299
|
-
throw new Error(`Unified config GraphQL errors: ${JSON.stringify(result.errors)}`);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const me = result.data?.me;
|
|
303
|
-
const widgetConfig = me?.getWidgetConfig;
|
|
304
|
-
const org = me?.org;
|
|
305
|
-
|
|
306
|
-
// A malformed products config (e.g. an invalid floating-button position)
|
|
307
|
-
// degrades to no config — widgets fall back to their mock defaults — WITHOUT
|
|
308
|
-
// losing the org / gates / experiments that came back fine. The fetch itself
|
|
309
|
-
// still throws above; only the products-config transform is guarded here.
|
|
310
|
-
const resolution = widgetConfig?.resolution as ExperimentConfigResolutionMetadata | undefined;
|
|
311
|
-
let orgConfig: GraphQlConfigValues;
|
|
312
|
-
try {
|
|
313
|
-
orgConfig = { ...transformV3ProductsConfig(widgetConfig?.productsConfig), resolution };
|
|
314
|
-
} catch (err) {
|
|
315
|
-
logger.logError('fetchUnifiedGraphQLConfig | Error transforming products config', err);
|
|
316
|
-
orgConfig = { colorsConfig: undefined, frontendConfig: undefined, resolution };
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
return {
|
|
320
|
-
// GraphQL Organization is camelCase; the rest of the app reads the
|
|
321
|
-
// snake_case shape /v1/org/config used to return. `status` isn't exposed by
|
|
322
|
-
// the GraphQL type (and isn't consumed downstream) — default it.
|
|
323
|
-
org: {
|
|
324
|
-
org: {
|
|
325
|
-
id: org?.id ?? '',
|
|
326
|
-
short_name: org?.shortName ?? '',
|
|
327
|
-
display_name: org?.displayName ?? '',
|
|
328
|
-
domain: org?.domain ?? '',
|
|
329
|
-
status: '',
|
|
330
|
-
created_at: org?.createdAt ?? '',
|
|
331
|
-
updated_at: org?.updatedAt ?? '',
|
|
332
|
-
},
|
|
333
|
-
},
|
|
334
|
-
gates: (
|
|
335
|
-
(widgetConfig?.gates ?? []) as Array<{
|
|
336
|
-
name?: string;
|
|
337
|
-
value?: boolean;
|
|
338
|
-
groupName?: string;
|
|
339
|
-
}>
|
|
340
|
-
).map(
|
|
341
|
-
(g): OrgConfigFeatureGate => ({
|
|
342
|
-
name: g.name,
|
|
343
|
-
value: g.value,
|
|
344
|
-
groupName: g.groupName,
|
|
345
|
-
}),
|
|
346
|
-
),
|
|
347
|
-
experiment_assignments: toExperimentAssignments(widgetConfig?.experimentAssignments),
|
|
348
|
-
orgConfig,
|
|
349
|
-
};
|
|
350
|
-
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { FeatureFlagService } from '../index';
|
|
2
|
+
|
|
3
|
+
// getFeatureFlagOverrides feeds /v1/session/bootstrap's url_resolving.feature_gates.
|
|
4
|
+
// It must return ONLY the gates the client actually overrides — never a full
|
|
5
|
+
// resolved map — so the server's own resolved gates survive the merge (the client
|
|
6
|
+
// on the bootstrap-only path has no server values to resolve from).
|
|
7
|
+
describe('FeatureFlagService.getFeatureFlagOverrides', () => {
|
|
8
|
+
const _envive = window as Window & { _envive?: { featureOverrides?: Record<string, boolean> } };
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
window.history.replaceState({}, '', '/');
|
|
12
|
+
window.localStorage.clear();
|
|
13
|
+
delete _envive._envive;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
window.history.replaceState({}, '', '/');
|
|
18
|
+
window.localStorage.clear();
|
|
19
|
+
delete _envive._envive;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('returns an empty map when nothing is overridden', () => {
|
|
23
|
+
expect(FeatureFlagService.getFeatureFlagOverrides()).toEqual({});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('includes only the gates overridden via query param', () => {
|
|
27
|
+
window.history.replaceState({}, '', '/?use_unified_config=true&is_empty_div_plp_enabled=false');
|
|
28
|
+
|
|
29
|
+
const overrides = FeatureFlagService.getFeatureFlagOverrides();
|
|
30
|
+
|
|
31
|
+
// Only the two overridden gates appear — un-overridden gates are absent
|
|
32
|
+
// entirely (NOT resolved to false), so the server's gates survive the merge.
|
|
33
|
+
expect(overrides).toEqual({ use_unified_config: true, is_empty_div_plp_enabled: false });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('includes a gate overridden via localStorage', () => {
|
|
37
|
+
window.localStorage.setItem(
|
|
38
|
+
'spiffy-feature-flags',
|
|
39
|
+
JSON.stringify({ use_unified_config: true }),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
expect(FeatureFlagService.getFeatureFlagOverrides()).toEqual({ use_unified_config: true });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('lets a query param override win over localStorage for the same gate', () => {
|
|
46
|
+
window.localStorage.setItem(
|
|
47
|
+
'spiffy-feature-flags',
|
|
48
|
+
JSON.stringify({ use_unified_config: false }),
|
|
49
|
+
);
|
|
50
|
+
window.history.replaceState({}, '', '/?use_unified_config=true');
|
|
51
|
+
|
|
52
|
+
expect(FeatureFlagService.getFeatureFlagOverrides().use_unified_config).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -160,17 +160,30 @@ export class FeatureFlagService {
|
|
|
160
160
|
: {};
|
|
161
161
|
};
|
|
162
162
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
163
|
+
// Only the gates the client actually overrides (query param / window / stored),
|
|
164
|
+
// with the un-overridden ones left out entirely — NOT resolved to false. This
|
|
165
|
+
// is what /v1/session/bootstrap's url_resolving wants: the server resolves the
|
|
166
|
+
// real gate values itself and merges this map OVER them, so a full map of
|
|
167
|
+
// resolved values would clobber the server's gates with the client's (which,
|
|
168
|
+
// on the bootstrap-only path, has no server values to resolve from). An empty
|
|
169
|
+
// map (the common case — no overrides) leaves the server's gates untouched.
|
|
170
|
+
static getFeatureFlagOverrides(): Record<string, boolean> {
|
|
171
|
+
return Object.values(FeatureGates)
|
|
168
172
|
.map(
|
|
169
173
|
featureGate =>
|
|
170
174
|
[featureGate, FeatureFlagService.getFeatureFlagOverride(featureGate)] as const,
|
|
171
175
|
)
|
|
172
176
|
.filter(([, value]) => value !== undefined)
|
|
173
177
|
.reduce<Record<string, boolean>>((acc, [key, value]) => ({ ...acc, [key]: value! }), {});
|
|
174
|
-
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
static persistFeatureGateOverrides(): void {
|
|
181
|
+
if (typeof window === 'undefined' || typeof window.localStorage === 'undefined') {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
window.localStorage.setItem(
|
|
185
|
+
FEATURE_FLAGS_STORAGE_KEY,
|
|
186
|
+
JSON.stringify(FeatureFlagService.getFeatureFlagOverrides()),
|
|
187
|
+
);
|
|
175
188
|
}
|
|
176
189
|
}
|
|
@@ -156,4 +156,9 @@ export const GA4_EVENT_SCHEMA: Record<EnviveMetricsEventName, GA4EventSchemaEntr
|
|
|
156
156
|
[EnviveMetricsEventName.WidgetTextClicked]: {
|
|
157
157
|
gaEventName: null,
|
|
158
158
|
},
|
|
159
|
+
|
|
160
|
+
// Internal eng operational monitoring — never projected to merchant GA4.
|
|
161
|
+
[EnviveMetricsEventName.Diagnostics]: {
|
|
162
|
+
gaEventName: null,
|
|
163
|
+
},
|
|
159
164
|
};
|