@base44-preview/sdk 0.8.48-pr.280.7eec9ba → 0.8.48-pr.280.b620aa9

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.
@@ -0,0 +1,12 @@
1
+ import type { AxiosInstance } from "axios";
2
+ import type { AnalyticsApiRequestData, AnalyticsModuleOptions } from "./analytics.types.js";
3
+ type Event = AnalyticsApiRequestData & {
4
+ event_id?: string;
5
+ };
6
+ /** @internal One transport queue per client, shared by goals and exposures. */
7
+ export declare function getAnalyticsQueue(axiosClient: AxiosInstance, appId: string, config: AnalyticsModuleOptions): {
8
+ enqueue(event: Event | Promise<Event>, authorization: string | null, userId: string | null | Promise<string | null>): void;
9
+ flush(): Promise<void>;
10
+ cleanup(): void;
11
+ };
12
+ export {};
@@ -0,0 +1,128 @@
1
+ const DELIVERY_BUDGET_MS = 5000;
2
+ const queues = new WeakMap();
3
+ /** @internal One transport queue per client, shared by goals and exposures. */
4
+ export function getAnalyticsQueue(axiosClient, appId, config) {
5
+ let queue = queues.get(axiosClient);
6
+ if (!queue) {
7
+ queue = createAnalyticsQueue(axiosClient, appId, config);
8
+ queues.set(axiosClient, queue);
9
+ }
10
+ return queue;
11
+ }
12
+ function createAnalyticsQueue(axiosClient, appId, config) {
13
+ const entries = [];
14
+ const pending = new Set();
15
+ let timer;
16
+ function deliver(batch) {
17
+ const controller = new AbortController();
18
+ const deadlineAt = Date.now() + DELIVERY_BUDGET_MS;
19
+ const deadline = new Promise((resolve) => {
20
+ controller.signal.addEventListener("abort", () => resolve(), { once: true });
21
+ });
22
+ const timeout = setTimeout(() => controller.abort(), DELIVERY_BUDGET_MS);
23
+ function send(prepared) {
24
+ var _a;
25
+ const groups = new Map();
26
+ for (const entry of prepared) {
27
+ const key = JSON.stringify([entry.authorization, entry.userId, entry.event.session_id]);
28
+ const group = (_a = groups.get(key)) !== null && _a !== void 0 ? _a : [];
29
+ group.push(entry);
30
+ groups.set(key, group);
31
+ }
32
+ return Promise.all([...groups.values()].map(async (group) => {
33
+ var _a, _b;
34
+ const events = group.map(({ event }) => event);
35
+ const exposures = events.filter((event) => event.event_name === "__experiment_exposure__");
36
+ const attempts = exposures.length ? 3 : 1;
37
+ for (let attempt = 0; attempt < attempts; attempt++) {
38
+ try {
39
+ if (controller.signal.aborted)
40
+ return;
41
+ await axiosClient.request({
42
+ method: "POST", url: `/apps/${appId}/analytics/track/batch`,
43
+ headers: { Authorization: group[0].authorization },
44
+ // Ordinary goals have no backend deduplication and remain single-attempt.
45
+ data: { events: attempt === 0 ? events : exposures },
46
+ timeout: Math.max(1, deadlineAt - Date.now()), signal: controller.signal,
47
+ });
48
+ return;
49
+ }
50
+ catch (error) {
51
+ const status = (_b = (_a = error.response) === null || _a === void 0 ? void 0 : _a.status) !== null && _b !== void 0 ? _b : error.status;
52
+ if (controller.signal.aborted || attempt === attempts - 1 ||
53
+ (status !== undefined && (status < 500 || status >= 600)))
54
+ return;
55
+ await new Promise((resolve) => setTimeout(resolve, attempt === 0 ? 100 : 500));
56
+ }
57
+ }
58
+ }));
59
+ }
60
+ const delivery = (async () => {
61
+ const ready = [];
62
+ const requests = [];
63
+ let wake = () => { };
64
+ let preparedAll = false;
65
+ const preparation = Promise.all(batch.map(async (entry) => {
66
+ const [event, userId] = await Promise.all([entry.event, entry.userId]);
67
+ if (event && !controller.signal.aborted)
68
+ ready.push({ event, userId, authorization: entry.authorization });
69
+ wake();
70
+ })).then(() => { preparedAll = true; wake(); });
71
+ while (!preparedAll || ready.length) {
72
+ if (!ready.length && !preparedAll)
73
+ await Promise.race([
74
+ new Promise((resolve) => { wake = resolve; }), deadline,
75
+ ]);
76
+ if (controller.signal.aborted)
77
+ return;
78
+ // Coalesce this turn's resolved identities without waiting for unrelated auth I/O.
79
+ let turnTimer;
80
+ await Promise.race([preparation, new Promise((resolve) => { turnTimer = setTimeout(resolve, 0); })]);
81
+ clearTimeout(turnTimer);
82
+ if (ready.length)
83
+ requests.push(send(ready.splice(0)));
84
+ }
85
+ await Promise.all(requests);
86
+ })();
87
+ // Identity lookup and transports that ignore cancellation must also be bounded.
88
+ const settlement = Promise.race([delivery, deadline]).catch(() => { }).finally(() => {
89
+ clearTimeout(timeout);
90
+ controller.abort();
91
+ pending.delete(settlement);
92
+ });
93
+ pending.add(settlement);
94
+ }
95
+ function schedule() {
96
+ var _a;
97
+ if (timer || entries.length === 0)
98
+ return;
99
+ timer = setTimeout(() => {
100
+ var _a;
101
+ timer = undefined;
102
+ deliver(entries.splice(0, (_a = config.batchSize) !== null && _a !== void 0 ? _a : 30));
103
+ schedule();
104
+ }, (_a = config.throttleTime) !== null && _a !== void 0 ? _a : 1000);
105
+ }
106
+ return {
107
+ enqueue(event, authorization, userId) {
108
+ var _a;
109
+ if (entries.length >= ((_a = config.maxQueueSize) !== null && _a !== void 0 ? _a : 1000))
110
+ return;
111
+ entries.push({ event: Promise.resolve(event).catch(() => undefined), authorization,
112
+ userId: Promise.resolve(userId).catch(() => null) });
113
+ schedule();
114
+ },
115
+ async flush() {
116
+ var _a;
117
+ clearTimeout(timer);
118
+ timer = undefined;
119
+ while (entries.length)
120
+ deliver(entries.splice(0, (_a = config.batchSize) !== null && _a !== void 0 ? _a : 30));
121
+ await Promise.all([...pending]);
122
+ },
123
+ cleanup() {
124
+ clearTimeout(timer);
125
+ timer = undefined;
126
+ },
127
+ };
128
+ }
@@ -1,5 +1,5 @@
1
1
  import { AxiosInstance } from "axios";
