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
95 changes: 94 additions & 1 deletion src/sentry/api/serializers/rest_framework/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,77 @@ def is_table_display_type(display_type):
MAX_WIDGET_COLS = 6


_DEFAULT_CHART_AND_TABLE_TYPES: frozenset[int] = frozenset(
{
DashboardWidgetDisplayTypes.LINE_CHART,
DashboardWidgetDisplayTypes.AREA_CHART,
DashboardWidgetDisplayTypes.BAR_CHART,
DashboardWidgetDisplayTypes.TABLE,
DashboardWidgetDisplayTypes.BIG_NUMBER,
DashboardWidgetDisplayTypes.CATEGORICAL_BAR_CHART,
}
)


class DatasetConfig(TypedDict):
supported_display_types: frozenset[int]


# Per-dataset config mirroring the frontend dataset configs
# (``static/app/views/dashboards/datasetConfig/*.tsx``). A display type is
# allowed for a widget_type iff it appears in ``supported_display_types`` here.
DATASET_CONFIG: dict[int, DatasetConfig] = {
DashboardWidgetTypes.DISCOVER: {"supported_display_types": _DEFAULT_CHART_AND_TABLE_TYPES},
# ERROR_EVENTS is intentionally omitted: it's the ``create_widget`` default
# when a request omits widget_type, so any system display type a prebuilt
# config doesn't tag will land here. Without a config entry the validation
# falls through and lets the request pass.
DashboardWidgetTypes.TRANSACTION_LIKE: {
"supported_display_types": _DEFAULT_CHART_AND_TABLE_TYPES
},
DashboardWidgetTypes.RELEASE_HEALTH: {
"supported_display_types": _DEFAULT_CHART_AND_TABLE_TYPES
},
DashboardWidgetTypes.METRICS: {"supported_display_types": _DEFAULT_CHART_AND_TABLE_TYPES},
DashboardWidgetTypes.LOGS: {"supported_display_types": _DEFAULT_CHART_AND_TABLE_TYPES},
DashboardWidgetTypes.ISSUE: {
"supported_display_types": frozenset(
{
DashboardWidgetDisplayTypes.TABLE,
DashboardWidgetDisplayTypes.LINE_CHART,
DashboardWidgetDisplayTypes.AREA_CHART,
DashboardWidgetDisplayTypes.BAR_CHART,
}
)
},
DashboardWidgetTypes.SPANS: {
"supported_display_types": _DEFAULT_CHART_AND_TABLE_TYPES
| frozenset(
{
DashboardWidgetDisplayTypes.DETAILS,
DashboardWidgetDisplayTypes.SERVER_TREE,
# WHEEL is used by built-in performance-score widgets.
DashboardWidgetDisplayTypes.WHEEL,
}
)
},
DashboardWidgetTypes.TRACEMETRICS: {
"supported_display_types": frozenset(
{
DashboardWidgetDisplayTypes.LINE_CHART,
DashboardWidgetDisplayTypes.AREA_CHART,
DashboardWidgetDisplayTypes.BAR_CHART,
DashboardWidgetDisplayTypes.BIG_NUMBER,
DashboardWidgetDisplayTypes.CATEGORICAL_BAR_CHART,
}
)
},
DashboardWidgetTypes.PREPROD_APP_SIZE: {
"supported_display_types": frozenset({DashboardWidgetDisplayTypes.LINE_CHART})
},
}


