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..d45d0b870854 100644
--- a/api/pyproject.toml
+++ b/api/pyproject.toml
@@ -67,9 +67,9 @@ 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",
+ "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",
@@ -244,10 +244,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",
@@ -294,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/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..ba607afb9a08 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
@@ -155,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())
@@ -181,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)
@@ -193,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())
@@ -230,3 +254,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..ff9795ab4c61 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", 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" },
@@ -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", 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" },
@@ -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]]
@@ -2639,15 +2639,15 @@ wheels = [
[[package]]
name = "openfeature-provider-flagsmith"
-version = "0.2.0"
+version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
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" }
+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/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" },
+ { 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]]
diff --git a/docs/docs/deployment-self-hosting/observability/_events-catalogue.md b/docs/docs/deployment-self-hosting/observability/_events-catalogue.md
index 591712fced24..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`
@@ -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/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)
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"
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..cd9aec24854b 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:
@@ -22243,6 +22252,7 @@ components:
readOnly: true
url:
type: string
+ format: uri
maxLength: 200
enabled:
type: boolean
@@ -24185,6 +24195,10 @@ components:
type: boolean
force_2fa:
type: boolean
+ onboarding_variant:
+ allOf:
+ - $ref: '#/components/schemas/OnboardingVariantEnum'
+ readOnly: true
PatchedOrganisationWebhook:
type: object
properties:
@@ -24193,6 +24207,7 @@ components:
readOnly: true
url:
type: string
+ format: uri
maxLength: 200
enabled:
type: boolean
@@ -25045,6 +25060,7 @@ components:
url:
type: string
format: uri
+ maxLength: 200
enabled:
type: boolean
created_at:
@@ -25066,6 +25082,7 @@ components:
readOnly: true
url:
type: string
+ format: uri
maxLength: 200
secret:
type: string
@@ -28035,6 +28052,7 @@ components:
url:
type: string
format: uri
+ maxLength: 200
enabled:
type: boolean
created_at:
@@ -28058,6 +28076,7 @@ components:
readOnly: true
url:
type: string
+ format: uri
maxLength: 200
secret:
type: string