Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
ac317d3
ref(tasks): Remove base64 encoding for bytes parameters in tasks (#11…
untitaker May 27, 2026
d2820f2
ref(issueDiff): Refactor event data fetching to use useQueries (#116042)
sentry[bot] May 27, 2026
e7a13d8
fix(code-mapping): update codeowners GET endpoint and tests (#116309)
giovanni-guidini May 27, 2026
62f4dc8
fix(api-logs): preserve snuba policy info on throttles (#116263)
cvxluo May 27, 2026
f4d567d
chore(issue-detection): Update badge for AI Issue Detection (#116311)
roggenkemper May 27, 2026
91739d1
release: 26.5.1
gricha May 27, 2026
b21513d
fix(pageFilters): Sort bookmarked projects above non-member projects …
JonasBa May 27, 2026
2fd64c2
feat(utils): Add shuffle option to CursoredScheduler (#116297)
roggenkemper May 27, 2026
87258e4
Merge branch 'releases/26.5.1'
sentry-release-bot[bot] May 27, 2026
c9c4615
meta: Bump new development version
sentry-release-bot[bot] May 27, 2026
8f5cbe9
feat(preprod): Add structured tags to snapshot test metadata (#116307)
mtopo27 May 27, 2026
53b64b5
feat(flagpole): Register onboarding-scm-project-creation-experiment f…
jaydgoss May 27, 2026
7e849ef
fix(explore): y-axis formatting decimal truncation for heatmaps (#116…
nikkikapadia May 27, 2026
0a64492
ref(utils): Small `SafeRolloutComparator` refactors (#116257)
lobsterkatie May 27, 2026
e212fbf
ref(onboarding): Decouple SCM step components from OnboardingContext …
jaydgoss May 27, 2026
6b8802f
feat(github-enterprise): Route install through API pipeline modal (#1…
evanpurkhiser May 27, 2026
ec77624
fix(traces): Downgrade Group.DoesNotExist log to info in trace serial…
wedamija May 27, 2026
0e2f01c
ref(flags): Remove organizations:insights-alerts (#116223)
wedamija May 27, 2026
c302e7b
fix(trace-item-details): allow timestamp (#116321)
wmak May 27, 2026
05073c1
fix(preprod): Pre-filter latest base snapshot query by project access…
NicoHinderling May 27, 2026
c828438
feat(issues): Consolidate user feedback activity styles (#116318)
scttcper May 27, 2026
4142da1
feat(issues): Use shared markdown component for activity notes (#116300)
scttcper May 27, 2026
ca6aa27
fix(oauth): Use hashed token lookup and reject tokens for inactive us…
michelletran-sentry May 27, 2026
d022578
fix(metrics): Skip tag validation when deleting Snuba subscriptions …
wedamija May 27, 2026
60f40a7
feat(bitbucket-server): Add API-driven pipeline backend for Bitbucket…
evanpurkhiser May 27, 2026
a61282c
ref(seer-grouping): rm backfill url (#116253)
kddubey May 27, 2026
c326bf7
fix(integrations): Use paginated jira projects endpoint, behind flag …
hobzcalvin May 27, 2026
2e78c41
feat(bitbucket-server): Add frontend pipeline steps for Bitbucket Ser…
evanpurkhiser May 27, 2026
eb6bcc9
ref: Delete plan migration frontend (#116331)
noahsmartin May 27, 2026
ceb6ae8
feat(cells): Remove cross-org feature gating from quota notifications…
lynnagara May 27, 2026
47c9a60
feat(scm): add github_enterprise support to SCM Platform RPC dispatch…
tnt-sentry May 27, 2026
aeba674
ref(alerts): Disable alert buttons for users without write access (#1…
malwilley May 27, 2026
fc813b0
chore(workflow-engine): Build out new registry for activities (#116200)
leeandher May 27, 2026
396d2ab
fix(autofix): Set default stopping point based on preferences (#116340)
Zylphrex May 27, 2026
4a2fb82
feat: install sentry-options (#115835)
joshuarli May 27, 2026
dc79a1d
fix(options): Suppress option seen logs in debug mode (#116324)
JoshFerge May 27, 2026
38e9856
fix(api-logs): log snuba throttle_threshold on rate-limited requests …
cvxluo May 27, 2026
f5603c7
fix(workflows): Update Workflows with org-scoped envs when transfered…
kcons May 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
734 changes: 734 additions & 0 deletions CHANGES

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ dependencies = [
"sentry-forked-email-reply-parser>=0.5.12.post1",
"sentry-kafka-schemas>=2.1.27",
"sentry-ophio>=1.1.3",
# sentry-options is only used in getsentry for now
"sentry-options>=1.0.13",
"sentry-protos>=0.13.0",
"sentry-redis-tools>=0.5.0",
"sentry-relay>=0.9.27",
Expand Down Expand Up @@ -1788,7 +1790,6 @@ module = [
"tests.sentry.api.endpoints.test_organization_auth_providers",
"tests.sentry.api.endpoints.test_organization_auth_token_details",
"tests.sentry.api.endpoints.test_organization_auth_tokens",
"tests.sentry.api.endpoints.test_organization_code_mapping_codeowners",
"tests.sentry.api.endpoints.test_organization_config_integrations",
"tests.sentry.api.endpoints.test_organization_events_trends_v2",
"tests.sentry.api.endpoints.test_organization_fork",
Expand Down
26 changes: 24 additions & 2 deletions src/sentry/api/endpoints/oauth_userinfo.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import hashlib

from rest_framework import status
from rest_framework.authentication import get_authorization_header
from rest_framework.exceptions import APIException
Expand Down Expand Up @@ -80,14 +82,34 @@ def get(self, request: Request) -> Response:
raise BearerTokenMissing()

access_token = auth_header[1].decode("utf-8")
hashed_token = hashlib.sha256(access_token.encode()).hexdigest()
try:
token_details = ApiToken.objects.get(token=access_token)
token_details = ApiToken.objects.select_related("user", "application").get(
hashed_token=hashed_token
)
except ApiToken.DoesNotExist:
raise BearerTokenInvalid()
try:
token_details = ApiToken.objects.select_related("user", "application").get(
token=access_token
)
except ApiToken.DoesNotExist:
raise BearerTokenInvalid()
else:
token_details.hashed_token = hashed_token
token_details.save(update_fields=["hashed_token"])

if token_details.is_expired():
raise BearerTokenInvalid()

if not token_details.user.is_active:
raise BearerTokenInvalid()

if getattr(token_details.user, "is_suspended", False):
raise BearerTokenInvalid()

if token_details.application is not None and not token_details.application.is_active:
raise BearerTokenInvalid()

scopes = token_details.get_scopes()
if "openid" not in scopes:
raise BearerTokenInsufficientScope()
Expand Down
43 changes: 36 additions & 7 deletions src/sentry/api/endpoints/project_trace_item_details.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import time
import uuid
from datetime import timedelta
from typing import Any, Literal

import sentry_sdk
Expand All @@ -17,8 +18,10 @@
from sentry.api.base import cell_silo_endpoint
from sentry.api.bases.project import ProjectEndpoint
from sentry.api.exceptions import BadRequest
from sentry.api.utils import get_date_range_from_params
from sentry.auth.staff import is_active_staff
from sentry.auth.superuser import is_active_superuser
from sentry.exceptions import InvalidParams
from sentry.models.project import Project
from sentry.search.eap import constants
from sentry.search.eap.types import (
Expand All @@ -36,8 +39,10 @@
translate_search_type_for_internal_column,
translate_to_sentry_conventions,
)
from sentry.search.utils import InvalidQuery, parse_datetime_string
from sentry.snuba.referrer import Referrer
from sentry.utils import json, snuba_rpc
from sentry.utils import json
from sentry.utils.snuba_rpc import trace_item_details_rpc

_NUMERIC_COERCIONS: dict[str, type] = {"valFloat": float, "valDouble": float}
_VAL_TYPE_TO_COLUMN_TYPE: dict[str, ColumnType] = {
Expand Down Expand Up @@ -362,9 +367,31 @@ def get(request: Request, project: Project, item_id: str) -> Response:
if not serializer.is_valid():
return Response(serializer.errors, status=400)

try:
start, end = get_date_range_from_params(request.GET, optional=True)
except InvalidParams:
return Response("date range parameters invalid", status=400)
if "timestamp" in request.GET:
try:
example_timestamp = parse_datetime_string(request.GET["timestamp"])
except InvalidQuery:
return Response("timestamp parameter invalid", status=400)
time_buffer = 1.5
example_start = example_timestamp - timedelta(days=time_buffer)
example_end = example_timestamp + timedelta(days=time_buffer)
if start is not None:
start = max(start, example_start)
else:
start = example_start
if end is not None:
end = min(end, example_end)
else:
end = example_end

serialized = serializer.validated_data
trace_id = serialized.get("trace_id")
item_type = serialized.get("item_type")
sentry_sdk.set_tag("trace_item_details.item_type", item_type)
referrer = serialized.get("referrer", Referrer.API_ORGANIZATION_TRACE_ITEM_DETAILS.value)

trace_item_type = None
Expand All @@ -377,12 +404,14 @@ def get(request: Request, project: Project, item_id: str) -> Response:
raise BadRequest(detail=f"Unknown trace item type: {item_type}")

start_timestamp_proto = ProtoTimestamp()
start_timestamp_proto.FromSeconds(0)

end_timestamp_proto = ProtoTimestamp()

# due to clock drift, the end time can be in the future - add a week to be safe
end_timestamp_proto.FromSeconds(int(time.time()) + 60 * 60 * 24 * 7)
if start is not None and end is not None:
start_timestamp_proto.FromDatetime(start)
end_timestamp_proto.FromDatetime(end)
else:
start_timestamp_proto.FromSeconds(0)
# due to clock drift, the end time can be in the future - add a week to be safe
end_timestamp_proto.FromSeconds(int(time.time()) + 60 * 60 * 24 * 7)

trace_id = request.GET.get("trace_id")
if not trace_id:
Expand All @@ -403,7 +432,7 @@ def get(request: Request, project: Project, item_id: str) -> Response:
trace_id=trace_id,
)

resp = MessageToDict(snuba_rpc.trace_item_details_rpc(req))
resp = MessageToDict(trace_item_details_rpc(req))

use_sentry_conventions = features.has(
"organizations:performance-sentry-conventions-fields",
Expand Down
1 change: 1 addition & 0 deletions src/sentry/api/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def custom_exception_handler(exc, context):
storage_key=exc.storage_key,
quota_used=exc.quota_used,
rejection_threshold=exc.rejection_threshold,
throttle_threshold=exc.throttle_threshold,
)

# capture the rate limited exception so we can see it in Sentry
Expand Down
4 changes: 1 addition & 3 deletions src/sentry/conf/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -2232,7 +2232,7 @@ def custom_parameter_sort(parameter: dict) -> tuple[str, int]:
SENTRY_SELF_HOSTED_ERRORS_ONLY = False
# only referenced in getsentry to provide the stable beacon version
# updated with scripts/bump-version.sh
SELF_HOSTED_STABLE_VERSION = "26.5.0"
SELF_HOSTED_STABLE_VERSION = "26.5.1"

# Whether we should look at X-Forwarded-For header or not
# when checking REMOTE_ADDR ip addresses
Expand Down Expand Up @@ -2910,8 +2910,6 @@ def custom_parameter_sort(parameter: dict) -> tuple[str, int]:

SEER_GROUPING_URL = SEER_DEFAULT_URL # for local development, these share a URL

SEER_GROUPING_BACKFILL_URL = SEER_DEFAULT_URL

SEER_SCORING_URL = SEER_DEFAULT_URL # for local development, these share a URL

SEER_ANOMALY_DETECTION_MODEL_VERSION = "v1"
Expand Down
36 changes: 20 additions & 16 deletions src/sentry/data_export/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,11 @@ def _sentry_metric_attrs(
return attrs


def _page_token_b64_from_processor(
def _page_token_from_processor(
processor: IssuesByTagProcessor | DiscoverProcessor | ExploreProcessor,
) -> str | None:
) -> bytes | None:
if isinstance(processor, TraceItemFullExportProcessor) and processor.page_token is not None:
return base64.b64encode(processor.page_token).decode("ascii")
return processor.page_token
return None


Expand Down Expand Up @@ -162,7 +162,7 @@ def export_chunk_to_stored_blobs(
export_limit: int,
environment_id: int | None,
first_page: bool = True,
page_token: str | None = None,
page_token: bytes | str | None = None,
offset: int = 0,
bytes_written: int = 0,
batch_size: int = SNUBA_MAX_RESULTS,
Expand All @@ -174,7 +174,7 @@ def export_chunk_to_stored_blobs(
data_export,
environment_id,
output_mode,
page_token_b64=page_token,
page_token=page_token,
)

with tempfile.TemporaryFile(mode="w+b") as tf:
Expand Down Expand Up @@ -240,7 +240,7 @@ def _schedule_retry(
base_bytes_written: int,
environment_id: int | None,
export_retries: int,
page_token: str | None,
page_token: bytes | str | None,
delay_retry: bool = False,
) -> None:
assemble_download.apply_async(
Expand Down Expand Up @@ -280,7 +280,7 @@ def _schedule_next_task(
"bytes_written": bytes_written,
"environment_id": environment_id,
"export_retries": export_retries,
"page_token": _page_token_b64_from_processor(processor),
"page_token": _page_token_from_processor(processor),
}
should_continue = next_offset < export_limit and (
(isinstance(processor, TraceItemFullExportProcessor) and processor.page_token is not None)
Expand Down Expand Up @@ -325,7 +325,7 @@ def assemble_download(
environment_id: int | None = None,
export_retries: int = DEFAULT_EXPORT_RETRIES,
*,
page_token: str | None = None,
page_token: bytes | str | None = None,
**kwargs: Any,
) -> None:
# The API response to export the data contains the ID which you can use
Expand Down Expand Up @@ -573,7 +573,7 @@ def get_processor(
environment_id: int | None,
output_mode: OutputMode,
*,
page_token_b64: str | None = None,
page_token: bytes | str | None = None,
) -> IssuesByTagProcessor | DiscoverProcessor | ExploreProcessor | TraceItemFullExportProcessor:
try:
if data_export.query_type == ExportQueryType.ISSUES_BY_TAG:
Expand All @@ -597,17 +597,21 @@ def get_processor(
output_mode=output_mode,
)
elif data_export.query_type == ExportQueryType.TRACE_ITEM_FULL_EXPORT:
page_token: bytes | None = None
if page_token_b64:
try:
page_token = base64.b64decode(page_token_b64)
except (ValueError, TypeError) as e:
raise ExportError("Invalid export trace item pagination state.") from e
page_token_bytes: bytes | None = None
if page_token is not None:
# Handle both bytes (new) and base64 string (legacy) page tokens
if isinstance(page_token, str):
try:
page_token_bytes = base64.b64decode(page_token)
except (ValueError, TypeError) as e:
raise ExportError("Invalid export trace item pagination state.") from e
else:
page_token_bytes = page_token
return TraceItemFullExportProcessor(
explore_query=data_export.query_info,
organization=data_export.organization,
output_mode=output_mode,
page_token=page_token,
page_token=page_token_bytes,
)

else:
Expand Down
6 changes: 4 additions & 2 deletions src/sentry/features/temporary.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ def register_temporary_features(manager: FeatureManager) -> None:
manager.add("organizations:integrations-gitlab-project-management", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Temporary: log full Jira Cloud `issue.updated` webhook payloads so we can design project-change link rewriting.
manager.add("organizations:jira-issue-updated-payload-logging", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)
# Use the paginated project endpoint in Jira org config to avoid timeouts on large instances.
manager.add("organizations:jira-paginated-project-config", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)
# Enable inviting billing members to organizations at the member limit.
manager.add("organizations:invite-billing", OrganizationFeature, FeatureHandlerStrategy.INTERNAL, default=False, api_expose=False)
# Enable inviting members to organizations.
Expand Down Expand Up @@ -177,6 +179,8 @@ def register_temporary_features(manager: FeatureManager) -> None:
manager.add("organizations:onboarding-scm-experiment", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Experiment: SCM onboarding project details A/B test
manager.add("organizations:onboarding-scm-project-details-experiment", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Experiment: SCM-first project creation wizard A/B test (project creation flow, not new-org onboarding)
manager.add("organizations:onboarding-scm-project-creation-experiment", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Enable large ownership rule file size limit
manager.add("organizations:ownership-size-limit-large", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)
# Enable xlarge ownership rule file size limit
Expand Down Expand Up @@ -341,8 +345,6 @@ def register_temporary_features(manager: FeatureManager) -> None:
manager.add("organizations:insights-query-date-range-limit", OrganizationFeature, FeatureHandlerStrategy.INTERNAL, api_expose=True)
# Make Insights overview pages use EAP instead of transactions (because eap is not on AM1)
manager.add("organizations:insights-modules-use-eap", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Enable access to insights metrics alerts
manager.add("organizations:insights-alerts", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Enable data browsing heat map widget
manager.add("organizations:data-browsing-heat-map-widget", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Enable public RPC endpoint for local seer development
Expand Down
7 changes: 0 additions & 7 deletions src/sentry/incidents/charts.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

from django.utils import timezone

from sentry import features
from sentry.api import client
from sentry.api.base import logger
from sentry.api.utils import get_datetime_from_stats_period
Expand Down Expand Up @@ -210,15 +209,9 @@ def build_metric_alert_chart(
),
}

allow_mri = features.has(
"organizations:insights-alerts",
organization,
actor=user,
)
aggregate = translate_aggregate_field(
snuba_query.aggregate,
reverse=True,
allow_mri=allow_mri,
allow_eap=dataset == Dataset.EventsAnalyticsPlatform,
)
# If we allow alerts to be across multiple orgs this will break
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ def convert_args(self, request: Request, organization_id_or_slug, config_id, *ar
return (args, kwargs)

def get(self, request: Request, config_id, organization, config) -> Response:
project = config.project_repository.project
if not request.access.has_project_access(project):
return self.respond(status=status.HTTP_403_FORBIDDEN)

try:
codeowner_contents = get_codeowner_contents(config)
except ApiError as e:
Expand Down
Loading
Loading