Skip to content

Commit cff8119

Browse files
Merge branch 'master' into brendan/get-orgs-with-usage
2 parents 9f7e564 + 246508e commit cff8119

File tree

38 files changed

+1700
-641
lines changed

38 files changed

+1700
-641
lines changed
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
from __future__ import annotations
2+
3+
from sentry_protos.billing.v1.data_category_pb2 import DataCategory as ProtoDataCategory
4+
5+
from sentry.constants import DataCategory
6+
from sentry.utils import metrics
7+
8+
SENTRY_TO_PROTO_CATEGORY: dict[int, int] = {
9+
int(DataCategory.ERROR): ProtoDataCategory.DATA_CATEGORY_ERROR,
10+
int(DataCategory.TRANSACTION): ProtoDataCategory.DATA_CATEGORY_TRANSACTION,
11+
int(DataCategory.ATTACHMENT): ProtoDataCategory.DATA_CATEGORY_ATTACHMENT,
12+
int(DataCategory.PROFILE): ProtoDataCategory.DATA_CATEGORY_PROFILE,
13+
int(DataCategory.REPLAY): ProtoDataCategory.DATA_CATEGORY_REPLAY,
14+
int(DataCategory.MONITOR): ProtoDataCategory.DATA_CATEGORY_MONITOR,
15+
int(DataCategory.SPAN): ProtoDataCategory.DATA_CATEGORY_SPAN,
16+
int(DataCategory.USER_REPORT_V2): ProtoDataCategory.DATA_CATEGORY_USER_REPORT_V2,
17+
int(DataCategory.PROFILE_DURATION): ProtoDataCategory.DATA_CATEGORY_PROFILE_DURATION,
18+
int(DataCategory.LOG_BYTE): ProtoDataCategory.DATA_CATEGORY_LOG_BYTE,
19+
int(DataCategory.PROFILE_DURATION_UI): ProtoDataCategory.DATA_CATEGORY_PROFILE_DURATION_UI,
20+
int(DataCategory.SEER_AUTOFIX): ProtoDataCategory.DATA_CATEGORY_SEER_AUTOFIX,
21+
int(DataCategory.SEER_SCANNER): ProtoDataCategory.DATA_CATEGORY_SEER_SCANNER,
22+
int(DataCategory.SIZE_ANALYSIS): ProtoDataCategory.DATA_CATEGORY_SIZE_ANALYSIS,
23+
int(DataCategory.INSTALLABLE_BUILD): ProtoDataCategory.DATA_CATEGORY_INSTALLABLE_BUILD,
24+
int(DataCategory.TRACE_METRIC): ProtoDataCategory.DATA_CATEGORY_TRACE_METRIC,
25+
int(DataCategory.DEFAULT): ProtoDataCategory.DATA_CATEGORY_DEFAULT,
26+
int(DataCategory.SECURITY): ProtoDataCategory.DATA_CATEGORY_SECURITY,
27+
int(DataCategory.PROFILE_CHUNK): ProtoDataCategory.DATA_CATEGORY_PROFILE_CHUNK,
28+
int(DataCategory.PROFILE_CHUNK_UI): ProtoDataCategory.DATA_CATEGORY_PROFILE_CHUNK_UI,
29+
}
30+
31+
32+
PROTO_TO_SENTRY_CATEGORY: dict[int, int] = {v: k for k, v in SENTRY_TO_PROTO_CATEGORY.items()}
33+
34+
35+
def proto_to_sentry_category(proto_category: int) -> int:
36+
"""Convert a proto DataCategory to its Sentry equivalent.
37+
38+
For categories with a known mapping, returns the sentry int value.
39+
For unmapped categories, passes through the original int value and
40+
emits a metric so we can track how often this happens.
41+
"""
42+
result = PROTO_TO_SENTRY_CATEGORY.get(proto_category)
43+
if result is None:
44+
metrics.incr(
45+
"billing.proto_category_mapping.unmapped_reverse",
46+
tags={"proto_category": str(proto_category)},
47+
)
48+
return proto_category
49+
return result
50+
51+
52+
def sentry_to_proto_category(category: int | DataCategory) -> ProtoDataCategory.ValueType:
53+
"""Convert a Sentry DataCategory to its proto equivalent.
54+
55+
For categories with a known mapping, returns the proto enum value.
56+
For unmapped categories, passes through the original int value and
57+
emits a metric so we can track how often this happens.
58+
"""
59+
cat_int = int(category)
60+
result = SENTRY_TO_PROTO_CATEGORY.get(cat_int)
61+
if result is None:
62+
metrics.incr(
63+
"billing.proto_category_mapping.unmapped",
64+
tags={"sentry_category": str(cat_int)},
65+
)
66+
return ProtoDataCategory.ValueType(cat_int)
67+
return ProtoDataCategory.ValueType(result)

