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
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ class OrganizationDashboardGenerateEndpoint(OrganizationEndpoint):
permission_classes = (OrganizationDashboardGeneratePermission,)

def post(self, request: Request, organization: Organization) -> Response:
has_access, error = has_seer_access_with_detail(organization, request.user)
has_access, error = has_seer_access_with_detail(organization)
if not has_access:
raise PermissionDenied(error)

Expand Down
2 changes: 0 additions & 2 deletions src/sentry/features/temporary.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,6 @@ def register_temporary_features(manager: FeatureManager) -> None:
manager.add("organizations:explore-errors", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Enable returning the migrated discover queries in explore saved queries
manager.add("organizations:expose-migrated-discover-queries", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Enable GenAI features such as Autofix and Issue Summary
manager.add("organizations:gen-ai-features", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Enable organization investigation notebooks.
manager.add("organizations:investigations", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Enable the 'translate' functionality for GenAI on the explore > traces page
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def get(self, request: Request, organization: Organization) -> Response:
:auth: required
"""

if not has_seer_access(organization, actor=request.user):
if not has_seer_access(organization):
return Response(
{"detail": "AI categorization is not available for this organization."}, status=403
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def get(self, request: Request, organization: Organization) -> Response:
:auth: required
"""

if not has_seer_access(organization, actor=request.user):
if not has_seer_access(organization):
return Response(
{"detail": "AI summaries are not available for this organization."}, status=403
)
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/integrations/utils/external_issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ def maybe_generate_external_issue_details(
) -> GeneratedExternalIssueDetails:
organization = group.organization
empty_result = GeneratedExternalIssueDetails(title=None, description=None)
if not has_seer_access(organization, actor=user):
if not has_seer_access(organization):
return empty_result
if not features.has("organizations:external-issues-ai-generate", organization, actor=user):
return empty_result
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/replays/endpoints/project_replay_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ def has_replay_summary_access(self, project: Project, request: Request) -> bool:
project.organization,
actor=request.user,
)
and has_seer_access(project.organization, actor=request.user)
and has_seer_access(project.organization)
)

def get(self, request: Request, project: Project, replay_id: str) -> Response:
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/seer/agent/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ def __init__(
raise ValueError("category_key and category_value must be provided together")

# Validate base Seer access on init (agent-specific flag checks are done at the endpoint level)
has_access, error = has_seer_access_with_detail(organization, user)
has_access, error = has_seer_access_with_detail(organization)
if not has_access:
raise SeerPermissionError(error or "Access denied")

Expand Down
4 changes: 2 additions & 2 deletions src/sentry/seer/agent/client_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,8 +389,8 @@ def has_seer_agent_access_with_detail(
Returns:
tuple[bool, str | None]: (has_access, error_message)
"""
# Check base Seer access (gen-ai-features, hide_ai_features, acknowledgement)
has_access, error = has_seer_access_with_detail(organization, actor)
# Check base Seer access (self-hosted, hide_ai_features, acknowledgement)
has_access, error = has_seer_access_with_detail(organization)
if not has_access:
return False, error

Expand Down
6 changes: 3 additions & 3 deletions src/sentry/seer/autofix/issue_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
from sentry.seer.entrypoints.operator import SeerAutofixOperator
from sentry.seer.models import SummarizeIssueResponse
from sentry.seer.models.run import SeerRun, SeerRunMirrorStatus
from sentry.seer.seer_setup import has_seer_access
from sentry.seer.seer_setup import has_seer_access, is_seer_available
from sentry.seer.signed_seer_api import (
SeerViewerContext,
SummarizeIssueRequest,
Expand Down Expand Up @@ -585,8 +585,8 @@ def get_issue_summary(
"""
if user is None:
user = AnonymousUser()
if not features.has("organizations:gen-ai-features", group.organization, actor=user):
return {"detail": "Feature flag not enabled"}, 400
if not is_seer_available():
return {"detail": "Seer is not available on this installation."}, 400

if group.organization.get_option("sentry:hide_ai_features"):
return {"detail": "AI features are disabled for this organization."}, 403
Expand Down
5 changes: 3 additions & 2 deletions src/sentry/seer/autofix/trigger.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,15 @@ def get_seer_automation_ineligibility_reason(
group: Group,
) -> SeerAutomationIneligibilityReason | None:
"""Return the reason an issue is ineligible for Seer automation, or None if eligible."""
from sentry import features, quotas
from sentry import quotas
from sentry.constants import DataCategory
from sentry.seer.autofix.utils import is_issue_category_eligible
from sentry.seer.seer_setup import is_seer_available

if not is_issue_category_eligible(group):
return "not_eligible.issue_category_ineligible"

if not features.has("organizations:gen-ai-features", group.organization):
if not is_seer_available():
return "not_eligible.gen_ai_feature_disabled"

gen_ai_allowed = not group.organization.get_option("sentry:hide_ai_features")
Expand Down
4 changes: 2 additions & 2 deletions src/sentry/seer/endpoints/organization_seer_agent_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ def get(
"""
has_access, error = has_seer_agent_access_with_detail(organization, request.user)

has_seer_access, _ = has_seer_access_with_detail(organization, request.user)
has_seer_access, _ = has_seer_access_with_detail(organization)

if not has_access and not has_seer_access:
raise PermissionDenied(error)
Expand Down Expand Up @@ -263,7 +263,7 @@ def post(
"""
has_access, error = has_seer_agent_access_with_detail(organization, request.user)

has_seer_access, _ = has_seer_access_with_detail(organization, request.user)
has_seer_access, _ = has_seer_access_with_detail(organization)
# Orgs with Seer access can continue existing dashboard generate runs, but cannot start new runs from this endpoint.
can_continue_dashboards_generate_run = has_seer_access and run_id is not None

Expand Down
2 changes: 1 addition & 1 deletion src/sentry/seer/endpoints/search_agent_start.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ def post(self, request: Request, organization: Organization) -> Response:
status=status.HTTP_403_FORBIDDEN,
)

has_seer_access, detail = has_seer_access_with_detail(organization, actor=request.user)
has_seer_access, detail = has_seer_access_with_detail(organization)
if not has_seer_access:
return Response(
{"detail": detail},
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/seer/endpoints/search_agent_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def get(self, request: Request, organization: Organization, run_id: str) -> Resp
status=status.HTTP_403_FORBIDDEN,
)

has_seer_access, detail = has_seer_access_with_detail(organization, actor=request.user)
has_seer_access, detail = has_seer_access_with_detail(organization)
if not has_seer_access:
return Response(
{"detail": detail},
Expand Down
6 changes: 2 additions & 4 deletions src/sentry/seer/endpoints/trace_explorer_ai_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@
from rest_framework.request import Request
from rest_framework.response import Response

from sentry import features
from sentry.api.api_owners import ApiOwner
from sentry.api.api_publish_status import ApiPublishStatus
from sentry.api.base import cell_silo_endpoint
from sentry.api.bases import OrganizationEndpoint
from sentry.models.organization import Organization
from sentry.seer.endpoints.trace_explorer_ai_setup import OrganizationTraceExplorerAIPermission
from sentry.seer.models import SeerApiError
from sentry.seer.seer_setup import is_seer_available
from sentry.seer.signed_seer_api import (
SeerViewerContext,
TranslateQueryRequest,
Expand Down Expand Up @@ -97,9 +97,7 @@ def post(self, request: Request, organization: Organization) -> Response:
status=status.HTTP_403_FORBIDDEN,
)

if not features.has(
"organizations:gen-ai-features", organization=organization, actor=request.user
):
if not is_seer_available():
return Response(
{"detail": "Organization does not have access to this feature"},
status=status.HTTP_403_FORBIDDEN,
Expand Down
6 changes: 2 additions & 4 deletions src/sentry/seer/endpoints/trace_explorer_ai_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
from rest_framework.exceptions import ParseError
from rest_framework.response import Response

from sentry import features
from sentry.api.api_owners import ApiOwner
from sentry.api.api_publish_status import ApiPublishStatus
from sentry.api.base import cell_silo_endpoint
from sentry.api.bases import OrganizationEndpoint
from sentry.api.bases.organization import OrganizationPermission
from sentry.models.organization import Organization
from sentry.seer.models import SeerApiError
from sentry.seer.seer_setup import is_seer_available
from sentry.seer.signed_seer_api import (
CreateCacheRequest,
SeerViewerContext,
Expand Down Expand Up @@ -84,9 +84,7 @@ def post(self, request: Request, organization: Organization) -> Response:
status=status.HTTP_403_FORBIDDEN,
)

if not features.has(
"organizations:gen-ai-features", organization=organization, actor=request.user
):
if not is_seer_available():
return Response(
{"detail": "Organization does not have access to this feature"},
status=status.HTTP_403_FORBIDDEN,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ def post(self, request: Request, organization: Organization) -> Response:
status=status.HTTP_403_FORBIDDEN,
)

has_seer_access, detail = has_seer_access_with_detail(organization, actor=request.user)
has_seer_access, detail = has_seer_access_with_detail(organization)
if not has_seer_access:
return Response(
{"detail": detail},
Expand Down
19 changes: 2 additions & 17 deletions src/sentry/seer/seer_setup.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
from django.contrib.auth.models import AnonymousUser

from sentry import features
from sentry.models.organization import Organization
from sentry.organizations.services.organization.model import RpcOrganization
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


Expand All @@ -20,27 +16,16 @@ def is_seer_available() -> bool:
return not is_self_hosted()


def has_seer_access(
organization: Organization | RpcOrganization,
actor: User | AnonymousUser | RpcUser | None = None,
) -> bool:
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(organization: Organization | RpcOrganization) -> bool:
return is_seer_available() 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"

if organization.get_option("sentry:hide_ai_features"):
return False, "AI features are disabled for this organization."

Expand Down
36 changes: 5 additions & 31 deletions src/sentry/tasks/seer/explorer_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
EXPLORER_INDEX_DISPATCH_STEP = timedelta(seconds=37)

FEATURE_NAMES = [
"organizations:gen-ai-features",
"organizations:seer-explorer-index",
"organizations:seat-based-seer-enabled",
"organizations:seer-added",
Expand Down Expand Up @@ -73,43 +72,18 @@ def get_seer_explorer_enabled_projects() -> Generator[tuple[int, int]]:
if bool(project.organization.get_option("sentry:hide_ai_features")):
continue

is_eligible = False
with start_span(
op="seer_explorer_index.has_feature", name="seer_explorer_index.has_feature"
):
batch_result = features.batch_has(FEATURE_NAMES, organization=project.organization)

if batch_result:
org_key = f"organization:{project.organization.id}"
org_features = batch_result.get(org_key, {})
has_gen_ai = org_features.get("organizations:gen-ai-features", False)
has_explorer_index = org_features.get("organizations:seer-explorer-index", False)

if has_explorer_index and has_gen_ai:
is_eligible = True

has_seer_plan = org_features.get(
"organizations:seat-based-seer-enabled", False
) or org_features.get("organizations:seer-added", False)

if has_seer_plan and has_gen_ai:
is_eligible = True

org_features = batch_result.get(f"organization:{project.organization.id}", {})
else:
has_gen_ai = features.has("organizations:gen-ai-features", project.organization)
has_explorer_index = features.has(
"organizations:seer-explorer-index", project.organization
)

if has_explorer_index and has_gen_ai:
is_eligible = True

has_seer_plan = features.has(
"organizations:seat-based-seer-enabled", project.organization
) or features.has("organizations:seer-added", project.organization)
org_features = {
name: features.has(name, project.organization) for name in FEATURE_NAMES
}

if has_seer_plan and has_gen_ai:
is_eligible = True
is_eligible = any(org_features.get(name, False) for name in FEATURE_NAMES)

if not is_eligible:
continue
Expand Down
3 changes: 1 addition & 2 deletions src/sentry/tasks/seer/night_shift/cron.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@

BATCH_FEATURE_NAMES = [
"organizations:seer-night-shift",
"organizations:gen-ai-features",
]
PER_ORG_FEATURE_NAMES = [
# INTERNAL handlers aren't routed through batch_has_for_organizations,
Expand Down Expand Up @@ -610,7 +609,7 @@ def _get_eligible_orgs_from_batch(
for org in eligible:
if all(features.has(f, org) for f in PER_ORG_FEATURE_NAMES):
paid_eligible.append(org)
elif features.has("organizations:gen-ai-features", org) and is_free_cohort_org(org):
elif is_free_cohort_org(org):
free_cohort_eligible.append(org)

return paid_eligible + free_cohort_eligible
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/testutils/helpers/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ def send_github_webhook_event(
@override_settings(SENTRY_SELF_HOSTED=False)
class GitHubWebhookCodeReviewTestCase(GitHubWebhookTestCase):
# Code review features are org features as set in options automator
CODE_REVIEW_FEATURES = {"organizations:gen-ai-features", "organizations:code-review-beta"}
CODE_REVIEW_FEATURES = {"organizations:code-review-beta"}
# Options to set are regional options as set in options automator
OPTIONS_TO_SET: dict[str, Any] = {}
# Org options are org options as set via OrganizationOption.objects.set_value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,7 @@ def post(
request: Request,
organization: Organization,
) -> Response:
# Check if AI features are enabled (includes gen-ai-features flag + hide_ai_features opt-out)
if not has_seer_access(organization, actor=request.user):
if not has_seer_access(organization):
return self.respond(
{"detail": "AI features are not enabled for this organization"},
status=403,
Expand Down
Loading
Loading