Skip to content

Commit 7a02f82

Browse files
committed
feat: Improve the filter builder
- Use INNER JOIN for identity overrides - Don't fetch integrations for the SDK endpoint
1 parent ade08b0 commit 7a02f82

File tree

9 files changed

+96
-57
lines changed

9 files changed

+96
-57
lines changed

api/environments/constants.py

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
IDENTITY_INTEGRATIONS_RELATION_NAMES = [
2+
"amplitude_config",
3+
"heap_config",
4+
"mixpanel_config",
5+
"rudderstack_config",
6+
"segment_config",
7+
"webhook_config",
8+
]

api/environments/identities/managers.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from typing import TYPE_CHECKING, Iterable
22

3-
from django.db.models import Count, Manager, QuerySet
3+
from django.db.models import Manager, Prefetch, QuerySet
44

55
if TYPE_CHECKING:
66
from environments.identities.models import Identity
@@ -31,10 +31,10 @@ def get_or_create_for_sdk(
3131
.get_or_create(identifier=identifier, environment=environment)
3232
)
3333

34-
def with_overrides(self) -> QuerySet["Identity"]:
34+
def only_overrides(self) -> QuerySet["Identity"]:
3535
"""
3636
Only include identities with overridden features.
3737
"""
38-
return self.annotate(overridden_count=Count("identity_features")).filter(
39-
overridden_count__gt=0
40-
)
38+
return self.prefetch_related(
39+
Prefetch("identity_features"),
40+
).filter(identity_features__isnull=False)

api/environments/managers.py

+9-8
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,19 @@
66

77

88
class EnvironmentManager(SoftDeleteManager):
9-
def filter_for_document_builder(self, *args, **kwargs):
9+
def filter_for_document_builder(
10+
self,
11+
*args,
12+
extra_select_related: list[str] | None = None,
13+
extra_prefetch_related: list[Prefetch | str] | None = None,
14+
**kwargs,
15+
):
1016
return (
1117
super()
1218
.select_related(
1319
"project",
1420
"project__organisation",
15-
"amplitude_config",
16-
"dynatrace_config",
17-
"heap_config",
18-
"mixpanel_config",
19-
"rudderstack_config",
20-
"segment_config",
21-
"webhook_config",
21+
*extra_select_related or (),
2222
)
2323
.prefetch_related(
2424
Prefetch(
@@ -55,6 +55,7 @@ def filter_for_document_builder(self, *args, **kwargs):
5555
"multivariate_feature_option"
5656
),
5757
),
58+
*extra_prefetch_related or (),
5859
)
5960
.filter(*args, **kwargs)
6061
)

api/environments/models.py

+23-12
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
1-
# -*- coding: utf-8 -*-
2-
from __future__ import unicode_literals
3-
41
import logging
52
import typing
63
from copy import deepcopy
74

