@base44-preview/sdk 0.8.48-pr.280.2c0d8bc → 0.8.48-pr.280.3801fa1

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.
@@ -1,5 +1,7 @@
1
1
  import { getSharedInstance } from "../utils/sharedInstance.js";
2
2
  import { generateUuid, isReactNative } from "../utils/common.js";
3
+ import { getExperimentsRuntime } from "./experiments-runtime.types.js";
4
+ import { getAnalyticsQueue } from "./analytics-queue.js";
3
5
  export const USER_HEARTBEAT_EVENT_NAME = "__user_heartbeat_event__";
4
6
  export const ANALYTICS_INITIALIZATION_EVENT_NAME = "__initialization_event__";
5
7
  export const ANALYTICS_SESSION_DURATION_EVENT_NAME = "__session_duration_event__";
@@ -13,111 +15,85 @@ const defaultConfiguration = {
13
15
  batchSize: 30,
14
16
  heartBeatInterval: 60 * 1000,
15
17
  };
16
- ///////////////////////////////////////////////
17
- //// shared queue for analytics events ////
18
- ///////////////////////////////////////////////
19
18
  const ANALYTICS_SHARED_STATE_NAME = "analytics";
20
- // shared state//
21
- const analyticsSharedState = getSharedInstance(ANALYTICS_SHARED_STATE_NAME, () => ({
22
- requestsQueue: [],
23
- isProcessing: false,
24
- isHeartBeatProcessing: false,
25
- wasInitializationTracked: false,
26
- sessionContext: null,
27
- sessionStartTime: null,
28
- // Memoized session id for when `localStorage` can't persist one — see
29
- // getAnalyticsSessionId.
30
- fallbackSessionId: null,
31
- config: {
32
- ...defaultConfiguration,
33
- ...getAnalyticsConfigFromUrlParams(),
34
- },
35
- }));
19
+ function createAnalyticsState() {
20
+ return {
21
+ isHeartBeatProcessing: false,
22
+ wasInitializationTracked: false,
23
+ sessionContext: null,
24
+ sessionContextPromise: null,
25
+ sessionStartTime: null,
26
+ // Memoized session id for when `localStorage` can't persist one — see
27
+ // getAnalyticsSessionId.
28
+ fallbackSessionId: null,
29
+ config: {
30
+ ...defaultConfiguration,
31
+ ...getAnalyticsConfigFromUrlParams(),
32
+ },
33
+ };
34
+ }
35
+ const analyticsSharedState = getSharedInstance(ANALYTICS_SHARED_STATE_NAME, createAnalyticsState);
36
+ const clientAnalyticsStates = new WeakMap();
36
37
  /** @internal */
