Skip to content

Commit 5b56826

Browse files
authored
Merge branch 'master' into jb/button/chonkify
2 parents c88ebac + 79c646d commit 5b56826

File tree

103 files changed

+2093
-855
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

103 files changed

+2093
-855
lines changed

.github/CODEOWNERS

+1-1
Original file line numberDiff line numberDiff line change
@@ -612,7 +612,7 @@ tests/sentry/api/endpoints/test_organization_dashboard_widget_details.py @ge
612612
/static/app/components/events/autofix/ @getsentry/machine-learning-ai
613613
/static/app/components/modals/autofixSetupModal.spec.tsx @getsentry/machine-learning-ai
614614
/static/app/components/modals/autofixSetupModal.tsx @getsentry/machine-learning-ai
615-
/src/sentry/seer/fetch_issues_given_patches.py @getsentry/machine-learning-ai
615+
/src/sentry/seer/ @getsentry/machine-learning-ai
616616
## End of ML & AI
617617

618618
## Processing

.github/workflows/scripts/getsentry-dispatch.js

-4
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,6 @@
77
* deleted/renamed in `getsentry`, this will fail
88
*/
99
const DISPATCHES = [
10-
{
11-
workflow: 'js-build-and-lint.yml',
12-
pathFilterName: 'frontend_all',
13-
},
1410
{
1511
workflow: 'backend.yml',
1612
pathFilterName: 'backend_all',

src/sentry/api/endpoints/group_ai_summary.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -245,9 +245,10 @@ def post(self, request: Request, group: Group) -> Response:
245245
group=group, event_id=event.event_id, user=request.user
246246
)
247247

248-
if response.status_code != 202:
249-
# If autofix trigger fails, we don't cache to let it error and we can run again, this is only temporary for when we're testing this internally.
250-
return response
248+
if response.status_code != 202:
249+
# If autofix trigger fails, we don't cache to let it error and we can run again
250+
# This is only temporary for when we're testing this internally.
251+
return response
251252

252253
summary_dict = issue_summary.dict()
253254
summary_dict["event_id"] = event.event_id

src/sentry/api/event_search.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -632,15 +632,13 @@ class SearchVisitor(NodeVisitor):
632632

633633
def __init__(
634634
self,
635-
config: SearchConfig | None = None,
635+
config: SearchConfig,
636636
params: ParamsType | None = None,
637637
get_field_type: Callable[[str], str | None] | None = None,
638638
get_function_result_type: Callable[[str], str | None] | None = None,
639639
) -> None:
640640
super().__init__()
641641

642-
if config is None:
643-
config = SearchConfig()
644642
self.config = config
645643
self.params = params if params is not None else {}
646644

src/sentry/api/helpers/group_index/index.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from sentry.models.environment import Environment
2121
from sentry.models.group import Group, looks_like_short_id
2222
from sentry.models.groupsearchview import GroupSearchView
23+
from sentry.models.groupsearchviewstarred import GroupSearchViewStarred
2324
from sentry.models.organization import Organization
2425
from sentry.models.project import Project
2526
from sentry.models.release import Release
@@ -104,11 +105,12 @@ def build_query_params_from_request(
104105
if selected_view_id:
105106
default_view = GroupSearchView.objects.filter(id=int(selected_view_id)).first()
106107
else:
107-
default_view = GroupSearchView.objects.filter(
108+
first_starred_view = GroupSearchViewStarred.objects.filter(
108109
organization=organization,
109110
user_id=request.user.id,
110111
position=0,
111112
).first()
113+
default_view = first_starred_view.group_search_view if first_starred_view else None
112114

113115
if default_view:
114116
query_kwargs["sort_by"] = default_view.query_sort

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

+4-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@ def get_attrs(self, item_list, user, **kwargs):
5050
)
5151
)
5252

53-
pull_request_by_commit = {pr.merge_commit_sha: serialize(pr) for pr in pull_requests}
53+
pull_request_by_commit = {
54+
pr.merge_commit_sha: serialized_pr
55+
for (pr, serialized_pr) in zip(pull_requests, serialize(pull_requests))
56+
}
5457

5558
result = {}
5659
for item in item_list:

src/sentry/incidents/metric_alert_detector.py

+2
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def validate(self, attrs):
5151
raise serializers.ValidationError("Too many conditions")
5252
return attrs
5353

54+
# TODO - @saponifi3d - we can make this more generic and move it into the base Detector
5455
def update_data_conditions(self, instance: Detector, data_conditions: list[DataConditionType]):
5556
"""
5657
Update the data condition if it already exists, create one if it does not
@@ -116,6 +117,7 @@ def update_data_source(self, instance: Detector, data_source: SnubaQueryDataSour
116117
event_types=data_source.get("event_types", [event_type for event_type in event_types]),
117118
)
118119

120+
# TODO - @saponifi3d - we can make this more generic and move it into the base Detector
119121
def update(self, instance: Detector, validated_data: dict[str, Any]):
120122
instance.name = validated_data.get("name", instance.name)
121123
instance.type = validated_data.get("detector_type", instance.group_type).slug

src/sentry/incidents/subscription_processor.py

-24
Original file line numberDiff line numberDiff line change
@@ -411,18 +411,6 @@ def process_update(self, subscription_update: QuerySubscriptionUpdate) -> None:
411411
)
412412

413413
aggregation_value = self.get_aggregation_value(subscription_update)
414-
if features.has(
415-
"organizations:failure-rate-metric-alert-logging",
416-
self.subscription.project.organization,
417-
):
418-
logger.info(
419-
"Update value in subscription processor",
420-
extra={
421-
"result": subscription_update,
422-
"aggregation_value": aggregation_value,
423-
"rule_id": self.alert_rule.id,
424-
},
425-
)
426414

427415
has_anomaly_detection = features.has(
428416
"organizations:anomaly-detection-alerts", self.subscription.project.organization
@@ -444,18 +432,6 @@ def process_update(self, subscription_update: QuerySubscriptionUpdate) -> None:
444432
last_update=self.last_update.timestamp(),
445433
aggregation_value=aggregation_value,
446434
)
447-
# XXX (mifu67): log problematic rule, to be deleted later
448-
if features.has(
449-
"feature.organizations:failure-rate-metric-alert-logging",
450-
self.subscription.project.organization,
451-
):
452-
logger.info(
453-
"Received this response from Seer",
454-
extra={
455-
"potential_anomalies": potential_anomalies,
456-
"alert_rule_id": self.alert_rule.id,
457-
},
458-
)
459435
if potential_anomalies is None:
460436
logger.info(
461437
"No potential anomalies found",

src/sentry/incidents/utils/metric_issue_poc.py

+35-13
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
from django.utils.translation import gettext as _
77

88
from sentry import features
9-
from sentry.incidents.models.alert_rule import AlertRule, AlertRuleThresholdType
10-
from sentry.incidents.models.incident import Incident, IncidentStatus
9+
from sentry.constants import CRASH_RATE_ALERT_AGGREGATE_ALIAS
10+
from sentry.incidents.models.alert_rule import AlertRule, AlertRuleThresholdType, AlertRuleTrigger
11+
from sentry.incidents.models.incident import INCIDENT_STATUS, Incident, IncidentStatus
1112
from sentry.incidents.utils.format_duration import format_duration_idiomatic
1213
from sentry.integrations.metric_alerts import TEXT_COMPARISON_DELTA
1314
from sentry.issues.grouptype import MetricIssuePOC
@@ -43,16 +44,21 @@ def to_dict(self) -> dict[str, Any]:
4344
"count_unique(tags[sentry:user])": "Number of users affected",
4445
"percentage(sessions_crashed, sessions)": "Crash free session rate",
4546
"percentage(users_crashed, users)": "Crash free user rate",
47+
"failure_rate()": "Failure rate",
48+
"apdex()": "Apdex score",
4649
}
4750

4851

49-
def construct_title(alert_rule: AlertRule) -> str:
52+
def construct_title(alert_rule: AlertRule, status: int) -> str:
5053
# Parse the aggregate key from the alert rule
5154
agg_display_key = alert_rule.snuba_query.aggregate
5255
if is_mri_field(agg_display_key):
53-
agg_text = format_mri_field(agg_display_key)
56+
aggregate = format_mri_field(agg_display_key)
57+
elif CRASH_RATE_ALERT_AGGREGATE_ALIAS in agg_display_key:
58+
agg_display_key = agg_display_key.split(f"AS {CRASH_RATE_ALERT_AGGREGATE_ALIAS}")[0].strip()
59+
aggregate = QUERY_AGGREGATION_DISPLAY.get(agg_display_key, agg_display_key)
5460
else:
55-
agg_text = QUERY_AGGREGATION_DISPLAY.get(agg_display_key, alert_rule.snuba_query.aggregate)
61+
aggregate = QUERY_AGGREGATION_DISPLAY.get(agg_display_key, alert_rule.snuba_query.aggregate)
5662

5763
# Determine the higher or lower comparison
5864
higher_or_lower = ""
@@ -61,19 +67,35 @@ def construct_title(alert_rule: AlertRule) -> str:
6167
else:
6268
higher_or_lower = "less than" if alert_rule.comparison_delta else "below"
6369

70+
label = INCIDENT_STATUS[IncidentStatus(status)]
71+
6472
# Format the time window for the threshold
65-
time_window = alert_rule.snuba_query.time_window // 60
66-
title = f"{agg_text} in the last {format_duration_idiomatic(time_window)} {higher_or_lower}"
73+
time_window = format_duration_idiomatic(alert_rule.snuba_query.time_window // 60)
6774

6875
# If the alert rule has a comparison delta, format the comparison string
76+
comparison: str | int | float = "threshold"
6977
if alert_rule.comparison_delta:
7078
comparison_delta_minutes = alert_rule.comparison_delta // 60
71-
comparison_string = TEXT_COMPARISON_DELTA.get(
72-
comparison_delta_minutes, f"same time {comparison_delta_minutes} minutes ago"
79+
comparison = TEXT_COMPARISON_DELTA.get(
80+
comparison_delta_minutes, f"same time {comparison_delta_minutes} minutes ago "
7381
)
74-
return _(f"{title} {comparison_string}")
75-
76-
return _(f"{title} threshold")
82+
else:
83+
# Otherwise, check if there is a trigger with a threshold
84+
trigger = AlertRuleTrigger.objects.filter(id=alert_rule.id, label=label.lower()).first()
85+
if trigger:
86+
threshold = trigger.alert_threshold
87+
comparison = int(threshold) if threshold % 1 == 0 else threshold
88+
89+
template = "{label}: {metric} in the last {time_window} {higher_or_lower} {comparison}"
90+
return _(
91+
template.format(
92+
label=label.capitalize(),
93+
metric=aggregate,
94+
higher_or_lower=higher_or_lower,
95+
comparison=comparison,
96+
time_window=time_window,
97+
)
98+
)
7799

78100

79101
def _build_occurrence_from_incident(
@@ -88,7 +110,7 @@ def _build_occurrence_from_incident(
88110
else PriorityLevel.MEDIUM
89111
)
90112
fingerprint = [str(incident.alert_rule.id)]
91-
title = construct_title(incident.alert_rule)
113+
title = construct_title(incident.alert_rule, incident.status)
92114
return IssueOccurrence(
93115
id=uuid4().hex,
94116
project_id=project.id,

src/sentry/integrations/bitbucket/client.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
from requests import PreparedRequest
99

10-
from sentry.integrations.base import IntegrationFeatureNotImplementedError
1110
from sentry.integrations.client import ApiClient
1211
from sentry.integrations.models.integration import Integration
1312
from sentry.integrations.services.integration.model import RpcIntegration
@@ -182,4 +181,13 @@ def check_file(self, repo: Repository, path: str, version: str | None) -> object
182181
def get_file(
183182
self, repo: Repository, path: str, ref: str | None, codeowners: bool = False
184183
) -> str:
185-
raise IntegrationFeatureNotImplementedError
184+
response = self.get_cached(
185+
path=BitbucketAPIPath.source.format(
186+
repo=repo.name,
187+
sha=ref,
188+
path=path,
189+
),
190+
allow_redirects=True,
191+
raw_response=True,
192+
)
193+
return response.text

src/sentry/integrations/bitbucket/integration.py

+9
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@
7878
""",
7979
IntegrationFeatures.STACKTRACE_LINK,
8080
),
81+
FeatureDescription(
82+
"""
83+
Import your Bitbucket [CODEOWNERS file](https://support.atlassian.com/bitbucket-cloud/docs/set-up-and-use-code-owners/) and use it alongside your ownership rules to assign Sentry issues.
84+
""",
85+
IntegrationFeatures.CODEOWNERS,
86+
),
8187
]
8288

