@base44-preview/sdk 0.8.48-pr.280.7eec9ba → 0.8.48-pr.280.b620aa9
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/modules/analytics-queue.d.ts +12 -0
- package/dist/modules/analytics-queue.js +128 -0
- package/dist/modules/analytics.d.ts +15 -6
- package/dist/modules/analytics.js +37 -109
- package/dist/modules/experiment-exposures.d.ts +1 -1
- package/dist/modules/experiment-exposures.js +19 -54
- package/dist/modules/experiments.types.d.ts +25 -7
- package/package.json +1 -1
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AxiosInstance } from "axios";
|
|
2
|
+
import type { AnalyticsApiRequestData, AnalyticsModuleOptions } from "./analytics.types.js";
|
|
3
|
+
type Event = AnalyticsApiRequestData & {
|
|
4
|
+
event_id?: string;
|
|
5
|
+
};
|
|
6
|
+
/** @internal One transport queue per client, shared by goals and exposures. */
|
|
7
|
+
export declare function getAnalyticsQueue(axiosClient: AxiosInstance, appId: string, config: AnalyticsModuleOptions): {
|
|
8
|
+
enqueue(event: Event | Promise<Event>, authorization: string | null, userId: string | null | Promise<string | null>): void;
|
|
9
|
+
flush(): Promise<void>;
|
|
10
|
+
cleanup(): void;
|
|
11
|
+
};
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const DELIVERY_BUDGET_MS = 5000;
|
|
2
|
+
const queues = new WeakMap();
|
|
3
|
+
/** @internal One transport queue per client, shared by goals and exposures. */
|
|
4
|
+
export function getAnalyticsQueue(axiosClient, appId, config) {
|
|
5
|
+
let queue = queues.get(axiosClient);
|
|
6
|
+
if (!queue) {
|
|
7
|
+
queue = createAnalyticsQueue(axiosClient, appId, config);
|
|
8
|
+
queues.set(axiosClient, queue);
|
|
9
|
+
}
|
|
10
|
+
return queue;
|
|
11
|
+
}
|
|
12
|
+
function createAnalyticsQueue(axiosClient, appId, config) {
|
|
13
|
+
const entries = [];
|
|
14
|
+
const pending = new Set();
|
|
15
|
+
let timer;
|
|
16
|
+
function deliver(batch) {
|
|
17
|
+
const controller = new AbortController();
|
|
18
|
+
const deadlineAt = Date.now() + DELIVERY_BUDGET_MS;
|
|
19
|
+
const deadline = new Promise((resolve) => {
|
|
20
|
+
controller.signal.addEventListener("abort", () => resolve(), { once: true });
|
|
21
|
+
});
|
|
22
|
+
const timeout = setTimeout(() => controller.abort(), DELIVERY_BUDGET_MS);
|
|
23
|
+
function send(prepared) {
|
|
24
|
+
var _a;
|
|
25
|
+
const groups = new Map();
|
|
26
|
+
for (const entry of prepared) {
|
|
27
|
+
const key = JSON.stringify([entry.authorization, entry.userId, entry.event.session_id]);
|
|
28
|
+
const group = (_a = groups.get(key)) !== null && _a !== void 0 ? _a : [];
|
|
29
|
+
group.push(entry);
|
|
30
|
+
groups.set(key, group);
|
|
31
|
+
}
|
|
32
|
+
return Promise.all([...groups.values()].map(async (group) => {
|
|
33
|
+
var _a, _b;
|
|
34
|
+
const events = group.map(({ event }) => event);
|
|
35
|
+
const exposures = events.filter((event) => event.event_name === "__experiment_exposure__");
|
|
36
|
+
const attempts = exposures.length ? 3 : 1;
|
|
37
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
38
|
+
try {
|
|
39
|
+
if (controller.signal.aborted)
|
|
40
|
+
return;
|
|
41
|
+
await axiosClient.request({
|
|
42
|
+
method: "POST", url: `/apps/${appId}/analytics/track/batch`,
|
|
43
|
+
headers: { Authorization: group[0].authorization },
|
|
44
|
+
// Ordinary goals have no backend deduplication and remain single-attempt.
|
|
45
|
+
data: { events: attempt === 0 ? events : exposures },
|
|
46
|
+
timeout: Math.max(1, deadlineAt - Date.now()), signal: controller.signal,
|
|
47
|
+
});
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
const status = (_b = (_a = error.response) === null || _a === void 0 ? void 0 : _a.status) !== null && _b !== void 0 ? _b : error.status;
|
|
52
|
+
if (controller.signal.aborted || attempt === attempts - 1 ||
|
|
53
|
+
(status !== undefined && (status < 500 || status >= 600)))
|
|
54
|
+
return;
|
|
55
|
+
await new Promise((resolve) => setTimeout(resolve, attempt === 0 ? 100 : 500));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
const delivery = (async () => {
|
|
61
|
+
const ready = [];
|
|
62
|
+
const requests = [];
|
|
63
|
+
let wake = () => { };
|
|
64
|
+
let preparedAll = false;
|
|
65
|
+
const preparation = Promise.all(batch.map(async (entry) => {
|
|
66
|
+
const [event, userId] = await Promise.all([entry.event, entry.userId]);
|
|
67
|
+
if (event && !controller.signal.aborted)
|
|
68
|
+
ready.push({ event, userId, authorization: entry.authorization });
|
|
69
|
+
wake();
|
|
70
|
+
})).then(() => { preparedAll = true; wake(); });
|
|
71
|
+
while (!preparedAll || ready.length) {
|
|
72
|
+
if (!ready.length && !preparedAll)
|
|
73
|
+
await Promise.race([
|
|
74
|
+
new Promise((resolve) => { wake = resolve; }), deadline,
|
|
75
|
+
]);
|
|
76
|
+
if (controller.signal.aborted)
|
|
77
|
+
return;
|
|
78
|
+
// Coalesce this turn's resolved identities without waiting for unrelated auth I/O.
|
|
79
|
+
let turnTimer;
|
|
80
|
+
await Promise.race([preparation, new Promise((resolve) => { turnTimer = setTimeout(resolve, 0); })]);
|
|
81
|
+
clearTimeout(turnTimer);
|
|
82
|
+
if (ready.length)
|
|
83
|
+
requests.push(send(ready.splice(0)));
|
|
84
|
+
}
|
|
85
|
+
await Promise.all(requests);
|
|
86
|
+
})();
|
|
87
|
+
// Identity lookup and transports that ignore cancellation must also be bounded.
|
|
88
|
+
const settlement = Promise.race([delivery, deadline]).catch(() => { }).finally(() => {
|
|
89
|
+
clearTimeout(timeout);
|
|
90
|
+
controller.abort();
|
|
91
|
+
pending.delete(settlement);
|
|
92
|
+
});
|
|
93
|
+
pending.add(settlement);
|
|
94
|
+
}
|
|
95
|
+
function schedule() {
|
|
96
|
+
var _a;
|
|
97
|
+
if (timer || entries.length === 0)
|
|
98
|
+
return;
|
|
99
|
+
timer = setTimeout(() => {
|
|
100
|
+
var _a;
|
|
101
|
+
timer = undefined;
|
|
102
|
+
deliver(entries.splice(0, (_a = config.batchSize) !== null && _a !== void 0 ? _a : 30));
|
|
103
|
+
schedule();
|
|
104
|
+
}, (_a = config.throttleTime) !== null && _a !== void 0 ? _a : 1000);
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
enqueue(event, authorization, userId) {
|
|
108
|
+
var _a;
|
|
109
|
+
if (entries.length >= ((_a = config.maxQueueSize) !== null && _a !== void 0 ? _a : 1000))
|
|
110
|
+
return;
|
|
111
|
+
entries.push({ event: Promise.resolve(event).catch(() => undefined), authorization,
|
|
112
|
+
userId: Promise.resolve(userId).catch(() => null) });
|
|
113
|
+
schedule();
|
|
114
|
+
},
|
|
115
|
+
async flush() {
|
|
116
|
+
var _a;
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
timer = undefined;
|
|
119
|
+
while (entries.length)
|
|
120
|
+
deliver(entries.splice(0, (_a = config.batchSize) !== null && _a !== void 0 ? _a : 30));
|
|
121
|
+
await Promise.all([...pending]);
|
|
122
|
+
},
|
|
123
|
+
cleanup() {
|
|
124
|
+
clearTimeout(timer);
|
|
125
|
+
timer = undefined;
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AxiosInstance } from "axios";
|
|
2
|
-
import { TrackEventParams,
|
|
2
|
+
import { TrackEventParams, AnalyticsModuleOptions, SessionContext } from "./analytics.types";
|
|
3
3
|
import type { InternalAuthModule } from "./auth.types";
|
|
4
4
|
import type { ExperimentsContext } from "./experiments-config.types.js";
|
|
5
5
|
export declare const USER_HEARTBEAT_EVENT_NAME = "__user_heartbeat_event__";
|
|
@@ -7,6 +7,18 @@ export declare const ANALYTICS_INITIALIZATION_EVENT_NAME = "__initialization_eve
|
|
|
7
7
|
export declare const ANALYTICS_SESSION_DURATION_EVENT_NAME = "__session_duration_event__";
|
|
8
8
|
export declare const ANALYTICS_CONFIG_ENABLE_URL_PARAM_KEY = "analytics-enable";
|
|
9
9
|
export declare const ANALYTICS_SESSION_ID_LOCAL_STORAGE_KEY = "base44_analytics_session_id";
|
|
10
|
+
declare function createAnalyticsState(): {
|
|
11
|
+
isHeartBeatProcessing: boolean;
|
|
12
|
+
wasInitializationTracked: boolean;
|
|
13
|
+
sessionContext: SessionContext | null;
|
|
14
|
+
sessionContextPromise: Promise<SessionContext> | null;
|
|
15
|
+
sessionStartTime: string | null;
|
|
16
|
+
fallbackSessionId: string | null;
|
|
17
|
+
config: Required<AnalyticsModuleOptions>;
|
|
18
|
+
};
|
|
19
|
+
type AnalyticsState = ReturnType<typeof createAnalyticsState>;
|
|
20
|
+
/** @internal */
|
|
21
|
+
export declare function getAnalyticsState(axiosClient: AxiosInstance): AnalyticsState;
|
|
10
22
|
export interface AnalyticsModuleArgs {
|
|
11
23
|
axiosClient: AxiosInstance;
|
|
12
24
|
serverUrl: string;
|
|
@@ -18,8 +30,6 @@ export interface AnalyticsModuleArgs {
|
|
|
18
30
|
}
|
|
19
31
|
/** @internal */
|
|
20
32
|
export declare function isAnalyticsEnabled(enabled: boolean, state?: {
|
|
21
|
-
requestsQueue: TrackEventData[];
|
|
22
|
-
isProcessing: boolean;
|
|
23
33
|
isHeartBeatProcessing: boolean;
|
|
24
34
|
wasInitializationTracked: boolean;
|
|
25
35
|
sessionContext: SessionContext | null;
|
|
@@ -28,7 +38,7 @@ export declare function isAnalyticsEnabled(enabled: boolean, state?: {
|
|
|
28
38
|
fallbackSessionId: string | null;
|
|
29
39
|
config: Required<AnalyticsModuleOptions>;
|
|
30
40
|
}): boolean;
|
|
31
|
-
export declare const createAnalyticsModule: ({ axiosClient,
|
|
41
|
+
export declare const createAnalyticsModule: ({ axiosClient, appId, userAuthModule, enabled, getVisitorId, experimentsContext, }: AnalyticsModuleArgs) => {
|
|
32
42
|
track: (params: TrackEventParams) => void;
|
|
33
43
|
cleanup: () => void;
|
|
34
44
|
};
|
|
@@ -45,8 +55,6 @@ export declare const createAnalyticsModule: ({ axiosClient, serverUrl, appId, us
|
|
|
45
55
|
export declare function resetAnalyticsSessionContext(axiosClient?: AxiosInstance): void;
|
|
46
56
|
export declare function getAnalyticsConfigFromUrlParams(): AnalyticsModuleOptions | undefined;
|
|
47
57
|
export declare function getAnalyticsSessionId(state?: {
|
|
48
|
-
requestsQueue: TrackEventData[];
|
|
49
|
-
isProcessing: boolean;
|
|
50
58
|
isHeartBeatProcessing: boolean;
|
|
51
59
|
wasInitializationTracked: boolean;
|
|
52
60
|
sessionContext: SessionContext | null;
|
|
@@ -55,3 +63,4 @@ export declare function getAnalyticsSessionId(state?: {
|
|
|
55
63
|
fallbackSessionId: string | null;
|
|
56
64
|
config: Required<AnalyticsModuleOptions>;
|
|
57
65
|
}): string;
|
|
66
|
+
export {};
|
|
@@ -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,14 +15,9 @@ 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
19
|
function createAnalyticsState() {
|
|
22
20
|
return {
|
|
23
|
-
requestsQueue: [],
|
|
24
|
-
isProcessing: false,
|
|
25
21
|
isHeartBeatProcessing: false,
|
|
26
22
|
wasInitializationTracked: false,
|
|
27
23
|
sessionContext: null,
|
|
@@ -37,17 +33,27 @@ function createAnalyticsState() {
|
|
|
37
33
|
};
|
|
38
34
|
}
|
|
39
35
|
const analyticsSharedState = getSharedInstance(ANALYTICS_SHARED_STATE_NAME, createAnalyticsState);
|
|
40
|
-
const
|
|
36
|
+
const clientAnalyticsStates = new WeakMap();
|
|
37
|
+
/** @internal */
|
|
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;
|
|
48
|
+
}
|
|
41
49
|
/** @internal */
|
|
42
50
|
export function isAnalyticsEnabled(enabled, state = analyticsSharedState) {
|
|
43
51
|
return enabled && state.config.enabled && !isReactNative;
|
|
44
52
|
}
|
|
45
|
-
export const createAnalyticsModule = ({ axiosClient,
|
|
46
|
-
const state =
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
// prevent overflow of events //
|
|
50
|
-
const { maxQueueSize, throttleTime, batchSize } = state.config;
|
|
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);
|
|
51
57
|
// Disable analytics on React Native. It defines `window` but not `document`,
|
|
52
58
|
// so the per-callsite `typeof window` guards below aren't enough to keep it
|
|
53
59
|
// from touching `document` (e.g. `document.referrer` on init). Node/SSR is
|
|
@@ -59,83 +65,35 @@ export const createAnalyticsModule = ({ axiosClient, serverUrl, appId, userAuthM
|
|
|
59
65
|
};
|
|
60
66
|
}
|
|
61
67
|
let clearHeartBeatProcessor = undefined;
|
|
62
|
-
const trackBatchUrl = `${serverUrl}/api/apps/${appId}/analytics/track/batch`;
|
|
63
|
-
const batchRequestFallback = async (events) => {
|
|
64
|
-
await axiosClient.request({
|
|
65
|
-
method: "POST",
|
|
66
|
-
url: `/apps/${appId}/analytics/track/batch`,
|
|
67
|
-
data: { events },
|
|
68
|
-
});
|
|
69
|
-
};
|
|
70
|
-
// currently disabled, until fully tested //
|
|
71
|
-
const beaconRequest = (events) => {
|
|
72
|
-
try {
|
|
73
|
-
const beaconPayload = JSON.stringify({ events });
|
|
74
|
-
const blob = new Blob([beaconPayload], { type: "application/json" });
|
|
75
|
-
return (typeof navigator === "undefined" ||
|
|
76
|
-
beaconPayload.length > 60000 ||
|
|
77
|
-
!navigator.sendBeacon(trackBatchUrl, blob));
|
|
78
|
-
}
|
|
79
|
-
catch (_a) {
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
const flush = async (eventsData, options = {}) => {
|
|
84
|
-
var _a;
|
|
85
|
-
if (eventsData.length === 0)
|
|
86
|
-
return;
|
|
87
|
-
const sessionContext_ = await getSessionContext(userAuthModule, state);
|
|
88
|
-
const events = eventsData.map(transformEventDataToApiRequestData({ ...sessionContext_, session_id: (_a = getVisitorId === null || getVisitorId === void 0 ? void 0 : getVisitorId()) !== null && _a !== void 0 ? _a : sessionContext_.session_id }));
|
|
89
|
-
try {
|
|
90
|
-
if (!options.isBeacon || !beaconRequest(events)) {
|
|
91
|
-
await batchRequestFallback(events);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
catch (_b) {
|
|
95
|
-
// do nothing
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
const startProcessing = () => {
|
|
99
|
-
startAnalyticsProcessor(flush, {
|
|
100
|
-
throttleTime,
|
|
101
|
-
batchSize,
|
|
102
|
-
}, state);
|
|
103
|
-
};
|
|
104
68
|
const track = (params) => {
|
|
105
|
-
var _a;
|
|
106
|
-
if (state.requestsQueue.length >= maxQueueSize) {
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
69
|
+
var _a, _b;
|
|
109
70
|
const intrinsicData = getEventIntrinsicData();
|
|
110
|
-
const
|
|
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"));
|
|
111
75
|
const properties = { ...params.properties };
|
|
112
76
|
delete properties.__b44_experiment_preview;
|
|
113
77
|
if (Object.keys(preview).length) {
|
|
114
78
|
// Capture now: a queued event must retain its occurrence-time preview.
|
|
115
79
|
properties.__b44_experiment_preview = JSON.stringify(preview);
|
|
116
80
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
81
|
+
const event = {
|
|
82
|
+
event_name: params.eventName,
|
|
83
|
+
timestamp: intrinsicData.timestamp,
|
|
84
|
+
page_url: intrinsicData.pageUrl,
|
|
120
85
|
properties: params.properties || Object.keys(properties).length ? properties : undefined,
|
|
121
|
-
}
|
|
122
|
-
|
|
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; }));
|
|
123
88
|
};
|
|
124
89
|
const onDocVisible = () => {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
batchSize,
|
|
128
|
-
}, state);
|
|
129
|
-
clearHeartBeatProcessor = startHeartBeatProcessor(track, state);
|
|
130
|
-
setSessionDurationTimerStart(state);
|
|
90
|
+
clearHeartBeatProcessor = startHeartBeatProcessor(track, automaticState);
|
|
91
|
+
setSessionDurationTimerStart(automaticState);
|
|
131
92
|
};
|
|
132
93
|
const onDocHidden = () => {
|
|
133
|
-
stopAnalyticsProcessor(state);
|
|
134
94
|
clearHeartBeatProcessor === null || clearHeartBeatProcessor === void 0 ? void 0 : clearHeartBeatProcessor();
|
|
135
|
-
trackSessionDurationEvent(track,
|
|
136
|
-
|
|
137
|
-
const eventsData = state.requestsQueue.splice(0);
|
|
138
|
-
flush(eventsData, { isBeacon: true });
|
|
95
|
+
trackSessionDurationEvent(track, automaticState);
|
|
96
|
+
void queue.flush();
|
|
139
97
|
};
|
|
140
98
|
const onVisibilityChange = () => {
|
|
141
99
|
if (typeof window === "undefined")
|
|
@@ -148,18 +106,16 @@ export const createAnalyticsModule = ({ axiosClient, serverUrl, appId, userAuthM
|
|
|
148
106
|
}
|
|
149
107
|
};
|
|
150
108
|
const cleanup = () => {
|
|
151
|
-
|
|
109
|
+
queue.cleanup();
|
|
152
110
|
clearHeartBeatProcessor === null || clearHeartBeatProcessor === void 0 ? void 0 : clearHeartBeatProcessor();
|
|
153
111
|
if (typeof window !== "undefined") {
|
|
154
112
|
window.removeEventListener("visibilitychange", onVisibilityChange);
|
|
155
113
|
}
|
|
156
114
|
};
|
|
157
|
-
// start the flusing process ///
|
|
158
|
-
startProcessing();
|
|
159
115
|
// start the heart beat processor //
|
|
160
|
-
clearHeartBeatProcessor = startHeartBeatProcessor(track,
|
|
116
|
+
clearHeartBeatProcessor = startHeartBeatProcessor(track, automaticState);
|
|
161
117
|
// track the referrer event //
|
|
162
|
-
trackInitializationEvent(track,
|
|
118
|
+
trackInitializationEvent(track, automaticState);
|
|
163
119
|
// start the visibility change listener //
|
|
164
120
|
if (typeof window !== "undefined") {
|
|
165
121
|
window.addEventListener("visibilitychange", onVisibilityChange);
|
|
@@ -169,24 +125,6 @@ export const createAnalyticsModule = ({ axiosClient, serverUrl, appId, userAuthM
|
|
|
169
125
|
cleanup,
|
|
170
126
|
};
|
|
171
127
|
};
|
|
172
|
-
function stopAnalyticsProcessor(state) {
|
|
173
|
-
state.isProcessing = false;
|
|
174
|
-
}
|
|
175
|
-
async function startAnalyticsProcessor(handleTrack, options, state) {
|
|
176
|
-
if (state.isProcessing) {
|
|
177
|
-
// only one instance of the analytics processor can be running at a time //
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
state.isProcessing = true;
|
|
181
|
-
const { throttleTime = 1000, batchSize = 30 } = options !== null && options !== void 0 ? options : {};
|
|
182
|
-
while (state.isProcessing &&
|
|
183
|
-
state.requestsQueue.length > 0) {
|
|
184
|
-
const requests = state.requestsQueue.splice(0, batchSize);
|
|
185
|
-
requests.length && (await handleTrack(requests));
|
|
186
|
-
await new Promise((resolve) => setTimeout(resolve, throttleTime));
|
|
187
|
-
}
|
|
188
|
-
state.isProcessing = false;
|
|
189
|
-
}
|
|
190
128
|
function startHeartBeatProcessor(track, state) {
|
|
191
129
|
var _a;
|
|
192
130
|
// Browser-only, like the other automatic events here (initialization, session
|
|
@@ -247,15 +185,6 @@ function getEventIntrinsicData() {
|
|
|
247
185
|
pageUrl: typeof window !== "undefined" ? (_b = (_a = window.location) === null || _a === void 0 ? void 0 : _a.pathname) !== null && _b !== void 0 ? _b : null : null,
|
|
248
186
|
};
|
|
249
187
|
}
|
|
250
|
-
function transformEventDataToApiRequestData(sessionContext) {
|
|
251
|
-
return (eventData) => ({
|
|
252
|
-
event_name: eventData.eventName,
|
|
253
|
-
properties: eventData.properties,
|
|
254
|
-
timestamp: eventData.timestamp,
|
|
255
|
-
page_url: eventData.pageUrl,
|
|
256
|
-
...sessionContext,
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
188
|
/**
|
|
260
189
|
* Clears the memoized analytics session context.
|
|
261
190
|
*
|
|
@@ -267,8 +196,7 @@ function transformEventDataToApiRequestData(sessionContext) {
|
|
|
267
196
|
* @internal
|
|
268
197
|
*/
|
|
269
198
|
export function resetAnalyticsSessionContext(axiosClient) {
|
|
270
|
-
|
|
271
|
-
const state = axiosClient ? (_a = serverAnalyticsStates.get(axiosClient)) !== null && _a !== void 0 ? _a : analyticsSharedState : analyticsSharedState;
|
|
199
|
+
const state = axiosClient ? getAnalyticsState(axiosClient) : analyticsSharedState;
|
|
272
200
|
state.sessionContext = null;
|
|
273
201
|
state.sessionContextPromise = null;
|
|
274
202
|
}
|
|
@@ -1,65 +1,30 @@
|
|
|
1
1
|
import { v4 as uuid } from "uuid";
|
|
2
|
-
import { isAnalyticsEnabled } from "./analytics.js";
|
|
2
|
+
import { getAnalyticsState, isAnalyticsEnabled } from "./analytics.js";
|
|
3
|
+
import { getAnalyticsQueue } from "./analytics-queue.js";
|
|
3
4
|
/** @internal */
|
|
4
5
|
export function createExposureTracker({ axiosClient, appId, enabled, source = "browser", pageUrl, }) {
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
}
|
|
6
|
+
const state = getAnalyticsState(axiosClient);
|
|
7
|
+
const queue = getAnalyticsQueue(axiosClient, appId, state.config);
|
|
8
|
+
const tracked = new Set();
|
|
35
9
|
return {
|
|
36
10
|
track(assignment, identity) {
|
|
37
|
-
if ((source === "browser" && typeof window === "undefined") || !isAnalyticsEnabled(enabled))
|
|
11
|
+
if ((source === "browser" && typeof window === "undefined") || !isAnalyticsEnabled(enabled, state))
|
|
38
12
|
return;
|
|
39
13
|
const { experiment_id, run_version, variant_key } = assignment;
|
|
40
14
|
const key = JSON.stringify([experiment_id, run_version, variant_key, identity.userId, identity.visitorId]);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
properties: { experiment_id, run_version, variant_key, source },
|
|
54
|
-
}] },
|
|
55
|
-
};
|
|
56
|
-
entries.set(key, entry);
|
|
57
|
-
}
|
|
58
|
-
if (!entry.acknowledged)
|
|
59
|
-
void send(entry);
|
|
60
|
-
},
|
|
61
|
-
async flush() {
|
|
62
|
-
await Promise.all([...entries.values()].filter((entry) => !entry.acknowledged).map(send));
|
|
15
|
+
if (tracked.has(key))
|
|
16
|
+
return;
|
|
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);
|
|
63
27
|
},
|
|
28
|
+
flush: queue.flush,
|
|
64
29
|
};
|
|
65
30
|
}
|
|
@@ -25,7 +25,7 @@ export interface ExperimentsSnapshot {
|
|
|
25
25
|
*/
|
|
26
26
|
export interface ExperimentsModule {
|
|
27
27
|
/**
|
|
28
|
-
* Reads a flag and queues
|
|
28
|
+
* Reads a flag and queues a best-effort exposure for its current assignment.
|
|
29
29
|
*
|
|
30
30
|
* Never starts an authentication request. Reads return the fallback while the
|
|
31
31
|
* app's normal auth initialization is pending or failed. Supply trusted bootstrap
|
|
@@ -34,8 +34,13 @@ export interface ExperimentsModule {
|
|
|
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
|
-
*
|
|
37
|
+
* experiment run, variant and identity. Network and server failures retry up to
|
|
38
|
+
* three attempts within five seconds of batch delivery, preserving the event ID,
|
|
39
|
+
* timestamp and credentials. HTTP successes (including rejected measurements) and client errors
|
|
40
|
+
* are terminal. Exposures share the Analytics batch with compatible ordinary
|
|
41
|
+
* events; credentials and user/visitor identities are captured when tracking.
|
|
42
|
+
* Only exposures are retried; ordinary goals retain single-attempt delivery.
|
|
43
|
+
* On servers, use the runtime's background lifetime mechanism.
|
|
39
44
|
*
|
|
40
45
|
* @param flagKey - Feature flag key defined in your app.
|
|
41
46
|
* @param fallback - Value for an unavailable flag or unresolved identity. Defaults to `false`.
|
|
@@ -97,10 +102,23 @@ export interface ExperimentsModule {
|
|
|
97
102
|
*/
|
|
98
103
|
ready(): Promise<ExperimentsSnapshot>;
|
|
99
104
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
105
|
+
* Flushes this client's queued Analytics goals and exposures without rejecting.
|
|
106
|
+
* Each delivery has a five-second total budget; exhausted or rejected events are
|
|
107
|
+
* dropped and are not retried by later reads or flushes. Settlement is not proof
|
|
108
|
+
* of ingestion, and raw storage is not exactly-once. No new exposures are created.
|
|
109
|
+
* Worker handlers should use `ctx.waitUntil(client.experiments.flush())` instead
|
|
110
|
+
* of awaiting Analytics on the application response path. Other runtimes must use
|
|
111
|
+
* their supported background lifetime mechanism; fire-and-forget alone may be cut off.
|
|
112
|
+
* Base44's legacy Cloudflare runtime exposes `globalThis.Base44.waitUntil(...)`;
|
|
113
|
+
* the newer runtime exports `waitUntil` from `base44:runtime`. Use the API provided
|
|
114
|
+
* by your deployed runtime. Deno without a background lifetime API must await flush.
|
|
115
|
+
*
|
|
116
|
+
* @returns A promise that resolves when the current batch deliveries settle.
|
|
117
|
+
* @example
|
|
118
|
+
* ```typescript
|
|
119
|
+
* // In a Worker handler with an execution context:
|
|
120
|
+
* ctx.waitUntil(base44.experiments.flush());
|
|
121
|
+
* ```
|
|
104
122
|
*/
|
|
105
123
|
flush(): Promise<void>;
|
|
106
124
|
}
|