@@ -11,11 +11,14 @@ import {
1111import { getSharedInstance } from "../utils/sharedInstance" ;
1212import type { AuthModule } from "./auth.types" ;
1313
14+ export const USER_HEARTBEAT_EVENT_NAME = "__user_heartbeat_event__" ;
15+
1416const defaultConfiguration : AnalyticsModuleOptions = {
1517 enabled : true ,
1618 maxQueueSize : 1000 ,
1719 throttleTime : 1000 ,
1820 batchSize : 30 ,
21+ heartBeatInterval : 60 * 1000 ,
1922} ;
2023
2124///////////////////////////////////////////////
@@ -29,10 +32,11 @@ const analyticsSharedState = getSharedInstance(
2932 ( ) => ( {
3033 requestsQueue : [ ] as TrackEventData [ ] ,
3134 isProcessing : false ,
35+ isHeartBeatProcessing : false ,
3236 sessionContext : null as SessionContext | null ,
3337 config : {
3438 ...defaultConfiguration ,
35- ...getAnalyticsModuleOptionsFromUrlParams ( ) ,
39+ ...getAnalyticsModuleOptionsFromLocalStorage ( ) ,
3640 } as Required < AnalyticsModuleOptions > ,
3741 } )
3842) ;
@@ -55,19 +59,9 @@ export const createAnalyticsModule = ({
5559 // prevent overflow of events //
5660 const { enabled, maxQueueSize, throttleTime, batchSize } =
5761 analyticsSharedState . config ;
58-
62+ let clearHeartBeatProcessor : ( ( ) => void ) | undefined = undefined ;
5963 const trackBatchUrl = `${ serverUrl } /api/apps/${ appId } /analytics/track/batch` ;
6064
61- const getSessionContext = async ( ) => {
62- if ( ! analyticsSharedState . sessionContext ) {
63- const user = await userAuthModule . me ( ) ;
64- analyticsSharedState . sessionContext = {
65- user_id : user . id ,
66- } ;
67- }
68- return analyticsSharedState . sessionContext ;
69- } ;
70-
7165 const batchRequestFallback = async ( events : AnalyticsApiRequestData [ ] ) => {
7266 await axiosClient . request ( {
7367 method : "POST" ,
@@ -77,49 +71,25 @@ export const createAnalyticsModule = ({
7771 } ;
7872
7973 const flush = async ( eventsData : TrackEventData [ ] ) => {
80- const sessionContext_ = await getSessionContext ( ) ;
74+ const sessionContext_ = await getSessionContext ( userAuthModule ) ;
8175 const events = eventsData . map (
8276 transformEventDataToApiRequestData ( sessionContext_ )
8377 ) ;
8478 const beaconPayload = JSON . stringify ( { events } ) ;
85-
86- if (
87- typeof navigator === "undefined" ||
88- beaconPayload . length > 60000 ||
89- ! navigator . sendBeacon ( trackBatchUrl , beaconPayload )
90- ) {
91- // beacon didn't work, fallback to axios
92- await batchRequestFallback ( events ) ;
93- }
94- } ;
95-
96- const onDocHidden = ( ) => {
97- stopAnalyticsProcessor ( ) ;
98- // flush entire queue on visibility change and hope for the best //
99- const eventsData = analyticsSharedState . requestsQueue . splice ( 0 ) ;
100- flush ( eventsData ) ;
101- } ;
102-
103- const onDocVisible = ( ) => {
104- startAnalyticsProcessor ( flush , {
105- throttleTime,
106- batchSize,
107- } ) ;
108- } ;
109-
110- const onVisibilityChange = ( ) => {
111- if ( typeof window === "undefined" ) return ;
112- if ( document . visibilityState === "hidden" ) {
113- onDocHidden ( ) ;
114- } else if ( document . visibilityState === "visible" ) {
115- onDocVisible ( ) ;
79+ try {
80+ if (
81+ typeof navigator === "undefined" ||
82+ beaconPayload . length > 60000 ||
83+ ! navigator . sendBeacon ( trackBatchUrl , beaconPayload )
84+ ) {
85+ // beacon didn't work, fallback to axios
86+ await batchRequestFallback ( events ) ;
87+ }
88+ } catch {
89+ // TODO: think about retries if needed
11690 }
11791 } ;
11892
119- if ( typeof window !== "undefined" && enabled ) {
120- window . addEventListener ( "visibilitychange" , onVisibilityChange ) ;
121- }
122-
12393 const startProcessing = ( ) => {
12494 if ( ! enabled ) return ;
12595 startAnalyticsProcessor ( flush , {
@@ -140,14 +110,47 @@ export const createAnalyticsModule = ({
140110 startProcessing ( ) ;
141111 } ;
142112
143- const cleanup = ( ) => {
113+ const onDocVisible = ( ) => {
114+ startAnalyticsProcessor ( flush , {
115+ throttleTime,
116+ batchSize,
117+ } ) ;
118+ clearHeartBeatProcessor = startHeartBeatProcessor ( track ) ;
119+ } ;
120+
121+ const onDocHidden = ( ) => {
122+ stopAnalyticsProcessor ( ) ;
123+ // flush entire queue on visibility change and hope for the best //
124+ const eventsData = analyticsSharedState . requestsQueue . splice ( 0 ) ;
125+ flush ( eventsData ) ;
126+ clearHeartBeatProcessor ?.( ) ;
127+ } ;
128+
129+ const onVisibilityChange = ( ) => {
144130 if ( typeof window === "undefined" ) return ;
145- window . removeEventListener ( "visibilitychange" , onVisibilityChange ) ;
131+ if ( document . visibilityState === "hidden" ) {
132+ onDocHidden ( ) ;
133+ } else if ( document . visibilityState === "visible" ) {
134+ onDocVisible ( ) ;
135+ }
136+ } ;
137+
138+ const cleanup = ( ) => {
146139 stopAnalyticsProcessor ( ) ;
140+ clearHeartBeatProcessor ?.( ) ;
141+ if ( typeof window !== "undefined" ) {
142+ window . removeEventListener ( "visibilitychange" , onVisibilityChange ) ;
143+ }
147144 } ;
148145
149146 // start the flusing process ///
150147 startProcessing ( ) ;
148+ // start the heart beat processor //
149+ clearHeartBeatProcessor = startHeartBeatProcessor ( track ) ;
150+ // start the visibility change listener //
151+ if ( typeof window !== "undefined" && enabled ) {
152+ window . addEventListener ( "visibilitychange" , onVisibilityChange ) ;
153+ }
151154
152155 return {
153156 track,
@@ -178,19 +181,31 @@ async function startAnalyticsProcessor(
178181 analyticsSharedState . requestsQueue . length > 0
179182 ) {
180183 const requests = analyticsSharedState . requestsQueue . splice ( 0 , batchSize ) ;
181- if ( requests . length > 0 ) {
182- try {
183- await handleTrack ( requests ) ;
184- } catch ( error ) {
185- // TODO: think about retries if needed
186- console . error ( "Error processing analytics request:" , error ) ;
187- }
188- }
184+ requests . length && ( await handleTrack ( requests ) ) ;
189185 await new Promise ( ( resolve ) => setTimeout ( resolve , throttleTime ) ) ;
190186 }
191187 analyticsSharedState . isProcessing = false ;
192188}
193189
190+ function startHeartBeatProcessor ( track : ( params : TrackEventParams ) => void ) {
191+ if (
192+ analyticsSharedState . isHeartBeatProcessing ||
193+ ( analyticsSharedState . config . heartBeatInterval ?? 0 ) < 10
194+ ) {
195+ return ( ) => { } ;
196+ }
197+
198+ analyticsSharedState . isHeartBeatProcessing = true ;
199+ const interval = setInterval ( ( ) => {
200+ track ( { eventName : USER_HEARTBEAT_EVENT_NAME } ) ;
201+ } , analyticsSharedState . config . heartBeatInterval ) ;
202+
203+ return ( ) => {
204+ clearInterval ( interval ) ;
205+ analyticsSharedState . isHeartBeatProcessing = false ;
206+ } ;
207+ }
208+
194209function getEventIntrinsicData ( ) : TrackEventIntrinsicData {
195210 return {
196211 timestamp : new Date ( ) . toISOString ( ) ,
@@ -208,14 +223,31 @@ function transformEventDataToApiRequestData(sessionContext: SessionContext) {
208223 } ) ;
209224}
210225
211- export function getAnalyticsModuleOptionsFromUrlParams ( ) :
226+ let sessionContextPromise : Promise < SessionContext > | null = null ;
227+ async function getSessionContext ( userAuthModule : AuthModule ) {
228+ if ( ! analyticsSharedState . sessionContext ) {
229+ if ( ! sessionContextPromise ) {
230+ sessionContextPromise = userAuthModule
231+ . me ( )
232+ . then ( ( user ) => ( {
233+ user_id : user . id ,
234+ } ) )
235+ . catch ( ( ) => ( {
236+ user_id : "unknown: error getting session context" ,
237+ } ) ) ;
238+ }
239+ analyticsSharedState . sessionContext = await sessionContextPromise ;
240+ }
241+ return analyticsSharedState . sessionContext ;
242+ }
243+
244+ export function getAnalyticsModuleOptionsFromLocalStorage ( ) :
212245 | AnalyticsModuleOptions
213246 | undefined {
214247 if ( typeof window === "undefined" ) return undefined ;
215- const urlParams = new URLSearchParams ( window . location . search ) ;
216- const jsonString = urlParams . get ( "analytics" ) ;
217- if ( ! jsonString ) return undefined ;
218248 try {
249+ const jsonString = localStorage . getItem ( "base44_analytics_config" ) ;
250+ if ( ! jsonString ) return undefined ;
219251 return JSON . parse ( jsonString ) ;
220252 } catch {
221253 return undefined ;
0 commit comments