src/sentry/billing/platform/services/usage/_category_mapping.py

Lines changed: 0 additions & 50 deletions
This file was deleted.

src/sentry/billing/platform/services/usage/_outcomes_query.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
)
3535
from snuba_sdk.orderby import Direction
3636

37-
from sentry.billing.platform.services.usage._category_mapping import proto_to_relay_category
37+
from sentry.billing.platform.services.category_mapping import proto_to_sentry_category
3838
from sentry.snuba.referrer import Referrer
3939
from sentry.utils import metrics
4040
from sentry.utils.outcomes import Outcome
@@ -94,7 +94,7 @@ def query_outcomes_usage(request: GetUsageRequest) -> GetUsageResponse:
9494
end = _timestamp_to_datetime(request.end) + timedelta(days=1)
9595
# Proto categories use different int values from Relay/ClickHouse
9696
# (e.g., proto ATTACHMENT=3 vs Relay ATTACHMENT=4). Convert before querying.
97-
categories = [proto_to_relay_category(c) for c in request.categories]
97+
categories = [proto_to_sentry_category(c) for c in request.categories]
9898

9999
snuba_request = _build_query(org_id, start, end, categories, total_outcomes=_BILLABLE_OUTCOMES)
100100
result = raw_snql_query(snuba_request, referrer=_REFERRER)

src/sentry/incidents/action_handlers.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -462,15 +462,22 @@ def send_alert(
462462

463463
incident_serialized_response = serialize(incident, serializer=IncidentSerializer())
464464

465-
send_incident_alert_notification(
465+
success = send_incident_alert_notification(
466466
notification_context=notification_context,
467467
alert_context=alert_context,
468468
metric_issue_context=metric_issue_context,
469469
incident_serialized_response=incident_serialized_response,
470470
organization=incident.organization,
471-
project_id=project.id,
472471
notification_uuid=notification_uuid,
473472
)
473+
if success:
474+
self.record_alert_sent_analytics(
475+
organization_id=incident.organization.id,
476+
project_id=project.id,
477+
alert_id=incident.alert_rule.id,
478+
external_id=action.sentry_app_id,
479+
notification_uuid=notification_uuid,
480+
)
474481

475482

476483
def format_duration(minutes):

src/sentry/integrations/services/integration/impl.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
serialize_integration_external_project,
4646
serialize_organization_integration,
4747
)
48+
from sentry.rules.actions.notify_event_service import find_alert_rule_action_ui_component
4849
from sentry.sentry_apps.api.serializers.app_platform_event import AppPlatformEvent
4950
from sentry.sentry_apps.metrics import (
5051
SentryAppEventType,
@@ -53,11 +54,7 @@
5354
)
5455
from sentry.sentry_apps.models.sentry_app import SentryApp
5556
from sentry.sentry_apps.models.sentry_app_installation import SentryAppInstallation
56-
from sentry.sentry_apps.utils.webhooks import (
57-
MetricAlertActionType,
58-
SentryAppResourceType,
59-
find_alert_rule_action_ui_component,
60-
)
57+
from sentry.sentry_apps.utils.webhooks import MetricAlertActionType, SentryAppResourceType
6158
from sentry.shared_integrations.exceptions import ApiError
6259
from sentry.utils import json
6360
from sentry.utils.sentry_apps import send_and_save_webhook_request

src/sentry/notifications/notification_action/metric_alert_registry/handlers/sentry_app_metric_alert_handler.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,5 @@ def send_alert(
5454
metric_issue_context=metric_issue_context,
5555
incident_serialized_response=incident_serialized_response,
5656
organization=organization,
57-
project_id=project.id,
5857
notification_uuid=notification_uuid,
5958
)

src/sentry/notifications/notification_action/utils.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,13 +124,16 @@ def issue_notification_data_factory(invocation: ActionInvocation) -> IssueNotifi
124124
detector=detector,
125125
event_data=event_data,
126126
)
127-
rule_instance.data["tags"] = action.data.get("tags", "")
128-
rule_instance.data["notes"] = action.data.get("notes", "")
127+
tags = action.data.get("tags", None)
128+
tag_list = [tag.strip() for tag in tags.split(",")] if tags else None
129+
notes = action.data.get("notes", None)
129130
rule = SerializableRuleProxy.from_rule(rule_instance)
130131