8389
metadata = IntegrationMetadata(
@@ -94,6 +100,8 @@
94100

95101

96102
class BitbucketIntegration(RepositoryIntegration, BitbucketIssuesSpec):
103+
codeowners_locations = [".bitbucket/CODEOWNERS"]
104+
97105
@property
98106
def integration_name(self) -> str:
99107
return "bitbucket"
@@ -185,6 +193,7 @@ class BitbucketIntegrationProvider(IntegrationProvider):
185193
IntegrationFeatures.ISSUE_BASIC,
186194
IntegrationFeatures.COMMITS,
187195
IntegrationFeatures.STACKTRACE_LINK,
196+
IntegrationFeatures.CODEOWNERS,
188197
]
189198
)
190199

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

+36-35
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,15 @@
4141
)
4242
from sentry.rules.actions.notify_event_service import find_alert_rule_action_ui_component
4343
from sentry.sentry_apps.api.serializers.app_platform_event import AppPlatformEvent
44+
from sentry.sentry_apps.metrics import (
45+
SentryAppEventType,
46+
SentryAppInteractionEvent,
47+
SentryAppInteractionType,
48+
)
4449
from sentry.sentry_apps.models.sentry_app import SentryApp
4550
from sentry.sentry_apps.models.sentry_app_installation import SentryAppInstallation
4651
from sentry.shared_integrations.exceptions import ApiError
47-
from sentry.utils import json, metrics
52+
from sentry.utils import json
4853
from sentry.utils.sentry_apps import send_and_save_webhook_request
4954

