@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.
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,18 @@ 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({
127
141
  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,
142
+ trackExposure: exposureTracker.track,
143
+ flushExposures: exposureTracker.flush,
144
+ context: experimentsContext,
133
145
  });
134
146
  const userAuthModule = createAuthModule(axiosClient, functionsAxiosClient, appId, {
135
147
  appBaseUrl: normalizedAppBaseUrl,
@@ -147,11 +159,19 @@ export function createClient(config) {
147
159
  userAuthModule.setToken(accessToken);
148
160
  }
149
161
  }
162
+ if (experimentsContext) {
163
+ const { userId, status } = experimentsContext.identity;
164
+ // The document's cookie identity may differ from this client's localStorage token.
165
+ const needsClientIdentity = typeof window !== "undefined" && userAuthModule.hasToken() &&
166
+ experimentsContext.config.experiments.some((experiment) => experiment.assign_by === "user");
167
+ experiments.onAuthStateChange(status === "pending" || needsClientIdentity ? { status: "pending" } :
168
+ userId ? { status: "authenticated", userId } : { status: "anonymous" });
169
+ }
150
170
  const actorsModule = createActorsModule({
151
171
  appId,
152
172
  // serverUrl is often relative/empty (same-origin app); the proxy-fallback
153
173
  // 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),
174
+ host: resolveActorsHost(serverUrl, typeof window !== "undefined" ? (_d = window.location) === null || _d === void 0 ? void 0 : _d.origin : undefined),
155
175
  functionsVersion,
156
176
  getAuthToken: () => token || getAccessToken(),
157
177
  mintConnectionToken: async (actorName, room, connectionId) => {
@@ -190,7 +210,7 @@ export function createClient(config) {
190
210
  }
191
211
  return headers;
192
212
  },
193
- baseURL: (_c = functionsAxiosClient.defaults) === null || _c === void 0 ? void 0 : _c.baseURL,
213
+ baseURL: (_e = functionsAxiosClient.defaults) === null || _e === void 0 ? void 0 : _e.baseURL,
194
214
  }),
195
215
  agents: createAgentsModule({
196
216
  axios: axiosClient,
@@ -208,7 +228,9 @@ export function createClient(config) {
208
228
  serverUrl,
209
229
  appId,
210
230
  userAuthModule,
211
- enabled: (_d = analytics === null || analytics === void 0 ? void 0 : analytics.enabled) !== null && _d !== void 0 ? _d : true,
231
+ enabled: (_f = analytics === null || analytics === void 0 ? void 0 : analytics.enabled) !== null && _f !== void 0 ? _f : true,
232
+ getVisitorId: experiments.visitorId,
233
+ experimentsContext,
212
234
  }),
213
235
  actors: actorsModule.module,
214
236
  cleanup: () => {
@@ -238,7 +260,7 @@ export function createClient(config) {
238
260
  }
239
261
  return headers;
240
262
  },
241
- baseURL: (_e = serviceRoleFunctionsAxiosClient.defaults) === null || _e === void 0 ? void 0 : _e.baseURL,
263
+ baseURL: (_g = serviceRoleFunctionsAxiosClient.defaults) === null || _g === void 0 ? void 0 : _g.baseURL,
242
264
  }),
243
265
  agents: createAgentsModule({
244
266
  axios: serviceRoleAxiosClient,
@@ -281,7 +303,10 @@ export function createClient(config) {
281
303
  appId: String(appId),
282
304
  serverUrl,
283
305
  functionsVersion,
284
- platformHeaders: optionalHeaders,
306
+ platformHeaders: {
307
+ ...headers,
308
+ ...(inheritedExperimentsContext ? { [EXPERIMENTS_CONTEXT_HEADER]: inheritedExperimentsContext } : {}),
309
+ },
285
310
  }),
286
311
  /**
287
312
  * Sets a new authentication token for all subsequent requests.
@@ -433,6 +458,10 @@ export function createClientFromRequest(request) {
433
458
  }
434
459
  // Prepare additional headers to propagate
435
460
  const additionalHeaders = {};
461
+ const encodedExperiments = request.headers.get(EXPERIMENTS_CONTEXT_HEADER);
462
+ const experimentsContext = readExperimentsContext(encodedExperiments, appId);
463
+ if (experimentsContext && encodedExperiments)
464
+ additionalHeaders[EXPERIMENTS_CONTEXT_HEADER] = encodedExperiments;
436
465
  if (stateHeader) {
437
466
  additionalHeaders["Base44-State"] = stateHeader;
438
467
  }
@@ -453,5 +482,6 @@ export function createClientFromRequest(request) {
453
482
  serviceToken: serviceRoleToken,
454
483
  functionsVersion: functionsVersion !== null && functionsVersion !== void 0 ? functionsVersion : undefined,
455
484
  headers: additionalHeaders,
485
+ experiments: experimentsContext ? { ...experimentsContext, pageUrl: request.url ? new URL(request.url).pathname : "/" } : undefined,
456
486
  });
457
487
  }
@@ -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 {};