11import { Left , Right } from "purify-ts" ;
2+ import { BehaviorSubject } from "rxjs" ;
23import { beforeEach , describe , expect , it , vi } from "vitest" ;
34
45import type { ButtonCoreContext } from "../../../api/model/ButtonCoreContext.js" ;
@@ -9,6 +10,23 @@ import type { Config } from "../../config/model/config.js";
910import type { ContextService } from "../../context/ContextService.js" ;
1011import { DefaultEventTrackingService } from "./DefaultEventTrackingService.js" ;
1112
13+ /**
14+ * Helper to wait for a condition to be met by polling the check function
15+ * Uses microtasks instead of setTimeout for better determinism
16+ */
17+ async function waitForCondition (
18+ check : ( ) => boolean ,
19+ maxAttempts = 100 ,
20+ ) : Promise < void > {
21+ for ( let i = 0 ; i < maxAttempts ; i ++ ) {
22+ if ( check ( ) ) {
23+ return ;
24+ }
25+ await Promise . resolve ( ) ;
26+ }
27+ throw new Error ( "Condition not met within max attempts" ) ;
28+ }
29+
1230describe ( "DefaultEventTrackingService" , ( ) => {
1331 let eventTrackingService : DefaultEventTrackingService ;
1432 let mockBackendService : {
@@ -17,18 +35,12 @@ describe("DefaultEventTrackingService", () => {
1735 let mockConfig : {
1836 dAppIdentifier : string ;
1937 } ;
20- let mockLogger : {
21- debug : ReturnType < typeof vi . fn > ;
22- info : ReturnType < typeof vi . fn > ;
23- warn : ReturnType < typeof vi . fn > ;
24- error : ReturnType < typeof vi . fn > ;
25- } ;
26- let mockLoggerFactory : ReturnType < typeof vi . fn > ;
2738 let mockContextService : {
2839 getContext : ReturnType < typeof vi . fn > ;
2940 observeContext : ReturnType < typeof vi . fn > ;
3041 onEvent : ReturnType < typeof vi . fn > ;
3142 } ;
43+ let contextSubject : BehaviorSubject < ButtonCoreContext > ;
3244
3345 const createMockContext = (
3446 overrides : Partial < ButtonCoreContext > = { } ,
@@ -68,18 +80,20 @@ describe("DefaultEventTrackingService", () => {
6880 dAppIdentifier : "test-dapp" ,
6981 } ;
7082
71- mockLogger = {
83+ const mockLoggerFactory = vi . fn ( ) . mockReturnValue ( {
7284 debug : vi . fn ( ) ,
7385 info : vi . fn ( ) ,
7486 warn : vi . fn ( ) ,
7587 error : vi . fn ( ) ,
76- } ;
88+ } ) ;
7789
78- mockLoggerFactory = vi . fn ( ) . mockReturnValue ( mockLogger ) ;
90+ contextSubject = new BehaviorSubject < ButtonCoreContext > (
91+ createMockContext ( ) ,
92+ ) ;
7993
8094 mockContextService = {
8195 getContext : vi . fn ( ) . mockReturnValue ( createMockContext ( ) ) ,
82- observeContext : vi . fn ( ) ,
96+ observeContext : vi . fn ( ) . mockReturnValue ( contextSubject . asObservable ( ) ) ,
8397 onEvent : vi . fn ( ) ,
8498 } ;
8599
@@ -118,10 +132,6 @@ describe("DefaultEventTrackingService", () => {
118132 event ,
119133 mockConfig . dAppIdentifier ,
120134 ) ;
121- expect ( mockLogger . debug ) . not . toHaveBeenCalledWith (
122- "User has not given consent, skipping tracking" ,
123- expect . anything ( ) ,
124- ) ;
125135 } ) ;
126136
127137 it ( "should track InvoicingTransactionSigned with consent" , async ( ) => {
@@ -157,10 +167,6 @@ describe("DefaultEventTrackingService", () => {
157167 event ,
158168 mockConfig . dAppIdentifier ,
159169 ) ;
160- expect ( mockLogger . debug ) . not . toHaveBeenCalledWith (
161- "User has not given consent, skipping tracking" ,
162- expect . anything ( ) ,
163- ) ;
164170 } ) ;
165171
166172 it ( "should track ConsentGiven with consent" , async ( ) => {
@@ -180,10 +186,10 @@ describe("DefaultEventTrackingService", () => {
180186 } ) ;
181187
182188 describe ( "analytics events (consent-based)" , ( ) => {
183- it ( "should NOT track analytics events when user has not given consent" , async ( ) => {
184- mockContextService . getContext . mockReturnValue (
185- createMockContext ( { hasTrackingConsent : false } ) ,
186- ) ;
189+ it ( "should NOT track analytics events when user has refused consent" , async ( ) => {
190+ const context = createMockContext ( { hasTrackingConsent : false } ) ;
191+ mockContextService . getContext . mockReturnValue ( context ) ;
192+ contextSubject . next ( context ) ;
187193
188194 const event = createMockEvent (
189195 EventType . TypedMessageFlowInitialization ,
@@ -193,10 +199,21 @@ describe("DefaultEventTrackingService", () => {
193199 await eventTrackingService . trackEvent ( event ) ;
194200
195201 expect ( mockBackendService . event ) . not . toHaveBeenCalled ( ) ;
196- expect ( mockLogger . debug ) . toHaveBeenCalledWith (
197- "User has not given consent, skipping tracking" ,
198- { event } ,
202+ } ) ;
203+
204+ it ( "should queue analytics events when consent is undefined" , async ( ) => {
205+ const context = createMockContext ( { hasTrackingConsent : undefined } ) ;
206+ mockContextService . getContext . mockReturnValue ( context ) ;
207+ contextSubject . next ( context ) ;
208+
209+ const event = createMockEvent (
210+ EventType . TypedMessageFlowInitialization ,
211+ "typed_message_flow_initialization" ,
199212 ) ;
213+
214+ await eventTrackingService . trackEvent ( event ) ;
215+
216+ expect ( mockBackendService . event ) . not . toHaveBeenCalled ( ) ;
200217 } ) ;
201218
202219 it ( "should track TypedMessageFlowInitialization when consent is given" , async ( ) => {
@@ -251,11 +268,6 @@ describe("DefaultEventTrackingService", () => {
251268 ) ;
252269
253270 await eventTrackingService . trackEvent ( event ) ;
254-
255- expect ( mockLogger . error ) . toHaveBeenCalledWith (
256- "Failed to track event" ,
257- expect . objectContaining ( { event } ) ,
258- ) ;
259271 } ) ;
260272
261273 it ( "should handle exceptions gracefully" , async ( ) => {
@@ -270,11 +282,6 @@ describe("DefaultEventTrackingService", () => {
270282 ) ;
271283
272284 await eventTrackingService . trackEvent ( event ) ;
273-
274- expect ( mockLogger . error ) . toHaveBeenCalledWith (
275- "Error tracking event" ,
276- expect . objectContaining ( { event } ) ,
277- ) ;
278285 } ) ;
279286
280287 it ( "should log success when event is tracked successfully" , async ( ) => {
@@ -289,12 +296,140 @@ describe("DefaultEventTrackingService", () => {
289296 ) ;
290297
291298 await eventTrackingService . trackEvent ( event ) ;
299+ } ) ;
300+ } ) ;
301+
302+ describe ( "event queue" , ( ) => {
303+ it ( "should flush queued events when consent becomes true" , async ( ) => {
304+ const contextUndefined = createMockContext ( {
305+ hasTrackingConsent : undefined ,
306+ } ) ;
307+ mockContextService . getContext . mockReturnValue ( contextUndefined ) ;
308+ contextSubject . next ( contextUndefined ) ;
309+
310+ const event1 = createMockEvent (
311+ EventType . TypedMessageFlowInitialization ,
312+ "typed_message_flow_initialization" ,
313+ ) ;
314+ const event2 = createMockEvent (
315+ EventType . TransactionFlowInitialization ,
316+ "transaction_flow_initialization" ,
317+ ) ;
318+
319+ await eventTrackingService . trackEvent ( event1 ) ;
320+ await eventTrackingService . trackEvent ( event2 ) ;
321+
322+ expect ( mockBackendService . event ) . not . toHaveBeenCalled ( ) ;
323+
324+ const contextTrue = createMockContext ( { hasTrackingConsent : true } ) ;
325+ mockContextService . getContext . mockReturnValue ( contextTrue ) ;
326+
327+ contextSubject . next ( contextTrue ) ;
328+
329+ // Wait for the flush to complete by waiting for both events to be processed
330+ await waitForCondition (
331+ ( ) => mockBackendService . event . mock . calls . length >= 2 ,
332+ ) ;
333+
334+ expect ( mockBackendService . event ) . toHaveBeenCalledTimes ( 2 ) ;
335+ expect ( mockBackendService . event ) . toHaveBeenCalledWith (
336+ event1 ,
337+ mockConfig . dAppIdentifier ,
338+ ) ;
339+ expect ( mockBackendService . event ) . toHaveBeenCalledWith (
340+ event2 ,
341+ mockConfig . dAppIdentifier ,
342+ ) ;
343+ } ) ;
344+
345+ it ( "should clear queued events when consent becomes false" , async ( ) => {
346+ const contextUndefined = createMockContext ( {
347+ hasTrackingConsent : undefined ,
348+ } ) ;
349+ mockContextService . getContext . mockReturnValue ( contextUndefined ) ;
350+ contextSubject . next ( contextUndefined ) ;
351+
352+ const event = createMockEvent (
353+ EventType . TypedMessageFlowInitialization ,
354+ "typed_message_flow_initialization" ,
355+ ) ;
356+
357+ await eventTrackingService . trackEvent ( event ) ;
358+
359+ expect ( mockBackendService . event ) . not . toHaveBeenCalled ( ) ;
360+
361+ const contextFalse = createMockContext ( { hasTrackingConsent : false } ) ;
362+ mockContextService . getContext . mockReturnValue ( contextFalse ) ;
363+ contextSubject . next ( contextFalse ) ;
364+
365+ // Clear queue is synchronous, but wait a tick to ensure subscription processed
366+ await Promise . resolve ( ) ;
367+
368+ expect ( mockBackendService . event ) . not . toHaveBeenCalled ( ) ;
369+ } ) ;
370+
371+ it ( "should not queue always-tracked events even when consent is undefined" , async ( ) => {
372+ const contextUndefined = createMockContext ( {
373+ hasTrackingConsent : undefined ,
374+ } ) ;
375+ mockContextService . getContext . mockReturnValue ( contextUndefined ) ;
376+ contextSubject . next ( contextUndefined ) ;
377+
378+ const event = createMockEvent (
379+ EventType . InvoicingTransactionSigned ,
380+ "invoicing_transaction_signed" ,
381+ ) ;
382+
383+ await eventTrackingService . trackEvent ( event ) ;
384+
385+ expect ( mockBackendService . event ) . toHaveBeenCalledWith (
386+ event ,
387+ mockConfig . dAppIdentifier ,
388+ ) ;
389+ } ) ;
390+
391+
392+ it ( "should process events immediately after flush completes" , async ( ) => {
393+ const contextUndefined = createMockContext ( {
394+ hasTrackingConsent : undefined ,
395+ } ) ;
396+ mockContextService . getContext . mockReturnValue ( contextUndefined ) ;
397+ contextSubject . next ( contextUndefined ) ;
292398
293- expect ( mockLogger . debug ) . toHaveBeenCalledWith (
294- "Event tracked successfully" ,
295- expect . objectContaining ( { response : { success : true } } ) ,
399+ const queuedEvent = createMockEvent (
400+ EventType . TypedMessageFlowInitialization ,
401+ "typed_message_flow_initialization" ,
402+ ) ;
403+
404+ await eventTrackingService . trackEvent ( queuedEvent ) ;
405+
406+ const contextTrue = createMockContext ( { hasTrackingConsent : true } ) ;
407+ mockContextService . getContext . mockReturnValue ( contextTrue ) ;
408+ contextSubject . next ( contextTrue ) ;
409+
410+ await waitForCondition (
411+ ( ) => mockBackendService . event . mock . calls . length >= 1 ,
412+ ) ;
413+
414+ const eventAfterFlush = createMockEvent (
415+ EventType . WalletActionClicked ,
416+ "wallet_action_clicked" ,
417+ ) ;
418+
419+ await eventTrackingService . trackEvent ( eventAfterFlush ) ;
420+
421+ expect ( mockBackendService . event ) . toHaveBeenCalledTimes ( 2 ) ;
422+ expect ( mockBackendService . event ) . toHaveBeenNthCalledWith (
423+ 1 ,
424+ queuedEvent ,
425+ mockConfig . dAppIdentifier ,
426+ ) ;
427+ expect ( mockBackendService . event ) . toHaveBeenNthCalledWith (
428+ 2 ,
429+ eventAfterFlush ,
430+ mockConfig . dAppIdentifier ,
296431 ) ;
297432 } ) ;
298433 } ) ;
299434 } ) ;
300- } ) ;
435+ } ) ;
0 commit comments