Skip to content

Commit 660c923

Browse files
authored
feat(growth): prompts activities endpoint (#28008)
* feat(analytics): prompts activities endpoint * Reusing existing endpoint * adding api * added tests
1 parent 1f6a9d6 commit 660c923

File tree

4 files changed

+130
-24
lines changed

4 files changed

+130
-24
lines changed

src/sentry/api/endpoints/prompts_activity.py

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import calendar
22

33
from django.db import IntegrityError, transaction
4+
from django.db.models import Q
45
from django.http import HttpResponse
56
from django.utils import timezone
67
from rest_framework import serializers
@@ -15,6 +16,7 @@
1516
VALID_STATUSES = frozenset(("snoozed", "dismissed"))
1617

1718

19+
# Endpoint to retrieve multiple PromptsActivity at once
1820
class PromptsActivitySerializer(serializers.Serializer):
1921
feature = serializers.CharField(required=True)
2022
status = serializers.ChoiceField(choices=zip(VALID_STATUSES, VALID_STATUSES), required=True)
@@ -33,24 +35,31 @@ class PromptsActivityEndpoint(Endpoint):
3335
def get(self, request):
3436
"""Return feature prompt status if dismissed or in snoozed period"""
3537

36-
feature = request.GET.get("feature")
37-
38-
if not prompt_config.has(feature):
39-
return Response({"detail": "Invalid feature name"}, status=400)
40-
41-
required_fields = prompt_config.required_fields(feature)
42-
for field in required_fields:
43-
if field not in request.GET:
44-
return Response({"detail": 'Missing required field "%s"' % field}, status=400)
45-
46-
filters = {k: request.GET.get(k) for k in required_fields}
47-
48-
try:
49-
result = PromptsActivity.objects.get(user=request.user, feature=feature, **filters)
50-
except PromptsActivity.DoesNotExist:
51-
return Response({})
52-
53-
return Response({"data": result.data})
38+
features = request.GET.getlist("feature")
39+
if len(features) == 0:
40+
return Response({"details": "No feature specified"}, status=400)
41+
42+
conditions = None
43+
for feature in features:
44+
if not prompt_config.has(feature):
45+
return Response({"detail": "Invalid feature name " + feature}, status=400)
46+
47+
required_fields = prompt_config.required_fields(feature)
48+
for field in required_fields:
49+
if field not in request.GET:
50+
return Response({"detail": 'Missing required field "%s"' % field}, status=400)
51+
filters = {k: request.GET.get(k) for k in required_fields}
52+
condition = Q(feature=feature, **filters)
53+
conditions = condition if conditions is None else (conditions | condition)
54+
55+
result = PromptsActivity.objects.filter(conditions, user=request.user)
56+
featuredata = {k.feature: k.data for k in result}
57+
if len(features) == 1:
58+
result = result.first()
59+
data = None if result is None else result.data
60+
return Response({"data": data, "features": featuredata})
61+
else:
62+
return Response({"features": featuredata})
5463

5564
def put(self, request):
5665
serializer = PromptsActivitySerializer(data=request.data)

static/app/actionCreators/prompts.tsx

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,13 @@ type PromptCheckParams = {
4646
feature: string;
4747
};
4848

49+
export type PromptResponseItem = {
50+
snoozed_ts?: number;
51+
dismissed_ts?: number;
52+
};
4953
export type PromptResponse = {
50-
data?: {
51-
snoozed_ts?: number;
52-
dismissed_ts?: number;
53-
};
54+
data?: PromptResponseItem;
55+
features?: {[key: string]: PromptResponseItem};
5456
};
5557

5658
export type PromptData = null | {
@@ -85,3 +87,40 @@ export async function promptsCheck(
8587
snoozedTime: data.snoozed_ts,
8688
};
8789
}
90+
91+
/**
92+
* Get the status of many prompt
93+
*/
94+
export async function batchedPromptsCheck<T extends readonly string[]>(
95+
api: Client,
96+
features: T,
97+
params: {organizationId: string; projectId?: string}
98+
): Promise<{[key in T[number]]: PromptData}> {
99+
const query = {
100+
feature: features,
101+
organization_id: params.organizationId,
102+
...(params.projectId === undefined ? {} : {project_id: params.projectId}),
103+
};
104+
105+
const response: PromptResponse = await api.requestPromise('/prompts-activity/', {
106+
query,
107+
});
108+
const responseFeatures = response?.features;
109+
110+
const result: {[key in T[number]]?: PromptData} = {};
111+
if (!responseFeatures) {
112+
return result as {[key in T[number]]: PromptData};
113+
}
114+
for (const featureName of features) {
115+
const item = responseFeatures[featureName];
116+
if (item) {
117+
result[featureName] = {
118+
dismissedTime: item.dismissed_ts,
119+
snoozedTime: item.snoozed_ts,
120+
};
121+
} else {
122+
result[featureName] = null;
123+
}
124+
}
125+
return result as {[key in T[number]]: PromptData};
126+
}

static/app/types/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,9 @@ export enum DataCategory {
785785
TRANSACTIONS = 'transactions',
786786
ATTACHMENTS = 'attachments',
787787
}
788+
789+
export type EventType = 'error' | 'transaction' | 'attachment';
790+
788791
export const DataCategoryName = {
789792
[DataCategory.ERRORS]: 'Errors',
790793
[DataCategory.TRANSACTIONS]: 'Transactions',

tests/sentry/api/endpoints/test_prompts_activity.py

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,20 @@ def test_invalid_feature(self):
3434

3535
assert resp.status_code == 400
3636

37+
def test_batched_invalid_feature(self):
38+
# Invalid feature prompt name
39+
resp = self.client.put(
40+
self.path,
41+
{
42+
"organization_id": self.org.id,
43+
"project_id": self.project.id,
44+
"feature": ["releases", "gibberish"],
45+
"status": "dismissed",
46+
},
47+
)
48+
49+
assert resp.status_code == 400
50+
3751
def test_invalid_project(self):
3852
# Invalid project id
3953
data = {
@@ -64,7 +78,7 @@ def test_dismiss(self):
6478
}
6579
resp = self.client.get(self.path, data)
6680
assert resp.status_code == 200
67-
assert resp.data == {}
81+
assert resp.data.get("data", None) is None
6882

6983
self.client.put(
7084
self.path,
@@ -89,7 +103,7 @@ def test_snooze(self):
89103
}
90104
resp = self.client.get(self.path, data)
91105
assert resp.status_code == 200
92-
assert resp.data == {}
106+
assert resp.data.get("data", None) is None
93107

94108
self.client.put(
95109
self.path,
@@ -106,3 +120,44 @@ def test_snooze(self):
106120
assert resp.status_code == 200
107121
assert "data" in resp.data
108122
assert "snoozed_ts" in resp.data["data"]
123+
124+
def test_batched(self):
125+
data = {
126+
"organization_id": self.org.id,
127+
"project_id": self.project.id,
128+
"feature": ["releases", "alert_stream"],
129+
}
130+
resp = self.client.get(self.path, data)
131+
assert resp.status_code == 200
132+
assert resp.data["features"].get("releases", None) is None
133+
assert resp.data["features"].get("alert_stream", None) is None
134+
135+
self.client.put(
136+
self.path,
137+
{
138+
"organization_id": self.org.id,
139+
"project_id": self.project.id,
140+
"feature": "releases",
141+
"status": "dismissed",
142+
},
143+
)
144+
145+
resp = self.client.get(self.path, data)
146+
assert resp.status_code == 200
147+
assert "dismissed_ts" in resp.data["features"]["releases"]
148+
assert resp.data["features"].get("alert_stream", None) is None
149+
150+
self.client.put(
151+
self.path,
152+
{
153+
"organization_id": self.org.id,
154+
"project_id": self.project.id,
155+
"feature": "alert_stream",
156+
"status": "snoozed",
157+
},
158+
)
159+
160+
resp = self.client.get(self.path, data)
161+
assert resp.status_code == 200
162+
assert "dismissed_ts" in resp.data["features"]["releases"]
163+
assert "snoozed_ts" in resp.data["features"]["alert_stream"]

0 commit comments

Comments
 (0)