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
6 changes: 5 additions & 1 deletion src/sentry/features/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ class FeatureHandler:

features: set[str] = set()

def __call__(self, feature: Feature, actor: User) -> bool | None:
def __call__(
self,
feature: Feature,
actor: User | RpcUser | AnonymousUser | None,
) -> bool | None:
if feature.name not in self.features:
return None

Expand Down
73 changes: 54 additions & 19 deletions src/sentry/features/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import abc
from collections import defaultdict
from collections.abc import Iterable, Sequence
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any

import sentry_sdk
Expand Down Expand Up @@ -36,6 +37,15 @@
logger = logging.getLogger(__name__)


@dataclass(frozen=True)
class _ProjectHandlerEvaluation:
"""Results from running registered handlers for one feature across projects."""

decisions: dict[Project, bool]
unresolved_projects: set[Project]
failed: bool


class RegisteredFeatureManager:
"""
Feature functions that are built around the need to register feature
Expand All @@ -58,7 +68,11 @@ def add_handler(self, handler: FeatureHandler) -> None:
for feature_name in handler.features:
self._handler_registry[feature_name].append(handler)

def _get_handler(self, feature: Feature, actor: User) -> bool | None:
def _get_handler(
self,
feature: Feature,
actor: User | RpcUser | AnonymousUser | None,
) -> bool | None:
for handler in self._handler_registry[feature.name]:
rv = handler(feature, actor)
if rv is not None:
Expand Down Expand Up @@ -103,39 +117,60 @@ def has_for_batch(
"""

result: dict[Project, bool | None] = {}
remaining = set(objects)
try:
evaluation = self._evaluate_handlers_for_projects(name, organization, objects, actor)
result.update(evaluation.decisions)

handlers = self._handler_registry[name]
if evaluation.failed:
return result

default_flag = settings.SENTRY_FEATURES.get(name, False)
for project in evaluation.unresolved_projects:
result[project] = default_flag
except Exception as e:
if in_random_rollout("features.error.capture_rate"):
sentry_sdk.capture_exception(e)

return result

def _evaluate_handlers_for_projects(
self,
feature_name: str,
organization: Organization,
projects: Iterable[Project],
actor: User | RpcUser | AnonymousUser | None,
) -> _ProjectHandlerEvaluation:
decisions: dict[Project, bool] = {}
unresolved_projects = set(projects)
try:
for handler in handlers:
if not remaining:
for handler in self._handler_registry[feature_name]:
if not unresolved_projects:
break

with start_span(
op="feature.has_for_batch.handler",
name=f"{type(handler).__name__} ({name})",
name=f"{type(handler).__name__} ({feature_name})",
) as span:
batch_size = len(remaining)
batch_size = len(unresolved_projects)
set_span_data(span, "Batch Size", batch_size)
set_span_data(span, "Feature Name", name)
set_span_data(span, "Feature Name", feature_name)
set_span_data(span, "Handler Type", type(handler).__name__)

batch = FeatureCheckBatch(self, name, organization, remaining, actor)
batch = FeatureCheckBatch(
self, feature_name, organization, unresolved_projects, actor
)
handler_result = handler.has_for_batch(batch)
for obj, flag in handler_result.items():
if flag is not None:
remaining.remove(obj)
result[obj] = flag
set_span_data(span, "Flags Found", batch_size - len(remaining))

default_flag = settings.SENTRY_FEATURES.get(name, False)
for obj in remaining:
result[obj] = default_flag
for project, value in handler_result.items():
if value is not None:
unresolved_projects.remove(project)
decisions[project] = value
set_span_data(span, "Flags Found", batch_size - len(unresolved_projects))
except Exception as e:
if in_random_rollout("features.error.capture_rate"):
sentry_sdk.capture_exception(e)
return _ProjectHandlerEvaluation(decisions, unresolved_projects, failed=True)

return result
return _ProjectHandlerEvaluation(decisions, unresolved_projects, failed=False)


FLAGPOLE_OPTION_PREFIX = "feature"
Expand Down
Loading