Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
82f640b
fix(replays): Use aggregate search config in bulk delete job (#96589)
cmanallen Jul 29, 2025
5abe19b
chore(emerge): Leverage new Flex, Text & Heading components for Emerg…
rbro112 Jul 29, 2025
95416cb
ref: strong typing for sentry.utils.query (#95982)
asottile-sentry Jul 29, 2025
070eea4
feat(Codecov): Get rid of main branch default value (#96652)
adrianviquez Jul 29, 2025
51eed24
Revert "fix(hybridcloud) Continue to track down write volume from sen…
getsentry-bot Jul 29, 2025
63d0d0c
fix(js-sdk): Use correct `enableLogs` flag (#96654)
AbhiPrasad Jul 29, 2025
53b78f5
chore(eap): Remove option to switch entities for EAP subscriptions (#…
phacops Jul 29, 2025
b8b13da
feat(logs): Return hasLogs in projects response (#96591)
Zylphrex Jul 29, 2025
200c45c
feat(codecov): Add TA onboarding steps for steps 4,5,6 (#96599)
calvin-codecov Jul 29, 2025
2ca9b7b
chore(trace-view-vitals): refactor pill styling (#96477)
Jul 29, 2025
9b1aebb
feat(issues): add subtitles for common search filters (#96382)
shayna-ch Jul 29, 2025
c070fb6
fix(aci): More manageable delayed_workflow logs (#96611)
kcons Jul 29, 2025
b6c5e78
ref(rr6): Remove RouteWithName export (#96563)
evanpurkhiser Jul 29, 2025
e28a732
:wrench: chore(aci): logs to check if we are correctly invoking the a…
iamrajjoshi Jul 29, 2025
d26758e
feat(feedback): label generation at ingest, stored as tags (#96390)
vishnupsatish Jul 29, 2025
4a75aa1
feat(rr6): Add deprecatedRouterProps to route objects (#96588)
scttcper Jul 29, 2025
a1c5287
fix(aci): Reject condition_group for error detectors (#96602)
malwilley Jul 29, 2025
a1ee381
ref(nav-v2): removes nav v2 flag (#95955)
cvxluo Jul 29, 2025
0787316
feat(onboarding): Add logs to ruby onboarding (#96653)
AbhiPrasad Jul 29, 2025
40fa0cc
feat(onboarding): Add logs to ruby/rails onboarding (#96671)
AbhiPrasad Jul 29, 2025
d49cf97
chore(detectors): Update span for performance detection span sorting …
roggenkemper Jul 29, 2025
a17ae30
ref(uptime): Remove unused uptimeSubscriptionId (#96670)
evanpurkhiser Jul 29, 2025
fb457a7
fix(replay): fix navigation timestamp in log (#96669)
michellewzhang Jul 29, 2025
bceadcd
fix(discover): Top N queries do not support grouping by array fields …
gggritso Jul 29, 2025
cc18504
feat(onboarding): Add logs to ruby-rack onboarding (#96673)
AbhiPrasad Jul 29, 2025
1103d2e
fix(issues): Prevent "Set up code mapping" from closing frame (#96675)
scttcper Jul 29, 2025
b9b17a7
perf(aci): Make get_latest_release_for_env cache not-found releases (…
kcons Jul 29, 2025
d7c6111
chore(auth-v2): record analytics for rotating csrf token (#96677)
cathteng Jul 29, 2025
6508c89
fix(dashboards): fix tooltip in certain fields not aligned to cell te…
lzhao-sentry Jul 29, 2025
66722af
chore(issues): remove option registration from inferring project plat…
cvxluo Jul 29, 2025
e77e179
ref: fix typing for fixtures.schema_validation (#96667)
asottile-sentry Jul 29, 2025
6cfe07a
ref(storybook): add primitives section (#96515)
JonasBa Jul 29, 2025
e67a783
test(explore): Add tests for the explore spans page (#96579)
wmak Jul 29, 2025
055056d
ref: convert ProjectOption.value to django JSONField (#96682)
asottile-sentry Jul 29, 2025
9d38989
grid: add grid implementation (#96597)
JonasBa Jul 29, 2025
ec21956
fix(billing): Change display name to Logs (#96660)
jarrettscott Jul 29, 2025
99ca862
feat(aci): set up detector Ongoing Issues (#96604)
ameliahsu Jul 29, 2025
bf052e4
ref(multi-query): Use shared chart visualization in multi query mode …
Zylphrex Jul 29, 2025
195d699
ref(grouping): Always use default config when loading unknown config …
lobsterkatie Jul 29, 2025
9301804
ci(jest): regenerate jest-balance.json (#95512)
getsentry-bot Jul 29, 2025
6540b68
Revert "ref: convert ProjectOption.value to django JSONField (#96682)"
getsentry-bot Jul 29, 2025
47382c2
Reapply "fix(hybridcloud) Continue to track down write volume from se…
markstory Jul 29, 2025
f01a21c
fix(discover): Refresh discover saved queries on delete/duplicate (#9…
scttcper Jul 29, 2025
228cc1a
chore(rate-limit): Remove legacy rate limit pages (#96354)
roggenkemper Jul 29, 2025
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
31 changes: 31 additions & 0 deletions fixtures/page_objects/explore_spans.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from selenium.webdriver.common.by import By

from .base import BasePage
from .global_selection import GlobalSelectionPage


class ExploreSpansPage(BasePage):
def __init__(self, browser, client):
super().__init__(browser)
self.client = client
self.global_selection = GlobalSelectionPage(browser)

def visit_explore_spans(self, org):
self.browser.get(f"/organizations/{org}/explore/traces/")
self.wait_until_loaded()

def get_spans_row_with_id(self, span_id):
row = self.browser.find_element(
by=By.XPATH,
value=f'//tr[.//*[contains(text(),"{span_id}")]]',
)
return row

def get_spans_row_columns(self, row):
# The expanded row actually makes a new sibling row that contains the fields-tree.
columns = row.find_elements(By.XPATH, "child::td")
return columns

def wait_until_loaded(self):
self.browser.wait_until_not('[data-test-id="loading-indicator"]')
self.browser.wait_until_test_id("spans-table")
21 changes: 14 additions & 7 deletions fixtures/schema_validation.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
import functools
from collections.abc import Callable

import pytest
from jsonschema import ValidationError


def invalid_schema(func):
def inner(self, *args, **kwargs):
def invalid_schema[**P](func: Callable[P, None]) -> Callable[P, None]:
@functools.wraps(func)
def inner(*args: P.args, **kwargs: P.kwargs) -> None:
with pytest.raises(ValidationError):
func(self)
func(*args, **kwargs)

return inner


def invalid_schema_with_error_message(message):
def decorator(func):
def inner(self, *args, **kwargs):
def invalid_schema_with_error_message[
**P
](message: str) -> Callable[[Callable[P, None]], Callable[P, None]]:
def decorator(func: Callable[P, None]) -> Callable[P, None]:
@functools.wraps(func)
def inner(*args: P.args, **kwargs: P.kwargs) -> None:
with pytest.raises(ValidationError) as excinfo:
func(self)
func(*args, **kwargs)
assert excinfo.value.message == message

return inner
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ disable_error_code = [
[[tool.mypy.overrides]]
module = [
"fixtures.safe_migrations_apps.*",
"fixtures.schema_validation",
"sentry.analytics.*",
"sentry.api.decorators",
"sentry.api.endpoints.integrations.sentry_apps.installation.external_issue.*",
Expand Down Expand Up @@ -436,6 +437,7 @@ module = [
"sentry.utils.projectflags",
"sentry.utils.prompts",
"sentry.utils.pubsub",
"sentry.utils.query",
"sentry.utils.redis",
"sentry.utils.redis_metrics",
"sentry.utils.registry",
Expand Down
1 change: 1 addition & 0 deletions src/sentry/analytics/events/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from .alert_sent import * # noqa: F401,F403
from .api_token_created import * # noqa: F401,F403
from .api_token_deleted import * # noqa: F401,F403
from .auth_v2 import * # noqa: F401,F403
from .checkin_processing_error_stored import * # noqa: F401,F403
from .codeowners_assignment import * # noqa: F401,F403
from .codeowners_created import * # noqa: F401,F403
Expand Down
16 changes: 16 additions & 0 deletions src/sentry/analytics/events/auth_v2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from sentry import analytics
from sentry.analytics import Event, eventclass


@eventclass("auth_v2.csrf_token.rotated")
class AuthV2CsrfTokenRotated(Event):
event: str


@eventclass("auth_v2.csrf_token.delete_login")
class AuthV2DeleteLogin(Event):
event: str


analytics.register(AuthV2CsrfTokenRotated)
analytics.register(AuthV2DeleteLogin)
12 changes: 12 additions & 0 deletions src/sentry/analytics/events/first_log_sent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from sentry import analytics


@analytics.eventclass("first_log.sent")
class FirstLogSentEvent(analytics.Event):
organization_id: int
project_id: int
platform: str | None = None
user_id: int | None = None


analytics.register(FirstLogSentEvent)
9 changes: 9 additions & 0 deletions src/sentry/api/endpoints/auth_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
from rest_framework.request import Request
from rest_framework.response import Response

from sentry import analytics
from sentry.analytics.events.auth_v2 import AuthV2DeleteLogin
from sentry.api.api_owners import ApiOwner
from sentry.api.api_publish_status import ApiPublishStatus
from sentry.api.authentication import QuietBasicAuthentication
Expand Down Expand Up @@ -328,6 +330,13 @@ def delete(self, request: Request, *args, **kwargs) -> Response:
response.delete_cookie(settings.CSRF_COOKIE_NAME, domain=settings.CSRF_COOKIE_DOMAIN)
response.delete_cookie(settings.SESSION_COOKIE_NAME, domain=settings.SESSION_COOKIE_DOMAIN)

if referrer := request.GET.get("referrer"):
analytics.record(
AuthV2DeleteLogin(
event=referrer,
)
)

if slo_url:
response.status_code = status.HTTP_200_OK
response.data = {"sloUrl": slo_url}
Expand Down
3 changes: 3 additions & 0 deletions src/sentry/api/serializers/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ class ProjectSerializerBaseResponse(_ProjectSerializerOptionalBaseResponse):
hasInsightsLlmMonitoring: bool
hasInsightsAgentMonitoring: bool
hasInsightsMCP: bool
hasLogs: bool


class ProjectSerializerResponse(ProjectSerializerBaseResponse):
Expand Down Expand Up @@ -554,6 +555,7 @@ def serialize(
"hasInsightsLlmMonitoring": bool(obj.flags.has_insights_llm_monitoring),
"hasInsightsAgentMonitoring": bool(obj.flags.has_insights_agent_monitoring),
"hasInsightsMCP": bool(obj.flags.has_insights_mcp),
"hasLogs": bool(obj.flags.has_logs),
"isInternal": obj.is_internal_project(),
"isPublic": obj.public,
# Projects don't have avatar uploads, but we need to maintain the payload shape for
Expand Down Expand Up @@ -790,6 +792,7 @@ def serialize( # type: ignore[override] # intentionally different data shape
hasInsightsLlmMonitoring=bool(obj.flags.has_insights_llm_monitoring),
hasInsightsAgentMonitoring=bool(obj.flags.has_insights_agent_monitoring),
hasInsightsMCP=bool(obj.flags.has_insights_mcp),
hasLogs=bool(obj.flags.has_logs),
platform=obj.platform,
platforms=attrs["platforms"],
latestRelease=attrs["latest_release"],
Expand Down
2 changes: 2 additions & 0 deletions src/sentry/apidocs/examples/organization_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,7 @@ class OrganizationExamples:
"hasInsightsLlmMonitoring": False,
"hasInsightsAgentMonitoring": False,
"hasInsightsMCP": False,
"hasLogs": False,
"platform": "node",
"platforms": [],
"latestRelease": None,
Expand Down Expand Up @@ -453,6 +454,7 @@ class OrganizationExamples:
"hasInsightsLlmMonitoring": False,
"hasInsightsAgentMonitoring": False,
"hasInsightsMCP": False,
"hasLogs": False,
"latestRelease": None,
}
],
Expand Down
2 changes: 2 additions & 0 deletions src/sentry/apidocs/examples/project_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
"hasInsightsLlmMonitoring": False,
"hasInsightsAgentMonitoring": False,
"hasInsightsMCP": False,
"hasLogs": False,
"isInternal": False,
"isPublic": False,
"avatar": {"avatarType": "letter_avatar", "avatarUuid": None},
Expand Down Expand Up @@ -339,6 +340,7 @@
"hasInsightsLlmMonitoring": False,
"hasInsightsAgentMonitoring": False,
"hasInsightsMCP": False,
"hasLogs": False,
"platform": "node-express",
"platforms": [],
"latestRelease": None,
Expand Down
2 changes: 2 additions & 0 deletions src/sentry/apidocs/examples/team_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ class TeamExamples:
"hasInsightsLlmMonitoring": False,
"hasInsightsAgentMonitoring": False,
"hasInsightsMCP": False,
"hasLogs": False,
"isInternal": False,
"isPublic": False,
"avatar": {"avatarType": "letter_avatar", "avatarUuid": None},
Expand Down Expand Up @@ -309,6 +310,7 @@ class TeamExamples:
"hasInsightsLlmMonitoring": False,
"hasInsightsAgentMonitoring": False,
"hasInsightsMCP": False,
"hasLogs": False,
"isInternal": False,
"isPublic": False,
"avatar": {"avatarType": "letter_avatar", "avatarUuid": None},
Expand Down
9 changes: 9 additions & 0 deletions src/sentry/auth_v2/endpoints/csrf.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from drf_spectacular.utils import extend_schema
from rest_framework import status

from sentry import analytics
from sentry.analytics.events.auth_v2 import AuthV2CsrfTokenRotated
from sentry.api.api_owners import ApiOwner
from sentry.api.api_publish_status import ApiPublishStatus
from sentry.api.base import Endpoint, control_silo_endpoint
Expand Down Expand Up @@ -65,6 +67,13 @@ def get(self, request, *args, **kwargs):
@method_decorator(ensure_csrf_cookie)
def put(self, request, *args, **kwargs):
rotate_token(request)
if referrer := request.GET.get("referrer"):
analytics.record(
AuthV2CsrfTokenRotated(
event=referrer,
)
)

return self.respond(
{
"detail": "Rotated CSRF cookie",
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/codecov/endpoints/test_results/test_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def get(self, request: Request, owner: RpcIntegration, repository: str, **kwargs
"owner": owner_slug,
"repo": repository,
"filters": {
"branch": request.query_params.get("branch", "main"),
"branch": request.query_params.get("branch"),
"parameter": request.query_params.get("filterBy"),
"interval": (
request.query_params.get("interval", MeasurementInterval.INTERVAL_30_DAY.value)
Expand Down
31 changes: 30 additions & 1 deletion src/sentry/feedback/usecases/ingest/create_feedback.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@

import jsonschema

from sentry import options
from sentry import features, options
from sentry.constants import DataCategory
from sentry.feedback.lib.utils import UNREAL_FEEDBACK_UNATTENDED_MESSAGE, FeedbackCreationSource
from sentry.feedback.usecases.label_generation import (
AI_LABEL_TAG_PREFIX,
MAX_AI_LABELS,
generate_labels,
)
from sentry.feedback.usecases.spam_detection import is_spam, spam_detection_enabled
from sentry.issues.grouptype import FeedbackGroup
from sentry.issues.issue_occurrence import IssueEvidence, IssueOccurrence
Expand Down Expand Up @@ -346,6 +351,30 @@ def create_feedback_issue(
}
)

# Generating labels using Seer, which will later be used to categorize feedbacks
if (
not is_message_spam
and features.has("organizations:user-feedback-ai-categorization", project.organization)
and features.has("organizations:gen-ai-features", project.organization)
):
try:
labels = generate_labels(feedback_message, project.organization_id)
if len(labels) > MAX_AI_LABELS:
logger.info(
"Feedback message has more than the maximum allowed labels.",
extra={
"project_id": project.id,
"entrypoint": "create_feedback_issue",
"feedback_message": feedback_message[:100],
},
)
labels = labels[:MAX_AI_LABELS]

for idx, label in enumerate(labels):
event_fixed["tags"][f"{AI_LABEL_TAG_PREFIX}.{idx}"] = label
except Exception:
logger.exception("Error generating labels", extra={"project_id": project.id})

# Set the user.email tag since we want to be able to display user.email on the feedback UI as a tag
# as well as be able to write alert conditions on it
user_email = get_path(event_fixed, "user", "email")
Expand Down
70 changes: 70 additions & 0 deletions src/sentry/feedback/usecases/label_generation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import logging
from typing import TypedDict

import requests
from django.conf import settings

from sentry.seer.signed_seer_api import sign_with_seer_secret
from sentry.utils import json, metrics

logger = logging.getLogger(__name__)


class LabelRequest(TypedDict):
"""Corresponds to GenerateFeedbackLabelsRequest in Seer."""

organization_id: int
feedback_message: str


AI_LABEL_TAG_PREFIX = "ai_categorization.label"
# If Seer generates more labels, we truncate it to this many labels
MAX_AI_LABELS = 15

SEER_GENERATE_LABELS_URL = f"{settings.SEER_AUTOFIX_URL}/v1/automation/summarize/feedback/labels"


@metrics.wraps("feedback.generate_labels", sample_rate=1.0)
def generate_labels(feedback_message: str, organization_id: int) -> list[str]:
"""
Generate labels for a feedback message.

The possible errors this can throw are:
- request.exceptions.Timeout, request.exceptions.ConnectionError, etc. while making the request
- request.exceptions.HTTPError (for raise_for_status)
- requests.exceptions.JSONDecodeError or another decode error if the response is not valid JSON
- KeyError / ValueError if the response JSON doesn't have the expected structure
"""
request = LabelRequest(
organization_id=organization_id,
feedback_message=feedback_message,
)

serialized_request = json.dumps(request)

response = requests.post(
SEER_GENERATE_LABELS_URL,
data=serialized_request,
headers={
"content-type": "application/json;charset=utf-8",
**sign_with_seer_secret(serialized_request.encode()),
},
timeout=10,
)

if response.status_code != 200:
logger.error(
"Failed to generate labels",
extra={
"status_code": response.status_code,
"response": response.text,
"content": response.content,
},
)

response.raise_for_status()

labels = response.json()["data"]["labels"]

# Guaranteed to be a list of strings (validated in Seer)
return labels
12 changes: 6 additions & 6 deletions src/sentry/grouping/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,6 @@ class GroupHashInfo:
NULL_GROUPHASH_INFO = GroupHashInfo(NULL_GROUPING_CONFIG, {}, [], [], None)


class GroupingConfigNotFound(LookupError):
pass


class GroupingConfig(TypedDict):
id: str
enhancements: str
Expand Down Expand Up @@ -220,14 +216,18 @@ def get_default_grouping_config_dict(config_id: str | None = None) -> GroupingCo


def load_grouping_config(config_dict: GroupingConfig | None = None) -> StrategyConfiguration:
"""Loads the given grouping config."""
"""
Load the given grouping config, or the default config if none is provided or if the given
config is not recognized.
"""
if config_dict is None:
config_dict = get_default_grouping_config_dict()
elif "id" not in config_dict:
raise ValueError("Malformed configuration dictionary")
config_id = config_dict["id"]
if config_id not in CONFIGURATIONS:
raise GroupingConfigNotFound(config_id)
config_dict = get_default_grouping_config_dict()
config_id = config_dict["id"]
return CONFIGURATIONS[config_id](enhancements=config_dict["enhancements"])


Expand Down
Loading