Skip to content

Commit 3ae32ac

Browse files
committed
Remove anonymous. Add generic evaluation endpoint
1 parent b9a7ca4 commit 3ae32ac

File tree

6 files changed

+196
-115
lines changed

6 files changed

+196
-115
lines changed

src/api/urls/v1.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
from rest_framework import authentication, permissions, routers
77

88
from environments.identities.traits.views import SDKTraits
9-
from environments.identities.views import SDKIdentities, SDKAnonymousIdentities
9+
from environments.identities.views import SDKIdentities
10+
from evaluations.views import SDKEvaluations
1011
from features.views import SDKFeatureStates
1112
from organisations.views import chargebee_webhook
1213

@@ -42,8 +43,7 @@
4243
# Client SDK urls
4344
url(r"^flags/$", SDKFeatureStates.as_view(), name="flags"),
4445
url(r"^identities/$", SDKIdentities.as_view(), name="sdk-identities"),
45-
url(r"^identities/anonymous$", SDKAnonymousIdentities.as_view(),
46-
name="sdk-anonymous-identities"),
46+
url(r"^evaluate/$", SDKEvaluations.as_view(), name="evaluations"),
4747
url(r"^traits/", include(traits_router.urls), name="traits"),
4848
url(r"^analytics/flags/$", SDKAnalyticsFlags.as_view()),
4949
# API documentation

src/environments/identities/models.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from environments.identities.traits.models import Trait
99
from features.models import FeatureState
1010

11-
1211
@python_2_unicode_compatible
1312
class Identity(models.Model):
1413
identifier = models.CharField(max_length=2000)
@@ -25,6 +24,73 @@ class Meta:
2524
# issues with production deployment due to multi server configuration.
2625
db_table = "environments_identity"
2726

