Skip to content

Commit

Permalink
feat: partial imports, off values as control value (#2864)
Browse files Browse the repository at this point in the history
  • Loading branch information
khvn26 authored Oct 23, 2023
1 parent fd161e1 commit 93df958
Show file tree
Hide file tree
Showing 5 changed files with 69 additions and 24 deletions.
52 changes: 38 additions & 14 deletions api/integrations/launch_darkly/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from django.core import signing
from django.utils import timezone
from requests.exceptions import HTTPError, RequestException
from requests.exceptions import RequestException

from environments.models import Environment
from features.feature_types import MULTIVARIATE, STANDARD, FeatureType
Expand Down Expand Up @@ -41,6 +41,13 @@ def _unsign_ld_value(value: str, user_id: int) -> str:
)


def _log_error(
import_request: LaunchDarklyImportRequest,
error_message: str,
) -> None:
import_request.status["error_message"] = error_message


@contextmanager
def _complete_import_request(
import_request: LaunchDarklyImportRequest,
Expand Down Expand Up @@ -164,31 +171,49 @@ def _create_mv_feature_states(
environments_by_ld_environment_key: dict[str, Environment],
) -> None:
variations = ld_flag["variations"]
variation_values_by_idx: dict[str, str] = {}
mv_feature_options_by_variation: dict[str, MultivariateFeatureOption] = {}

for idx, variation in enumerate(variations):
variation_idx = str(idx)
variation_value = variation["value"]
variation_values_by_idx[variation_idx] = variation_value
(
mv_feature_options_by_variation[str(idx)],
_,
) = MultivariateFeatureOption.objects.update_or_create(
feature=feature,
string_value=variation["value"],
string_value=variation_value,
defaults={"default_percentage_allocation": 0, "type": STRING},
)

for ld_environment_key, environment in environments_by_ld_environment_key.items():
ld_flag_config = ld_flag["environments"][ld_environment_key]

is_flag_on = ld_flag_config["on"]

feature_state, _ = FeatureState.objects.update_or_create(
feature=feature,
environment=environment,
defaults={"enabled": ld_flag_config["on"]},
defaults={"enabled": is_flag_on},
)

cumulative_rollout = rollout_baseline = 0

if ld_flag_config_summary := ld_flag_config.get("_summary"):
enabled_variations = ld_flag_config_summary.get("variations") or {}
for variation_idx, variation_config in enabled_variations.items():
if variation_config.get("isOff"):
# Set LD's off value as the control value.
# We expect only one off variation.
FeatureStateValue.objects.update_or_create(
feature_state=feature_state,
defaults={
"type": STRING,
"string_value": variation_values_by_idx[variation_idx],
},
)

mv_feature_option = mv_feature_options_by_variation[variation_idx]
percentage_allocation = 0

Expand Down Expand Up @@ -298,13 +323,12 @@ def create_import_request(
ld_token: str,
) -> LaunchDarklyImportRequest:
ld_client = LaunchDarklyClient(ld_token)
ld_project = ld_client.get_project(project_key=ld_project_key)

requested_environment_count = ld_project["environments"]["totalCount"]
ld_project = ld_client.get_project(project_key=ld_project_key)
requested_flag_count = ld_client.get_flag_count(project_key=ld_project_key)

status: LaunchDarklyImportStatus = {
"requested_environment_count": requested_environment_count,
"requested_environment_count": ld_project["environments"]["totalCount"],
"requested_flag_count": requested_flag_count,
}

Expand Down Expand Up @@ -333,15 +357,15 @@ def process_import_request(
ld_environments = ld_client.get_environments(project_key=ld_project_key)
ld_flags = ld_client.get_flags(project_key=ld_project_key)
ld_tags = ld_client.get_flag_tags()
except HTTPError as exc:
import_request.status[
"error_message"
] = f"HTTP {exc.response.status_code} when requesting LaunchDarkly"
raise
except RequestException as exc:
import_request.status[
"error_message"
] = f"{exc.__class__.__name__} when requesting LaunchDarkly"
_log_error(
import_request=import_request,
error_message=(
f"{exc.__class__.__name__} "
f"{str(exc.response.status_code) + ' ' if exc.response else ''}"
f"when requesting {exc.request.path_url}"
),
)
raise

environments_by_ld_environment_key = _create_environments_from_ld(
Expand Down
7 changes: 2 additions & 5 deletions api/integrations/launch_darkly/tasks.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
from django.db import transaction

from integrations.launch_darkly.models import LaunchDarklyImportRequest
from integrations.launch_darkly.services import process_import_request
from task_processor.decorators import register_task_handler


@register_task_handler()
def process_launch_darkly_import_request(import_request_id: int) -> None:
with transaction.atomic():
import_request = LaunchDarklyImportRequest.objects.get(id=import_request_id)
process_import_request(import_request)
import_request = LaunchDarklyImportRequest.objects.get(id=import_request_id)
process_import_request(import_request)
22 changes: 17 additions & 5 deletions api/tests/unit/integrations/launch_darkly/test_services.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
from typing import Type
from unittest.mock import MagicMock

import pytest
from django.core import signing
from requests.exceptions import HTTPError, Timeout
from requests.exceptions import HTTPError, RequestException, Timeout

from environments.models import Environment
from features.models import Feature, FeatureState
Expand Down Expand Up @@ -60,20 +59,21 @@ def test_create_import_request__return_expected(
[
(
HTTPError(response=MagicMock(status_code=503)),
"HTTP 503 when requesting LaunchDarkly",
"HTTPError 503 when requesting /expected_path",
),
(Timeout(), "Timeout when requesting LaunchDarkly"),
(Timeout(), "Timeout when requesting /expected_path"),
],
)
def test_process_import_request__api_error__expected_status(
ld_client_mock: MagicMock,
ld_client_class_mock: MagicMock,
failing_ld_client_method_name: str,
exception: Type[Exception],
exception: RequestException,
expected_error_message: str,
import_request: LaunchDarklyImportRequest,
) -> None:
# Given
exception.request = MagicMock(path_url="/expected_path")
getattr(ld_client_mock, failing_ld_client_method_name).side_effect = exception

# When
Expand Down Expand Up @@ -187,6 +187,12 @@ def test_process_import_request__success__expected_status(
}

assert percentage_mv_feature_states_by_env_name["Test"].enabled is False

# The `off` variation from LD's environment is imported as the control value.
assert (
percentage_mv_feature_states_by_env_name["Test"].get_feature_state_value()
== "variation2"
)
assert list(
percentage_mv_feature_states_by_env_name[
"Test"
Expand All @@ -197,6 +203,12 @@ def test_process_import_request__success__expected_status(
) == [("variation1", 100), ("variation2", 0), ("variation3", 0)]

assert percentage_mv_feature_states_by_env_name["Production"].enabled is True

# The `off` variation from LD's environment is imported as the control value.
assert (
percentage_mv_feature_states_by_env_name["Production"].get_feature_state_value()
== "variation3"
)
assert list(
percentage_mv_feature_states_by_env_name[
"Production"
Expand Down
8 changes: 8 additions & 0 deletions docs/docs/integrations/importers/launchdarkly.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ import has finished.

:::

:::caution

Import operations will overwrite existing environments and flags in your project.

:::

## Integration Setup

1. Create a LaunchDarkly Access Token. In LaunchDarkly: Account settings > Authorization > Access tokens.
Expand Down Expand Up @@ -45,3 +51,5 @@ Boolean values will be taken from the `_summary -> on` field of within LaunchDar
Multivariate LaunchDarkly flags will be imported into Flagsmith as MultiVariate Flagsmith flag values.

Multivariate values will be taken from the `variations` field of within LaunchDarkly.

Values set to serve when targeting is off will be imported as control values.
4 changes: 4 additions & 0 deletions frontend/web/components/pages/ImportPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
useGetLaunchDarklyProjectImportQuery,
} from 'common/services/useLaunchDarklyProjectImport'
import AppLoader from 'components/AppLoader'
import InfoMessage from 'components/InfoMessage'

type ImportPageType = {
projectId: string
Expand Down Expand Up @@ -83,6 +84,9 @@ const ImportPage: FC<ImportPageType> = ({ projectId, projectName }) => {
</div>
)}
<div className='mt-4'>
<InfoMessage>
Import operations will overwrite existing environments and flags in your project.
</InfoMessage>
<h5>Import LaunchDarkly Projects</h5>
<label>Set LaunchDarkly key</label>
<FormGroup>
Expand Down

3 comments on commit 93df958

@vercel
Copy link

@vercel vercel bot commented on 93df958 Oct 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on 93df958 Oct 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on 93df958 Oct 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

docs – ./docs

docs-git-main-flagsmith.vercel.app
docs-flagsmith.vercel.app
docs.bullet-train.io
docs.flagsmith.com

Please sign in to comment.