11import pytest
2+ from growthbook .common_types import FeatureResult
3+ from sqlalchemy import select
24
35from couchers import experimentation
46from couchers .config import config
57from couchers .context import make_background_user_context , make_logged_out_context
8+ from couchers .db import session_scope
9+ from couchers .experimentation import _record_feature_usage
610from couchers .i18n import LocalizationContext
11+ from couchers .models .logging import FeatureUsage
712
813
914@pytest .fixture
@@ -22,7 +27,7 @@ def experimentation_snapshot(monkeypatch):
2227 monkeypatch .setitem (config , "EXPERIMENTATION_PASS_ALL_GATES" , False )
2328
2429
25- def test_logged_in_user_is_bucketed_into_rollout (experimentation_snapshot ):
30+ def test_logged_in_user_is_bucketed_into_rollout (db , experimentation_snapshot ):
2631 context = make_background_user_context (123 )
2732 assert context .get_string_value ("rollout_flag" , "fallback" ) == "treatment"
2833
@@ -41,3 +46,53 @@ def test_anonymous_user_still_gets_global_force_on_flag(experimentation_snapshot
4146def test_unknown_feature_returns_in_code_default (experimentation_snapshot ):
4247 context = make_logged_out_context (LocalizationContext .en_utc ())
4348 assert context .get_string_value ("does_not_exist" , "my_default" ) == "my_default"
49+
50+
51+ def _get_usage (session , user_id ):
52+ return (
53+ session .execute (select (FeatureUsage ).where (FeatureUsage .user_id == user_id ).order_by (FeatureUsage .id ))
54+ .scalars ()
55+ .all ()
56+ )
57+
58+
59+ def test_record_feature_usage_appends_a_row (db ):
60+ _record_feature_usage (1 , "my_feature" , FeatureResult (value = True , source = "defaultValue" ))
61+
62+ with session_scope () as session :
63+ rows = _get_usage (session , 1 )
64+ assert len (rows ) == 1
65+ assert rows [0 ].feature_key == "my_feature"
66+ assert rows [0 ].value is True
67+ assert rows [0 ].time is not None
68+
69+
70+ def test_record_feature_usage_appends_a_row_per_check (db ):
71+ # every check appends - the log is append-only, not deduplicated per (user, feature)
72+ _record_feature_usage (1 , "my_feature" , FeatureResult (value = "first" , source = "force" ))
73+ _record_feature_usage (1 , "my_feature" , FeatureResult (value = "second" , source = "force" ))
74+
75+ with session_scope () as session :
76+ rows = _get_usage (session , 1 )
77+ assert len (rows ) == 2
78+ assert [row .value for row in rows ] == ["first" , "second" ]
79+
80+
81+ def test_record_feature_usage_records_each_user_and_feature (db ):
82+ _record_feature_usage (1 , "feature_a" , FeatureResult (value = 1 , source = "force" ))
83+ _record_feature_usage (1 , "feature_b" , FeatureResult (value = 2 , source = "force" ))
84+ _record_feature_usage (2 , "feature_a" , FeatureResult (value = 3 , source = "force" ))
85+
86+ with session_scope () as session :
87+ assert {row .feature_key for row in _get_usage (session , 1 )} == {"feature_a" , "feature_b" }
88+ assert len (_get_usage (session , 2 )) == 1
89+
90+
91+ def test_record_feature_usage_none_value (db ):
92+ # unknown features evaluate to a None value - must persist without violating NOT NULL
93+ _record_feature_usage (1 , "unknown_feature" , FeatureResult (value = None , source = "unknownFeature" ))
94+
95+ with session_scope () as session :
96+ rows = _get_usage (session , 1 )
97+ assert len (rows ) == 1
98+ assert rows [0 ].value is None
0 commit comments