85
from core.models import abstract_base_auditable_model_factory
96
from core.request_origin import RequestOrigin
7+
from django.apps import apps
108
from django.conf import settings
119
from django.contrib.contenttypes.fields import GenericRelation
1210
from django.core.cache import caches
1311
from django.db import models
14-
from django.db.models import Q
12+
from django.db.models import Prefetch, Q
1513
from django.utils import timezone
1614
from django.utils.translation import ugettext_lazy as _
1715
from django_lifecycle import (
@@ -36,6 +34,7 @@
3634
generate_client_api_key,
3735
generate_server_api_key,
3836
)
37+
from environments.constants import IDENTITY_INTEGRATIONS_RELATION_NAMES
3938
from environments.dynamodb import (
4039
DynamoEnvironmentAPIKeyWrapper,
4140
DynamoEnvironmentV2Wrapper,
@@ -213,11 +212,7 @@ def get_from_cache(cls, api_key):
213212
select_related_args = (
214213
"project",
215214
"project__organisation",
216-
"mixpanel_config",
217-
"segment_config",
218-
"amplitude_config",
219-
"heap_config",
220-
"dynatrace_config",
215+
*IDENTITY_INTEGRATIONS_RELATION_NAMES,
221216
)
222217
base_qs = cls.objects.select_related(*select_related_args).defer(
223218
"description"
@@ -243,7 +238,10 @@ def write_environments_to_dynamodb(
243238
Q(id=environment_id) if environment_id else Q(project_id=project_id)
244239
)
245240
environments = list(
246-
cls.objects.filter_for_document_builder(environments_filter)
241+
cls.objects.filter_for_document_builder(
242+
environments_filter,
243+
extra_select_related=IDENTITY_INTEGRATIONS_RELATION_NAMES,
244+
)
247245
)
248246
if not environments:
249247
return
@@ -369,8 +367,21 @@ def _get_environment_document_from_db(
369367
cls,
370368
api_key: str,
371369
) -> dict[str, typing.Any]:
372-
environment = cls.objects.filter_for_document_builder(api_key=api_key).get()
373-
return map_environment_to_sdk_document(environment)
370+
Identity = apps.get_model("identities", "Identity")
371+
environment = cls.objects.filter_for_document_builder(
372+
api_key=api_key,
373+
extra_prefetch_related=[
374+
Prefetch(
375+
"identities",
376+
queryset=Identity.objects.only_overrides(),
377+
to_attr="identities_with_overrides",
378+
)
379+
],
380+
).get()
381+
return map_environment_to_sdk_document(
382+
environment,
383+
identities_with_overrides=environment.identities_with_overrides,
384+
)
374385

375386
def _get_environment(self):
376387
return self

api/integrations/integration.py

+14-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from typing import Type, TypedDict
22

3+
from environments.constants import IDENTITY_INTEGRATIONS_RELATION_NAMES
34
from integrations.amplitude.amplitude import AmplitudeWrapper
45
from integrations.common.wrapper import AbstractBaseIdentityIntegrationWrapper
56
from integrations.heap.heap import HeapWrapper
@@ -16,13 +17,24 @@ class IntegrationConfig(TypedDict):
1617

1718
IDENTITY_INTEGRATIONS: list[IntegrationConfig] = [
1819
{"relation_name": "amplitude_config", "wrapper": AmplitudeWrapper},
19-
{"relation_name": "segment_config", "wrapper": SegmentWrapper},
2020
{"relation_name": "heap_config", "wrapper": HeapWrapper},
2121
{"relation_name": "mixpanel_config", "wrapper": MixpanelWrapper},
22-
{"relation_name": "webhook_config", "wrapper": WebhookWrapper},
2322
{"relation_name": "rudderstack_config", "wrapper": RudderstackWrapper},
23+
{"relation_name": "segment_config", "wrapper": SegmentWrapper},
24+
{"relation_name": "webhook_config", "wrapper": WebhookWrapper},
2425
]
2526

27+
assert set(IDENTITY_INTEGRATIONS_RELATION_NAMES) == (
28+
_configured_integrations := {
29+
integration_config["relation_name"]
30+
for integration_config in IDENTITY_INTEGRATIONS
31+
}
32+
), (
33+
"Check that `environments.constants.IDENTITY_INTEGRATIONS_RELATION_NAMES` and "
34+
"`integration.integration.IDENTITY_INTEGRATIONS` contain the same values. \n"
35+
f"Unconfigured integrations: {set(IDENTITY_INTEGRATIONS_RELATION_NAMES) - _configured_integrations}"
36+
)
37+
2638

2739
def identify_integrations(identity, all_feature_states, trait_models=None):
2840
for integration in IDENTITY_INTEGRATIONS:

api/tests/unit/util/mappers/test_unit_mappers_sdk.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,15 @@ def test_map_environment_to_sdk_document__return_expected(
3636
identity_featurestate.enabled = True
3737
identity_featurestate.save()
3838

39+
identities_with_overrides = (
40+
Identity.objects.only_overrides().filter(environment=environment).all()
41+
)
42+
3943
# When
40-
result = map_environment_to_sdk_document(environment)
44+
result = map_environment_to_sdk_document(
45+
environment,
46+
identities_with_overrides=identities_with_overrides,
47+
)
4148

4249
# Then
4350
assert result == {

api/util/mappers/dynamodb.py

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ def map_environment_to_environment_document(
4848
field_name: _map_value_to_document_value(value)
4949
for field_name, value in map_environment_to_engine(
5050
environment,
51+
with_integrations=True,
5152
)
5253
}
5354

api/util/mappers/engine.py

+22-23
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,16 @@
4949
"map_traits_to_engine",
5050
)
5151

52+
INTEGRATIONS_RELATION_NAMES = [
53+
"amplitude_config",
54+
"dynatrace_config",
55+
"heap_config",
56+
"mixpanel_config",
57+
"rudderstack_config",
58+
"segment_config",
59+
"webhook_config",
60+
]
61+
5262

5363
def map_traits_to_engine(traits: Iterable["Trait"]) -> list[TraitModel]:
5464
return [
@@ -174,6 +184,8 @@ def map_mv_option_to_engine(
174184

175185
def map_environment_to_engine(
176186
environment: "Environment",
187+
*,
188+
with_integrations: bool = True,
177189
) -> EnvironmentModel:
178190
"""
179191
Maps Core API's `environments.models.Environment` model instance to the
@@ -215,22 +227,14 @@ def map_environment_to_engine(
215227
}
216228

217229
# Read integrations.
218-
integration_configs: dict[str, Optional["EnvironmentIntegrationModel"]] = {}
219-
for attr_name in (
220-
"amplitude_config",
221-
"dynatrace_config",
222-
"heap_config",
223-
"mixpanel_config",
224-
"rudderstack_config",
225-
"segment_config",
226-
):
227-
integration_config = getattr(environment, attr_name, None)
228-
if integration_config and not integration_config.deleted:
229-
integration_configs[attr_name] = integration_config
230-
231-
webhook_config: Optional["WebhookConfiguration"] = getattr(
232-
environment, "webhook_config", None
233-
)
230+
integration_configs: dict[
231+
str, "EnvironmentIntegrationModel | WebhookConfiguration | None"
232+
] = {}
233+
if with_integrations:
234+
for attr_name in INTEGRATIONS_RELATION_NAMES:
235+
integration_config = getattr(environment, attr_name, None)
236+
if integration_config and not integration_config.deleted:
237+
integration_configs[attr_name] = integration_config
234238

235239
# No reading from ORM past this point!
236240

@@ -306,13 +310,8 @@ def map_environment_to_engine(
306310
segment_config_model = map_integration_to_engine(
307311
integration_configs.pop("segment_config", None),
308312
)
309-
310-
webhook_config_model = (
311-
map_webhook_config_to_engine(
312-
webhook_config,
313-
)
314-
if webhook_config and not webhook_config.deleted
315-
else None
313+
webhook_config_model = map_webhook_config_to_engine(
314+
integration_configs.pop("webhook_config", None),
316315
)
317316

318317
return EnvironmentModel(

api/util/mappers/sdk.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
map_identity_to_engine,
66
)
77

8-
if TYPE_CHECKING:
8+
if TYPE_CHECKING: # pragma: no cover
9+
from environments.identities.models import Identity
910
from environments.models import Environment
1011

1112
ENVIRONMENT_RESPONSE_EXCLUDE_FIELDS = [
@@ -25,6 +26,8 @@
2526

2627
def map_environment_to_sdk_document(
2728
environment: "Environment",
29+
*,
30+
identities_with_overrides: list["Identity"] | None = None,
2831
) -> SDKDocument:
2932
"""
3033
Map an `environments.models.Environment` instance to an SDK document
@@ -33,17 +36,14 @@ def map_environment_to_sdk_document(
3336
It's virtually the same data that gets indexed in DynamoDB,
3437
except it presents identity overrides and omits integrations configurations.
3538
"""
36-
# Read relationships.
37-
identities_with_overrides = environment.identities.with_overrides().all()
38-
3939
# Get the engine data.
40-
engine_environment = map_environment_to_engine(environment)
40+
engine_environment = map_environment_to_engine(environment, with_integrations=False)
4141

4242
# No reading from ORM past this point!
4343

4444
# Prepare relationships.
4545
engine_environment.identity_overrides = [
46-
map_identity_to_engine(identity) for identity in identities_with_overrides
46+
map_identity_to_engine(identity) for identity in identities_with_overrides or []
4747
]
4848

4949
return engine_environment.model_dump(

0 commit comments

Comments
 (0)