@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 +42 -12
- package/dist/client.types.d.ts +8 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/modules/analytics-queue.d.ts +12 -0
- package/dist/modules/analytics-queue.js +128 -0
- package/dist/modules/analytics.d.ts +37 -5
- package/dist/modules/analytics.js +99 -154
- package/dist/modules/auth.js +4 -2
- package/dist/modules/experiment-exposures.d.ts +4 -1
- package/dist/modules/experiment-exposures.js +21 -36
- package/dist/modules/experiments-config.types.d.ts +39 -0
- package/dist/modules/experiments-config.types.js +1 -0
- package/dist/modules/experiments-context.d.ts +10 -0
- package/dist/modules/experiments-context.js +44 -0
- package/dist/modules/experiments-evaluator.d.ts +11 -0
- package/dist/modules/experiments-evaluator.js +45 -0
- package/dist/modules/experiments.d.ts +5 -1
- package/dist/modules/experiments.js +21 -37
- package/dist/modules/experiments.types.d.ts +42 -14
- package/dist/utils/fetch-with-auth.js +6 -0
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getSharedInstance } from "../utils/sharedInstance.js";
|
|
2
2
|
import { generateUuid, isReactNative } from "../utils/common.js";
|
|
3
3
|
import { getExperimentsRuntime } from "./experiments-runtime.types.js";
|
|
4
|
+
import { getAnalyticsQueue } from "./analytics-queue.js";
|
|
4
5
|
export const USER_HEARTBEAT_EVENT_NAME = "__user_heartbeat_event__";
|
|
5
6
|
export const ANALYTICS_INITIALIZATION_EVENT_NAME = "__initialization_event__";
|
|
6
7
|
export const ANALYTICS_SESSION_DURATION_EVENT_NAME = "__session_duration_event__";
|
|
@@ -14,111 +15,85 @@ const defaultConfiguration = {
|
|
|
14
15
|
batchSize: 30,
|
|
15
16
|
heartBeatInterval: 60 * 1000,
|
|
16
17
|
};
|
|
17
|
-
///////////////////////////////////////////////
|
|
18
|
-
//// shared queue for analytics events ////
|
|
19
|
-
///////////////////////////////////////////////
|
|
20
18
|
const ANALYTICS_SHARED_STATE_NAME = "analytics";
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
}
|
|
19
|
+
function createAnalyticsState() {
|
|
20
|
+
return {
|
|
21
|
+
isHeartBeatProcessing: false,
|
|
22
|
+
wasInitializationTracked: false,
|
|
23
|
+
sessionContext: null,
|
|
24
|
+
sessionContextPromise: null,
|
|
25
|
+
sessionStartTime: null,
|
|
26
|
+
// Memoized session id for when `localStorage` can't persist one — see
|
|
27
|
+
// getAnalyticsSessionId.
|
|
28
|
+
fallbackSessionId: null,
|
|
29
|
+
config: {
|
|
30
|
+
...defaultConfiguration,
|
|
31
|
+
...getAnalyticsConfigFromUrlParams(),
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const analyticsSharedState = getSharedInstance(ANALYTICS_SHARED_STATE_NAME, createAnalyticsState);
|
|
36
|
+
const clientAnalyticsStates = new WeakMap();
|
|
37
37
|
/** @internal */
|
|
38
|
-
export function
|
|
39
|
-
|
|
38
|
+
export function getAnalyticsState(axiosClient) {
|
|
39
|
+
let state = clientAnalyticsStates.get(axiosClient);
|
|
40
|
+
if (!state) {
|
|
41
|
+
state = createAnalyticsState();
|
|
42
|
+
if (typeof window !== "undefined") {
|
|
43
|
+
state.config = analyticsSharedState.config;
|
|
44
|
+
}
|
|
45
|
+
clientAnalyticsStates.set(axiosClient, state);
|
|
46
|
+
}
|
|
47
|
+
return state;
|
|
40
48
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
/** @internal */
|
|
50
|
+
export function isAnalyticsEnabled(enabled, state = analyticsSharedState) {
|
|
51
|
+
return enabled && state.config.enabled && !isReactNative;
|
|
52
|
+
}
|
|
53
|
+
export const createAnalyticsModule = ({ axiosClient, appId, userAuthModule, enabled, getVisitorId, experimentsContext, }) => {
|
|
54
|
+
const state = getAnalyticsState(axiosClient);
|
|
55
|
+
const automaticState = typeof window === "undefined" ? state : analyticsSharedState;
|
|
56
|
+
const queue = getAnalyticsQueue(axiosClient, appId, state.config);
|
|
44
57
|
// Disable analytics on React Native. It defines `window` but not `document`,
|
|
45
58
|
// so the per-callsite `typeof window` guards below aren't enough to keep it
|
|
46
59
|
// from touching `document` (e.g. `document.referrer` on init). Node/SSR is
|
|
47
60
|
// still handled by those `window` guards, so this doesn't affect it.
|
|
48
|
-
if (!isAnalyticsEnabled(enabled)) {
|
|
61
|
+
if (!isAnalyticsEnabled(enabled, state)) {
|
|
49
62
|
return {
|
|
50
63
|
track: () => { },
|
|
51
64
|
cleanup: () => { },
|
|
52
65
|
};
|
|
53
66
|
}
|
|
54
67
|
let clearHeartBeatProcessor = undefined;
|
|
55
|
-
const trackBatchUrl = `${serverUrl}/api/apps/${appId}/analytics/track/batch`;
|
|
56
|
-
const batchRequestFallback = async (events) => {
|
|
57
|
-
await axiosClient.request({
|
|
58
|
-
method: "POST",
|
|
59
|
-
url: `/apps/${appId}/analytics/track/batch`,
|
|
60
|
-
data: { events },
|
|
61
|
-
});
|
|
62
|
-
};
|
|
63
|
-
// currently disabled, until fully tested //
|
|
64
|
-
const beaconRequest = (events) => {
|
|
65
|
-
try {
|
|
66
|
-
const beaconPayload = JSON.stringify({ events });
|
|
67
|
-
const blob = new Blob([beaconPayload], { type: "application/json" });
|
|
68
|
-
return (typeof navigator === "undefined" ||
|
|
69
|
-
beaconPayload.length > 60000 ||
|
|
70
|
-
!navigator.sendBeacon(trackBatchUrl, blob));
|
|
71
|
-
}
|
|
72
|
-
catch (_a) {
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
|
-
};
|
|
76
|
-
const flush = async (eventsData, options = {}) => {
|
|
77
|
-
if (eventsData.length === 0)
|
|
78
|
-
return;
|
|
79
|
-
const sessionContext_ = await getSessionContext(userAuthModule);
|
|
80
|
-
const events = eventsData.map(transformEventDataToApiRequestData(sessionContext_));
|
|
81
|
-
try {
|
|
82
|
-
if (!options.isBeacon || !beaconRequest(events)) {
|
|
83
|
-
await batchRequestFallback(events);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
catch (_a) {
|
|
87
|
-
// do nothing
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
|
-
const startProcessing = () => {
|
|
91
|
-
startAnalyticsProcessor(flush, {
|
|
92
|
-
throttleTime,
|
|
93
|
-
batchSize,
|
|
94
|
-
});
|
|
95
|
-
};
|
|
96
68
|
const track = (params) => {
|
|
97
|
-
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
69
|
+
var _a, _b;
|
|
100
70
|
const intrinsicData = getEventIntrinsicData();
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
});
|
|
105
|
-
|
|
71
|
+
const visitorId = (_a = getVisitorId === null || getVisitorId === void 0 ? void 0 : getVisitorId()) !== null && _a !== void 0 ? _a : getAnalyticsSessionId(state);
|
|
72
|
+
const authorization = userAuthModule.hasToken() ? axiosClient.defaults.headers.common.Authorization : null;
|
|
73
|
+
const context = getSessionContext(userAuthModule, state);
|
|
74
|
+
const preview = Object.fromEntries(Object.entries((_b = experimentsContext === null || experimentsContext === void 0 ? void 0 : experimentsContext.preview) !== null && _b !== void 0 ? _b : {}).filter(([, value]) => typeof value === "boolean"));
|
|
75
|
+
const properties = { ...params.properties };
|
|
76
|
+
delete properties.__b44_experiment_preview;
|
|
77
|
+
if (Object.keys(preview).length) {
|
|
78
|
+
// Capture now: a queued event must retain its occurrence-time preview.
|
|
79
|
+
properties.__b44_experiment_preview = JSON.stringify(preview);
|
|
80
|
+
}
|
|
81
|
+
const event = {
|
|
82
|
+
event_name: params.eventName,
|
|
83
|
+
timestamp: intrinsicData.timestamp,
|
|
84
|
+
page_url: intrinsicData.pageUrl,
|
|
85
|
+
properties: params.properties || Object.keys(properties).length ? properties : undefined,
|
|
86
|
+
};
|
|
87
|
+
queue.enqueue(context.then((identity) => ({ ...event, ...identity, session_id: visitorId })), typeof authorization === "string" ? authorization : null, context.then((identity) => { var _a; return (_a = identity.user_id) !== null && _a !== void 0 ? _a : null; }));
|
|
106
88
|
};
|
|
107
89
|
const onDocVisible = () => {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
batchSize,
|
|
111
|
-
});
|
|
112
|
-
clearHeartBeatProcessor = startHeartBeatProcessor(track);
|
|
113
|
-
setSessionDurationTimerStart();
|
|
90
|
+
clearHeartBeatProcessor = startHeartBeatProcessor(track, automaticState);
|
|
91
|
+
setSessionDurationTimerStart(automaticState);
|
|
114
92
|
};
|
|
115
93
|
const onDocHidden = () => {
|
|
116
|
-
stopAnalyticsProcessor();
|
|
117
94
|
clearHeartBeatProcessor === null || clearHeartBeatProcessor === void 0 ? void 0 : clearHeartBeatProcessor();
|
|
118
|
-
trackSessionDurationEvent(track);
|
|
119
|
-
|
|
120
|
-
const eventsData = analyticsSharedState.requestsQueue.splice(0);
|
|
121
|
-
flush(eventsData, { isBeacon: true });
|
|
95
|
+
trackSessionDurationEvent(track, automaticState);
|
|
96
|
+
void queue.flush();
|
|
122
97
|
};
|
|
123
98
|
const onVisibilityChange = () => {
|
|
124
99
|
if (typeof window === "undefined")
|
|
@@ -131,18 +106,16 @@ export const createAnalyticsModule = ({ axiosClient, serverUrl, appId, userAuthM
|
|
|
131
106
|
}
|
|
132
107
|
};
|
|
133
108
|
const cleanup = () => {
|
|
134
|
-
|
|
109
|
+
queue.cleanup();
|
|
135
110
|
clearHeartBeatProcessor === null || clearHeartBeatProcessor === void 0 ? void 0 : clearHeartBeatProcessor();
|
|
136
111
|
if (typeof window !== "undefined") {
|
|
137
112
|
window.removeEventListener("visibilitychange", onVisibilityChange);
|
|
138
113
|
}
|
|
139
114
|
};
|
|
140
|
-
// start the flusing process ///
|
|
141
|
-
startProcessing();
|
|
142
115
|
// start the heart beat processor //
|
|
143
|
-
clearHeartBeatProcessor = startHeartBeatProcessor(track);
|
|
116
|
+
clearHeartBeatProcessor = startHeartBeatProcessor(track, automaticState);
|
|
144
117
|
// track the referrer event //
|
|
145
|
-
trackInitializationEvent(track);
|
|
118
|
+
trackInitializationEvent(track, automaticState);
|
|
146
119
|
// start the visibility change listener //
|
|
147
120
|
if (typeof window !== "undefined") {
|
|
148
121
|
window.addEventListener("visibilitychange", onVisibilityChange);
|
|
@@ -152,50 +125,32 @@ export const createAnalyticsModule = ({ axiosClient, serverUrl, appId, userAuthM
|
|
|
152
125
|
cleanup,
|
|
153
126
|
};
|
|
154
127
|
};
|
|
155
|
-
function
|
|
156
|
-
analyticsSharedState.isProcessing = false;
|
|
157
|
-
}
|
|
158
|
-
async function startAnalyticsProcessor(handleTrack, options) {
|
|
159
|
-
if (analyticsSharedState.isProcessing) {
|
|
160
|
-
// only one instance of the analytics processor can be running at a time //
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
analyticsSharedState.isProcessing = true;
|
|
164
|
-
const { throttleTime = 1000, batchSize = 30 } = options !== null && options !== void 0 ? options : {};
|
|
165
|
-
while (analyticsSharedState.isProcessing &&
|
|
166
|
-
analyticsSharedState.requestsQueue.length > 0) {
|
|
167
|
-
const requests = analyticsSharedState.requestsQueue.splice(0, batchSize);
|
|
168
|
-
requests.length && (await handleTrack(requests));
|
|
169
|
-
await new Promise((resolve) => setTimeout(resolve, throttleTime));
|
|
170
|
-
}
|
|
171
|
-
analyticsSharedState.isProcessing = false;
|
|
172
|
-
}
|
|
173
|
-
function startHeartBeatProcessor(track) {
|
|
128
|
+
function startHeartBeatProcessor(track, state) {
|
|
174
129
|
var _a;
|
|
175
130
|
// Browser-only, like the other automatic events here (initialization, session
|
|
176
131
|
// duration, visibility). Outside a browser this timer fired a `me()` every
|
|
177
132
|
// interval for the lifetime of a long-lived server-side client, and kept the
|
|
178
133
|
// Node event loop alive. Explicit `analytics.track()` calls still work.
|
|
179
134
|
if (typeof window === "undefined" ||
|
|
180
|
-
|
|
181
|
-
((_a =
|
|
135
|
+
state.isHeartBeatProcessing ||
|
|
136
|
+
((_a = state.config.heartBeatInterval) !== null && _a !== void 0 ? _a : 0) < 10) {
|
|
182
137
|
return () => { };
|
|
183
138
|
}
|
|
184
|
-
|
|
139
|
+
state.isHeartBeatProcessing = true;
|
|
185
140
|
const interval = setInterval(() => {
|
|
186
141
|
track({ eventName: USER_HEARTBEAT_EVENT_NAME });
|
|
187
|
-
},
|
|
142
|
+
}, state.config.heartBeatInterval);
|
|
188
143
|
return () => {
|
|
189
144
|
clearInterval(interval);
|
|
190
|
-
|
|
145
|
+
state.isHeartBeatProcessing = false;
|
|
191
146
|
};
|
|
192
147
|
}
|
|
193
|
-
function trackInitializationEvent(track) {
|
|
148
|
+
function trackInitializationEvent(track, state) {
|
|
194
149
|
if (typeof window === "undefined" ||
|
|
195
|
-
|
|
150
|
+
state.wasInitializationTracked) {
|
|
196
151
|
return;
|
|
197
152
|
}
|
|
198
|
-
|
|
153
|
+
state.wasInitializationTracked = true;
|
|
199
154
|
track({
|
|
200
155
|
eventName: ANALYTICS_INITIALIZATION_EVENT_NAME,
|
|
201
156
|
properties: {
|
|
@@ -203,20 +158,20 @@ function trackInitializationEvent(track) {
|
|
|
203
158
|
},
|
|
204
159
|
});
|
|
205
160
|
}
|
|
206
|
-
function setSessionDurationTimerStart() {
|
|
161
|
+
function setSessionDurationTimerStart(state) {
|
|
207
162
|
if (typeof window === "undefined" ||
|
|
208
|
-
|
|
163
|
+
state.sessionStartTime !== null) {
|
|
209
164
|
return;
|
|
210
165
|
}
|
|
211
|
-
|
|
166
|
+
state.sessionStartTime = new Date().toISOString();
|
|
212
167
|
}
|
|
213
|
-
function trackSessionDurationEvent(track) {
|
|
168
|
+
function trackSessionDurationEvent(track, state) {
|
|
214
169
|
if (typeof window === "undefined" ||
|
|
215
|
-
|
|
170
|
+
state.sessionStartTime === null)
|
|
216
171
|
return;
|
|
217
172
|
const sessionDuration = new Date().getTime() -
|
|
218
|
-
new Date(
|
|
219
|
-
|
|
173
|
+
new Date(state.sessionStartTime).getTime();
|
|
174
|
+
state.sessionStartTime = null;
|
|
220
175
|
track({
|
|
221
176
|
eventName: ANALYTICS_SESSION_DURATION_EVENT_NAME,
|
|
222
177
|
properties: { sessionDuration },
|
|
@@ -230,16 +185,6 @@ function getEventIntrinsicData() {
|
|
|
230
185
|
pageUrl: typeof window !== "undefined" ? (_b = (_a = window.location) === null || _a === void 0 ? void 0 : _a.pathname) !== null && _b !== void 0 ? _b : null : null,
|
|
231
186
|
};
|
|
232
187
|
}
|
|
233
|
-
function transformEventDataToApiRequestData(sessionContext) {
|
|
234
|
-
return (eventData) => ({
|
|
235
|
-
event_name: eventData.eventName,
|
|
236
|
-
properties: eventData.properties,
|
|
237
|
-
timestamp: eventData.timestamp,
|
|
238
|
-
page_url: eventData.pageUrl,
|
|
239
|
-
...sessionContext,
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
let sessionContextPromise = null;
|
|
243
188
|
/**
|
|
244
189
|
* Clears the memoized analytics session context.
|
|
245
190
|
*
|
|
@@ -250,22 +195,23 @@ let sessionContextPromise = null;
|
|
|
250
195
|
*
|
|
251
196
|
* @internal
|
|
252
197
|
*/
|
|
253
|
-
export function resetAnalyticsSessionContext() {
|
|
254
|
-
|
|
255
|
-
|
|
198
|
+
export function resetAnalyticsSessionContext(axiosClient) {
|
|
199
|
+
const state = axiosClient ? getAnalyticsState(axiosClient) : analyticsSharedState;
|
|
200
|
+
state.sessionContext = null;
|
|
201
|
+
state.sessionContextPromise = null;
|
|
256
202
|
}
|
|
257
|
-
async function getSessionContext(userAuthModule) {
|
|
258
|
-
if (!
|
|
203
|
+
async function getSessionContext(userAuthModule, state) {
|
|
204
|
+
if (!state.sessionContext) {
|
|
259
205
|
// With no token there is no identity to resolve: `me()` can only answer 401,
|
|
260
206
|
// which the browser logs to the console before any handler here sees it. On
|
|
261
207
|
// a public page that request is the sole reason an error appears, so skip
|
|
262
208
|
// it. This is not memoized — a visitor who logs in later must still resolve.
|
|
263
209
|
if (!userAuthModule.hasToken()) {
|
|
264
|
-
return { user_id: null, session_id: getAnalyticsSessionId() };
|
|
210
|
+
return { user_id: null, session_id: getAnalyticsSessionId(state) };
|
|
265
211
|
}
|
|
266
|
-
if (!sessionContextPromise) {
|
|
267
|
-
const sessionId = getAnalyticsSessionId();
|
|
268
|
-
sessionContextPromise = userAuthModule
|
|
212
|
+
if (!state.sessionContextPromise) {
|
|
213
|
+
const sessionId = getAnalyticsSessionId(state);
|
|
214
|
+
state.sessionContextPromise = userAuthModule
|
|
269
215
|
.me()
|
|
270
216
|
.then((user) => ({
|
|
271
217
|
user_id: user.id,
|
|
@@ -276,7 +222,7 @@ async function getSessionContext(userAuthModule) {
|
|
|
276
222
|
session_id: sessionId,
|
|
277
223
|
}));
|
|
278
224
|
}
|
|
279
|
-
const pending = sessionContextPromise;
|
|
225
|
+
const pending = state.sessionContextPromise;
|
|
280
226
|
const context = await pending;
|
|
281
227
|
// Publish only if this lookup is still the current one. A reset that lands
|
|
282
228
|
// while the request is in flight nulls `sessionContextPromise`, and an
|
|
@@ -284,12 +230,12 @@ async function getSessionContext(userAuthModule) {
|
|
|
284
230
|
// for the rest of the session. The awaited value is still returned: these
|
|
285
231
|
// events were queued before the identity changed, so that is who they
|
|
286
232
|
// belong to.
|
|
287
|
-
if (sessionContextPromise === pending) {
|
|
288
|
-
|
|
233
|
+
if (state.sessionContextPromise === pending) {
|
|
234
|
+
state.sessionContext = context;
|
|
289
235
|
}
|
|
290
236
|
return context;
|
|
291
237
|
}
|
|
292
|
-
return
|
|
238
|
+
return state.sessionContext;
|
|
293
239
|
}
|
|
294
240
|
export function getAnalyticsConfigFromUrlParams() {
|
|
295
241
|
// `window.location` is absent on React Native. This runs at module load (via
|
|
@@ -311,19 +257,18 @@ export function getAnalyticsConfigFromUrlParams() {
|
|
|
311
257
|
// return the config object //
|
|
312
258
|
return { enabled: analyticsEnable === "true" };
|
|
313
259
|
}
|
|
314
|
-
//
|
|
315
|
-
|
|
316
|
-
function getFallbackSessionId() {
|
|
260
|
+
// Without persistent storage, keep the id stable within this analytics state.
|
|
261
|
+
function getFallbackSessionId(state) {
|
|
317
262
|
var _a;
|
|
318
|
-
return ((_a =
|
|
263
|
+
return ((_a = state.fallbackSessionId) !== null && _a !== void 0 ? _a : (state.fallbackSessionId = generateUuid()));
|
|
319
264
|
}
|
|
320
|
-
export function getAnalyticsSessionId() {
|
|
265
|
+
export function getAnalyticsSessionId(state = analyticsSharedState) {
|
|
321
266
|
var _a;
|
|
322
267
|
const visitorId = (_a = getExperimentsRuntime()) === null || _a === void 0 ? void 0 : _a.visitorId;
|
|
323
268
|
if (visitorId && visitorId !== "anon")
|
|
324
269
|
return visitorId;
|
|
325
270
|
if (typeof window === "undefined") {
|
|
326
|
-
return getFallbackSessionId();
|
|
271
|
+
return getFallbackSessionId(state);
|
|
327
272
|
}
|
|
328
273
|
try {
|
|
329
274
|
const sessionId = localStorage.getItem(ANALYTICS_SESSION_ID_LOCAL_STORAGE_KEY);
|
|
@@ -335,6 +280,6 @@ export function getAnalyticsSessionId() {
|
|
|
335
280
|
return sessionId;
|
|
336
281
|
}
|
|
337
282
|
catch (_b) {
|
|
338
|
-
return getFallbackSessionId();
|
|
283
|
+
return getFallbackSessionId(state);
|
|
339
284
|
}
|
|
340
285
|
}
|
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(axios);
|
|
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(axios);
|
|
211
211
|
hasAccessToken = true;
|
|
212
212
|
// handle token change for axios clients
|
|
213
213
|
axios.defaults.headers.common["Authorization"] = `Bearer ${token}`;
|
|
@@ -239,6 +239,8 @@ 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 });
|
|
242
244
|
}
|
|
243
245
|
return {
|
|
244
246
|
access_token,
|
|
@@ -1,9 +1,11 @@
|
|
|
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, source, pageUrl, }: {
|
|
4
4
|
axiosClient: AxiosInstance;
|
|
5
5
|
appId: string;
|
|
6
6
|
enabled: boolean;
|
|
7
|
+
source?: "browser" | "backend";
|
|
8
|
+
pageUrl?: string;
|
|
7
9
|
}): {
|
|
8
10
|
track(assignment: {
|
|
9
11
|
experiment_id: string;
|
|
@@ -13,4 +15,5 @@ export declare function createExposureTracker({ axiosClient, appId, enabled, }:
|
|
|
13
15
|
visitorId: string;
|
|
14
16
|
userId: string | null;
|
|
15
17
|
}): void;
|
|
18
|
+
flush: () => Promise<void>;
|
|
16
19
|
};
|
|
@@ -1,45 +1,30 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { v4 as uuid } from "uuid";
|
|
2
|
+
import { getAnalyticsState, isAnalyticsEnabled } from "./analytics.js";
|
|
3
|
+
import { getAnalyticsQueue } from "./analytics-queue.js";
|
|
2
4
|
/** @internal */
|
|
3
|
-
export function createExposureTracker({ axiosClient, appId, enabled, }) {
|
|
4
|
-
const
|
|
5
|
+
export function createExposureTracker({ axiosClient, appId, enabled, source = "browser", pageUrl, }) {
|
|
6
|
+
const state = getAnalyticsState(axiosClient);
|
|
7
|
+
const queue = getAnalyticsQueue(axiosClient, appId, state.config);
|
|
8
|
+
const tracked = new Set();
|
|
5
9
|
return {
|
|
6
10
|
track(assignment, identity) {
|
|
7
|
-
|
|
8
|
-
if (typeof window === "undefined" || !isAnalyticsEnabled(enabled))
|
|
11
|
+
if ((source === "browser" && typeof window === "undefined") || !isAnalyticsEnabled(enabled, state))
|
|
9
12
|
return;
|
|
10
13
|
const { experiment_id, run_version, variant_key } = assignment;
|
|
11
|
-
const key = JSON.stringify([
|
|
12
|
-
|
|
13
|
-
run_version,
|
|
14
|
-
variant_key,
|
|
15
|
-
identity.userId,
|
|
16
|
-
identity.visitorId,
|
|
17
|
-
]);
|
|
18
|
-
if (exposed.has(key))
|
|
14
|
+
const key = JSON.stringify([experiment_id, run_version, variant_key, identity.userId, identity.visitorId]);
|
|
15
|
+
if (tracked.has(key))
|
|
19
16
|
return;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
:
|
|
25
|
-
|
|
26
|
-
.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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));
|
|
17
|
+
tracked.add(key);
|
|
18
|
+
const authorization = identity.userId ? axiosClient.defaults.headers.common.Authorization : null;
|
|
19
|
+
queue.enqueue({
|
|
20
|
+
event_id: uuid(),
|
|
21
|
+
event_name: "__experiment_exposure__",
|
|
22
|
+
timestamp: new Date().toISOString(),
|
|
23
|
+
session_id: identity.visitorId,
|
|
24
|
+
page_url: pageUrl !== null && pageUrl !== void 0 ? pageUrl : (typeof window === "undefined" ? "/" : window.location.pathname),
|
|
25
|
+
properties: { experiment_id, run_version, variant_key, source },
|
|
26
|
+
}, typeof authorization === "string" ? authorization : null, identity.userId);
|
|
43
27
|
},
|
|
28
|
+
flush: queue.flush,
|
|
44
29
|
};
|
|
45
30
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
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;
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
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
|
+
};
|