@base44-preview/sdk 0.8.48-pr.280.cc2ee4a → 0.8.48-pr.280.cd0dea6

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,6 +1,7 @@
1
1
  import { getSharedInstance } from "../utils/sharedInstance.js";
2
2
  import { generateUuid, isReactNative } from "../utils/common.js";
3
3
  import { getExperimentsRuntime } from "./experiments-runtime.types.js";
4
+ import { getAnalyticsQueue } from "./analytics-queue.js";
4
5
  export const USER_HEARTBEAT_EVENT_NAME = "__user_heartbeat_event__";
5
6
  export const ANALYTICS_INITIALIZATION_EVENT_NAME = "__initialization_event__";
6
7
  export const ANALYTICS_SESSION_DURATION_EVENT_NAME = "__session_duration_event__";
@@ -14,111 +15,85 @@ const defaultConfiguration = {
14
15
  batchSize: 30,
15
16
  heartBeatInterval: 60 * 1000,
16
17
  };
17
- ///////////////////////////////////////////////
18
- //// shared queue for analytics events ////
19
- ///////////////////////////////////////////////
20
18
  const ANALYTICS_SHARED_STATE_NAME = "analytics";
21
- // shared state//
22
- const analyticsSharedState = getSharedInstance(ANALYTICS_SHARED_STATE_NAME, () => ({
23
- requestsQueue: [],
24
- isProcessing: false,
25
- isHeartBeatProcessing: false,
26
- wasInitializationTracked: false,
27
- sessionContext: null,
28
- sessionStartTime: null,
29
- // Memoized session id for when `localStorage` can't persist one — see
30
- // getAnalyticsSessionId.
31
- fallbackSessionId: null,
32
- config: {
33
- ...defaultConfiguration,
34
- ...getAnalyticsConfigFromUrlParams(),
35
- },
36
- }));
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();
37
37
  /** @internal */
38
- export function isAnalyticsEnabled(enabled) {
39
- 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;
40
48
  }
41
- export const createAnalyticsModule = ({ axiosClient, serverUrl, appId, userAuthModule, enabled, }) => {
42
- // prevent overflow of events //
43
- 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);
44
57
  // Disable analytics on React Native. It defines `window` but not `document`,
45
58
  // so the per-callsite `typeof window` guards below aren't enough to keep it
46
59
  // from touching `document` (e.g. `document.referrer` on init). Node/SSR is
47
60
  // still handled by those `window` guards, so this doesn't affect it.
48
- if (!isAnalyticsEnabled(enabled)) {
61
+ if (!isAnalyticsEnabled(enabled, state)) {
49
62
  return {
50
63
  track: () => { },
51
64
  cleanup: () => { },
52
65
  };
53
66
  }
54
67
  let clearHeartBeatProcessor = undefined;
55
- const trackBatchUrl = `${serverUrl}/api/apps/${appId}/analytics/track/batch`;
56
- const batchRequestFallback = async (events) => {
57
- await axiosClient.request({
58
- method: "POST",
59
- url: `/apps/${appId}/analytics/track/batch`,
60
- data: { events },
61
- });
62
- };
63
- // currently disabled, until fully tested //
64
- const beaconRequest = (events) => {
65
- try {
66
- const beaconPayload = JSON.stringify({ events });
67
- const blob = new Blob([beaconPayload], { type: "application/json" });
68
- return (typeof navigator === "undefined" ||
69
- beaconPayload.length > 60000 ||
70
- !navigator.sendBeacon(trackBatchUrl, blob));
71
- }
72
- catch (_a) {
73
- return false;
74
- }
75
- };
76
- const flush = async (eventsData, options = {}) => {
77
- if (eventsData.length === 0)
78
- return;
79
- const sessionContext_ = await getSessionContext(userAuthModule);
80
- const events = eventsData.map(transformEventDataToApiRequestData(sessionContext_));
81
- try {
82
- if (!options.isBeacon || !beaconRequest(events)) {
83
- await batchRequestFallback(events);
84
- }
85
- }
86
- catch (_a) {
87
- // do nothing
88
- }
89
- };
90
- const startProcessing = () => {
91
- startAnalyticsProcessor(flush, {
92
- throttleTime,
93
- batchSize,
94
- });
95
- };
96
68
  const track = (params) => {
97
- if (analyticsSharedState.requestsQueue.length >= maxQueueSize) {
98
- return;
99
- }
69
+ var _a, _b;
100
70
  const intrinsicData = getEventIntrinsicData();
101
- analyticsSharedState.requestsQueue.push({
102
- ...params,
103
- ...intrinsicData,
104
- });
105
- 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; }));
106
88
  };
