From 9b6552bd72a87750d4e9afe560587566a0f06025 Mon Sep 17 00:00:00 2001 From: Evandro Myller Date: Mon, 5 Jan 2026 12:35:23 -0300 Subject: [PATCH 1/3] Archive deprecated LaunchDarkly flags on import --- api/integrations/launch_darkly/constants.py | 1 + api/integrations/launch_darkly/models.py | 1 + api/integrations/launch_darkly/serializers.py | 1 + api/integrations/launch_darkly/services.py | 16 ++++++++++++++-- .../client_responses/get_flags.json | 5 +++++ ...orrectly_imported__import_request_status.json | 1 + .../integrations/launch_darkly/test_services.py | 6 ++++++ .../integrations/launch_darkly/test_views.py | 2 ++ .../data-management/import-from-launchdarkly.md | 12 ++++++++++++ 9 files changed, 43 insertions(+), 2 deletions(-) diff --git a/api/integrations/launch_darkly/constants.py b/api/integrations/launch_darkly/constants.py index 21085564bf07..73547d7e378e 100644 --- a/api/integrations/launch_darkly/constants.py +++ b/api/integrations/launch_darkly/constants.py @@ -9,6 +9,7 @@ LAUNCH_DARKLY_IMPORTED_TAG_COLOR = "#3d4db6" LAUNCH_DARKLY_IMPORTED_DEFAULT_TAG_LABEL = "Imported" +LAUNCH_DARKLY_IMPORTED_DEPRECATED_TAG_LABEL = "Deprecated" BACKOFF_MAX_RETRIES = 5 BACKOFF_DEFAULT_RETRY_AFTER_SECONDS = 10 diff --git a/api/integrations/launch_darkly/models.py b/api/integrations/launch_darkly/models.py index d59034eb2799..f80d30ca73cf 100644 --- a/api/integrations/launch_darkly/models.py +++ b/api/integrations/launch_darkly/models.py @@ -17,6 +17,7 @@ class LaunchDarklyImportStatus(TypedDict): requested_environment_count: int requested_flag_count: int + deprecated_flag_count: NotRequired[int] result: NotRequired[LaunchDarklyImportResult] error_messages: list[str] diff --git a/api/integrations/launch_darkly/serializers.py b/api/integrations/launch_darkly/serializers.py index c9f1645b876d..9986db3a6f2a 100644 --- a/api/integrations/launch_darkly/serializers.py +++ b/api/integrations/launch_darkly/serializers.py @@ -11,6 +11,7 @@ class LaunchDarklyImportRequestStatusSerializer(serializers.Serializer): # type: ignore[type-arg] requested_environment_count = serializers.IntegerField(read_only=True) requested_flag_count = serializers.IntegerField(read_only=True) + deprecated_flag_count = serializers.IntegerField(read_only=True, default=0) result = serializers.ChoiceField( get_args(LaunchDarklyImportResult), read_only=True, diff --git a/api/integrations/launch_darkly/services.py b/api/integrations/launch_darkly/services.py index 84ca823f3594..8357e2930465 100644 --- a/api/integrations/launch_darkly/services.py +++ b/api/integrations/launch_darkly/services.py @@ -28,6 +28,7 @@ from integrations.launch_darkly.client import LaunchDarklyClient from integrations.launch_darkly.constants import ( LAUNCH_DARKLY_IMPORTED_DEFAULT_TAG_LABEL, + LAUNCH_DARKLY_IMPORTED_DEPRECATED_TAG_LABEL, LAUNCH_DARKLY_IMPORTED_TAG_COLOR, ) from integrations.launch_darkly.exceptions import LaunchDarklyRateLimitError @@ -124,7 +125,11 @@ def _create_tags_from_ld( ) -> dict[str, Tag]: tags_by_ld_tag = {} - for ld_tag in (*ld_tags, LAUNCH_DARKLY_IMPORTED_DEFAULT_TAG_LABEL): + for ld_tag in ( + *ld_tags, + LAUNCH_DARKLY_IMPORTED_DEFAULT_TAG_LABEL, + LAUNCH_DARKLY_IMPORTED_DEPRECATED_TAG_LABEL, + ): tags_by_ld_tag[ld_tag], _ = Tag.objects.update_or_create( label=ld_tag, project_id=project_id, @@ -903,6 +908,8 @@ def _create_feature_from_ld( tags_by_ld_tag[LAUNCH_DARKLY_IMPORTED_DEFAULT_TAG_LABEL], *(tags_by_ld_tag[ld_tag] for ld_tag in ld_flag["tags"]), ] + if ld_flag["deprecated"]: + tags.append(tags_by_ld_tag[LAUNCH_DARKLY_IMPORTED_DEPRECATED_TAG_LABEL]) feature, _ = Feature.objects.update_or_create( project_id=project_id, @@ -911,7 +918,7 @@ def _create_feature_from_ld( "description": ld_flag.get("description"), "default_enabled": False, "type": feature_type, - "is_archived": ld_flag["archived"], + "is_archived": ld_flag["archived"] or ld_flag["deprecated"], }, ) feature.tags.set(tags) @@ -1169,3 +1176,8 @@ def process_import_request( segments_by_ld_key=segments_by_ld_key, project_id=import_request.project_id, ) + + # Count deprecated flags for reporting + import_request.status["deprecated_flag_count"] = sum( + 1 for ld_flag in ld_flags if ld_flag["deprecated"] + ) diff --git a/api/tests/unit/integrations/launch_darkly/client_responses/get_flags.json b/api/tests/unit/integrations/launch_darkly/client_responses/get_flags.json index 22245da752c9..19184591949a 100644 --- a/api/tests/unit/integrations/launch_darkly/client_responses/get_flags.json +++ b/api/tests/unit/integrations/launch_darkly/client_responses/get_flags.json @@ -35,6 +35,7 @@ "offVariation": 1, "onVariation": 0 }, + "deprecated": true, "description": "", "environments": { "production": { @@ -162,6 +163,7 @@ "offVariation": 1, "onVariation": 0 }, + "deprecated": false, "description": "", "environments": { "production": { @@ -289,6 +291,7 @@ "offVariation": 1, "onVariation": 0 }, + "deprecated": false, "description": "", "environments": { "production": { @@ -420,6 +423,7 @@ "offVariation": 1, "onVariation": 0 }, + "deprecated": false, "description": "", "environments": { "production": { @@ -558,6 +562,7 @@ "offVariation": 1, "onVariation": 0 }, + "deprecated": false, "description": "", "environments": { "production": { diff --git a/api/tests/unit/integrations/launch_darkly/snapshots/test_process_import_request__large_segments__correctly_imported__import_request_status.json b/api/tests/unit/integrations/launch_darkly/snapshots/test_process_import_request__large_segments__correctly_imported__import_request_status.json index 4c7e66bb9417..5c8dd5470a34 100644 --- a/api/tests/unit/integrations/launch_darkly/snapshots/test_process_import_request__large_segments__correctly_imported__import_request_status.json +++ b/api/tests/unit/integrations/launch_darkly/snapshots/test_process_import_request__large_segments__correctly_imported__import_request_status.json @@ -1,4 +1,5 @@ { + "deprecated_flag_count": 1, "error_messages": [ "Targeting key 'user-...63a49' exceeds the limit of 1000 characters, skipping for segment 'Large User List (Override for test)'", "Segment condition value '.*1a6...\\.com' for property 'email' exceeds the limit of 1000 characters, skipping for segment 'Large Dynamic List (Override for test)'", diff --git a/api/tests/unit/integrations/launch_darkly/test_services.py b/api/tests/unit/integrations/launch_darkly/test_services.py index 46385c887f7d..2482af6c13a7 100644 --- a/api/tests/unit/integrations/launch_darkly/test_services.py +++ b/api/tests/unit/integrations/launch_darkly/test_services.py @@ -143,11 +143,13 @@ def test_process_import_request__success__expected_status( # type: ignore[no-un ("testtag", "#3d4db6"), ("testtag2", "#3d4db6"), ("Imported", "#3d4db6"), + ("Deprecated", "#3d4db6"), } assert set( Feature.objects.filter(project=project).values_list("name", "tags__label") ) == { ("flag1", "Imported"), + ("flag1", "Deprecated"), ("flag2_value", "Imported"), ("flag3_multivalue", "Imported"), ("flag4_multivalue", "Imported"), @@ -160,6 +162,10 @@ def test_process_import_request__success__expected_status( # type: ignore[no-un ("TEST_COMBINED_TARGET", "Imported"), } + # Deprecated flags are archived. + deprecated_feature = Feature.objects.get(project=project, name="flag1") + assert deprecated_feature.is_archived is True + # Standard feature states have expected values. boolean_standard_feature = Feature.objects.get(project=project, name="flag1") boolean_standard_feature_states_by_env_name = { diff --git a/api/tests/unit/integrations/launch_darkly/test_views.py b/api/tests/unit/integrations/launch_darkly/test_views.py index 8c661e8ae252..e901a0332e56 100644 --- a/api/tests/unit/integrations/launch_darkly/test_views.py +++ b/api/tests/unit/integrations/launch_darkly/test_views.py @@ -49,6 +49,7 @@ def test_launch_darkly_import_request_view__list__return_expected( "id": import_request.id, "project": project.id, "status": { + "deprecated_flag_count": 0, "error_messages": [], "requested_environment_count": 2, "requested_flag_count": 9, @@ -92,6 +93,7 @@ def test_launch_darkly_import_request_view__create__return_expected( "id": created_import_request.id, "project": project.id, "status": { + "deprecated_flag_count": 0, "error_messages": [], "requested_environment_count": 2, "requested_flag_count": 9, diff --git a/docs/docs/administration-and-security/data-management/import-from-launchdarkly.md b/docs/docs/administration-and-security/data-management/import-from-launchdarkly.md index 770eb5bfc4f1..e7f3a3c492b3 100644 --- a/docs/docs/administration-and-security/data-management/import-from-launchdarkly.md +++ b/docs/docs/administration-and-security/data-management/import-from-launchdarkly.md @@ -65,3 +65,15 @@ Multivariate LaunchDarkly flags will be imported into Flagsmith as MultiVariate 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. + +#### Archived and deprecated flags + +Archived flags in LaunchDarkly are imported as archived flags in Flagsmith. + +Deprecated flags in LaunchDarkly are also imported as archived flags in Flagsmith. They receive a "Deprecated" tag to distinguish them from flags that were archived in LaunchDarkly. + +:::note + +In both LaunchDarkly and Flagsmith, archived flags continue to evaluate normally for SDKs. Archiving a flag hides it from the dashboard but does not affect evaluation. + +::: From cfedde232b2cc1daec8a42e20cc4e655bf461544 Mon Sep 17 00:00:00 2001 From: Evandro Myller Date: Fri, 6 Feb 2026 18:32:53 -0300 Subject: [PATCH 2/3] Improve UI around importing from LaunchDarkly --- frontend/common/types/responses.ts | 3 +- .../components/import-export/ImportPage.tsx | 55 ++++++++----------- frontend/web/project/toast.tsx | 1 + 3 files changed, 27 insertions(+), 32 deletions(-) diff --git a/frontend/common/types/responses.ts b/frontend/common/types/responses.ts index 7f3f5236c767..b5f2d7e577ab 100644 --- a/frontend/common/types/responses.ts +++ b/frontend/common/types/responses.ts @@ -186,8 +186,9 @@ export type LaunchDarklyProjectImport = { status: { requested_environment_count: number requested_flag_count: number + deprecated_flag_count?: number result: string | null - error_message: string | null + error_messages: string[] } project: number } diff --git a/frontend/web/components/import-export/ImportPage.tsx b/frontend/web/components/import-export/ImportPage.tsx index 9d8311270deb..9dcc40952016 100644 --- a/frontend/web/components/import-export/ImportPage.tsx +++ b/frontend/web/components/import-export/ImportPage.tsx @@ -27,51 +27,43 @@ const ImportPage: FC = ({ projectId, projectName }) => { const [LDKey, setLDKey] = useState('') const [importId, setImportId] = useState() const [isLoading, setIsLoading] = useState(false) - const [isAppLoading, setAppIsLoading] = useState(false) const [projects, setProjects] = useState<{ key: string; name: string }[]>([]) const [createLaunchDarklyProjectImport, { data, isSuccess }] = useCreateLaunchDarklyProjectImportMutation() - const { - data: status, - isSuccess: statusLoaded, - isUninitialized, - refetch, - } = useGetLaunchDarklyProjectImportQuery( + const { data: status } = useGetLaunchDarklyProjectImportQuery( { import_id: `${importId}`, project_id: projectId, }, - { skip: !importId }, + { + pollingInterval: importId ? 1000 : 0, + skip: !importId, + }, ) useEffect(() => { - const checkImportStatus = async () => { - setAppIsLoading(true) - const intervalId = setInterval(async () => { - await refetch() - - if (statusLoaded && status && status.status.result === 'success') { - clearInterval(intervalId) - setAppIsLoading(false) - window.location.reload() - } - }, 1000) - } - - if (statusLoaded) { - checkImportStatus() + if (isSuccess && data?.id) { + setImportId(data.id) } - }, [statusLoaded, status, refetch]) + }, [isSuccess, data]) useEffect(() => { - if (isSuccess && data?.id) { - setImportId(data.id) - if (!isUninitialized) { - refetch() + if (status?.status?.result === 'success') { + const count = status.status.requested_flag_count + const deprecated = status.status.deprecated_flag_count ?? 0 + let message = `Imported ${count} flag${count !== 1 ? 's' : ''} from LaunchDarkly.` + if (deprecated > 0) { + message += ` ${deprecated} deprecated flag${deprecated !== 1 ? 's were' : ' was'} archived.` } + toast(message, 'success', 0) + history.push(`/project/${projectId}`) + } else if (status?.status?.result === 'failure') { + const errors = status.status.error_messages.join('; ') + toast(`Importing from LaunchDarkly failed: ${errors}`, 'danger', 0) + setImportId(undefined) } - }, [isSuccess, data, refetch, isUninitialized]) + }, [status, projectId, history]) const getProjectList = (LDKey: string) => { setIsLoading(true) @@ -218,11 +210,12 @@ const ImportPage: FC = ({ projectId, projectName }) => { ) + const isImporting = !!importId && status?.status?.result !== 'success' + return ( <> - {isAppLoading && ( + {isImporting && (
-
Importing Project
)} diff --git a/frontend/web/project/toast.tsx b/frontend/web/project/toast.tsx index 48d22712b023..9e7a08da56e6 100644 --- a/frontend/web/project/toast.tsx +++ b/frontend/web/project/toast.tsx @@ -68,6 +68,7 @@ const Message: FC = ({ }) => { const theme = _theme || 'success' useEffect(() => { + if (!expiry) return const timeout = setTimeout(remove, expiry) return () => clearTimeout(timeout) }, [remove, expiry]) From 1278f0469e274f3eb46bfeeff13392d2dd30df9d Mon Sep 17 00:00:00 2001 From: Evandro Myller Date: Fri, 6 Feb 2026 18:39:49 -0300 Subject: [PATCH 3/3] No slop --- .../data-management/import-from-launchdarkly.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/docs/administration-and-security/data-management/import-from-launchdarkly.md b/docs/docs/administration-and-security/data-management/import-from-launchdarkly.md index e7f3a3c492b3..3c0bc71bb0bf 100644 --- a/docs/docs/administration-and-security/data-management/import-from-launchdarkly.md +++ b/docs/docs/administration-and-security/data-management/import-from-launchdarkly.md @@ -71,9 +71,3 @@ Values set to serve when targeting is off will be imported as control values. Archived flags in LaunchDarkly are imported as archived flags in Flagsmith. Deprecated flags in LaunchDarkly are also imported as archived flags in Flagsmith. They receive a "Deprecated" tag to distinguish them from flags that were archived in LaunchDarkly. - -:::note - -In both LaunchDarkly and Flagsmith, archived flags continue to evaluate normally for SDKs. Archiving a flag hides it from the dashboard but does not affect evaluation. - -:::