Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -338,6 +338,7 @@ tests/sentry/api/endpoints/test_organization_attribute_mappings.py @get
/static/app/views/explore/spans/spansTabSeerComboBox.tsx @getsentry/explore @getsentry/machine-learning-ai
/static/app/views/traces/ @getsentry/explore
/static/app/components/quickTrace/ @getsentry/explore
/static/app/components/dnd/ @getsentry/explore
/src/sentry/insights/ @getsentry/data-browsing
/static/app/views/performance/ @getsentry/data-browsing
/static/app/components/performance/ @getsentry/data-browsing
Expand Down
15 changes: 13 additions & 2 deletions .github/workflows/scripts/compute-sentry-selected-tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@ def main() -> int:
required=True,
help="Space-separated changed files relative to sentry repo root",
)
parser.add_argument(
"--previous-filenames",
default="",
help="Space-separated previous filenames for renamed files (queried against coverage DB)",
)
parser.add_argument("--output", help="Output file path for selected test files (one per line)")
parser.add_argument("--github-output", action="store_true", help="Write to GITHUB_OUTPUT")
args = parser.parse_args()
Expand All @@ -145,24 +150,30 @@ def main() -> int:
return 1

changed = [f.strip() for f in args.changed_files.split() if f.strip()]
previous_filenames = [f.strip() for f in args.previous_filenames.split() if f.strip()]

selective_applied = False

if not changed:
print("No changed files provided, running full test suite")
affected_test_files: set[str] = set()
else:
all_paths = changed + previous_filenames
triggered_by = [
f for f in changed if any(_matches_trigger(f, t) for t in FULL_SUITE_TRIGGERS)
f for f in all_paths if any(_matches_trigger(f, t) for t in FULL_SUITE_TRIGGERS)
]
if triggered_by:
print(f"Full test suite triggered by: {', '.join(triggered_by)}")
affected_test_files = set()
else:
selective_applied = True

# Map repo-relative paths to DB format (add ../sentry/ prefix)
# Map repo-relative paths to DB format (add ../sentry/ prefix).
# Include previous filenames for renames so the coverage DB
# (which still stores the old path) can find the right tests.
db_paths = [DB_PREFIX + f for f in changed]
for old_name in previous_filenames:
db_paths.append(DB_PREFIX + old_name)