107
89
  const onDocVisible = () => {
108
- startAnalyticsProcessor(flush, {
109
- throttleTime,
110
- batchSize,
111
- });
112
- clearHeartBeatProcessor = startHeartBeatProcessor(track);
113
- setSessionDurationTimerStart();
90
+ clearHeartBeatProcessor = startHeartBeatProcessor(track, automaticState);
91
+ setSessionDurationTimerStart(automaticState);
114
92
  };
115
93
  const onDocHidden = () => {
116
- stopAnalyticsProcessor();
117
94
  clearHeartBeatProcessor === null || clearHeartBeatProcessor === void 0 ? void 0 : clearHeartBeatProcessor();
118
- trackSessionDurationEvent(track);
119
- // flush entire queue on visibility change and hope for the best //
120
- const eventsData = analyticsSharedState.requestsQueue.splice(0);
121
- flush(eventsData, { isBeacon: true });
95
+ trackSessionDurationEvent(track, automaticState);
96
+ void queue.flush();
122
97
  };
123
98
  const onVisibilityChange = () => {
124
99
  if (typeof window === "undefined")
@@ -131,18 +106,16 @@ export const createAnalyticsModule = ({ axiosClient, serverUrl, appId, userAuthM
131
106
  }
132
107
  };
133
108
  const cleanup = () => {
134
- stopAnalyticsProcessor();
109
+ queue.cleanup();
135
110
  clearHeartBeatProcessor === null || clearHeartBeatProcessor === void 0 ? void 0 : clearHeartBeatProcessor();
136
111
  if (typeof window !== "undefined") {
137
112
  window.removeEventListener("visibilitychange", onVisibilityChange);
138
113
  }
139
114
  };
140
- // start the flusing process ///
141
- startProcessing();
142
115
  // start the heart beat processor //
143
- clearHeartBeatProcessor = startHeartBeatProcessor(track);
116
+ clearHeartBeatProcessor = startHeartBeatProcessor(track, automaticState);
144
117
  // track the referrer event //
145
- trackInitializationEvent(track);
118
+ trackInitializationEvent(track, automaticState);
146
119
  // start the visibility change listener //
147
120
  if (typeof window !== "undefined") {
148
121
  window.addEventListener("visibilitychange", onVisibilityChange);
@@ -152,50 +125,32 @@ export const createAnalyticsModule = ({ axiosClient, serverUrl, appId, userAuthM
152
125
  cleanup,
153
126
  };
154
127
  };
155
- function stopAnalyticsProcessor() {
156
- analyticsSharedState.isProcessing = false;
157
- }
158
- async function startAnalyticsProcessor(handleTrack, options) {
159
- if (analyticsSharedState.isProcessing) {
160
- // only one instance of the analytics processor can be running at a time //
161
- return;
162
- }
163
- analyticsSharedState.isProcessing = true;
164
- const { throttleTime = 1000, batchSize = 30 } = options !== null && options !== void 0 ? options : {};
165
- while (analyticsSharedState.isProcessing &&
166
- analyticsSharedState.requestsQueue.length > 0) {
167
- const requests = analyticsSharedState.requestsQueue.splice(0, batchSize);
168
- requests.length && (await handleTrack(requests));
169
- await new Promise((resolve) => setTimeout(resolve, throttleTime));
170
- }
171
- analyticsSharedState.isProcessing = false;
172
- }
173
- function startHeartBeatProcessor(track) {
128
+ function startHeartBeatProcessor(track, state) {
174
129
  var _a;
175
130
  // Browser-only, like the other automatic events here (initialization, session
176
131
  // duration, visibility). Outside a browser this timer fired a `me()` every
177
132
  // interval for the lifetime of a long-lived server-side client, and kept the
178
133
  // Node event loop alive. Explicit `analytics.track()` calls still work.
179
134
  if (typeof window === "undefined" ||
180
- analyticsSharedState.isHeartBeatProcessing ||
181
- ((_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) {
182
137
  return () => { };
183
138
  }
184
- analyticsSharedState.isHeartBeatProcessing = true;
139
+ state.isHeartBeatProcessing = true;
185
140
  const interval = setInterval(() => {
186
141
  track({ eventName: USER_HEARTBEAT_EVENT_NAME });
187
- }, analyticsSharedState.config.heartBeatInterval);
142
+ }, state.config.heartBeatInterval);
188
143
  return () => {
189
144
  clearInterval(interval);
190
- analyticsSharedState.isHeartBeatProcessing = false;
145
+ state.isHeartBeatProcessing = false;
191
146
  };
192
147
  }
193
- function trackInitializationEvent(track) {
148
+ function trackInitializationEvent(track, state) {
194
149
  if (typeof window === "undefined" ||
195
- analyticsSharedState.wasInitializationTracked) {
150
+ state.wasInitializationTracked) {
196
151
  return;
197
152
  }
198
- analyticsSharedState.wasInitializationTracked = true;
153
+ state.wasInitializationTracked = true;
199
154
  track({
200
155
  eventName: ANALYTICS_INITIALIZATION_EVENT_NAME,
201
156
  properties: {
@@ -203,20 +158,20 @@ function trackInitializationEvent(track) {
203
158
  },
204
159
  });
205
160
  }
206
- function setSessionDurationTimerStart() {
161
+ function setSessionDurationTimerStart(state) {
207
162
  if (typeof window === "undefined" ||
208
- analyticsSharedState.sessionStartTime !== null) {
163
+ state.sessionStartTime !== null) {
209
164
  return;
210
165
  }
211
- analyticsSharedState.sessionStartTime = new Date().toISOString();
166
+ state.sessionStartTime = new Date().toISOString();
212
167
  }
213
- function trackSessionDurationEvent(track) {
168
+ function trackSessionDurationEvent(track, state) {
214
169
  if (typeof window === "undefined" ||
215
- analyticsSharedState.sessionStartTime === null)
170
+ state.sessionStartTime === null)
216
171
  return;
217
172
  const sessionDuration = new Date().getTime() -
218
- new Date(analyticsSharedState.sessionStartTime).getTime();
219
- analyticsSharedState.sessionStartTime = null;
173
+ new Date(state.sessionStartTime).getTime();
174
+ state.sessionStartTime = null;
220
175
  track({
221
176
  eventName: ANALYTICS_SESSION_DURATION_EVENT_NAME,
222
177
  properties: { sessionDuration },
@@ -230,16 +185,6 @@ function getEventIntrinsicData() {
230
185
  pageUrl: typeof window !== "undefined" ? (_b = (_a = window.location) === null || _a === void 0 ? void 0 : _a.pathname) !== null && _b !== void 0 ? _b : null : null,
231
186
  };
232
187
  }
233
- function transformEventDataToApiRequestData(sessionContext) {
234
- return (eventData) => ({
235
- event_name: eventData.eventName,
236
- properties: eventData.properties,
237
- timestamp: eventData.timestamp,
238
- page_url: eventData.pageUrl,
239
- ...sessionContext,
240
- });
241
- }
242
- let sessionContextPromise = null;
243
188
  /**
244
189
  * Clears the memoized analytics session context.
245
190
  *
@@ -250,22 +195,23 @@ let sessionContextPromise = null;
250
195
  *
251
196
  * @internal
252
197
  */
253
- export function resetAnalyticsSessionContext() {
254
- analyticsSharedState.sessionContext = null;
255
- sessionContextPromise = null;
198
+ export function resetAnalyticsSessionContext(axiosClient) {
199
+ const state = axiosClient ? getAnalyticsState(axiosClient) : analyticsSharedState;
200
+ state.sessionContext = null;
201
+ state.sessionContextPromise = null;
256
202
  }
257
- async function getSessionContext(userAuthModule) {
258
- if (!analyticsSharedState.sessionContext) {
203
+ async function getSessionContext(userAuthModule, state) {
204
+ if (!state.sessionContext) {
259
205
  // With no token there is no identity to resolve: `me()` can only answer 401,
260
206
  // which the browser logs to the console before any handler here sees it. On
261
207
  // a public page that request is the sole reason an error appears, so skip
262
208
  // it. This is not memoized — a visitor who logs in later must still resolve.
263
209
  if (!userAuthModule.hasToken()) {
264
- return { user_id: null, session_id: getAnalyticsSessionId() };
210
+ return { user_id: null, session_id: getAnalyticsSessionId(state) };
265
211
  }
266
- if (!sessionContextPromise) {
267
- const sessionId = getAnalyticsSessionId();
268
- sessionContextPromise = userAuthModule
212
+ if (!state.sessionContextPromise) {
213
+ const sessionId = getAnalyticsSessionId(state);
214
+ state.sessionContextPromise = userAuthModule
269
215
  .me()
270
216
  .then((user) => ({
271
217
  user_id: user.id,
@@ -276,7 +222,7 @@ async function getSessionContext(userAuthModule) {
276
222
  session_id: sessionId,
277
223
  }));
278
224
  }
279
- const pending = sessionContextPromise;
225
+ const pending = state.sessionContextPromise;
280
226
  const context = await pending;
281
227
  // Publish only if this lookup is still the current one. A reset that lands
282
228
  // while the request is in flight nulls `sessionContextPromise`, and an
@@ -284,12 +230,12 @@ async function getSessionContext(userAuthModule) {
284
230
  // for the rest of the session. The awaited value is still returned: these
285
231
  // events were queued before the identity changed, so that is who they
286
232
  // belong to.
287
- if (sessionContextPromise === pending) {
288
- analyticsSharedState.sessionContext = context;
233
+ if (state.sessionContextPromise === pending) {
234
+ state.sessionContext = context;
289
235
  }
290
236
  return context;
291
237
  }
292
- return analyticsSharedState.sessionContext;
238
+ return state.sessionContext;
293
239
  }
294
240
  export function getAnalyticsConfigFromUrlParams() {
295
241
  // `window.location` is absent on React Native. This runs at module load (via
@@ -311,19 +257,18 @@ export function getAnalyticsConfigFromUrlParams() {
311
257
  // return the config object //
312
258
  return { enabled: analyticsEnable === "true" };
313
259
  }
314
- // When the id can't be persisted (React Native has no `localStorage`), keep
315
- // it stable for the process instead of minting a fresh one per call.
316
- function getFallbackSessionId() {
260
+ // Without persistent storage, keep the id stable within this analytics state.
261
+ function getFallbackSessionId(state) {
317
262
  var _a;
318
- 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()));
319
264
  }
320
- export function getAnalyticsSessionId() {
265
+ export function getAnalyticsSessionId(state = analyticsSharedState) {
321
266
  var _a;
322
267
  const visitorId = (_a = getExperimentsRuntime()) === null || _a === void 0 ? void 0 : _a.visitorId;
323
268
  if (visitorId && visitorId !== "anon")
324
269
  return visitorId;
325
270
  if (typeof window === "undefined") {
326
- return getFallbackSessionId();
271
+ return getFallbackSessionId(state);
327
272
  }
328
273
  try {
329
274
  const sessionId = localStorage.getItem(ANALYTICS_SESSION_ID_LOCAL_STORAGE_KEY);
@@ -335,6 +280,6 @@ export function getAnalyticsSessionId() {
335
280
  return sessionId;
336
281
  }
337
282
  catch (_b) {
338
- return getFallbackSessionId();
283
+ return getFallbackSessionId(state);
339
284
  }
340
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
+ };