131132
event_id = getattr(event_data.event, "event_id", None) if event_data.event else None
132133

133134
return IssueNotificationData(
135+
tags=tag_list,
136+
notes=notes,
134137
event_id=event_id,
135138
group_id=event_data.group.id,
136139
notification_uuid=invocation.notification_uuid,

src/sentry/notifications/platform/discord/provider.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
NotificationBodyFormattingBlockType,
2222
NotificationBodyTextBlock,
2323
NotificationBodyTextBlockType,
24+
NotificationCategory,
2425
NotificationData,
2526
NotificationProviderKey,
2627
NotificationRenderedTemplate,
@@ -136,6 +137,16 @@ def is_available(cls, *, organization: RpcOrganizationSummary | None = None) ->
136137
# TODO(ecosystem): Check for the integration, maybe a feature as well
137138
return False
138139

140+
@classmethod
141+
def get_renderer(
142+
cls, *, data: NotificationData, category: NotificationCategory
143+
) -> type[NotificationRenderer[DiscordRenderable]]:
144+
from sentry.notifications.platform.discord.renderers.issue import IssueDiscordRenderer
145+
146+
if category == NotificationCategory.ISSUE:
147+
return IssueDiscordRenderer
148+
return cls.default_renderer
149+
139150
@classmethod
140151
def send(
141152
cls,

src/sentry/notifications/platform/discord/renderers/__init__.py

Whitespace-only changes.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
from __future__ import annotations
2+
3+
from sentry import eventstore
4+
from sentry.integrations.discord.message_builder.issues import DiscordIssuesMessageBuilder
5+
from sentry.models.group import Group
6+
from sentry.notifications.platform.discord.provider import DiscordRenderable
7+
from sentry.notifications.platform.renderer import NotificationRenderer
8+
from sentry.notifications.platform.service import NotificationRenderError
9+
from sentry.notifications.platform.templates.issue import IssueNotificationData
10+
from sentry.notifications.platform.types import (
11+
NotificationData,
12+
NotificationProviderKey,
13+
NotificationRenderedTemplate,
14+
)
15+
from sentry.services.eventstore.models import Event
16+
17+
18+
class IssueDiscordRenderer(NotificationRenderer[DiscordRenderable]):
19+
provider_key = NotificationProviderKey.DISCORD
20+
21+
@classmethod
22+
def render[DataT: NotificationData](
23+
cls, *, data: DataT, rendered_template: NotificationRenderedTemplate
24+
) -> DiscordRenderable:
25+
if not isinstance(data, IssueNotificationData):
26+
raise ValueError(f"IssueDiscordRenderer does not support {data.__class__.__name__}")
27+
28+
# Retrieving Group and Event data is an anti-pattern, do not do this
29+
# in permanent renderers.
30+
try:
31+
group = Group.objects.get_from_cache(id=data.group_id)
32+
except Group.DoesNotExist:
33+
raise NotificationRenderError(f"Group {data.group_id} not found")
34+
35+
group_event = None
36+
if data.event_id:
37+
try:
38+
event = eventstore.backend.get_event_by_id(
39+
project_id=group.project.id, event_id=data.event_id, group_id=data.group_id
40+
)
41+
if isinstance(event, Event):
42+
# Discord only supports GroupEvents, and we can't guarantee
43+
# the type passed by eventstore, so we convert base Events
44+
# to GroupEvents.
45+
group_event = event.for_group(group)
46+
else:
47+
group_event = event
48+
except Exception:
49+
raise NotificationRenderError(f"Failed to retrieve event {data.event_id}")
50+
51+
rules = [data.rule.to_rule()] if data.rule else []
52+
53+
return DiscordIssuesMessageBuilder(
54+
group=group,
55+
event=group_event,
56+
tags=set(data.tags) if data.tags else None,
57+
rules=rules,
58+
link_to_event=True,
59+
).build(notification_uuid=data.notification_uuid)

0 commit comments

Comments
 (0)