Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enable feature versioning for environments in newly created projects #5108

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ repos:
args:
- --check
types: [javascript, jsx, ts, tsx, markdown, mdx, html, css, json, yaml]

- id: python-typecheck
name: python-typecheck
language: system
Expand Down
1 change: 1 addition & 0 deletions api/app/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -1124,6 +1124,7 @@
FLAGSMITH_ON_FLAGSMITH_SERVER_API_URL = env(
"FLAGSMITH_ON_FLAGSMITH_SERVER_API_URL", default=FLAGSMITH_ON_FLAGSMITH_API_URL
)
USE_GLOBAL_FLAGSMITH_CLIENTS = True

FLAGSMITH_ON_FLAGSMITH_FEATURE_EXPORT_ENVIRONMENT_ID = env.int(
"FLAGSMITH_ON_FLAGSMITH_FEATURE_EXPORT_ENVIRONMENT_ID",
Expand Down
2 changes: 2 additions & 0 deletions api/app/settings/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,5 @@
ENABLE_POSTPONE_DECORATOR = False

DEBUG = True

USE_GLOBAL_FLAGSMITH_CLIENTS = False
19 changes: 19 additions & 0 deletions api/environments/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import datetime
import logging
import typing
import uuid
Expand All @@ -16,6 +17,7 @@
AFTER_DELETE,
AFTER_SAVE,
AFTER_UPDATE,
BEFORE_CREATE,
LifecycleModel,
hook,
)
Expand All @@ -42,6 +44,7 @@
from environments.managers import EnvironmentManager
from features.models import Feature, FeatureSegment, FeatureState
from features.multivariate.models import MultivariateFeatureStateValue
from integrations.flagsmith.client import get_client
from metadata.models import Metadata
from projects.models import Project
from segments.models import Segment
Expand Down Expand Up @@ -165,6 +168,22 @@ def delete_from_dynamo(self):

delete_environment_from_dynamo.delay(args=(self.api_key, self.id))

@hook(BEFORE_CREATE)
def enable_v2_versioning(self):
flagsmith_client = get_client("local", local_eval=True)
organisation = self.project.organisation
flag = flagsmith_client.get_identity_flags(
organisation.flagsmith_identifier,
traits={"organisation_id": organisation.id},
).get_flag("enable_feature_versioning_for_new_projects")

if (
flag.enabled
and self.project.created_date.date()
>= datetime.date.fromisoformat(flag.value)
):
self.use_v2_feature_versioning = True

def __str__(self):
return "Project %s - Environment %s" % (self.project.name, self.name)

Expand Down
16 changes: 9 additions & 7 deletions api/integrations/flagsmith/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@
def get_client(name: str = "default", local_eval: bool = False) -> Flagsmith:
global _flagsmith_clients

try:
_flagsmith_client = _flagsmith_clients[name]
except (KeyError, TypeError):
kwargs = _get_client_kwargs()
kwargs["enable_local_evaluation"] = local_eval
_flagsmith_client = Flagsmith(**kwargs)
_flagsmith_clients[name] = _flagsmith_client
if settings.USE_GLOBAL_FLAGSMITH_CLIENTS and (
client := _flagsmith_clients.get(name)
):
return client

kwargs = _get_client_kwargs()
kwargs["enable_local_evaluation"] = local_eval
_flagsmith_client = Flagsmith(**kwargs)
_flagsmith_clients[name] = _flagsmith_client

return _flagsmith_client

Expand Down
7 changes: 7 additions & 0 deletions api/tests/types.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import typing
from typing import Callable, Literal

from environments.permissions.models import UserEnvironmentPermission
Expand All @@ -15,3 +16,9 @@
]

AdminClientAuthType = Literal["user", "master_api_key"]


class TestFlagData(typing.NamedTuple):
feature_name: str
enabled: bool
value: typing.Any
60 changes: 60 additions & 0 deletions api/tests/unit/conftest.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
import typing

import pytest
from flag_engine.environments.models import EnvironmentModel
from flag_engine.features.models import FeatureModel, FeatureStateModel
from flag_engine.organisations.models import OrganisationModel
from flag_engine.projects.models import ProjectModel
from flagsmith.offline_handlers import BaseOfflineHandler
from pytest_mock import MockerFixture