37
- export function isAnalyticsEnabled(enabled) {
38
- return enabled && analyticsSharedState.config.enabled && !isReactNative;
38
+ export function getAnalyticsState(axiosClient) {
39
+ let state = clientAnalyticsStates.get(axiosClient);
40
+ if (!state) {
41
+ state = createAnalyticsState();
42
+ if (typeof window !== "undefined") {
43
+ state.config = analyticsSharedState.config;
44
+ }
45
+ clientAnalyticsStates.set(axiosClient, state);
46
+ }
47
+ return state;
39
48
  }
40
- export const createAnalyticsModule = ({ axiosClient, serverUrl, appId, userAuthModule, enabled, }) => {
41
- // prevent overflow of events //
42
- const { maxQueueSize, throttleTime, batchSize } = analyticsSharedState.config;
49
+ /** @internal */
50
+ export function isAnalyticsEnabled(enabled, state = analyticsSharedState) {
51
+ return enabled && state.config.enabled && !isReactNative;
52
+ }
53
+ export const createAnalyticsModule = ({ axiosClient, appId, userAuthModule, enabled, getVisitorId, experimentsContext, }) => {
54
+ const state = getAnalyticsState(axiosClient);
55
+ const automaticState = typeof window === "undefined" ? state : analyticsSharedState;
56
+ const queue = getAnalyticsQueue(axiosClient, appId, state.config);
43
57
  // Disable analytics on React Native. It defines `window` but not `document`,
44
58
  // so the per-callsite `typeof window` guards below aren't enough to keep it
45
59
  // from touching `document` (e.g. `document.referrer` on init). Node/SSR is
46
60
  // still handled by those `window` guards, so this doesn't affect it.
47
- if (!isAnalyticsEnabled(enabled)) {
61
+ if (!isAnalyticsEnabled(enabled, state)) {
48
62
  return {
49
63
  track: () => { },
50
64
  cleanup: () => { },
51
65
  };
52
66
  }
53
67
  let clearHeartBeatProcessor = undefined;
54
- const trackBatchUrl = `${serverUrl}/api/apps/${appId}/analytics/track/batch`;
55
- const batchRequestFallback = async (events) => {
56
- await axiosClient.request({
57
- method: "POST",
58
- url: `/apps/${appId}/analytics/track/batch`,
59
- data: { events },
60
- });
61
- };
62
- // currently disabled, until fully tested //
63
- const beaconRequest = (events) => {
64
- try {
65
- const beaconPayload = JSON.stringify({ events });
66
- const blob = new Blob([beaconPayload], { type: "application/json" });
67
- return (typeof navigator === "undefined" ||
68
- beaconPayload.length > 60000 ||
69
- !navigator.sendBeacon(trackBatchUrl, blob));
70
- }
71
- catch (_a) {
72
- return false;
73
- }
74
- };
75
- const flush = async (eventsData, options = {}) => {
76
- if (eventsData.length === 0)
77
- return;
78
- const sessionContext_ = await getSessionContext(userAuthModule);
79
- const events = eventsData.map(transformEventDataToApiRequestData(sessionContext_));
80
- try {
81
- if (!options.isBeacon || !beaconRequest(events)) {
82
- await batchRequestFallback(events);
83
- }
84
- }
85
- catch (_a) {
86
- // do nothing
87
- }
88
- };
89
- const startProcessing = () => {
90
- startAnalyticsProcessor(flush, {
91
- throttleTime,
92
- batchSize,
93
- });
94
- };
95
68
  const track = (params) => {
96
- if (analyticsSharedState.requestsQueue.length >= maxQueueSize) {
97
- return;
98
- }
69
+ var _a, _b;
99
70
  const intrinsicData = getEventIntrinsicData();
100
- analyticsSharedState.requestsQueue.push({
101
- ...params,
102
- ...intrinsicData,
103
- });
104
- startProcessing();
71
+ const visitorId = (_a = getVisitorId === null || getVisitorId === void 0 ? void 0 : getVisitorId()) !== null && _a !== void 0 ? _a : getAnalyticsSessionId(state);
72
+ const authorization = userAuthModule.hasToken() ? axiosClient.defaults.headers.common.Authorization : null;
73
+ const context = getSessionContext(userAuthModule, state);
74
+ const preview = Object.fromEntries(Object.entries((_b = experimentsContext === null || experimentsContext === void 0 ? void 0 : experimentsContext.preview) !== null && _b !== void 0 ? _b : {}).filter(([, value]) => typeof value === "boolean"));
75
+ const properties = { ...params.properties };
76
+ delete properties.__b44_experiment_preview;
77
+ if (Object.keys(preview).length) {
78
+ // Capture now: a queued event must retain its occurrence-time preview.
79
+ properties.__b44_experiment_preview = JSON.stringify(preview);
80
+ }
81
+ const event = {
82
+ event_name: params.eventName,
83
+ timestamp: intrinsicData.timestamp,
84
+ page_url: intrinsicData.pageUrl,
85
+ properties: params.properties || Object.keys(properties).length ? properties : undefined,
86
+ };
87
+ queue.enqueue(context.then((identity) => ({ ...event, ...identity, session_id: visitorId })), typeof authorization === "string" ? authorization : null, context.then((identity) => { var _a; return (_a = identity.user_id) !== null && _a !== void 0 ? _a : null; }));
105
88
  };
106
89
  const onDocVisible = () => {
107
- startAnalyticsProcessor(flush, {
108
- throttleTime,
109
- batchSize,
110
- });
111
- clearHeartBeatProcessor = startHeartBeatProcessor(track);
112
- setSessionDurationTimerStart();
90
+ clearHeartBeatProcessor = startHeartBeatProcessor(track, automaticState);
91
+ setSessionDurationTimerStart(automaticState);
113
92
  };
114
93
  const onDocHidden = () => {
115
- stopAnalyticsProcessor();
116
94
  clearHeartBeatProcessor === null || clearHeartBeatProcessor === void 0 ? void 0 : clearHeartBeatProcessor();
117
- trackSessionDurationEvent(track);
118
- // flush entire queue on visibility change and hope for the best //
119
- const eventsData = analyticsSharedState.requestsQueue.splice(0);
120
- flush(eventsData, { isBeacon: true });
95
+ trackSessionDurationEvent(track, automaticState);
96
+ void queue.flush();
121
97
  };
122
98
  const onVisibilityChange = () => {
123
99
  if (typeof window === "undefined")
@@ -130,18 +106,16 @@ export const createAnalyticsModule = ({ axiosClient, serverUrl, appId, userAuthM
130
106
  }
131
107
  };
132
108
  const cleanup = () => {
133
- stopAnalyticsProcessor();
109
+ queue.cleanup();
134
110
  clearHeartBeatProcessor === null || clearHeartBeatProcessor === void 0 ? void 0 : clearHeartBeatProcessor();
135
111
  if (typeof window !== "undefined") {
136
112
  window.removeEventListener("visibilitychange", onVisibilityChange);
137
113
  }
138
114
  };
139
- // start the flusing process ///
140
- startProcessing();
141
115
  // start the heart beat processor //
142
- clearHeartBeatProcessor = startHeartBeatProcessor(track);
116
+ clearHeartBeatProcessor = startHeartBeatProcessor(track, automaticState);
143
117
  // track the referrer event //
144
- trackInitializationEvent(track);
118
+ trackInitializationEvent(track, automaticState);
145
119
  // start the visibility change listener //
146
120
  if (typeof window !== "undefined") {
147
121
  window.addEventListener("visibilitychange", onVisibilityChange);
@@ -151,50 +125,32 @@ export const createAnalyticsModule = ({ axiosClient, serverUrl, appId, userAuthM
151
125
  cleanup,
152
126
  };
153
127
  };
154
- function stopAnalyticsProcessor() {
155
- analyticsSharedState.isProcessing = false;
156
- }
157
- async function startAnalyticsProcessor(handleTrack, options) {
158
- if (analyticsSharedState.isProcessing) {
159
- // only one instance of the analytics processor can be running at a time //
160
- return;
161
- }
162
- analyticsSharedState.isProcessing = true;
163
- const { throttleTime = 1000, batchSize = 30 } = options !== null && options !== void 0 ? options : {};
164
- while (analyticsSharedState.isProcessing &&
165
- analyticsSharedState.requestsQueue.length > 0) {
166
- const requests = analyticsSharedState.requestsQueue.splice(0, batchSize);
167
- requests.length && (await handleTrack(requests));
168
- await new Promise((resolve) => setTimeout(resolve, throttleTime));
169
- }
170
- analyticsSharedState.isProcessing = false;
171
- }
172
- function startHeartBeatProcessor(track) {
128
+ function startHeartBeatProcessor(track, state) {
173
129
  var _a;
174
130
  // Browser-only, like the other automatic events here (initialization, session
175
131
  // duration, visibility). Outside a browser this timer fired a `me()` every
176
132
  // interval for the lifetime of a long-lived server-side client, and kept the
177
133
  // Node event loop alive. Explicit `analytics.track()` calls still work.
178
134
  if (typeof window === "undefined" ||
179
- analyticsSharedState.isHeartBeatProcessing ||
180
- ((_a = analyticsSharedState.config.heartBeatInterval) !== null && _a !== void 0 ? _a : 0) < 10) {
135
+ state.isHeartBeatProcessing ||
136
+ ((_a = state.config.heartBeatInterval) !== null && _a !== void 0 ? _a : 0) < 10) {
181
137
  return () => { };
182
138
  }
183
- analyticsSharedState.isHeartBeatProcessing = true;
139
+ state.isHeartBeatProcessing = true;
184
140
  const interval = setInterval(() => {
185
141
  track({ eventName: USER_HEARTBEAT_EVENT_NAME });
186
- }, analyticsSharedState.config.heartBeatInterval);
142
+ }, state.config.heartBeatInterval);
187
143
  return () => {
188
144
  clearInterval(interval);
189
- analyticsSharedState.isHeartBeatProcessing = false;
145
+ state.isHeartBeatProcessing = false;
190
146
  };
191
147
  }
192
- function trackInitializationEvent(track) {
148
+ function trackInitializationEvent(track, state) {
193
149
  if (typeof window === "undefined" ||
194
- analyticsSharedState.wasInitializationTracked) {
150
+ state.wasInitializationTracked) {
195
151
  return;
196
152
  }
197
- analyticsSharedState.wasInitializationTracked = true;
153
+ state.wasInitializationTracked = true;
198
154
  track({
199
155
  eventName: ANALYTICS_INITIALIZATION_EVENT_NAME,
200
156
  properties: {
@@ -202,20 +158,20 @@ function trackInitializationEvent(track) {
202
158
  },
203
159
  });
204
160
  }
205
- function setSessionDurationTimerStart() {
161
+ function setSessionDurationTimerStart(state) {
206
162
  if (typeof window === "undefined" ||
207
- analyticsSharedState.sessionStartTime !== null) {
163
+ state.sessionStartTime !== null) {
208
164
  return;
209
165
  }
210
- analyticsSharedState.sessionStartTime = new Date().toISOString();
166
+ state.sessionStartTime = new Date().toISOString();
211
167
  }
212
- function trackSessionDurationEvent(track) {
168
+ function trackSessionDurationEvent(track, state) {
213
169
  if (typeof window === "undefined" ||
214
- analyticsSharedState.sessionStartTime === null)
170
+ state.sessionStartTime === null)
215
171
  return;
216
172
  const sessionDuration = new Date().getTime() -
217
- new Date(analyticsSharedState.sessionStartTime).getTime();
218
- analyticsSharedState.sessionStartTime = null;
173
+ new Date(state.sessionStartTime).getTime();
174
+ state.sessionStartTime = null;
219
175
  track({
220
176
  eventName: ANALYTICS_SESSION_DURATION_EVENT_NAME,
221
177
  properties: { sessionDuration },
@@ -229,16 +185,6 @@ function getEventIntrinsicData() {
229
185
  pageUrl: typeof window !== "undefined" ? (_b = (_a = window.location) === null || _a === void 0 ? void 0 : _a.pathname) !== null && _b !== void 0 ? _b : null : null,
230
186
  };
231
187
  }
232
- function transformEventDataToApiRequestData(sessionContext) {
233
- return (eventData) => ({
234
- event_name: eventData.eventName,
235
- properties: eventData.properties,
236
- timestamp: eventData.timestamp,
237
- page_url: eventData.pageUrl,
238
- ...sessionContext,
239
- });
240
- }
241
- let sessionContextPromise = null;
242
188
  /**
243
189
  * Clears the memoized analytics session context.
244
190
  *
@@ -249,22 +195,23 @@ let sessionContextPromise = null;
249
195
  *
250
196
  * @internal
251
197
  */
252
- export function resetAnalyticsSessionContext() {
253
- analyticsSharedState.sessionContext = null;
254
- sessionContextPromise = null;
198
+ export function resetAnalyticsSessionContext(axiosClient) {
199
+ const state = axiosClient ? getAnalyticsState(axiosClient) : analyticsSharedState;
200
+ state.sessionContext = null;
201
+ state.sessionContextPromise = null;
255
202
  }
256
- async function getSessionContext(userAuthModule) {
257
- if (!analyticsSharedState.sessionContext) {
203
+ async function getSessionContext(userAuthModule, state) {
204
+ if (!state.sessionContext) {
258
205
  // With no token there is no identity to resolve: `me()` can only answer 401,
259
206
  // which the browser logs to the console before any handler here sees it. On
260
207
  // a public page that request is the sole reason an error appears, so skip
261
208
  // it. This is not memoized — a visitor who logs in later must still resolve.
262
209
  if (!userAuthModule.hasToken()) {
263
- return { user_id: null, session_id: getAnalyticsSessionId() };
210
+ return { user_id: null, session_id: getAnalyticsSessionId(state) };
264
211
  }
265
- if (!sessionContextPromise) {
266
- const sessionId = getAnalyticsSessionId();
267
- sessionContextPromise = userAuthModule
212
+ if (!state.sessionContextPromise) {
213
+ const sessionId = getAnalyticsSessionId(state);
214
+ state.sessionContextPromise = userAuthModule
268
215
  .me()
269
216
  .then((user) => ({
270
217
  user_id: user.id,
@@ -275,7 +222,7 @@ async function getSessionContext(userAuthModule) {
275
222
  session_id: sessionId,
276
223
  }));
277
224
  }
278
- const pending = sessionContextPromise;
225
+ const pending = state.sessionContextPromise;
279
226
  const context = await pending;
280
227
  // Publish only if this lookup is still the current one. A reset that lands
281
228
  // while the request is in flight nulls `sessionContextPromise`, and an
@@ -283,12 +230,12 @@ async function getSessionContext(userAuthModule) {
283
230
  // for the rest of the session. The awaited value is still returned: these
284
231
  // events were queued before the identity changed, so that is who they
285
232
  // belong to.
286
- if (sessionContextPromise === pending) {
287
- analyticsSharedState.sessionContext = context;
233
+ if (state.sessionContextPromise === pending) {
234
+ state.sessionContext = context;
288
235
  }
289
236
  return context;
290
237
  }
291
- return analyticsSharedState.sessionContext;
238
+ return state.sessionContext;
292
239
  }
293
240
  export function getAnalyticsConfigFromUrlParams() {
294
241
  // `window.location` is absent on React Native. This runs at module load (via
@@ -310,15 +257,18 @@ export function getAnalyticsConfigFromUrlParams() {
310
257
  // return the config object //
311
258
  return { enabled: analyticsEnable === "true" };
312
259
  }
313
- // When the id can't be persisted (React Native has no `localStorage`), keep
314
- // it stable for the process instead of minting a fresh one per call.
315
- function getFallbackSessionId() {
260
+ // Without persistent storage, keep the id stable within this analytics state.
261
+ function getFallbackSessionId(state) {
316
262
  var _a;
317
- return ((_a = analyticsSharedState.fallbackSessionId) !== null && _a !== void 0 ? _a : (analyticsSharedState.fallbackSessionId = generateUuid()));
263
+ return ((_a = state.fallbackSessionId) !== null && _a !== void 0 ? _a : (state.fallbackSessionId = generateUuid()));
318
264
  }
319
- export function getAnalyticsSessionId() {
265
+ export function getAnalyticsSessionId(state = analyticsSharedState) {
266
+ var _a;
267
+ const visitorId = (_a = getExperimentsRuntime()) === null || _a === void 0 ? void 0 : _a.visitorId;
268
+ if (visitorId && visitorId !== "anon")
269
+ return visitorId;
320
270
  if (typeof window === "undefined") {
321
- return getFallbackSessionId();
271
+ return getFallbackSessionId(state);
322
272
  }
323
273
  try {
324
274
  const sessionId = localStorage.getItem(ANALYTICS_SESSION_ID_LOCAL_STORAGE_KEY);
@@ -329,7 +279,7 @@ export function getAnalyticsSessionId() {
329
279
  }
330
280
  return sessionId;
331
281
  }
332
- catch (_a) {
333
- return getFallbackSessionId();
282
+ catch (_b) {
283
+ return getFallbackSessionId(state);
334
284
  }
335
285
  }
@@ -177,7 +177,7 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
177
177
  // Drop identity resolved under the previous session: a `me()` already in
178
178
  // flight would otherwise resolve into callers that run after the logout.
179
179
  clearPendingMe();
180
- resetAnalyticsSessionContext();
180
+ resetAnalyticsSessionContext(axios);
181
181
  hasAccessToken = false;
182
182
  notifyAuthState({ status: "anonymous" });
183
183
  // Only do the rest if in a browser environment
@@ -207,7 +207,7 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
207
207
  // Same reasoning as in `logout`: the identity changes here, so anything
208
208
  // resolved for the previous one must not be handed to later callers.
209
209
  clearPendingMe();
210
- resetAnalyticsSessionContext();
210
+ resetAnalyticsSessionContext(axios);
211
211
  hasAccessToken = true;
212
212
  // handle token change for axios clients
213
213
  axios.defaults.headers.common["Authorization"] = `Bearer ${token}`;
@@ -239,6 +239,8 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
239
239
  const { access_token, user } = response;
240
240
  if (access_token) {
241
241
  this.setToken(access_token);
242
+ if (typeof (user === null || user === void 0 ? void 0 : user.id) === "string")
243
+ notifyAuthState({ status: "authenticated", userId: user.id });
242
244
  }
243
245
  return {
244
246
  access_token,
@@ -1,9 +1,11 @@
1
1
  import type { AxiosInstance } from "axios";
2
2
  /** @internal */
3
- export declare function createExposureTracker({ axiosClient, appId, enabled, }: {
3
+ export declare function createExposureTracker({ axiosClient, appId, enabled, source, pageUrl, }: {
4
4
  axiosClient: AxiosInstance;
5
5
  appId: string;
6
6
  enabled: boolean;
7
+ source?: "browser" | "backend";
8
+ pageUrl?: string;
7
9
  }): {
8
10
  track(assignment: {
9
11
  experiment_id: string;
@@ -13,4 +15,5 @@ export declare function createExposureTracker({ axiosClient, appId, enabled, }:
13
15
  visitorId: string;
14
16
  userId: string | null;
15
17
  }): void;
18
+ flush: () => Promise<void>;
16
19
  };
@@ -1,45 +1,30 @@
1
- import { isAnalyticsEnabled } from "./analytics.js";
1
+ import { v4 as uuid } from "uuid";
2
+ import { getAnalyticsState, isAnalyticsEnabled } from "./analytics.js";
3
+ import { getAnalyticsQueue } from "./analytics-queue.js";
2
4
  /** @internal */
3
- export function createExposureTracker({ axiosClient, appId, enabled, }) {
4
- const exposed = new Set();
5
+ export function createExposureTracker({ axiosClient, appId, enabled, source = "browser", pageUrl, }) {
6
+ const state = getAnalyticsState(axiosClient);
7
+ const queue = getAnalyticsQueue(axiosClient, appId, state.config);
8
+ const tracked = new Set();
5
9
  return {
6
10
  track(assignment, identity) {
7
- var _a;
8
- if (typeof window === "undefined" || !isAnalyticsEnabled(enabled))
11
+ if ((source === "browser" && typeof window === "undefined") || !isAnalyticsEnabled(enabled, state))
9
12
  return;
10
13
  const { experiment_id, run_version, variant_key } = assignment;
11
- const key = JSON.stringify([
12
- experiment_id,
13
- run_version,
14
- variant_key,
15
- identity.userId,
16
- identity.visitorId,
17
- ]);
18
- if (exposed.has(key))
14
+ const key = JSON.stringify([experiment_id, run_version, variant_key, identity.userId, identity.visitorId]);
15
+ if (tracked.has(key))
19
16
  return;
20
- exposed.add(key);
21
- // Pin the event's auth before an account change can alter Axios defaults.
22
- const authorization = identity.userId
23
- ? ((_a = axiosClient.defaults.headers.common.Authorization) !== null && _a !== void 0 ? _a : null)
24
- : null;
25
- void axiosClient
26
- .request({
27
- method: "POST",
28
- url: `/apps/${appId}/analytics/track/batch`,
29
- headers: { Authorization: authorization },
30
- data: {
31
- events: [
32
- {
33
- event_name: "__experiment_exposure__",
34
- timestamp: new Date().toISOString(),
35
- session_id: identity.visitorId,
36
- page_url: window.location.pathname,
37
- properties: { experiment_id, run_version, variant_key },
38
- },
39
- ],
40
- },
41
- })
42
- .catch(() => exposed.delete(key));
17
+ tracked.add(key);
18
+ const authorization = identity.userId ? axiosClient.defaults.headers.common.Authorization : null;
19
+ queue.enqueue({
20
+ event_id: uuid(),
21
+ event_name: "__experiment_exposure__",
22
+ timestamp: new Date().toISOString(),
23
+ session_id: identity.visitorId,
24
+ page_url: pageUrl !== null && pageUrl !== void 0 ? pageUrl : (typeof window === "undefined" ? "/" : window.location.pathname),
25
+ properties: { experiment_id, run_version, variant_key, source },
26
+ }, typeof authorization === "string" ? authorization : null, identity.userId);
43
27
  },
28
+ flush: queue.flush,
44
29
  };
45
30
  }
@@ -0,0 +1,39 @@
1
+ import type { ExperimentsSnapshot } from "./experiments.types.js";
2
+ /** Shared versioned configuration published by the platform, never visitor-specific. */
3
+ export interface ExperimentsConfig {
4
+ v: 1;
5
+ app_id: string;
6
+ revision?: number;
7
+ flags: {
8
+ key: string;
9
+ rollout_percentage: number;
10
+ }[];
11
+ experiments: {
12
+ id: string;
13
+ flag_key: string;
14
+ run_version: number;
15
+ assign_by: "visitor" | "user";
16
+ traffic_allocation: number;
17
+ variants: {
18
+ key: string;
19
+ value: boolean;
20
+ weight: number;
21
+ }[];
22
+ }[];
23
+ }
24
+ /** Identity supplied by the platform's normal authenticated request/bootstrap path. */
25
+ export interface ExperimentsIdentity {
26
+ visitorId: string;
27
+ userId: string | null;
28
+ status?: "authenticated" | "anonymous" | "pending";
29
+ }
30
+ /** One request's or browser page's context. Never share it between server requests. */
31
+ export interface ExperimentsContext {
32
+ config: ExperimentsConfig;
33
+ identity: ExperimentsIdentity;
34
+ preview?: Readonly<Record<string, boolean>>;
35
+ /** Request pathname used for server-side exposure events. */
36
+ pageUrl?: string;
37
+ /** Exact server-rendered flags, retained for the browser's first hydration render. */
38
+ serverSnapshot?: ExperimentsSnapshot;
39
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ import type { ExperimentsContext } from "./experiments-config.types.js";
2
+ import type { ExperimentsRuntime } from "./experiments-runtime.types.js";
3
+ /** @internal Platform ingress overwrites this header; it is not authentication. */
4
+ export declare const EXPERIMENTS_CONTEXT_HEADER = "Base44-Experiments-Context";
5
+ /** @internal */
6
+ export declare function readExperimentsContext(encoded: string | null, appId: string): ExperimentsContext | undefined;
7
+ /** @internal */
8
+ export declare function getBrowserExperimentsContext(appId: string): ExperimentsContext | undefined;
9
+ /** One independent evaluator instance for one client/request. @internal */
10
+ export declare function createExperimentsRuntime(context: ExperimentsContext): ExperimentsRuntime;
@@ -0,0 +1,44 @@
1
+ import { evaluateExperiments } from "./experiments-evaluator.js";
2
+ /** @internal Platform ingress overwrites this header; it is not authentication. */
3
+ export const EXPERIMENTS_CONTEXT_HEADER = "Base44-Experiments-Context";
4
+ /** @internal */
5
+ export function readExperimentsContext(encoded, appId) {
6
+ if (!encoded || encoded.length > 96 * 1024)
7
+ return;
8
+ try {
9
+ const bytes = Uint8Array.from(atob(encoded.replace(/-/g, "+").replace(/_/g, "/")), (character) => character.charCodeAt(0));
10
+ return matchingContext(JSON.parse(new TextDecoder().decode(bytes)), appId);
11
+ }
12
+ catch (_a) {
13
+ return;
14
+ }
15
+ }
16
+ function matchingContext(value, appId) {
17
+ var _a, _b;
18
+ return ((_a = value === null || value === void 0 ? void 0 : value.config) === null || _a === void 0 ? void 0 : _a.v) === 1 && value.config.app_id === appId &&
19
+ typeof ((_b = value.identity) === null || _b === void 0 ? void 0 : _b.visitorId) === "string" && value.identity.visitorId &&
20
+ (value.identity.userId === null || typeof value.identity.userId === "string")
21
+ ? value : undefined;
22
+ }
23
+ /** @internal */
24
+ export function getBrowserExperimentsContext(appId) {
25
+ if (typeof window === "undefined" || typeof document === "undefined")
26
+ return;
27
+ return matchingContext(window.__B44_EXPERIMENTS_BOOTSTRAP__, appId);
28
+ }
29
+ /** One independent evaluator instance for one client/request. @internal */
30
+ export function createExperimentsRuntime(context) {
31
+ const identity = { ...context.identity };
32
+ const evaluate = () => evaluateExperiments(context.config, identity, context.preview);
33
+ const runtime = {
34
+ ...evaluate(),
35
+ visitorId: identity.visitorId,
36
+ userId: identity.userId,
37
+ pendingUser: identity.status === "pending",
38
+ setUser(userId) {
39
+ identity.userId = userId;
40
+ Object.assign(runtime, evaluate(), { userId, pendingUser: false });
41
+ },
42
+ };
43
+ return runtime;
44
+ }
@@ -0,0 +1,11 @@
1
+ import type { ExperimentAssignment } from "./experiments-runtime.types.js";
2
+ import type { ExperimentsConfig, ExperimentsIdentity } from "./experiments-config.types.js";
3
+ /**
4
+ * Evaluates flags locally without storage, network, clock, or browser globals.
5
+ * The same config and identity always produce the same assignments.
6
+ * This controls presentation, never authorization or access to data.
7
+ */
8
+ export declare function evaluateExperiments(config: ExperimentsConfig, identity: ExperimentsIdentity, preview?: Readonly<Record<string, boolean>>): {
9
+ flags: Record<string, boolean>;
10
+ assignments: ExperimentAssignment[];
11
+ };