5055
if TYPE_CHECKING:
@@ -368,44 +373,40 @@ def send_incident_alert_notification(
368373
notification_uuid: str | None = None,
369374
) -> bool:
370375
try:
371-
sentry_app = SentryApp.objects.get(id=sentry_app_id)
372-
except SentryApp.DoesNotExist:
373-
logger.info(
374-
"metric_alert_webhook.missing_sentryapp",
375-
extra={
376-
"sentry_app_id": sentry_app_id,
377-
"organization_id": organization_id,
378-
},
379-
)
376+
new_status_str = INCIDENT_STATUS[IncidentStatus(new_status)].lower()
377+
event = SentryAppEventType(f"metric_alert.{new_status_str}")
378+
except ValueError as e:
379+
sentry_sdk.capture_exception(e)
380380
return False
381381

382-
metrics.incr("notifications.sent", instance=sentry_app.slug, skip_internal=False)
382+
with SentryAppInteractionEvent(
383+
operation_type=SentryAppInteractionType.PREPARE_WEBHOOK,
384+
event_type=event,
385+
).capture() as lifecycle:
386+
try:
387+
sentry_app = SentryApp.objects.get(id=sentry_app_id)
388+
except SentryApp.DoesNotExist as e:
389+
sentry_sdk.capture_exception(e)
390+
lifecycle.record_failure(e)
391+
return False
383392

