Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions scripts/mintlify-post-processing/appended-articles.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"interfaces/ExperimentsModule": [
"interfaces/ExperimentsSnapshot"
],
"interfaces/ConnectorsModule": [
"type-aliases/ConnectorIntegrationType",
"interfaces/ConnectorIntegrationTypeRegistry",
Expand Down
2 changes: 2 additions & 0 deletions scripts/mintlify-post-processing/types-to-expose.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
"EntityHandler",
"EntityRecord",
"EntityTypeRegistry",
"ExperimentsModule",
"ExperimentsSnapshot",
"FunctionName",
"FunctionNameRegistry",
"FunctionsModule",
Expand Down
47 changes: 45 additions & 2 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ import type {
CreateClientOptions,
} from "./client.types.js";
import { createAnalyticsModule } from "./modules/analytics.js";
import { createExperimentsModule } from "./modules/experiments.js";
import { createExposureTracker } from "./modules/experiment-exposures.js";
import { EXPERIMENTS_CONTEXT_HEADER, getBrowserExperimentsContext, readExperimentsContext } from "./modules/experiments-context.js";
import {
createActorsModule,
resolveActorsHost,
Expand Down Expand Up @@ -90,6 +93,7 @@ export function createClient(config: CreateClientConfig): Base44Client {

// Normalize appBaseUrl to always be a string (empty if not provided or invalid)
const normalizedAppBaseUrl = typeof appBaseUrl === "string" ? appBaseUrl : "";
const experimentsContext = config.experiments ?? getBrowserExperimentsContext(appId);

const socketConfig: RoomsSocketConfig = {
serverUrl,
Expand All @@ -110,9 +114,14 @@ export function createClient(config: CreateClientConfig): Base44Client {
return socket;
};

const { [EXPERIMENTS_CONTEXT_HEADER]: inheritedExperimentsContext, ...requestHeaders } = optionalHeaders ?? {};
const headers = {
...optionalHeaders,
...requestHeaders,
"X-App-Id": String(appId),
...(experimentsContext ? {
"Base44-Visitor-Id": experimentsContext.identity.visitorId,
"Base44-Experiment-Preview": JSON.stringify(experimentsContext.preview ?? {}),
} : {}),
};

const functionHeaders = functionsVersion
Expand Down Expand Up @@ -166,6 +175,20 @@ export function createClient(config: CreateClientConfig): Base44Client {
headers,
});

const exposureTracker = createExposureTracker({
axiosClient,
appId,
enabled: analytics?.enabled ?? true,
source: typeof window === "undefined" ? "backend" : "browser",
pageUrl: experimentsContext?.pageUrl,
});
const experiments = createExperimentsModule({
getAuth: () => userAuthModule,
trackExposure: exposureTracker.track,
flushExposures: exposureTracker.flush,
context: experimentsContext,
});

const userAuthModule = createAuthModule(
axiosClient,
functionsAxiosClient,
Expand All @@ -174,6 +197,7 @@ export function createClient(config: CreateClientConfig): Base44Client {
appBaseUrl: normalizedAppBaseUrl,
serverUrl,
token,
onAuthStateChange: experiments.onAuthStateChange,
}
);

Expand All @@ -187,6 +211,14 @@ export function createClient(config: CreateClientConfig): Base44Client {
userAuthModule.setToken(accessToken);
}
}
if (experimentsContext) {
const { userId, status } = experimentsContext.identity;
// The document's cookie identity may differ from this client's localStorage token.
const needsClientIdentity = typeof window !== "undefined" && userAuthModule.hasToken() &&
experimentsContext.config.experiments.some((experiment) => experiment.assign_by === "user");
experiments.onAuthStateChange(status === "pending" || needsClientIdentity ? { status: "pending" } :
userId ? { status: "authenticated", userId } : { status: "anonymous" });
}

const actorsModule = createActorsModule({
appId,
Expand Down Expand Up @@ -228,6 +260,7 @@ export function createClient(config: CreateClientConfig): Base44Client {
integrations: createIntegrationsModule(axiosClient, appId),
connectors: createUserConnectorsModule(axiosClient, appId),
auth: userAuthModule,
experiments: experiments.module,
functions: createFunctionsModule(functionsAxiosClient, appId, {
getAuthHeaders: () => {
const headers: Record<string, string> = {};
Expand Down Expand Up @@ -257,10 +290,13 @@ export function createClient(config: CreateClientConfig): Base44Client {
appId,
userAuthModule,
enabled: analytics?.enabled ?? true,
getVisitorId: experiments.visitorId,
experimentsContext,
}),
actors: actorsModule.module,
cleanup: () => {
userModules.analytics.cleanup();
experiments.cleanup();
actorsModule.closeAll();
if (socket) {
socket.disconnect();
Expand Down Expand Up @@ -331,7 +367,10 @@ export function createClient(config: CreateClientConfig): Base44Client {
appId: String(appId),
serverUrl,
functionsVersion,
platformHeaders: optionalHeaders,
platformHeaders: {
...headers,
...(inheritedExperimentsContext ? { [EXPERIMENTS_CONTEXT_HEADER]: inheritedExperimentsContext } : {}),
},
}),

/**
Expand Down Expand Up @@ -507,6 +546,9 @@ export function createClientFromRequest(request: Request): Base44Client {

// Prepare additional headers to propagate
const additionalHeaders: Record<string, string> = {};
const encodedExperiments = request.headers.get(EXPERIMENTS_CONTEXT_HEADER);
const experimentsContext = readExperimentsContext(encodedExperiments, appId);
if (experimentsContext && encodedExperiments) additionalHeaders[EXPERIMENTS_CONTEXT_HEADER] = encodedExperiments;
if (stateHeader) {
additionalHeaders["Base44-State"] = stateHeader;
}
Expand All @@ -528,5 +570,6 @@ export function createClientFromRequest(request: Request): Base44Client {
serviceToken: serviceRoleToken,
functionsVersion: functionsVersion ?? undefined,
headers: additionalHeaders,
experiments: experimentsContext ? { ...experimentsContext, pageUrl: request.url ? new URL(request.url).pathname : "/" } : undefined,
});
}
16 changes: 13 additions & 3 deletions src/client.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import type { AiGatewayModule } from "./modules/ai-gateway.types.js";
import type { AppLogsModule } from "./modules/app-logs.types.js";
import type { AppModule } from "./modules/app.types.js";
import type { AnalyticsModule } from "./modules/analytics.types.js";
import type { ExperimentsModule } from "./modules/experiments.types.js";
import type { ExperimentsContext } from "./modules/experiments-config.types.js";
import type { ActorsModule } from "./modules/actors.types.js";
import type { FetchWithAuthInit } from "./utils/fetch-with-auth.js";

Expand Down Expand Up @@ -44,9 +46,9 @@ export interface CreateClientAnalyticsConfig {
/**
* Whether app analytics is enabled for this client.
*
* When disabled, automatic analytics and calls to `analytics.track()` are
* no-ops. The SDK does not create an analytics session identifier, start
* heartbeat timers, or send analytics requests.
* When disabled, automatic analytics, experiment exposures and calls to
* `analytics.track()` are no-ops. The SDK does not create an analytics session
* identifier, start heartbeat timers, or send analytics requests.
*
* @defaultValue `true`
*/
Expand Down Expand Up @@ -85,6 +87,12 @@ export interface CreateClientConfig {
* Omit this option to preserve the default analytics behavior.
*/
analytics?: CreateClientAnalyticsConfig;
/**
* Platform-validated context for local flag evaluation. Request-scoped on servers.
* Automatically read from the platform bootstrap in browsers and trusted headers
* by createClientFromRequest(). Not an authorization credential.
*/
experiments?: ExperimentsContext;
/**
* User authentication token. Used to authenticate as a specific user.
*
Expand Down Expand Up @@ -141,6 +149,8 @@ export interface Base44Client {
connectors: UserConnectorsModule;
/** {@link EntitiesModule | Entities module} for CRUD operations on your data models. */
entities: EntitiesModule;
/** {@link ExperimentsModule | Experiments module} for local feature flags and exposures. */
experiments: ExperimentsModule;
/** {@link FunctionsModule | Functions module} for invoking custom backend functions. */
functions: FunctionsModule;
/** {@link IntegrationsModule | Integrations module} for calling pre-built integration endpoints. */
Expand Down
7 changes: 7 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,15 @@ export type {
};

export * from "./types.js";
export { evaluateExperiments } from "./modules/experiments-evaluator.js";
export type { ExperimentsConfig, ExperimentsContext, ExperimentsIdentity } from "./modules/experiments-config.types.js";

// Module types
export type {
ExperimentsModule,
ExperimentsSnapshot,
} from "./modules/experiments.types.js";

export type {
DeleteManyResult,
DeleteResult,
Expand Down
Loading
Loading