Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/integrations/launch_darkly/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions api/integrations/launch_darkly/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
1 change: 1 addition & 0 deletions api/integrations/launch_darkly/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
16 changes: 14 additions & 2 deletions api/integrations/launch_darkly/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand Down Expand Up @@ -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"]
)
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"offVariation": 1,
"onVariation": 0
},
"deprecated": true,
"description": "",
"environments": {
"production": {
Expand Down Expand Up @@ -162,6 +163,7 @@
"offVariation": 1,
"onVariation": 0
},
"deprecated": false,
"description": "",
"environments": {
"production": {
Expand Down Expand Up @@ -289,6 +291,7 @@
"offVariation": 1,
"onVariation": 0
},
"deprecated": false,
"description": "",
"environments": {
"production": {
Expand Down Expand Up @@ -420,6 +423,7 @@
"offVariation": 1,
"onVariation": 0
},
"deprecated": false,
"description": "",
"environments": {
"production": {
Expand Down Expand Up @@ -558,6 +562,7 @@
"offVariation": 1,
"onVariation": 0
},
"deprecated": false,
"description": "",
"environments": {
"production": {
Expand Down
Original file line number Diff line number Diff line change
@@ -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)'",
Expand Down
6 changes: 6 additions & 0 deletions api/tests/unit/integrations/launch_darkly/test_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -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 = {
Expand Down
2 changes: 2 additions & 0 deletions api/tests/unit/integrations/launch_darkly/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,9 @@ 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.
3 changes: 2 additions & 1 deletion frontend/common/types/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
55 changes: 24 additions & 31 deletions frontend/web/components/import-export/ImportPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,51 +27,43 @@ const ImportPage: FC<ImportPageType> = ({ projectId, projectName }) => {
const [LDKey, setLDKey] = useState<string>('')
const [importId, setImportId] = useState<number>()
const [isLoading, setIsLoading] = useState<boolean>(false)
const [isAppLoading, setAppIsLoading] = useState<boolean>(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)
Expand Down Expand Up @@ -218,11 +210,12 @@ const ImportPage: FC<ImportPageType> = ({ projectId, projectName }) => {
</>
)

const isImporting = !!importId && status?.status?.result !== 'success'

return (
<>
{isAppLoading && (
{isImporting && (
<div className='overlay'>
<div className='title'>Importing Project</div>
<AppLoader />
</div>
)}
Expand Down
1 change: 1 addition & 0 deletions frontend/web/project/toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const Message: FC<MessageProps> = ({
}) => {
const theme = _theme || 'success'
useEffect(() => {
if (!expiry) return
const timeout = setTimeout(remove, expiry)
return () => clearTimeout(timeout)
}, [remove, expiry])
Expand Down
Loading