Skip to content

Commit b765881

Browse files
authored
Merge branch 'master' into bruno-garcia-patch-1
2 parents 98b141e + b2a034c commit b765881

File tree

205 files changed

+4954
-1553
lines changed

Some content is hidden

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

205 files changed

+4954
-1553
lines changed

migrations_lockfile.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ hybridcloud: 0021_django_arrayfield_scope_list
1313

1414
insights: 0001_add_starred_transactions_model
1515

16-
monitors: 0004_record_date_in_progress_sql_only
16+
monitors: 0005_record_date_in_progress_state
1717

1818
nodestore: 0002_nodestore_no_dictfield
1919

src/sentry/api/endpoints/organization_sdk_deprecations.py

Lines changed: 7 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,18 @@
22
from typing import DefaultDict, TypedDict
33

44
import sentry_sdk
5-
from packaging.version import InvalidVersion, Version
5+
from packaging.version import InvalidVersion
66
from packaging.version import parse as parse_version
77
from rest_framework import serializers
88
from rest_framework.request import Request
99
from rest_framework.response import Response
1010

11-
from sentry import options
1211
from sentry.api.api_owners import ApiOwner
1312
from sentry.api.api_publish_status import ApiPublishStatus
1413
from sentry.api.base import region_silo_endpoint
1514
from sentry.api.bases.organization import OrganizationEndpoint
1615
from sentry.models.project import Project
17-
from sentry.models.projectsdk import EventType, ProjectSDK
16+
from sentry.models.projectsdk import EventType, ProjectSDK, get_minimum_sdk_version
1817

1918

2019
class SDKDeprecationsSerializer(serializers.Serializer):
@@ -93,40 +92,18 @@ def get_event_types(raw_event_type: str) -> list[EventType]:
9392
raise ValueError(f"Unknown event type: {raw_event_type}")
9493

9594

96-
MINIMUM_SDK_VERSION_OPTIONS: dict[tuple[int, str], str] = {
97-
(EventType.PROFILE_CHUNK.value, "sentry.cocoa"): "sdk-deprecation.profile-chunk.cocoa",
98-
(EventType.PROFILE_CHUNK.value, "sentry.python"): "sdk-deprecation.profile-chunk.python",
99-
}
100-
101-
102-
def get_minimum_sdk_version(project_sdk: ProjectSDK) -> Version | None:
103-
parts = project_sdk.sdk_name.split(".", 2)
104-
if len(parts) < 2:
105-
return None
106-
107-
sdk_name = ".".join(parts[:2])
108-
109-
sdk_version_option = MINIMUM_SDK_VERSION_OPTIONS.get((project_sdk.event_type, sdk_name))
110-
if sdk_version_option is None:
111-
return None
112-
113-
sdk_version = options.get(sdk_version_option)
114-
if sdk_version:
115-
try:
116-
return parse_version(sdk_version)
117-
except InvalidVersion as e:
118-
sentry_sdk.capture_exception(e)
119-
return None
120-
121-
12295
def get_deprecation_status(project_sdk: ProjectSDK) -> SDKDeprecation | None:
12396
try:
12497
sdk_version = parse_version(project_sdk.sdk_version)
12598
except InvalidVersion as e:
12699
sentry_sdk.capture_exception(e)
127100
return None
128101

129-
minimum_sdk_version = get_minimum_sdk_version(project_sdk)
102+
minimum_sdk_version = get_minimum_sdk_version(
103+
project_sdk.event_type,
104+
project_sdk.sdk_name,
105+
hard_limit=False,
106+
)
130107

131108
# no minimum sdk version was specified
132109
if minimum_sdk_version is None:

src/sentry/api/endpoints/project_rule_details.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ def put(self, request: Request, project, rule) -> Response:
245245
"name": data["name"],
246246
"environment": data.get("environment"),
247247
"project": project,
248+
"project_id": project.id,
248249
"action_match": data["actionMatch"],
249250
"filter_match": data.get("filterMatch"),
250251
"conditions": conditions,

src/sentry/api/endpoints/project_rules.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -800,6 +800,7 @@ def post(self, request: Request, project) -> Response:
800800
"name": data["name"],
801801
"environment": data.get("environment"),
802802
"project": project,
803+
"project_id": project.id,
803804
"action_match": data["actionMatch"],
804805
"filter_match": data.get("filterMatch"),
805806
"conditions": conditions,

