Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
455bbca
fix(ai-insights): sort conversation spans by start timestamp (#111185)
obostjancic Mar 23, 2026
e434a0f
fix(ai-insights): token input/output count (#111284)
obostjancic Mar 23, 2026
0e064c1
feat(scm): Move static facade to module level and dynamically constru…
cmanallen Mar 23, 2026
537ef28
ref(db): Remove stale entries from create_or_update allowlist (#111287)
vgrozdanic Mar 23, 2026
d129221
feat(seer): Add SeerOperatorExplorerCache for completion hook payload…
alexsohn1126 Mar 23, 2026
2801a91
feat(occurrences on eap): Implement errors EAP query for organization…
shashjar Mar 23, 2026
30448b1
fix(tasks) Use SingletonProducer to better manage producer buffers (#…
markstory Mar 23, 2026
80216b7
feat(seer): Add SeerExplorerResponse notification data and Slack rend…
alexsohn1126 Mar 23, 2026
a2862a7
Scope get_latest_issue_event ID query to the organization (#111252)
geoffg-sentry Mar 23, 2026
b2c4940
ref(db): Stop using legacy create_or_update in API endpoints (#111288)
vgrozdanic Mar 23, 2026
e1f2981
fix(dashboards): Set breakdown legend widget limit to 3 in prebuilt c…
DominikB2014 Mar 23, 2026
512adf9
chore(autofix): Only require autofix-on-explorer for automations (#11…
Zylphrex Mar 23, 2026
74a116c
ref(dashboards): Extract breakdown table matching and support multi-g…
DominikB2014 Mar 23, 2026
c7e0983
fix(ai-insights): Use AIContentRenderer for conversation table toolti…
obostjancic Mar 23, 2026
92b7993
fix(autofix): Remove broken docs link from GitHub Copilot CTA (#111298)
JoshFerge Mar 23, 2026
a186745
feat(dashboards): Improvements to Dashboard generation tracking (#111…
edwardgou-sentry Mar 23, 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
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -818,6 +818,7 @@ tests/sentry/api/endpoints/test_organization_attribute_mappings.py @get
/tests/sentry/releases/ @getsentry/coding-workflows-sentry-backend

## SCM
/src/sentry/scm/ @getsentry/scm
/src/sentry/integrations/source_code_management/ @getsentry/product-owners-settings-integrations @getsentry/ecosystem @getsentry/scm
/src/sentry/integrations/repository/ @getsentry/product-owners-settings-integrations @getsentry/ecosystem @getsentry/scm
/src/sentry/integrations/github_enterprise/ @getsentry/product-owners-settings-integrations @getsentry/ecosystem @getsentry/scm
Expand Down
16 changes: 0 additions & 16 deletions .github/codeowners-coverage-baseline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -236,22 +236,6 @@ src/sentry/runner/importer.py
src/sentry/runner/initializer.py
src/sentry/runner/main.py
src/sentry/runner/settings.py
src/sentry/scm/actions.py
src/sentry/scm/apps.py
src/sentry/scm/endpoints/scm_rpc.py
src/sentry/scm/errors.py
src/sentry/scm/private/event_stream.py
src/sentry/scm/private/helpers.py
src/sentry/scm/private/ipc.py
src/sentry/scm/private/provider.py
src/sentry/scm/private/providers/github.py
src/sentry/scm/private/providers/gitlab.py
src/sentry/scm/private/rpc.py
src/sentry/scm/private/stream_producer.py
src/sentry/scm/private/webhooks/github.py
src/sentry/scm/stream.py
src/sentry/scm/types.py
src/sentry/scm/utils.py
src/sentry/scripts/alerts/conditional_zrem.lua
src/sentry/scripts/digests/digests.lua
src/sentry/scripts/quotas/is_rate_limited.lua
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ dependencies = [
"statsd>=3.3.0",
"structlog>=22.1.0",
"symbolic>=12.14.1",
"taskbroker-client>=0.1.6",
"taskbroker-client>=0.1.7",
"tiktoken>=0.8.0",
"tokenizers>=0.22.0",
"tldextract>=5.1.2",
Expand Down
71 changes: 71 additions & 0 deletions src/sentry/api/endpoints/organization_events_trace.py
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,77 @@ def query_trace_data(
for result, query in zip(results, [transaction_query, error_query, occurrence_query])
]

errors_callsite = "api.trace.query_trace_data.errors"
if EAPOccurrencesComparator.should_check_experiment(errors_callsite):
try:
eap_error_result = Occurrences.run_table_query(
params=snuba_params,
query_string=f"trace:{trace_id}",
selected_columns=[
"id",
"project_id",
"project.name",
"timestamp",
"span_id",
"transaction",
"group_id",
"title",
"message",
"level",
],
orderby=["id"],
offset=0,
limit=limit,
referrer=Referrer.API_TRACE_VIEW_GET_EVENTS.value,
config=SearchResolverConfig(),
occurrence_category=OccurrenceCategory.ERROR,
)
eap_errors = [
{
"id": row.get("id", ""),
"project": row.get("project.name", ""),
"project.id": row.get("project_id"),
"timestamp": row.get("timestamp", ""),
"timestamp_ms": row.get("timestamp", ""),
"trace.span": row.get("span_id", ""),
"transaction": row.get("transaction", ""),
"issue": row.get("group_id"),
"issue.id": row.get("group_id"),
"title": row.get("title", ""),
"message": row.get("message", ""),
"tags[level]": row.get("level", ""),
}
for row in eap_error_result.get("data", [])
]
except Exception:
logger.exception(
"Fetching error occurrences for trace from EAP failed in query_trace_data",
extra={
"trace_id": trace_id,
"organization_id": (
snuba_params.organization.id if snuba_params.organization else None
),
},
)
eap_errors = []

transformed_results[1] = EAPOccurrencesComparator.check_and_choose(
control_data=transformed_results[1],
experimental_data=eap_errors,
callsite=errors_callsite,
is_experimental_data_a_null_result=len(eap_errors) == 0,
reasonable_match_comparator=lambda snuba, eap: {e["id"] for e in eap}.issubset(
{e["id"] for e in snuba}
),
debug_context={
"trace_id": trace_id,
"organization_id": (
snuba_params.organization.id if snuba_params.organization else None
),
"project_ids": snuba_params.project_ids,
},
)

# Join group IDs from the occurrence dataset to transactions data
occurrence_issue_ids = defaultdict(list)
occurrence_ids = defaultdict(list)
Expand Down
4 changes: 2 additions & 2 deletions src/sentry/api/endpoints/organization_pinned_searches.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,13 @@ def put(self, request: Request, organization: Organization) -> Response:
return Response(serializer.errors, status=400)

result = serializer.validated_data
SavedSearch.objects.create_or_update(
SavedSearch.objects.update_or_create(
organization=organization,
name=PINNED_SEARCH_NAME,
owner_id=request.user.id,
type=result["type"],
visibility=Visibility.OWNER_PINNED,
values={"query": result["query"], "sort": result["sort"]},
defaults={"query": result["query"], "sort": result["sort"]},
)

# This entire endpoint will be removed once custom views are GA'd
Expand Down
6 changes: 3 additions & 3 deletions src/sentry/api/endpoints/organization_recent_searches.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,13 @@ def post(self, request: Request, organization) -> Response:
if serializer.is_valid():
result = serializer.validated_data

created = RecentSearch.objects.create_or_update(
_, created = RecentSearch.objects.update_or_create(
organization_id=organization.id,
user_id=request.user.id,
type=result["type"],
query=result["query"],
values={"last_seen": timezone.now()},
)[1]
defaults={"last_seen": timezone.now()},
)
if created:
remove_excess_recent_searches(organization, request.user, result["type"])
status = 201 if created else 204
Expand Down
4 changes: 2 additions & 2 deletions src/sentry/api/endpoints/organization_unsubscribe.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,9 @@ def fetch_instance(
return issue

def unsubscribe(self, request: Request, instance: Group):
GroupSubscription.objects.create_or_update(
GroupSubscription.objects.update_or_create(
group=instance,
project_id=instance.project_id,
user_id=request.user.pk,
values={"is_active": False},
defaults={"is_active": False},
)
4 changes: 2 additions & 2 deletions src/sentry/api/endpoints/prompts_activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,8 @@ def put(self, request: Request, organization: Organization, **kwargs) -> Respons

try:
with transaction.atomic(router.db_for_write(PromptsActivity)):
PromptsActivity.objects.create_or_update(
feature=feature, user_id=request.user.id, values={"data": data}, **fields
PromptsActivity.objects.update_or_create(
feature=feature, user_id=request.user.id, defaults={"data": data}, **fields
)
except IntegrityError:
pass
Expand Down
10 changes: 4 additions & 6 deletions src/sentry/api/helpers/group_index/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
from sentry.analytics.events.manual_issue_assignment import ManualIssueAssignment
from sentry.api.serializers import serialize
from sentry.api.serializers.models.actor import ActorSerializer, ActorSerializerResponse
from sentry.db.models.query import create_or_update
from sentry.hybridcloud.rpc import coerce_id_from
from sentry.integrations.tasks.kick_off_status_syncs import kick_off_status_syncs
from sentry.issues.grouptype import GroupCategory
Expand Down Expand Up @@ -928,11 +927,11 @@ def handle_is_subscribed(
# subscribed" to "you were subscribed since you were
# assigned" just by clicking the "subscribe" button (and you
# may no longer be assigned to the issue anyway).
GroupSubscription.objects.create_or_update(
GroupSubscription.objects.update_or_create(
user_id=acting_user.id if acting_user else None,
group=group,
project=project_lookup[group.project_id],
values={"is_active": is_subscribed, "reason": GroupSubscriptionReason.unknown},
defaults={"is_active": is_subscribed, "reason": GroupSubscriptionReason.unknown},
)

return {"reason": SUBSCRIPTION_REASON_MAP.get(GroupSubscriptionReason.unknown, "unknown")}
Expand Down Expand Up @@ -990,12 +989,11 @@ def handle_has_seen(
if has_seen:
for group in group_list:
if is_member_map.get(group.project_id):
create_or_update(
GroupSeen,
GroupSeen.objects.update_or_create(
group=group,
user_id=user_id,
project=project_lookup[group.project_id],
values={"last_seen": django_timezone.now()},
defaults={"last_seen": django_timezone.now()},
)
elif has_seen is False and user_id is not None:
GroupSeen.objects.filter(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ def post(self, request: Request, organization: Organization, view_id: str) -> Re
return Response(status=status.HTTP_404_NOT_FOUND)

# Create or update the last_visited timestamp
GroupSearchViewLastVisited.objects.create_or_update(
GroupSearchViewLastVisited.objects.update_or_create(
organization=organization,
user_id=request.user.id,
group_search_view=view,
values={"last_visited": timezone.now()},
defaults={"last_visited": timezone.now()},
)

return Response(status=status.HTTP_204_NO_CONTENT)
23 changes: 23 additions & 0 deletions src/sentry/notifications/platform/slack/renderers/seer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
ContextBlock,
InteractiveElement,
LinkButtonElement,
MarkdownBlock,
MarkdownTextObject,
PlainTextObject,
RichTextBlock,
Expand All @@ -26,6 +27,8 @@
SeerAutofixError,
SeerAutofixTrigger,
SeerAutofixUpdate,
SeerExplorerError,
SeerExplorerResponse,
)
from sentry.notifications.platform.types import (
NotificationData,
Expand Down Expand Up @@ -91,6 +94,10 @@ def render[DataT: NotificationData](
return cls._render_autofix_error(data)
elif isinstance(data, SeerAutofixUpdate):
return cls._render_autofix_update(data)
elif isinstance(data, SeerExplorerError):
return cls._render_explorer_error(data)
elif isinstance(data, SeerExplorerResponse):
return cls._render_explorer_response(data)
else:
raise ValueError(f"SeerSlackRenderer does not support {data.__class__.__name__}")

Expand Down Expand Up @@ -187,6 +194,22 @@ def _render_autofix_update(cls, data: SeerAutofixUpdate) -> SlackRenderable:

return SlackRenderable(blocks=blocks, text="Seer has emerged with news from its voyage")

@classmethod
def _render_explorer_error(cls, data: SeerExplorerError) -> SlackRenderable:
return SlackRenderable(
blocks=[
SectionBlock(text=data.error_title),
SectionBlock(text=MarkdownTextObject(text=f">{data.error_message}")),
],
text=f"Seer stumbled: {data.error_title}",
)

@classmethod
def _render_explorer_response(cls, data: SeerExplorerResponse) -> SlackRenderable:
blocks: list[Block] = [MarkdownBlock(text=data.summary)]

return SlackRenderable(blocks=blocks, text="Seer Explorer has finished")

@classmethod
def _render_link_button(
cls,
Expand Down
47 changes: 47 additions & 0 deletions src/sentry/notifications/platform/templates/seer.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,50 @@ def from_update(update: SeerAutofixUpdate) -> SeerAutofixTrigger:
organization_id=update.organization_id,
stopping_point=stopping_point,
)


class SeerExplorerError(NotificationData):
error_message: str
error_title: str = "Seer had some trouble..."
source: NotificationSource = NotificationSource.SEER_EXPLORER_ERROR


@template_registry.register(NotificationSource.SEER_EXPLORER_ERROR)
class SeerExplorerErrorTemplate(NotificationTemplate[SeerExplorerError]):
category = NotificationCategory.SEER
example_data = SeerExplorerError(
error_title="Seer had some trouble...",
error_message="Seer could not explore your organization.",
)
hide_from_debugger = True

def render(self, data: SeerExplorerError) -> NotificationRenderedTemplate:
return NotificationRenderedTemplate(
subject=data.error_title,
body=[ParagraphBlock(blocks=[PlainTextBlock(text=data.error_message)])],
)


class SeerExplorerResponse(NotificationData):
"""Notification data for Explorer completion response in Slack."""

run_id: int
organization_id: int
explorer_link: str
summary: str
source: NotificationSource = NotificationSource.SEER_EXPLORER_RESPONSE


@template_registry.register(NotificationSource.SEER_EXPLORER_RESPONSE)
class SeerExplorerResponseTemplate(NotificationTemplate[SeerExplorerResponse]):
category = NotificationCategory.SEER
example_data = SeerExplorerResponse(
run_id=12345,
organization_id=1,
explorer_link="https://sentry.sentry.io/explore/seer/12345/",
summary="I've finished analyzing your question.",
)
hide_from_debugger = True

def render(self, data: SeerExplorerResponse) -> NotificationRenderedTemplate:
return NotificationRenderedTemplate(subject="Seer Explorer Response", body=[])
4 changes: 4 additions & 0 deletions src/sentry/notifications/platform/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ class NotificationSource(StrEnum):
SEER_AUTOFIX_TRIGGER = "seer-autofix-trigger"
SEER_AUTOFIX_FOOTER = "seer-autofix-footer"
SEER_AUTOFIX_SUCCESS = "seer-autofix-success"
SEER_EXPLORER_RESPONSE = "seer-explorer-response"
SEER_EXPLORER_ERROR = "seer-explorer-error"


NOTIFICATION_SOURCE_MAP: dict[NotificationCategory, list[NotificationSource]] = {
Expand All @@ -83,6 +85,8 @@ class NotificationSource(StrEnum):
NotificationSource.SEER_AUTOFIX_ERROR,
NotificationSource.SEER_AUTOFIX_SUCCESS,
NotificationSource.SEER_AUTOFIX_UPDATE,
NotificationSource.SEER_EXPLORER_RESPONSE,
NotificationSource.SEER_EXPLORER_ERROR,
],
}

Expand Down
6 changes: 6 additions & 0 deletions src/sentry/options/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -3858,6 +3858,12 @@
default=True,
flags=FLAG_AUTOMATOR_MODIFIABLE,
)
register(
"taskworker.producer.max_futures",
type=Int,
default=1000,
flags=FLAG_AUTOMATOR_MODIFIABLE,
)
register(
"taskworker.route.overrides",
default={},
Expand Down
Loading
Loading