From aef53abce1d9cd875f71513eddc57c9d4f1ad47d Mon Sep 17 00:00:00 2001 From: genrichez <2.2434764e+07+genrichez@users.noreply.github.com> Date: Sun, 26 Jul 2026 12:30:16 +0000 Subject: [PATCH 1/5] fix: send falsy variant values to Mixpanel and Rudderstack integrations --- api/integrations/mixpanel/mixpanel.py | 4 +- api/integrations/rudderstack/rudderstack.py | 4 +- .../mixpanel/test_unit_mixpanel.py | 69 ++++++++++++++++++- .../rudderstack/test_unit_rudderstack.py | 39 +++++++++++ 4 files changed, 112 insertions(+), 4 deletions(-) diff --git a/api/integrations/mixpanel/mixpanel.py b/api/integrations/mixpanel/mixpanel.py index cc2820aaa53c..2e98ab171dd1 100644 --- a/api/integrations/mixpanel/mixpanel.py +++ b/api/integrations/mixpanel/mixpanel.py @@ -47,7 +47,9 @@ def generate_user_data( for feature_state in feature_states: value = feature_state.get_feature_state_value(identity=identity) feature_properties[feature_state.feature.name] = ( - value if (feature_state.enabled and value) else feature_state.enabled + value + if (feature_state.enabled and value is not None) + else feature_state.enabled ) return [ diff --git a/api/integrations/rudderstack/rudderstack.py b/api/integrations/rudderstack/rudderstack.py index 1c6a248ca8c7..b0e19b5490aa 100644 --- a/api/integrations/rudderstack/rudderstack.py +++ b/api/integrations/rudderstack/rudderstack.py @@ -32,7 +32,9 @@ def generate_user_data( for feature_state in feature_states: value = feature_state.get_feature_state_value(identity=identity) feature_properties[feature_state.feature.name] = ( - value if (feature_state.enabled and value) else feature_state.enabled + value + if (feature_state.enabled and value is not None) + else feature_state.enabled ) return { diff --git a/api/tests/unit/integrations/mixpanel/test_unit_mixpanel.py b/api/tests/unit/integrations/mixpanel/test_unit_mixpanel.py index aca925dc0c85..1525698c606d 100644 --- a/api/tests/unit/integrations/mixpanel/test_unit_mixpanel.py +++ b/api/tests/unit/integrations/mixpanel/test_unit_mixpanel.py @@ -1,3 +1,4 @@ +import typing from logging import DEBUG from typing import TYPE_CHECKING @@ -13,7 +14,7 @@ from environments.identities.models import Identity from environments.models import Environment - from features.models import Feature + from features.models import Feature, FeatureState from projects.models import Project @@ -140,7 +141,9 @@ def test_mixpanel_generate_user_data__identity_with_features__returns_expected_f for feature_state in feature_states: value = feature_state.get_feature_state_value() feature_properties[feature_state.feature.name] = ( - value if (feature_state.enabled and value) else feature_state.enabled + value + if (feature_state.enabled and value is not None) + else feature_state.enabled ) expected_user_data = [ @@ -199,3 +202,65 @@ def test_identify_integrations__mixpanel_configured__posts_to_expected_url( # Then assert mocked_post.call_args.args[0] == expected_url assert mocked_post.call_args.kwargs["json"][0]["$token"] == api_key + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "feature_state_with_value,expected_property_value", + [(False, False), (True, True), ("foo", "foo"), (1, 1), (0, 0)], + indirect=["feature_state_with_value"], +) +def test_mixpanel_generate_user_data__falsy_values__returns_value_not_enabled_state( + expected_property_value: typing.Any, + environment: "Environment", + feature_state: "FeatureState", + feature_state_with_value: "FeatureState", + identity: "Identity", +) -> None: + # Given + config = MixpanelConfiguration(api_key="123key") + mixpanel = MixpanelWrapper(config) + + # When + user_data = mixpanel.generate_user_data( + identity=identity, + feature_states=[feature_state, feature_state_with_value], + trait_models=[], + ) + + # Then + expected_user_data = [ + { + "$token": config.api_key, + "$distinct_id": identity.identifier, + "$set": { + feature_state.feature.name: feature_state.enabled, + feature_state_with_value.feature.name: expected_property_value, + }, + "$ip": "0", + } + ] + assert user_data == expected_user_data + + +@pytest.mark.django_db +def test_mixpanel_generate_user_data__disabled_flag__returns_enabled_state( + feature: "Feature", + environment: "Environment", + identity: "Identity", +) -> None: + # Given + config = MixpanelConfiguration(api_key="123key") + mixpanel = MixpanelWrapper(config) + feature_states = [*feature.feature_states.all()] + + # When + user_data = mixpanel.generate_user_data( + identity=identity, + feature_states=feature_states, + trait_models=[], + ) + + # Then + # Default feature is not enabled, so the value should be False + assert user_data[0]["$set"][feature.name] is False diff --git a/api/tests/unit/integrations/rudderstack/test_unit_rudderstack.py b/api/tests/unit/integrations/rudderstack/test_unit_rudderstack.py index 6f86fb58b07d..841deed6a797 100644 --- a/api/tests/unit/integrations/rudderstack/test_unit_rudderstack.py +++ b/api/tests/unit/integrations/rudderstack/test_unit_rudderstack.py @@ -1,3 +1,7 @@ +import typing + +import pytest + from environments.identities.models import Identity from environments.models import Environment from features.models import Feature, FeatureState @@ -27,3 +31,38 @@ def test_rudderstack_generate_user_data__valid_identity__returns_expected_data( "user_id": identity.identifier, "traits": {feature.name: False}, } + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "feature_state_with_value,expected_property_value", + [(False, False), (True, True), ("foo", "foo"), (1, 1), (0, 0)], + indirect=["feature_state_with_value"], +) +def test_rudderstack_generate_user_data__falsy_values__returns_value_not_enabled_state( + expected_property_value: typing.Any, + environment: Environment, + feature_state: FeatureState, + feature_state_with_value: FeatureState, + identity: Identity, +) -> None: + # Given + config = RudderstackConfiguration( + api_key="123key", base_url="https://api.rudderstack.com/" + ) + rudderstack_wrapper = RudderstackWrapper(config) + + # When + user_data = rudderstack_wrapper.generate_user_data( + identity=identity, + feature_states=[feature_state, feature_state_with_value], + ) + + # Then + assert user_data == { + "user_id": identity.identifier, + "traits": { + feature_state.feature.name: feature_state.enabled, + feature_state_with_value.feature.name: expected_property_value, + }, + } From 9a27fadd7acf76ec296b2afdaef2afbb86b136af Mon Sep 17 00:00:00 2001 From: genrichez <2.2434764e+07+genrichez@users.noreply.github.com> Date: Sun, 26 Jul 2026 13:00:55 +0000 Subject: [PATCH 2/5] test: add None value case to parametrized integration tests --- api/tests/unit/integrations/mixpanel/test_unit_mixpanel.py | 2 +- .../unit/integrations/rudderstack/test_unit_rudderstack.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/tests/unit/integrations/mixpanel/test_unit_mixpanel.py b/api/tests/unit/integrations/mixpanel/test_unit_mixpanel.py index 1525698c606d..d392f521a38c 100644 --- a/api/tests/unit/integrations/mixpanel/test_unit_mixpanel.py +++ b/api/tests/unit/integrations/mixpanel/test_unit_mixpanel.py @@ -207,7 +207,7 @@ def test_identify_integrations__mixpanel_configured__posts_to_expected_url( @pytest.mark.django_db @pytest.mark.parametrize( "feature_state_with_value,expected_property_value", - [(False, False), (True, True), ("foo", "foo"), (1, 1), (0, 0)], + [(False, False), (True, True), ("foo", "foo"), (1, 1), (0, 0), (None, True)], indirect=["feature_state_with_value"], ) def test_mixpanel_generate_user_data__falsy_values__returns_value_not_enabled_state( diff --git a/api/tests/unit/integrations/rudderstack/test_unit_rudderstack.py b/api/tests/unit/integrations/rudderstack/test_unit_rudderstack.py index 841deed6a797..20d56179dd20 100644 --- a/api/tests/unit/integrations/rudderstack/test_unit_rudderstack.py +++ b/api/tests/unit/integrations/rudderstack/test_unit_rudderstack.py @@ -36,7 +36,7 @@ def test_rudderstack_generate_user_data__valid_identity__returns_expected_data( @pytest.mark.django_db @pytest.mark.parametrize( "feature_state_with_value,expected_property_value", - [(False, False), (True, True), ("foo", "foo"), (1, 1), (0, 0)], + [(False, False), (True, True), ("foo", "foo"), (1, 1), (0, 0), (None, True)], indirect=["feature_state_with_value"], ) def test_rudderstack_generate_user_data__falsy_values__returns_value_not_enabled_state( From 3af58af535abb758ca6aab81f9dbff9d2c899c19 Mon Sep 17 00:00:00 2001 From: genrichez <2.2434764e+07+genrichez@users.noreply.github.com> Date: Sun, 26 Jul 2026 21:39:40 +0000 Subject: [PATCH 3/5] test: add empty string case to falsy value parametrized tests --- api/tests/unit/integrations/mixpanel/test_unit_mixpanel.py | 2 +- .../unit/integrations/rudderstack/test_unit_rudderstack.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/tests/unit/integrations/mixpanel/test_unit_mixpanel.py b/api/tests/unit/integrations/mixpanel/test_unit_mixpanel.py index d392f521a38c..974acd48b7ac 100644 --- a/api/tests/unit/integrations/mixpanel/test_unit_mixpanel.py +++ b/api/tests/unit/integrations/mixpanel/test_unit_mixpanel.py @@ -207,7 +207,7 @@ def test_identify_integrations__mixpanel_configured__posts_to_expected_url( @pytest.mark.django_db @pytest.mark.parametrize( "feature_state_with_value,expected_property_value", - [(False, False), (True, True), ("foo", "foo"), (1, 1), (0, 0), (None, True)], + [(False, False), (True, True), ("foo", "foo"), (1, 1), (0, 0), ("", ""), (None, True)], indirect=["feature_state_with_value"], ) def test_mixpanel_generate_user_data__falsy_values__returns_value_not_enabled_state( diff --git a/api/tests/unit/integrations/rudderstack/test_unit_rudderstack.py b/api/tests/unit/integrations/rudderstack/test_unit_rudderstack.py index 20d56179dd20..4a587be059ca 100644 --- a/api/tests/unit/integrations/rudderstack/test_unit_rudderstack.py +++ b/api/tests/unit/integrations/rudderstack/test_unit_rudderstack.py @@ -36,7 +36,7 @@ def test_rudderstack_generate_user_data__valid_identity__returns_expected_data( @pytest.mark.django_db @pytest.mark.parametrize( "feature_state_with_value,expected_property_value", - [(False, False), (True, True), ("foo", "foo"), (1, 1), (0, 0), (None, True)], + [(False, False), (True, True), ("foo", "foo"), (1, 1), (0, 0), ("", ""), (None, True)], indirect=["feature_state_with_value"], ) def test_rudderstack_generate_user_data__falsy_values__returns_value_not_enabled_state( From c34e3719b991534533ab7ab55297b268eb3df52a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 26 Jul 2026 21:41:40 +0000 Subject: [PATCH 4/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../unit/integrations/mixpanel/test_unit_mixpanel.py | 10 +++++++++- .../integrations/rudderstack/test_unit_rudderstack.py | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/api/tests/unit/integrations/mixpanel/test_unit_mixpanel.py b/api/tests/unit/integrations/mixpanel/test_unit_mixpanel.py index 974acd48b7ac..640ecc29320e 100644 --- a/api/tests/unit/integrations/mixpanel/test_unit_mixpanel.py +++ b/api/tests/unit/integrations/mixpanel/test_unit_mixpanel.py @@ -207,7 +207,15 @@ def test_identify_integrations__mixpanel_configured__posts_to_expected_url( @pytest.mark.django_db @pytest.mark.parametrize( "feature_state_with_value,expected_property_value", - [(False, False), (True, True), ("foo", "foo"), (1, 1), (0, 0), ("", ""), (None, True)], + [ + (False, False), + (True, True), + ("foo", "foo"), + (1, 1), + (0, 0), + ("", ""), + (None, True), + ], indirect=["feature_state_with_value"], ) def test_mixpanel_generate_user_data__falsy_values__returns_value_not_enabled_state( diff --git a/api/tests/unit/integrations/rudderstack/test_unit_rudderstack.py b/api/tests/unit/integrations/rudderstack/test_unit_rudderstack.py index 4a587be059ca..094db360c073 100644 --- a/api/tests/unit/integrations/rudderstack/test_unit_rudderstack.py +++ b/api/tests/unit/integrations/rudderstack/test_unit_rudderstack.py @@ -36,7 +36,15 @@ def test_rudderstack_generate_user_data__valid_identity__returns_expected_data( @pytest.mark.django_db @pytest.mark.parametrize( "feature_state_with_value,expected_property_value", - [(False, False), (True, True), ("foo", "foo"), (1, 1), (0, 0), ("", ""), (None, True)], + [ + (False, False), + (True, True), + ("foo", "foo"), + (1, 1), + (0, 0), + ("", ""), + (None, True), + ], indirect=["feature_state_with_value"], ) def test_rudderstack_generate_user_data__falsy_values__returns_value_not_enabled_state( From 3db7c73f720badbc451265c288edc6e337ff304f Mon Sep 17 00:00:00 2001 From: genrichez <2.2434764e+07+genrichez@users.noreply.github.com> Date: Mon, 27 Jul 2026 08:50:40 +0000 Subject: [PATCH 5/5] docs: add inline comments documenting the value-mapping contract --- api/integrations/mixpanel/mixpanel.py | 3 +++ api/integrations/rudderstack/rudderstack.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/api/integrations/mixpanel/mixpanel.py b/api/integrations/mixpanel/mixpanel.py index 2e98ab171dd1..b2102d75f8b3 100644 --- a/api/integrations/mixpanel/mixpanel.py +++ b/api/integrations/mixpanel/mixpanel.py @@ -46,6 +46,9 @@ def generate_user_data( for feature_state in feature_states: value = feature_state.get_feature_state_value(identity=identity) + # enabled + value set (incl. falsy like 0 or "") -> value + # enabled + value is None -> True + # disabled -> False feature_properties[feature_state.feature.name] = ( value if (feature_state.enabled and value is not None) diff --git a/api/integrations/rudderstack/rudderstack.py b/api/integrations/rudderstack/rudderstack.py index b0e19b5490aa..f88d38b345ef 100644 --- a/api/integrations/rudderstack/rudderstack.py +++ b/api/integrations/rudderstack/rudderstack.py @@ -31,6 +31,9 @@ def generate_user_data( for feature_state in feature_states: value = feature_state.get_feature_state_value(identity=identity) + # enabled + value set (incl. falsy like 0 or "") -> value + # enabled + value is None -> True + # disabled -> False feature_properties[feature_state.feature.name] = ( value if (feature_state.enabled and value is not None)