src/sentry/celery.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,13 @@ def holds_bad_pickle_object(value, memo=None):
3333
bad_object = holds_bad_pickle_object(item, memo)
3434
if bad_object is not None:
3535
return bad_object
36+
return
3637
elif isinstance(value, dict):
3738
for item in value.values():
3839
bad_object = holds_bad_pickle_object(item, memo)
3940
if bad_object is not None:
4041
return bad_object
41-
42+
return
4243
if isinstance(value, models.Model):
4344
return (
4445
value,
@@ -56,7 +57,7 @@ def holds_bad_pickle_object(value, memo=None):
5657
return None
5758
elif value is None:
5859
return None
59-
elif not isinstance(value, (dict, list, str, float, int, bool, tuple, frozenset)):
60+
elif not isinstance(value, (str, float, int, bool)):
6061
return value, "do not pickle stdlib classes"
6162
return None
6263

@@ -69,9 +70,9 @@ def good_use_of_pickle_or_bad_use_of_pickle(task, args, kwargs):
6970
if bad is not None:
7071
bad_object, reason = bad
7172
raise TypeError(
72-
"Task %r was invoked with an object that we do not want "
73+
"Task %s was invoked with an object that we do not want "
7374
"to pass via pickle (%r, reason is %s) in argument %s"
74-
% (task, bad_object, reason, name)
75+
% (task.name, bad_object, reason, name)
7576
)
7677

7778

src/sentry/features/temporary.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,8 @@ def register_temporary_features(manager: FeatureManager):
273273
manager.add("organizations:profiling-beta", OrganizationFeature, FeatureHandlerStrategy.INTERNAL, api_expose=True)
274274
# Enables monitoring for latest profiling sdk used
275275
manager.add("organizations:profiling-sdks", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
276+
# Enables dropping of deprecated profiling sdks used
277+
manager.add("organizations:profiling-deprecate-sdks", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
276278
# Enables production profiling in sentry browser application
277279
manager.add("organizations:profiling-browser", OrganizationFeature, FeatureHandlerStrategy.INTERNAL, api_expose=False)
278280
# Enables separate differential flamegraph page

src/sentry/grouping/api.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
DefaultGroupingComponent,
1717
SystemGroupingComponent,
1818
)
19-
from sentry.grouping.enhancer import LATEST_VERSION, Enhancements, get_enhancements_version
19+
from sentry.grouping.enhancer import Enhancements, get_enhancements_version
2020
from sentry.grouping.enhancer.exceptions import InvalidEnhancerConfig
2121
from sentry.grouping.strategies.base import DEFAULT_GROUPING_ENHANCEMENTS_BASE, GroupingContext
2222
from sentry.grouping.strategies.configurations import CONFIGURATIONS
@@ -93,14 +93,15 @@ def _get_enhancements(self, project: Project) -> str:
9393

9494
config_id = self._get_config_id(project)
9595
enhancements_base = CONFIGURATIONS[config_id].enhancements_base
96+
enhancements_version = get_enhancements_version(project, config_id)
9697

9798
# Instead of parsing and dumping out config here, we can make a
9899
# shortcut
99100
from sentry.utils.cache import cache
100101
from sentry.utils.hashlib import md5_text
101102

102103
cache_prefix = self.cache_prefix
103-
cache_prefix += f"{LATEST_VERSION}:"
104+
cache_prefix += f"{enhancements_version}:"
104105
cache_key = (
105106
cache_prefix
106107
+ md5_text(
@@ -124,7 +125,7 @@ def _get_enhancements(self, project: Project) -> str:
124125
enhancements = Enhancements.from_rules_text(
125126
enhancements_string,
126127
bases=[enhancements_base] if enhancements_base else [],
127-
version=get_enhancements_version(project, config_id),
128+
version=enhancements_version,
128129
referrer="project_rules",
129130
).base64_string
130131
except InvalidEnhancerConfig:

src/sentry/grouping/strategies/base.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
FrameGroupingComponent,
1212
StacktraceGroupingComponent,
1313
)
14-
from sentry.grouping.enhancer import Enhancements
14+
from sentry.grouping.enhancer import ENHANCEMENT_BASES, Enhancements
15+
from sentry.grouping.enhancer.exceptions import InvalidEnhancerConfig
1516
from sentry.interfaces.base import Interface
1617
from sentry.interfaces.exception import SingleException
1718
from sentry.interfaces.stacktrace import Frame, Stacktrace
@@ -305,9 +306,18 @@ def __init__(self, enhancements: str | None = None, **extra: Any):
305306
if enhancements is None:
306307
enhancements_instance = Enhancements.from_rules_text("", referrer="strategy_config")
307308
else:
308-
enhancements_instance = Enhancements.from_base64_string(
309-
enhancements, referrer="strategy_config"
310-
)
309+
# If the enhancements string has been loaded from an existing event, it may be from an
310+
# obsolete enhancements version, in which case we just use the default enhancements for
311+
# this grouping config
312+
try:
313+
enhancements_instance = Enhancements.from_base64_string(
314+
enhancements, referrer="strategy_config"
315+
)
316+
except InvalidEnhancerConfig:
317+
enhancements_instance = ENHANCEMENT_BASES[
318+
self.enhancements_base or DEFAULT_GROUPING_ENHANCEMENTS_BASE
319+
]
320+
311321
self.enhancements = enhancements_instance
312322

313323
def __repr__(self) -> str:

src/sentry/incidents/logic.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,7 @@ def get_metric_issue_aggregates(
415415
"incidents.get_incident_aggregates.snql.query.error",
416416
tags={
417417
"dataset": params.snuba_query.dataset,
418-
"entity": EntityKey.EAPSpans.value,
418+
"entity": EntityKey.EAPItemsSpan.value,
419419
},
420420
)
421421
raise

src/sentry/incidents/subscription_processor.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -312,14 +312,6 @@ def get_comparison_aggregation_value(
312312

313313
if not comparison_aggregate:
314314
metrics.incr("incidents.alert_rules.skipping_update_comparison_value_invalid")
315-
logger.info(
316-
"No comparison aggregate",
317-
extra={
318-
"alert_rule_id": self.alert_rule.id,
319-
"subscription_id": subscription_update.get("subscription_id"),
320-
"organization_id": self.alert_rule.organization_id,
321-
},
322-
)
323315
return None
324316

325317
return (aggregation_value / comparison_aggregate) * 100

src/sentry/integrations/slack/tasks/find_channel_id_for_rule.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,20 +32,23 @@
3232
),
3333
)
3434
def find_channel_id_for_rule(
35-
project: Project,
35+
project: Project | None,
3636
actions: Sequence[dict[str, Any]],
3737
uuid: str,
3838
rule_id: int | None = None,
3939
user_id: int | None = None,
40+
project_id: int | None = None,
4041
**kwargs: Any,
4142
) -> None:
4243
redis_rule_status = RedisRuleStatus(uuid)
4344

44-
try:
45-
project = Project.objects.get(id=project.id)
46-
except Project.DoesNotExist:
47-
redis_rule_status.set_value("failed")
48-
return
45+
if not project and project_id:
46+
try:
47+
project = Project.objects.get_from_cache(id=project_id)
48+
except Project.DoesNotExist:
49+
redis_rule_status.set_value("failed")
50+
return
51+
assert project, "find_channel_id_for_rule requires a project or project_id"
4952

5053
organization = project.organization
5154
integration_id: int | None = None

src/sentry/models/projectsdk.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
from collections.abc import Sequence
55
from enum import Enum
66

7+
import sentry_sdk
78
from django.db import models
89
from packaging.version import InvalidVersion, Version
910
from packaging.version import parse as parse_version
1011

12+
from sentry import options
1113
from sentry.backup.scopes import RelocationScope
1214
from sentry.db.models import BoundedIntegerField, FlexibleForeignKey, region_silo_model, sane_repr
1315
from sentry.db.models.base import DefaultFieldsModel
@@ -170,3 +172,33 @@ def normalize_sdk_name(sdk_name: str) -> str | None:
170172
return sdk_name
171173

172174
return None
175+
176+
177+
MINIMUM_SDK_VERSION_OPTIONS: dict[tuple[int, str], str] = {
178+
(EventType.PROFILE_CHUNK.value, "sentry.cocoa"): "sdk-deprecation.profile-chunk.cocoa",
179+
(EventType.PROFILE_CHUNK.value, "sentry.python"): "sdk-deprecation.profile-chunk.python",
180+
}
181+
182+
183+
def get_minimum_sdk_version(event_type: int, sdk_name: str, hard_limit: bool) -> Version | None:
184+
parts = sdk_name.split(".", 2)
185+
if len(parts) < 2:
186+
return None
187+
188+
normalized_sdk_name = ".".join(parts[:2])
189+
190+
sdk_version_option = MINIMUM_SDK_VERSION_OPTIONS.get((event_type, normalized_sdk_name))
191+
if sdk_version_option is None:
192+
return None
193+
194+
if hard_limit:
195+
sdk_version = options.get(f"{sdk_version_option}.hard")
196+
else:
197+
sdk_version = options.get(sdk_version_option)
198+
199+
if sdk_version:
200+
try:
201+
return parse_version(sdk_version)
202+
except InvalidVersion as e:
203+
sentry_sdk.capture_exception(e)
204+
return None

src/sentry/monitors/consumers/monitor_consumer.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -828,13 +828,17 @@ def _process_checkin(item: CheckinItem, txn: Transaction | Span) -> None:
828828
# to UTC
829829
clock_time = item.ts.replace(tzinfo=UTC)
830830

831+
# Record the reported in_progress time when the check is in progress
832+
date_in_progress = start_time if status == CheckInStatus.IN_PROGRESS else None
833+
831834
check_in, created = MonitorCheckIn.objects.get_or_create(
832835
defaults={
833836
"duration": duration,
834837
"status": status,
835838
"date_added": start_time,
836839
"date_updated": start_time,
837840
"date_clock": clock_time,
841+
"date_in_progress": date_in_progress,
838842
"expected_time": expected_time,
839843
"timeout_at": timeout_at,
840844
"monitor_config": monitor_config,
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Generated by Django 5.2.1 on 2025-05-15 16:51
2+
3+
from django.db import migrations, models
4+
from django.db.migrations import SeparateDatabaseAndState
5+
6+
from sentry.new_migrations.migrations import CheckedMigration
7+
8+
9+
class Migration(CheckedMigration):
10+
# This flag is used to mark that a migration shouldn't be automatically run in production.
11+
# This should only be used for operations where it's safe to run the migration after your
12+
# code has deployed. So this should not be used for most operations that alter the schema
13+
# of a table.
14+
# Here are some things that make sense to mark as post deployment:
15+
# - Large data migrations. Typically we want these to be run manually so that they can be
16+
# monitored and not block the deploy for a long period of time while they run.
17+
# - Adding indexes to large tables. Since this can take a long time, we'd generally prefer to
18+
# run this outside deployments so that we don't block them. Note that while adding an index
19+
# is a schema change, it's completely safe to run the operation after the code has deployed.
20+
# Once deployed, run these manually via: https://develop.sentry.dev/database-migrations/#migration-deployment
21+
22+
is_post_deployment = False
23+
24+
dependencies = [
25+
("monitors", "0004_record_date_in_progress_sql_only"),
26+
]
27+
28+
operations = [
29+
SeparateDatabaseAndState(
30+
database_operations=[],
31+
state_operations=[
32+
migrations.AddField(
33+
model_name="monitorcheckin",
34+
name="date_in_progress",
35+
field=models.DateTimeField(null=True),
36+
)
37+
],
38+
)
39+
]

src/sentry/monitors/models.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,12 @@ class MonitorCheckIn(Model):
495495
as detecting misses)
496496
"""
497497

498+
date_in_progress = models.DateTimeField(null=True)
499+
"""
500+
Records the time when the first in_progress check-in was received by relay.
501+
If no in_progress check-in was ever sent this will remain null.
502+
"""
503+
498504
expected_time = models.DateTimeField(null=True)
499505
"""
500506
Holds the exact time we expected to receive this check-in

src/sentry/monitors/serializers.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ class MonitorCheckInSerializerResponse(MonitorCheckInSerializerResponseOptional)
278278
dateCreated: datetime
279279
dateAdded: datetime
280280
dateUpdated: datetime
281+
dateInProgress: datetime | None
281282
dateClock: datetime
282283
expectedTime: datetime
283284
monitorConfig: MonitorConfigSerializerResponse
@@ -348,6 +349,7 @@ def serialize(self, obj, attrs, user, **kwargs) -> MonitorCheckInSerializerRespo
348349
"dateAdded": obj.date_added,
349350
"dateUpdated": obj.date_updated,
350351
"dateClock": obj.date_clock,
352+
"dateInProgress": obj.date_in_progress,
351353
"expectedTime": obj.expected_time,
352354
"monitorConfig": cast(MonitorConfigSerializerResponse, config),
353355
}

0 commit comments

Comments
 (0)