27+
def get_feature_states(self, features: typing.List[str], traits: typing.List[Trait] = None):
28+
"""
29+
Get a specified set of feature states for an identity. This method returns a single flag for
30+
each feature in the requested list that exists in the identity's environment's project. The
31+
flag returned is the correct flag based on the priorities as follows (highest -> lowest):
32+
33+
1. Identity - flag override for this specific identity
34+
2. Segment - flag overridden for a segment this identity belongs to
35+
3. Environment - default value for the environment
36+
37+
:return: (list) flags for an identity with the correct values based on
38+
identity / segment priorities
39+
"""
40+
segments = self.get_segments(traits=traits)
41+
42+
# define sub queries
43+
belongs_to_environment_query = Q(environment=self.environment)
44+
is_requested = Q(feature__name__in=features)
45+
overridden_for_identity_query = Q(identity=self)
46+
overridden_for_segment_query = Q(
47+
feature_segment__segment__in=segments,
48+
feature_segment__environment=self.environment,
49+
)
50+
environment_default_query = Q(identity=None, feature_segment=None)
51+
52+
# Only look for identity overrides if this identity has been persisted already
53+
if self._state.adding:
54+
full_query = belongs_to_environment_query & is_requested & (
55+
overridden_for_segment_query
56+
| environment_default_query
57+
)
58+
else:
59+
full_query = belongs_to_environment_query & is_requested & (
60+
overridden_for_identity_query
61+
| overridden_for_segment_query
62+
| environment_default_query
63+
)
64+
65+
select_related_args = [
66+
"feature",
67+
"feature_state_value",
68+
"feature_segment",
69+
"feature_segment__segment",
70+
]
71+
72+
# When Project's hide_disabled_flags enabled, exclude disabled Features from the list
73+
all_flags = FeatureState.objects.select_related(*select_related_args).filter(
74+
full_query
75+
)
76+
77+
# iterate over all the flags and build a dictionary keyed on feature with the highest priority flag
78+
# for the given identity as the value.
79+
identity_flags = {}
80+
for flag in all_flags:
81+
if flag.feature_id not in identity_flags:
82+
identity_flags[flag.feature_id] = flag
83+
else:
84+
if flag > identity_flags[flag.feature_id]:
85+
identity_flags[flag.feature_id] = flag
86+
87+
if self.environment.project.hide_disabled_flags:
88+
# filter out any flags that are disabled if configured on the project
89+
# Note: done here instead of the DB because of CH1245
90+
return [value for value in identity_flags.values() if value.enabled]
91+
92+
return list(identity_flags.values())
93+
2894
def get_all_feature_states(self, traits: typing.List[Trait] = None):
2995
"""
3096
Get all feature states for an identity. This method returns a single flag for

src/environments/identities/views.py

Lines changed: 1 addition & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -198,113 +198,4 @@ def _get_all_feature_states_for_user_response(self, identity, trait_models=None)
198198
response = {"flags": serialized_flags.data,
199199
"traits": serialized_traits.data}
200200

201-
return Response(data=response, status=status.HTTP_200_OK)
202-
203-
204-
class SDKAnonymousIdentities(SDKAPIView):
205-
serializer_class = IdentifyWithTraitsSerializer
206-
pagination_class = None # set here to ensure documentation is correct
207-
208-
def get(self, request):
209-
identifier = request.query_params.get("identifier")
210-
if not identifier:
211-
return Response(
212-
{"detail": "Missing identifier"}
213-
) # TODO: add 400 status - will this break the clients?
214-
215-
try:
216-
identity = (
217-
Identity.objects.select_related(
218-
"environment", "environment__project")
219-
.prefetch_related("identity_traits", "environment__project__segments")
220-
.get(identifier=identifier, environment=request.environment)
221-
)
222-
except ObjectDoesNotExist:
223-
identity = Identity(identifier=identifier,
224-
environment=request.environment)
225-
226-
# Create temporary trait models
227-
temporary_traits = request.query_params.get("traits")
228-
229-
if temporary_traits:
230-
decoded_traits = json.loads(temporary_traits)
231-
traits = list(map(lambda t: self._make_temporary_trait(
232-
identity, t), decoded_traits))
233-
else:
234-
traits = None
235-
236-
feature_name = request.query_params.get("feature")
237-
if feature_name:
238-
return self._get_single_feature_state_response(identity, feature_name)
239-
else:
240-
return self._get_all_feature_states_for_user_response(identity, traits)
241-
242-
def _make_temporary_trait(self, identity, trait_data):
243-
print(trait_data)
244-
return Trait(
245-
identity=identity,
246-
trait_key=trait_data.get('trait_key'),
247-
value_type=trait_data.get('value_type'),
248-
boolean_value=trait_data.get('boolean_value'),
249-
integer_value=trait_data.get('integer_value'),
250-
string_value=trait_data.get('string_value'),
251-
float_value=trait_data.get('float_value')
252-
)
253-
254-
def get_serializer_context(self):
255-
context = super(SDKIdentities, self).get_serializer_context()
256-
if hasattr(self.request, "environment"):
257-
# only set it if the request has the attribute to ensure that the
258-
# documentation works correctly still
259-
context["environment"] = self.request.environment
260-
return context
261-
262-
def post(self, request):
263-
serializer = self.get_serializer(data=request.data)
264-
serializer.is_valid(raise_exception=True)
265-
instance = serializer.save()
266-
267-
# we need to serialize the response again to ensure that the
268-
# trait values are serialized correctly
269-
response_serializer = IdentifyWithTraitsSerializer(instance=instance)
270-
return Response(response_serializer.data)
271-
272-
def _get_single_feature_state_response(self, identity, feature_name):
273-
for feature_state in identity.get_all_feature_states():
274-
if feature_state.feature.name == feature_name:
275-
serializer = FeatureStateSerializerFull(feature_state)
276-
return Response(data=serializer.data, status=status.HTTP_200_OK)
277-
278-
return Response(
279-
{"detail": "Given feature not found"}, status=status.HTTP_404_NOT_FOUND
280-
)
281-
282-
def _get_all_feature_states_for_user_response(self, identity, trait_models=None):
283-
"""
284-
Get all feature states for an identity
285-
286-
:param identity: Identity model to return feature states for
287-
:param trait_models: optional list of trait_models to pass in for organisations that don't persist them
288-
:return: Response containing lists of both serialized flags and traits
289-
"""
290-
shadowed_keys = [] if trait_models is None else map(
291-
lambda t: t.trait_key, trait_models)
292-
293-
traits = identity.identity_traits.all() if trait_models is None else list(
294-
identity.identity_traits.all().exclude(trait_key__in=shadowed_keys)) + trait_models
295-
296-
all_feature_states = identity.get_all_feature_states(traits)
297-
298-
serialized_flags = FeatureStateSerializerFull(
299-
all_feature_states, many=True)
300-
301-
serialized_traits = TraitSerializerBasic(
302-
traits, many=True
303-
)
304-
305-
identify_integrations(identity, all_feature_states)
306-
307-
response = {"flags": serialized_flags.data,
308-
"traits": serialized_traits.data}
309-
310-
return Response(data=response, status=status.HTTP_200_OK)
201+
return Response(data=response, status=status.HTTP_200_OK)

src/evaluations/serializers.py

Whitespace-only changes.

src/evaluations/views.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
from collections import namedtuple
2+
import json
3+
4+
from django.core.exceptions import ObjectDoesNotExist
5+
6+
import coreapi
7+
from rest_framework import status, viewsets
8+
from rest_framework.permissions import IsAuthenticated
9+
from rest_framework.response import Response
10+
from rest_framework.schemas import AutoSchema
11+
12+
from app.pagination import CustomPagination
13+
from environments.identities.helpers import identify_integrations
14+
from environments.identities.models import Identity
15+
from environments.identities.serializers import IdentitySerializer
16+
from environments.identities.traits.models import Trait
17+
from environments.identities.traits.serializers import TraitSerializerBasic
18+
from environments.models import Environment
19+
from environments.permissions.permissions import NestedEnvironmentPermissions
20+
from environments.sdk.serializers import (
21+
IdentifyWithTraitsSerializer,
22+
IdentitySerializerWithTraitsAndSegments,
23+
)
24+
from features.serializers import FeatureStateSerializerFull
25+
from util.views import SDKAPIView
26+
27+
class SDKEvaluations(SDKAPIView):
28+
pagination_class = None
29+
30+
def get(self, request):
31+
identifier = request.query_params.get("identifier")
32+
33+
if identifier is None:
34+
return Response(
35+
{"detail": "Missing identifier"}, status=status.HTTP_400_BAD_REQUEST
36+
)
37+
38+
try:
39+
identity = (
40+
Identity.objects.select_related(
41+
"environment", "environment__project")
42+
.prefetch_related("identity_traits", "environment__project__segments")
43+
.get(identifier=identifier, environment=request.environment)
44+
)
45+
except ObjectDoesNotExist:
46+
identity = Identity(identifier=identifier,
47+
environment=request.environment)
48+
49+
# Create temporary trait models
50+
temporary_traits = request.query_params.get("traits")
51+
52+
try:
53+
if temporary_traits:
54+
decoded_traits = json.loads(temporary_traits)
55+
traits = list(map(lambda t: self._make_temporary_trait(
56+
identity, t), decoded_traits))
57+
else:
58+
traits = None
59+
except json.JSONDecodeError:
60+
return Response(
61+
{"detail": "Unable to parse traits"}, status=status.HTTP_400_BAD_REQUEST
62+
)
63+
64+
features = request.query_params.get("features")
65+
66+
try:
67+
if features:
68+
features_list = json.loads(features)
69+
else:
70+
features_list = None
71+
except json.JSONDecodeError:
72+
return Response(
73+
{"detail": "Unable to parse features list"}, status=status.HTTP_400_BAD_REQUEST
74+
)
75+
76+
return self._get_feature_states_for_user_response(identity, traits, features_list)
77+
78+
def _get_feature_states_for_user_response(self, identity, trait_models=None, features=None):
79+
"""
80+
Get all feature (or a subset of them) states for an identity
81+
82+
:param identity: Identity model to return feature states for
83+
:param trait_models: optional list of trait_models to apply over top of any already persisted traits for the identity
84+
:return: Response containing lists of both serialized flags and traits
85+
"""
86+
shadowed_keys = [] if trait_models is None else map(
87+
lambda t: t.trait_key, trait_models)
88+
89+
traits = identity.identity_traits.all() if trait_models is None else list(
90+
identity.identity_traits.all().exclude(trait_key__in=shadowed_keys)) + trait_models
91+
92+
if features is None:
93+
feature_states = identity.get_all_feature_states(traits)
94+
else:
95+
feature_states = identity.get_feature_states(features, traits)
96+
97+
serialized_flags = FeatureStateSerializerFull(
98+
feature_states, many=True)
99+
100+
serialized_traits = TraitSerializerBasic(
101+
traits, many=True
102+
)
103+
104+
identify_integrations(identity, feature_states)
105+
106+
response = {"flags": serialized_flags.data,
107+
"traits": serialized_traits.data}
108+
109+
return Response(data=response, status=status.HTTP_200_OK)
110+
111+
112+
def _make_temporary_trait(self, identity, trait_data):
113+
print(trait_data)
114+
return Trait(
115+
identity=identity,
116+
trait_key=trait_data.get('trait_key'),
117+
value_type=trait_data.get('value_type'),
118+
boolean_value=trait_data.get('boolean_value'),
119+
integer_value=trait_data.get('integer_value'),
120+
string_value=trait_data.get('string_value'),
121+
float_value=trait_data.get('float_value')
122+
)

src/features/models.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,10 +412,12 @@ def generate_feature_state_value_data(self, value):
412412

413413
def __str__(self):
414414
if self.environment is not None:
415-
return "Project %s - Environment %s - Feature %s - Enabled: %r" % (
415+
return "Project %s - Environment %s - Feature %s - Segment %s - Identity %s - Enabled: %r" % (
416416
self.environment.project.name,
417417
self.environment.name,
418418
self.feature.name,
419+
self.feature_segment,
420+
self.identity,
419421
self.enabled,
420422
)
421423
elif self.identity is not None:

0 commit comments

Comments
 (0)