Skip to content

Commit 5e54cbc

Browse files
authored
Merge branch 'master' into tkdodo/ref/dashboards-endpoint-to-apiOptions
2 parents b1cbc9c + 7900d40 commit 5e54cbc

File tree

138 files changed

+4229
-23703
lines changed

Some content is hidden

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

138 files changed

+4229
-23703
lines changed

CHANGES

Lines changed: 2286 additions & 0 deletions
Large diffs are not rendered by default.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ dependencies = [
9090
"sentry-protos>=0.8.12",
9191
"sentry-redis-tools>=0.5.0",
9292
"sentry-relay>=0.9.27",
93+
"sentry-scm>=0.1.7",
9394
"sentry-sdk[http2]>=2.47.0",
9495
"sentry-usage-accountant>=0.0.10",
9596
# remove once there are no unmarked transitive dependencies on setuptools

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[metadata]
22
name = sentry
3-
version = 26.4.0.dev0
3+
version = 26.5.0.dev0
44
description = A realtime logging and aggregation server.
55
long_description = file: README.md
66
long_description_content_type = text/markdown

src/sentry/api/endpoints/internal/feature_flags.py

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from collections.abc import Mapping
2+
13
from django.conf import settings
24
from rest_framework.request import Request
35
from rest_framework.response import Response
@@ -10,6 +12,30 @@
1012
from sentry.runner.settings import configure, discover_configs
1113

1214

15+
def _coerce_early_feature_flag_value(value: object) -> bool | None:
16+
"""
17+
Map API input to a bool for SENTRY_FEATURES. Accepts JSON booleans, 0/1
18+
(common in scripts), and the strings "true"/"false" (case-insensitive).
19+
Returns None if the value cannot be interpreted safely.
20+
"""
21+
if isinstance(value, bool):
22+
return value
23+
if isinstance(value, int):
24+
if value == 1:
25+
return True
26+
if value == 0:
27+
return False
28+
return None
29+
if isinstance(value, str):
30+
lowered = value.lower()
31+
if lowered == "true":
32+
return True
33+
if lowered == "false":
34+
return False
35+
return None
36+
return None
37+
38+
1339
@all_silo_endpoint
1440
class InternalFeatureFlagsEndpoint(Endpoint):
1541
permission_classes = (SuperuserPermission,)
@@ -36,18 +62,38 @@ def put(self, request: Request) -> Response:
3662
if not settings.SENTRY_SELF_HOSTED:
3763
return Response("You are not self-hosting Sentry.", status=403)
3864

39-
data = request.data.keys()
40-
valid_feature_flags = [flag for flag in data if SENTRY_EARLY_FEATURES.get(flag, False)]
65+
payload: object = request.data
66+
if not isinstance(payload, Mapping):
67+
return Response(
68+
{"detail": "Feature flag updates must be a JSON object."},
69+
status=400,
70+
)
71+
72+
valid_feature_flags = [flag for flag in payload if flag in SENTRY_EARLY_FEATURES]
73+
coerced_values: dict[str, bool] = {}
74+
for valid_flag in valid_feature_flags:
75+
coerced = _coerce_early_feature_flag_value(payload.get(valid_flag))
76+
if coerced is None:
77+
return Response(
78+
{
79+
"detail": (
80+
f'Feature flag "{valid_flag}" must be true or false '
81+
f"(boolean, 0 or 1, or the string true or false)."
82+
)
83+
},
84+
status=400,
85+
)
86+
coerced_values[valid_flag] = coerced
87+
4188
_, py, yml = discover_configs()
4289
# Open the file for reading and writing
4390
with open(py, "r+") as file:
4491
lines = file.readlines()
4592
# print(lines)
4693
for valid_flag in valid_feature_flags:
4794
match_found = False
48-
new_string = (
49-
f'\nSENTRY_FEATURES["{valid_flag}"]={request.data.get(valid_flag, False)}\n'
50-
)
95+
python_bool = "True" if coerced_values[valid_flag] else "False"
96+
new_string = f'\nSENTRY_FEATURES["{valid_flag}"]={python_bool}\n'
5197
# Search for the string match and update lines
5298
for i, line in enumerate(lines):
5399
if valid_flag in line:

src/sentry/api/endpoints/organization_spans_fields.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,14 @@
3636
from sentry.utils import snuba_rpc
3737

3838

39-
def as_tag_key(name: str, type: Literal["string", "number", "boolean"]):
40-
key, _, _ = translate_internal_to_public_alias(name, type, SupportedTraceItemType.SPANS)
39+
def as_tag_key(name: str, search_type: Literal["string", "number", "boolean"]):
40+
key, _, _ = translate_internal_to_public_alias(name, search_type, SupportedTraceItemType.SPANS)
4141

4242
if key is not None:
4343
name = key
44-
elif type == "number":
44+
elif search_type == "number":
4545
key = f"tags[{name},number]"
46-
elif type == "boolean":
46+
elif search_type == "boolean":
4747
key = f"tags[{name},boolean]"
4848
else:
4949
key = name

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,5 @@ def serialize(self, obj, attrs, user, **kwargs):
4747
"status": obj.status,
4848
"checksum": checksum,
4949
"fileName": file_name,
50+
"export_format": obj.export_format,
5051
}

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
from sentry.api.serializers import Serializer, register
1212
from sentry.constants import ObjectStatus
1313
from sentry.db.models.manager.base_query_set import BaseQuerySet
14+
from sentry.integrations.services.integration.model import RpcIntegration
15+
from sentry.integrations.services.integration.service import integration_service
1416
from sentry.models.environment import Environment
1517
from sentry.models.project import Project
1618
from sentry.models.rule import NeglectedRule, Rule, RuleActivity, RuleActivityType
@@ -591,6 +593,22 @@ def get_attrs(self, item_list: Sequence[Workflow], user, **kwargs):
591593
)
592594
actions_by_dcg = self._fetch_actions_by_dcg(all_dcg_ids)
593595

