Skip to content
Open
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
14 changes: 12 additions & 2 deletions src/sentry/seer/seer_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from sentry.seer.constants import SEER_GITLAB_SCM_PROVIDERS, SEER_SUPPORTED_SCM_PROVIDERS
from sentry.users.models.user import User
from sentry.users.services.user.model import RpcUser
from sentry.utils.settings import is_self_hosted


def get_supported_scm_providers(organization: Organization | None = None) -> list[str]:
Expand All @@ -15,19 +16,28 @@ def get_supported_scm_providers(organization: Organization | None = None) -> lis
return providers


def is_seer_available() -> bool:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just use not is_self_hosted() directly? More clear than using is_seer_available since that could stand for whether org can sign up for seer or not.

return not is_self_hosted()


def has_seer_access(
organization: Organization | RpcOrganization,
actor: User | AnonymousUser | RpcUser | None = None,
) -> bool:
return features.has("organizations:gen-ai-features", organization, actor=actor) and not bool(
organization.get_option("sentry:hide_ai_features")
return (
is_seer_available()
and features.has("organizations:gen-ai-features", organization, actor=actor)
and not bool(organization.get_option("sentry:hide_ai_features"))
)


def has_seer_access_with_detail(
organization: Organization | RpcOrganization,
actor: User | AnonymousUser | RpcUser | None = None,
) -> tuple[bool, str | None]:
if not is_seer_available():
return False, "Seer is not available on this installation."

if not features.has("organizations:gen-ai-features", organization, actor=actor):
return False, "Feature flag not enabled"

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {ApiQueryKey} from 'sentry/utils/api/apiQueryKey';
import {getApiUrl} from 'sentry/utils/api/getApiUrl';
import {useApiQuery, type UseApiQueryOptions} from 'sentry/utils/queryClient';
import {areAiFeaturesAllowed as computeAreAiFeaturesAllowed} from 'sentry/utils/seer/areAiFeaturesAllowed';
import {useOrganization} from 'sentry/utils/useOrganization';

interface OrganizationSeerSetupResponse {
Expand All @@ -24,8 +25,7 @@ export function useOrganizationSeerSetup(
) {
const organization = useOrganization();
const orgSlug = organization.slug;
const areAiFeaturesAllowed =
!organization.hideAiFeatures && organization.features.includes('gen-ai-features');
const areAiFeaturesAllowed = computeAreAiFeaturesAllowed(organization);

const queryData = useApiQuery<OrganizationSeerSetupResponse>(
makeOrganizationSeerSetupQueryKey(orgSlug),
Expand Down
40 changes: 40 additions & 0 deletions static/app/utils/seer/areAiFeaturesAllowed.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {OrganizationFixture} from 'sentry-fixture/organization';

import {ConfigStore} from 'sentry/stores/configStore';
import {areAiFeaturesAllowed} from 'sentry/utils/seer/areAiFeaturesAllowed';

describe('areAiFeaturesAllowed', () => {
beforeEach(() => {
ConfigStore.set('isSelfHosted', false);
});

it('allows when flagged, not hidden, and not self-hosted', () => {
const organization = OrganizationFixture({
features: ['gen-ai-features'],
hideAiFeatures: false,
});
expect(areAiFeaturesAllowed(organization)).toBe(true);
});

it('denies without the flag', () => {
const organization = OrganizationFixture({features: [], hideAiFeatures: false});
expect(areAiFeaturesAllowed(organization)).toBe(false);
});

it('denies when the org hides AI features', () => {
const organization = OrganizationFixture({
features: ['gen-ai-features'],
hideAiFeatures: true,
});
expect(areAiFeaturesAllowed(organization)).toBe(false);
});

it('denies on self-hosted', () => {
ConfigStore.set('isSelfHosted', true);
const organization = OrganizationFixture({
features: ['gen-ai-features'],
hideAiFeatures: false,
});
expect(areAiFeaturesAllowed(organization)).toBe(false);
});
});
12 changes: 12 additions & 0 deletions static/app/utils/seer/areAiFeaturesAllowed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {ConfigStore} from 'sentry/stores/configStore';
import type {Organization} from 'sentry/types/organization';

export function areAiFeaturesAllowed(
organization: Pick<Organization, 'features' | 'hideAiFeatures'>
): boolean {
return (
!ConfigStore.get('isSelfHosted') &&
!organization.hideAiFeatures &&
organization.features.includes('gen-ai-features')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the flag be removed here too?

);
}
4 changes: 2 additions & 2 deletions static/app/views/issueDetails/hooks/useAiConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {useAutofixSetup} from 'sentry/components/events/autofix/useAutofixSetup'
import type {Group} from 'sentry/types/group';
import type {Project} from 'sentry/types/project';
import {getConfigForIssueType} from 'sentry/utils/issueTypeConfig';
import {areAiFeaturesAllowed as computeAreAiFeaturesAllowed} from 'sentry/utils/seer/areAiFeaturesAllowed';
import {useOrganization} from 'sentry/utils/useOrganization';
import {useIsSampleEvent} from 'sentry/views/issueDetails/utils';

Expand Down Expand Up @@ -34,8 +35,7 @@ export const useAiConfig = (group: Group, project: Project): AiConfigResult => {

const issueTypeConfig = getConfigForIssueType(group, project);

const areAiFeaturesAllowed =
!organization.hideAiFeatures && organization.features.includes('gen-ai-features');
const areAiFeaturesAllowed = computeAreAiFeaturesAllowed(organization);

const isSummaryEnabled = issueTypeConfig.issueSummary.enabled;
const isAutofixEnabled = issueTypeConfig.autofix;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@
from typing import Any
from unittest.mock import ANY, MagicMock, patch

from django.test import override_settings

from sentry.dashboards.on_completion_hook import DashboardOnCompletionHook
from sentry.seer.models import SeerPermissionError
from sentry.testutils.cases import APITestCase
from sentry.testutils.helpers.features import with_feature


@override_settings(SENTRY_SELF_HOSTED=False)
@with_feature("organizations:gen-ai-features")
class OrganizationDashboardGenerateEndpointTest(APITestCase):
endpoint = "sentry-api-0-organization-dashboards-generate"
Expand Down
5 changes: 5 additions & 0 deletions tests/sentry/integrations/slack/test_message_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from typing import Any
from unittest.mock import MagicMock, Mock, patch

from django.test import override_settings

from sentry.grouping.grouptype import ErrorGroupType
from sentry.integrations.messaging.message_builder import (
build_attachment_text,
Expand Down Expand Up @@ -996,20 +998,23 @@ def _has_autofix_button(self, blocks: dict[str, Any]) -> bool:
return True
return False

@override_settings(SENTRY_SELF_HOSTED=False)
@patch("sentry.quotas.backend.check_seer_quota", return_value=True)
@with_feature({"organizations:gen-ai-features": True})
def test_autofix_button_shown_when_all_conditions_met(self, mock_quota: MagicMock) -> None:
group = self.create_group(project=self.project)
blocks = SlackIssuesMessageBuilder(group).build()
assert self._has_autofix_button(blocks)

@override_settings(SENTRY_SELF_HOSTED=False)
@patch("sentry.quotas.backend.check_seer_quota", return_value=True)
@with_feature({"organizations:gen-ai-features": True})
def test_autofix_button_hidden_on_unfurl(self, mock_quota: MagicMock) -> None:
group = self.create_group(project=self.project)
blocks = SlackIssuesMessageBuilder(group, is_unfurl=True).build()
assert not self._has_autofix_button(blocks)

@override_settings(SENTRY_SELF_HOSTED=False)
@patch("sentry.quotas.backend.check_seer_quota", return_value=True)
@with_feature({"organizations:gen-ai-features": True})
def test_autofix_button_hidden_when_no_other_actions(self, mock_quota: MagicMock) -> None:
Expand Down
2 changes: 2 additions & 0 deletions tests/sentry/integrations/slack/webhooks/events/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from unittest.mock import patch

import orjson
from django.test import override_settings

from sentry.testutils.cases import APITestCase
from sentry.testutils.helpers import install_slack
Expand Down Expand Up @@ -54,6 +55,7 @@ def build_test_block(link):
}


@override_settings(SENTRY_SELF_HOSTED=False)
class BaseEventTest(APITestCase):
def setUp(self) -> None:
super().setUp()
Expand Down
2 changes: 2 additions & 0 deletions tests/sentry/pr_metrics/test_webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from django.conf import settings
from django.core.cache import cache
from django.db import OperationalError
from django.test import override_settings

from sentry.analytics.events.pr_metrics_events import PrCloseMetricsEvent
from sentry.integrations.github.webhook import PullRequestEventWebhook
Expand Down Expand Up @@ -2172,6 +2173,7 @@ def test_check_suite_judge_in_progress_skips(self) -> None:
assert not PullRequestActivity.objects.filter(pull_request=self.pr).exists()


@override_settings(SENTRY_SELF_HOSTED=False)
@with_feature(["organizations:pr-metrics", "organizations:gen-ai-features"])
@cell_silo_test
class HandleWebhookForPrMetricsJudgeForwardTest(TestCase):
Expand Down
1 change: 1 addition & 0 deletions tests/sentry/seer/agent/test_client_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from sentry.viewer_context import ActorType, ViewerContext, viewer_context_scope


@override_settings(SENTRY_SELF_HOSTED=False)
class TestHasSeerAgentAccessWithDetail(TestCase):
def setUp(self) -> None:
super().setUp()
Expand Down
2 changes: 2 additions & 0 deletions tests/sentry/seer/autofix/test_autofix_agent.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from unittest.mock import MagicMock, patch

import pytest
from django.test import override_settings
from rest_framework.exceptions import PermissionDenied

from sentry.analytics.events.autofix_events import AiAutofixSolutionCompletedEvent
Expand Down Expand Up @@ -1813,6 +1814,7 @@ def test_trigger_coding_agent_handoff_resolves_default_branch_when_empty(
assert repos[0].branch_name == "main"


@override_settings(SENTRY_SELF_HOSTED=False)
class TestTriggerPushChanges(TestCase):
"""Tests for trigger_push_changes function."""

Expand Down
5 changes: 5 additions & 0 deletions tests/sentry/seer/autofix/test_issue_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import orjson
import pytest
from django.test import override_settings

from sentry.api.serializers.rest_framework.base import convert_dict_key_case, snake_to_camel_case
from sentry.issues.action_log.types import SYSTEM_ACTOR, ActionSource, TriggerAutofixAction
Expand Down Expand Up @@ -70,6 +71,7 @@ def test_post_process_kickoff_creates_system_activity(
)


@override_settings(SENTRY_SELF_HOSTED=False)
@with_feature("organizations:gen-ai-features")
class IssueSummaryTest(APITestCase, SnubaTestCase, OccurrenceTestMixin):
def setUp(self) -> None:
Expand Down Expand Up @@ -878,6 +880,7 @@ def test_stopping_point_mapping(self, score, expected):
assert _get_stopping_point_from_fixability(score) == expected


@override_settings(SENTRY_SELF_HOSTED=False)
@patch("sentry.seer.autofix.issue_summary.is_seer_seat_based_tier_enabled", return_value=True)
@with_feature({"organizations:gen-ai-features": True})
class TestRunAutomationStoppingPoint(APITestCase, SnubaTestCase):
Expand Down Expand Up @@ -1027,6 +1030,7 @@ def test_upper_bound_combinations(self, fixability, user_pref, expected):
assert result == expected


@override_settings(SENTRY_SELF_HOSTED=False)
@patch("sentry.seer.autofix.issue_summary.is_seer_seat_based_tier_enabled", return_value=True)
@with_feature({"organizations:gen-ai-features": True})
class TestRunAutomationWithUpperBound(APITestCase, SnubaTestCase):
Expand Down Expand Up @@ -1244,6 +1248,7 @@ def test_no_summary_in_cache_calls_seer_without_summary(self, mock_request):
assert "summary" not in payload


@override_settings(SENTRY_SELF_HOSTED=False)
@with_feature("organizations:gen-ai-features")
class TestIsGroupEligibleForAutomation(APITestCase, SnubaTestCase):
def setUp(self) -> None:
Expand Down
3 changes: 3 additions & 0 deletions tests/sentry/seer/endpoints/test_group_ai_autofix.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import uuid
from unittest.mock import ANY, Mock, call, patch

from django.test import override_settings

from sentry.integrations.services.integration import RpcIntegration
from sentry.integrations.types import ExternalProviders
from sentry.integrations.utils.github_permission_tiers import PR_ITERATION_TIER
Expand Down Expand Up @@ -57,6 +59,7 @@ def _user_context_length_calls(mock_distribution: Mock) -> list:
]


@override_settings(SENTRY_SELF_HOSTED=False)
@with_feature("organizations:gen-ai-features")
class GroupAutofixEndpointTest(APITestCase, SnubaTestCase):
def _get_url(self, group_id: int) -> str:
Expand Down
3 changes: 3 additions & 0 deletions tests/sentry/seer/endpoints/test_group_autofix_repos.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from unittest.mock import MagicMock, patch

from django.test import override_settings

from sentry.testutils.cases import APITestCase, SnubaTestCase
from sentry.testutils.helpers.features import with_feature


@override_settings(SENTRY_SELF_HOSTED=False)
@with_feature("organizations:gen-ai-features")
class GroupAutofixReposEndpointTest(APITestCase, SnubaTestCase):
def setUp(self) -> None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from unittest.mock import ANY, MagicMock, Mock, patch

import pytest
from django.test import override_settings

from sentry.seer.agent.client_models import (
MemoryBlock,
Expand All @@ -20,6 +21,7 @@


@with_feature("organizations:seer-explorer")
@override_settings(SENTRY_SELF_HOSTED=False)
@with_feature("organizations:gen-ai-features")
class OrganizationSeerAgentChatEndpointTest(APITestCase):
def setUp(self) -> None:
Expand Down Expand Up @@ -671,6 +673,7 @@ def test_outbox_path_flush_error_marks_failed_and_raises(


@with_feature("organizations:seer-explorer")
@override_settings(SENTRY_SELF_HOSTED=False)
@with_feature("organizations:gen-ai-features")
class OrganizationSeerAgentChatContextEngineTest(APITestCase):
"""End-to-end tests verifying is_context_engine_enabled reaches make_agent_chat_request."""
Expand Down
3 changes: 3 additions & 0 deletions tests/sentry/seer/endpoints/test_organization_seer_runs.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@
from typing import Any
from unittest.mock import patch

from django.test import override_settings

from sentry.seer.models.run import SeerRunPullRequest, SeerRunType
from sentry.seer.run_questions import QUESTIONS, question_hash
from sentry.testutils.cases import APITestCase
from sentry.testutils.helpers.datetime import before_now
from sentry.testutils.helpers.features import with_feature


@override_settings(SENTRY_SELF_HOSTED=False)
@with_feature("organizations:seer-explorer")
@with_feature("organizations:gen-ai-features")
class OrganizationSeerRunsEndpointTest(APITestCase):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from unittest.mock import patch

from django.test import override_settings

from sentry.hybridcloud.models.outbox import CellOutbox
from sentry.hybridcloud.outbox.category import OutboxCategory
from sentry.models.pullrequest import PullRequestLifecycleState
Expand All @@ -21,6 +23,7 @@
from sentry.testutils.factories import Factories


@override_settings(SENTRY_SELF_HOSTED=False)
class OrganizationSeerWorkflowsTest(APITestCase):
endpoint = "sentry-api-0-organization-seer-workflows"

Expand Down Expand Up @@ -428,12 +431,18 @@ def create_agent_workflow(
)


@override_settings(SENTRY_SELF_HOSTED=False)
class OrganizationSeerMonitorCleanupTest(APITestCase):
endpoint = "sentry-api-0-organization-seer-workflows"
method = "post"

def setUp(self) -> None:
super().setUp()
rate_limit_patcher = patch(
"sentry.middleware.ratelimit.get_rate_limit_value", return_value=None
)
rate_limit_patcher.start()
self.addCleanup(rate_limit_patcher.stop)
self.keep = self.create_detector(project=self.project, type="metric_issue", name="Keep")
self.duplicate = self.create_detector(
project=self.project, type="metric_issue", name="Copy"
Expand Down
1 change: 1 addition & 0 deletions tests/sentry/seer/endpoints/test_search_agent_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from sentry.testutils.cases import APITestCase


@override_settings(SENTRY_SELF_HOSTED=False)
@override_settings(SEER_AUTOFIX_URL="https://seer.example.com")
class SearchAgentStateEndpointTest(APITestCase):
endpoint = "sentry-api-0-search-agent-state"
Expand Down
Loading
Loading