class WidgetLayoutSerializer(CamelSnakeSerializer[Dashboard]):
"""Widget grid layout position and dimensions.

Expand Down Expand Up @@ -326,7 +397,24 @@ class DashboardWidgetSerializer(CamelSnakeSerializer[Dashboard]):
)

def validate_display_type(self, display_type):
return DashboardWidgetDisplayTypes.get_id_for_type_name(display_type)
display_type_id = DashboardWidgetDisplayTypes.get_id_for_type_name(display_type)

widget_type_name = self.context.get("widget_type")
if widget_type_name is not None and display_type_id is not None:
widget_type_id = DashboardWidgetTypes.get_id_for_type_name(widget_type_name)
config = DATASET_CONFIG.get(widget_type_id)
if config is not None and display_type_id not in config["supported_display_types"]:
supported_names = sorted(
DashboardWidgetDisplayTypes.get_type_name(d) or str(d)
for d in config["supported_display_types"]
)
raise serializers.ValidationError(
f"Display type '{display_type}' is not supported for the "
f"'{widget_type_name}' dataset. Supported display types: "
f"{', '.join(supported_names)}."
)

return display_type_id

def _validate_widget_type(self, data):
widget_type = DashboardWidgetTypes.get_id_for_type_name(data.get("widget_type"))
Expand Down Expand Up @@ -358,6 +446,11 @@ def to_internal_value(self, data):
queries_serializer = self.fields["queries"]
additional_context = {}

# Always reset; with ``many=True`` DRF reuses one child serializer
# instance across items, so stale values would otherwise leak between
# widgets in the same request.
self.context["widget_type"] = data.get("widget_type")

if data.get("display_type"):
additional_context["display_type"] = data.get("display_type")
if data.get("widget_type"):
Expand Down
4 changes: 0 additions & 4 deletions src/sentry/features/temporary.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,6 @@ def register_temporary_features(manager: FeatureManager) -> None:
manager.add("organizations:dashboards-basic", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True, default=True)
# Enables custom editable dashboards
manager.add("organizations:dashboards-edit", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True, default=True)
# Enable unfurling of dashboard widgets in Slack
manager.add("organizations:dashboards-widget-unfurl", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)
# Enable metrics enhanced performance for AM2+ customers as they transition from AM2 to AM3
manager.add("organizations:dashboards-metrics-transition", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Enable drilldown flow for dashboards
Expand Down Expand Up @@ -348,8 +346,6 @@ def register_temporary_features(manager: FeatureManager) -> None:
manager.add("organizations:insights-alerts", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Enable data browsing heat map widget
manager.add("organizations:data-browsing-heat-map-widget", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Enable data browsing widget unfurl
manager.add("organizations:data-browsing-widget-unfurl", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Enable public RPC endpoint for local seer development
manager.add("organizations:seer-public-rpc", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)
# Organizations on the old usage-based (v0) Seer plan
Expand Down
8 changes: 3 additions & 5 deletions src/sentry/hybridcloud/outbox/category.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ class OutboxCategory(IntEnum):
ISSUE_COMMENT_UPDATE = 34
EXTERNAL_ACTOR_UPDATE = 35

RELOCATION_EXPORT_REQUEST = 36 # no longer in use
RELOCATION_EXPORT_REPLY = 37 # no longer in use
UNUSED_FIVE = 36
UNUSED_SIX = 37

SEND_VERCEL_INVOICE = 38
FTC_CONSENT = 39
Expand Down Expand Up @@ -337,9 +337,7 @@ class OutboxScope(IntEnum):
)
SUBSCRIPTION_SCOPE = scope_categories(9, {OutboxCategory.SUBSCRIPTION_UPDATE})
# relocation scope is no longer in use.
RELOCATION_SCOPE = scope_categories(
10, {OutboxCategory.RELOCATION_EXPORT_REQUEST, OutboxCategory.RELOCATION_EXPORT_REPLY}
)
RELOCATION_SCOPE = scope_categories(10, {OutboxCategory.UNUSED_FIVE, OutboxCategory.UNUSED_SIX})
API_TOKEN_SCOPE = scope_categories(11, {OutboxCategory.API_TOKEN_UPDATE})
ACTION_SCOPE = scope_categories(12, {OutboxCategory.SENTRY_APP_NORMALIZE_ACTIONS})
SEER_SCOPE = scope_categories(13, {OutboxCategory.SEER_RUN_CREATE})
Expand Down
1 change: 1 addition & 0 deletions src/sentry/hybridcloud/rpc/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,7 @@ def list_all_service_method_signatures() -> Iterable[RpcMethodSignature]:
"sentry.notifications.services",
"sentry.organizations.services",
"sentry.projects.services",
"sentry.relocation.services",
"sentry.sentry_apps.services",
"sentry.users.services",
)
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/integrations/slack/unfurl/dashboards.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ def _unfurl_dashboards(
enabled_orgs = {
slug: org
for slug, org in orgs_by_slug.items()
if features.has("organizations:dashboards-widget-unfurl", org, actor=user)
if features.has("organizations:dashboards-basic", org, actor=user)
}
if not enabled_orgs:
return {}
Expand Down
3 changes: 1 addition & 2 deletions src/sentry/integrations/slack/unfurl/explore.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,11 +375,10 @@ def _unfurl_explore(
)
orgs_by_slug = {org.slug: org for org in organizations}

# Check if any org has the feature flag enabled before doing any work
enabled_orgs = {
slug: org
for slug, org in orgs_by_slug.items()
if features.has("organizations:data-browsing-widget-unfurl", org, actor=user)
if features.has("organizations:visibility-explore-view", org, actor=user)
}
if not enabled_orgs:
return {}
Expand Down
4 changes: 2 additions & 2 deletions src/sentry/integrations/slack/webhooks/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,8 +229,8 @@ def _get_unfurlable_links(

feature_flag = {
LinkType.DISCOVER: "organizations:discover-basic",
LinkType.EXPLORE: "organizations:data-browsing-widget-unfurl",
LinkType.DASHBOARDS: "organizations:dashboards-widget-unfurl",
LinkType.EXPLORE: "organizations:visibility-explore-view",
LinkType.DASHBOARDS: "organizations:dashboards-basic",
}.get(link_type)

if (
Expand Down
7 changes: 0 additions & 7 deletions src/sentry/options/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -3048,13 +3048,6 @@
flags=FLAG_SCALAR | FLAG_AUTOMATOR_MODIFIABLE,
)

register(
"relocation.outbox-orgslug.killswitch",
default=[],
type=Sequence,
flags=FLAG_AUTOMATOR_MODIFIABLE,
)

register(
"profiling.killswitch.ingest-profiles",
type=Sequence,
Expand Down
42 changes: 0 additions & 42 deletions src/sentry/receivers/outbox/cell.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

from django.dispatch import receiver

from sentry import options
from sentry.audit_log.services.log import AuditLogEvent, UserIpEvent, log_rpc_service
from sentry.auth.services.auth import auth_service
from sentry.auth.services.orgauthtoken import orgauthtoken_rpc_service
Expand All @@ -28,11 +27,9 @@
)
from sentry.integrations.services.integration import integration_service
from sentry.models.authproviderreplica import AuthProviderReplica
from sentry.models.files.utils import get_relocation_storage
from sentry.models.organization import Organization
from sentry.models.project import Project
from sentry.receivers.outbox import maybe_process_tombstone
from sentry.relocation.services.relocation_export.service import control_relocation_export_service
from sentry.seer.agent.client_utils import AgentChatRequest, make_agent_chat_request
from sentry.seer.autofix.utils import make_autofix_start_request
from sentry.seer.models.run import SeerRun, SeerRunMirrorStatus, SeerRunType
Expand Down Expand Up @@ -210,45 +207,6 @@ def process_disable_auth_provider(object_identifier: int, shard_identifier: int,
AuthProviderReplica.objects.filter(auth_provider_id=object_identifier).delete()


# See the comment on /src/sentry/relocation/tasks/process.py::uploading_start for a detailed description of
# how this outbox drain handler fits into the entire SAAS->SAAS relocation workflow.
@receiver(process_cell_outbox, sender=OutboxCategory.RELOCATION_EXPORT_REPLY)
def process_relocation_reply_with_export(payload: Any, **kwds):
uuid = payload["relocation_uuid"]
slug = payload["org_slug"]

killswitch_orgs = options.get("relocation.outbox-orgslug.killswitch")
if slug in killswitch_orgs:
logger.info(
"relocation.killswitch.org",
extra={
"org_slug": slug,
"relocation_uuid": uuid,
},
)
return

relocation_storage = get_relocation_storage()
path = f"runs/{uuid}/saas_to_saas_export/{slug}.tar"
try:
encrypted_bytes = relocation_storage.open(path)
except Exception:
raise FileNotFoundError(
"Could not open SaaS -> SaaS export in export-side relocation bucket."
)

with encrypted_bytes:
control_relocation_export_service.reply_with_export(
relocation_uuid=uuid,
requesting_region_name=payload["requesting_region_name"],
replying_region_name=payload["replying_region_name"],
org_slug=slug,
# TODO(azaslavsky): finish transfer from `encrypted_contents` -> `encrypted_bytes`.
encrypted_contents=None,
encrypted_bytes=[int(byte) for byte in encrypted_bytes.read()],
)


@receiver(process_cell_outbox, sender=OutboxCategory.SEER_RUN_CREATE)
def handle_seer_run_create(object_identifier: int, payload: Any, **kwds: Any) -> None:
try:
Expand Down
Empty file.
12 changes: 9 additions & 3 deletions src/sentry/relocation/services/relocation_export/impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@
CellRelocationExportService,
ControlRelocationExportService,
)
from sentry.relocation.tasks.process import fulfill_cross_region_export_request, uploading_complete
from sentry.relocation.tasks.transfer import process_relocation_transfer_control
from sentry.relocation.utils import RELOCATION_BLOB_SIZE, RELOCATION_FILE_TYPE
from sentry.utils.db import atomic_transaction

Expand All @@ -43,6 +41,8 @@ def request_new_export(
org_slug: str,
encrypt_with_public_key: bytes,
) -> None:
from sentry.relocation.tasks.process import fulfill_cross_region_export_request

logger_data = {
"uuid": relocation_uuid,
"requesting_region_name": requesting_region_name,
Expand Down Expand Up @@ -81,6 +81,8 @@ def reply_with_export(
# TODO(azaslavsky): finish transfer from `encrypted_contents` -> `encrypted_bytes`.
encrypted_contents: bytes | None = None,
) -> None:
from sentry.relocation.tasks.process import uploading_complete

with atomic_transaction(
using=(
router.db_for_write(Relocation),
Expand Down Expand Up @@ -127,7 +129,7 @@ def reply_with_export(

logger.info("SaaS -> SaaS relocation RelocationFile saved", extra=logger_data)

uploading_complete.apply_async(args=[relocation.uuid])
uploading_complete.apply_async(args=[str(relocation.uuid)])
logger.info("SaaS -> SaaS relocation next task scheduled", extra=logger_data)


Expand All @@ -141,6 +143,8 @@ def request_new_export(
org_slug: str,
encrypt_with_public_key: bytes,
) -> None:
from sentry.relocation.tasks.transfer import process_relocation_transfer_control

logger_data = {
"uuid": relocation_uuid,
"requesting_region_name": requesting_region_name,
Expand Down Expand Up @@ -173,6 +177,8 @@ def reply_with_export(
# TODO(azaslavsky): finish transfer from `encrypted_contents` -> `encrypted_bytes`.
encrypted_contents: bytes | None = None,
) -> None:
from sentry.relocation.tasks.transfer import process_relocation_transfer_control

logger_data = {
"uuid": relocation_uuid,
"requesting_region_name": requesting_region_name,
Expand Down
9 changes: 5 additions & 4 deletions src/sentry/relocation/tasks/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@
RegionRelocationTransfer,
RelocationTransferState,
)
from sentry.relocation.tasks.transfer import process_relocation_transfer_region
from sentry.relocation.services.relocation_export.service import (
control_relocation_export_service,
)
from sentry.relocation.utils import (
TASK_TO_STEP,
LoggingPrinter,
Expand Down Expand Up @@ -247,9 +249,6 @@ def uploading_start(uuid: str, replying_cell_name: str | None, org_slug: str | N
with the `Relocation` that originally triggered `uploading_start`, and the next task in the
sequence (`uploading_complete`) is scheduled.
"""
from sentry.relocation.services.relocation_export.service import (
control_relocation_export_service,
)

uuid = str(uuid)
(relocation, attempts_left) = start_relocation_task(
Expand Down Expand Up @@ -333,6 +332,8 @@ def fulfill_cross_region_export_request(
call is received with the encrypted export in tow, it will trigger the next step in the
`SAAS_TO_SAAS` relocation's pipeline, namely `uploading_complete`.
"""
from sentry.relocation.tasks.transfer import process_relocation_transfer_region

encrypt_with_public_key_bytes = base64.b64decode(encrypt_with_public_key.encode("utf8"))

logger_data = {
Expand Down
13 changes: 13 additions & 0 deletions src/sentry/seer/autofix/on_completion_hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,19 @@ def _maybe_continue_pipeline(
reached_stopping_point=reached_stopping_point,
)
)
logger.info(
"autofix.on_completion_hook.introspection",
extra={
"organization_id": organization.id,
"project_id": group.project_id,
"group_id": group.id,
"referrer": referrer.value,
"step": current_step.value,
"action": decision.action.value,
"reason": decision.reason,
"reached_stopping_point": reached_stopping_point,
},
)

if stopping_point is None or reached_stopping_point:
# We've reached the stopping point
Expand Down
Loading
Loading