From 35443937d752c1f4972b2d5352babbbecb35b0ea Mon Sep 17 00:00:00 2001 From: Giovanni M Guidini Date: Thu, 29 Oct 2026 10:39:39 +0100 Subject: [PATCH] feat(integrations): Add SCM milestone lifecycle metrics Separate internal SCM operations from customer-facing feature outcomes while preserving provider and parent interaction dimensions. Refs SCM-304 --- .../source_code_management/metrics.py | 53 +++++++++++++++++-- src/sentry/metrics/middleware.py | 4 ++ .../test_commit_context.py | 18 +++++++ tests/sentry/metrics/test_middleware.py | 11 ++++ 4 files changed, 81 insertions(+), 5 deletions(-) diff --git a/src/sentry/integrations/source_code_management/metrics.py b/src/sentry/integrations/source_code_management/metrics.py index 6ccb852b367c..747afc8f6f77 100644 --- a/src/sentry/integrations/source_code_management/metrics.py +++ b/src/sentry/integrations/source_code_management/metrics.py @@ -1,8 +1,8 @@ from collections.abc import Mapping from enum import StrEnum -from typing import Any +from typing import Any, Generic, TypeVar -from attr import dataclass +from attr import dataclass, field from sentry.integrations.base import IntegrationDomain from sentry.integrations.utils.metrics import IntegrationEventLifecycleMetric @@ -70,18 +70,36 @@ class SCMIntegrationInteractionType(StrEnum): GET_REPO_TREE = "get_repo_tree" +class SCMIntegrationMilestoneType(StrEnum): + """ + Internal operations performed while delivering an SCM integration feature. + + Values remain in SCMIntegrationInteractionType until their callers migrate to + SCMIntegrationMilestoneEvent. + """ + + CHECK_FILE = "check_file" + GET = "get" + GET_RATE_LIMIT = "get_rate_limit" + GET_REPOSITORIES = "get_repositories" + GET_REPO_TREE = "get_repo_tree" + + class SCMResolutionHaltReason(StrEnum): GROUP_LINK_ALREADY_EXISTS = "group_link_already_exists" ISSUE_ALREADY_RESOLVED = "issue_already_resolved" +SCMInteractionType = TypeVar("SCMInteractionType", bound=StrEnum) + + @dataclass -class SCMIntegrationInteractionEvent(IntegrationEventLifecycleMetric): +class _SCMIntegrationEvent(IntegrationEventLifecycleMetric, Generic[SCMInteractionType]): """ - An instance to be recorded of an SCM integration feature call. + Shared dimensions for an SCM integration lifecycle event. """ - interaction_type: SCMIntegrationInteractionType + interaction_type: SCMInteractionType provider_key: str integration_id: int | None = None organization_id: int | None = None @@ -105,6 +123,31 @@ def get_extras(self) -> Mapping[str, Any]: } +@dataclass +class SCMIntegrationInteractionEvent(_SCMIntegrationEvent[SCMIntegrationInteractionType]): + """ + An instance to be recorded of an SCM integration feature call. + """ + + +@dataclass +class SCMIntegrationMilestoneEvent(_SCMIntegrationEvent[SCMIntegrationMilestoneType]): + """ + An internal operation performed while delivering an SCM integration feature. + """ + + parent_interaction_type: SCMIntegrationInteractionType = field(kw_only=True) + + def get_metrics_domain(self) -> str: + return "milestone" + + def get_metric_tags(self) -> Mapping[str, str]: + return { + **super().get_metric_tags(), + "parent_interaction_type": str(self.parent_interaction_type), + } + + @dataclass class CommitContextIntegrationInteractionEvent(SCMIntegrationInteractionEvent): """ diff --git a/src/sentry/metrics/middleware.py b/src/sentry/metrics/middleware.py index 5a9849257c88..8857ea6bff69 100644 --- a/src/sentry/metrics/middleware.py +++ b/src/sentry/metrics/middleware.py @@ -26,6 +26,10 @@ "integrations.slo.success", "integrations.slo.halted", "integrations.slo.failure", + "integrations.milestone.started", + "integrations.milestone.success", + "integrations.milestone.halted", + "integrations.milestone.failure", ] ) diff --git a/tests/sentry/integrations/source_code_management/test_commit_context.py b/tests/sentry/integrations/source_code_management/test_commit_context.py index 8e3cfbfbe2eb..ce1fd5ab8af0 100644 --- a/tests/sentry/integrations/source_code_management/test_commit_context.py +++ b/tests/sentry/integrations/source_code_management/test_commit_context.py @@ -14,6 +14,8 @@ from sentry.integrations.source_code_management.metrics import ( SCMIntegrationInteractionEvent, SCMIntegrationInteractionType, + SCMIntegrationMilestoneEvent, + SCMIntegrationMilestoneType, ) from sentry.integrations.types import EventLifecycleOutcome from sentry.models.repository import Repository @@ -47,6 +49,22 @@ def test_scm_interaction_event_normalizes_provider( } +def test_scm_milestone_event_assigns_metric_key_and_tags() -> None: + event = SCMIntegrationMilestoneEvent( + interaction_type=SCMIntegrationMilestoneType.CHECK_FILE, + parent_interaction_type=SCMIntegrationInteractionType.GET_STACKTRACE_LINK, + provider_key="integrations:github", + ) + + assert event.get_metric_key(EventLifecycleOutcome.STARTED) == "integrations.milestone.started" + assert event.get_metric_tags() == { + "integration_domain": "source_code_management", + "integration_name": "github", + "interaction_type": "check_file", + "parent_interaction_type": "get_stacktrace_link", + } + + class MockCommitContextIntegration(CommitContextIntegration): """Mock implementation for testing""" diff --git a/tests/sentry/metrics/test_middleware.py b/tests/sentry/metrics/test_middleware.py index 45270dbdd516..80d410599488 100644 --- a/tests/sentry/metrics/test_middleware.py +++ b/tests/sentry/metrics/test_middleware.py @@ -32,6 +32,17 @@ def test_filter_tags_prod() -> None: } +def test_filter_tags_allows_milestone_integration_id() -> None: + tags = { + "integration_id": 42, + "interaction_type": "check_file", + "parent_interaction_type": "get_stacktrace_link", + } + + with override_settings(SENTRY_METRICS_DISALLOW_BAD_TAGS=True): + assert _filter_tags("integrations.milestone.started", tags) == tags + + def test_global() -> None: assert get_current_global_tags() == {}