diff --git a/src/sentry/api/endpoints/prompts_activity.py b/src/sentry/api/endpoints/prompts_activity.py index 5fa40da5e8259e..65e49235639d48 100644 --- a/src/sentry/api/endpoints/prompts_activity.py +++ b/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 @@ 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) diff --git a/static/app/actionCreators/prompts.tsx b/static/app/actionCreators/prompts.tsx index 9ae27a2732e764..a582515ee12843 100644 --- a/static/app/actionCreators/prompts.tsx +++ b/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( + 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}; +} diff --git a/static/app/types/index.tsx b/static/app/types/index.tsx index 644600113223ce..582cca348231e5 100644 --- a/static/app/types/index.tsx +++ b/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', diff --git a/tests/sentry/api/endpoints/test_prompts_activity.py b/tests/sentry/api/endpoints/test_prompts_activity.py index 0ae529ed90eac7..a1077bb741fe7f 100644 --- a/tests/sentry/api/endpoints/test_prompts_activity.py +++ b/tests/sentry/api/endpoints/test_prompts_activity.py @@ -34,6 +34,20 @@ def test_invalid_feature(self): 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 @@ def test_dismiss(self): } 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 @@ def test_snooze(self): } 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 @@ def test_snooze(self): 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"]