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