from environments.models import Environment
from features.feature_types import STANDARD
from features.models import Feature
from organisations.models import Organisation, OrganisationRole
from projects.models import Project
from projects.tags.models import Tag
from tests.types import TestFlagData
from users.models import FFAdminUser


Expand Down Expand Up @@ -200,3 +210,53 @@ def project_two_feature(project_two: Project) -> Feature:
return Feature.objects.create(
name="project_two_feature", project=project_two, initial_value="initial_value"
)


@pytest.fixture()
def set_flagsmith_client_flags(
mocker: MockerFixture,
) -> typing.Callable[[list[TestFlagData]], None]:
class TestOfflineHandler(BaseOfflineHandler):
def __init__(self):
self.environment = EnvironmentModel(
id=1,
api_key="flagsmith-environment-key",
project=ProjectModel(
id=1,
name="flagsmith-project",
organisation=OrganisationModel(
id=1,
name="flagsmith-organisation",
feature_analytics=False,
stop_serving_flags=False,
persist_trait_data=False,
),
hide_disabled_flags=False,
),
feature_states=[],
)

def set_flags(self, flags: list[TestFlagData]) -> None:
self.environment.feature_states = [
FeatureStateModel(
feature=FeatureModel(
id=i, name=flag_data.feature_name, type=STANDARD
),
enabled=flag_data.enabled,
feature_state_value=flag_data.value,
)
for i, flag_data in enumerate(flags)
]

def get_environment(self) -> EnvironmentModel:
return self.environment

offline_handler = TestOfflineHandler()
mocker.patch(
"integrations.flagsmith.client.LocalFileHandler", return_value=offline_handler
)

def _setter(flags: list[TestFlagData]) -> None:
offline_handler.set_flags(flags)

return _setter
64 changes: 64 additions & 0 deletions api/tests/unit/environments/test_unit_environments_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from organisations.models import Organisation, OrganisationRole
from projects.models import EdgeV2MigrationStatus, Project
from segments.models import Segment
from tests.types import TestFlagData
from util.mappers import map_environment_to_environment_document

if typing.TYPE_CHECKING:
Expand Down Expand Up @@ -1021,3 +1022,66 @@ def test_environment_clone_async(
"clone_environment_id": cloned_environment.id,
}
)


def test_environment_create_with_use_v2_feature_versioning_true(
project: Project,
environment_v2_versioning: Environment,
feature: Feature,
set_flagsmith_client_flags: typing.Callable[[list[TestFlagData]], None],
) -> None:
# Given
set_flagsmith_client_flags(
[TestFlagData("enable_feature_versioning_for_new_projects", True, "2025-02-17")]
)

# When
new_environment = Environment.objects.create(
name="new-environment",
project=project,
)

# Then
assert EnvironmentFeatureVersion.objects.filter(
environment=new_environment, feature=feature
).exists()


def test_environment_clone_from_versioned_environment_with_use_v2_feature_versioning_true(
project: Project,
environment_v2_versioning: Environment,
feature: Feature,
set_flagsmith_client_flags: typing.Callable[[list[TestFlagData]], None],
) -> None:
# Given
set_flagsmith_client_flags(
[TestFlagData("enable_feature_versioning_for_new_projects", True, "2025-02-17")]
)

# When
new_environment = environment_v2_versioning.clone(name="new-environment")

# Then
assert EnvironmentFeatureVersion.objects.filter(
environment=new_environment, feature=feature
).exists()


def test_environment_clone_from_non_versioned_environment_with_use_v2_feature_versioning_true(
project: Project,
environment: Environment,
feature: Feature,
set_flagsmith_client_flags: typing.Callable[[list[TestFlagData]], None],
) -> None:
# Given
set_flagsmith_client_flags(
[TestFlagData("enable_feature_versioning_for_new_projects", True, "2025-02-17")]
)

# When
new_environment = environment.clone(name="new-environment")

# Then
assert not EnvironmentFeatureVersion.objects.filter(
environment=new_environment, feature=feature
).exists()
Loading