@base44-preview/sdk 0.8.48-pr.280.2c0d8bc → 0.8.48-pr.280.35d591e

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/client.js CHANGED
@@ -16,6 +16,7 @@ import { RoomsSocket } from "./utils/socket-utils.js";
16
16
  import { createAnalyticsModule } from "./modules/analytics.js";
17
17
  import { createExperimentsModule } from "./modules/experiments.js";
18
18
  import { createExposureTracker } from "./modules/experiment-exposures.js";
19
+ import { EXPERIMENTS_CONTEXT_HEADER, getBrowserExperimentsContext, readExperimentsContext } from "./modules/experiments-context.js";
19
20
  import { createActorsModule, resolveActorsHost, } from "./modules/actors.js";
20
21
  /**
21
22
  * Creates a Base44 client.
@@ -55,10 +56,11 @@ import { createActorsModule, resolveActorsHost, } from "./modules/actors.js";
55
56
  * ```
56
57
  */
57
58
  export function createClient(config) {
58
- var _a, _b, _c, _d, _e;
59
+ var _a, _b, _c, _d, _e, _f, _g;
59
60
  const { serverUrl = "https://base44.app", appId, analytics, token, serviceToken, requiresAuth = false, appBaseUrl, options, functionsVersion, headers: optionalHeaders, } = config;
60
61
  // Normalize appBaseUrl to always be a string (empty if not provided or invalid)
61
62
  const normalizedAppBaseUrl = typeof appBaseUrl === "string" ? appBaseUrl : "";
63
+ const experimentsContext = (_a = config.experiments) !== null && _a !== void 0 ? _a : getBrowserExperimentsContext(appId);
62
64
  const socketConfig = {
63
65
  serverUrl,
64
66
  mountPath: "/ws-user-apps/socket.io/",
@@ -75,9 +77,14 @@ export function createClient(config) {
75
77
  }
76
78
  return socket;
77
79
  };
80
+ const { [EXPERIMENTS_CONTEXT_HEADER]: inheritedExperimentsContext, ...requestHeaders } = optionalHeaders !== null && optionalHeaders !== void 0 ? optionalHeaders : {};
78
81
  const headers = {
79
- ...optionalHeaders,
82
+ ...requestHeaders,
80
83
  "X-App-Id": String(appId),
84
+ ...(experimentsContext ? {
85
+ "Base44-Visitor-Id": experimentsContext.identity.visitorId,
86
+ "Base44-Experiment-Preview": JSON.stringify((_b = experimentsContext.preview) !== null && _b !== void 0 ? _b : {}),
87
+ } : {}),
81
88
  };
82
89
  const functionHeaders = functionsVersion
83
90
  ? {
@@ -123,13 +130,19 @@ export function createClient(config) {
123
130
  baseURL: `${serverUrl}/api`,
124
131
  headers,
125
132
  });
133
+ const exposureTracker = createExposureTracker({
134
+ axiosClient,
135
+ appId,
136
+ enabled: (_c = analytics === null || analytics === void 0 ? void 0 : analytics.enabled) !== null && _c !== void 0 ? _c : true,
137
+ source: typeof window === "undefined" ? "backend" : "browser",
138
+ pageUrl: experimentsContext === null || experimentsContext === void 0 ? void 0 : experimentsContext.pageUrl,
139
+ });
126
140
  const experiments = createExperimentsModule({
141
+ appId,
127
142
  getAuth: () => userAuthModule,
128
- trackExposure: createExposureTracker({
129
- axiosClient,
130
- appId,
131
- enabled: (_a = analytics === null || analytics === void 0 ? void 0 : analytics.enabled) !== null && _a !== void 0 ? _a : true,
132
- }).track,
143
+ trackExposure: exposureTracker.track,
144
+ flushExposures: exposureTracker.flush,
145
+ context: experimentsContext,
133
146
  });
134
147
  const userAuthModule = createAuthModule(axiosClient, functionsAxiosClient, appId, {
135
148
  appBaseUrl: normalizedAppBaseUrl,
@@ -147,11 +160,19 @@ export function createClient(config) {
147
160
  userAuthModule.setToken(accessToken);
148
161
  }
149
162
  }
163
+ if (experimentsContext) {
164
+ const { userId, status } = experimentsContext.identity;
165
+ // The document's cookie identity may differ from this client's localStorage token.
166
+ const needsClientIdentity = typeof window !== "undefined" && userAuthModule.hasToken() &&
167
+ experimentsContext.config.experiments.some((experiment) => experiment.assign_by === "user");
168
+ experiments.onAuthStateChange(status === "pending" || needsClientIdentity ? { status: "pending" } :
169
+ userId ? { status: "authenticated", userId } : { status: "anonymous" });
170
+ }
150
171
  const actorsModule = createActorsModule({
151
172
  appId,
152
173
  // serverUrl is often relative/empty (same-origin app); the proxy-fallback
153
174
  // URL needs an absolute host, so fall back to the page origin.
154
- host: resolveActorsHost(serverUrl, typeof window !== "undefined" ? (_b = window.location) === null || _b === void 0 ? void 0 : _b.origin : undefined),
175
+ host: resolveActorsHost(serverUrl, typeof window !== "undefined" ? (_d = window.location) === null || _d === void 0 ? void 0 : _d.origin : undefined),
155
176
  functionsVersion,
156
177
  getAuthToken: () => token || getAccessToken(),
157
178
  mintConnectionToken: async (actorName, room, connectionId) => {
@@ -190,7 +211,7 @@ export function createClient(config) {
190
211
  }
191
212
  return headers;
192
213
  },
193
- baseURL: (_c = functionsAxiosClient.defaults) === null || _c === void 0 ? void 0 : _c.baseURL,
214
+ baseURL: (_e = functionsAxiosClient.defaults) === null || _e === void 0 ? void 0 : _e.baseURL,
194
215
  }),
195
216
  agents: createAgentsModule({
196
217
  axios: axiosClient,
@@ -208,7 +229,9 @@ export function createClient(config) {
208
229
  serverUrl,
209
230
  appId,
210
231
  userAuthModule,
211
- enabled: (_d = analytics === null || analytics === void 0 ? void 0 : analytics.enabled) !== null && _d !== void 0 ? _d : true,
232
+ enabled: (_f = analytics === null || analytics === void 0 ? void 0 : analytics.enabled) !== null && _f !== void 0 ? _f : true,
233
+ getVisitorId: experiments.visitorId,
234
+ experimentsContext,
212
235
  }),
213
236
  actors: actorsModule.module,
214
237
  cleanup: () => {
@@ -238,7 +261,7 @@ export function createClient(config) {
238
261
  }
239
262
  return headers;
240
263
  },
241
- baseURL: (_e = serviceRoleFunctionsAxiosClient.defaults) === null || _e === void 0 ? void 0 : _e.baseURL,
264
+ baseURL: (_g = serviceRoleFunctionsAxiosClient.defaults) === null || _g === void 0 ? void 0 : _g.baseURL,
242
265
  }),
243
266
  agents: createAgentsModule({
244
267
  axios: serviceRoleAxiosClient,
@@ -281,7 +304,10 @@ export function createClient(config) {
281
304
  appId: String(appId),
282
305
  serverUrl,
283
306
  functionsVersion,
284
- platformHeaders: optionalHeaders,
307
+ platformHeaders: {
308
+ ...headers,
309
+ ...(inheritedExperimentsContext ? { [EXPERIMENTS_CONTEXT_HEADER]: inheritedExperimentsContext } : {}),
310
+ },
285
311
  }),
286
312
  /**
287
313
  * Sets a new authentication token for all subsequent requests.
@@ -433,6 +459,10 @@ export function createClientFromRequest(request) {
433
459
  }
434
460
  // Prepare additional headers to propagate
435
461
  const additionalHeaders = {};
462
+ const encodedExperiments = request.headers.get(EXPERIMENTS_CONTEXT_HEADER);
463
+ const experimentsContext = readExperimentsContext(encodedExperiments, appId);
464
+ if (experimentsContext && encodedExperiments)
465
+ additionalHeaders[EXPERIMENTS_CONTEXT_HEADER] = encodedExperiments;
436
466
  if (stateHeader) {
437
467
  additionalHeaders["Base44-State"] = stateHeader;
438
468
  }
@@ -453,5 +483,6 @@ export function createClientFromRequest(request) {
453
483
  serviceToken: serviceRoleToken,
454
484
  functionsVersion: functionsVersion !== null && functionsVersion !== void 0 ? functionsVersion : undefined,
455
485
  headers: additionalHeaders,
486
+ experiments: experimentsContext ? { ...experimentsContext, pageUrl: request.url ? new URL(request.url).pathname : "/" } : undefined,
456
487
  });
457
488
  }
@@ -10,6 +10,7 @@ import type { AppLogsModule } from "./modules/app-logs.types.js";
10
10
  import type { AppModule } from "./modules/app.types.js";
11
11
  import type { AnalyticsModule } from "./modules/analytics.types.js";
12
12
  import type { ExperimentsModule } from "./modules/experiments.types.js";
13
+ import type { ExperimentsContext } from "./modules/experiments-config.types.js";
13
14
  import type { ActorsModule } from "./modules/actors.types.js";
14
15
  import type { FetchWithAuthInit } from "./utils/fetch-with-auth.js";
15
16
  /**
@@ -80,6 +81,12 @@ export interface CreateClientConfig {
80
81
  * Omit this option to preserve the default analytics behavior.
81
82
  */
82
83
  analytics?: CreateClientAnalyticsConfig;
84
+ /**
85
+ * Platform-validated context for local flag evaluation. Request-scoped on servers.
86
+ * Automatically read from the platform bootstrap in browsers and trusted headers
87
+ * by createClientFromRequest(). Not an authorization credential.
88
+ */
89
+ experiments?: ExperimentsContext;
83
90
  /**
84
91
  * User authentication token. Used to authenticate as a specific user.
85
92
  *
@@ -135,7 +142,7 @@ export interface Base44Client {
135
142
  connectors: UserConnectorsModule;
136
143
  /** {@link EntitiesModule | Entities module} for CRUD operations on your data models. */
137
144
  entities: EntitiesModule;
138
- /** {@link ExperimentsModule | Experiments module} for browser feature flags and exposures. */
145
+ /** {@link ExperimentsModule | Experiments module} for local feature flags and exposures. */
139
146
  experiments: ExperimentsModule;
140
147
  /** {@link FunctionsModule | Functions module} for invoking custom backend functions. */
141
148
  functions: FunctionsModule;
package/dist/index.d.ts CHANGED
@@ -4,6 +4,8 @@ import { getAccessToken, saveAccessToken, removeAccessToken, getLoginUrl } from
4
4
  export { createClient, createClientFromRequest, Base44Error, getAccessToken, saveAccessToken, removeAccessToken, getLoginUrl, };
5
5
  export type { Base44Client, CreateClientAnalyticsConfig, CreateClientConfig, CreateClientOptions, Base44ErrorJSON, };
6
6
  export * from "./types.js";
7
+ export { evaluateExperiments } from "./modules/experiments-evaluator.js";
8
+ export type { ExperimentsConfig, ExperimentsContext, ExperimentsIdentity } from "./modules/experiments-config.types.js";
7
9
  export type { ExperimentsModule, ExperimentsSnapshot, } from "./modules/experiments.types.js";
8
10
  export type { DeleteManyResult, DeleteResult, EntitiesModule, EntityFilterOperators, EntityFilterQuery, EntityFilterValue, EntityHandler, EntityRecord, EntityTypeRegistry, ImportResult, RealtimeEventType, RealtimeEvent, RealtimeCallback, SortField, UpdateManyResult, } from "./modules/entities.types.js";
9
11
  export type { AuthModule, LoginResponse, RegisterParams, VerifyOtpParams, ChangePasswordParams, ResetPasswordParams, User, } from "./modules/auth.types.js";
package/dist/index.js CHANGED
@@ -3,4 +3,5 @@ import { Base44Error } from "./utils/axios-client.js";
3
3
  import { getAccessToken, saveAccessToken, removeAccessToken, getLoginUrl, } from "./utils/auth-utils.js";
4
4
  export { createClient, createClientFromRequest, Base44Error, getAccessToken, saveAccessToken, removeAccessToken, getLoginUrl, };
5
5
  export * from "./types.js";
6
+ export { evaluateExperiments } from "./modules/experiments-evaluator.js";
6
7
  export { Actor } from "./actor.js";
@@ -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,21 +1,44 @@
1
1
  import { AxiosInstance } from "axios";
2
- import { TrackEventParams, AnalyticsModuleOptions } from "./analytics.types";
2
+ import { TrackEventParams, AnalyticsModuleOptions, SessionContext } from "./analytics.types";
3
3
  import type { InternalAuthModule } from "./auth.types";
4
+ import type { ExperimentsContext } from "./experiments-config.types.js";
4
5
  export declare const USER_HEARTBEAT_EVENT_NAME = "__user_heartbeat_event__";
5
6
  export declare const ANALYTICS_INITIALIZATION_EVENT_NAME = "__initialization_event__";
6
7
  export declare const ANALYTICS_SESSION_DURATION_EVENT_NAME = "__session_duration_event__";
7
8
  export declare const ANALYTICS_CONFIG_ENABLE_URL_PARAM_KEY = "analytics-enable";
8
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;
9
22
  export interface AnalyticsModuleArgs {
10
23
  axiosClient: AxiosInstance;
11
24
  serverUrl: string;
12
25
  appId: string;
13
26
  userAuthModule: InternalAuthModule;
14
27
  enabled: boolean;
28
+ getVisitorId?: () => string | undefined;
29
+ experimentsContext?: ExperimentsContext;
15
30
  }
16
31
  /** @internal */
17
- export declare function isAnalyticsEnabled(enabled: boolean): boolean;
18
- export declare const createAnalyticsModule: ({ axiosClient, serverUrl, appId, userAuthModule, enabled, }: AnalyticsModuleArgs) => {
32
+ export declare function isAnalyticsEnabled(enabled: boolean, state?: {
33
+ isHeartBeatProcessing: boolean;
34
+ wasInitializationTracked: boolean;
35
+ sessionContext: SessionContext | null;
36
+ sessionContextPromise: Promise<SessionContext> | null;
37
+ sessionStartTime: string | null;
38
+ fallbackSessionId: string | null;
39
+ config: Required<AnalyticsModuleOptions>;
40
+ }): boolean;
41
+ export declare const createAnalyticsModule: ({ axiosClient, appId, userAuthModule, enabled, getVisitorId, experimentsContext, }: AnalyticsModuleArgs) => {
19
42
  track: (params: TrackEventParams) => void;
20
43
  cleanup: () => void;
21
44
  };
@@ -29,6 +52,15 @@ export declare const createAnalyticsModule: ({ axiosClient, serverUrl, appId, us
29
52
  *
30
53
  * @internal
31
54
  */
32
- export declare function resetAnalyticsSessionContext(): void;
55
+ export declare function resetAnalyticsSessionContext(axiosClient?: AxiosInstance): void;
33
56
  export declare function getAnalyticsConfigFromUrlParams(): AnalyticsModuleOptions | undefined;
34
- export declare function getAnalyticsSessionId(): string;
57
+ export declare function getAnalyticsSessionId(state?: {
58
+ isHeartBeatProcessing: boolean;
59
+ wasInitializationTracked: boolean;
60
+ sessionContext: SessionContext | null;
61
+ sessionContextPromise: Promise<SessionContext> | null;
62
+ sessionStartTime: string | null;
63
+ fallbackSessionId: string | null;
64
+ config: Required<AnalyticsModuleOptions>;
65
+ }): string;
66
+ export {};