print(f"Computing selected tests for {len(changed)} changed files...")
try:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/scripts/getsentry-dispatch.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export async function dispatch({
fileChanges,
mergeCommitSha,
sentryChangedFiles,
sentryPreviousFilenames,
targetWorkflow,
}) {
core.startGroup('Dispatching request to getsentry.');
Expand All @@ -42,6 +43,7 @@ export async function dispatch({

// Changed files for selective testing. Empty string means full suite.
'sentry-changed-files': sentryChangedFiles || '',
'sentry-previous-filenames': sentryPreviousFilenames || '',
};

core.info(
Expand Down
66 changes: 66 additions & 0 deletions .github/workflows/scripts/test_compute_sentry_selected_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,72 @@ def test_zero_tests_signals_selective_applied(self, tmp_path):
assert "test-count=0" in gh
assert output.read_text() == ""

def test_renamed_file_queries_old_path(self, tmp_path):
"""When a file is renamed, the old path should be queried against the coverage DB."""
db_path = tmp_path / "coverage.db"
_create_coverage_db(
str(db_path),
{
# Coverage DB still has the old filename
"../sentry/src/sentry/models/old_name.py": [
"../sentry/tests/sentry/test_old_name.py::T::test|run",
],
},
)
output = tmp_path / "output.txt"
gh_output = tmp_path / "gh_output"
gh_output.write_text("")

with mock.patch("compute_sentry_selected_tests.Path.exists", return_value=True):
_run(
[
"--coverage-db",
str(db_path),
"--changed-files",
"src/sentry/models/new_name.py",
"--previous-filenames",
"src/sentry/models/old_name.py",
"--output",
str(output),
"--github-output",
],
{"GITHUB_OUTPUT": str(gh_output)},
)

gh = gh_output.read_text()
assert "has-selected-tests=true" in gh
assert "test-count=1" in gh
assert output.read_text().strip() == "tests/sentry/test_old_name.py"

def test_renamed_file_without_previous_misses_coverage(self, tmp_path):
"""Without --previous-filenames, a renamed file gets no coverage hits."""
db_path = tmp_path / "coverage.db"
_create_coverage_db(
str(db_path),
{
"../sentry/src/sentry/models/old_name.py": [
"../sentry/tests/sentry/test_old_name.py::T::test|run",
],
},
)
gh_output = tmp_path / "gh_output"
gh_output.write_text("")

_run(
[
"--coverage-db",
str(db_path),
"--changed-files",
"src/sentry/models/new_name.py",
"--github-output",
],
{"GITHUB_OUTPUT": str(gh_output)},
)

gh = gh_output.read_text()
assert "has-selected-tests=true" in gh
assert "test-count=0" in gh

def test_missing_db_returns_error(self):
ret = _run(["--coverage-db", "/nonexistent/coverage.db", "--changed-files", "foo.py"])
assert ret == 1
2 changes: 2 additions & 0 deletions knip.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ const productionEntryPoints = [
'static/app/chartcuterie/**/*.{js,ts,tsx}',
// TODO: Remove when used
'static/app/components/pipeline/**/*.{js,ts,tsx}',
// TODO: Remove when used
'static/app/views/seerExplorer/contexts/**/*.{js,ts,tsx}',
];

const testingEntryPoints = [
Expand Down
1 change: 1 addition & 0 deletions src/sentry/analytics/events/ai_autofix_pr_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class AiAutofixPrEvent(analytics.Event):
run_id: int
integration: str
github_app: str
referrer: str | None = None


@analytics.eventclass("ai.autofix.pr.closed")
Expand Down
14 changes: 9 additions & 5 deletions src/sentry/api/serializers/models/organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
ROLLBACK_ENABLED_DEFAULT,
SAMPLING_MODE_DEFAULT,
SCRAPE_JAVASCRIPT_DEFAULT,
SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT,
SEER_DEFAULT_CODING_AGENT_DEFAULT,
TARGET_SAMPLE_RATE_DEFAULT,
ObjectStatus,
Expand Down Expand Up @@ -558,8 +559,9 @@ class DetailedOrganizationSerializerResponse(_DetailedOrganizationSerializerResp
defaultSeerScannerAutomation: bool
enableSeerEnhancedAlerts: bool
enableSeerCoding: bool
defaultCodingAgent: str | None
defaultCodingAgent: str
defaultCodingAgentIntegrationId: int | None
defaultAutomatedRunStoppingPoint: str
autoEnableCodeReview: bool
autoOpenPrs: bool
defaultCodeReviewTriggers: list[str]
Expand Down Expand Up @@ -734,12 +736,14 @@ def serialize( # type: ignore[override]
)
),
"defaultCodingAgent": obj.get_option(
"sentry:seer_default_coding_agent",
SEER_DEFAULT_CODING_AGENT_DEFAULT,
"sentry:seer_default_coding_agent", SEER_DEFAULT_CODING_AGENT_DEFAULT
),
"defaultCodingAgentIntegrationId": obj.get_option(
"sentry:seer_default_coding_agent_integration_id",
None,
"sentry:seer_default_coding_agent_integration_id", None
),
"defaultAutomatedRunStoppingPoint": obj.get_option(
"sentry:default_automated_run_stopping_point",
SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT,
),
"autoOpenPrs": bool(
obj.get_option(
Expand Down
3 changes: 2 additions & 1 deletion src/sentry/apidocs/examples/organization_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,8 +304,9 @@ class OrganizationExamples:
"enableSeerCoding": True,
"enableSeerEnhancedAlerts": True,
"autoOpenPrs": False,
"defaultCodingAgent": None,
"defaultCodingAgent": "seer",
"defaultCodingAgentIntegrationId": None,
"defaultAutomatedRunStoppingPoint": "code_changes",
"issueAlertsThreadFlag": True,
"metricAlertsThreadFlag": True,
"trustedRelays": [],
Expand Down
4 changes: 0 additions & 4 deletions src/sentry/conf/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1013,10 +1013,6 @@ def SOCIAL_AUTH_DEFAULT_USERNAME() -> str:
"task": "workflow_engine:sentry.workflow_engine.tasks.workflows.schedule_delayed_workflows",
"schedule": timedelta(seconds=15),
},
"prune-old-fire-history": {
"task": "workflow_engine:sentry.workflow_engine.tasks.cleanup.prune_old_fire_history",
"schedule": timedelta(minutes=2),
},
"resolve-stale-sourcemap-detectors": {
"task": "workflow_engine:sentry.processing_errors.tasks.resolve_stale_sourcemap_detectors",
"schedule": crontab("*/5", "*", "*", "*", "*"),
Expand Down
26 changes: 24 additions & 2 deletions src/sentry/core/endpoints/organization_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
ROLLBACK_ENABLED_DEFAULT,
SAMPLING_MODE_DEFAULT,
SCRAPE_JAVASCRIPT_DEFAULT,
SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT,
SEER_DEFAULT_CODING_AGENT_DEFAULT,
TARGET_SAMPLE_RATE_DEFAULT,
ObjectStatus,
Expand Down Expand Up @@ -254,12 +255,17 @@
None,
),
(
# Informs UI default for automated_run_stopping_point in project preferences
"autoOpenPrs",
"sentry:auto_open_prs",
bool,
AUTO_OPEN_PRS_DEFAULT,
),
(
"defaultAutomatedRunStoppingPoint",
"sentry:default_automated_run_stopping_point",
str,
SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT,
),
(
"autoEnableCodeReview",
"sentry:auto_enable_code_review",
Expand Down Expand Up @@ -371,8 +377,15 @@ class OrganizationSerializer(BaseOrganizationSerializer):
dashboardsAsyncQueueParallelLimit = serializers.IntegerField(required=False, min_value=1)
enableSeerEnhancedAlerts = serializers.BooleanField(required=False)
enableSeerCoding = serializers.BooleanField(required=False)
defaultCodingAgent = serializers.CharField(required=False, allow_null=True)
defaultCodingAgent = serializers.ChoiceField(
choices=["seer", "cursor", "claude_code", "cursor_background_agent", "claude_code_agent"],
required=False,
allow_null=True,
)
defaultCodingAgentIntegrationId = serializers.IntegerField(required=False, allow_null=True)
defaultAutomatedRunStoppingPoint = serializers.ChoiceField(
choices=["code_changes", "open_pr"], required=False
)
autoOpenPrs = serializers.BooleanField(required=False)
autoEnableCodeReview = serializers.BooleanField(required=False)
defaultCodeReviewTriggers = serializers.ListField(
Expand Down Expand Up @@ -401,6 +414,15 @@ def validate_relayPiiConfig(self, value):
organization = self.context["organization"]
return validate_pii_config_update(organization, value)

def validate_defaultCodingAgent(self, value: str | None) -> str:
if value is None:
return SEER_DEFAULT_CODING_AGENT_DEFAULT
coding_agent_aliases: dict[str, str] = {
"cursor": "cursor_background_agent",
"claude_code": "claude_code_agent",
}
return coding_agent_aliases.get(value, value)

def validate_defaultCodingAgentIntegrationId(self, value: int | None) -> int | None:
if value is None:
return None
Expand Down
4 changes: 2 additions & 2 deletions src/sentry/core/endpoints/team_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@
from sentry.models.team import Team
from sentry.seer.similarity.utils import (
project_is_seer_eligible,
set_default_project_auto_open_prs,
set_default_project_autofix_automation_tuning,
set_default_project_seer_preferences,
set_default_project_seer_scanner_automation,
)
from sentry.signals import project_created
Expand All @@ -56,7 +56,7 @@ def apply_default_project_settings(organization: Organization, project: Project)

set_default_project_autofix_automation_tuning(organization, project)
set_default_project_seer_scanner_automation(organization, project)
set_default_project_auto_open_prs(organization, project)
set_default_project_seer_preferences(organization, project)


class ProjectPostSerializer(serializers.Serializer):
Expand Down
37 changes: 37 additions & 0 deletions src/sentry/seer/autofix/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

from sentry import features, options, ratelimits
from sentry.constants import (
AUTO_OPEN_PRS_DEFAULT,
SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT,
DataCategory,
ObjectStatus,
Expand Down Expand Up @@ -42,6 +43,10 @@
SeerProjectRepository,
SeerProjectRepositoryBranchOverride,
)
from sentry.seer.models.seer_api_models import (
AutofixHandoffPoint,
SeerAutomationHandoffConfiguration,
)
from sentry.seer.signed_seer_api import SeerViewerContext, make_signed_seer_api_request
from sentry.utils.cache import cache
from sentry.utils.outcomes import Outcome, track_outcome
Expand Down Expand Up @@ -393,6 +398,38 @@ def default_seer_project_preference(project: Project) -> SeerProjectPreference:
)


def get_org_default_seer_automation_handoff(
organization: Organization,
) -> tuple[str, SeerAutomationHandoffConfiguration | None]:
"""Get the default stopping point and automation handoff for an organization."""
stopping_point = organization.get_option(
"sentry:default_automated_run_stopping_point", SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT
)

auto_open_prs = organization.get_option("sentry:auto_open_prs", AUTO_OPEN_PRS_DEFAULT)

automation_handoff: SeerAutomationHandoffConfiguration | None = None
coding_agent = organization.get_option("sentry:seer_default_coding_agent")
coding_agent_integration_id = organization.get_option(
"sentry:seer_default_coding_agent_integration_id"
)
if coding_agent and coding_agent != "seer" and coding_agent_integration_id is not None:
automation_handoff = SeerAutomationHandoffConfiguration(
handoff_point=AutofixHandoffPoint.ROOT_CAUSE,
target=coding_agent,
integration_id=coding_agent_integration_id,
auto_create_pr=auto_open_prs,
)
# If Seer agent and auto open PRs, we can run up to open_pr.
elif auto_open_prs:
stopping_point = "open_pr"
# If Seer agent and no auto open PRs, we shouldn't go past code_changes.
elif stopping_point == "open_pr":
stopping_point = "code_changes"

return stopping_point, automation_handoff


def get_project_seer_preferences(project_id: int) -> SeerRawPreferenceResponse:
"""
Fetch Seer project preferences from the Seer API.
Expand Down
Loading
Loading