From 0ff5f3f87559cd9e400f7971f521f26b1abe28d3 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Fri, 7 Aug 2026 16:54:41 +0100 Subject: [PATCH 1/6] feat(onboarding): Org-level onboarding experiment events The onboarding flow decision is now made in the backend, per organisation. Serving a multivariate split assignment records the experiment exposure; the first SDK evaluation of any of the organisation's environments records the conversion, both against the organisation targeting key. - flagsmith 6.2.0 and openfeature-provider-flagsmith from its experimentation-tracking PR branch - events enabled on the Flagsmith-on-Flagsmith client via FLAGSMITH_ON_FLAGSMITH_SERVER_EVENTS_API_URL (set in staging and production ECS task definitions; never enabled in offline mode) - get_openfeature_client() attaches the provider's exposure hook, so any multivariate split evaluation records an exposure - onboarding_variant field on the organisation serialiser beep boop --- api/app/settings/common.py | 3 + api/environments/onboarding/services.py | 13 ++++ api/integrations/flagsmith/client.py | 11 ++- api/organisations/serializers.py | 7 ++ api/organisations/services.py | 17 +++++ api/pyproject.toml | 8 +- ...t_unit_environments_onboarding_services.py | 60 +++++++++++++++ .../flagsmith/test_unit_flagsmith_client.py | 58 ++++++++++++++ .../test_unit_organisations_serializers.py | 24 +++++- .../test_unit_organisations_services.py | 76 +++++++++++++++++++ api/uv.lock | 28 +++---- .../observability/_events-catalogue.md | 4 +- .../ecs-task-definition-admin-api.json | 4 + .../ecs-task-definition-sdk-api.json | 4 + .../ecs-task-definition-task-processor.json | 4 + .../ecs-task-definition-admin-api.json | 4 + .../staging/ecs-task-definition-sdk-api.json | 4 + .../ecs-task-definition-task-processor.json | 4 + 18 files changed, 310 insertions(+), 23 deletions(-) create mode 100644 api/organisations/services.py create mode 100644 api/tests/unit/environments/onboarding/test_unit_environments_onboarding_services.py create mode 100644 api/tests/unit/organisations/test_unit_organisations_services.py diff --git a/api/app/settings/common.py b/api/app/settings/common.py index 572ea0a02d48..8dca1608e0e8 100644 --- a/api/app/settings/common.py +++ b/api/app/settings/common.py @@ -1303,6 +1303,9 @@ FLAGSMITH_ON_FLAGSMITH_SERVER_API_URL = env( "FLAGSMITH_ON_FLAGSMITH_SERVER_API_URL", default=FLAGSMITH_ON_FLAGSMITH_API_URL ) +FLAGSMITH_ON_FLAGSMITH_SERVER_EVENTS_API_URL = env( + "FLAGSMITH_ON_FLAGSMITH_SERVER_EVENTS_API_URL", default=None +) FLAGSMITH_ON_FLAGSMITH_FEATURE_EXPORT_ENVIRONMENT_ID = env.int( "FLAGSMITH_ON_FLAGSMITH_FEATURE_EXPORT_ENVIRONMENT_ID", diff --git a/api/environments/onboarding/services.py b/api/environments/onboarding/services.py index 83b639b88aca..2a5a94bb3e54 100644 --- a/api/environments/onboarding/services.py +++ b/api/environments/onboarding/services.py @@ -1,8 +1,11 @@ import structlog from django.utils import timezone +from openfeature.evaluation_context import EvaluationContext +from openfeature.track import TrackingEventDetails from app_analytics.types import KnownSDK from environments.models import Environment +from integrations.flagsmith.client import get_openfeature_client logger = structlog.get_logger("onboarding") @@ -29,4 +32,14 @@ def record_environment_first_evaluation( Environment.write_environment_documents(environment_id=environment.id) + get_openfeature_client().track( + "environment.first_evaluated", + evaluation_context=EvaluationContext( + targeting_key=environment.project.organisation.openfeature_evaluation_context.targeting_key, + ), + tracking_event_details=TrackingEventDetails( + attributes={"sdk_label": sdk_label}, + ), + ) + log.info("environment.first_evaluated") diff --git a/api/integrations/flagsmith/client.py b/api/integrations/flagsmith/client.py index 4ada0eb41d4a..e1d94c0a62d1 100644 --- a/api/integrations/flagsmith/client.py +++ b/api/integrations/flagsmith/client.py @@ -18,8 +18,10 @@ import openfeature.api as openfeature_api from django.conf import settings from flagsmith import Flagsmith +from flagsmith.analytics import EventProcessorConfig from flagsmith.offline_handlers import LocalFileHandler from openfeature.client import OpenFeatureClient +from openfeature_flagsmith.hooks import FlagsmithExposureHook from openfeature_flagsmith.provider import FlagsmithProvider from integrations.flagsmith.exceptions import FlagsmithIntegrationError @@ -47,6 +49,7 @@ def initialise_provider( flagsmith_client = Flagsmith(**kwargs) provider = FlagsmithProvider(client=flagsmith_client) openfeature_api.set_provider(provider, domain=domain) + openfeature_api.add_hooks([FlagsmithExposureHook(provider)]) def get_provider_kwargs() -> dict[str, typing.Any]: @@ -61,11 +64,17 @@ def get_provider_kwargs() -> dict[str, typing.Any]: settings.FLAGSMITH_ON_FLAGSMITH_SERVER_KEY and settings.FLAGSMITH_ON_FLAGSMITH_SERVER_API_URL ): - return { + kwargs = { "environment_key": settings.FLAGSMITH_ON_FLAGSMITH_SERVER_KEY, "api_url": settings.FLAGSMITH_ON_FLAGSMITH_SERVER_API_URL, **common_kwargs, } + if events_api_url := settings.FLAGSMITH_ON_FLAGSMITH_SERVER_EVENTS_API_URL: + kwargs["enable_events"] = True + kwargs["event_processor_config"] = EventProcessorConfig( + events_api_url=events_api_url, + ) + return kwargs raise FlagsmithIntegrationError( "Must either use offline mode, or provide " diff --git a/api/organisations/serializers.py b/api/organisations/serializers.py index baf70e32ebc7..805077c4ac29 100644 --- a/api/organisations/serializers.py +++ b/api/organisations/serializers.py @@ -11,6 +11,7 @@ get_subscription_data_from_hosted_page, ) from organisations.invites.models import Invite +from organisations.services import OnboardingVariant, get_onboarding_variant from users.models import FFAdminUser, UserPermissionGroup from .models import ( @@ -40,6 +41,7 @@ def get_has_active_billing_periods(self, obj): # type: ignore[no-untyped-def] class OrganisationSerializerFull(serializers.ModelSerializer): # type: ignore[type-arg] subscription = SubscriptionSerializer(required=False) role = serializers.SerializerMethodField() + onboarding_variant = serializers.SerializerMethodField() class Meta: model = Organisation @@ -56,6 +58,7 @@ class Meta: "block_access_to_admin", "restrict_project_create_to_admin", "force_2fa", + "onboarding_variant", ) read_only_fields = ( "id", @@ -72,6 +75,10 @@ def get_role(self, instance): # type: ignore[no-untyped-def] user = self.context["request"].user return user.get_organisation_role(instance) + @extend_schema_field({"type": "string", "enum": ["control", "single_page"]}) + def get_onboarding_variant(self, instance: Organisation) -> OnboardingVariant: + return get_onboarding_variant(instance) + class OrganisationSerializerBasic(serializers.ModelSerializer): # type: ignore[type-arg] class Meta: diff --git a/api/organisations/services.py b/api/organisations/services.py new file mode 100644 index 000000000000..3b7485cbd3d9 --- /dev/null +++ b/api/organisations/services.py @@ -0,0 +1,17 @@ +import typing + +from integrations.flagsmith.client import get_openfeature_client +from organisations.models import Organisation + +OnboardingVariant = typing.Literal["control", "single_page"] + + +def get_onboarding_variant(organisation: Organisation) -> OnboardingVariant: + details = get_openfeature_client().get_boolean_details( + "onboarding_quickstart_flow", + default_value=False, + evaluation_context=organisation.openfeature_evaluation_context, + ) + if not details.value or details.variant == "control": + return "control" + return "single_page" diff --git a/api/pyproject.toml b/api/pyproject.toml index a1c2b0800606..8a001a83fccf 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -67,7 +67,7 @@ dependencies = [ "pydantic>=2.12.0,<3.0.0", "pydantic-collections>=0.6.0,<0.7.0", "pyngo>=2.4.1,<2.5.0", - "flagsmith>=5.3.0,<6.0.0", + "flagsmith>=6.2.0,<7.0.0", "openfeature-sdk>=0.9.0,<0.10.0", "openfeature-provider-flagsmith>=0.2.0", "python-gnupg>=0.5.1,<0.6.0", @@ -159,6 +159,8 @@ explicit = true [tool.uv.sources] flagsmith-private = { index = "flagsmith-pypi-production" } django-clickhouse-backend = { git = "https://github.com/jayvynl/django-clickhouse-backend", rev = "d884c7d" } +# TODO: back to PyPI once Flagsmith/flagsmith-openfeature-provider-python#37 is released +openfeature-provider-flagsmith = { git = "https://github.com/Flagsmith/flagsmith-openfeature-provider-python", rev = "b8aa9310dbedc8658606c0958eae7a78f8dac6df" } [tool.uv] required-version = ">=0.11.18" # Ensure this matches the version in .pre-commit-config.yaml @@ -244,10 +246,10 @@ override-dependencies = [ "executing==2.2.1", "fancycompleter==0.9.1", "filelock==3.20.3", - "flagsmith==5.3.0", + "flagsmith==6.2.0", # flagsmith-common version is set via the [project] dependency spec which # also carries the extras; an override here would strip the extras. - "flagsmith-flag-engine==10.1.0", + "flagsmith-flag-engine==10.2.0", "freezegun==1.5.5", "genson==1.2.2", "google-api-core==2.29.0", diff --git a/api/tests/unit/environments/onboarding/test_unit_environments_onboarding_services.py b/api/tests/unit/environments/onboarding/test_unit_environments_onboarding_services.py new file mode 100644 index 000000000000..003b5237346e --- /dev/null +++ b/api/tests/unit/environments/onboarding/test_unit_environments_onboarding_services.py @@ -0,0 +1,60 @@ +from unittest.mock import MagicMock, Mock + +import pytest +from django.utils import timezone +from pytest_mock import MockerFixture + +from environments.models import Environment +from environments.onboarding.services import record_environment_first_evaluation + + +@pytest.fixture(autouse=True) +def write_environment_documents(mocker: MockerFixture) -> Mock: + return mocker.patch.object(Environment, "write_environment_documents") + + +@pytest.fixture() +def mock_openfeature_client(mocker: MockerFixture) -> MagicMock: + mock_client: MagicMock = mocker.MagicMock() + mocker.patch( + "environments.onboarding.services.get_openfeature_client", + return_value=mock_client, + ) + return mock_client + + +def test_record_environment_first_evaluation__first_evaluation__tracks_conversion_event( + environment: Environment, + mock_openfeature_client: MagicMock, +) -> None: + # Given + organisation_id = environment.project.organisation_id + + # When + record_environment_first_evaluation(environment, "flagsmith-python-sdk") + + # Then + mock_openfeature_client.track.assert_called_once() + call_args = mock_openfeature_client.track.call_args + assert call_args.args == ("environment.first_evaluated",) + assert ( + call_args.kwargs["evaluation_context"].targeting_key == f"org.{organisation_id}" + ) + assert call_args.kwargs["tracking_event_details"].attributes == { + "sdk_label": "flagsmith-python-sdk", + } + + +def test_record_environment_first_evaluation__already_evaluated__does_not_track( + environment: Environment, + mock_openfeature_client: MagicMock, +) -> None: + # Given + environment.first_evaluated_at = timezone.now() + environment.save(update_fields=["first_evaluated_at"]) + + # When + record_environment_first_evaluation(environment, "flagsmith-python-sdk") + + # Then + mock_openfeature_client.track.assert_not_called() diff --git a/api/tests/unit/integrations/flagsmith/test_unit_flagsmith_client.py b/api/tests/unit/integrations/flagsmith/test_unit_flagsmith_client.py index 8af126f9c9cf..5586b24c1074 100644 --- a/api/tests/unit/integrations/flagsmith/test_unit_flagsmith_client.py +++ b/api/tests/unit/integrations/flagsmith/test_unit_flagsmith_client.py @@ -2,6 +2,7 @@ import openfeature.api as openfeature_api import pytest +from flagsmith.analytics import EventProcessorConfig from flagsmith.offline_handlers import LocalFileHandler from openfeature.provider.in_memory_provider import InMemoryProvider from openfeature.provider.metadata import Metadata @@ -230,3 +231,60 @@ def test_get_provider_kwargs__missing_server_key__raises_error( # When / Then with pytest.raises(FlagsmithIntegrationError): get_provider_kwargs() + + +def test_get_provider_kwargs__events_api_url_set__enables_events( + settings: SettingsWrapper, mock_local_file_handler_class: MagicMock +) -> None: + # Given + settings.FLAGSMITH_ON_FLAGSMITH_SERVER_OFFLINE_MODE = False + settings.FLAGSMITH_ON_FLAGSMITH_SERVER_KEY = "ser.some-key" + settings.FLAGSMITH_ON_FLAGSMITH_SERVER_API_URL = "https://my.flagsmith.api/api/v1/" + settings.FLAGSMITH_ON_FLAGSMITH_SERVER_EVENTS_API_URL = ( + "https://events.my.flagsmith.api/" + ) + + # When + kwargs = get_provider_kwargs() + + # Then + assert kwargs["enable_events"] is True + assert kwargs["event_processor_config"] == EventProcessorConfig( + events_api_url="https://events.my.flagsmith.api/", + ) + + +def test_get_provider_kwargs__no_events_api_url__events_not_enabled( + settings: SettingsWrapper, mock_local_file_handler_class: MagicMock +) -> None: + # Given + settings.FLAGSMITH_ON_FLAGSMITH_SERVER_OFFLINE_MODE = False + settings.FLAGSMITH_ON_FLAGSMITH_SERVER_KEY = "ser.some-key" + settings.FLAGSMITH_ON_FLAGSMITH_SERVER_API_URL = "https://my.flagsmith.api/api/v1/" + settings.FLAGSMITH_ON_FLAGSMITH_SERVER_EVENTS_API_URL = None + + # When + kwargs = get_provider_kwargs() + + # Then + assert "enable_events" not in kwargs + assert "event_processor_config" not in kwargs + + +def test_get_provider_kwargs__offline_mode_with_events_api_url__events_not_enabled( + settings: SettingsWrapper, mock_local_file_handler_class: MagicMock +) -> None: + # Given + # The SDK never initialises events in offline mode; don't pretend otherwise. + settings.FLAGSMITH_ON_FLAGSMITH_SERVER_OFFLINE_MODE = True + settings.FLAGSMITH_ON_FLAGSMITH_SERVER_EVENTS_API_URL = ( + "https://events.my.flagsmith.api/" + ) + + # When + kwargs = get_provider_kwargs() + + # Then + assert kwargs["offline_mode"] is True + assert "enable_events" not in kwargs + assert "event_processor_config" not in kwargs diff --git a/api/tests/unit/organisations/test_unit_organisations_serializers.py b/api/tests/unit/organisations/test_unit_organisations_serializers.py index d9033659f800..080fd505cb1e 100644 --- a/api/tests/unit/organisations/test_unit_organisations_serializers.py +++ b/api/tests/unit/organisations/test_unit_organisations_serializers.py @@ -2,7 +2,29 @@ from pytest_mock import MockerFixture from organisations.models import Organisation -from organisations.serializers import UpdateSubscriptionSerializer +from organisations.serializers import ( + OrganisationSerializerFull, + UpdateSubscriptionSerializer, +) + + +def test_organisation_serializer_full__onboarding_org__serialises_onboarding_variant( + organisation: Organisation, + mocker: MockerFixture, +) -> None: + # Given + get_onboarding_variant_mock = mocker.patch( + "organisations.serializers.get_onboarding_variant", + return_value="single_page", + autospec=True, + ) + + # When + data = OrganisationSerializerFull(instance=organisation).data + + # Then + assert data["onboarding_variant"] == "single_page" + get_onboarding_variant_mock.assert_called_once_with(organisation) def test_update_subscription_serializer__create__updates_subscription( diff --git a/api/tests/unit/organisations/test_unit_organisations_services.py b/api/tests/unit/organisations/test_unit_organisations_services.py new file mode 100644 index 000000000000..315a2d13b1d2 --- /dev/null +++ b/api/tests/unit/organisations/test_unit_organisations_services.py @@ -0,0 +1,76 @@ +import openfeature.api as openfeature_api +from openfeature.provider.in_memory_provider import InMemoryFlag, InMemoryProvider + +from integrations.flagsmith.client import DEFAULT_OPENFEATURE_DOMAIN +from organisations.models import Organisation +from organisations.services import get_onboarding_variant + + +def test_get_onboarding_variant__flag_not_configured__returns_control( + organisation: Organisation, +) -> None: + # Given + # the flag doesn't exist + + # When / Then + assert get_onboarding_variant(organisation) == "control" + + +def test_get_onboarding_variant__flag_disabled__returns_control( + organisation: Organisation, +) -> None: + # Given + openfeature_api.set_provider( + InMemoryProvider( + { + "onboarding_quickstart_flow": InMemoryFlag( + variants={"off": False}, + default_variant="off", + ) + } + ), + domain=DEFAULT_OPENFEATURE_DOMAIN, + ) + + # When / Then + assert get_onboarding_variant(organisation) == "control" + + +def test_get_onboarding_variant__variant_assigned__returns_single_page( + organisation: Organisation, +) -> None: + # Given + openfeature_api.set_provider( + InMemoryProvider( + { + "onboarding_quickstart_flow": InMemoryFlag( + variants={"control": True, "single_page": True}, + default_variant="single_page", + ) + } + ), + domain=DEFAULT_OPENFEATURE_DOMAIN, + ) + + # When / Then + assert get_onboarding_variant(organisation) == "single_page" + + +def test_get_onboarding_variant__control_variant_assigned__returns_control( + organisation: Organisation, +) -> None: + # Given + openfeature_api.set_provider( + InMemoryProvider( + { + "onboarding_quickstart_flow": InMemoryFlag( + variants={"control": True, "single_page": True}, + default_variant="control", + ) + } + ), + domain=DEFAULT_OPENFEATURE_DOMAIN, + ) + + # When / Then + assert get_onboarding_variant(organisation) == "control" diff --git a/api/uv.lock b/api/uv.lock index e961a2f4535c..cf2af47c9245 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -85,8 +85,8 @@ overrides = [ { name = "executing", specifier = "==2.2.1" }, { name = "fancycompleter", specifier = "==0.9.1" }, { name = "filelock", specifier = "==3.20.3" }, - { name = "flagsmith", specifier = "==5.3.0" }, - { name = "flagsmith-flag-engine", specifier = "==10.1.0" }, + { name = "flagsmith", specifier = "==6.2.0" }, + { name = "flagsmith-flag-engine", specifier = "==10.2.0" }, { name = "freezegun", specifier = "==1.5.5" }, { name = "genson", specifier = "==1.2.2" }, { name = "google-api-core", specifier = "==2.29.0" }, @@ -133,7 +133,7 @@ overrides = [ { name = "nodeenv", specifier = "==1.9.1" }, { name = "oauth2client", specifier = "==4.1.3" }, { name = "oauthlib", specifier = "==3.2.2" }, - { name = "openfeature-provider-flagsmith", specifier = "==0.2.0" }, + { name = "openfeature-provider-flagsmith", git = "https://github.com/Flagsmith/flagsmith-openfeature-provider-python?rev=b8aa9310dbedc8658606c0958eae7a78f8dac6df" }, { name = "openfeature-sdk", specifier = "==0.9.0" }, { name = "opentelemetry-api", specifier = "==1.40.0" }, { name = "opentelemetry-exporter-otlp-proto-common", specifier = "==1.40.0" }, @@ -1527,7 +1527,7 @@ wheels = [ [[package]] name = "flagsmith" -version = "5.3.0" +version = "6.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "flagsmith-flag-engine" }, @@ -1536,9 +1536,9 @@ dependencies = [ { name = "sseclient-py" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d9/4d/d4e61cddab5cdc62878aeffd4e4947e03470baeea4a29dcd8b3d2fb7de82/flagsmith-5.3.0.tar.gz", hash = "sha256:79befb381fb759d13402ae471fe00c0539ba7799a93edea4b7fa82b7835d0f41", size = 16115, upload-time = "2026-04-28T18:05:33.182Z" } +sdist = { url = "https://files.pythonhosted.org/packages/91/0e/061ecf0eaf8c3dfd6b45b6d403c9dc5d89eff921ed5fa3de91f41cb91e05/flagsmith-6.2.0.tar.gz", hash = "sha256:be0c144016c3cd5ea5c23de86df1d96b97c71a98594cf2b9a31a775f75ee8b11", size = 17209, upload-time = "2026-08-07T10:24:21.812Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/75/54/e9bb3dab6d802b76c2ff9ef26f2e6fc43a58d392598aafa60e9347b9eab3/flagsmith-5.3.0-py3-none-any.whl", hash = "sha256:47a589a6e55cf9151f03092c11aa344e68df44d9b081d83716ca44cf79fc8be1", size = 21089, upload-time = "2026-04-28T18:05:31.953Z" }, + { url = "https://files.pythonhosted.org/packages/f7/61/8972a9d4840306ffa8796fc95c257445a0f46e91a3088446c9368827aab8/flagsmith-6.2.0-py3-none-any.whl", hash = "sha256:2147c751461e8a056ad2a792105b9012938ee299b9108f92e457d396473aca1d", size = 22329, upload-time = "2026-08-07T10:24:20.559Z" }, ] [[package]] @@ -1717,7 +1717,7 @@ requires-dist = [ { name = "drf-writable-nested", specifier = ">=0.6.2,<0.7.0" }, { name = "email-validator", marker = "extra == 'dev'", specifier = ">=2.0.0" }, { name = "environs", specifier = ">=14.1.1,<15.0.0" }, - { name = "flagsmith", specifier = ">=5.3.0,<6.0.0" }, + { name = "flagsmith", specifier = ">=6.2.0,<7.0.0" }, { name = "flagsmith-common", extras = ["common-core", "flagsmith-schemas", "task-processor"], specifier = ">=3.12.1,<4" }, { name = "flagsmith-common", extras = ["test-tools"], marker = "extra == 'dev'" }, { name = "flagsmith-flag-engine", specifier = ">=10.1.0,<11.0.0" }, @@ -1734,7 +1734,7 @@ requires-dist = [ { name = "mypy-boto3-dynamodb", marker = "extra == 'dev'", specifier = ">=1.33.0,<2.0.0" }, { name = "mypy-boto3-s3", marker = "extra == 'dev'", specifier = ">=1.36.0,<2.0.0" }, { name = "oauth2client", specifier = ">=4.1.3,<4.2.0" }, - { name = "openfeature-provider-flagsmith", specifier = ">=0.2.0" }, + { name = "openfeature-provider-flagsmith", git = "https://github.com/Flagsmith/flagsmith-openfeature-provider-python?rev=b8aa9310dbedc8658606c0958eae7a78f8dac6df" }, { name = "openfeature-sdk", specifier = ">=0.9.0,<0.10.0" }, { name = "pdbpp", marker = "extra == 'dev'", specifier = ">=0.10.3,<0.11.0" }, { name = "pep8", marker = "extra == 'dev'", specifier = ">=1.7.1,<1.8.0" }, @@ -1845,16 +1845,16 @@ test-tools = [ [[package]] name = "flagsmith-flag-engine" -version = "10.1.0" +version = "10.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jsonpath-rfc9535" }, { name = "semver" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b4/07/548dcb463fc6dba513b595e49043167efdf787cd1f277ff383390735cbdd/flagsmith_flag_engine-10.1.0.tar.gz", hash = "sha256:fcb7e6833a874001c4ad3b91a66a4c31f050d53d94b116f88ad5c7ecd9650e8a", size = 10992, upload-time = "2026-05-06T08:26:07.293Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/11/aa67ecac85d1879ff116f94c8518c922ed687908dc11f71aebb1a5be5a87/flagsmith_flag_engine-10.2.0.tar.gz", hash = "sha256:d935c9fb639e8acc5b9ff4599ec570e1b2f3f7b7874fc789a6eca3db5665a31b", size = 11056, upload-time = "2026-06-09T07:06:25.609Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/55/cfc11f6bd20209c1b77e8449c01d369d12a87d38c3ac064e3742d1a53389/flagsmith_flag_engine-10.1.0-py3-none-any.whl", hash = "sha256:767dcf2f32586948eaa7816b5cbdae272d76d89e30c4642cbd74894c89a2d469", size = 14181, upload-time = "2026-05-06T08:26:06.131Z" }, + { url = "https://files.pythonhosted.org/packages/62/e5/1eb841fa1dfe6a0e3f53d54ac1cd9e8cdac0ab5cc96dfa58c1c70e8fe897/flagsmith_flag_engine-10.2.0-py3-none-any.whl", hash = "sha256:c9bed3ee15487057dc61144d34d101d98db255f17d2c739f02794841a5c98502", size = 14254, upload-time = "2026-06-09T07:06:24.487Z" }, ] [[package]] @@ -2640,15 +2640,11 @@ wheels = [ [[package]] name = "openfeature-provider-flagsmith" version = "0.2.0" -source = { registry = "https://pypi.org/simple" } +source = { git = "https://github.com/Flagsmith/flagsmith-openfeature-provider-python?rev=b8aa9310dbedc8658606c0958eae7a78f8dac6df#b8aa9310dbedc8658606c0958eae7a78f8dac6df" } dependencies = [ { name = "flagsmith" }, { name = "openfeature-sdk" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/67/98/3db0f72e19114a9e259889ab4ed425899a2448cc839e949b977e56581010/openfeature_provider_flagsmith-0.2.0.tar.gz", hash = "sha256:b5411613fa24efde96356d987b15c3efbb0455c4771306c26d73d526a0021a73", size = 5296, upload-time = "2026-04-30T07:17:21.773Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/15/d7/12486d65e1ff168b5485dba850f55732d77c370a321a5d64a8d0d3937d60/openfeature_provider_flagsmith-0.2.0-py3-none-any.whl", hash = "sha256:5a87df0ddaf0efaa664f0a71a9422733ac4ab7bd47782503039022c9c3206d8e", size = 6557, upload-time = "2026-04-30T07:17:20.57Z" }, -] [[package]] name = "openfeature-sdk" diff --git a/docs/docs/deployment-self-hosting/observability/_events-catalogue.md b/docs/docs/deployment-self-hosting/observability/_events-catalogue.md index 591712fced24..b5c5d016cedb 100644 --- a/docs/docs/deployment-self-hosting/observability/_events-catalogue.md +++ b/docs/docs/deployment-self-hosting/observability/_events-catalogue.md @@ -466,7 +466,7 @@ Attributes: ### `onboarding.environment.already_evaluated` Logged at `info` from: - - `api/environments/onboarding/services.py:23` + - `api/environments/onboarding/services.py:26` Attributes: - `environment.id` @@ -477,7 +477,7 @@ Attributes: ### `onboarding.environment.first_evaluated` Logged at `info` from: - - `api/environments/onboarding/services.py:32` + - `api/environments/onboarding/services.py:45` Attributes: - `environment.id` diff --git a/infrastructure/aws/production/ecs-task-definition-admin-api.json b/infrastructure/aws/production/ecs-task-definition-admin-api.json index 697d657b39d7..9aeb52d20dee 100644 --- a/infrastructure/aws/production/ecs-task-definition-admin-api.json +++ b/infrastructure/aws/production/ecs-task-definition-admin-api.json @@ -212,6 +212,10 @@ "name": "FLAGSMITH_ON_FLAGSMITH_SERVER_API_URL", "value": "https://edge.api.flagsmith.com/api/v1/" }, + { + "name": "FLAGSMITH_ON_FLAGSMITH_SERVER_EVENTS_API_URL", + "value": "https://events.api.flagsmith.com/" + }, { "name": "FLAGSMITH_API_URL", "value": "https://api.flagsmith.com" diff --git a/infrastructure/aws/production/ecs-task-definition-sdk-api.json b/infrastructure/aws/production/ecs-task-definition-sdk-api.json index d3051ea4dac7..4051db72835d 100644 --- a/infrastructure/aws/production/ecs-task-definition-sdk-api.json +++ b/infrastructure/aws/production/ecs-task-definition-sdk-api.json @@ -229,6 +229,10 @@ "name": "FLAGSMITH_ON_FLAGSMITH_SERVER_API_URL", "value": "https://edge.api.flagsmith.com/api/v1/" }, + { + "name": "FLAGSMITH_ON_FLAGSMITH_SERVER_EVENTS_API_URL", + "value": "https://events.api.flagsmith.com/" + }, { "name": "FLAGSMITH_API_URL", "value": "https://api.flagsmith.com" diff --git a/infrastructure/aws/production/ecs-task-definition-task-processor.json b/infrastructure/aws/production/ecs-task-definition-task-processor.json index 84093a250c52..52784ee018c3 100644 --- a/infrastructure/aws/production/ecs-task-definition-task-processor.json +++ b/infrastructure/aws/production/ecs-task-definition-task-processor.json @@ -154,6 +154,10 @@ "name": "FLAGSMITH_ON_FLAGSMITH_SERVER_API_URL", "value": "https://edge.api.flagsmith.com/api/v1/" }, + { + "name": "FLAGSMITH_ON_FLAGSMITH_SERVER_EVENTS_API_URL", + "value": "https://events.api.flagsmith.com/" + }, { "name": "FLAGSMITH_ON_FLAGSMITH_SERVER_OFFLINE_MODE", "value": "False" diff --git a/infrastructure/aws/staging/ecs-task-definition-admin-api.json b/infrastructure/aws/staging/ecs-task-definition-admin-api.json index 197ce6dc362b..726ee1f7fb2d 100644 --- a/infrastructure/aws/staging/ecs-task-definition-admin-api.json +++ b/infrastructure/aws/staging/ecs-task-definition-admin-api.json @@ -225,6 +225,10 @@ "name": "FLAGSMITH_ON_FLAGSMITH_SERVER_API_URL", "value": "https://edge.api.flagsmith.com/api/v1/" }, + { + "name": "FLAGSMITH_ON_FLAGSMITH_SERVER_EVENTS_API_URL", + "value": "https://events.bullet-train-staging.win/" + }, { "name": "FLAGSMITH_API_URL", "value": "https://api-staging.flagsmith.com" diff --git a/infrastructure/aws/staging/ecs-task-definition-sdk-api.json b/infrastructure/aws/staging/ecs-task-definition-sdk-api.json index 3286cd98c4e6..ce504e87c97e 100644 --- a/infrastructure/aws/staging/ecs-task-definition-sdk-api.json +++ b/infrastructure/aws/staging/ecs-task-definition-sdk-api.json @@ -240,6 +240,10 @@ "name": "FLAGSMITH_ON_FLAGSMITH_SERVER_API_URL", "value": "https://edge.api.flagsmith.com/api/v1/" }, + { + "name": "FLAGSMITH_ON_FLAGSMITH_SERVER_EVENTS_API_URL", + "value": "https://events.bullet-train-staging.win/" + }, { "name": "FLAGSMITH_API_URL", "value": "https://api-staging.flagsmith.com" diff --git a/infrastructure/aws/staging/ecs-task-definition-task-processor.json b/infrastructure/aws/staging/ecs-task-definition-task-processor.json index c588ebf331ef..6da5dc76270e 100644 --- a/infrastructure/aws/staging/ecs-task-definition-task-processor.json +++ b/infrastructure/aws/staging/ecs-task-definition-task-processor.json @@ -152,6 +152,10 @@ "name": "FLAGSMITH_ON_FLAGSMITH_SERVER_API_URL", "value": "https://edge.api.flagsmith.com/api/v1/" }, + { + "name": "FLAGSMITH_ON_FLAGSMITH_SERVER_EVENTS_API_URL", + "value": "https://events.bullet-train-staging.win/" + }, { "name": "FLAGSMITH_ON_FLAGSMITH_SERVER_OFFLINE_MODE", "value": "False" From 1b4d68c8d5be0ec9f5f327b1bfc16ddf367d3162 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Fri, 7 Aug 2026 16:58:19 +0100 Subject: [PATCH 2/6] feat(onboarding): Read onboarding variant from the organisation API The gate no longer evaluates the Flagsmith-on-Flagsmith flag client-side; the backend decides per organisation and the exposure is recorded server-side. beep boop --- frontend/common/types/responses.ts | 2 ++ frontend/common/utils/getOnboardingVariant.ts | 13 ---------- .../pages/onboarding/GettingStartedGate.tsx | 24 +++++++------------ 3 files changed, 10 insertions(+), 29 deletions(-) delete mode 100644 frontend/common/utils/getOnboardingVariant.ts diff --git a/frontend/common/types/responses.ts b/frontend/common/types/responses.ts index 0d8a5ba0096b..f50c81d9269a 100644 --- a/frontend/common/types/responses.ts +++ b/frontend/common/types/responses.ts @@ -516,6 +516,7 @@ export type Subscription = { notes: string | null } +export type OnboardingVariant = 'control' | 'single_page' export type Organisation = { id: number name: string @@ -527,6 +528,7 @@ export type Organisation = { persist_trait_data: boolean block_access_to_admin: boolean restrict_project_create_to_admin: boolean + onboarding_variant: OnboardingVariant } export type Identity = { id: string diff --git a/frontend/common/utils/getOnboardingVariant.ts b/frontend/common/utils/getOnboardingVariant.ts deleted file mode 100644 index c684e37ac7fc..000000000000 --- a/frontend/common/utils/getOnboardingVariant.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Utils from './utils' - -export type OnboardingVariant = 'control' | 'single_page' - -// The served variant name picks the arm ('control' is the reserved key the -// API reports for the default arm). Enabled with no variant is the -// pre-conversion boolean flag, which means the new flow. -export const isSinglePageOnboarding = (): boolean => - Utils.getFlagsmithHasFeature('onboarding_quickstart_flow') && - Utils.getFlagsmithVariant('onboarding_quickstart_flow') !== 'control' - -export const getOnboardingVariant = (): OnboardingVariant => - isSinglePageOnboarding() ? 'single_page' : 'control' diff --git a/frontend/web/components/pages/onboarding/GettingStartedGate.tsx b/frontend/web/components/pages/onboarding/GettingStartedGate.tsx index 97291785b515..1e622985aaf2 100644 --- a/frontend/web/components/pages/onboarding/GettingStartedGate.tsx +++ b/frontend/web/components/pages/onboarding/GettingStartedGate.tsx @@ -1,30 +1,22 @@ import React, { FC, useEffect } from 'react' -import flagsmith from '@flagsmith/flagsmith' import ConfigProvider from 'common/providers/ConfigProvider' -import { - getOnboardingVariant, - isSinglePageOnboarding, -} from 'common/utils/getOnboardingVariant' +import AccountStore from 'common/stores/account-store' import API from 'project/api' import GettingStartedPage from 'components/pages/GettingStartedPage' import OnboardingFlow from './OnboardingFlow' const GettingStartedGate: FC = () => { - // ConfigProvider re-renders the gate on every SDK fetch; only tag the - // variant once the server has answered for this identity. - const trustworthy = - !flagsmith.loadingState?.isFetching && - flagsmith.loadingState?.source === 'SERVER' && - !!flagsmith.getContext().identity - - const variant = getOnboardingVariant() + // The backend decides the variant per organisation and serves it on the + // organisation payload; the exposure is recorded server-side. + const organisation = AccountStore.getOrganisation() + const variant = organisation?.onboarding_variant ?? 'control' useEffect(() => { - if (!trustworthy) return + if (!organisation?.id) return API.trackTraits({ onboarding_variant: variant }) - }, [trustworthy, variant]) + }, [organisation?.id, variant]) - return isSinglePageOnboarding() ? : + return variant === 'single_page' ? : } export default ConfigProvider(GettingStartedGate) From 015d7fd0072525629c589b9eaa290019b88fdf78 Mon Sep 17 00:00:00 2001 From: "flagsmith-engineering[bot]" Date: Fri, 7 Aug 2026 16:06:34 +0000 Subject: [PATCH 3/6] chore: Update documentation artefacts --- mcp/src/flagsmith_mcp/openapi.json | 15 +++++++++++++++ openapi.yaml | 13 +++++++++++++ 2 files changed, 28 insertions(+) diff --git a/mcp/src/flagsmith_mcp/openapi.json b/mcp/src/flagsmith_mcp/openapi.json index 330b2f25c097..328286cade66 100644 --- a/mcp/src/flagsmith_mcp/openapi.json +++ b/mcp/src/flagsmith_mcp/openapi.json @@ -5714,6 +5714,13 @@ "NullEnum": { "type": "null" }, + "OnboardingVariantEnum": { + "type": "string", + "enum": [ + "control", + "single_page" + ] + }, "OrganisationSerializerFull": { "type": "object", "properties": { @@ -5773,6 +5780,14 @@ }, "force_2fa": { "type": "boolean" + }, + "onboarding_variant": { + "allOf": [ + { + "$ref": "#/components/schemas/OnboardingVariantEnum" + } + ], + "readOnly": true } }, "required": [ diff --git a/openapi.yaml b/openapi.yaml index ccfdf0f08579..320aff928940 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -22162,6 +22162,11 @@ components: is_new_user: type: boolean readOnly: true + OnboardingVariantEnum: + type: string + enum: + - control + - single_page OrganisationAPIUsageNotification: type: object properties: @@ -22233,6 +22238,10 @@ components: type: boolean force_2fa: type: boolean + onboarding_variant: + allOf: + - $ref: '#/components/schemas/OnboardingVariantEnum' + readOnly: true required: - name OrganisationWebhook: @@ -24185,6 +24194,10 @@ components: type: boolean force_2fa: type: boolean + onboarding_variant: + allOf: + - $ref: '#/components/schemas/OnboardingVariantEnum' + readOnly: true PatchedOrganisationWebhook: type: object properties: From 1935afa1af410e213b3059b8d4371d5a0591c9f1 Mon Sep 17 00:00:00 2001 From: "flagsmith-engineering[bot]" Date: Fri, 7 Aug 2026 16:11:01 +0000 Subject: [PATCH 4/6] chore: Update documentation artefacts --- .../observability/_events-catalogue.md | 2 +- openapi.yaml | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/docs/deployment-self-hosting/observability/_events-catalogue.md b/docs/docs/deployment-self-hosting/observability/_events-catalogue.md index b5c5d016cedb..2b86c1557e47 100644 --- a/docs/docs/deployment-self-hosting/observability/_events-catalogue.md +++ b/docs/docs/deployment-self-hosting/observability/_events-catalogue.md @@ -103,7 +103,7 @@ Attributes: ### `core.encrypted_field.decrypt_failed` Logged at `warning` from: - - `api/core/fields.py:37` + - `api/core/fields.py:62` Attributes: - `exc_info` diff --git a/openapi.yaml b/openapi.yaml index 320aff928940..cd9aec24854b 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -22252,6 +22252,7 @@ components: readOnly: true url: type: string + format: uri maxLength: 200 enabled: type: boolean @@ -24206,6 +24207,7 @@ components: readOnly: true url: type: string + format: uri maxLength: 200 enabled: type: boolean @@ -25058,6 +25060,7 @@ components: url: type: string format: uri + maxLength: 200 enabled: type: boolean created_at: @@ -25079,6 +25082,7 @@ components: readOnly: true url: type: string + format: uri maxLength: 200 secret: type: string @@ -28048,6 +28052,7 @@ components: url: type: string format: uri + maxLength: 200 enabled: type: boolean created_at: @@ -28071,6 +28076,7 @@ components: readOnly: true url: type: string + format: uri maxLength: 200 secret: type: string From c3944b19e273e09006dbb6150279948402f28b20 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Fri, 7 Aug 2026 17:24:57 +0100 Subject: [PATCH 5/6] build(deps): openfeature-provider-flagsmith 1.0.0 from PyPI Also refreshes a stale line reference in the generated events catalogue. beep boop --- api/pyproject.toml | 6 ++---- api/uv.lock | 12 ++++++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/api/pyproject.toml b/api/pyproject.toml index 8a001a83fccf..d45d0b870854 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -69,7 +69,7 @@ dependencies = [ "pyngo>=2.4.1,<2.5.0", "flagsmith>=6.2.0,<7.0.0", "openfeature-sdk>=0.9.0,<0.10.0", - "openfeature-provider-flagsmith>=0.2.0", + "openfeature-provider-flagsmith>=1.0.0,<2.0.0", "python-gnupg>=0.5.1,<0.6.0", "django-redis>=5.4.0,<6.0.0", "pygithub>=2.8,<2.9.0", @@ -159,8 +159,6 @@ explicit = true [tool.uv.sources] flagsmith-private = { index = "flagsmith-pypi-production" } django-clickhouse-backend = { git = "https://github.com/jayvynl/django-clickhouse-backend", rev = "d884c7d" } -# TODO: back to PyPI once Flagsmith/flagsmith-openfeature-provider-python#37 is released -openfeature-provider-flagsmith = { git = "https://github.com/Flagsmith/flagsmith-openfeature-provider-python", rev = "b8aa9310dbedc8658606c0958eae7a78f8dac6df" } [tool.uv] required-version = ">=0.11.18" # Ensure this matches the version in .pre-commit-config.yaml @@ -296,7 +294,7 @@ override-dependencies = [ "nodeenv==1.9.1", "oauth2client==4.1.3", "oauthlib==3.2.2", - "openfeature-provider-flagsmith==0.2.0", + "openfeature-provider-flagsmith==1.0.0", "openfeature-sdk==0.9.0", "opentelemetry-api==1.40.0", "opentelemetry-exporter-otlp-proto-common==1.40.0", diff --git a/api/uv.lock b/api/uv.lock index cf2af47c9245..ff9795ab4c61 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -133,7 +133,7 @@ overrides = [ { name = "nodeenv", specifier = "==1.9.1" }, { name = "oauth2client", specifier = "==4.1.3" }, { name = "oauthlib", specifier = "==3.2.2" }, - { name = "openfeature-provider-flagsmith", git = "https://github.com/Flagsmith/flagsmith-openfeature-provider-python?rev=b8aa9310dbedc8658606c0958eae7a78f8dac6df" }, + { name = "openfeature-provider-flagsmith", specifier = "==1.0.0" }, { name = "openfeature-sdk", specifier = "==0.9.0" }, { name = "opentelemetry-api", specifier = "==1.40.0" }, { name = "opentelemetry-exporter-otlp-proto-common", specifier = "==1.40.0" }, @@ -1734,7 +1734,7 @@ requires-dist = [ { name = "mypy-boto3-dynamodb", marker = "extra == 'dev'", specifier = ">=1.33.0,<2.0.0" }, { name = "mypy-boto3-s3", marker = "extra == 'dev'", specifier = ">=1.36.0,<2.0.0" }, { name = "oauth2client", specifier = ">=4.1.3,<4.2.0" }, - { name = "openfeature-provider-flagsmith", git = "https://github.com/Flagsmith/flagsmith-openfeature-provider-python?rev=b8aa9310dbedc8658606c0958eae7a78f8dac6df" }, + { name = "openfeature-provider-flagsmith", specifier = ">=1.0.0,<2.0.0" }, { name = "openfeature-sdk", specifier = ">=0.9.0,<0.10.0" }, { name = "pdbpp", marker = "extra == 'dev'", specifier = ">=0.10.3,<0.11.0" }, { name = "pep8", marker = "extra == 'dev'", specifier = ">=1.7.1,<1.8.0" }, @@ -2639,12 +2639,16 @@ wheels = [ [[package]] name = "openfeature-provider-flagsmith" -version = "0.2.0" -source = { git = "https://github.com/Flagsmith/flagsmith-openfeature-provider-python?rev=b8aa9310dbedc8658606c0958eae7a78f8dac6df#b8aa9310dbedc8658606c0958eae7a78f8dac6df" } +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "flagsmith" }, { name = "openfeature-sdk" }, ] +sdist = { url = "https://files.pythonhosted.org/packages/f2/c2/73618a78380b3996cecb71827ee58dcaab699f54b428b40ed1a037fe1fbc/openfeature_provider_flagsmith-1.0.0.tar.gz", hash = "sha256:21b3ef02e6c0716a81500e39b1f489028765e13ccf76df191fc2d2315ae2bbee", size = 8177, upload-time = "2026-08-07T16:18:54.076Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/7b/1ded625a9c1bacc0564378fe309c8af19282474e4e35d3f1fa71db99a2c5/openfeature_provider_flagsmith-1.0.0-py3-none-any.whl", hash = "sha256:9cc510ec288ae38a1ae8b7d0cad38ea46a802c8b5a20e7df197d5c8dfc1a01bd", size = 10529, upload-time = "2026-08-07T16:18:52.96Z" }, +] [[package]] name = "openfeature-sdk" From 9d61fbe3135afa1c581134427dbda454f7cb6881 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Fri, 7 Aug 2026 17:51:52 +0100 Subject: [PATCH 6/6] improve test_initialise_provider tests --- .../flagsmith/test_unit_flagsmith_client.py | 35 +++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/api/tests/unit/integrations/flagsmith/test_unit_flagsmith_client.py b/api/tests/unit/integrations/flagsmith/test_unit_flagsmith_client.py index 5586b24c1074..ba607afb9a08 100644 --- a/api/tests/unit/integrations/flagsmith/test_unit_flagsmith_client.py +++ b/api/tests/unit/integrations/flagsmith/test_unit_flagsmith_client.py @@ -156,11 +156,22 @@ def test_initialise_provider__offline_mode_disabled__initialises_with_server_key settings.FLAGSMITH_ON_FLAGSMITH_SERVER_API_URL = api_url settings.FLAGSMITH_ON_FLAGSMITH_SERVER_OFFLINE_MODE = False - mock_flagsmith_class = mocker.patch("integrations.flagsmith.client.Flagsmith") + mock_flagsmith_class = mocker.patch( + "integrations.flagsmith.client.Flagsmith", + autospec=True, + ) mock_provider_class = mocker.patch( - "integrations.flagsmith.client.FlagsmithProvider" + "integrations.flagsmith.client.FlagsmithProvider", + autospec=True, + ) + mock_exposure_hook_class = mocker.patch( + "integrations.flagsmith.client.FlagsmithExposureHook", + autospec=True, + ) + mock_openfeature_api = mocker.patch( + "integrations.flagsmith.client.openfeature_api", + autospec=True, ) - mock_openfeature_api = mocker.patch("integrations.flagsmith.client.openfeature_api") # When initialise_provider(**get_provider_kwargs()) @@ -182,6 +193,11 @@ def test_initialise_provider__offline_mode_disabled__initialises_with_server_key domain=DEFAULT_OPENFEATURE_DOMAIN, ) + mock_exposure_hook_class.assert_called_once_with(mock_provider_class.return_value) + mock_openfeature_api.add_hooks.assert_called_once_with( + [mock_exposure_hook_class.return_value] + ) + mock_local_file_handler_class.assert_called_once_with(ENVIRONMENT_JSON_PATH) @@ -194,11 +210,18 @@ def test_initialise_provider__offline_mode_enabled__initialises_with_offline_han # Given settings.FLAGSMITH_ON_FLAGSMITH_SERVER_OFFLINE_MODE = True - mock_flagsmith_class = mocker.patch("integrations.flagsmith.client.Flagsmith") + mock_flagsmith_class = mocker.patch( + "integrations.flagsmith.client.Flagsmith", + autospec=True, + ) mock_provider_class = mocker.patch( - "integrations.flagsmith.client.FlagsmithProvider" + "integrations.flagsmith.client.FlagsmithProvider", + autospec=True, + ) + mock_openfeature_api = mocker.patch( + "integrations.flagsmith.client.openfeature_api", + autospec=True, ) - mock_openfeature_api = mocker.patch("integrations.flagsmith.client.openfeature_api") # When initialise_provider(**get_provider_kwargs())