Просмотр исходного кода

feat(growth): prompts activities endpoint (#28008)

* feat(analytics): prompts activities endpoint

* Reusing existing endpoint

* adding api

* added tests
Zhixing Zhang 3 лет назад
Родитель
Сommit
660c9231f2

+ 27 - 18
src/sentry/api/endpoints/prompts_activity.py

@@ -1,6 +1,7 @@
 import calendar
 
 from django.db import IntegrityError, transaction
+from django.db.models import Q
 from django.http import HttpResponse
 from django.utils import timezone
 from rest_framework import serializers
@@ -15,6 +16,7 @@ from sentry.utils.prompts import prompt_config
 VALID_STATUSES = frozenset(("snoozed", "dismissed"))
 
 
+# Endpoint to retrieve multiple PromptsActivity at once
 class PromptsActivitySerializer(serializers.Serializer):
     feature = serializers.CharField(required=True)
     status = serializers.ChoiceField(choices=zip(VALID_STATUSES, VALID_STATUSES), required=True)
@@ -33,24 +35,31 @@ class PromptsActivityEndpoint(Endpoint):
     def get(self, request):
         """Return feature prompt status if dismissed or in snoozed period"""
 
-        feature = request.GET.get("feature")
-
-        if not prompt_config.has(feature):
-            return Response({"detail": "Invalid feature name"}, status=400)
-
-        required_fields = prompt_config.required_fields(feature)
-        for field in required_fields:
-            if field not in request.GET:
-                return Response({"detail": 'Missing required field "%s"' % field}, status=400)
-
-        filters = {k: request.GET.get(k) for k in required_fields}
-
-        try:
-            result = PromptsActivity.objects.get(user=request.user, feature=feature, **filters)
-        except PromptsActivity.DoesNotExist:
-            return Response({})
-
-        return Response({"data": result.data})
+        features = request.GET.getlist("feature")
+        if len(features) == 0:
+            return Response({"details": "No feature specified"}, status=400)
+
+        conditions = None
+        for feature in features:
+            if not prompt_config.has(feature):
+                return Response({"detail": "Invalid feature name " + feature}, status=400)
+
+            required_fields = prompt_config.required_fields(feature)
+            for field in required_fields:
+                if field not in request.GET:
+                    return Response({"detail": 'Missing required field "%s"' % field}, status=400)
+            filters = {k: request.GET.get(k) for k in required_fields}
+            condition = Q(feature=feature, **filters)
+            conditions = condition if conditions is None else (conditions | condition)
+
+        result = PromptsActivity.objects.filter(conditions, user=request.user)
+        featuredata = {k.feature: k.data for k in result}
+        if len(features) == 1:
+            result = result.first()
+            data = None if result is None else result.data
+            return Response({"data": data, "features": featuredata})
+        else:
+            return Response({"features": featuredata})
 
     def put(self, request):
         serializer = PromptsActivitySerializer(data=request.data)

+ 43 - 4
static/app/actionCreators/prompts.tsx

@@ -46,11 +46,13 @@ type PromptCheckParams = {
   feature: string;
 };
 
+export type PromptResponseItem = {
+  snoozed_ts?: number;
+  dismissed_ts?: number;
+};
 export type PromptResponse = {
-  data?: {
-    snoozed_ts?: number;
-    dismissed_ts?: number;
-  };
+  data?: PromptResponseItem;
+  features?: {[key: string]: PromptResponseItem};
 };
 
 export type PromptData = null | {
@@ -85,3 +87,40 @@ export async function promptsCheck(
     snoozedTime: data.snoozed_ts,
   };
 }
+
+/**
+ * Get the status of many prompt
+ */
+export async function batchedPromptsCheck<T extends readonly string[]>(
+  api: Client,
+  features: T,
+  params: {organizationId: string; projectId?: string}
+): Promise<{[key in T[number]]: PromptData}> {
+  const query = {
+    feature: features,
+    organization_id: params.organizationId,
+    ...(params.projectId === undefined ? {} : {project_id: params.projectId}),
+  };
+
+  const response: PromptResponse = await api.requestPromise('/prompts-activity/', {
+    query,
+  });
+  const responseFeatures = response?.features;
+
+  const result: {[key in T[number]]?: PromptData} = {};
+  if (!responseFeatures) {
+    return result as {[key in T[number]]: PromptData};
+  }
+  for (const featureName of features) {
+    const item = responseFeatures[featureName];
+    if (item) {
+      result[featureName] = {
+        dismissedTime: item.dismissed_ts,
+        snoozedTime: item.snoozed_ts,
+      };
+    } else {
+      result[featureName] = null;
+    }
+  }
+  return result as {[key in T[number]]: PromptData};
+}

+ 3 - 0
static/app/types/index.tsx

@@ -785,6 +785,9 @@ export enum DataCategory {
   TRANSACTIONS = 'transactions',
   ATTACHMENTS = 'attachments',
 }
+
+export type EventType = 'error' | 'transaction' | 'attachment';
+
 export const DataCategoryName = {
   [DataCategory.ERRORS]: 'Errors',
   [DataCategory.TRANSACTIONS]: 'Transactions',

+ 57 - 2
tests/sentry/api/endpoints/test_prompts_activity.py

@@ -34,6 +34,20 @@ class PromptsActivityTest(APITestCase):
 
         assert resp.status_code == 400
 
+    def test_batched_invalid_feature(self):
+        # Invalid feature prompt name
+        resp = self.client.put(
+            self.path,
+            {
+                "organization_id": self.org.id,
+                "project_id": self.project.id,
+                "feature": ["releases", "gibberish"],
+                "status": "dismissed",
+            },
+        )
+
+        assert resp.status_code == 400
+
     def test_invalid_project(self):
         # Invalid project id
         data = {
@@ -64,7 +78,7 @@ class PromptsActivityTest(APITestCase):
         }
         resp = self.client.get(self.path, data)
         assert resp.status_code == 200
-        assert resp.data == {}
+        assert resp.data.get("data", None) is None
 
         self.client.put(
             self.path,
@@ -89,7 +103,7 @@ class PromptsActivityTest(APITestCase):
         }
         resp = self.client.get(self.path, data)
         assert resp.status_code == 200
-        assert resp.data == {}
+        assert resp.data.get("data", None) is None
 
         self.client.put(
             self.path,
@@ -106,3 +120,44 @@ class PromptsActivityTest(APITestCase):
         assert resp.status_code == 200
         assert "data" in resp.data
         assert "snoozed_ts" in resp.data["data"]
+
+    def test_batched(self):
+        data = {
+            "organization_id": self.org.id,
+            "project_id": self.project.id,
+            "feature": ["releases", "alert_stream"],
+        }
+        resp = self.client.get(self.path, data)
+        assert resp.status_code == 200
+        assert resp.data["features"].get("releases", None) is None
+        assert resp.data["features"].get("alert_stream", None) is None
+
+        self.client.put(
+            self.path,
+            {
+                "organization_id": self.org.id,
+                "project_id": self.project.id,
+                "feature": "releases",
+                "status": "dismissed",
+            },
+        )
+
+        resp = self.client.get(self.path, data)
+        assert resp.status_code == 200
+        assert "dismissed_ts" in resp.data["features"]["releases"]
+        assert resp.data["features"].get("alert_stream", None) is None
+
+        self.client.put(
+            self.path,
+            {
+                "organization_id": self.org.id,
+                "project_id": self.project.id,
+                "feature": "alert_stream",
+                "status": "snoozed",
+            },
+        )
+
+        resp = self.client.get(self.path, data)
+        assert resp.status_code == 200
+        assert "dismissed_ts" in resp.data["features"]["releases"]
+        assert "snoozed_ts" in resp.data["features"]["alert_stream"]