2
- import { TrackEventParams, TrackEventData, AnalyticsModuleOptions, SessionContext } from "./analytics.types";
2
+ import { TrackEventParams, AnalyticsModuleOptions, SessionContext } from "./analytics.types";
3
3
  import type { InternalAuthModule } from "./auth.types";
4
4
  import type { ExperimentsContext } from "./experiments-config.types.js";
5
5
  export declare const USER_HEARTBEAT_EVENT_NAME = "__user_heartbeat_event__";
@@ -7,6 +7,18 @@ export declare const ANALYTICS_INITIALIZATION_EVENT_NAME = "__initialization_eve
7
7
  export declare const ANALYTICS_SESSION_DURATION_EVENT_NAME = "__session_duration_event__";
8
8
  export declare const ANALYTICS_CONFIG_ENABLE_URL_PARAM_KEY = "analytics-enable";
9
9
  export declare const ANALYTICS_SESSION_ID_LOCAL_STORAGE_KEY = "base44_analytics_session_id";
10
+ declare function createAnalyticsState(): {
11
+ isHeartBeatProcessing: boolean;
12
+ wasInitializationTracked: boolean;
13
+ sessionContext: SessionContext | null;
14
+ sessionContextPromise: Promise<SessionContext> | null;
15
+ sessionStartTime: string | null;
16
+ fallbackSessionId: string | null;
17
+ config: Required<AnalyticsModuleOptions>;
18
+ };
19
+ type AnalyticsState = ReturnType<typeof createAnalyticsState>;
20
+ /** @internal */
21
+ export declare function getAnalyticsState(axiosClient: AxiosInstance): AnalyticsState;
10
22
  export interface AnalyticsModuleArgs {
11
23
  axiosClient: AxiosInstance;
12
24
  serverUrl: string;
@@ -18,8 +30,6 @@ export interface AnalyticsModuleArgs {
18
30
  }
19
31
  /** @internal */
20
32
  export declare function isAnalyticsEnabled(enabled: boolean, state?: {
21
- requestsQueue: TrackEventData[];
22
- isProcessing: boolean;
23
33
  isHeartBeatProcessing: boolean;
24
34
  wasInitializationTracked: boolean;
25
35
  sessionContext: SessionContext | null;
@@ -28,7 +38,7 @@ export declare function isAnalyticsEnabled(enabled: boolean, state?: {
28
38
  fallbackSessionId: string | null;
29
39
  config: Required<AnalyticsModuleOptions>;
30
40
  }): boolean;
31
- export declare const createAnalyticsModule: ({ axiosClient, serverUrl, appId, userAuthModule, enabled, getVisitorId, experimentsContext, }: AnalyticsModuleArgs) => {
41
+ export declare const createAnalyticsModule: ({ axiosClient, appId, userAuthModule, enabled, getVisitorId, experimentsContext, }: AnalyticsModuleArgs) => {
32
42
  track: (params: TrackEventParams) => void;
33
43
  cleanup: () => void;
34
44
  };
@@ -45,8 +55,6 @@ export declare const createAnalyticsModule: ({ axiosClient, serverUrl, appId, us
45
55
  export declare function resetAnalyticsSessionContext(axiosClient?: AxiosInstance): void;
46
56
  export declare function getAnalyticsConfigFromUrlParams(): AnalyticsModuleOptions | undefined;
47
57
  export declare function getAnalyticsSessionId(state?: {
48
- requestsQueue: TrackEventData[];
49
- isProcessing: boolean;
50
58
  isHeartBeatProcessing: boolean;
51
59
  wasInitializationTracked: boolean;
52
60
  sessionContext: SessionContext | null;
@@ -55,3 +63,4 @@ export declare function getAnalyticsSessionId(state?: {
55
63
  fallbackSessionId: string | null;
56
64
  config: Required<AnalyticsModuleOptions>;
57
65
  }): string;
66
+ export {};
@@ -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,14 +15,9 @@ 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
19
  function createAnalyticsState() {
22
20
  return {
23
- requestsQueue: [],
24
- isProcessing: false,
25
21
  isHeartBeatProcessing: false,
26
22
  wasInitializationTracked: false,
27
23
  sessionContext: null,
@@ -37,17 +33,27 @@ function createAnalyticsState() {
37
33
  };
38
34
  }
39
35
  const analyticsSharedState = getSharedInstance(ANALYTICS_SHARED_STATE_NAME, createAnalyticsState);
40
- const serverAnalyticsStates = new WeakMap();
36
+ const clientAnalyticsStates = new WeakMap();
37
+ /** @internal */
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;
48
+ }
41
49
  /** @internal */
42
50
  export function isAnalyticsEnabled(enabled, state = analyticsSharedState) {
43
51
  return enabled && state.config.enabled && !isReactNative;
44
52
  }
45
- export const createAnalyticsModule = ({ axiosClient, serverUrl, appId, userAuthModule, enabled, getVisitorId, experimentsContext, }) => {
46
- const state = typeof window === "undefined" ? createAnalyticsState() : analyticsSharedState;
47
- if (typeof window === "undefined")
48
- serverAnalyticsStates.set(axiosClient, state);
49
- // prevent overflow of events //
50
- const { maxQueueSize, throttleTime, batchSize } = state.config;
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);
51
57
  // Disable analytics on React Native. It defines `window` but not `document`,
52
58
  // so the per-callsite `typeof window` guards below aren't enough to keep it
53
59
  // from touching `document` (e.g. `document.referrer` on init). Node/SSR is
@@ -59,83 +65,35 @@ export const createAnalyticsModule = ({ axiosClient, serverUrl, appId, userAuthM
59
65
  };
60
66
  }
61
67
  let clearHeartBeatProcessor = undefined;
62
- const trackBatchUrl = `${serverUrl}/api/apps/${appId}/analytics/track/batch`;
63
- const batchRequestFallback = async (events) => {
64
- await axiosClient.request({
65
- method: "POST",
66
- url: `/apps/${appId}/analytics/track/batch`,
67
- data: { events },
68
- });
69
- };
70
- // currently disabled, until fully tested //
71
- const beaconRequest = (events) => {
72
- try {
73
- const beaconPayload = JSON.stringify({ events });
74
- const blob = new Blob([beaconPayload], { type: "application/json" });
75
- return (typeof navigator === "undefined" ||
76
- beaconPayload.length > 60000 ||
77
- !navigator.sendBeacon(trackBatchUrl, blob));
78
- }
79
- catch (_a) {
80
- return false;
81
- }
82
- };
83
- const flush = async (eventsData, options = {}) => {
84
- var _a;
85
- if (eventsData.length === 0)
86
- return;
87
- const sessionContext_ = await getSessionContext(userAuthModule, state);
88
- const events = eventsData.map(transformEventDataToApiRequestData({ ...sessionContext_, session_id: (_a = getVisitorId === null || getVisitorId === void 0 ? void 0 : getVisitorId()) !== null && _a !== void 0 ? _a : sessionContext_.session_id }));
89
- try {
90
- if (!options.isBeacon || !beaconRequest(events)) {
91
- await batchRequestFallback(events);
92
- }
93
- }
94
- catch (_b) {
95
- // do nothing
96
- }
97
- };
98
- const startProcessing = () => {
99
- startAnalyticsProcessor(flush, {
100
- throttleTime,
101
- batchSize,
102
- }, state);
103
- };
104
68
  const track = (params) => {
105
- var _a;
106
- if (state.requestsQueue.length >= maxQueueSize) {
107
- return;
108
- }
69
+ var _a, _b;
109
70
  const intrinsicData = getEventIntrinsicData();
110
- const preview = Object.fromEntries(Object.entries((_a = experimentsContext === null || experimentsContext === void 0 ? void 0 : experimentsContext.preview) !== null && _a !== void 0 ? _a : {}).filter(([, value]) => typeof value === "boolean"));
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"));
111
75
  const properties = { ...params.properties };
112
76
  delete properties.__b44_experiment_preview;
113
77
  if (Object.keys(preview).length) {
114
78
  // Capture now: a queued event must retain its occurrence-time preview.
115
79
  properties.__b44_experiment_preview = JSON.stringify(preview);
116
80
  }
117
- state.requestsQueue.push({
118
- ...params,
119
- ...intrinsicData,
81
+ const event = {
82
+ event_name: params.eventName,
83
+ timestamp: intrinsicData.timestamp,
84
+ page_url: intrinsicData.pageUrl,
120
85
  properties: params.properties || Object.keys(properties).length ? properties : undefined,
121
- });
122
- startProcessing();
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; }));
123
88
  };
124
89
  const onDocVisible = () => {
125
- startAnalyticsProcessor(flush, {
126
- throttleTime,
127
- batchSize,
128
- }, state);
129
- clearHeartBeatProcessor = startHeartBeatProcessor(track, state);
130
- setSessionDurationTimerStart(state);
90
+ clearHeartBeatProcessor = startHeartBeatProcessor(track, automaticState);
91
+ setSessionDurationTimerStart(automaticState);
131
92
  };
132
93
  const onDocHidden = () => {
133
- stopAnalyticsProcessor(state);
134
94
  clearHeartBeatProcessor === null || clearHeartBeatProcessor === void 0 ? void 0 : clearHeartBeatProcessor();
135
- trackSessionDurationEvent(track, state);
136
- // flush entire queue on visibility change and hope for the best //
137
- const eventsData = state.requestsQueue.splice(0);
138
- flush(eventsData, { isBeacon: true });
95
+ trackSessionDurationEvent(track, automaticState);
96
+ void queue.flush();
139
97
  };
140
98
  const onVisibilityChange = () => {
141
99
  if (typeof window === "undefined")
@@ -148,18 +106,16 @@ export const createAnalyticsModule = ({ axiosClient, serverUrl, appId, userAuthM
148
106
  }
149
107
  };
150
108
  const cleanup = () => {
151
- stopAnalyticsProcessor(state);
109
+ queue.cleanup();
152
110
  clearHeartBeatProcessor === null || clearHeartBeatProcessor === void 0 ? void 0 : clearHeartBeatProcessor();
153
111
  if (typeof window !== "undefined") {
154
112
  window.removeEventListener("visibilitychange", onVisibilityChange);
155
113
  }
156
114
  };
157
- // start the flusing process ///
158
- startProcessing();
159
115
  // start the heart beat processor //
160
- clearHeartBeatProcessor = startHeartBeatProcessor(track, state);
116
+ clearHeartBeatProcessor = startHeartBeatProcessor(track, automaticState);
161
117
  // track the referrer event //
162
- trackInitializationEvent(track, state);
118
+ trackInitializationEvent(track, automaticState);
163
119
  // start the visibility change listener //
164
120
  if (typeof window !== "undefined") {
165
121
  window.addEventListener("visibilitychange", onVisibilityChange);
@@ -169,24 +125,6 @@ export const createAnalyticsModule = ({ axiosClient, serverUrl, appId, userAuthM
169
125
  cleanup,
170
126
  };
171
127
  };
172
- function stopAnalyticsProcessor(state) {
173
- state.isProcessing = false;
174
- }
175
- async function startAnalyticsProcessor(handleTrack, options, state) {
176
- if (state.isProcessing) {
177
- // only one instance of the analytics processor can be running at a time //
178
- return;
179
- }
180
- state.isProcessing = true;
181
- const { throttleTime = 1000, batchSize = 30 } = options !== null && options !== void 0 ? options : {};
182
- while (state.isProcessing &&
183
- state.requestsQueue.length > 0) {
184
- const requests = state.requestsQueue.splice(0, batchSize);
185
- requests.length && (await handleTrack(requests));
186
- await new Promise((resolve) => setTimeout(resolve, throttleTime));
187
- }
188
- state.isProcessing = false;
189
- }
190
128
  function startHeartBeatProcessor(track, state) {
191
129
  var _a;
192
130
  // Browser-only, like the other automatic events here (initialization, session
@@ -247,15 +185,6 @@ function getEventIntrinsicData() {
247
185
  pageUrl: typeof window !== "undefined" ? (_b = (_a = window.location) === null || _a === void 0 ? void 0 : _a.pathname) !== null && _b !== void 0 ? _b : null : null,
248
186
  };
249
187
  }
250
- function transformEventDataToApiRequestData(sessionContext) {
251
- return (eventData) => ({
252
- event_name: eventData.eventName,
253
- properties: eventData.properties,
254
- timestamp: eventData.timestamp,
255
- page_url: eventData.pageUrl,
256
- ...sessionContext,
257
- });
258
- }
259
188
  /**
260
189
  * Clears the memoized analytics session context.
261
190
  *
@@ -267,8 +196,7 @@ function transformEventDataToApiRequestData(sessionContext) {
267
196
  * @internal
268
197
  */
269
198
  export function resetAnalyticsSessionContext(axiosClient) {
270
- var _a;
271
- const state = axiosClient ? (_a = serverAnalyticsStates.get(axiosClient)) !== null && _a !== void 0 ? _a : analyticsSharedState : analyticsSharedState;
199
+ const state = axiosClient ? getAnalyticsState(axiosClient) : analyticsSharedState;
272
200
  state.sessionContext = null;
273
201
  state.sessionContextPromise = null;
274
202
  }
@@ -15,5 +15,5 @@ export declare function createExposureTracker({ axiosClient, appId, enabled, sou
15
15
  visitorId: string;
16
16
  userId: string | null;
17
17
  }): void;
18
- flush(): Promise<void>;
18
+ flush: () => Promise<void>;
19
19
  };
@@ -1,65 +1,30 @@
1
1
  import { v4 as uuid } from "uuid";
2
- import { isAnalyticsEnabled } from "./analytics.js";
2
+ import { getAnalyticsState, isAnalyticsEnabled } from "./analytics.js";
3
+ import { getAnalyticsQueue } from "./analytics-queue.js";
3
4
  /** @internal */
4
5
  export function createExposureTracker({ axiosClient, appId, enabled, source = "browser", pageUrl, }) {
5
- const entries = new Map();
6
- function send(entry) {
7
- if (entry.pending)
8
- return entry.pending;
9
- const pending = (async () => {
10
- for (let attempt = 0;; attempt++) {
11
- try {
12
- const response = await axiosClient.request({
13
- method: "POST",
14
- url: `/apps/${appId}/analytics/track/batch`,
15
- headers: { Authorization: entry.authorization },
16
- data: entry.data,
17
- });
18
- if (response.accepted !== 1)
19
- throw new Error("Experiment exposure was not accepted");
20
- entry.acknowledged = true;
21
- return;
22
- }
23
- catch (error) {
24
- if (attempt === 2)
25
- throw error;
26
- await new Promise((resolve) => setTimeout(resolve, attempt === 0 ? 100 : 500));
27
- }
28
- }
29
- })().finally(() => { entry.pending = undefined; });
30
- entry.pending = pending;
31
- // Reads stay synchronous; flush() lets request handlers observe delivery failures.
32
- void pending.catch(() => { });
33
- return pending;
34
- }
6
+ const state = getAnalyticsState(axiosClient);
7
+ const queue = getAnalyticsQueue(axiosClient, appId, state.config);
8
+ const tracked = new Set();
35
9
  return {
36
10
  track(assignment, identity) {
37
- if ((source === "browser" && typeof window === "undefined") || !isAnalyticsEnabled(enabled))
11
+ if ((source === "browser" && typeof window === "undefined") || !isAnalyticsEnabled(enabled, state))
38
12
  return;
39
13
  const { experiment_id, run_version, variant_key } = assignment;
40
14
  const key = JSON.stringify([experiment_id, run_version, variant_key, identity.userId, identity.visitorId]);
41
- let entry = entries.get(key);
42
- if (!entry) {
43
- const authorization = identity.userId ? axiosClient.defaults.headers.common.Authorization : null;
44
- entry = {
45
- acknowledged: false,
46
- authorization: typeof authorization === "string" ? authorization : null,
47
- data: { events: [{
48
- event_id: uuid(),
49
- event_name: "__experiment_exposure__",
50
- timestamp: new Date().toISOString(),
51
- session_id: identity.visitorId,
52
- page_url: pageUrl !== null && pageUrl !== void 0 ? pageUrl : (typeof window === "undefined" ? "/" : window.location.pathname),
53
- properties: { experiment_id, run_version, variant_key, source },
54
- }] },
55
- };
56
- entries.set(key, entry);
57
- }
58
- if (!entry.acknowledged)
59
- void send(entry);
60
- },
61
- async flush() {
62
- await Promise.all([...entries.values()].filter((entry) => !entry.acknowledged).map(send));
15
+ if (tracked.has(key))
16
+ return;
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);
63
27
  },
28
+ flush: queue.flush,
64
29
  };
65
30
  }
@@ -25,7 +25,7 @@ export interface ExperimentsSnapshot {
25
25
  */
26
26
  export interface ExperimentsModule {
27
27
  /**
28
- * Reads a flag and queues an acknowledged exposure for its current assignment.
28
+ * Reads a flag and queues a best-effort exposure for its current assignment.
29
29
  *
30
30
  * Never starts an authentication request. Reads return the fallback while the
31
31
  * app's normal auth initialization is pending or failed. Supply trusted bootstrap
@@ -34,8 +34,13 @@ export interface ExperimentsModule {
34
34
  * Call only where the feature is used: a read counts as exposure, not proof of
35
35
  * visibility. Preview overrides and flags without an assignment are not tracked.
36
36
  * Exposures respect the client's analytics setting, are deduplicated per client,
37
- * experiment run, variant and identity. Failed sends retry up to three attempts
38
- * with the same event ID, timestamp and credentials. Await flush() on servers.
37
+ * experiment run, variant and identity. Network and server failures retry up to
38
+ * three attempts within five seconds of batch delivery, preserving the event ID,
39
+ * timestamp and credentials. HTTP successes (including rejected measurements) and client errors
40
+ * are terminal. Exposures share the Analytics batch with compatible ordinary
41
+ * events; credentials and user/visitor identities are captured when tracking.
42
+ * Only exposures are retried; ordinary goals retain single-attempt delivery.
43
+ * On servers, use the runtime's background lifetime mechanism.
39
44
  *
40
45
  * @param flagKey - Feature flag key defined in your app.
41
46
  * @param fallback - Value for an unavailable flag or unresolved identity. Defaults to `false`.
@@ -97,10 +102,23 @@ export interface ExperimentsModule {
97
102
  */
98
103
  ready(): Promise<ExperimentsSnapshot>;
99
104
  /**
100
- * Waits until queued exposures are acknowledged; rejects after bounded retries.
101
- * Server/Worker handlers must await this before ending the request (or use waitUntil).
102
- * Retries preserve event IDs but raw storage is not exactly-once. Calling again
103
- * retries unacknowledged events with the same IDs. No new exposures are created.
105
+ * Flushes this client's queued Analytics goals and exposures without rejecting.
106
+ * Each delivery has a five-second total budget; exhausted or rejected events are
107
+ * dropped and are not retried by later reads or flushes. Settlement is not proof
108
+ * of ingestion, and raw storage is not exactly-once. No new exposures are created.
109
+ * Worker handlers should use `ctx.waitUntil(client.experiments.flush())` instead
110
+ * of awaiting Analytics on the application response path. Other runtimes must use
111
+ * their supported background lifetime mechanism; fire-and-forget alone may be cut off.
112
+ * Base44's legacy Cloudflare runtime exposes `globalThis.Base44.waitUntil(...)`;
113
+ * the newer runtime exports `waitUntil` from `base44:runtime`. Use the API provided
114
+ * by your deployed runtime. Deno without a background lifetime API must await flush.
115
+ *
116
+ * @returns A promise that resolves when the current batch deliveries settle.
117
+ * @example
118
+ * ```typescript
119
+ * // In a Worker handler with an execution context:
120
+ * ctx.waitUntil(base44.experiments.flush());
121
+ * ```
104
122
  */
105
123
  flush(): Promise<void>;
106
124
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@base44-preview/sdk",
3
- "version": "0.8.48-pr.280.7eec9ba",
3
+ "version": "0.8.48-pr.280.b620aa9",
4
4
  "description": "JavaScript SDK for Base44 API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",