278 lines
10 KiB
JavaScript
278 lines
10 KiB
JavaScript
import { getSharedInstance } from "../utils/sharedInstance.js";
|
|
import { generateUuid } from "../utils/common.js";
|
|
export const USER_HEARTBEAT_EVENT_NAME = "__user_heartbeat_event__";
|
|
export const ANALYTICS_INITIALIZATION_EVENT_NAME = "__initialization_event__";
|
|
export const ANALYTICS_SESSION_DURATION_EVENT_NAME = "__session_duration_event__";
|
|
export const ANALYTICS_CONFIG_ENABLE_URL_PARAM_KEY = "analytics-enable";
|
|
export const ANALYTICS_SESSION_ID_LOCAL_STORAGE_KEY = "base44_analytics_session_id";
|
|
const defaultConfiguration = {
|
|
// default to enabled //
|
|
enabled: true,
|
|
maxQueueSize: 1000,
|
|
throttleTime: 1000,
|
|
batchSize: 30,
|
|
heartBeatInterval: 60 * 1000,
|
|
};
|
|
///////////////////////////////////////////////
|
|
//// shared queue for analytics events ////
|
|
///////////////////////////////////////////////
|
|
const ANALYTICS_SHARED_STATE_NAME = "analytics";
|
|
// shared state//
|
|
const analyticsSharedState = getSharedInstance(ANALYTICS_SHARED_STATE_NAME, () => ({
|
|
requestsQueue: [],
|
|
isProcessing: false,
|
|
isHeartBeatProcessing: false,
|
|
wasInitializationTracked: false,
|
|
sessionContext: null,
|
|
sessionStartTime: null,
|
|
config: {
|
|
...defaultConfiguration,
|
|
...getAnalyticsConfigFromUrlParams(),
|
|
},
|
|
}));
|
|
export const createAnalyticsModule = ({ axiosClient, serverUrl, appId, userAuthModule, }) => {
|
|
var _a;
|
|
// prevent overflow of events //
|
|
const { maxQueueSize, throttleTime, batchSize } = analyticsSharedState.config;
|
|
if (!((_a = analyticsSharedState.config) === null || _a === void 0 ? void 0 : _a.enabled)) {
|
|
return {
|
|
track: () => { },
|
|
cleanup: () => { },
|
|
};
|
|
}
|
|
let clearHeartBeatProcessor = undefined;
|
|
const trackBatchUrl = `${serverUrl}/api/apps/${appId}/analytics/track/batch`;
|
|
const batchRequestFallback = async (events) => {
|
|
await axiosClient.request({
|
|
method: "POST",
|
|
url: `/apps/${appId}/analytics/track/batch`,
|
|
data: { events },
|
|
});
|
|
};
|
|
// currently disabled, until fully tested //
|
|
const beaconRequest = (events) => {
|
|
try {
|
|
const beaconPayload = JSON.stringify({ events });
|
|
const blob = new Blob([beaconPayload], { type: "application/json" });
|
|
return (typeof navigator === "undefined" ||
|
|
beaconPayload.length > 60000 ||
|
|
!navigator.sendBeacon(trackBatchUrl, blob));
|
|
}
|
|
catch (_a) {
|
|
return false;
|
|
}
|
|
};
|
|
const flush = async (eventsData, options = {}) => {
|
|
if (eventsData.length === 0)
|
|
return;
|
|
const sessionContext_ = await getSessionContext(userAuthModule);
|
|
const events = eventsData.map(transformEventDataToApiRequestData(sessionContext_));
|
|
try {
|
|
if (!options.isBeacon || !beaconRequest(events)) {
|
|
await batchRequestFallback(events);
|
|
}
|
|
}
|
|
catch (_a) {
|
|
// do nothing
|
|
}
|
|
};
|
|
const startProcessing = () => {
|
|
startAnalyticsProcessor(flush, {
|
|
throttleTime,
|
|
batchSize,
|
|
});
|
|
};
|
|
const track = (params) => {
|
|
if (analyticsSharedState.requestsQueue.length >= maxQueueSize) {
|
|
return;
|
|
}
|
|
const intrinsicData = getEventIntrinsicData();
|
|
analyticsSharedState.requestsQueue.push({
|
|
...params,
|
|
...intrinsicData,
|
|
});
|
|
startProcessing();
|
|
};
|
|
const onDocVisible = () => {
|
|
startAnalyticsProcessor(flush, {
|
|
throttleTime,
|
|
batchSize,
|
|
});
|
|
clearHeartBeatProcessor = startHeartBeatProcessor(track);
|
|
setSessionDurationTimerStart();
|
|
};
|
|
const onDocHidden = () => {
|
|
stopAnalyticsProcessor();
|
|
clearHeartBeatProcessor === null || clearHeartBeatProcessor === void 0 ? void 0 : clearHeartBeatProcessor();
|
|
trackSessionDurationEvent(track);
|
|
// flush entire queue on visibility change and hope for the best //
|
|
const eventsData = analyticsSharedState.requestsQueue.splice(0);
|
|
flush(eventsData, { isBeacon: true });
|
|
};
|
|
const onVisibilityChange = () => {
|
|
if (typeof window === "undefined")
|
|
return;
|
|
if (document.visibilityState === "hidden") {
|
|
onDocHidden();
|
|
}
|
|
else if (document.visibilityState === "visible") {
|
|
onDocVisible();
|
|
}
|
|
};
|
|
const cleanup = () => {
|
|
stopAnalyticsProcessor();
|
|
clearHeartBeatProcessor === null || clearHeartBeatProcessor === void 0 ? void 0 : clearHeartBeatProcessor();
|
|
if (typeof window !== "undefined") {
|
|
window.removeEventListener("visibilitychange", onVisibilityChange);
|
|
}
|
|
};
|
|
// start the flusing process ///
|
|
startProcessing();
|
|
// start the heart beat processor //
|
|
clearHeartBeatProcessor = startHeartBeatProcessor(track);
|
|
// track the referrer event //
|
|
trackInitializationEvent(track);
|
|
// start the visibility change listener //
|
|
if (typeof window !== "undefined") {
|
|
window.addEventListener("visibilitychange", onVisibilityChange);
|
|
}
|
|
return {
|
|
track,
|
|
cleanup,
|
|
};
|
|
};
|
|
function stopAnalyticsProcessor() {
|
|
analyticsSharedState.isProcessing = false;
|
|
}
|
|
async function startAnalyticsProcessor(handleTrack, options) {
|
|
if (analyticsSharedState.isProcessing) {
|
|
// only one instance of the analytics processor can be running at a time //
|
|
return;
|
|
}
|
|
analyticsSharedState.isProcessing = true;
|
|
const { throttleTime = 1000, batchSize = 30 } = options !== null && options !== void 0 ? options : {};
|
|
while (analyticsSharedState.isProcessing &&
|
|
analyticsSharedState.requestsQueue.length > 0) {
|
|
const requests = analyticsSharedState.requestsQueue.splice(0, batchSize);
|
|
requests.length && (await handleTrack(requests));
|
|
await new Promise((resolve) => setTimeout(resolve, throttleTime));
|
|
}
|
|
analyticsSharedState.isProcessing = false;
|
|
}
|
|
function startHeartBeatProcessor(track) {
|
|
var _a;
|
|
if (analyticsSharedState.isHeartBeatProcessing ||
|
|
((_a = analyticsSharedState.config.heartBeatInterval) !== null && _a !== void 0 ? _a : 0) < 10) {
|
|
return () => { };
|
|
}
|
|
analyticsSharedState.isHeartBeatProcessing = true;
|
|
const interval = setInterval(() => {
|
|
track({ eventName: USER_HEARTBEAT_EVENT_NAME });
|
|
}, analyticsSharedState.config.heartBeatInterval);
|
|
return () => {
|
|
clearInterval(interval);
|
|
analyticsSharedState.isHeartBeatProcessing = false;
|
|
};
|
|
}
|
|
function trackInitializationEvent(track) {
|
|
if (typeof window === "undefined" ||
|
|
analyticsSharedState.wasInitializationTracked) {
|
|
return;
|
|
}
|
|
analyticsSharedState.wasInitializationTracked = true;
|
|
track({
|
|
eventName: ANALYTICS_INITIALIZATION_EVENT_NAME,
|
|
properties: {
|
|
referrer: document === null || document === void 0 ? void 0 : document.referrer,
|
|
},
|
|
});
|
|
}
|
|
function setSessionDurationTimerStart() {
|
|
if (typeof window === "undefined" ||
|
|
analyticsSharedState.sessionStartTime !== null) {
|
|
return;
|
|
}
|
|
analyticsSharedState.sessionStartTime = new Date().toISOString();
|
|
}
|
|
function trackSessionDurationEvent(track) {
|
|
if (typeof window === "undefined" ||
|
|
analyticsSharedState.sessionStartTime === null)
|
|
return;
|
|
const sessionDuration = new Date().getTime() -
|
|
new Date(analyticsSharedState.sessionStartTime).getTime();
|
|
analyticsSharedState.sessionStartTime = null;
|
|
track({
|
|
eventName: ANALYTICS_SESSION_DURATION_EVENT_NAME,
|
|
properties: { sessionDuration },
|
|
});
|
|
}
|
|
function getEventIntrinsicData() {
|
|
return {
|
|
timestamp: new Date().toISOString(),
|
|
pageUrl: typeof window !== "undefined" ? window.location.pathname : null,
|
|
};
|
|
}
|
|
function transformEventDataToApiRequestData(sessionContext) {
|
|
return (eventData) => ({
|
|
event_name: eventData.eventName,
|
|
properties: eventData.properties,
|
|
timestamp: eventData.timestamp,
|
|
page_url: eventData.pageUrl,
|
|
...sessionContext,
|
|
});
|
|
}
|
|
let sessionContextPromise = null;
|
|
async function getSessionContext(userAuthModule) {
|
|
if (!analyticsSharedState.sessionContext) {
|
|
if (!sessionContextPromise) {
|
|
const sessionId = getAnalyticsSessionId();
|
|
sessionContextPromise = userAuthModule
|
|
.me()
|
|
.then((user) => ({
|
|
user_id: user.id,
|
|
session_id: sessionId,
|
|
}))
|
|
.catch(() => ({
|
|
user_id: null,
|
|
session_id: sessionId,
|
|
}));
|
|
}
|
|
analyticsSharedState.sessionContext = await sessionContextPromise;
|
|
}
|
|
return analyticsSharedState.sessionContext;
|
|
}
|
|
export function getAnalyticsConfigFromUrlParams() {
|
|
if (typeof window === "undefined")
|
|
return undefined;
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const analyticsEnable = urlParams.get(ANALYTICS_CONFIG_ENABLE_URL_PARAM_KEY);
|
|
// if the url param is not set, return undefined //
|
|
if (analyticsEnable == null || !analyticsEnable.length)
|
|
return undefined;
|
|
// remove the url param from the url //
|
|
const newUrlParams = new URLSearchParams(window.location.search);
|
|
newUrlParams.delete(ANALYTICS_CONFIG_ENABLE_URL_PARAM_KEY);
|
|
const newUrl = window.location.pathname +
|
|
(newUrlParams.toString() ? "?" + newUrlParams.toString() : "");
|
|
window.history.replaceState({}, "", newUrl);
|
|
// return the config object //
|
|
return { enabled: analyticsEnable === "true" };
|
|
}
|
|
export function getAnalyticsSessionId() {
|
|
if (typeof window === "undefined") {
|
|
return generateUuid();
|
|
}
|
|
try {
|
|
const sessionId = localStorage.getItem(ANALYTICS_SESSION_ID_LOCAL_STORAGE_KEY);
|
|
if (!sessionId) {
|
|
const newSessionId = generateUuid();
|
|
localStorage.setItem(ANALYTICS_SESSION_ID_LOCAL_STORAGE_KEY, newSessionId);
|
|
return newSessionId;
|
|
}
|
|
return sessionId;
|
|
}
|
|
catch (_a) {
|
|
return generateUuid();
|
|
}
|
|
}
|