@envive-ai/react-hooks 0.3.62 → 0.3.64
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/application/models/featureGates.cjs +2 -1
- package/dist/application/models/featureGates.d.cts +2 -1
- package/dist/application/models/featureGates.d.ts +2 -1
- package/dist/application/models/featureGates.js +2 -1
- package/dist/application/utils/widgetTextRequestToApiRequest.cjs +1 -0
- package/dist/application/utils/widgetTextRequestToApiRequest.js +1 -1
- package/dist/atoms/app/variant.d.cts +6 -6
- package/dist/atoms/chat/chatState.d.cts +1 -1
- 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 +7 -7
- 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 +3 -3
- 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 +3 -3
- package/dist/atoms/chat/suggestions.d.cts +3 -3
- package/dist/atoms/chat/suggestions.d.ts +2 -2
- package/dist/atoms/envive/enviveConfig.d.cts +13 -13
- package/dist/atoms/envive/enviveConfig.d.ts +14 -14
- 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.ts +2 -2
- package/dist/atoms/org/orgAnalyticsConfig.d.ts +4 -4
- package/dist/atoms/search/chatSearch.d.ts +17 -17
- package/dist/atoms/search/searchAPI.d.ts +13 -13
- package/dist/atoms/search/utils.d.ts +1 -1
- package/dist/atoms/widget/chatPreviewLoading.d.ts +2 -2
- 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/hooks/GrabAndScroll/useGrabAndScroll.d.cts +2 -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/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 +49 -3
- package/dist/services/enviveConfigService/enviveConfigService.d.cts +16 -2
- package/dist/services/enviveConfigService/enviveConfigService.d.ts +16 -2
- package/dist/services/enviveConfigService/enviveConfigService.js +49 -3
- package/dist/services/enviveConfigService/fetchBootstrapConfig.cjs +102 -0
- package/dist/services/enviveConfigService/fetchBootstrapConfig.js +102 -0
- package/dist/services/enviveConfigService/fetchGraphQLConfig.cjs +2 -1
- package/dist/services/enviveConfigService/fetchGraphQLConfig.js +2 -2
- package/dist/services/ga4ProjectionService/ga4EventSchema.cjs +3 -2
- package/dist/services/ga4ProjectionService/ga4EventSchema.js +3 -2
- package/dist/services/hardcopyService/hardcopyService.cjs +31 -6
- package/dist/services/hardcopyService/hardcopyService.d.cts +5 -2
- package/dist/services/hardcopyService/hardcopyService.d.ts +5 -2
- package/dist/services/hardcopyService/hardcopyService.js +31 -7
- package/dist/services/hardcopyService/index.cjs +1 -0
- package/dist/services/hardcopyService/index.d.cts +2 -2
- package/dist/services/hardcopyService/index.d.ts +2 -2
- package/dist/services/hardcopyService/index.js +2 -2
- package/dist/services/urlResolverWarmupService/index.cjs +3 -3
- package/dist/services/urlResolverWarmupService/index.d.cts +8 -2
- package/dist/services/urlResolverWarmupService/index.d.ts +8 -2
- package/dist/services/urlResolverWarmupService/index.js +3 -3
- package/package.json +5 -1
- package/src/application/models/featureGates.ts +6 -0
- 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 +226 -0
- package/src/services/enviveConfigService/__tests__/fetchBootstrapConfig.test.ts +271 -0
- package/src/services/enviveConfigService/enviveConfigService.ts +124 -5
- package/src/services/enviveConfigService/fetchBootstrapConfig.ts +276 -0
- package/src/services/enviveConfigService/fetchGraphQLConfig.ts +7 -4
- package/src/services/ga4ProjectionService/ga4EventSchema.ts +5 -0
- package/src/services/hardcopyService/__tests__/hardcopyService.test.ts +73 -3
- package/src/services/hardcopyService/hardcopyService.ts +55 -5
- package/src/services/urlResolverWarmupService/__tests__/urlResolverWarmupService.test.ts +55 -0
- package/src/services/urlResolverWarmupService/index.ts +18 -1
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { type FetchBootstrapConfigParams, fetchBootstrapConfig } from '../fetchBootstrapConfig';
|
|
2
|
+
|
|
3
|
+
const mockFetch = vi.fn();
|
|
4
|
+
globalThis.fetch = mockFetch;
|
|
5
|
+
|
|
6
|
+
// A representative /v1/session/bootstrap response (REST snake_case). products_config
|
|
7
|
+
// is left empty so the shared transformV3ProductsConfig falls back to mock config —
|
|
8
|
+
// its internals are covered by fetchGraphQLConfig.test.ts; here we only assert the
|
|
9
|
+
// bootstrap-specific mapping (org/gates/assignments/resolution/url_resolving).
|
|
10
|
+
function makeBootstrapResponse() {
|
|
11
|
+
mockFetch.mockResolvedValueOnce({
|
|
12
|
+
ok: true,
|
|
13
|
+
json: () =>
|
|
14
|
+
Promise.resolve({
|
|
15
|
+
org_config: {
|
|
16
|
+
org: {
|
|
17
|
+
org: {
|
|
18
|
+
id: 'org-boot',
|
|
19
|
+
short_name: 'boot-org',
|
|
20
|
+
display_name: 'Boot Org',
|
|
21
|
+
domain: 'boot.com',
|
|
22
|
+
status: 'active',
|
|
23
|
+
created_at: '2024-01-01T00:00:00.000Z',
|
|
24
|
+
updated_at: '2024-02-02T00:00:00.000Z',
|
|
25
|
+
},
|
|
26
|
+
settings: {},
|
|
27
|
+
},
|
|
28
|
+
gates: [
|
|
29
|
+
{ name: 'use_unified_config', value: true, group_name: 'Treatment', rule_id: 'r1' },
|
|
30
|
+
{ name: 'is_client_session_enabled', value: false, group_name: null, rule_id: null },
|
|
31
|
+
],
|
|
32
|
+
// Decoy: org_config.experiment_assignments is server-gated on
|
|
33
|
+
// unified_config_active and must NOT be the source. If the mapper reads
|
|
34
|
+
// this, the assertion below fails.
|
|
35
|
+
experiment_assignments: [
|
|
36
|
+
{
|
|
37
|
+
layer_name: 'DECOY_org_config',
|
|
38
|
+
namespace: 'ml',
|
|
39
|
+
allocated_experiment_name: 'should_not_be_read',
|
|
40
|
+
group_name: 'X',
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
resolution: { base_version: '1.0.0', base_version_source: 'merchant_layer' },
|
|
44
|
+
},
|
|
45
|
+
widget_config: {
|
|
46
|
+
products_config: {},
|
|
47
|
+
// The correct source — mirrors getWidgetConfig (ungated).
|
|
48
|
+
experiment_assignments: [
|
|
49
|
+
{
|
|
50
|
+
layer_name: 'ml',
|
|
51
|
+
namespace: 'ml',
|
|
52
|
+
allocated_experiment_name: 'exp_a',
|
|
53
|
+
group_name: 'A',
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
resolution: { base_version: '1.0.0', base_version_source: 'merchant_layer' },
|
|
57
|
+
},
|
|
58
|
+
url_resolving: { variant_type: 'pdp', specific_details: {}, ready: true },
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const baseParams: FetchBootstrapConfigParams = {
|
|
64
|
+
baseUrl: 'https://commerce-api.spiffy.ai',
|
|
65
|
+
apiKey: 'test-key',
|
|
66
|
+
userId: 'user-1',
|
|
67
|
+
source: 'app',
|
|
68
|
+
namespace: 'spiffy-react-components',
|
|
69
|
+
gateNames: ['use_unified_config', 'is_client_session_enabled'],
|
|
70
|
+
url: 'https://shop.example.com/products/thing',
|
|
71
|
+
orgId: 'org-1',
|
|
72
|
+
orgShortName: 'test-org',
|
|
73
|
+
contextSource: 'app',
|
|
74
|
+
env: 'prod',
|
|
75
|
+
featureGates: { use_unified_config: true, is_empty_div_plp_enabled: true },
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
beforeEach(() => {
|
|
79
|
+
vi.clearAllMocks();
|
|
80
|
+
window.history.replaceState({}, '', '/');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('fetchBootstrapConfig', () => {
|
|
84
|
+
it('POSTs to /v1/session/bootstrap with the bearer key', async () => {
|
|
85
|
+
makeBootstrapResponse();
|
|
86
|
+
|
|
87
|
+
await fetchBootstrapConfig(baseParams);
|
|
88
|
+
|
|
89
|
+
const [url, options] = mockFetch.mock.calls[0];
|
|
90
|
+
expect(url).toBe('https://commerce-api.spiffy.ai/v1/session/bootstrap');
|
|
91
|
+
expect(options.method).toBe('POST');
|
|
92
|
+
expect(options.headers.Authorization).toBe('Bearer test-key');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('maps org identity, gates (group_name→groupName), assignments, and resolution', async () => {
|
|
96
|
+
makeBootstrapResponse();
|
|
97
|
+
|
|
98
|
+
const { config } = await fetchBootstrapConfig(baseParams);
|
|
99
|
+
|
|
100
|
+
expect(config.org.org).toEqual({
|
|
101
|
+
id: 'org-boot',
|
|
102
|
+
short_name: 'boot-org',
|
|
103
|
+
display_name: 'Boot Org',
|
|
104
|
+
domain: 'boot.com',
|
|
105
|
+
status: 'active',
|
|
106
|
+
created_at: '2024-01-01T00:00:00.000Z',
|
|
107
|
+
updated_at: '2024-02-02T00:00:00.000Z',
|
|
108
|
+
});
|
|
109
|
+
// snake group_name → camel groupName; null → undefined.
|
|
110
|
+
expect(config.gates).toEqual([
|
|
111
|
+
{ name: 'use_unified_config', value: true, groupName: 'Treatment' },
|
|
112
|
+
{ name: 'is_client_session_enabled', value: false, groupName: undefined },
|
|
113
|
+
]);
|
|
114
|
+
// experiment_assignments already snake — pass straight through.
|
|
115
|
+
expect(config.experiment_assignments).toEqual([
|
|
116
|
+
{ layer_name: 'ml', namespace: 'ml', allocated_experiment_name: 'exp_a', group_name: 'A' },
|
|
117
|
+
]);
|
|
118
|
+
// resolution snake → camel, attached to orgConfig.
|
|
119
|
+
expect(config.orgConfig?.resolution).toEqual({
|
|
120
|
+
baseVersion: '1.0.0',
|
|
121
|
+
baseVersionSource: 'merchant_layer',
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('reads experiment_assignments from widget_config, not the server-gated org_config', async () => {
|
|
126
|
+
makeBootstrapResponse();
|
|
127
|
+
|
|
128
|
+
const { config } = await fetchBootstrapConfig(baseParams);
|
|
129
|
+
|
|
130
|
+
// org_config carried a DECOY; the value must come from widget_config so it
|
|
131
|
+
// matches getWidgetConfig (which is ungated by use_unified_config). Reading
|
|
132
|
+
// org_config would drop assignments whenever the gate is off server-side.
|
|
133
|
+
expect(config.experiment_assignments).toEqual([
|
|
134
|
+
{ layer_name: 'ml', namespace: 'ml', allocated_experiment_name: 'exp_a', group_name: 'A' },
|
|
135
|
+
]);
|
|
136
|
+
expect(config.experiment_assignments?.some(a => a.layer_name === 'DECOY_org_config')).toBe(
|
|
137
|
+
false,
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('returns the url_resolving payload verbatim for the warm-up to seed', async () => {
|
|
142
|
+
makeBootstrapResponse();
|
|
143
|
+
|
|
144
|
+
const { urlResolving } = await fetchBootstrapConfig(baseParams);
|
|
145
|
+
|
|
146
|
+
expect(urlResolving).toEqual({ variant_type: 'pdp', specific_details: {}, ready: true });
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('returns urlResolving null when bootstrap omitted the branch', async () => {
|
|
150
|
+
mockFetch.mockResolvedValueOnce({
|
|
151
|
+
ok: true,
|
|
152
|
+
json: () =>
|
|
153
|
+
Promise.resolve({
|
|
154
|
+
org_config: {
|
|
155
|
+
org: { org: { id: 'o' } },
|
|
156
|
+
gates: [{ name: 'use_unified_config', value: true }],
|
|
157
|
+
},
|
|
158
|
+
widget_config: { products_config: {} },
|
|
159
|
+
url_resolving: null,
|
|
160
|
+
}),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const { urlResolving } = await fetchBootstrapConfig(baseParams);
|
|
164
|
+
|
|
165
|
+
expect(urlResolving).toBeNull();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('builds the url_resolving context and version split from params', async () => {
|
|
169
|
+
makeBootstrapResponse();
|
|
170
|
+
|
|
171
|
+
await fetchBootstrapConfig({ ...baseParams, chatId: 'chat-9' });
|
|
172
|
+
|
|
173
|
+
const body = JSON.parse(mockFetch.mock.calls[0][1].body as string);
|
|
174
|
+
expect(body).toMatchObject({
|
|
175
|
+
user_id: 'user-1',
|
|
176
|
+
source: 'app',
|
|
177
|
+
// Scopes org_config.configs — must be sent or bootstrap dumps internal configs.
|
|
178
|
+
namespace: 'spiffy-react-components',
|
|
179
|
+
include_feature_gates: ['use_unified_config', 'is_client_session_enabled'],
|
|
180
|
+
});
|
|
181
|
+
expect(body.url_resolving).toMatchObject({
|
|
182
|
+
url: 'https://shop.example.com/products/thing',
|
|
183
|
+
// Override-aware gates forwarded so client overrides reach page-variant resolution.
|
|
184
|
+
feature_gates: { use_unified_config: true, is_empty_div_plp_enabled: true },
|
|
185
|
+
});
|
|
186
|
+
// No org_base_config_version — the version comes only from the URL query param
|
|
187
|
+
// (override_config_version), never from getWidgetConfig's resolution.
|
|
188
|
+
expect(body.url_resolving.org_base_config_version).toBeUndefined();
|
|
189
|
+
expect(body.url_resolving.context).toEqual({
|
|
190
|
+
user_id: 'user-1',
|
|
191
|
+
org_id: 'org-1',
|
|
192
|
+
org_short_name: 'test-org',
|
|
193
|
+
chat_id: 'chat-9',
|
|
194
|
+
source: 'app',
|
|
195
|
+
env: 'prod',
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('threads a config-version query param into version + override_config_version', async () => {
|
|
200
|
+
window.history.replaceState({}, '', '/?envive_config_version=9.9.9');
|
|
201
|
+
makeBootstrapResponse();
|
|
202
|
+
|
|
203
|
+
await fetchBootstrapConfig(baseParams);
|
|
204
|
+
|
|
205
|
+
const body = JSON.parse(mockFetch.mock.calls[0][1].body as string);
|
|
206
|
+
expect(body.version).toBe('9.9.9');
|
|
207
|
+
expect(body.url_resolving.override_config_version).toBe('9.9.9');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('throws on a non-ok response so the caller can fall back', async () => {
|
|
211
|
+
mockFetch.mockResolvedValueOnce({ ok: false, statusText: 'Bad Gateway' });
|
|
212
|
+
|
|
213
|
+
await expect(fetchBootstrapConfig(baseParams)).rejects.toThrow(
|
|
214
|
+
'Bootstrap config request failed',
|
|
215
|
+
);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Each case isolates ONE missing/degraded required section (the others valid).
|
|
219
|
+
const okGates = [{ name: 'use_unified_config', value: true }];
|
|
220
|
+
const okOrg = { org: { org: { id: 'o' } }, gates: okGates };
|
|
221
|
+
const okWidget = { products_config: {} };
|
|
222
|
+
it.each([
|
|
223
|
+
['missing org_config', { org_config: undefined, widget_config: okWidget }],
|
|
224
|
+
[
|
|
225
|
+
'partial org_config (no org.org)',
|
|
226
|
+
{ org_config: { org: {}, gates: okGates }, widget_config: okWidget },
|
|
227
|
+
],
|
|
228
|
+
['absent gates', { org_config: { org: { org: { id: 'o' } } }, widget_config: okWidget }],
|
|
229
|
+
[
|
|
230
|
+
'empty gates',
|
|
231
|
+
{ org_config: { org: { org: { id: 'o' } }, gates: [] }, widget_config: okWidget },
|
|
232
|
+
],
|
|
233
|
+
['missing widget_config', { org_config: okOrg, widget_config: null }],
|
|
234
|
+
])(
|
|
235
|
+
'throws on a 2xx with %s so the caller can fall back to getWidgetConfig',
|
|
236
|
+
async (_label, payload) => {
|
|
237
|
+
mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(payload) });
|
|
238
|
+
|
|
239
|
+
await expect(fetchBootstrapConfig(baseParams)).rejects.toThrow('missing required sections');
|
|
240
|
+
},
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
it('passes an AbortSignal to fetch (timeout wiring)', async () => {
|
|
244
|
+
makeBootstrapResponse();
|
|
245
|
+
|
|
246
|
+
await fetchBootstrapConfig(baseParams);
|
|
247
|
+
|
|
248
|
+
const [, options] = mockFetch.mock.calls[0];
|
|
249
|
+
expect(options.signal).toBeInstanceOf(AbortSignal);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('aborts and rejects when the request exceeds the timeout so the caller can fall back', async () => {
|
|
253
|
+
vi.useFakeTimers();
|
|
254
|
+
// Mimic real fetch: reject with an AbortError once its signal aborts.
|
|
255
|
+
mockFetch.mockImplementationOnce(
|
|
256
|
+
(_url: string, opts: { signal: AbortSignal }) =>
|
|
257
|
+
new Promise((_resolve, reject) => {
|
|
258
|
+
opts.signal.addEventListener('abort', () =>
|
|
259
|
+
reject(new DOMException('The operation was aborted', 'AbortError')),
|
|
260
|
+
);
|
|
261
|
+
}),
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const promise = fetchBootstrapConfig(baseParams);
|
|
265
|
+
const rejection = expect(promise).rejects.toThrow();
|
|
266
|
+
await vi.advanceTimersByTimeAsync(5000);
|
|
267
|
+
await rejection;
|
|
268
|
+
|
|
269
|
+
vi.useRealTimers();
|
|
270
|
+
});
|
|
271
|
+
});
|
|
@@ -2,7 +2,15 @@ import { OrgConfigExperimentAssignment, OrgConfigFeatureGate } from 'src/applica
|
|
|
2
2
|
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
|
+
import { FeatureFlagService } from 'src/services/featureFlagService';
|
|
6
|
+
import Logger from 'src/application/logging/logger';
|
|
7
|
+
import type { UrlResolverResponse } from 'src/atoms/app/variant';
|
|
8
|
+
import { getAtomStore } from 'src/atoms/atomStore/atomStore';
|
|
9
|
+
import { chatIdAtom } from 'src/atoms/app';
|
|
5
10
|
import { fetchUnifiedGraphQLConfig } from './fetchGraphQLConfig';
|
|
11
|
+
import { fetchBootstrapConfig } from './fetchBootstrapConfig';
|
|
12
|
+
|
|
13
|
+
const logger = new Logger('enviveConfigService');
|
|
6
14
|
|
|
7
15
|
// Short-lived so config changes (gates, experiments) still propagate within
|
|
8
16
|
// a session, while sparing every page navigation from re-fetching org
|
|
@@ -21,6 +29,12 @@ type EnviveConfigServiceProps = {
|
|
|
21
29
|
userId: string;
|
|
22
30
|
namespace: string;
|
|
23
31
|
source: string;
|
|
32
|
+
// The url_resolving context bits the hooks layer can't derive itself, supplied
|
|
33
|
+
// by the injection bundle so getEnviveConfig can call /v1/session/bootstrap on
|
|
34
|
+
// the use_unified_config gate-ON path. Optional: when absent (mocks, non-bundle
|
|
35
|
+
// callers) the bootstrap branch is skipped and getWidgetConfig stands.
|
|
36
|
+
env?: string;
|
|
37
|
+
contextSource?: string;
|
|
24
38
|
};
|
|
25
39
|
|
|
26
40
|
export type EnviveServiceConfig = {
|
|
@@ -36,9 +50,10 @@ export type EnviveServiceConfig = {
|
|
|
36
50
|
};
|
|
37
51
|
};
|
|
38
52
|
gates: OrgConfigFeatureGate[];
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
53
|
+
// The user's Statsig experiment-layer assignments, from the widget config's
|
|
54
|
+
// resolved experiments — getWidgetConfig, or bootstrap's `widget_config`
|
|
55
|
+
// (both ungated by use_unified_config; empty only under a version pin).
|
|
56
|
+
// amplitudeService reads these to enrich events with `experiment.{ns}.*` props.
|
|
42
57
|
experiment_assignments?: OrgConfigExperimentAssignment[];
|
|
43
58
|
// orgConfig.resolution is populated only on the unified path (sourced from
|
|
44
59
|
// getWidgetConfig.resolution, not /v1/org/config.resolution) so the version
|
|
@@ -51,8 +66,18 @@ export interface IEnviveConfigService {
|
|
|
51
66
|
readonly baseUrl: string;
|
|
52
67
|
readonly apiKey: string;
|
|
53
68
|
getEnviveConfig(): Promise<EnviveServiceConfig>;
|
|
69
|
+
// NB: `takeBootstrapUrlResolving` is intentionally NOT on this interface. It's
|
|
70
|
+
// a bootstrap-only detail consumed solely by the injection bundle via the
|
|
71
|
+
// concrete EnviveConfigService; keeping it off the public DI contract avoids a
|
|
72
|
+
// breaking change for external consumers that supply their own config service
|
|
73
|
+
// (EnviveProvider only ever calls getEnviveConfig / baseUrl / apiKey).
|
|
54
74
|
}
|
|
55
75
|
|
|
76
|
+
// The bootstrap url_resolving payload plus the URL it was resolved for, so the
|
|
77
|
+
// warm-up can verify the URL still matches before seeding (see F1: a SPA nav
|
|
78
|
+
// during the bootstrap round trip must not cache URL A's result under URL B).
|
|
79
|
+
type BootstrapUrlResolving = { url: string; response: UrlResolverResponse };
|
|
80
|
+
|
|
56
81
|
export class EnviveConfigService implements IEnviveConfigService {
|
|
57
82
|
public readonly baseUrl: string;
|
|
58
83
|
|
|
@@ -64,14 +89,40 @@ export class EnviveConfigService implements IEnviveConfigService {
|
|
|
64
89
|
|
|
65
90
|
private readonly source: string;
|
|
66
91
|
|
|
92
|
+
private readonly env?: string;
|
|
93
|
+
|
|
94
|
+
private readonly contextSource?: string;
|
|
95
|
+
|
|
67
96
|
private response: EnviveServiceConfig | null = null;
|
|
68
97
|
|
|
69
|
-
|
|
98
|
+
private bootstrapUrlResolving: BootstrapUrlResolving | null = null;
|
|
99
|
+
|
|
100
|
+
constructor({
|
|
101
|
+
baseUrl,
|
|
102
|
+
apiKey,
|
|
103
|
+
userId,
|
|
104
|
+
namespace,
|
|
105
|
+
source,
|
|
106
|
+
env,
|
|
107
|
+
contextSource,
|
|
108
|
+
}: EnviveConfigServiceProps) {
|
|
70
109
|
this.baseUrl = baseUrl;
|
|
71
110
|
this.apiKey = apiKey;
|
|
72
111
|
this.userId = userId;
|
|
73
112
|
this.namespace = namespace;
|
|
74
113
|
this.source = source;
|
|
114
|
+
this.env = env;
|
|
115
|
+
this.contextSource = contextSource;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Hands off (once) the url_resolving payload that /v1/session/bootstrap
|
|
119
|
+
// returned alongside the config — paired with the URL it was resolved for — so
|
|
120
|
+
// the warm-up seeds urlResolverAtom with it instead of firing a separate
|
|
121
|
+
// /v1/url_resolving. null on the getWidgetConfig / cached path.
|
|
122
|
+
takeBootstrapUrlResolving(): BootstrapUrlResolving | null {
|
|
123
|
+
const value = this.bootstrapUrlResolving;
|
|
124
|
+
this.bootstrapUrlResolving = null;
|
|
125
|
+
return value;
|
|
75
126
|
}
|
|
76
127
|
|
|
77
128
|
private getCacheKey(): string {
|
|
@@ -132,16 +183,84 @@ export class EnviveConfigService implements IEnviveConfigService {
|
|
|
132
183
|
// list of gates the app checks, so the resolver evaluates and returns them.
|
|
133
184
|
const gateNames = Object.values(FeatureGates).map(featureGate => featureGate.toString());
|
|
134
185
|
|
|
135
|
-
|
|
186
|
+
const widgetConfig = await fetchUnifiedGraphQLConfig(
|
|
136
187
|
this.baseUrl,
|
|
137
188
|
this.apiKey,
|
|
138
189
|
this.userId,
|
|
139
190
|
gateNames,
|
|
140
191
|
this.source,
|
|
141
192
|
);
|
|
193
|
+
|
|
194
|
+
this.response = await this.maybeUpgradeToBootstrap(widgetConfig, gateNames);
|
|
142
195
|
this.writeCachedConfig(this.response);
|
|
143
196
|
return this.response;
|
|
144
197
|
}
|
|
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
|
+
}
|
|
145
264
|
}
|
|
146
265
|
|
|
147
266
|
/**
|