1313from experimenter .experiments .models import NimbusChangeLog , NimbusExperiment
1414from experimenter .experiments .tests .factories import NimbusExperimentFactory
1515from experimenter .jetstream import tasks
16- from experimenter .jetstream .client import get_data , get_monitoring_data
16+ from experimenter .jetstream .client import (
17+ get_data ,
18+ get_enrollment_funnel_data ,
19+ get_monitoring_data ,
20+ )
1721from experimenter .jetstream .models import AnalysisWindow , Group
1822from experimenter .jetstream .tests import mock_valid_outcomes
1923from experimenter .jetstream .tests .constants import (
@@ -3441,14 +3445,48 @@ def mock_monitoring_data(request):
34413445 return data
34423446
34433447
3444- @pytest .mark .usefixtures ("mock_monitoring_data" )
3448+ @pytest .fixture
3449+ def mock_funnel_entries (request ):
3450+ data = [
3451+ {
3452+ "app_name" : "firefox_desktop" ,
3453+ "branch" : "control" ,
3454+ "status" : "Enrolled" ,
3455+ "reason" : "Qualified" ,
3456+ "conflict_slug" : None ,
3457+ "client_count" : 750000 ,
3458+ },
3459+ {
3460+ "app_name" : "firefox_desktop" ,
3461+ "branch" : None ,
3462+ "status" : "NotEnrolled" ,
3463+ "reason" : "NotTargeted" ,
3464+ "conflict_slug" : None ,
3465+ "client_count" : 5000000 ,
3466+ },
3467+ ]
3468+ if request .instance :
3469+ request .instance .funnel_entries = data
3470+ return data
3471+
3472+
3473+ @pytest .mark .usefixtures ("mock_monitoring_data" , "mock_funnel_entries" )
34453474class TestFetchMonitoringDataTask (TestCase ):
34463475 def setUp (self ):
34473476 super ().setUp ()
34483477 patcher = patch ("experimenter.jetstream.tasks.get_monitoring_data" )
34493478 self .mock_get_monitoring_data = patcher .start ()
34503479 self .addCleanup (patcher .stop )
34513480
3481+ self ._funnel_patcher = patch (
3482+ "experimenter.jetstream.tasks.get_enrollment_funnel_data"
3483+ )
3484+ self .mock_get_funnel_data = self ._funnel_patcher .start ()
3485+ self .mock_get_funnel_data .return_value = {"v1" : {}}
3486+
3487+ def tearDown (self ):
3488+ self ._funnel_patcher .stop ()
3489+
34523490 @parameterized .expand ([(None ,), ({},)])
34533491 def test_fetch_monitoring_data_no_data (self , return_value ):
34543492 self .mock_get_monitoring_data .return_value = return_value
@@ -3469,9 +3507,62 @@ def test_fetch_monitoring_data_updates_live_experiment(self):
34693507 tasks .fetch_monitoring_data ()
34703508
34713509 experiment .refresh_from_db ()
3472- self .assertEqual (experiment .monitoring_data , self .monitoring_data )
3510+ self .assertEqual (
3511+ experiment .monitoring_data ,
3512+ {** self .monitoring_data , "enrollment_funnel" : []},
3513+ )
34733514 self .assertIsNotNone (experiment .monitoring_data_updated_at )
34743515
3516+ def test_fetch_monitoring_data_merges_funnel_data (self ):
3517+ experiment = NimbusExperimentFactory .create (
3518+ status = NimbusExperiment .Status .LIVE ,
3519+ monitoring_data = {},
3520+ )
3521+ self .mock_get_monitoring_data .return_value = {
3522+ "v1" : {experiment .slug : self .monitoring_data }
3523+ }
3524+ self .mock_get_funnel_data .return_value = {
3525+ "v1" : {experiment .slug : self .funnel_entries }
3526+ }
3527+
3528+ tasks .fetch_monitoring_data ()
3529+
3530+ experiment .refresh_from_db ()
3531+ self .assertEqual (
3532+ experiment .monitoring_data ,
3533+ {** self .monitoring_data , "enrollment_funnel" : self .funnel_entries },
3534+ )
3535+
3536+ def test_fetch_monitoring_data_funnel_defaults_to_empty_list_when_missing (self ):
3537+ experiment = NimbusExperimentFactory .create (
3538+ status = NimbusExperiment .Status .LIVE ,
3539+ monitoring_data = {},
3540+ )
3541+ self .mock_get_monitoring_data .return_value = {
3542+ "v1" : {experiment .slug : self .monitoring_data }
3543+ }
3544+ self .mock_get_funnel_data .return_value = {"v1" : {}}
3545+
3546+ tasks .fetch_monitoring_data ()
3547+
3548+ experiment .refresh_from_db ()
3549+ self .assertEqual (experiment .monitoring_data ["enrollment_funnel" ], [])
3550+
3551+ def test_fetch_monitoring_data_continues_if_funnel_fetch_fails (self ):
3552+ experiment = NimbusExperimentFactory .create (
3553+ status = NimbusExperiment .Status .LIVE ,
3554+ monitoring_data = {},
3555+ )
3556+ self .mock_get_monitoring_data .return_value = {
3557+ "v1" : {experiment .slug : self .monitoring_data }
3558+ }
3559+ self .mock_get_funnel_data .side_effect = Exception ("GCS unavailable" )
3560+
3561+ tasks .fetch_monitoring_data ()
3562+
3563+ experiment .refresh_from_db ()
3564+ self .assertEqual (experiment .monitoring_data ["enrollment_funnel" ], [])
3565+
34753566 @parameterized .expand (
34763567 [
34773568 (NimbusExperiment .Status .DRAFT ,),
@@ -3558,8 +3649,8 @@ def test_fetch_monitoring_data_updates_multiple_experiments(self):
35583649
35593650 exp1 .refresh_from_db ()
35603651 exp2 .refresh_from_db ()
3561- self .assertEqual (exp1 .monitoring_data , data1 )
3562- self .assertEqual (exp2 .monitoring_data , data2 )
3652+ self .assertEqual (exp1 .monitoring_data , { ** data1 , "enrollment_funnel" : []} )
3653+ self .assertEqual (exp2 .monitoring_data , { ** data2 , "enrollment_funnel" : []} )
35633654
35643655 def test_fetch_monitoring_data_fatal_error (self ):
35653656 self .mock_get_monitoring_data .side_effect = Exception ("GCS connection failed" )
@@ -3586,7 +3677,9 @@ def test_get_monitoring_data_success(self, mock_load):
35863677 result = get_monitoring_data ()
35873678
35883679 self .assertEqual (result , data )
3589- mock_load .assert_called_once ()
3680+ mock_load .assert_called_once_with (
3681+ "enrollment_counts/enrollment_counts_latest.json"
3682+ )
35903683
35913684 @patch ("experimenter.jetstream.client.load_data_from_gcs" )
35923685 def test_get_monitoring_data_no_data (self , mock_load ):
@@ -3598,7 +3691,37 @@ def test_get_monitoring_data_no_data(self, mock_load):
35983691
35993692 @patch ("experimenter.jetstream.client.load_data_from_gcs" )
36003693 def test_get_monitoring_data_exception (self , mock_load ):
3601- mock_load .side_effect = Exception ("GCS error" )
3694+ mock_load .side_effect = RuntimeError ("GCS error" )
36023695
3603- with self .assertRaises (Exception ):
3696+ with self .assertRaises (RuntimeError ):
36043697 get_monitoring_data ()
3698+
3699+
3700+ @pytest .mark .usefixtures ("mock_funnel_entries" )
3701+ class TestGetEnrollmentFunnelData (TestCase ):
3702+ @patch ("experimenter.jetstream.client.load_data_from_gcs" )
3703+ def test_get_enrollment_funnel_data_success (self , mock_load ):
3704+ data = {"v1" : {"experiment-1" : self .funnel_entries }}
3705+ mock_load .return_value = data
3706+
3707+ result = get_enrollment_funnel_data ()
3708+
3709+ self .assertEqual (result , data )
3710+ mock_load .assert_called_once_with (
3711+ "enrollment_counts/enrollment_funnel_v1_latest.json"
3712+ )
3713+
3714+ @patch ("experimenter.jetstream.client.load_data_from_gcs" )
3715+ def test_get_enrollment_funnel_data_no_data (self , mock_load ):
3716+ mock_load .return_value = None
3717+
3718+ result = get_enrollment_funnel_data ()
3719+
3720+ self .assertIsNone (result )
3721+
3722+ @patch ("experimenter.jetstream.client.load_data_from_gcs" )
3723+ def test_get_enrollment_funnel_data_exception (self , mock_load ):
3724+ mock_load .side_effect = RuntimeError ("GCS error" )
3725+
3726+ with self .assertRaises (RuntimeError ):
3727+ get_enrollment_funnel_data ()
0 commit comments