384-
try:
385-
install = SentryAppInstallation.objects.get(
386-
organization_id=organization_id,
387-
sentry_app=sentry_app,
388-
status=SentryAppInstallationStatus.INSTALLED,
389-
)
390-
except SentryAppInstallation.DoesNotExist:
391-
logger.info(
392-
"metric_alert_webhook.missing_installation",
393-
extra={
394-
"action": action_id,
395-
"incident": incident_id,
396-
"organization_id": organization_id,
397-
"sentry_app_id": sentry_app.id,
398-
},
399-
exc_info=True,
400-
)
401-
return False
393+
try:
394+
install = SentryAppInstallation.objects.get(
395+
organization_id=organization_id,
396+
sentry_app=sentry_app,
397+
status=SentryAppInstallationStatus.INSTALLED,
398+
)
399+
except SentryAppInstallation.DoesNotExist as e:
400+
sentry_sdk.capture_exception(e)
401+
lifecycle.record_failure(e)
402+
return False
402403

403-
app_platform_event = AppPlatformEvent(
404-
resource="metric_alert",
405-
action=INCIDENT_STATUS[IncidentStatus(new_status)].lower(),
406-
install=install,
407-
data=json.loads(incident_attachment_json),
408-
)
404+
app_platform_event = AppPlatformEvent(
405+
resource="metric_alert",
406+
action=new_status_str,
407+
install=install,
408+
data=json.loads(incident_attachment_json),
409+
)
409410

410411
# Can raise errors if client returns >= 400
411412
send_and_save_webhook_request(

src/sentry/models/commitfilechange.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,16 @@ def is_valid_type(value: str) -> bool:
4848

4949

5050
def process_resource_change(instance, **kwargs):
51+
from sentry.integrations.bitbucket.integration import BitbucketIntegration
5152
from sentry.integrations.github.integration import GitHubIntegration
5253
from sentry.integrations.gitlab.integration import GitlabIntegration
5354
from sentry.tasks.codeowners import code_owners_auto_sync
5455

5556
def _spawn_task():
56-
filepaths = set(GitHubIntegration.codeowners_locations) | set(
57-
GitlabIntegration.codeowners_locations
57+
filepaths = (
58+
set(GitHubIntegration.codeowners_locations)
59+
| set(GitlabIntegration.codeowners_locations)
60+
| set(BitbucketIntegration.codeowners_locations)
5861
)
5962

6063
# CODEOWNERS file added or modified, trigger auto-sync

src/sentry/models/project.py

+1
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@
108108
"javascript-solidstart",
109109
"javascript-svelte",
110110
"javascript-sveltekit",
111+
"javascript-tanstackstart-react",
111112
"javascript-nuxt",
112113
"javascript-vue",
113114
"kotlin",

0 commit comments

Comments
 (0)