database-petani-mobile/node_modules/@base44/sdk/dist/modules/analytics.js
2026-02-23 16:39:35 +07:00

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();
}
}