Skip to content

Commit dca612f

Browse files
authored
Merge branch 'main' into chore/make-add-known-sdk-version-28934229164
2 parents d4614b9 + 60aeb71 commit dca612f

30 files changed

Lines changed: 476 additions & 660 deletions

api/app_analytics/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
"8.0.2",
6969
"8.1.0",
7070
"8.1.1",
71+
"8.1.2",
7172
"9.0.0",
7273
],
7374
"flagsmith-php-sdk": [

api/integrations/launch_darkly/services.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1106,6 +1106,13 @@ def create_import_request(
11061106
def process_import_request(
11071107
import_request: LaunchDarklyImportRequest,
11081108
) -> None:
1109+
if import_request.completed_at is not None:
1110+
logger.warning(
1111+
"Ignoring already-completed LaunchDarkly import request %d.",
1112+
import_request.id,
1113+
)
1114+
return
1115+
11091116
with _complete_import_request(import_request):
11101117
ld_token = _unsign_ld_value(
11111118
import_request.ld_token,

api/segment_membership/tasks.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,10 +212,14 @@ def refresh_project_segment_counts(project_id: int) -> None:
212212

213213
project = Project.objects.select_related("organisation").get(pk=project_id)
214214
if not is_membership_enabled(project.organisation):
215+
deleted, _ = SegmentMembershipCount.objects.filter(
216+
segment__project=project
217+
).delete()
215218
logger.info(
216219
"refresh.project.skipped",
217220
project__id=project_id,
218221
reason="ff_disabled",
222+
stale_counts__count=deleted,
219223
)
220224
return
221225

api/tests/unit/integrations/launch_darkly/test_services.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from common.test_tools import SnapshotFixture
99
from django.conf import settings
1010
from django.core import signing
11+
from django.utils import timezone
1112
from flag_engine.segments import constants as segment_constants
1213
from pytest_mock import MockerFixture
1314
from requests.exceptions import HTTPError, RequestException, Timeout
@@ -262,6 +263,27 @@ def test_process_import_request__success__expected_status( # type: ignore[no-un
262263
[tag.label for tag in tagged_feature.tags.all()] == ["testtag", "testtag2"]
263264

264265

266+
@pytest.mark.django_db(transaction=True)
267+
def test_process_import_request__already_completed__does_not_reprocess(
268+
ld_client_class_mock: MagicMock,
269+
ld_client_mock: MagicMock,
270+
import_request: LaunchDarklyImportRequest,
271+
) -> None:
272+
# Given
273+
import_request.status["result"] = "success"
274+
import_request.completed_at = timezone.now()
275+
import_request.ld_token = ""
276+
import_request.save()
277+
ld_client_class_mock.reset_mock()
278+
279+
# When
280+
process_import_request(import_request)
281+
282+
# Then
283+
ld_client_class_mock.assert_not_called()
284+
ld_client_mock.get_environments.assert_not_called()
285+
286+
265287
@pytest.mark.django_db(transaction=True)
266288
def test_process_import_request__valid_segments__imports_correctly( # type: ignore[no-untyped-def]
267289
project: Project,

api/tests/unit/segment_membership/test_unit_segment_membership_tasks.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -449,23 +449,36 @@ def test_refresh_project_segment_counts__no_clickhouse_creds__skips(
449449
)
450450

451451

452-
def test_refresh_project_segment_counts__ff_disabled__skips(
452+
def test_refresh_project_segment_counts__ff_disabled__skips_and_purges_stale_counts(
453453
mocker: MockerFixture,
454454
settings: SettingsWrapper,
455455
project: Project,
456+
environment: Environment,
457+
segment: Segment,
456458
log: StructuredLogCapture,
457459
) -> None:
458460
# Given
459461
settings.CLICKHOUSE_ENABLED = True
460462
spy = mocker.patch.object(tasks, "open_clickhouse_cursor")
463+
SegmentMembershipCount.objects.create(
464+
segment=segment,
465+
environment=environment,
466+
count=15,
467+
last_synced_at=timezone.now(),
468+
)
461469

462470
# When
463471
refresh_project_segment_counts(project.id)
464472

465473
# Then
466474
spy.assert_not_called()
475+
assert not SegmentMembershipCount.objects.filter(
476+
segment=segment, environment=environment
477+
).exists()
467478
assert any(
468-
e["event"] == "refresh.project.skipped" and e["reason"] == "ff_disabled"
479+
e["event"] == "refresh.project.skipped"
480+
and e["reason"] == "ff_disabled"
481+
and e["stale_counts__count"] == 1
469482
for e in log.events
470483
)
471484

docs/docs/deployment-self-hosting/observability/_events-catalogue.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,7 @@ Attributes:
388388
### `segment_membership.refresh.project.completed`
389389

390390
Logged at `info` from:
391-
- `api/segment_membership/tasks.py:262`
391+
- `api/segment_membership/tasks.py:266`
392392

393393
Attributes:
394394
- `membership_counts.count`
@@ -398,7 +398,7 @@ Attributes:
398398
### `segment_membership.refresh.project.failed`
399399

400400
Logged at `exception` from:
401-
- `api/segment_membership/tasks.py:235`
401+
- `api/segment_membership/tasks.py:239`
402402

403403
Attributes:
404404
- `project.id`
@@ -407,11 +407,12 @@ Attributes:
407407

408408
Logged at `info` from:
409409
- `api/segment_membership/tasks.py:206`
410-
- `api/segment_membership/tasks.py:215`
410+
- `api/segment_membership/tasks.py:218`
411411

412412
Attributes:
413413
- `project.id`
414414
- `reason`
415+
- `stale_counts.count`
415416

416417
### `segment_membership.seed.environment.completed`
417418

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
2+
3+
type LifecycleEnvironmentState = {
4+
// Maps a project id to the environment id selected for lifecycle analysis.
5+
byProject: Record<number, number>
6+
}
7+
8+
const initialState: LifecycleEnvironmentState = {
9+
byProject: {},
10+
}
11+
12+
const lifecycleEnvironmentSlice = createSlice({
13+
initialState,
14+
name: 'lifecycleEnvironment',
15+
reducers: {
16+
setLifecycleEnvironment(
17+
state,
18+
action: PayloadAction<{ projectId: number; environmentId: number }>,
19+
) {
20+
state.byProject[action.payload.projectId] = action.payload.environmentId
21+
},
22+
},
23+
})
24+
25+
export const { setLifecycleEnvironment } = lifecycleEnvironmentSlice.actions
26+
export default lifecycleEnvironmentSlice.reducer

frontend/common/services/useProjectFlag.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,13 @@ function recursivePageGet(
3535
}
3636
export const projectFlagService = service
3737
.enhanceEndpoints({
38-
addTagTypes: ['ProjectFlag', 'FeatureList', 'FeatureState', 'Environment'],
38+
addTagTypes: [
39+
'ProjectFlag',
40+
'FeatureList',
41+
'FeatureState',
42+
'Environment',
43+
'LifecycleCounts',
44+
],
3945
})
4046
.injectEndpoints({
4147
endpoints: (builder) => ({
@@ -68,6 +74,7 @@ export const projectFlagService = service
6874
invalidatesTags: [
6975
{ id: 'LIST', type: 'ProjectFlag' },
7076
{ id: 'LIST', type: 'FeatureList' },
77+
'LifecycleCounts',
7178
],
7279
query: (query: Req['createProjectFlag']) => ({
7380
body: query.body,
@@ -132,6 +139,16 @@ export const projectFlagService = service
132139
}),
133140
}),
134141

142+
getLifecycleStatusCounts: builder.query<
143+
Res['lifecycleStatusCounts'],
144+
Req['getLifecycleStatusCounts']
145+
>({
146+
providesTags: ['LifecycleCounts'],
147+
query: ({ environment }) => ({
148+
url: `environments/${environment}/feature-lifecycle-counts/`,
149+
}),
150+
}),
151+
135152
getProjectFlag: builder.query<Res['projectFlag'], Req['getProjectFlag']>({
136153
providesTags: (res) => [{ id: res?.id, type: 'ProjectFlag' }],
137154
query: (query: Req['getProjectFlag']) => ({
@@ -198,6 +215,7 @@ export const projectFlagService = service
198215
{ id: 'LIST', type: 'ProjectFlag' },
199216
{ id: 'LIST', type: 'FeatureList' },
200217
{ id: 'METRICS', type: 'Environment' },
218+
'LifecycleCounts',
201219
],
202220
query: ({ flag_id, project_id }) => ({
203221
method: 'DELETE',
@@ -213,6 +231,7 @@ export const projectFlagService = service
213231
{ id: 'LIST', type: 'ProjectFlag' },
214232
{ id: res?.id, type: 'ProjectFlag' },
215233
{ id: 'LIST', type: 'FeatureList' },
234+
'LifecycleCounts',
216235
],
217236
query: (query: Req['updateProjectFlag']) => ({
218237
body: query.body,
@@ -284,6 +303,7 @@ export const {
284303
useAddFlagOwnersMutation,
285304
useCreateProjectFlagMutation,
286305
useGetFeatureListQuery,
306+
useGetLifecycleStatusCountsQuery,
287307
useGetProjectFlagQuery,
288308
useGetProjectFlagsQuery,
289309
useRemoveFlagGroupOwnersMutation,

frontend/common/store.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ import storage from 'redux-persist/lib/storage'
1414
import { Persistor } from 'redux-persist/es/types'
1515
import { service } from './service'
1616
import selectedOrganisationReducer from './selectedOrganisationSlice'
17+
import lifecycleEnvironmentReducer from './lifecycleEnvironmentSlice'
1718
// END OF IMPORTS
1819
const createStore = () => {
1920
const reducer = combineReducers({
2021
[service.reducerPath]: service.reducer,
22+
lifecycleEnvironment: lifecycleEnvironmentReducer,
2123
selectedOrganisation: selectedOrganisationReducer,
2224
// END OF REDUCERS
2325
})
@@ -36,7 +38,7 @@ const createStore = () => {
3638
key: 'root',
3739
storage,
3840
version: 1,
39-
whitelist: ['user'],
41+
whitelist: ['user', 'lifecycleEnvironment'],
4042
},
4143
reducer,
4244
),

frontend/common/types/requests.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
FlagsmithValue,
3333
TagStrategy,
3434
FeatureType,
35+
LifecycleStage,
3536
} from './responses'
3637
import { UtmsType } from './utms'
3738

@@ -401,6 +402,10 @@ export type Req = {
401402
group_owners?: number[]
402403
sort_field?: string
403404
sort_direction?: SortOrder
405+
lifecycle_stage?: LifecycleStage
406+
}
407+
getLifecycleStatusCounts: {
408+
environment: number
404409
}
405410
getProjectFlag: { project: number; id: number }
406411
getRolesPermissionUsers: { organisation_id: number; role_id: number }

0 commit comments

Comments
 (0)