Skip to content

Commit 173d79f

Browse files
authored
Merge branch 'master' into roggenkemper/feat/budget-paced-detection-scheduling
2 parents 57ad04a + 99884e6 commit 173d79f

File tree

85 files changed

+1138
-526
lines changed

Some content is hidden

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

85 files changed

+1138
-526
lines changed

.github/file-filters.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
sentry_frontend_workflow_file: &sentry_frontend_workflow_file
44
- added|modified: '.github/workflows/frontend.yml'
55

6+
sentry_frontend_snapshots_workflow_file: &sentry_frontend_snapshots_workflow_file
7+
- added|modified: '.github/workflows/frontend-snapshots.yml'
8+
69
# Provides list of changed files to test (jest)
710
# getsentry/sentry does not use the list directly, instead we shard tests inside jest.config.js
811
testable_modified: &testable_modified
@@ -30,6 +33,7 @@ typecheckable_rules_changed: &typecheckable_rules_changed
3033
# Trigger to apply the 'Scope: Frontend' label to PRs
3134
frontend_all: &frontend_all
3235
- *sentry_frontend_workflow_file
36+
- *sentry_frontend_snapshots_workflow_file
3337
- added|modified: '**/*.{ts,tsx,js,jsx,mjs}'
3438
- added|modified: 'static/**/*.{less,json,yml,md,mdx}'
3539
- added|modified: '{vercel,tsconfig,biome,package}.json'

.github/workflows/frontend-snapshots.yml

Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -69,37 +69,19 @@ jobs:
6969

7070
- name: Install sentry-cli
7171
if: ${{ !cancelled() }}
72-
run: curl -sL https://sentry.io/get-cli/ | SENTRY_CLI_VERSION=3.3.4 sh
72+
run: curl -sL https://sentry.io/get-cli/ | SENTRY_CLI_VERSION=3.3.5 sh
7373

7474
- name: Upload snapshots
7575
id: upload
7676
if: ${{ !cancelled() }}
7777
env:
7878
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_SNAPSHOTS_AUTH_TOKEN }}
79-
run: |
80-
ARGS=(
81-
--log-level=debug
82-
--auth-token "${{ secrets.SENTRY_SNAPSHOTS_AUTH_TOKEN }}"
83-
build snapshots "${{ env.SNAPSHOT_OUTPUT_DIR }}"
84-
--app-id sentry-frontend
85-
--project sentry-frontend
86-
--head-sha "${{ github.event.pull_request.head.sha || github.sha }}"
87-
--vcs-provider github
88-
--head-repo-name "${{ github.repository }}"
89-
)
90-
91-
# PR-only flags: base-sha, base-ref, base-repo-name, head-ref, pr-number
92-
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
93-
ARGS+=(
94-
--base-sha "${{ github.event.pull_request.base.sha }}"
95-
--base-repo-name "${{ github.repository }}"
96-
--head-ref "${{ github.head_ref }}"
97-
--base-ref "${{ github.base_ref }}"
98-
--pr-number "${{ github.event.number }}"
99-
)
100-
fi
101-
102-
sentry-cli "${ARGS[@]}"
79+
run: >
80+
sentry-cli
81+
--log-level=debug
82+
build snapshots "${{ env.SNAPSHOT_OUTPUT_DIR }}"
83+
--app-id sentry-frontend
84+
--project sentry-frontend
10385
10486
- name: Report upload failure to Sentry
10587
if: ${{ failure() && steps.upload.outcome == 'failure' }}

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/serializers/models/organization.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,13 @@ class BaseOrganizationSerializer(serializers.Serializer):
170170
max_length=DEFAULT_SLUG_MAX_LENGTH,
171171
)
172172

173+
def validate_name(self, value: str) -> str:
174+
if "://" in value:
175+
raise serializers.ValidationError(
176+
"Organization name cannot contain URL schemes (e.g. http:// or https://)."
177+
)
178+
return value
179+
173180
def validate_slug(self, value: str) -> str:
174181
# Historically, the only check just made sure there was more than 1
175182
# character for the slug, but since then, there are many slugs that

src/sentry/conf/server.py

Lines changed: 1 addition & 1 deletion
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",

