55 "errors"
66 "regexp"
77 "testing"
8- "time"
98
109 of "github.com/open-feature/go-sdk/openfeature"
1110 imp "github.com/open-feature/go-sdk/openfeature/memprovider"
@@ -359,29 +358,29 @@ func TestMultiProvider_StateUpdateWithSameTypeProviders(t *testing.T) {
359358 "secondary" : secondaryProvider ,
360359 }
361360
362- multiProvider , err := NewProvider (providers , StrategyFirstMatch )
361+ mp , err := NewProvider (providers , StrategyFirstMatch )
363362 if err != nil {
364363 t .Fatalf ("failed to create multi-provider: %v" , err )
365364 }
366- t .Cleanup (multiProvider .Shutdown )
365+ t .Cleanup (mp .Shutdown )
367366
368367 // Initialize the provider
369368 ctx := of .NewEvaluationContext ("test" , nil )
370- if err := multiProvider .Init (ctx ); err != nil {
369+ if err := mp .Init (ctx ); err != nil {
371370 t .Fatalf ("failed to initialize multi-provider: %v" , err )
372371 }
373372
374373 primaryProvider .EmitEvent (of .ProviderError , "fail to fetch data" )
375374 secondaryProvider .EmitEvent (of .ProviderReady , "rev 1" )
376-
377- time . Sleep ( 200 * time . Millisecond )
375+ // wait for processing
376+ <- mp . outboundEvents
378377
379378 // Check the state after the error event
380- multiProvider .providerStatusLock .Lock ()
381- primaryState := multiProvider .providerStatus ["primary" ]
382- secondaryState := multiProvider .providerStatus ["secondary" ]
383- numProviders := len (multiProvider .providerStatus )
384- multiProvider .providerStatusLock .Unlock ()
379+ mp .providerStatusLock .Lock ()
380+ primaryState := mp .providerStatus ["primary" ]
381+ secondaryState := mp .providerStatus ["secondary" ]
382+ numProviders := len (mp .providerStatus )
383+ mp .providerStatusLock .Unlock ()
385384
386385 if primaryState != of .ErrorState {
387386 t .Errorf ("Expected primary-mock state to be ERROR after emitting error event, got %s" , primaryState )
@@ -396,19 +395,150 @@ func TestMultiProvider_StateUpdateWithSameTypeProviders(t *testing.T) {
396395 }
397396}
398397
398+ func TestMultiProvider_Track (t * testing.T ) {
399+ t .Run ("forwards tracking to all ready providers that implement Tracker" , func (t * testing.T ) {
400+ ctrl := gomock .NewController (t )
401+ t .Cleanup (ctrl .Finish )
402+
403+ provider1 := newMockProviderWithEvents (ctrl , "provider1" )
404+ provider2 := newMockProviderWithEvents (ctrl , "provider2" )
405+ provider3 := imp .NewInMemoryProvider (map [string ]imp.InMemoryFlag {}) // Does not implement Tracker
406+
407+ providers := make (ProviderMap )
408+ providers ["provider1" ] = provider1
409+ providers ["provider2" ] = provider2
410+ providers ["provider3" ] = provider3
411+
412+ mp , err := NewProvider (providers , StrategyFirstSuccess )
413+ require .NoError (t , err )
414+ t .Cleanup (mp .Shutdown )
415+
416+ evalCtx := of .NewEvaluationContext ("user-123" , map [string ]any {"plan" : "premium" })
417+ err = mp .Init (evalCtx )
418+ require .NoError (t , err )
419+
420+ trackingEventName := "button-clicked"
421+ details := of .NewTrackingEventDetails (42.0 ).Add ("currency" , "USD" )
422+
423+ ctx := t .Context ()
424+ // Expect Track to be called on providers that implement Tracker
425+ provider1 .MockTracker .EXPECT ().Track (ctx , trackingEventName , evalCtx , details ).Times (1 )
426+ provider2 .MockTracker .EXPECT ().Track (ctx , trackingEventName , evalCtx , details ).Times (1 )
427+
428+ mp .Track (ctx , trackingEventName , evalCtx , details )
429+ })
430+
431+ t .Run ("does not track when provider is not initialized" , func (t * testing.T ) {
432+ ctrl := gomock .NewController (t )
433+ t .Cleanup (ctrl .Finish )
434+
435+ provider1 := newMockProviderWithEvents (ctrl , "provider1" )
436+ // manual shutdown on cleanup because multi-provider won't be initialized
437+ t .Cleanup (provider1 .Shutdown )
438+
439+ providers := make (ProviderMap )
440+ providers ["provider1" ] = provider1
441+
442+ mp , err := NewProvider (providers , StrategyFirstSuccess )
443+ require .NoError (t , err )
444+ t .Cleanup (mp .Shutdown )
445+
446+ // Don't initialize the multi-provider
447+ ctx := context .Background ()
448+ trackingEventName := "button-clicked"
449+ evalCtx := of .NewEvaluationContext ("user-123" , map [string ]any {})
450+ details := of.TrackingEventDetails {}
451+
452+ // Should not call Track on provider
453+ provider1 .MockTracker .EXPECT ().Track (gomock .Any (), gomock .Any (), gomock .Any (), gomock .Any ()).Times (0 )
454+
455+ mp .Track (ctx , trackingEventName , evalCtx , details )
456+ })
457+
458+ t .Run ("only tracks on providers in ready state" , func (t * testing.T ) {
459+ ctrl := gomock .NewController (t )
460+ t .Cleanup (ctrl .Finish )
461+
462+ readyProvider := newMockProviderWithEvents (ctrl , "ready-provider" )
463+ errorProvider := newMockProviderWithEvents (ctrl , "error-provider" )
464+
465+ providers := make (ProviderMap )
466+ providers ["ready-provider" ] = readyProvider
467+ providers ["error-provider" ] = errorProvider
468+
469+ mp , err := NewProvider (providers , StrategyFirstSuccess )
470+ require .NoError (t , err )
471+ t .Cleanup (mp .Shutdown )
472+
473+ evalCtx := of .NewEvaluationContext ("user-456" , map [string ]any {})
474+ err = mp .Init (evalCtx )
475+ require .NoError (t , err )
476+
477+ // Simulate error state for one provider
478+ errorProvider .eventChannel <- of.Event {
479+ ProviderName : "error-provider" ,
480+ EventType : of .ProviderError ,
481+ ProviderEventDetails : of.ProviderEventDetails {
482+ Message : "error" ,
483+ EventMetadata : make (map [string ]any ),
484+ },
485+ }
486+ // wait for event processing
487+ <- mp .outboundEvents
488+
489+ trackingEventName := "page-view"
490+ details := of.TrackingEventDetails {}
491+
492+ ctx := t .Context ()
493+ readyProvider .MockTracker .EXPECT ().Track (ctx , trackingEventName , evalCtx , details ).Times (1 )
494+ errorProvider .MockTracker .EXPECT ().Track (gomock .Any (), gomock .Any (), gomock .Any (), gomock .Any ()).Times (0 )
495+
496+ mp .Track (ctx , trackingEventName , evalCtx , details )
497+ })
498+
499+ t .Run ("handles providers that don't implement Tracker" , func (t * testing.T ) {
500+ ctrl := gomock .NewController (t )
501+ t .Cleanup (ctrl .Finish )
502+
503+ trackerProvider := newMockProviderWithEvents (ctrl , "tracker-provider" )
504+ nonTrackerProvider := imp .NewInMemoryProvider (map [string ]imp.InMemoryFlag {})
505+
506+ providers := make (ProviderMap )
507+ providers ["tracker-provider" ] = trackerProvider
508+ providers ["non-tracker" ] = nonTrackerProvider
509+
510+ mp , err := NewProvider (providers , StrategyFirstSuccess )
511+ require .NoError (t , err )
512+ t .Cleanup (mp .Shutdown )
513+
514+ evalCtx := of .NewEvaluationContext ("user-789" , map [string ]any {})
515+ err = mp .Init (evalCtx )
516+ require .NoError (t , err )
517+
518+ trackingEventName := "conversion"
519+ details := of .NewTrackingEventDetails (99.99 )
520+
521+ ctx := t .Context ()
522+ trackerProvider .MockTracker .EXPECT ().Track (ctx , trackingEventName , evalCtx , details ).Times (1 )
523+ mp .Track (ctx , trackingEventName , evalCtx , details )
524+ })
525+ }
526+
399527var _ of.StateHandler = (* mockProviderWithEvents )(nil )
400528
401- // mockProviderWithEvents wraps a mock provider to add EventHandler capability
529+ // mockProviderWithEvents wraps a mock provider to add EventHandler and optional Tracker capability
402530type mockProviderWithEvents struct {
403531 * of.MockFeatureProvider
404532 * of.MockStateHandler
533+ * of.MockTracker
405534 eventChannel chan of.Event
406535 metadata of.Metadata
407536}
408537
409538func newMockProviderWithEvents (ctrl * gomock.Controller , name string ) * mockProviderWithEvents {
410539 mockProvider := of .NewMockFeatureProvider (ctrl )
411540 mockStateHandler := of .NewMockStateHandler (ctrl )
541+ mockTracker := of .NewMockTracker (ctrl )
412542 eventChan := make (chan of.Event , 10 )
413543
414544 metadata := of.Metadata {Name : name }
@@ -434,6 +564,7 @@ func newMockProviderWithEvents(ctrl *gomock.Controller, name string) *mockProvid
434564 MockStateHandler : mockStateHandler ,
435565 eventChannel : eventChan ,
436566 metadata : metadata ,
567+ MockTracker : mockTracker ,
437568 }
438569}
439570
@@ -460,3 +591,9 @@ func (m *mockProviderWithEvents) EmitEvent(eventType of.EventType, message strin
460591 },
461592 }
462593}
594+
595+ func (m * mockProviderWithEvents ) Track (ctx context.Context , trackingEventName string , evaluationContext of.EvaluationContext , details of.TrackingEventDetails ) {
596+ if m .MockTracker != nil {
597+ m .MockTracker .Track (ctx , trackingEventName , evaluationContext , details )
598+ }
599+ }
0 commit comments