11import logging
2+ import typing
23
3- from django .conf import settings
44from drf_yasg .utils import swagger_auto_schema # type: ignore[import-untyped]
55from rest_framework import status
66from rest_framework .decorators import api_view , permission_classes
77from rest_framework .fields import IntegerField
8- from rest_framework .generics import CreateAPIView , GenericAPIView
8+ from rest_framework .generics import CreateAPIView
99from rest_framework .permissions import IsAuthenticated
1010from rest_framework .request import Request
1111from rest_framework .response import Response
1616 get_usage_data ,
1717)
1818from app_analytics .cache import FeatureEvaluationCache
19- from app_analytics .tasks import (
20- track_feature_evaluation_influxdb_v2 ,
21- track_feature_evaluation_v2 ,
22- )
2319from environments .authentication import EnvironmentKeyAuthentication
2420from environments .permissions .permissions import EnvironmentKeyPermissions
2521from features .models import FeatureState
@@ -44,60 +40,26 @@ class SDKAnalyticsFlagsV2(CreateAPIView): # type: ignore[type-arg]
4440 serializer_class = SDKAnalyticsFlagsSerializer
4541 throttle_classes = []
4642
43+ @swagger_auto_schema ( # type: ignore[misc]
44+ request_body = SDKAnalyticsFlagsSerializer (),
45+ responses = {204 : Response (status = status .HTTP_204_NO_CONTENT )},
46+ )
4747 def create (self , request : Request , * args , ** kwargs ) -> Response : # type: ignore[no-untyped-def]
4848 serializer = self .get_serializer (data = request .data )
4949 serializer .is_valid (raise_exception = True )
50-
51- self .evaluations = serializer .validated_data ["evaluations" ]
52- if not self ._is_data_valid ():
53- return Response (
54- {"detail" : "Invalid feature names associated with the project." },
55- content_type = "application/json" ,
56- status = status .HTTP_400_BAD_REQUEST ,
57- )
58- if settings .USE_POSTGRES_FOR_ANALYTICS :
59- track_feature_evaluation_v2 .delay (
60- args = (
61- request .environment .id ,
62- self .evaluations ,
63- )
64- )
65- elif settings .INFLUXDB_TOKEN :
66- track_feature_evaluation_influxdb_v2 .delay (
67- args = (
68- request .environment .id ,
69- self .evaluations ,
70- )
71- )
72-
50+ serializer .save (environment = self .request .environment )
7351 return Response (status = status .HTTP_204_NO_CONTENT )
7452
75- def _is_data_valid (self ) -> bool :
76- environment_feature_names = set (
77- FeatureState .objects .filter (
78- environment = self .request .environment ,
79- feature_segment = None ,
80- identity = None ,
81- ).values_list ("feature__name" , flat = True )
82- )
83-
84- valid = True
85- for evaluation in self .evaluations :
86- if evaluation ["feature_name" ] in environment_feature_names :
87- continue
88- valid = False
89-
90- return valid
91-
9253
93- class SDKAnalyticsFlags (GenericAPIView ): # type: ignore[type-arg]
54+ class SDKAnalyticsFlags (CreateAPIView ): # type: ignore[type-arg]
9455 """
9556 Class to handle flag analytics events
9657 """
9758
9859 permission_classes = (EnvironmentKeyPermissions ,)
9960 authentication_classes = (EnvironmentKeyAuthentication ,)
10061 throttle_classes = []
62+ format_kwarg = None
10163
10264 def get_serializer_class (self ): # type: ignore[no-untyped-def]
10365 if getattr (self , "swagger_fake_view" , False ):
@@ -118,57 +80,33 @@ def get_fields(self): # type: ignore[no-untyped-def]
11880 for feature_name in environment_feature_names
11981 }
12082
83+ def save (self , ** kwargs : typing .Any ) -> None :
84+ request = self .context ["request" ]
85+ for feature_name , eval_count in self .validated_data .items ():
86+ feature_evaluation_cache .track_feature_evaluation (
87+ request .environment .id , feature_name , eval_count
88+ )
89+
12190 return _AnalyticsSerializer
12291
123- def post (self , request , * args , ** kwargs ): # type: ignore[no-untyped-def]
92+ @swagger_auto_schema ( # type: ignore[misc]
93+ request_body = SDKAnalyticsFlagsSerializer (),
94+ responses = {200 : Response (status = status .HTTP_200_OK )},
95+ )
96+ def create (
97+ self , request : Request , * args : typing .Any , ** kwargs : typing .Any
98+ ) -> Response :
12499 """
125100 Send flag evaluation events from the SDK back to the API for reporting.
126101
127-
128102 TODO: Eventually replace this with the v2 version of
129103 this endpoint once SDKs have been updated.
130104 """
131- is_valid = self ._is_data_valid ()
132- if not is_valid :
133- # for now, return 200 to avoid breaking client integrations
134- return Response (
135- {"detail" : "Invalid data. Not logged." },
136- content_type = "application/json" ,
137- status = status .HTTP_200_OK ,
138- )
139- for feature_name , eval_count in self .request .data .items ():
140- feature_evaluation_cache .track_feature_evaluation (
141- request .environment .id , feature_name , eval_count
142- )
105+ serializer = self .get_serializer (data = request .data )
106+ serializer .is_valid ()
107+ serializer .save (environment = self .request .environment )
143108 return Response (status = status .HTTP_200_OK )
144109
145- def _is_data_valid (self ) -> bool :
146- environment_feature_names = set (
147- FeatureState .objects .filter (
148- environment = self .request .environment ,
149- feature_segment = None ,
150- identity = None ,
151- ).values_list ("feature__name" , flat = True )
152- )
153-
154- is_valid = True
155- for feature_name , request_count in self .request .data .items ():
156- if not (
157- isinstance (feature_name , str )
158- and feature_name in environment_feature_names
159- ):
160- logger .warning ("Feature %s does not belong to project" , feature_name )
161- is_valid = False
162-
163- if not (isinstance (request_count , int )):
164- logger .error (
165- "Analytics data contains non integer request count. User agent: %s" ,
166- self .request .headers .get ("User-Agent" , "Not found" ),
167- )
168- is_valid = False
169-
170- return is_valid
171-
172110
173111class SelfHostedTelemetryAPIView (CreateAPIView ): # type: ignore[type-arg]
174112 """
0 commit comments