@base44-preview/sdk 0.8.48-pr.280.7eec9ba → 0.8.48-pr.280.cc2ee4a
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 +12 -42
- package/dist/client.types.d.ts +1 -8
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -1
- package/dist/modules/analytics.d.ts +5 -28
- package/dist/modules/analytics.js +82 -99
- package/dist/modules/auth.js +2 -4
- package/dist/modules/experiment-exposures.d.ts +1 -4
- package/dist/modules/experiment-exposures.js +36 -56
- package/dist/modules/experiments.d.ts +1 -5
- package/dist/modules/experiments.js +37 -21
- package/dist/modules/experiments.types.d.ts +14 -24
- package/dist/utils/fetch-with-auth.js +0 -6
- package/package.json +1 -1
- package/dist/modules/experiments-config.types.d.ts +0 -39
- package/dist/modules/experiments-config.types.js +0 -1
- package/dist/modules/experiments-context.d.ts +0 -10
- package/dist/modules/experiments-context.js +0 -44
- package/dist/modules/experiments-evaluator.d.ts +0 -11
- package/dist/modules/experiments-evaluator.js +0 -45
package/dist/client.js
CHANGED
|
@@ -16,7 +16,6 @@ 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";
|
|
20
19
|
import { createActorsModule, resolveActorsHost, } from "./modules/actors.js";
|
|
21
20
|
/**
|
|
22
21
|
* Creates a Base44 client.
|
|
@@ -56,11 +55,10 @@ import { createActorsModule, resolveActorsHost, } from "./modules/actors.js";
|
|
|
56
55
|
* ```
|
|
57
56
|
*/
|
|
58
57
|
export function createClient(config) {
|
|
59
|
-
var _a, _b, _c, _d, _e
|
|
58
|
+
var _a, _b, _c, _d, _e;
|
|
60
59
|
const { serverUrl = "https://base44.app", appId, analytics, token, serviceToken, requiresAuth = false, appBaseUrl, options, functionsVersion, headers: optionalHeaders, } = config;
|
|
61
60
|
// Normalize appBaseUrl to always be a string (empty if not provided or invalid)
|
|
62
61
|
const normalizedAppBaseUrl = typeof appBaseUrl === "string" ? appBaseUrl : "";
|
|
63
|
-
const experimentsContext = (_a = config.experiments) !== null && _a !== void 0 ? _a : getBrowserExperimentsContext(appId);
|
|
64
62
|
const socketConfig = {
|
|
65
63
|
serverUrl,
|
|
66
64
|
mountPath: "/ws-user-apps/socket.io/",
|
|
@@ -77,14 +75,9 @@ export function createClient(config) {
|
|
|
77
75
|
}
|
|
78
76
|
return socket;
|
|
79
77
|
};
|
|
80
|
-
const { [EXPERIMENTS_CONTEXT_HEADER]: inheritedExperimentsContext, ...requestHeaders } = optionalHeaders !== null && optionalHeaders !== void 0 ? optionalHeaders : {};
|
|
81
78
|
const headers = {
|
|
82
|
-
...
|
|
79
|
+
...optionalHeaders,
|
|
83
80
|
"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
|
-
} : {}),
|
|
88
81
|
};
|
|
89
82
|
const functionHeaders = functionsVersion
|
|
90
83
|
? {
|
|
@@ -130,18 +123,13 @@ export function createClient(config) {
|
|
|
130
123
|
baseURL: `${serverUrl}/api`,
|
|
131
124
|
headers,
|
|
132
125
|
});
|
|
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
|
-
});
|
|
140
126
|
const experiments = createExperimentsModule({
|
|
141
127
|
getAuth: () => userAuthModule,
|
|
142
|
-
trackExposure:
|
|
143
|
-
|
|
144
|
-
|
|
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,
|
|
145
133
|
});
|
|
146
134
|
const userAuthModule = createAuthModule(axiosClient, functionsAxiosClient, appId, {
|
|
147
135
|
appBaseUrl: normalizedAppBaseUrl,
|
|
@@ -159,19 +147,11 @@ export function createClient(config) {
|
|
|
159
147
|
userAuthModule.setToken(accessToken);
|
|
160
148
|
}
|
|
161
149
|
}
|
|
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
|
-
}
|
|
170
150
|
const actorsModule = createActorsModule({
|
|
171
151
|
appId,
|
|
172
152
|
// serverUrl is often relative/empty (same-origin app); the proxy-fallback
|
|
173
153
|
// URL needs an absolute host, so fall back to the page origin.
|
|
174
|
-
host: resolveActorsHost(serverUrl, typeof window !== "undefined" ? (
|
|
154
|
+
host: resolveActorsHost(serverUrl, typeof window !== "undefined" ? (_b = window.location) === null || _b === void 0 ? void 0 : _b.origin : undefined),
|
|
175
155
|
functionsVersion,
|
|
176
156
|
getAuthToken: () => token || getAccessToken(),
|
|
177
157
|
mintConnectionToken: async (actorName, room, connectionId) => {
|
|
@@ -210,7 +190,7 @@ export function createClient(config) {
|
|
|
210
190
|
}
|
|
211
191
|
return headers;
|
|
212
192
|
},
|
|
213
|
-
baseURL: (
|
|
193
|
+
baseURL: (_c = functionsAxiosClient.defaults) === null || _c === void 0 ? void 0 : _c.baseURL,
|
|
214
194
|
}),
|
|
215
195
|
agents: createAgentsModule({
|
|
216
196
|
axios: axiosClient,
|
|
@@ -228,9 +208,7 @@ export function createClient(config) {
|
|
|
228
208
|
serverUrl,
|
|
229
209
|
appId,
|
|
230
210
|
userAuthModule,
|
|
231
|
-
enabled: (
|
|
232
|
-
getVisitorId: experiments.visitorId,
|
|
233
|
-
experimentsContext,
|
|
211
|
+
enabled: (_d = analytics === null || analytics === void 0 ? void 0 : analytics.enabled) !== null && _d !== void 0 ? _d : true,
|
|
234
212
|
}),
|
|
235
213
|
actors: actorsModule.module,
|
|
236
214
|
cleanup: () => {
|
|
@@ -260,7 +238,7 @@ export function createClient(config) {
|
|
|
260
238
|
}
|
|
261
239
|
return headers;
|
|
262
240
|
},
|
|
263
|
-
baseURL: (
|
|
241
|
+
baseURL: (_e = serviceRoleFunctionsAxiosClient.defaults) === null || _e === void 0 ? void 0 : _e.baseURL,
|
|
264
242
|
}),
|
|
265
243
|
agents: createAgentsModule({
|
|
266
244
|
axios: serviceRoleAxiosClient,
|
|
@@ -303,10 +281,7 @@ export function createClient(config) {
|
|
|
303
281
|
appId: String(appId),
|
|
304
282
|
serverUrl,
|
|
305
283
|
functionsVersion,
|
|
306
|
-
platformHeaders:
|
|
307
|
-
...headers,
|
|
308
|
-
...(inheritedExperimentsContext ? { [EXPERIMENTS_CONTEXT_HEADER]: inheritedExperimentsContext } : {}),
|
|
309
|
-
},
|
|
284
|
+
platformHeaders: optionalHeaders,
|
|
310
285
|
}),
|
|
311
286
|
/**
|
|
312
287
|
* Sets a new authentication token for all subsequent requests.
|
|
@@ -458,10 +433,6 @@ export function createClientFromRequest(request) {
|
|
|
458
433
|
}
|
|
459
434
|
// Prepare additional headers to propagate
|
|
460
435
|
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;
|
|
465
436
|
if (stateHeader) {
|
|
466
437
|
additionalHeaders["Base44-State"] = stateHeader;
|
|
467
438
|
}
|
|
@@ -482,6 +453,5 @@ export function createClientFromRequest(request) {
|
|
|
482
453
|
serviceToken: serviceRoleToken,
|
|
483
454
|
functionsVersion: functionsVersion !== null && functionsVersion !== void 0 ? functionsVersion : undefined,
|
|
484
455
|
headers: additionalHeaders,
|
|
485
|
-
experiments: experimentsContext ? { ...experimentsContext, pageUrl: request.url ? new URL(request.url).pathname : "/" } : undefined,
|
|
486
456
|
});
|
|
487
457
|
}
|
package/dist/client.types.d.ts
CHANGED
|
@@ -10,7 +10,6 @@ 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";
|
|
14
13
|
import type { ActorsModule } from "./modules/actors.types.js";
|
|
15
14
|
import type { FetchWithAuthInit } from "./utils/fetch-with-auth.js";
|
|
16
15
|
/**
|
|
@@ -81,12 +80,6 @@ export interface CreateClientConfig {
|
|
|
81
80
|
* Omit this option to preserve the default analytics behavior.
|
|
82
81
|
*/
|
|
83
82
|
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;
|
|
90
83
|
/**
|
|
91
84
|
* User authentication token. Used to authenticate as a specific user.
|
|
92
85
|
*
|
|
@@ -142,7 +135,7 @@ export interface Base44Client {
|
|
|
142
135
|
connectors: UserConnectorsModule;
|
|
143
136
|
/** {@link EntitiesModule | Entities module} for CRUD operations on your data models. */
|
|
144
137
|
entities: EntitiesModule;
|
|
145
|
-
/** {@link ExperimentsModule | Experiments module} for
|
|
138
|
+
/** {@link ExperimentsModule | Experiments module} for browser feature flags and exposures. */
|
|
146
139
|
experiments: ExperimentsModule;
|
|
147
140
|
/** {@link FunctionsModule | Functions module} for invoking custom backend functions. */
|
|
148
141
|
functions: FunctionsModule;
|
package/dist/index.d.ts
CHANGED
|
@@ -4,8 +4,6 @@ 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";
|
|
9
7
|
export type { ExperimentsModule, ExperimentsSnapshot, } from "./modules/experiments.types.js";
|
|
10
8
|
export type { DeleteManyResult, DeleteResult, EntitiesModule, EntityFilterOperators, EntityFilterQuery, EntityFilterValue, EntityHandler, EntityRecord, EntityTypeRegistry, ImportResult, RealtimeEventType, RealtimeEvent, RealtimeCallback, SortField, UpdateManyResult, } from "./modules/entities.types.js";
|
|
11
9
|
export type { AuthModule, LoginResponse, RegisterParams, VerifyOtpParams, ChangePasswordParams, ResetPasswordParams, User, } from "./modules/auth.types.js";
|
package/dist/index.js
CHANGED
|
@@ -3,5 +3,4 @@ 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";
|
|
7
6
|
export { Actor } from "./actor.js";
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { AxiosInstance } from "axios";
|
|
2
|
-
import { TrackEventParams,
|
|
2
|
+
import { TrackEventParams, AnalyticsModuleOptions } from "./analytics.types";
|
|
3
3
|
import type { InternalAuthModule } from "./auth.types";
|
|
4
|
-
import type { ExperimentsContext } from "./experiments-config.types.js";
|
|
5
4
|
export declare const USER_HEARTBEAT_EVENT_NAME = "__user_heartbeat_event__";
|
|
6
5
|
export declare const ANALYTICS_INITIALIZATION_EVENT_NAME = "__initialization_event__";
|
|
7
6
|
export declare const ANALYTICS_SESSION_DURATION_EVENT_NAME = "__session_duration_event__";
|
|
@@ -13,22 +12,10 @@ export interface AnalyticsModuleArgs {
|
|
|
13
12
|
appId: string;
|
|
14
13
|
userAuthModule: InternalAuthModule;
|
|
15
14
|
enabled: boolean;
|
|
16
|
-
getVisitorId?: () => string | undefined;
|
|
17
|
-
experimentsContext?: ExperimentsContext;
|
|
18
15
|
}
|
|
19
16
|
/** @internal */
|
|
20
|
-
export declare function isAnalyticsEnabled(enabled: boolean
|
|
21
|
-
|
|
22
|
-
isProcessing: boolean;
|
|
23
|
-
isHeartBeatProcessing: boolean;
|
|
24
|
-
wasInitializationTracked: boolean;
|
|
25
|
-
sessionContext: SessionContext | null;
|
|
26
|
-
sessionContextPromise: Promise<SessionContext> | null;
|
|
27
|
-
sessionStartTime: string | null;
|
|
28
|
-
fallbackSessionId: string | null;
|
|
29
|
-
config: Required<AnalyticsModuleOptions>;
|
|
30
|
-
}): boolean;
|
|
31
|
-
export declare const createAnalyticsModule: ({ axiosClient, serverUrl, appId, userAuthModule, enabled, getVisitorId, experimentsContext, }: AnalyticsModuleArgs) => {
|
|
17
|
+
export declare function isAnalyticsEnabled(enabled: boolean): boolean;
|
|
18
|
+
export declare const createAnalyticsModule: ({ axiosClient, serverUrl, appId, userAuthModule, enabled, }: AnalyticsModuleArgs) => {
|
|
32
19
|
track: (params: TrackEventParams) => void;
|
|
33
20
|
cleanup: () => void;
|
|
34
21
|
};
|
|
@@ -42,16 +29,6 @@ export declare const createAnalyticsModule: ({ axiosClient, serverUrl, appId, us
|
|
|
42
29
|
*
|
|
43
30
|
* @internal
|
|
44
31
|
*/
|
|
45
|
-
export declare function resetAnalyticsSessionContext(
|
|
32
|
+
export declare function resetAnalyticsSessionContext(): void;
|
|
46
33
|
export declare function getAnalyticsConfigFromUrlParams(): AnalyticsModuleOptions | undefined;
|
|
47
|
-
export declare function getAnalyticsSessionId(
|
|
48
|
-
requestsQueue: TrackEventData[];
|
|
49
|
-
isProcessing: boolean;
|
|
50
|
-
isHeartBeatProcessing: boolean;
|
|
51
|
-
wasInitializationTracked: boolean;
|
|
52
|
-
sessionContext: SessionContext | null;
|
|
53
|
-
sessionContextPromise: Promise<SessionContext> | null;
|
|
54
|
-
sessionStartTime: string | null;
|
|
55
|
-
fallbackSessionId: string | null;
|
|
56
|
-
config: Required<AnalyticsModuleOptions>;
|
|
57
|
-
}): string;
|
|
34
|
+
export declare function getAnalyticsSessionId(): string;
|
|
@@ -18,41 +18,34 @@ const defaultConfiguration = {
|
|
|
18
18
|
//// shared queue for analytics events ////
|
|
19
19
|
///////////////////////////////////////////////
|
|
20
20
|
const ANALYTICS_SHARED_STATE_NAME = "analytics";
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
const analyticsSharedState = getSharedInstance(ANALYTICS_SHARED_STATE_NAME, createAnalyticsState);
|
|
40
|
-
const serverAnalyticsStates = new WeakMap();
|
|
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
|
+
}));
|
|
41
37
|
/** @internal */
|
|
42
|
-
export function isAnalyticsEnabled(enabled
|
|
43
|
-
return enabled &&
|
|
38
|
+
export function isAnalyticsEnabled(enabled) {
|
|
39
|
+
return enabled && analyticsSharedState.config.enabled && !isReactNative;
|
|
44
40
|
}
|
|
45
|
-
export const createAnalyticsModule = ({ axiosClient, serverUrl, appId, userAuthModule, enabled,
|
|
46
|
-
const state = typeof window === "undefined" ? createAnalyticsState() : analyticsSharedState;
|
|
47
|
-
if (typeof window === "undefined")
|
|
48
|
-
serverAnalyticsStates.set(axiosClient, state);
|
|
41
|
+
export const createAnalyticsModule = ({ axiosClient, serverUrl, appId, userAuthModule, enabled, }) => {
|
|
49
42
|
// prevent overflow of events //
|
|
50
|
-
const { maxQueueSize, throttleTime, batchSize } =
|
|
43
|
+
const { maxQueueSize, throttleTime, batchSize } = analyticsSharedState.config;
|
|
51
44
|
// Disable analytics on React Native. It defines `window` but not `document`,
|
|
52
45
|
// so the per-callsite `typeof window` guards below aren't enough to keep it
|
|
53
46
|
// from touching `document` (e.g. `document.referrer` on init). Node/SSR is
|
|
54
47
|
// still handled by those `window` guards, so this doesn't affect it.
|
|
55
|
-
if (!isAnalyticsEnabled(enabled
|
|
48
|
+
if (!isAnalyticsEnabled(enabled)) {
|
|
56
49
|
return {
|
|
57
50
|
track: () => { },
|
|
58
51
|
cleanup: () => { },
|
|
@@ -81,17 +74,16 @@ export const createAnalyticsModule = ({ axiosClient, serverUrl, appId, userAuthM
|
|
|
81
74
|
}
|
|
82
75
|
};
|
|
83
76
|
const flush = async (eventsData, options = {}) => {
|
|
84
|
-
var _a;
|
|
85
77
|
if (eventsData.length === 0)
|
|
86
78
|
return;
|
|
87
|
-
const sessionContext_ = await getSessionContext(userAuthModule
|
|
88
|
-
const events = eventsData.map(transformEventDataToApiRequestData(
|
|
79
|
+
const sessionContext_ = await getSessionContext(userAuthModule);
|
|
80
|
+
const events = eventsData.map(transformEventDataToApiRequestData(sessionContext_));
|
|
89
81
|
try {
|
|
90
82
|
if (!options.isBeacon || !beaconRequest(events)) {
|
|
91
83
|
await batchRequestFallback(events);
|
|
92
84
|
}
|
|
93
85
|
}
|
|
94
|
-
catch (
|
|
86
|
+
catch (_a) {
|
|
95
87
|
// do nothing
|
|
96
88
|
}
|
|
97
89
|
};
|
|
@@ -99,25 +91,16 @@ export const createAnalyticsModule = ({ axiosClient, serverUrl, appId, userAuthM
|
|
|
99
91
|
startAnalyticsProcessor(flush, {
|
|
100
92
|
throttleTime,
|
|
101
93
|
batchSize,
|
|
102
|
-
}
|
|
94
|
+
});
|
|
103
95
|
};
|
|
104
96
|
const track = (params) => {
|
|
105
|
-
|
|
106
|
-
if (state.requestsQueue.length >= maxQueueSize) {
|
|
97
|
+
if (analyticsSharedState.requestsQueue.length >= maxQueueSize) {
|
|
107
98
|
return;
|
|
108
99
|
}
|
|
109
100
|
const intrinsicData = getEventIntrinsicData();
|
|
110
|
-
|
|
111
|
-
const properties = { ...params.properties };
|
|
112
|
-
delete properties.__b44_experiment_preview;
|
|
113
|
-
if (Object.keys(preview).length) {
|
|
114
|
-
// Capture now: a queued event must retain its occurrence-time preview.
|
|
115
|
-
properties.__b44_experiment_preview = JSON.stringify(preview);
|
|
116
|
-
}
|
|
117
|
-
state.requestsQueue.push({
|
|
101
|
+
analyticsSharedState.requestsQueue.push({
|
|
118
102
|
...params,
|
|
119
103
|
...intrinsicData,
|
|
120
|
-
properties: params.properties || Object.keys(properties).length ? properties : undefined,
|
|
121
104
|
});
|
|
122
105
|
startProcessing();
|
|
123
106
|
};
|
|
@@ -125,16 +108,16 @@ export const createAnalyticsModule = ({ axiosClient, serverUrl, appId, userAuthM
|
|
|
125
108
|
startAnalyticsProcessor(flush, {
|
|
126
109
|
throttleTime,
|
|
127
110
|
batchSize,
|
|
128
|
-
}
|
|
129
|
-
clearHeartBeatProcessor = startHeartBeatProcessor(track
|
|
130
|
-
setSessionDurationTimerStart(
|
|
111
|
+
});
|
|
112
|
+
clearHeartBeatProcessor = startHeartBeatProcessor(track);
|
|
113
|
+
setSessionDurationTimerStart();
|
|
131
114
|
};
|
|
132
115
|
const onDocHidden = () => {
|
|
133
|
-
stopAnalyticsProcessor(
|
|
116
|
+
stopAnalyticsProcessor();
|
|
134
117
|
clearHeartBeatProcessor === null || clearHeartBeatProcessor === void 0 ? void 0 : clearHeartBeatProcessor();
|
|
135
|
-
trackSessionDurationEvent(track
|
|
118
|
+
trackSessionDurationEvent(track);
|
|
136
119
|
// flush entire queue on visibility change and hope for the best //
|
|
137
|
-
const eventsData =
|
|
120
|
+
const eventsData = analyticsSharedState.requestsQueue.splice(0);
|
|
138
121
|
flush(eventsData, { isBeacon: true });
|
|
139
122
|
};
|
|
140
123
|
const onVisibilityChange = () => {
|
|
@@ -148,7 +131,7 @@ export const createAnalyticsModule = ({ axiosClient, serverUrl, appId, userAuthM
|
|
|
148
131
|
}
|
|
149
132
|
};
|
|
150
133
|
const cleanup = () => {
|
|
151
|
-
stopAnalyticsProcessor(
|
|
134
|
+
stopAnalyticsProcessor();
|
|
152
135
|
clearHeartBeatProcessor === null || clearHeartBeatProcessor === void 0 ? void 0 : clearHeartBeatProcessor();
|
|
153
136
|
if (typeof window !== "undefined") {
|
|
154
137
|
window.removeEventListener("visibilitychange", onVisibilityChange);
|
|
@@ -157,9 +140,9 @@ export const createAnalyticsModule = ({ axiosClient, serverUrl, appId, userAuthM
|
|
|
157
140
|
// start the flusing process ///
|
|
158
141
|
startProcessing();
|
|
159
142
|
// start the heart beat processor //
|
|
160
|
-
clearHeartBeatProcessor = startHeartBeatProcessor(track
|
|
143
|
+
clearHeartBeatProcessor = startHeartBeatProcessor(track);
|
|
161
144
|
// track the referrer event //
|
|
162
|
-
trackInitializationEvent(track
|
|
145
|
+
trackInitializationEvent(track);
|
|
163
146
|
// start the visibility change listener //
|
|
164
147
|
if (typeof window !== "undefined") {
|
|
165
148
|
window.addEventListener("visibilitychange", onVisibilityChange);
|
|
@@ -169,50 +152,50 @@ export const createAnalyticsModule = ({ axiosClient, serverUrl, appId, userAuthM
|
|
|
169
152
|
cleanup,
|
|
170
153
|
};
|
|
171
154
|
};
|
|
172
|
-
function stopAnalyticsProcessor(
|
|
173
|
-
|
|
155
|
+
function stopAnalyticsProcessor() {
|
|
156
|
+
analyticsSharedState.isProcessing = false;
|
|
174
157
|
}
|
|
175
|
-
async function startAnalyticsProcessor(handleTrack, options
|
|
176
|
-
if (
|
|
158
|
+
async function startAnalyticsProcessor(handleTrack, options) {
|
|
159
|
+
if (analyticsSharedState.isProcessing) {
|
|
177
160
|
// only one instance of the analytics processor can be running at a time //
|
|
178
161
|
return;
|
|
179
162
|
}
|
|
180
|
-
|
|
163
|
+
analyticsSharedState.isProcessing = true;
|
|
181
164
|
const { throttleTime = 1000, batchSize = 30 } = options !== null && options !== void 0 ? options : {};
|
|
182
|
-
while (
|
|
183
|
-
|
|
184
|
-
const requests =
|
|
165
|
+
while (analyticsSharedState.isProcessing &&
|
|
166
|
+
analyticsSharedState.requestsQueue.length > 0) {
|
|
167
|
+
const requests = analyticsSharedState.requestsQueue.splice(0, batchSize);
|
|
185
168
|
requests.length && (await handleTrack(requests));
|
|
186
169
|
await new Promise((resolve) => setTimeout(resolve, throttleTime));
|
|
187
170
|
}
|
|
188
|
-
|
|
171
|
+
analyticsSharedState.isProcessing = false;
|
|
189
172
|
}
|
|
190
|
-
function startHeartBeatProcessor(track
|
|
173
|
+
function startHeartBeatProcessor(track) {
|
|
191
174
|
var _a;
|
|
192
175
|
// Browser-only, like the other automatic events here (initialization, session
|
|
193
176
|
// duration, visibility). Outside a browser this timer fired a `me()` every
|
|
194
177
|
// interval for the lifetime of a long-lived server-side client, and kept the
|
|
195
178
|
// Node event loop alive. Explicit `analytics.track()` calls still work.
|
|
196
179
|
if (typeof window === "undefined" ||
|
|
197
|
-
|
|
198
|
-
((_a =
|
|
180
|
+
analyticsSharedState.isHeartBeatProcessing ||
|
|
181
|
+
((_a = analyticsSharedState.config.heartBeatInterval) !== null && _a !== void 0 ? _a : 0) < 10) {
|
|
199
182
|
return () => { };
|
|
200
183
|
}
|
|
201
|
-
|
|
184
|
+
analyticsSharedState.isHeartBeatProcessing = true;
|
|
202
185
|
const interval = setInterval(() => {
|
|
203
186
|
track({ eventName: USER_HEARTBEAT_EVENT_NAME });
|
|
204
|
-
},
|
|
187
|
+
}, analyticsSharedState.config.heartBeatInterval);
|
|
205
188
|
return () => {
|
|
206
189
|
clearInterval(interval);
|
|
207
|
-
|
|
190
|
+
analyticsSharedState.isHeartBeatProcessing = false;
|
|
208
191
|
};
|
|
209
192
|
}
|
|
210
|
-
function trackInitializationEvent(track
|
|
193
|
+
function trackInitializationEvent(track) {
|
|
211
194
|
if (typeof window === "undefined" ||
|
|
212
|
-
|
|
195
|
+
analyticsSharedState.wasInitializationTracked) {
|
|
213
196
|
return;
|
|
214
197
|
}
|
|
215
|
-
|
|
198
|
+
analyticsSharedState.wasInitializationTracked = true;
|
|
216
199
|
track({
|
|
217
200
|
eventName: ANALYTICS_INITIALIZATION_EVENT_NAME,
|
|
218
201
|
properties: {
|
|
@@ -220,20 +203,20 @@ function trackInitializationEvent(track, state) {
|
|
|
220
203
|
},
|
|
221
204
|
});
|
|
222
205
|
}
|
|
223
|
-
function setSessionDurationTimerStart(
|
|
206
|
+
function setSessionDurationTimerStart() {
|
|
224
207
|
if (typeof window === "undefined" ||
|
|
225
|
-
|
|
208
|
+
analyticsSharedState.sessionStartTime !== null) {
|
|
226
209
|
return;
|
|
227
210
|
}
|
|
228
|
-
|
|
211
|
+
analyticsSharedState.sessionStartTime = new Date().toISOString();
|
|
229
212
|
}
|
|
230
|
-
function trackSessionDurationEvent(track
|
|
213
|
+
function trackSessionDurationEvent(track) {
|
|
231
214
|
if (typeof window === "undefined" ||
|
|
232
|
-
|
|
215
|
+
analyticsSharedState.sessionStartTime === null)
|
|
233
216
|
return;
|
|
234
217
|
const sessionDuration = new Date().getTime() -
|
|
235
|
-
new Date(
|
|
236
|
-
|
|
218
|
+
new Date(analyticsSharedState.sessionStartTime).getTime();
|
|
219
|
+
analyticsSharedState.sessionStartTime = null;
|
|
237
220
|
track({
|
|
238
221
|
eventName: ANALYTICS_SESSION_DURATION_EVENT_NAME,
|
|
239
222
|
properties: { sessionDuration },
|
|
@@ -256,6 +239,7 @@ function transformEventDataToApiRequestData(sessionContext) {
|
|
|
256
239
|
...sessionContext,
|
|
257
240
|
});
|
|
258
241
|
}
|
|
242
|
+
let sessionContextPromise = null;
|
|
259
243
|
/**
|
|
260
244
|
* Clears the memoized analytics session context.
|
|
261
245
|
*
|
|
@@ -266,24 +250,22 @@ function transformEventDataToApiRequestData(sessionContext) {
|
|
|
266
250
|
*
|
|
267
251
|
* @internal
|
|
268
252
|
*/
|
|
269
|
-
export function resetAnalyticsSessionContext(
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
state.sessionContext = null;
|
|
273
|
-
state.sessionContextPromise = null;
|
|
253
|
+
export function resetAnalyticsSessionContext() {
|
|
254
|
+
analyticsSharedState.sessionContext = null;
|
|
255
|
+
sessionContextPromise = null;
|
|
274
256
|
}
|
|
275
|
-
async function getSessionContext(userAuthModule
|
|
276
|
-
if (!
|
|
257
|
+
async function getSessionContext(userAuthModule) {
|
|
258
|
+
if (!analyticsSharedState.sessionContext) {
|
|
277
259
|
// With no token there is no identity to resolve: `me()` can only answer 401,
|
|
278
260
|
// which the browser logs to the console before any handler here sees it. On
|
|
279
261
|
// a public page that request is the sole reason an error appears, so skip
|
|
280
262
|
// it. This is not memoized — a visitor who logs in later must still resolve.
|
|
281
263
|
if (!userAuthModule.hasToken()) {
|
|
282
|
-
return { user_id: null, session_id: getAnalyticsSessionId(
|
|
264
|
+
return { user_id: null, session_id: getAnalyticsSessionId() };
|
|
283
265
|
}
|
|
284
|
-
if (!
|
|
285
|
-
const sessionId = getAnalyticsSessionId(
|
|
286
|
-
|
|
266
|
+
if (!sessionContextPromise) {
|
|
267
|
+
const sessionId = getAnalyticsSessionId();
|
|
268
|
+
sessionContextPromise = userAuthModule
|
|
287
269
|
.me()
|
|
288
270
|
.then((user) => ({
|
|
289
271
|
user_id: user.id,
|
|
@@ -294,7 +276,7 @@ async function getSessionContext(userAuthModule, state) {
|
|
|
294
276
|
session_id: sessionId,
|
|
295
277
|
}));
|
|
296
278
|
}
|
|
297
|
-
const pending =
|
|
279
|
+
const pending = sessionContextPromise;
|
|
298
280
|
const context = await pending;
|
|
299
281
|
// Publish only if this lookup is still the current one. A reset that lands
|
|
300
282
|
// while the request is in flight nulls `sessionContextPromise`, and an
|
|
@@ -302,12 +284,12 @@ async function getSessionContext(userAuthModule, state) {
|
|
|
302
284
|
// for the rest of the session. The awaited value is still returned: these
|
|
303
285
|
// events were queued before the identity changed, so that is who they
|
|
304
286
|
// belong to.
|
|
305
|
-
if (
|
|
306
|
-
|
|
287
|
+
if (sessionContextPromise === pending) {
|
|
288
|
+
analyticsSharedState.sessionContext = context;
|
|
307
289
|
}
|
|
308
290
|
return context;
|
|
309
291
|
}
|
|
310
|
-
return
|
|
292
|
+
return analyticsSharedState.sessionContext;
|
|
311
293
|
}
|
|
312
294
|
export function getAnalyticsConfigFromUrlParams() {
|
|
313
295
|
// `window.location` is absent on React Native. This runs at module load (via
|
|
@@ -329,18 +311,19 @@ export function getAnalyticsConfigFromUrlParams() {
|
|
|
329
311
|
// return the config object //
|
|
330
312
|
return { enabled: analyticsEnable === "true" };
|
|
331
313
|
}
|
|
332
|
-
//
|
|
333
|
-
|
|
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() {
|
|
334
317
|
var _a;
|
|
335
|
-
return ((_a =
|
|
318
|
+
return ((_a = analyticsSharedState.fallbackSessionId) !== null && _a !== void 0 ? _a : (analyticsSharedState.fallbackSessionId = generateUuid()));
|
|
336
319
|
}
|
|
337
|
-
export function getAnalyticsSessionId(
|
|
320
|
+
export function getAnalyticsSessionId() {
|
|
338
321
|
var _a;
|
|
339
322
|
const visitorId = (_a = getExperimentsRuntime()) === null || _a === void 0 ? void 0 : _a.visitorId;
|
|
340
323
|
if (visitorId && visitorId !== "anon")
|
|
341
324
|
return visitorId;
|
|
342
325
|
if (typeof window === "undefined") {
|
|
343
|
-
return getFallbackSessionId(
|
|
326
|
+
return getFallbackSessionId();
|
|
344
327
|
}
|
|
345
328
|
try {
|
|
346
329
|
const sessionId = localStorage.getItem(ANALYTICS_SESSION_ID_LOCAL_STORAGE_KEY);
|
|
@@ -352,6 +335,6 @@ export function getAnalyticsSessionId(state = analyticsSharedState) {
|
|
|
352
335
|
return sessionId;
|
|
353
336
|
}
|
|
354
337
|
catch (_b) {
|
|
355
|
-
return getFallbackSessionId(
|
|
338
|
+
return getFallbackSessionId();
|
|
356
339
|
}
|
|
357
340
|
}
|
package/dist/modules/auth.js
CHANGED
|
@@ -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();
|
|
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();
|
|
211
211
|
hasAccessToken = true;
|
|
212
212
|
// handle token change for axios clients
|
|
213
213
|
axios.defaults.headers.common["Authorization"] = `Bearer ${token}`;
|
|
@@ -239,8 +239,6 @@ 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 });
|
|
244
242
|
}
|
|
245
243
|
return {
|
|
246
244
|
access_token,
|
|
@@ -1,11 +1,9 @@
|
|
|
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, }: {
|
|
4
4
|
axiosClient: AxiosInstance;
|
|
5
5
|
appId: string;
|
|
6
6
|
enabled: boolean;
|
|
7
|
-
source?: "browser" | "backend";
|
|
8
|
-
pageUrl?: string;
|
|
9
7
|
}): {
|
|
10
8
|
track(assignment: {
|
|
11
9
|
experiment_id: string;
|
|
@@ -15,5 +13,4 @@ export declare function createExposureTracker({ axiosClient, appId, enabled, sou
|
|
|
15
13
|
visitorId: string;
|
|
16
14
|
userId: string | null;
|
|
17
15
|
}): void;
|
|
18
|
-
flush(): Promise<void>;
|
|
19
16
|
};
|
|
@@ -1,65 +1,45 @@
|
|
|
1
|
-
import { v4 as uuid } from "uuid";
|
|
2
1
|
import { isAnalyticsEnabled } from "./analytics.js";
|
|
3
2
|
/** @internal */
|
|
4
|
-
export function createExposureTracker({ axiosClient, appId, enabled,
|
|
5
|
-
const
|
|
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
|
-
}
|
|
3
|
+
export function createExposureTracker({ axiosClient, appId, enabled, }) {
|
|
4
|
+
const exposed = new Set();
|
|
35
5
|
return {
|
|
36
6
|
track(assignment, identity) {
|
|
37
|
-
|
|
7
|
+
var _a;
|
|
8
|
+
if (typeof window === "undefined" || !isAnalyticsEnabled(enabled))
|
|
38
9
|
return;
|
|
39
10
|
const { experiment_id, run_version, variant_key } = assignment;
|
|
40
|
-
const key = JSON.stringify([
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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))
|
|
19
|
+
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));
|
|
63
43
|
},
|
|
64
44
|
};
|
|
65
45
|
}
|
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import type { AuthState, InternalAuthModule } from "./auth.types.js";
|
|
2
2
|
import type { ExperimentsModule } from "./experiments.types.js";
|
|
3
3
|
import type { createExposureTracker } from "./experiment-exposures.js";
|
|
4
|
-
import type { ExperimentsContext } from "./experiments-config.types.js";
|
|
5
4
|
/** @internal */
|
|
6
|
-
export declare function createExperimentsModule({ getAuth, trackExposure,
|
|
5
|
+
export declare function createExperimentsModule({ getAuth, trackExposure, }: {
|
|
7
6
|
getAuth: () => InternalAuthModule;
|
|
8
7
|
trackExposure: ReturnType<typeof createExposureTracker>["track"];
|
|
9
|
-
flushExposures?: () => Promise<void>;
|
|
10
|
-
context?: ExperimentsContext;
|
|
11
8
|
}): {
|
|
12
9
|
module: ExperimentsModule;
|
|
13
10
|
onAuthStateChange: (next: AuthState) => void;
|
|
14
|
-
visitorId: () => string | undefined;
|
|
15
11
|
cleanup(): void;
|
|
16
12
|
};
|
|
@@ -1,28 +1,19 @@
|
|
|
1
1
|
import { getExperimentsRuntime, } from "./experiments-runtime.types.js";
|
|
2
|
-
import { createExperimentsRuntime } from "./experiments-context.js";
|
|
3
2
|
const EMPTY = Object.freeze({
|
|
4
3
|
flags: Object.freeze({}),
|
|
5
4
|
isLoading: false,
|
|
6
5
|
});
|
|
7
6
|
/** @internal */
|
|
8
|
-
export function createExperimentsModule({ getAuth, trackExposure,
|
|
9
|
-
|
|
10
|
-
let
|
|
11
|
-
let state = context
|
|
12
|
-
? context.identity.status === "pending" ? { status: "pending" }
|
|
13
|
-
: context.identity.userId ? { status: "authenticated", userId: context.identity.userId }
|
|
14
|
-
: { status: "anonymous" }
|
|
15
|
-
: undefined;
|
|
7
|
+
export function createExperimentsModule({ getAuth, trackExposure, }) {
|
|
8
|
+
let runtime;
|
|
9
|
+
let state;
|
|
16
10
|
let snapshot = EMPTY;
|
|
17
11
|
let active = false;
|
|
18
12
|
let disposed = false;
|
|
13
|
+
let generation = 0;
|
|
14
|
+
let pending;
|
|
19
15
|
const listeners = new Set();
|
|
20
16
|
const readyWaiters = new Set();
|
|
21
|
-
const initial = (_a = context === null || context === void 0 ? void 0 : context.serverSnapshot) !== null && _a !== void 0 ? _a : (context ? {
|
|
22
|
-
flags: context.identity.status === "pending" ? {} : runtime.flags,
|
|
23
|
-
isLoading: context.identity.status === "pending",
|
|
24
|
-
} : EMPTY);
|
|
25
|
-
const serverSnapshot = Object.freeze({ ...initial, flags: Object.freeze({ ...initial.flags }) });
|
|
26
17
|
function settleReady() {
|
|
27
18
|
if (snapshot.isLoading)
|
|
28
19
|
return;
|
|
@@ -62,12 +53,25 @@ export function createExperimentsModule({ getAuth, trackExposure, flushExposures
|
|
|
62
53
|
}
|
|
63
54
|
publish();
|
|
64
55
|
}
|
|
56
|
+
function resolveIdentity() {
|
|
57
|
+
if (!runtime || pending || disposed)
|
|
58
|
+
return;
|
|
59
|
+
state = { status: "pending" };
|
|
60
|
+
applyIdentity();
|
|
61
|
+
const currentGeneration = generation;
|
|
62
|
+
pending = getAuth()
|
|
63
|
+
.me()
|
|
64
|
+
.then(() => { }, () => { })
|
|
65
|
+
.finally(() => {
|
|
66
|
+
if (currentGeneration === generation)
|
|
67
|
+
pending = undefined;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
65
70
|
function activate() {
|
|
66
71
|
if (disposed)
|
|
67
72
|
return;
|
|
68
73
|
active = true;
|
|
69
|
-
|
|
70
|
-
runtime = getExperimentsRuntime();
|
|
74
|
+
runtime = getExperimentsRuntime();
|
|
71
75
|
if (!runtime) {
|
|
72
76
|
publish();
|
|
73
77
|
return;
|
|
@@ -77,16 +81,23 @@ export function createExperimentsModule({ getAuth, trackExposure, flushExposures
|
|
|
77
81
|
? { status: "pending" }
|
|
78
82
|
: { status: "anonymous" };
|
|
79
83
|
applyIdentity();
|
|
84
|
+
if (state.status === "pending")
|
|
85
|
+
resolveIdentity();
|
|
80
86
|
}
|
|
81
87
|
function onAuthStateChange(next) {
|
|
82
88
|
if (disposed)
|
|
83
89
|
return;
|
|
84
90
|
state = next;
|
|
91
|
+
if (next.status === "pending" || next.status === "anonymous") {
|
|
92
|
+
generation++;
|
|
93
|
+
pending = undefined;
|
|
94
|
+
}
|
|
85
95
|
if (!active)
|
|
86
96
|
return;
|
|
87
|
-
|
|
88
|
-
runtime = getExperimentsRuntime();
|
|
97
|
+
runtime = getExperimentsRuntime();
|
|
89
98
|
applyIdentity();
|
|
99
|
+
if (next.status === "pending")
|
|
100
|
+
resolveIdentity();
|
|
90
101
|
}
|
|
91
102
|
const module = {
|
|
92
103
|
isEnabled(flagKey, fallback = false) {
|
|
@@ -102,7 +113,6 @@ export function createExperimentsModule({ getAuth, trackExposure, flushExposures
|
|
|
102
113
|
activate();
|
|
103
114
|
return snapshot;
|
|
104
115
|
},
|
|
105
|
-
getServerSnapshot: () => serverSnapshot,
|
|
106
116
|
subscribe(listener) {
|
|
107
117
|
activate();
|
|
108
118
|
if (!disposed)
|
|
@@ -113,18 +123,24 @@ export function createExperimentsModule({ getAuth, trackExposure, flushExposures
|
|
|
113
123
|
},
|
|
114
124
|
async ready() {
|
|
115
125
|
activate();
|
|
126
|
+
if ((state === null || state === void 0 ? void 0 : state.status) === "error") {
|
|
127
|
+
// Wait for auth.me() to release its shared, failed request before retrying.
|
|
128
|
+
await pending;
|
|
129
|
+
if ((state === null || state === void 0 ? void 0 : state.status) === "error")
|
|
130
|
+
resolveIdentity();
|
|
131
|
+
}
|
|
116
132
|
if (snapshot.isLoading)
|
|
117
133
|
return new Promise((resolve) => readyWaiters.add(resolve));
|
|
118
134
|
return snapshot;
|
|
119
135
|
},
|
|
120
|
-
flush: flushExposures,
|
|
121
136
|
};
|
|
122
137
|
return {
|
|
123
138
|
module,
|
|
124
139
|
onAuthStateChange,
|
|
125
|
-
visitorId: () => runtime === null || runtime === void 0 ? void 0 : runtime.visitorId,
|
|
126
140
|
cleanup() {
|
|
127
141
|
disposed = true;
|
|
142
|
+
generation++;
|
|
143
|
+
pending = undefined;
|
|
128
144
|
runtime = undefined;
|
|
129
145
|
snapshot = EMPTY;
|
|
130
146
|
settleReady();
|
|
@@ -6,7 +6,7 @@ export interface ExperimentsSnapshot {
|
|
|
6
6
|
readonly isLoading: boolean;
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
9
|
+
* Reads feature flags evaluated by the Base44 browser runtime.
|
|
10
10
|
*
|
|
11
11
|
* - Reads flags and reports experiment exposures when a flag is used.
|
|
12
12
|
* - Synchronizes assignments with this client's SDK login, token changes, and logout.
|
|
@@ -14,10 +14,9 @@ export interface ExperimentsSnapshot {
|
|
|
14
14
|
*
|
|
15
15
|
* Available as `base44.experiments` for anonymous and signed-in app visitors,
|
|
16
16
|
* not in service role mode. Use one client for the app whose runtime is on the page.
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* common auth bootstrap must supply a resolved identity before mounting the app.
|
|
17
|
+
* The platform must inject the Experiments runtime before this module can evaluate
|
|
18
|
+
* flags. Without it, including on servers and Workers, reads return their fallback;
|
|
19
|
+
* this module does not provide server-side evaluation or hydration guarantees.
|
|
21
20
|
* Goal conversions use the existing {@link AnalyticsModule | analytics module}.
|
|
22
21
|
* Visitor-keyed conversions share the injected runtime's visitor ID. When browser
|
|
23
22
|
* storage is blocked, the platform must supply a unique per-page ID; attribution
|
|
@@ -25,17 +24,17 @@ export interface ExperimentsSnapshot {
|
|
|
25
24
|
*/
|
|
26
25
|
export interface ExperimentsModule {
|
|
27
26
|
/**
|
|
28
|
-
* Reads a flag and
|
|
27
|
+
* Reads a flag and reports a best-effort exposure for its current assignment.
|
|
29
28
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
29
|
+
* The first use resolves identity through {@link AuthModule.me | auth.me()}
|
|
30
|
+
* when the client has a token. Reads return the fallback while identity is
|
|
31
|
+
* pending or could not be resolved. Await {@link ExperimentsModule.ready | ready()}
|
|
32
|
+
* or subscribe to updates before displaying authenticated variants.
|
|
33
33
|
*
|
|
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
|
|
38
|
-
* with the same event ID, timestamp and credentials. Await flush() on servers.
|
|
37
|
+
* experiment run, variant and identity, and retry only on a later read after failure.
|
|
39
38
|
*
|
|
40
39
|
* @param flagKey - Feature flag key defined in your app.
|
|
41
40
|
* @param fallback - Value for an unavailable flag or unresolved identity. Defaults to `false`.
|
|
@@ -50,7 +49,7 @@ export interface ExperimentsModule {
|
|
|
50
49
|
/**
|
|
51
50
|
* Returns the current flags and identity-loading state without tracking exposures.
|
|
52
51
|
*
|
|
53
|
-
*
|
|
52
|
+
* Starts lazy identity resolution if needed. The returned object retains its
|
|
54
53
|
* reference until its values change, for use with external-store subscriptions.
|
|
55
54
|
* Use {@link ExperimentsModule.isEnabled | isEnabled()} at the feature boundary
|
|
56
55
|
* to record exposure rather than displaying a variant directly from this snapshot.
|
|
@@ -62,8 +61,6 @@ export interface ExperimentsModule {
|
|
|
62
61
|
* ```
|
|
63
62
|
*/
|
|
64
63
|
getSnapshot(): ExperimentsSnapshot;
|
|
65
|
-
/** Immutable initial platform snapshot for matching server render and hydration. */
|
|
66
|
-
getServerSnapshot(): ExperimentsSnapshot;
|
|
67
64
|
/**
|
|
68
65
|
* Listens for flag or loading-state changes caused by this client's SDK auth flows.
|
|
69
66
|
*
|
|
@@ -82,10 +79,10 @@ export interface ExperimentsModule {
|
|
|
82
79
|
*/
|
|
83
80
|
subscribe(listener: () => void): () => void;
|
|
84
81
|
/**
|
|
85
|
-
* Waits for the
|
|
82
|
+
* Waits for the current identity lookup, including a token change during that lookup.
|
|
86
83
|
*
|
|
87
|
-
* Resolves with empty flags after an identity lookup failure
|
|
88
|
-
*
|
|
84
|
+
* Resolves with empty flags after an identity lookup failure; calling again retries
|
|
85
|
+
* the lookup. Missing runtimes resolve immediately. This does not wait for a future
|
|
89
86
|
* runtime injection or for exposure delivery, and never records an exposure itself.
|
|
90
87
|
*
|
|
91
88
|
* @returns A snapshot after the current identity lookup settles.
|
|
@@ -96,11 +93,4 @@ export interface ExperimentsModule {
|
|
|
96
93
|
* ```
|
|
97
94
|
*/
|
|
98
95
|
ready(): Promise<ExperimentsSnapshot>;
|
|
99
|
-
/**
|
|
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.
|
|
104
|
-
*/
|
|
105
|
-
flush(): Promise<void>;
|
|
106
96
|
}
|
|
@@ -29,7 +29,6 @@ export function createFetchWithAuth({ axios, serviceRoleAxios, appId, serverUrl,
|
|
|
29
29
|
? header
|
|
30
30
|
: null;
|
|
31
31
|
};
|
|
32
|
-
const contextAuthorization = bearer(axios);
|
|
33
32
|
return async function fetchWithAuth(path, init = {}) {
|
|
34
33
|
assertOwnOriginPath(path);
|
|
35
34
|
const { fetch: transport = fetch, ...requestInit } = init;
|
|
@@ -50,11 +49,6 @@ export function createFetchWithAuth({ axios, serviceRoleAxios, appId, serverUrl,
|
|
|
50
49
|
inherit("Base44-Functions-Version", functionsVersion);
|
|
51
50
|
inherit("Base44-State", inherited.get("Base44-State"));
|
|
52
51
|
inherit("X-Data-Env", inherited.get("X-Data-Env"));
|
|
53
|
-
inherit("Base44-Visitor-Id", inherited.get("Base44-Visitor-Id"));
|
|
54
|
-
inherit("Base44-Experiment-Preview", inherited.get("Base44-Experiment-Preview"));
|
|
55
|
-
if (headers.get("Authorization") === contextAuthorization) {
|
|
56
|
-
inherit("Base44-Experiments-Context", inherited.get("Base44-Experiments-Context"));
|
|
57
|
-
}
|
|
58
52
|
// The path is passed through untouched: resolving it here would need a
|
|
59
53
|
// document, and a root-relative path is already what a runtime that
|
|
60
54
|
// dispatches in-process (Nitro's `fetch`) expects. `host` is deliberately
|
package/package.json
CHANGED
|
@@ -1,39 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,10 +0,0 @@
|
|
|
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;
|
|
@@ -1,44 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
function bucket(parts) {
|
|
2
|
-
let hash = 0x811c9dc5;
|
|
3
|
-
for (const byte of new TextEncoder().encode(parts.join(":"))) {
|
|
4
|
-
hash = Math.imul(hash ^ byte, 0x01000193) >>> 0;
|
|
5
|
-
}
|
|
6
|
-
return hash % 100;
|
|
7
|
-
}
|
|
8
|
-
/**
|
|
9
|
-
* Evaluates flags locally without storage, network, clock, or browser globals.
|
|
10
|
-
* The same config and identity always produce the same assignments.
|
|
11
|
-
* This controls presentation, never authorization or access to data.
|
|
12
|
-
*/
|
|
13
|
-
export function evaluateExperiments(config, identity, preview = {}) {
|
|
14
|
-
const flags = Object.fromEntries(config.flags.map((flag) => [
|
|
15
|
-
flag.key,
|
|
16
|
-
bucket(["rollout", config.app_id, flag.key, identity.visitorId]) < flag.rollout_percentage,
|
|
17
|
-
]));
|
|
18
|
-
const assignments = [];
|
|
19
|
-
for (const experiment of config.experiments) {
|
|
20
|
-
if (Object.prototype.hasOwnProperty.call(preview, experiment.flag_key))
|
|
21
|
-
continue;
|
|
22
|
-
const key = experiment.assign_by === "user" ? identity.userId : identity.visitorId;
|
|
23
|
-
if (!key || bucket(["enroll", config.app_id, experiment.id, experiment.run_version, key]) >= experiment.traffic_allocation)
|
|
24
|
-
continue;
|
|
25
|
-
const value = bucket(["variant", config.app_id, experiment.id, experiment.run_version, key]);
|
|
26
|
-
let total = 0;
|
|
27
|
-
let variant = experiment.variants[experiment.variants.length - 1];
|
|
28
|
-
for (const candidate of experiment.variants) {
|
|
29
|
-
total += candidate.weight;
|
|
30
|
-
if (value < total) {
|
|
31
|
-
variant = candidate;
|
|
32
|
-
break;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
flags[experiment.flag_key] = variant.value;
|
|
36
|
-
assignments.push({
|
|
37
|
-
experiment_id: experiment.id,
|
|
38
|
-
flag_key: experiment.flag_key,
|
|
39
|
-
run_version: experiment.run_version,
|
|
40
|
-
variant_key: variant.key,
|
|
41
|
-
preview: false,
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
return { flags: { ...flags, ...preview }, assignments };
|
|
45
|
-
}
|