Skip to content

Commit 1c48ac1

Browse files
committed
Custom-Events | Analytics module
1 parent 6f08dfe commit 1c48ac1

6 files changed

Lines changed: 196 additions & 1 deletion

File tree

src/client.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {
1515
CreateClientConfig,
1616
CreateClientOptions,
1717
} from "./client.types.js";
18+
import { createAnalyticsModule } from "./modules/analytics.js";
1819

1920
// Re-export client types
2021
export type { Base44Client, CreateClientConfig, CreateClientOptions };
@@ -144,6 +145,11 @@ export function createClient(config: CreateClientConfig): Base44Client {
144145
}),
145146
appLogs: createAppLogsModule(axiosClient, appId),
146147
users: createUsersModule(axiosClient, appId),
148+
analytics: createAnalyticsModule({
149+
axiosClient,
150+
appId,
151+
options: options?.analytics,
152+
}),
147153
cleanup: () => {
148154
if (socket) {
149155
socket.disconnect();

src/client.types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { ConnectorsModule } from "./modules/connectors.types.js";
66
import type { FunctionsModule } from "./modules/functions.types.js";
77
import type { AgentsModule } from "./modules/agents.types.js";
88
import type { AppLogsModule } from "./modules/app-logs.types.js";
9+
import type { AnalyticsModuleOptions } from "./modules/analytics.types.js";
910

1011
/**
1112
* Options for creating a Base44 client.
@@ -15,6 +16,7 @@ export interface CreateClientOptions {
1516
* Optional error handler that will be called whenever an API error occurs.
1617
*/
1718
onError?: (error: Error) => void;
19+
analytics?: AnalyticsModuleOptions;
1820
}
1921

2022
/**

src/modules/analytics.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { AxiosInstance } from "axios";
2+
import {
3+
TrackEventParams,
4+
TrackEventData,
5+
AnalyticsApiRequestData,
6+
AnalyticsApiBatchRequest,
7+
TrackEventIntrinsicData,
8+
AnalyticsModuleOptions,
9+
} from "./analytics.types";
10+
import {
11+
getSharedInstance,
12+
getSharedInstanceRefCount,
13+
} from "../utils/singleton";
14+
15+
///////////////////////////////////////////////
16+
//// shared queue for analytics events ////
17+
///////////////////////////////////////////////
18+
type AnalyticsSharedState = {
19+
requestsQueue: TrackEventData[];
20+
};
21+
22+
const ANALYTICS_SHARED_STATE_NAME = "analytics";
23+
// shared state//
24+
const analyticsSharedState = getSharedInstance<AnalyticsSharedState>(
25+
ANALYTICS_SHARED_STATE_NAME,
26+
() => ({
27+
requestsQueue: [],
28+
})
29+
);
30+
///////////////////////////////////////////////
31+
32+
export interface AnalyticsModuleArgs {
33+
axiosClient: AxiosInstance;
34+
appId: string;
35+
options?: AnalyticsModuleOptions;
36+
}
37+
38+
export const createAnalyticsModule = ({
39+
axiosClient,
40+
appId,
41+
options,
42+
}: AnalyticsModuleArgs) => {
43+
// prevent overflow of events //
44+
const MAX_QUEUE_SIZE = 1000;
45+
const isEnabled = options?.enabled !== false;
46+
47+
const track = (params: TrackEventParams) => {
48+
if (
49+
!isEnabled ||
50+
analyticsSharedState.requestsQueue.length >= MAX_QUEUE_SIZE
51+
) {
52+
return;
53+
}
54+
const intrinsicData = getEventIntrinsicData();
55+
analyticsSharedState.requestsQueue.push({
56+
...params,
57+
...intrinsicData,
58+
});
59+
};
60+
61+
const flush = async (eventsData: TrackEventData[]) => {
62+
const apiEvents = eventsData.map(transformEventDataToApiRequestData);
63+
await axiosClient.request({
64+
method: "POST",
65+
url: `/apps/${appId}/analytics/track/batch`,
66+
data: { events: apiEvents },
67+
} as AnalyticsApiBatchRequest);
68+
};
69+
70+
// start analytics processor only if it's the first instance and analytics is enabled //
71+
if (
72+
getSharedInstanceRefCount(ANALYTICS_SHARED_STATE_NAME) <= 1 &&
73+
isEnabled
74+
) {
75+
startAnalyticsProcessor(flush, options?.trackService);
76+
}
77+
78+
return {
79+
track,
80+
};
81+
};
82+
83+
async function startAnalyticsProcessor(
84+
handleTrack: (trackRequest: TrackEventData[]) => Promise<void>,
85+
options?: {
86+
throttleTime: number;
87+
batchSize: number;
88+
}
89+
) {
90+
const { throttleTime = 1000, batchSize = 30 } = options ?? {};
91+
while (true) {
92+
await new Promise((resolve) => setTimeout(resolve, throttleTime));
93+
const requests = analyticsSharedState.requestsQueue.splice(0, batchSize);
94+
if (requests.length > 0) {
95+
try {
96+
await handleTrack(requests);
97+
} catch (error) {
98+
// TODO: think about retries if needed
99+
console.error("Error processing analytics request:", error);
100+
}
101+
}
102+
}
103+
}
104+
105+
function getEventIntrinsicData(): TrackEventIntrinsicData {
106+
return {
107+
timestamp: new Date().toISOString(),
108+
pageUrl: typeof window !== "undefined" ? window.location.href : null,
109+
};
110+
}
111+
112+
function transformEventDataToApiRequestData(
113+
eventData: TrackEventData
114+
): AnalyticsApiRequestData {
115+
return {
116+
event_name: eventData.eventName,
117+
properties: eventData.properties,
118+
timestamp: eventData.timestamp,
119+
page_url: eventData.pageUrl,
120+
};
121+
}

src/modules/analytics.types.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
export type TrackEventProperties = {
2+
[key: string]: string | number | boolean | null | undefined;
3+
};
4+
5+
export type TrackEventParams = {
6+
eventName: string;
7+
properties?: TrackEventProperties;
8+
};
9+
10+
export type TrackEventIntrinsicData = {
11+
timestamp: string;
12+
pageUrl?: string | null;
13+
};
14+
15+
export type TrackEventData = {
16+
properties?: TrackEventProperties;
17+
eventName: string;
18+
} & TrackEventIntrinsicData;
19+
20+
export type AnalyticsApiRequestData = {
21+
event_name: string;
22+
properties?: TrackEventProperties;
23+
timestamp?: string;
24+
page_url?: string | null;
25+
};
26+
27+
export type AnalyticsApiBatchRequest = {
28+
method: "POST";
29+
url: `/apps/${string}/analytics/track/batch`;
30+
data: {
31+
events: AnalyticsApiRequestData[];
32+
};
33+
};
34+
35+
export type AnalyticsModuleOptions = {
36+
enabled?: boolean;
37+
trackService?: {
38+
throttleTime: number;
39+
batchSize: number;
40+
};
41+
};

src/modules/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from "./app.types.js";
22
export * from "./agents.types.js";
3-
export * from "./connectors.types.js";
3+
export * from "./connectors.types.js";
4+
export * from "./analytics.types.js";

src/utils/singleton.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
2+
3+
4+
// Singleton (shared between sdk instances)//
5+
export function getSharedInstance<T>(name: string, factory: () => T): T {
6+
const windowObj: Window & {
7+
base44?: { [key: string]: { instance: T; _refCount: number } };
8+
} = typeof window !== "undefined" ? (window as any) : { base44: {} };
9+
if (!windowObj.base44) {
10+
windowObj.base44 = {};
11+
}
12+
if (!windowObj.base44[name]) {
13+
windowObj.base44[name] = { instance: factory(), _refCount: 1 };
14+
}
15+
return windowObj.base44[name].instance;
16+
}
17+
18+
export function getSharedInstanceRefCount<T>(name: string): number {
19+
const windowObj: Window & {
20+
base44?: { [key: string]: { instance: T; _refCount: number } };
21+
} = typeof window !== "undefined" ? (window as any) : { base44: {} };
22+
23+
return windowObj.base44?.[name]?._refCount ?? 0;
24+
}

0 commit comments

Comments
 (0)