Skip to content

Commit 6457270

Browse files
authored
feat(cra-metrics): Adds crash rate alerts over metrics [INGEST-629] [INGEST-632] (#30400)
* feat(cra-metrics): Adds crash rate alerts over metrics Adds ability to create crash rate alerts over metrics for sessions. Adds `EntitySubscription` class that is an abstraction layer for all entity subscriptions
1 parent 44d7aa2 commit 6457270

File tree

12 files changed

+737
-153
lines changed

12 files changed

+737
-153
lines changed

mypy.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ files = src/sentry/api/bases/external_actor.py,
5959
src/sentry/shared_integrations/constants.py,
6060
src/sentry/snuba/outcomes.py,
6161
src/sentry/snuba/query_subscription_consumer.py,
62+
src/sentry/snuba/entity_subscription.py,
6263
src/sentry/spans/,
6364
src/sentry/tasks/app_store_connect.py,
6465
src/sentry/tasks/low_priority_symbolication.py,

src/sentry/api/serializers/models/incident.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
IncidentSeen,
1212
IncidentSubscription,
1313
)
14+
from sentry.snuba.entity_subscription import apply_dataset_query_conditions
1415
from sentry.snuba.models import QueryDatasets
15-
from sentry.snuba.tasks import apply_dataset_query_conditions
1616

1717

1818
@register(Incident)

src/sentry/exceptions.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,11 @@ class InvalidSearchQuery(Exception):
6969

7070
class UnableToAcceptMemberInvitationException(Exception):
7171
pass
72+
73+
74+
class UnsupportedQuerySubscription(Exception):
75+
pass
76+
77+
78+
class InvalidQuerySubscription(Exception):
79+
pass

src/sentry/incidents/endpoints/serializers.py

Lines changed: 50 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
import logging
22
import operator
3+
from copy import copy
34
from datetime import timedelta
45

56
from django.db import transaction
67
from django.utils import timezone
78
from django.utils.encoding import force_text
89
from rest_framework import serializers
10+
from snuba_sdk.legacy import json_to_snql
911

1012
from sentry import analytics
1113
from sentry.api.fields.actor import ActorField
1214
from sentry.api.serializers.rest_framework.base import CamelSnakeModelSerializer
1315
from sentry.api.serializers.rest_framework.environment import EnvironmentField
1416
from sentry.api.serializers.rest_framework.project import ProjectField
15-
from sentry.exceptions import InvalidSearchQuery
17+
from sentry.exceptions import InvalidSearchQuery, UnsupportedQuerySubscription
1618
from sentry.incidents.logic import (
1719
CRITICAL_TRIGGER_LABEL,
1820
WARNING_TRIGGER_LABEL,
@@ -43,10 +45,12 @@
4345
from sentry.models import OrganizationMember, SentryAppInstallation, Team, User
4446
from sentry.shared_integrations.exceptions import ApiRateLimitedError
4547
from sentry.snuba.dataset import Dataset
48+
from sentry.snuba.entity_subscription import map_aggregate_to_entity_subscription
4649
from sentry.snuba.models import QueryDatasets, SnubaQueryEventType
4750
from sentry.snuba.tasks import build_snuba_filter
51+
from sentry.utils import json
4852
from sentry.utils.compat import zip
49-
from sentry.utils.snuba import raw_query
53+
from sentry.utils.snuba import raw_snql_query
5054

5155
logger = logging.getLogger(__name__)
5256

@@ -443,7 +447,8 @@ def validate_dataset(self, dataset):
443447
)
444448