596+
# Batch-fetch integrations for all actions to avoid per-action RPC calls in render_label
597+
all_integration_ids: set[int] = set()
598+
for action_list in actions_by_dcg.values():
599+
for action in action_list:
600+
if action.integration_id is not None:
601+
all_integration_ids.add(action.integration_id)
602+
603+
integration_cache: dict[int, RpcIntegration] = {}
604+
if all_integration_ids and item_list:
605+
integrations = integration_service.get_integrations(
606+
integration_ids=list(all_integration_ids),
607+
organization_id=item_list[0].organization_id,
608+
status=ObjectStatus.ACTIVE,
609+
)
610+
integration_cache = {i.id: i for i in integrations}
611+
594612
last_triggered_lookup: dict[int, datetime] = {}
595613
if "lastTriggered" in self.expand:
596614
last_triggered_lookup = self._fetch_workflow_last_triggered(item_list)
@@ -667,7 +685,7 @@ def get_attrs(self, item_list: Sequence[Workflow], user, **kwargs):
667685
continue
668686

669687
action_data["name"] = action_to_handler[action].render_label(
670-
workflow.organization_id, action_data
688+
workflow.organization_id, action_data, integration_cache=integration_cache
671689
)
672690
installation_uuid = action_data.get("sentryAppInstallationUuid")
673691
install_context = None

src/sentry/api/urls.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3617,7 +3617,7 @@ def create_group_urls(name_prefix: str) -> list[URLPattern | URLResolver]:
36173617
name="sentry-api-0-rpc-service",
36183618
),
36193619
re_path(
3620-
r"^scm-rpc/(?P<method_name>\w+)/$",
3620+
r"^scm-rpc/$",
36213621
ScmRpcServiceEndpoint.as_view(),
36223622
name="sentry-api-0-scm-rpc-service",
36233623
),