src/sentry/features/temporary.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ def register_temporary_features(manager: FeatureManager) -> None:
5050
manager.add("organizations:alert-allow-indexed", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
5151
# Enables flag for org specific runs on alerts comparison script for spans migration
5252
manager.add("organizations:alerts-timeseries-comparison", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)
53+
# Enable AI-based issue detection for an organization (budget-paced scheduling)
54+
manager.add("organizations:ai-issue-detection", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)
5355
# Enable anomaly detection feature for EAP spans
5456
manager.add("organizations:anomaly-detection-eap", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
5557
# Enables the cron job to auto-enable codecov integrations.
@@ -104,8 +106,6 @@ def register_temporary_features(manager: FeatureManager) -> None:
104106
manager.add("organizations:intercom-support", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
105107
# Enable default anomaly detection metric monitor for new projects
106108
manager.add("organizations:default-anomaly-detector", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)
107-
# Enables synthesis of device.class in ingest
108-
manager.add("organizations:device-class-synthesis", OrganizationFeature, FeatureHandlerStrategy.INTERNAL, api_expose=False)
109109
# Enable the 'discover' interface. (might be unused)
110110
manager.add("organizations:discover", OrganizationFeature, FeatureHandlerStrategy.INTERNAL, api_expose=True)
111111
# Enable the discover saved queries deprecation warnings

src/sentry/incidents/endpoints/serializers/workflow_engine_incident.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,18 @@ def get_attrs(
9393
results[open_period] = {"projects": [open_period.project.slug]}
9494
results[open_period]["alert_rule"] = alert_rules.get(str(alert_rule_id))
9595

96+
igops = IncidentGroupOpenPeriod.objects.filter(group_open_period__in=results.keys())
97+
igop_by_open_period_id = {igop.group_open_period_id: igop for igop in igops}
98+
99+
for open_period in results:
100+
if igop := igop_by_open_period_id.get(open_period.id):
101+
results[open_period]["incident_id"] = igop.incident_id
102+
results[open_period]["incident_identifier"] = igop.incident_identifier
103+
else:
104+
fake_id = get_fake_id_from_object_id(open_period.id)
105+
results[open_period]["incident_id"] = fake_id
106+
results[open_period]["incident_identifier"] = fake_id
107+
96108
if "activities" in self.expand:
97109
gopas = list(
98110
GroupOpenPeriodActivity.objects.filter(group_open_period__in=item_list).order_by(
@@ -169,13 +181,8 @@ def serialize(
169181
"""
170182
Temporary serializer to take a GroupOpenPeriod and serialize it for the old incident endpoint
171183
"""
172-
try:
173-
igop = IncidentGroupOpenPeriod.objects.get(group_open_period=obj)
174-
incident_id = igop.incident_id
175-
incident_identifier = igop.incident_identifier
176-
except IncidentGroupOpenPeriod.DoesNotExist:
177-
incident_id = get_fake_id_from_object_id(obj.id)
178-
incident_identifier = incident_id
184+
incident_id = attrs["incident_id"]
185+
incident_identifier = attrs["incident_identifier"]
179186

180187
date_closed = obj.date_ended.replace(second=0, microsecond=0) if obj.date_ended else None
181188
return {

src/sentry/models/organizationmember.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@ def get_invite_link(self, referrer: str | None = None):
348348

349349
def send_invite_email(self, referrer: str | None = None):
350350
from sentry.utils.email import MessageBuilder
351+
from sentry.utils.email.sanitize import sanitize_outbound_name
351352

352353
context = {
353354
"email": self.email,
@@ -356,7 +357,7 @@ def send_invite_email(self, referrer: str | None = None):
356357
}
357358

358359
msg = MessageBuilder(
359-
subject="Join %s in using Sentry" % self.organization.name,
360+
subject="Join %s in using Sentry" % sanitize_outbound_name(self.organization.name),
360361
template="sentry/emails/member-invite.txt",
361362
html_template="sentry/emails/member-invite.html",
362363
type="organization.invite",
@@ -371,6 +372,7 @@ def send_invite_email(self, referrer: str | None = None):
371372

372373
def send_sso_link_email(self, sending_user_email: str, provider):
373374
from sentry.utils.email import MessageBuilder
375+
from sentry.utils.email.sanitize import sanitize_outbound_name
374376

375377
link_args = {"organization_slug": self.organization.slug}
376378
context = {
@@ -381,7 +383,7 @@ def send_sso_link_email(self, sending_user_email: str, provider):
381383
}
382384

383385
msg = MessageBuilder(
384-
subject=f"Action Required for {self.organization.name}",
386+
subject=f"Action Required for {sanitize_outbound_name(self.organization.name)}",
385387
template="sentry/emails/auth-link-identity.txt",
386388
html_template="sentry/emails/auth-link-identity.html",
387389
type="organization.auth_link",
@@ -392,6 +394,7 @@ def send_sso_link_email(self, sending_user_email: str, provider):
392394
def send_sso_unlink_email(self, disabling_user: RpcUser | str, provider):
393395
from sentry.users.services.lost_password_hash import lost_password_hash_service
394396
from sentry.utils.email import MessageBuilder
397+
from sentry.utils.email.sanitize import sanitize_outbound_name
395398

396399
# Nothing to send if this member isn't associated to a user
397400
if not self.user_id:
@@ -424,7 +427,7 @@ def send_sso_unlink_email(self, disabling_user: RpcUser | str, provider):
424427
context["set_password_url"] = password_hash.get_absolute_url(mode="set_password")
425428

426429
msg = MessageBuilder(
427-
subject=f"Action Required for {self.organization.name}",
430+
subject=f"Action Required for {sanitize_outbound_name(self.organization.name)}",
428431
template="sentry/emails/auth-sso-disabled.txt",
429432
html_template="sentry/emails/auth-sso-disabled.html",
430433
type="organization.auth_sso_disabled",

src/sentry/notifications/notifications/organization_request/abstract_invite_request.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from sentry.notifications.utils.actions import MessageAction
1717
from sentry.types.actor import Actor
1818
from sentry.users.services.user.service import user_service
19+
from sentry.utils.email.sanitize import sanitize_outbound_name
1920

2021
if TYPE_CHECKING:
2122
from sentry.users.models.user import User
@@ -40,7 +41,7 @@ def members_url(self) -> str:
4041
)
4142

4243
def get_subject(self, context: Mapping[str, Any] | None = None) -> str:
43-
return f"Access request to {self.organization.name}"
44+
return f"Access request to {sanitize_outbound_name(self.organization.name)}"
4445

4546
def get_recipient_context(
4647
self, recipient: Actor, extra_context: Mapping[str, Any]

src/sentry/notifications/notifications/organization_request/integration_request.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
)
1212
from sentry.notifications.utils.actions import MessageAction
1313
from sentry.types.actor import Actor
14+
from sentry.utils.email.sanitize import sanitize_outbound_name
1415

1516
if TYPE_CHECKING:
1617
from sentry.models.organization import Organization
@@ -71,7 +72,7 @@ def get_context(self) -> MutableMapping[str, Any]:
7172
}
7273

7374
def get_subject(self, context: Mapping[str, Any] | None = None) -> str:
74-
return f"Your team member requested the {self.provider_name} integration on Sentry"
75+
return f"Your team member requested the {sanitize_outbound_name(self.provider_name)} integration on Sentry"
7576

7677
def get_notification_title(
7778
self, provider: ExternalProviders, context: Mapping[str, Any] | None = None
@@ -82,11 +83,13 @@ def build_attachment_title(self, recipient: Actor) -> str:
8283
return "Request to Install"
8384

8485
def get_message_description(self, recipient: Actor, provider: ExternalProviders) -> str:
85-
requester_name = self.requester.get_display_name()
86+
requester_name = sanitize_outbound_name(self.requester.get_display_name())
8687
optional_message = (
87-
f" They've included this message `{self.message}`" if self.message else ""
88+
f" They've included this message `{sanitize_outbound_name(self.message)}`"
89+
if self.message
90+
else ""
8891
)
89-
return f"{requester_name} is requesting to install the {self.provider_name} integration into {self.organization.name}.{optional_message}"
92+
return f"{requester_name} is requesting to install the {sanitize_outbound_name(self.provider_name)} integration into {sanitize_outbound_name(self.organization.name)}.{optional_message}"
9093

9194
def get_message_actions(
9295
self, recipient: Actor, provider: ExternalProviders

0 commit comments

Comments
 (0)