445449
def validate_event_types(self, event_types):
446-
if self.initial_data.get("dataset") == Dataset.Sessions.value:
450+
dataset = self.initial_data.get("dataset")
451+
if dataset not in [Dataset.Events.value, Dataset.Transactions.value]:
447452
return []
448453
try:
449454
return [SnubaQueryEventType.EventType[event_type.upper()] for event_type in event_types]
@@ -477,13 +482,24 @@ def validate(self, data):
477482
# the query. We don't use the returned data anywhere, so it doesn't
478483
# matter which.
479484
project_id = list(self.context["organization"].project_set.all()[:1])
485+
486+
try:
487+
entity_subscription = map_aggregate_to_entity_subscription(
488+
dataset=QueryDatasets(data["dataset"]),
489+
aggregate=data["aggregate"],
490+
extra_fields={
491+
"org_id": project_id[0].organization_id,
492+
"event_types": data.get("event_types"),
493+
},
494+
)
495+
except UnsupportedQuerySubscription as e:
496+
raise serializers.ValidationError(f"{e}")
497+
480498
try:
481499
snuba_filter = build_snuba_filter(
482-
data["dataset"],
500+
entity_subscription,
483501
data["query"],
484-
data["aggregate"],
485502
data.get("environment"),
486-
data.get("event_types"),
487503
params={
488504
"project_id": [p.id for p in project_id],
489505
"start": timezone.now() - timedelta(minutes=10),
@@ -503,18 +519,35 @@ def validate(self, data):
503519
dataset = Dataset(data["dataset"].value)
504520
self._validate_time_window(dataset, data.get("time_window"))
505521

522+
conditions = copy(snuba_filter.conditions)
523+
time_col = entity_subscription.time_col
524+
conditions += [
525+
[time_col, ">=", snuba_filter.start],
526+
[time_col, "<", snuba_filter.end],
527+
]
528+
529+
body = {
530+
"project": project_id[0].id,
531+
"project_id": project_id[0].id,
532+
"aggregations": snuba_filter.aggregations,
533+
"conditions": conditions,
534+
"filter_keys": snuba_filter.filter_keys,
535+
"having": snuba_filter.having,
536+
"dataset": dataset.value,
537+
"limit": 1,
538+
**entity_subscription.get_entity_extra_params(),
539+
}
540+
506541
try:
507-
raw_query(
508-
aggregations=snuba_filter.aggregations,
509-
start=snuba_filter.start,
510-
end=snuba_filter.end,
511-
conditions=snuba_filter.conditions,
512-
filter_keys=snuba_filter.filter_keys,
513-
having=snuba_filter.having,
514-
dataset=dataset,
515-
limit=1,
516-
referrer="alertruleserializer.test_query",
542+
snql_query = json_to_snql(body, entity_subscription.entity_key.value)
543+
snql_query.validate()
544+
except Exception as e:
545+
raise serializers.ValidationError(
546+
str(e), params={"params": json.dumps(body), "dataset": data["dataset"].value}
517547
)
548+
549+
try:
550+
raw_snql_query(snql_query, referrer="alertruleserializer.test_query")
518551
except Exception:
519552
logger.exception("Error while validating snuba alert rule query")
520553
raise serializers.ValidationError(
@@ -582,7 +615,7 @@ def _translate_thresholds(self, threshold_type, comparison_delta, triggers, data
582615

583616
@staticmethod
584617
def _validate_time_window(dataset, time_window):
585-
if dataset == Dataset.Sessions:
618+
if dataset in [Dataset.Sessions, Dataset.Metrics]:
586619
# Validate time window
587620
if time_window not in CRASH_RATE_ALERTS_ALLOWED_TIME_WINDOWS:
588621
raise serializers.ValidationError(

src/sentry/incidents/logic.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from sentry.search.events.filter import get_filter
3636
from sentry.shared_integrations.exceptions import DuplicateDisplayNameError
3737
from sentry.snuba.dataset import Dataset
38+
from sentry.snuba.entity_subscription import map_aggregate_to_entity_subscription
3839
from sentry.snuba.models import QueryDatasets
3940
from sentry.snuba.subscriptions import (
4041
bulk_create_snuba_subscriptions,
@@ -288,12 +289,15 @@ def build_incident_query_params(incident, start=None, end=None, windowed_stats=F
288289
params["project_id"] = project_ids
289290

290291
snuba_query = incident.alert_rule.snuba_query
292+
entity_subscription = map_aggregate_to_entity_subscription(
293+
dataset=QueryDatasets(snuba_query.dataset),
294+
aggregate=snuba_query.aggregate,
295+
extra_fields={"org_id": incident.organization.id, "event_types": snuba_query.event_types},
296+
)
291297
snuba_filter = build_snuba_filter(
292-
QueryDatasets(snuba_query.dataset),
298+
entity_subscription,
293299
snuba_query.query,
294-
snuba_query.aggregate,
295300
snuba_query.environment,
296-
snuba_query.event_types,
297301
params=params,
298302
)
299303

src/sentry/incidents/subscription_processor.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
from sentry.release_health.metrics import reverse_tag_value, tag_key
3333
from sentry.snuba.dataset import Dataset
3434
from sentry.snuba.models import QueryDatasets
35-
from sentry.snuba.tasks import build_snuba_filter
35+
from sentry.snuba.tasks import build_snuba_filter, map_aggregate_to_entity_subscription
3636
from sentry.utils import metrics, redis
3737
from sentry.utils.compat import zip
3838
from sentry.utils.dates import to_datetime, to_timestamp
@@ -176,13 +176,19 @@ def get_comparison_aggregation_value(self, subscription_update, aggregation_valu
176176
snuba_query = self.subscription.snuba_query
177177
start = end - timedelta(seconds=snuba_query.time_window)
178178

179+
entity_subscription = map_aggregate_to_entity_subscription(
180+
dataset=QueryDatasets(snuba_query.dataset),
181+
aggregate=snuba_query.aggregate,
182+
extra_fields={
183+
"org_id": self.subscription.project.organization,
184+
"event_types": snuba_query.event_types,
185+
},
186+
)
179187
try:
180188
snuba_filter = build_snuba_filter(
181-
QueryDatasets(snuba_query.dataset),
189+
entity_subscription,
182190
snuba_query.query,
183-
snuba_query.aggregate,
184191
snuba_query.environment,
185-
snuba_query.event_types,
186192
params={
187193
"project_id": [self.subscription.project_id],
188194
"start": start,

src/sentry/snuba/dataset.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ class Dataset(Enum):
1414

1515
@unique
1616
class EntityKey(Enum):
17+
Events = "events"
18+
Sessions = "sessions"
19+
Transactions = "transactions"
1720
MetricsSets = "metrics_sets"
1821
MetricsCounters = "metrics_counters"
1922
MetricsDistributions = "metrics_distributions"

0 commit comments

Comments
 (0)