src/sentry/conf/server.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1216,7 +1216,7 @@ def SOCIAL_AUTH_DEFAULT_USERNAME() -> str:
12161216
},
12171217
"llm-issue-detection": {
12181218
"task": "issues:sentry.tasks.llm_issue_detection.run_llm_issue_detection",
1219-
"schedule": crontab("0", "*", "*", "*", "*"),
1219+
"schedule": crontab("*/15", "*", "*", "*", "*"),
12201220
},
12211221
"preprod-detect-expired-artifacts": {
12221222
"task": "preprod:sentry.preprod.tasks.detect_expired_preprod_artifacts",
@@ -2240,7 +2240,7 @@ def custom_parameter_sort(parameter: dict) -> tuple[str, int]:
22402240
SENTRY_SELF_HOSTED_ERRORS_ONLY = False
22412241
# only referenced in getsentry to provide the stable beacon version
22422242
# updated with scripts/bump-version.sh
2243-
SELF_HOSTED_STABLE_VERSION = "26.3.1"
2243+
SELF_HOSTED_STABLE_VERSION = "26.4.0"
22442244

22452245
# Whether we should look at X-Forwarded-For header or not
22462246
# when checking REMOTE_ADDR ip addresses

src/sentry/data_export/base.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,19 @@ class ExportQueryType:
2929
ISSUES_BY_TAG = 0
3030
DISCOVER = 1
3131
EXPLORE = 2
32+
TRACE_ITEM_FULL_EXPORT = 3
3233
ISSUES_BY_TAG_STR = "Issues-by-Tag"
3334
DISCOVER_STR = "Discover"
3435
EXPLORE_STR = "Explore"
36+
TRACE_ITEM_FULL_EXPORT_STR = "trace_item_full_export"
3537

3638
@classmethod
3739
def as_choices(cls) -> tuple[tuple[int, str], ...]:
3840
return (
3941
(cls.ISSUES_BY_TAG, str(cls.ISSUES_BY_TAG_STR)),
4042
(cls.DISCOVER, str(cls.DISCOVER_STR)),
4143
(cls.EXPLORE, str(cls.EXPLORE_STR)),
44+
(cls.TRACE_ITEM_FULL_EXPORT, str(cls.TRACE_ITEM_FULL_EXPORT_STR)),
4245
)
4346

4447
@classmethod
@@ -47,6 +50,7 @@ def as_str_choices(cls) -> tuple[tuple[str, str], ...]:
4750
(cls.ISSUES_BY_TAG_STR, cls.ISSUES_BY_TAG_STR),
4851
(cls.DISCOVER_STR, cls.DISCOVER_STR),
4952
(cls.EXPLORE_STR, cls.EXPLORE_STR),
53+
(cls.TRACE_ITEM_FULL_EXPORT_STR, cls.TRACE_ITEM_FULL_EXPORT_STR),
5054
)
5155

5256
@classmethod
@@ -57,6 +61,8 @@ def as_str(cls, integer: int) -> str:
5761
return cls.DISCOVER_STR
5862
elif integer == cls.EXPLORE:
5963
return cls.EXPLORE_STR
64+
elif integer == cls.TRACE_ITEM_FULL_EXPORT:
65+
return cls.TRACE_ITEM_FULL_EXPORT_STR
6066
raise ValueError(f"Invalid ExportQueryType: {integer}")
6167

6268
@classmethod
@@ -67,4 +73,6 @@ def from_str(cls, string: str) -> int:
6773
return cls.DISCOVER
6874
elif string == cls.EXPLORE_STR:
6975
return cls.EXPLORE
76+
elif string == cls.TRACE_ITEM_FULL_EXPORT_STR:
77+
return cls.TRACE_ITEM_FULL_EXPORT
7078
raise ValueError(f"Invalid ExportQueryType: {string}")

0 commit comments

Comments
 (0)