From e0fe946508a19efaea6256660c68b79dfb579cdc Mon Sep 17 00:00:00 2001 From: Fabian Schindler Date: Thu, 10 Jul 2025 09:23:27 +0200 Subject: [PATCH 1/7] ref(analytics): Transform analytics events for TET-827 - Transform event classes to use @analytics.eventclass decorator - Transform analytics.record calls to use event class instances - Update imports as needed Closes TET-827 --- .../events/advanced_search_feature_gated.py | 11 +- src/sentry/analytics/events/alert_edited.py | 17 +- .../events/first_new_feedback_sent.py | 13 +- .../analytics/events/first_sourcemaps_sent.py | 17 +- src/sentry/analytics/events/issue_assigned.py | 13 +- src/sentry/analytics/events/issue_deleted.py | 17 +- .../analytics/events/issue_escalating.py | 15 +- .../analytics/events/issue_mark_reviewed.py | 13 +- src/sentry/analytics/events/issue_resolved.py | 23 +-- .../analytics/events/issue_unignored.py | 15 +- .../analytics/events/onboarding_complete.py | 11 +- src/sentry/analytics/events/plugin_enabled.py | 13 +- src/sentry/analytics/events/repo_linked.py | 15 +- src/sentry/integrations/analytics.py | 147 ++++++-------- .../integrations/tasks/create_comment.py | 13 +- .../tasks/sync_assignee_outbound.py | 10 +- .../integrations/tasks/sync_status_inbound.py | 20 +- .../tasks/sync_status_outbound.py | 10 +- .../integrations/tasks/update_comment.py | 13 +- .../endpoints/project_stacktrace_link.py | 20 +- .../backends/organization_onboarding_task.py | 10 +- .../providers/integration_repository.py | 10 +- src/sentry/receivers/features.py | 186 ++++++++++-------- src/sentry/receivers/onboarding.py | 28 +-- src/sentry/receivers/releases.py | 19 +- 25 files changed, 328 insertions(+), 351 deletions(-) diff --git a/src/sentry/analytics/events/advanced_search_feature_gated.py b/src/sentry/analytics/events/advanced_search_feature_gated.py index bcf98c3ad2d72c..3d33c0733cd7d0 100644 --- a/src/sentry/analytics/events/advanced_search_feature_gated.py +++ b/src/sentry/analytics/events/advanced_search_feature_gated.py @@ -1,14 +1,11 @@ from sentry import analytics +@analytics.eventclass("advanced_search.feature_gated") class AdvancedSearchFeatureGateEvent(analytics.Event): - type = "advanced_search.feature_gated" - - attributes = ( - analytics.Attribute("user_id", required=False), - analytics.Attribute("default_user_id"), - analytics.Attribute("organization_id"), - ) + user_id: str | None = None + default_user_id: str + organization_id: str analytics.register(AdvancedSearchFeatureGateEvent) diff --git a/src/sentry/analytics/events/alert_edited.py b/src/sentry/analytics/events/alert_edited.py index 6ffe2cc8d6e09b..a54c6e35c92846 100644 --- a/src/sentry/analytics/events/alert_edited.py +++ b/src/sentry/analytics/events/alert_edited.py @@ -1,17 +1,14 @@ from sentry import analytics +@analytics.eventclass("alert.edited") class AlertEditedEvent(analytics.Event): - type = "alert.edited" - - attributes = ( - analytics.Attribute("user_id", required=False), - analytics.Attribute("default_user_id"), - analytics.Attribute("organization_id"), - analytics.Attribute("rule_id"), - analytics.Attribute("rule_type"), - analytics.Attribute("is_api_token"), - ) + user_id: str | None = None + default_user_id: str + organization_id: str + rule_id: str + rule_type: str + is_api_token: str analytics.register(AlertEditedEvent) diff --git a/src/sentry/analytics/events/first_new_feedback_sent.py b/src/sentry/analytics/events/first_new_feedback_sent.py index 33fa5a926c1cbd..9576a21b2732ed 100644 --- a/src/sentry/analytics/events/first_new_feedback_sent.py +++ b/src/sentry/analytics/events/first_new_feedback_sent.py @@ -1,15 +1,12 @@ from sentry import analytics +@analytics.eventclass("first_new_feedback.sent") class FirstNewFeedbackSentEvent(analytics.Event): - type = "first_new_feedback.sent" - - attributes = ( - analytics.Attribute("organization_id"), - analytics.Attribute("project_id"), - analytics.Attribute("platform", required=False), - analytics.Attribute("user_id", required=False), - ) + organization_id: str + project_id: str + platform: str | None = None + user_id: str | None = None analytics.register(FirstNewFeedbackSentEvent) diff --git a/src/sentry/analytics/events/first_sourcemaps_sent.py b/src/sentry/analytics/events/first_sourcemaps_sent.py index 8ac85faefaafa4..9b7f47f70c2775 100644 --- a/src/sentry/analytics/events/first_sourcemaps_sent.py +++ b/src/sentry/analytics/events/first_sourcemaps_sent.py @@ -1,17 +1,14 @@ from sentry import analytics +@analytics.eventclass("first_sourcemaps.sent") class FirstSourcemapsSentEvent(analytics.Event): - type = "first_sourcemaps.sent" - - attributes = ( - analytics.Attribute("user_id"), - analytics.Attribute("organization_id"), - analytics.Attribute("project_id"), - analytics.Attribute("platform", required=False), - analytics.Attribute("url", required=False), - analytics.Attribute("project_platform", required=False), - ) + user_id: str + organization_id: str + project_id: str + platform: str | None = None + url: str | None = None + project_platform: str | None = None class FirstSourcemapsSentEventForProject(FirstSourcemapsSentEvent): diff --git a/src/sentry/analytics/events/issue_assigned.py b/src/sentry/analytics/events/issue_assigned.py index 5224051ffb1510..c2f93fbf65effb 100644 --- a/src/sentry/analytics/events/issue_assigned.py +++ b/src/sentry/analytics/events/issue_assigned.py @@ -1,15 +1,12 @@ from sentry import analytics +@analytics.eventclass("issue.assigned") class IssueAssignedEvent(analytics.Event): - type = "issue.assigned" - - attributes = ( - analytics.Attribute("user_id", required=False), - analytics.Attribute("default_user_id"), - analytics.Attribute("organization_id"), - analytics.Attribute("group_id"), - ) + user_id: str | None = None + default_user_id: str + organization_id: str + group_id: str analytics.register(IssueAssignedEvent) diff --git a/src/sentry/analytics/events/issue_deleted.py b/src/sentry/analytics/events/issue_deleted.py index 25e477bd984985..9ad6af4f2c97a8 100644 --- a/src/sentry/analytics/events/issue_deleted.py +++ b/src/sentry/analytics/events/issue_deleted.py @@ -1,17 +1,14 @@ from sentry import analytics +@analytics.eventclass("issue.deleted") class IssueDeletedEvent(analytics.Event): - type = "issue.deleted" - - attributes = ( - analytics.Attribute("group_id"), - analytics.Attribute("delete_type"), - analytics.Attribute("organization_id"), - analytics.Attribute("project_id"), - analytics.Attribute("user_id", required=False), - analytics.Attribute("default_user_id"), - ) + group_id: str + delete_type: str + organization_id: str + project_id: str + user_id: str | None = None + default_user_id: str analytics.register(IssueDeletedEvent) diff --git a/src/sentry/analytics/events/issue_escalating.py b/src/sentry/analytics/events/issue_escalating.py index ea1c5a22e0f1ab..26cc2b95e859d0 100644 --- a/src/sentry/analytics/events/issue_escalating.py +++ b/src/sentry/analytics/events/issue_escalating.py @@ -1,16 +1,13 @@ from sentry import analytics +@analytics.eventclass("issue.escalating") class IssueEscalatingEvent(analytics.Event): - type = "issue.escalating" - - attributes = ( - analytics.Attribute("organization_id", type=int), - analytics.Attribute("project_id", type=int), - analytics.Attribute("group_id"), - analytics.Attribute("event_id", required=False), - analytics.Attribute("was_until_escalating"), - ) + organization_id: int + project_id: int + group_id: str + event_id: str | None = None + was_until_escalating: str analytics.register(IssueEscalatingEvent) diff --git a/src/sentry/analytics/events/issue_mark_reviewed.py b/src/sentry/analytics/events/issue_mark_reviewed.py index 9b51f4865e9668..8172321c9b9a12 100644 --- a/src/sentry/analytics/events/issue_mark_reviewed.py +++ b/src/sentry/analytics/events/issue_mark_reviewed.py @@ -1,15 +1,12 @@ from sentry import analytics +@analytics.eventclass("issue.mark_reviewed") class IssueMarkReviewedEvent(analytics.Event): - type = "issue.mark_reviewed" - - attributes = ( - analytics.Attribute("user_id", required=False), - analytics.Attribute("default_user_id"), - analytics.Attribute("organization_id"), - analytics.Attribute("group_id"), - ) + user_id: str | None = None + default_user_id: str + organization_id: str + group_id: str analytics.register(IssueMarkReviewedEvent) diff --git a/src/sentry/analytics/events/issue_resolved.py b/src/sentry/analytics/events/issue_resolved.py index 89ffb2de004704..c102e1349281d1 100644 --- a/src/sentry/analytics/events/issue_resolved.py +++ b/src/sentry/analytics/events/issue_resolved.py @@ -1,20 +1,17 @@ from sentry import analytics +@analytics.eventclass("issue.resolved") class IssueResolvedEvent(analytics.Event): - type = "issue.resolved" - - attributes = ( - analytics.Attribute("user_id", required=False), - analytics.Attribute("project_id", required=False), - analytics.Attribute("default_user_id"), - analytics.Attribute("organization_id"), - analytics.Attribute("group_id"), - analytics.Attribute("resolution_type"), - # TODO: make required once we validate that all events have this - analytics.Attribute("issue_category", required=False), - analytics.Attribute("issue_type", required=False), - ) + user_id: str | None = None + project_id: str | None = None + default_user_id: str + organization_id: str + group_id: str + resolution_type: str + # TODO: make required once we validate that all events have this + issue_category: str | None = None + issue_type: str | None = None analytics.register(IssueResolvedEvent) diff --git a/src/sentry/analytics/events/issue_unignored.py b/src/sentry/analytics/events/issue_unignored.py index 83e7e9816fe0fc..f769da561413c3 100644 --- a/src/sentry/analytics/events/issue_unignored.py +++ b/src/sentry/analytics/events/issue_unignored.py @@ -1,16 +1,13 @@ from sentry import analytics +@analytics.eventclass("issue.unignored") class IssueUnignoredEvent(analytics.Event): - type = "issue.unignored" - - attributes = ( - analytics.Attribute("user_id", type=int, required=False), - analytics.Attribute("default_user_id", type=int), - analytics.Attribute("organization_id", type=int), - analytics.Attribute("group_id"), - analytics.Attribute("transition_type"), - ) + user_id: int | None = None + default_user_id: int + organization_id: int + group_id: str + transition_type: str analytics.register(IssueUnignoredEvent) diff --git a/src/sentry/analytics/events/onboarding_complete.py b/src/sentry/analytics/events/onboarding_complete.py index 59b2924442aa84..2d10a7d48284e7 100644 --- a/src/sentry/analytics/events/onboarding_complete.py +++ b/src/sentry/analytics/events/onboarding_complete.py @@ -1,14 +1,11 @@ from sentry import analytics +@analytics.eventclass("onboarding.complete") class OnboardingCompleteEvent(analytics.Event): - type = "onboarding.complete" - - attributes = ( - analytics.Attribute("user_id"), - analytics.Attribute("organization_id"), - analytics.Attribute("referrer"), - ) + user_id: str + organization_id: str + referrer: str analytics.register(OnboardingCompleteEvent) diff --git a/src/sentry/analytics/events/plugin_enabled.py b/src/sentry/analytics/events/plugin_enabled.py index 8144b691a10521..97aca875e8df00 100644 --- a/src/sentry/analytics/events/plugin_enabled.py +++ b/src/sentry/analytics/events/plugin_enabled.py @@ -1,15 +1,12 @@ from sentry import analytics +@analytics.eventclass("plugin.enabled") class PluginEnabledEvent(analytics.Event): - type = "plugin.enabled" - - attributes = ( - analytics.Attribute("user_id"), - analytics.Attribute("organization_id"), - analytics.Attribute("project_id"), - analytics.Attribute("plugin"), - ) + user_id: str + organization_id: str + project_id: str + plugin: str analytics.register(PluginEnabledEvent) diff --git a/src/sentry/analytics/events/repo_linked.py b/src/sentry/analytics/events/repo_linked.py index 270f2055f0c06b..4342dfaf92a99f 100644 --- a/src/sentry/analytics/events/repo_linked.py +++ b/src/sentry/analytics/events/repo_linked.py @@ -1,16 +1,13 @@ from sentry import analytics +@analytics.eventclass("repo.linked") class RepoLinkedEvent(analytics.Event): - type = "repo.linked" - - attributes = ( - analytics.Attribute("user_id", required=False), - analytics.Attribute("default_user_id"), - analytics.Attribute("organization_id"), - analytics.Attribute("repository_id"), - analytics.Attribute("provider"), - ) + user_id: str | None = None + default_user_id: str + organization_id: str + repository_id: str + provider: str analytics.register(RepoLinkedEvent) diff --git a/src/sentry/integrations/analytics.py b/src/sentry/integrations/analytics.py index 2f155d92537095..8578dbe81d95af 100644 --- a/src/sentry/integrations/analytics.py +++ b/src/sentry/integrations/analytics.py @@ -1,127 +1,94 @@ from sentry import analytics +@analytics.eventclass("integration.added") class IntegrationAddedEvent(analytics.Event): - type = "integration.added" - - attributes = ( - analytics.Attribute("provider"), - analytics.Attribute("id"), - analytics.Attribute("organization_id"), - analytics.Attribute("user_id", required=False), - analytics.Attribute("default_user_id"), - ) + provider: str + id: str + organization_id: str + user_id: str | None = None + default_user_id: str +@analytics.eventclass("integration.disabled.notified") class IntegrationDisabledNotified(analytics.Event): - type = "integration.disabled.notified" - - attributes = ( - analytics.Attribute("organization_id"), - analytics.Attribute("provider"), - analytics.Attribute("integration_type"), - analytics.Attribute("integration_id"), - analytics.Attribute("user_id", required=False), - ) + organization_id: str + provider: str + integration_type: str + integration_id: str + user_id: str | None = None +@analytics.eventclass("integration.issue.created") class IntegrationIssueCreatedEvent(analytics.Event): - type = "integration.issue.created" - - attributes = ( - analytics.Attribute("provider"), - analytics.Attribute("id"), - analytics.Attribute("organization_id"), - analytics.Attribute("user_id", required=False), - analytics.Attribute("default_user_id"), - ) + provider: str + id: str + organization_id: str + user_id: str | None = None + default_user_id: str +@analytics.eventclass("integration.issue.linked") class IntegrationIssueLinkedEvent(analytics.Event): - type = "integration.issue.linked" - - attributes = ( - analytics.Attribute("provider"), - analytics.Attribute("id"), - analytics.Attribute("organization_id"), - analytics.Attribute("user_id", required=False), - analytics.Attribute("default_user_id"), - ) + provider: str + id: str + organization_id: str + user_id: str | None = None + default_user_id: str +@analytics.eventclass("integration.issue.status.synced") class IntegrationIssueStatusSyncedEvent(analytics.Event): - type = "integration.issue.status.synced" - - attributes = ( - analytics.Attribute("provider"), - analytics.Attribute("id"), - analytics.Attribute("organization_id"), - ) + provider: str + id: str + organization_id: str +@analytics.eventclass("integration.issue.assignee.synced") class IntegrationIssueAssigneeSyncedEvent(analytics.Event): - type = "integration.issue.assignee.synced" - - attributes = ( - analytics.Attribute("provider"), - analytics.Attribute("id"), - analytics.Attribute("organization_id"), - ) + provider: str + id: str + organization_id: str +@analytics.eventclass("integration.issue.comments.synced") class IntegrationIssueCommentsSyncedEvent(analytics.Event): - type = "integration.issue.comments.synced" - - attributes = ( - analytics.Attribute("provider"), - analytics.Attribute("id"), - analytics.Attribute("organization_id"), - ) + provider: str + id: str + organization_id: str +@analytics.eventclass("integration.repo.added") class IntegrationRepoAddedEvent(analytics.Event): - type = "integration.repo.added" - - attributes = ( - analytics.Attribute("provider"), - analytics.Attribute("id"), - analytics.Attribute("organization_id"), - ) + provider: str + id: str + organization_id: str +@analytics.eventclass("integration.resolve.commit") class IntegrationResolveCommitEvent(analytics.Event): - type = "integration.resolve.commit" - - attributes = ( - analytics.Attribute("provider"), - analytics.Attribute("id"), - analytics.Attribute("organization_id"), - ) + provider: str + id: str + organization_id: str +@analytics.eventclass("integration.resolve.pr") class IntegrationResolvePREvent(analytics.Event): - type = "integration.resolve.pr" - - attributes = ( - analytics.Attribute("provider"), - analytics.Attribute("id"), - analytics.Attribute("organization_id"), - ) + provider: str + id: str + organization_id: str +@analytics.eventclass("integration.stacktrace.linked") class IntegrationStacktraceLinkEvent(analytics.Event): - type = "integration.stacktrace.linked" - - attributes = ( - analytics.Attribute("provider"), - analytics.Attribute("config_id"), - analytics.Attribute("project_id"), - analytics.Attribute("organization_id"), - analytics.Attribute("filepath"), - analytics.Attribute("status"), - analytics.Attribute("link_fetch_iterations"), - analytics.Attribute("platform", required=False), - ) + provider: str + config_id: str + project_id: str + organization_id: str + filepath: str + status: str + link_fetch_iterations: str + platform: str | None = None def register_analytics() -> None: diff --git a/src/sentry/integrations/tasks/create_comment.py b/src/sentry/integrations/tasks/create_comment.py index 370bbc03533395..9b5b7a79bbe2c9 100644 --- a/src/sentry/integrations/tasks/create_comment.py +++ b/src/sentry/integrations/tasks/create_comment.py @@ -1,4 +1,5 @@ from sentry import analytics +from sentry.integrations.analytics import IntegrationIssueCommentsSyncedEvent from sentry.integrations.models.external_issue import ExternalIssue from sentry.integrations.models.integration import Integration from sentry.integrations.source_code_management.metrics import ( @@ -68,10 +69,10 @@ def create_comment(external_issue_id: int, user_id: int, group_note_id: int) -> note.data["external_id"] = installation.get_comment_id(comment) note.save() analytics.record( - # TODO(lb): this should be changed and/or specified? - "integration.issue.comments.synced", - provider=installation.model.provider, - id=installation.model.id, - organization_id=external_issue.organization_id, - user_id=user_id, + IntegrationIssueCommentsSyncedEvent( + provider=installation.model.provider, + id=installation.model.id, + organization_id=external_issue.organization_id, + user_id=user_id, + ) ) diff --git a/src/sentry/integrations/tasks/sync_assignee_outbound.py b/src/sentry/integrations/tasks/sync_assignee_outbound.py index c16d51080c7a59..c5c98c0b7edf45 100644 --- a/src/sentry/integrations/tasks/sync_assignee_outbound.py +++ b/src/sentry/integrations/tasks/sync_assignee_outbound.py @@ -3,6 +3,7 @@ from sentry import analytics, features from sentry.constants import ObjectStatus from sentry.exceptions import InvalidConfiguration +from sentry.integrations.analytics import IntegrationIssueAssigneeSyncedEvent from sentry.integrations.errors import OrganizationIntegrationNotFound from sentry.integrations.models.external_issue import ExternalIssue from sentry.integrations.models.integration import Integration @@ -93,10 +94,11 @@ def sync_assignee_outbound( external_issue, user, assign=assign, assignment_source=parsed_assignment_source ) analytics.record( - "integration.issue.assignee.synced", - provider=integration.provider, - id=integration.id, - organization_id=external_issue.organization_id, + IntegrationIssueAssigneeSyncedEvent( + provider=integration.provider, + id=integration.id, + organization_id=external_issue.organization_id, + ) ) except (OrganizationIntegrationNotFound, ApiUnauthorized, InvalidConfiguration) as e: lifecycle.record_halt(halt_reason=e) diff --git a/src/sentry/integrations/tasks/sync_status_inbound.py b/src/sentry/integrations/tasks/sync_status_inbound.py index c27967d4df99bb..41893edb498947 100644 --- a/src/sentry/integrations/tasks/sync_status_inbound.py +++ b/src/sentry/integrations/tasks/sync_status_inbound.py @@ -7,6 +7,7 @@ from django.utils import timezone as django_timezone from sentry import analytics +from sentry.analytics.events.issue_resolved import IssueResolvedEvent from sentry.api.helpers.group_index.update import get_current_release_version_of_group from sentry.constants import ObjectStatus from sentry.integrations.models.integration import Integration @@ -293,15 +294,16 @@ def sync_status_inbound( ) analytics.record( - "issue.resolved", - project_id=group.project.id, - default_user_id="Sentry Jira", - organization_id=organization_id, - group_id=group.id, - resolution_type="with_third_party_app", - provider=provider.key, - issue_type=group.issue_type.slug, - issue_category=group.issue_category.name.lower(), + IssueResolvedEvent( + project_id=group.project.id, + default_user_id="Sentry Jira", + organization_id=organization_id, + group_id=group.id, + resolution_type="with_third_party_app", + provider=provider.key, + issue_type=group.issue_type.slug, + issue_category=group.issue_category.name.lower(), + ) ) elif action == ResolveSyncAction.UNRESOLVE: diff --git a/src/sentry/integrations/tasks/sync_status_outbound.py b/src/sentry/integrations/tasks/sync_status_outbound.py index 1cea6900c38e04..5500f7abc42ac8 100644 --- a/src/sentry/integrations/tasks/sync_status_outbound.py +++ b/src/sentry/integrations/tasks/sync_status_outbound.py @@ -1,6 +1,7 @@ from sentry import analytics, features from sentry.constants import ObjectStatus from sentry.exceptions import InvalidIdentity +from sentry.integrations.analytics import IntegrationIssueStatusSyncedEvent from sentry.integrations.base import IntegrationInstallation from sentry.integrations.errors import OrganizationIntegrationNotFound from sentry.integrations.models.external_issue import ExternalIssue @@ -84,10 +85,11 @@ def sync_status_outbound(group_id: int, external_issue_id: int) -> bool | None: ) analytics.record( - "integration.issue.status.synced", - provider=integration.provider, - id=integration.id, - organization_id=external_issue.organization_id, + IntegrationIssueStatusSyncedEvent( + provider=integration.provider, + id=integration.id, + organization_id=external_issue.organization_id, + ) ) except ( IntegrationFormError, diff --git a/src/sentry/integrations/tasks/update_comment.py b/src/sentry/integrations/tasks/update_comment.py index 25a1ebc11c8ca5..3f79a1db86c29a 100644 --- a/src/sentry/integrations/tasks/update_comment.py +++ b/src/sentry/integrations/tasks/update_comment.py @@ -1,4 +1,5 @@ from sentry import analytics +from sentry.integrations.analytics import IntegrationIssueCommentsSyncedEvent from sentry.integrations.models.external_issue import ExternalIssue from sentry.integrations.models.integration import Integration from sentry.integrations.source_code_management.metrics import ( @@ -68,10 +69,10 @@ def update_comment(external_issue_id: int, user_id: int, group_note_id: int) -> ) installation.update_comment(external_issue.key, user_id, note) analytics.record( - # TODO(lb): this should be changed and/or specified? - "integration.issue.comments.synced", - provider=installation.model.provider, - id=installation.model.id, - organization_id=external_issue.organization_id, - user_id=user_id, + IntegrationIssueCommentsSyncedEvent( + provider=installation.model.provider, + id=installation.model.id, + organization_id=external_issue.organization_id, + user_id=user_id, + ) ) diff --git a/src/sentry/issues/endpoints/project_stacktrace_link.py b/src/sentry/issues/endpoints/project_stacktrace_link.py index 5fc9d9a082e7af..be194b8b1fd285 100644 --- a/src/sentry/issues/endpoints/project_stacktrace_link.py +++ b/src/sentry/issues/endpoints/project_stacktrace_link.py @@ -14,6 +14,7 @@ from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.serializers import serialize +from sentry.integrations.analytics import IntegrationStacktraceLinkEvent from sentry.integrations.api.serializers.models.integration import IntegrationSerializer from sentry.integrations.base import IntegrationFeatures from sentry.integrations.services.integration import integration_service @@ -175,15 +176,16 @@ def get(self, request: Request, project: Project) -> Response: if result["current_config"] and serialized_config: analytics.record( - "integration.stacktrace.linked", - provider=serialized_config["provider"]["key"], - config_id=serialized_config["id"], - project_id=project.id, - organization_id=project.organization_id, - filepath=filepath, - status=error or "success", - link_fetch_iterations=result["iteration_count"], - platform=ctx["platform"], + IntegrationStacktraceLinkEvent( + provider=serialized_config["provider"]["key"], + config_id=serialized_config["id"], + project_id=project.id, + organization_id=project.organization_id, + filepath=filepath, + status=error or "success", + link_fetch_iterations=result["iteration_count"], + platform=ctx["platform"], + ) ) return Response( { diff --git a/src/sentry/onboarding_tasks/backends/organization_onboarding_task.py b/src/sentry/onboarding_tasks/backends/organization_onboarding_task.py index e8ae7126a7261d..763dc5798f9408 100644 --- a/src/sentry/onboarding_tasks/backends/organization_onboarding_task.py +++ b/src/sentry/onboarding_tasks/backends/organization_onboarding_task.py @@ -6,6 +6,7 @@ from django.utils import timezone from sentry import analytics +from sentry.analytics.events.onboarding_complete import OnboardingCompleteEvent from sentry.models.options.organization_option import OrganizationOption from sentry.models.organization import Organization from sentry.models.organizationonboardingtask import ( @@ -103,10 +104,11 @@ def try_mark_onboarding_complete(self, organization_id: int): value={"updated": json.datetime_to_str(timezone.now())}, ) analytics.record( - "onboarding.complete", - user_id=organization.default_owner_id, - organization_id=organization_id, - referrer="onboarding_tasks", + OnboardingCompleteEvent( + user_id=organization.default_owner_id, + organization_id=organization_id, + referrer="onboarding_tasks", + ) ) except IntegrityError: pass diff --git a/src/sentry/plugins/providers/integration_repository.py b/src/sentry/plugins/providers/integration_repository.py index f55bb932e670ed..8359834dd97177 100644 --- a/src/sentry/plugins/providers/integration_repository.py +++ b/src/sentry/plugins/providers/integration_repository.py @@ -13,6 +13,7 @@ from sentry import analytics from sentry.api.exceptions import SentryAPIException from sentry.constants import ObjectStatus +from sentry.integrations.analytics import IntegrationRepoAddedEvent from sentry.integrations.base import IntegrationInstallation from sentry.integrations.models.integration import Integration from sentry.integrations.services.integration import integration_service @@ -198,10 +199,11 @@ def dispatch(self, request: Request, organization, **kwargs): repo_linked.send_robust(repo=repo, user=request.user, sender=self.__class__) analytics.record( - "integration.repo.added", - provider=self.id, - id=result.get("integration_id"), - organization_id=organization.id, + IntegrationRepoAddedEvent( + provider=self.id, + id=result.get("integration_id"), + organization_id=organization.id, + ) ) return Response( repository_service.serialize_repository( diff --git a/src/sentry/receivers/features.py b/src/sentry/receivers/features.py index 07b4af5d2528a7..236f2bd67fc22f 100644 --- a/src/sentry/receivers/features.py +++ b/src/sentry/receivers/features.py @@ -4,6 +4,21 @@ from sentry import analytics from sentry.adoption import manager +from sentry.analytics.events.advanced_search_feature_gated import AdvancedSearchFeatureGateEvent +from sentry.analytics.events.alert_edited import AlertEditedEvent +from sentry.analytics.events.issue_assigned import IssueAssignedEvent +from sentry.analytics.events.issue_deleted import IssueDeletedEvent +from sentry.analytics.events.issue_escalating import IssueEscalatingEvent +from sentry.analytics.events.issue_mark_reviewed import IssueMarkReviewedEvent +from sentry.analytics.events.issue_resolved import IssueResolvedEvent +from sentry.analytics.events.issue_unignored import IssueUnignoredEvent +from sentry.analytics.events.plugin_enabled import PluginEnabledEvent +from sentry.analytics.events.repo_linked import RepoLinkedEvent +from sentry.integrations.analytics import ( + IntegrationAddedEvent, + IntegrationIssueCreatedEvent, + IntegrationIssueLinkedEvent, +) from sentry.integrations.services.integration import integration_service from sentry.models.featureadoption import FeatureAdoption from sentry.models.group import Group @@ -200,11 +215,12 @@ def record_issue_assigned(project, group, user, **kwargs): user_id = None default_user_id = project.organization.default_owner_id or UNKNOWN_DEFAULT_USER_ID analytics.record( - "issue.assigned", - user_id=user_id, - default_user_id=default_user_id, - organization_id=project.organization_id, - group_id=group.id, + IssueAssignedEvent( + user_id=user_id, + default_user_id=default_user_id, + organization_id=project.organization_id, + group_id=group.id, + ) ) @@ -232,15 +248,16 @@ def record_issue_resolved(organization_id, project, group, user, resolution_type default_user_id = project.organization.default_owner_id or UNKNOWN_DEFAULT_USER_ID analytics.record( - "issue.resolved", - user_id=user_id, - project_id=project.id, - default_user_id=default_user_id, - organization_id=organization_id, - group_id=group.id, - resolution_type=resolution_type, - issue_type=group.issue_type.slug, - issue_category=group.issue_category.name.lower(), + IssueResolvedEvent( + user_id=user_id, + project_id=project.id, + default_user_id=default_user_id, + organization_id=organization_id, + group_id=group.id, + resolution_type=resolution_type, + issue_type=group.issue_type.slug, + issue_category=group.issue_category.name.lower(), + ) ) @@ -278,10 +295,11 @@ def record_advanced_search_feature_gated(user, organization, **kwargs): default_user_id = organization.get_default_owner().id analytics.record( - "advanced_search.feature_gated", - user_id=user_id, - default_user_id=default_user_id, - organization_id=organization.id, + AdvancedSearchFeatureGateEvent( + user_id=user_id, + default_user_id=default_user_id, + organization_id=organization.id, + ) ) @@ -377,25 +395,27 @@ def record_alert_rule_edited( default_user_id = project.organization.get_default_owner().id analytics.record( - "alert.edited", - user_id=user_id, - default_user_id=default_user_id, - organization_id=project.organization_id, - project_id=project.id, - rule_id=rule.id, - rule_type=rule_type, - is_api_token=is_api_token, + AlertEditedEvent( + user_id=user_id, + default_user_id=default_user_id, + organization_id=project.organization_id, + project_id=project.id, + rule_id=rule.id, + rule_type=rule_type, + is_api_token=is_api_token, + ) ) @plugin_enabled.connect(weak=False) def record_plugin_enabled(plugin, project, user, **kwargs): analytics.record( - "plugin.enabled", - user_id=user.id if user else None, - organization_id=project.organization_id, - project_id=project.id, - plugin=plugin.slug, + PluginEnabledEvent( + user_id=user.id if user else None, + organization_id=project.organization_id, + project_id=project.id, + plugin=plugin.slug, + ) ) if isinstance(plugin, (IssueTrackingPlugin, IssueTrackingPlugin2)): FeatureAdoption.objects.record( @@ -449,12 +469,13 @@ def record_repo_linked(repo, user, **kwargs): default_user_id = Organization.objects.get(id=repo.organization_id).get_default_owner().id analytics.record( - "repo.linked", - user_id=user_id, - default_user_id=default_user_id, - organization_id=repo.organization_id, - repository_id=repo.id, - provider=repo.provider, + RepoLinkedEvent( + user_id=user_id, + default_user_id=default_user_id, + organization_id=repo.organization_id, + repository_id=repo.id, + provider=repo.provider, + ) ) @@ -530,12 +551,13 @@ def record_issue_archived(project, user, group_list, activity_data, **kwargs): @issue_escalating.connect(weak=False) def record_issue_escalating(project, group, event, was_until_escalating, **kwargs): analytics.record( - "issue.escalating", - organization_id=project.organization_id, - project_id=project.id, - group_id=group.id, - event_id=event.event_id if event else None, - was_until_escalating=was_until_escalating, + IssueEscalatingEvent( + organization_id=project.organization_id, + project_id=project.id, + group_id=group.id, + event_id=event.event_id if event else None, + was_until_escalating=was_until_escalating, + ) ) @@ -571,12 +593,13 @@ def record_issue_unignored(project, user_id, group, transition_type, **kwargs): default_user_id = project.organization.get_default_owner().id analytics.record( - "issue.unignored", - user_id=user_id, - default_user_id=default_user_id, - organization_id=project.organization_id, - group_id=group.id, - transition_type=transition_type, + IssueUnignoredEvent( + user_id=user_id, + default_user_id=default_user_id, + organization_id=project.organization_id, + group_id=group.id, + transition_type=transition_type, + ) ) @@ -589,11 +612,12 @@ def record_issue_reviewed(project, user, group, **kwargs): default_user_id = project.organization.get_default_owner().id analytics.record( - "issue.mark_reviewed", - user_id=user_id, - default_user_id=default_user_id, - organization_id=project.organization_id, - group_id=group.id, + IssueMarkReviewedEvent( + user_id=user_id, + default_user_id=default_user_id, + organization_id=project.organization_id, + group_id=group.id, + ) ) @@ -643,12 +667,13 @@ def record_integration_added( default_user_id = organization.get_default_owner().id analytics.record( - "integration.added", - user_id=user_id, - default_user_id=default_user_id, - organization_id=organization.id, - provider=integration.provider, - id=integration.id, + IntegrationAddedEvent( + user_id=user_id, + default_user_id=default_user_id, + organization_id=organization.id, + provider=integration.provider, + id=integration.id, + ) ) metrics.incr( "integration.added", @@ -665,12 +690,13 @@ def record_integration_issue_created(integration, organization, user, **kwargs): user_id = None default_user_id = organization.get_default_owner().id analytics.record( - "integration.issue.created", - user_id=user_id, - default_user_id=default_user_id, - organization_id=organization.id, - provider=integration.provider, - id=integration.id, + IntegrationIssueCreatedEvent( + user_id=user_id, + default_user_id=default_user_id, + organization_id=organization.id, + provider=integration.provider, + id=integration.id, + ) ) @@ -682,12 +708,13 @@ def record_integration_issue_linked(integration, organization, user, **kwargs): user_id = None default_user_id = organization.get_default_owner().id analytics.record( - "integration.issue.linked", - user_id=user_id, - default_user_id=default_user_id, - organization_id=organization.id, - provider=integration.provider, - id=integration.id, + IntegrationIssueLinkedEvent( + user_id=user_id, + default_user_id=default_user_id, + organization_id=organization.id, + provider=integration.provider, + id=integration.id, + ) ) @@ -699,13 +726,14 @@ def record_issue_deleted(group, user, delete_type, **kwargs): user_id = None default_user_id = group.project.organization.get_default_owner().id analytics.record( - "issue.deleted", - user_id=user_id, - default_user_id=default_user_id, - organization_id=group.project.organization_id, - group_id=group.id, - project_id=group.project_id, - delete_type=delete_type, + IssueDeletedEvent( + user_id=user_id, + default_user_id=default_user_id, + organization_id=group.project.organization_id, + group_id=group.id, + project_id=group.project_id, + delete_type=delete_type, + ) ) diff --git a/src/sentry/receivers/onboarding.py b/src/sentry/receivers/onboarding.py index f2ad6b7158cd74..197ce6f5df6455 100644 --- a/src/sentry/receivers/onboarding.py +++ b/src/sentry/receivers/onboarding.py @@ -7,6 +7,8 @@ from django.db.models import F from sentry import analytics +from sentry.analytics.events.first_new_feedback_sent import FirstNewFeedbackSentEvent +from sentry.analytics.events.first_sourcemaps_sent import FirstSourcemapsSentEvent from sentry.integrations.base import IntegrationDomain, get_integration_types from sentry.integrations.services.integration import RpcIntegration, integration_service from sentry.models.organization import Organization @@ -237,11 +239,12 @@ def record_first_feedback(project, **kwargs): ) def record_first_new_feedback(project, **kwargs): analytics.record( - "first_new_feedback.sent", - user_id=get_owner_id(project), - organization_id=project.organization_id, - project_id=project.id, - platform=project.platform, + FirstNewFeedbackSentEvent( + user_id=get_owner_id(project), + organization_id=project.organization_id, + project_id=project.id, + platform=project.platform, + ) ) @@ -386,13 +389,14 @@ def record_sourcemaps_received(project, event, **kwargs): ) return analytics.record( - "first_sourcemaps.sent", - user_id=owner_id, - organization_id=project.organization_id, - project_id=project.id, - platform=event.platform, - project_platform=project.platform, - url=dict(event.tags).get("url", None), + FirstSourcemapsSentEvent( + user_id=owner_id, + organization_id=project.organization_id, + project_id=project.id, + platform=event.platform, + project_platform=project.platform, + url=dict(event.tags).get("url", None), + ) ) diff --git a/src/sentry/receivers/releases.py b/src/sentry/receivers/releases.py index c676a4285acad9..7215fcd306a2a6 100644 --- a/src/sentry/receivers/releases.py +++ b/src/sentry/receivers/releases.py @@ -6,6 +6,7 @@ from sentry import analytics from sentry.db.postgres.transactions import in_test_hide_transaction_boundary +from sentry.integrations.analytics import IntegrationResolveCommitEvent, IntegrationResolvePREvent from sentry.models.activity import Activity from sentry.models.commit import Commit from sentry.models.group import Group, GroupStatus @@ -176,10 +177,11 @@ def resolved_in_commit(instance: Commit, created, **kwargs): if repo is not None: if repo.integration_id is not None: analytics.record( - "integration.resolve.commit", - provider=repo.provider, - id=repo.integration_id, - organization_id=repo.organization_id, + IntegrationResolveCommitEvent( + provider=repo.provider, + id=repo.integration_id, + organization_id=repo.organization_id, + ) ) issue_resolved.send_robust( @@ -251,10 +253,11 @@ def resolved_in_pull_request(instance: PullRequest, created, **kwargs): else: if repo is not None and repo.integration_id is not None: analytics.record( - "integration.resolve.pr", - provider=repo.provider, - id=repo.integration_id, - organization_id=repo.organization_id, + IntegrationResolvePREvent( + provider=repo.provider, + id=repo.integration_id, + organization_id=repo.organization_id, + ) ) From 2c18bc155a07f2dfaf274c552b42facb6b13ca1f Mon Sep 17 00:00:00 2001 From: Fabian Schindler Date: Fri, 11 Jul 2025 16:22:27 +0200 Subject: [PATCH 2/7] test(analytics): add assert_last_analytics_event for testing --- src/sentry/testutils/helpers/analytics.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/sentry/testutils/helpers/analytics.py b/src/sentry/testutils/helpers/analytics.py index 3c2803f63b600e..b3683ec8fd400a 100644 --- a/src/sentry/testutils/helpers/analytics.py +++ b/src/sentry/testutils/helpers/analytics.py @@ -32,6 +32,16 @@ def assert_analytics_events_recorded( assert_event_equal(expected_event, recorded_event, check_uuid, check_datetime) +def assert_last_analytics_event( + mock_record: MagicMock, + expected_event: Event, + check_uuid: bool = False, + check_datetime: bool = False, +): + recorded_event = mock_record.call_args_list[-1].args[0] + assert_event_equal(expected_event, recorded_event, check_uuid, check_datetime) + + @contextlib.contextmanager def assert_analytics_events( expected_events: list[Event], From 9746ca6968b93933eaf88ddec0c46b424c34ebc1 Mon Sep 17 00:00:00 2001 From: Fabian Schindler Date: Fri, 11 Jul 2025 16:28:19 +0200 Subject: [PATCH 3/7] test(analytics): fix testing analytics in onboarding --- tests/sentry/receivers/test_onboarding.py | 79 +++++++++++++---------- 1 file changed, 46 insertions(+), 33 deletions(-) diff --git a/tests/sentry/receivers/test_onboarding.py b/tests/sentry/receivers/test_onboarding.py index 0d43858c790e91..b3a90cc6795b0b 100644 --- a/tests/sentry/receivers/test_onboarding.py +++ b/tests/sentry/receivers/test_onboarding.py @@ -6,6 +6,8 @@ from sentry import onboarding_tasks from sentry.analytics import record +from sentry.analytics.events.onboarding_complete import OnboardingCompleteEvent +from sentry.integrations.analytics import IntegrationAddedEvent from sentry.models.options.organization_option import OrganizationOption from sentry.models.organizationonboardingtask import ( OnboardingTask, @@ -30,6 +32,7 @@ ) from sentry.silo.base import SiloMode from sentry.testutils.cases import TestCase +from sentry.testutils.helpers.analytics import assert_last_analytics_event from sentry.testutils.helpers.datetime import before_now from sentry.testutils.helpers.features import with_feature from sentry.testutils.silo import assume_test_silo_mode @@ -745,13 +748,15 @@ def test_real_time_notifications_added(self, record_analytics): ) assert task is not None - record_analytics.assert_called_with( - "integration.added", - user_id=self.user.id, - default_user_id=self.organization.default_owner_id, - organization_id=self.organization.id, - id=integration_id, - provider="slack", + assert_last_analytics_event( + record_analytics, + IntegrationAddedEvent( + user_id=self.user.id, + default_user_id=self.organization.default_owner_id, + organization_id=self.organization.id, + id=integration_id, + provider="slack", + ), ) @patch("sentry.analytics.record", wraps=record) @@ -770,13 +775,15 @@ def test_source_code_management_added(self, record_analytics): ) assert task is not None - record_analytics.assert_called_with( - "integration.added", - user_id=self.user.id, - default_user_id=self.organization.default_owner_id, - organization_id=self.organization.id, - id=integration_id, - provider="github", + assert_last_analytics_event( + record_analytics, + IntegrationAddedEvent( + user_id=self.user.id, + default_user_id=self.organization.default_owner_id, + organization_id=self.organization.id, + id=integration_id, + provider="github", + ), ) def test_second_platform_complete(self): @@ -985,13 +992,15 @@ def test_new_onboarding_complete(self, record_analytics): ) is not None ) - record_analytics.assert_called_with( - "integration.added", - user_id=self.user.id, - default_user_id=self.organization.default_owner_id, - organization_id=self.organization.id, - provider=github_integration.provider, - id=github_integration.id, + assert_last_analytics_event( + record_analytics, + IntegrationAddedEvent( + user_id=self.user.id, + default_user_id=self.organization.default_owner_id, + organization_id=self.organization.id, + provider=github_integration.provider, + id=github_integration.id, + ), ) # Invite your team @@ -1064,13 +1073,15 @@ def test_new_onboarding_complete(self, record_analytics): ) is not None ) - record_analytics.assert_called_with( - "integration.added", - user_id=self.user.id, - default_user_id=self.organization.default_owner_id, - organization_id=self.organization.id, - provider=slack_integration.provider, - id=slack_integration.id, + assert_last_analytics_event( + record_analytics, + IntegrationAddedEvent( + user_id=self.user.id, + default_user_id=self.organization.default_owner_id, + organization_id=self.organization.id, + provider=slack_integration.provider, + id=slack_integration.id, + ), ) # Add Sentry to other parts app @@ -1113,11 +1124,13 @@ def test_new_onboarding_complete(self, record_analytics): ).count() == 1 ) - record_analytics.assert_called_with( - "onboarding.complete", - user_id=self.user.id, - organization_id=self.organization.id, - referrer="onboarding_tasks", + assert_last_analytics_event( + record_analytics, + OnboardingCompleteEvent( + user_id=self.user.id, + organization_id=self.organization.id, + referrer="onboarding_tasks", + ), ) @patch("sentry.analytics.record", wraps=record) From e8b551fdbc32504bb7c91f29810db92731d26fe6 Mon Sep 17 00:00:00 2001 From: Fabian Schindler Date: Fri, 11 Jul 2025 16:33:21 +0200 Subject: [PATCH 4/7] test(analytics): fix testing analytics in test_signals --- tests/sentry/receivers/test_signals.py | 75 +++++++++++++++----------- 1 file changed, 43 insertions(+), 32 deletions(-) diff --git a/tests/sentry/receivers/test_signals.py b/tests/sentry/receivers/test_signals.py index fe02d3e41f98fd..4fc778ad7ffb94 100644 --- a/tests/sentry/receivers/test_signals.py +++ b/tests/sentry/receivers/test_signals.py @@ -2,6 +2,8 @@ from django.utils import timezone +from sentry.analytics.events.issue_assigned import IssueAssignedEvent +from sentry.analytics.events.issue_resolved import IssueResolvedEvent from sentry.models.groupassignee import GroupAssignee from sentry.signals import ( issue_assigned, @@ -12,6 +14,7 @@ issue_update_priority, ) from sentry.testutils.cases import SnubaTestCase, TestCase +from sentry.testutils.helpers.analytics import assert_last_analytics_event class SignalsTest(TestCase, SnubaTestCase): @@ -88,16 +91,18 @@ def test_issue_resolved_with_default_owner(self, mock_record): resolution_type="now", sender=type(self.project), ) - mock_record.assert_called_once_with( - "issue.resolved", - user_id=None, - project_id=self.project.id, - default_user_id=self.organization.default_owner_id, - organization_id=self.organization.id, - group_id=self.group.id, - resolution_type="now", - issue_type="error", - issue_category="error", + assert_last_analytics_event( + mock_record, + IssueResolvedEvent( + user_id=None, + project_id=self.project.id, + default_user_id=self.organization.default_owner_id, + organization_id=self.organization.id, + group_id=self.group.id, + resolution_type="now", + issue_type="error", + issue_category="error", + ), ) @patch("sentry.analytics.record") @@ -115,16 +120,18 @@ def test_issue_resolved_without_default_owner(self, mock_record): resolution_type="now", sender=type(project), ) - mock_record.assert_called_once_with( - "issue.resolved", - user_id=None, - project_id=project.id, - default_user_id="unknown", - organization_id=organization.id, - group_id=group.id, - resolution_type="now", - issue_type="error", - issue_category="error", + assert_last_analytics_event( + mock_record, + IssueResolvedEvent( + user_id=None, + project_id=project.id, + default_user_id="unknown", + organization_id=organization.id, + group_id=group.id, + resolution_type="now", + issue_type="error", + issue_category="error", + ), ) @patch("sentry.analytics.record") @@ -182,12 +189,14 @@ def test_issue_assigned_with_default_owner(self, mock_record): transition_type="manual", sender=type(self.project), ) - mock_record.assert_called_once_with( - "issue.assigned", - user_id=None, - default_user_id=self.organization.default_owner_id, - organization_id=self.organization.id, - group_id=self.group.id, + assert_last_analytics_event( + mock_record, + IssueAssignedEvent( + user_id=None, + default_user_id=self.organization.default_owner_id, + organization_id=self.organization.id, + group_id=self.group.id, + ), ) @patch("sentry.analytics.record") @@ -207,10 +216,12 @@ def test_issue_assigned_without_default_owner(self, mock_record): transition_type="manual", sender=type(project), ) - mock_record.assert_called_once_with( - "issue.assigned", - user_id=None, - default_user_id="unknown", - organization_id=organization.id, - group_id=group.id, + assert_last_analytics_event( + mock_record, + IssueAssignedEvent( + user_id=None, + default_user_id="unknown", + organization_id=organization.id, + group_id=group.id, + ), ) From a68628d7b3aa5436720644faa272ba487dd02c45 Mon Sep 17 00:00:00 2001 From: Fabian Schindler Date: Fri, 11 Jul 2025 16:49:59 +0200 Subject: [PATCH 5/7] test(analytics): fix tests in test_organization_group_index --- .../test_organization_group_index.py | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/tests/sentry/issues/endpoints/test_organization_group_index.py b/tests/sentry/issues/endpoints/test_organization_group_index.py index 0abcaf91e0f1d9..6cdd74a31131ae 100644 --- a/tests/sentry/issues/endpoints/test_organization_group_index.py +++ b/tests/sentry/issues/endpoints/test_organization_group_index.py @@ -12,6 +12,7 @@ from django.utils import timezone from sentry import options +from sentry.analytics.events.advanced_search_feature_gated import AdvancedSearchFeatureGateEvent from sentry.feedback.usecases.create_feedback import FeedbackCreationSource, create_feedback_issue from sentry.integrations.models.external_issue import ExternalIssue from sentry.integrations.models.organization_integration import OrganizationIntegration @@ -63,6 +64,7 @@ from sentry.silo.base import SiloMode from sentry.testutils.cases import APITestCase, SnubaTestCase from sentry.testutils.helpers import parse_link_header +from sentry.testutils.helpers.analytics import assert_last_analytics_event from sentry.testutils.helpers.datetime import before_now from sentry.testutils.helpers.features import Feature, apply_feature_flag_on_cls, with_feature from sentry.testutils.helpers.options import override_options @@ -909,11 +911,13 @@ def test_advanced_search_errors(self, mock_record: MagicMock, _: MagicMock) -> N "search" == response.data["detail"] ) - mock_record.assert_called_with( - "advanced_search.feature_gated", - user_id=self.user.id, - default_user_id=self.user.id, - organization_id=self.organization.id, + assert_last_analytics_event( + mock_record, + AdvancedSearchFeatureGateEvent( + user_id=self.user.id, + default_user_id=self.user.id, + organization_id=self.organization.id, + ), ) # This seems like a random override, but this test needed a way to override @@ -3825,11 +3829,13 @@ def test_snuba_heavy_advanced_search_errors(self, mock_record: MagicMock, _: Mag "search" == response.data["detail"] ) - mock_record.assert_called_with( - "advanced_search.feature_gated", - user_id=self.user.id, - default_user_id=self.user.id, - organization_id=self.organization.id, + assert_last_analytics_event( + mock_record, + AdvancedSearchFeatureGateEvent( + user_id=self.user.id, + default_user_id=self.user.id, + organization_id=self.organization.id, + ), ) def test_snuba_heavy_filter_not_unresolved(self, _: MagicMock) -> None: From 063d3b34ba43a800c52b7eb8c405be56df18b494 Mon Sep 17 00:00:00 2001 From: Fabian Schindler Date: Fri, 11 Jul 2025 16:52:19 +0200 Subject: [PATCH 6/7] test(analytics) fix tests in test_issues --- tests/sentry/integrations/test_issues.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/tests/sentry/integrations/test_issues.py b/tests/sentry/integrations/test_issues.py index ccccba56286f10..290bbc84ed3ee0 100644 --- a/tests/sentry/integrations/test_issues.py +++ b/tests/sentry/integrations/test_issues.py @@ -3,6 +3,7 @@ from django.utils import timezone +from sentry.analytics.events.issue_resolved import IssueResolvedEvent from sentry.integrations.example.integration import AliasedIntegrationProvider, ExampleIntegration from sentry.integrations.mixins.issues import IssueSyncIntegration as IssueSyncIntegrationBase from sentry.integrations.models.external_issue import ExternalIssue @@ -16,6 +17,7 @@ from sentry.models.release import Release from sentry.silo.base import SiloMode from sentry.testutils.cases import TestCase +from sentry.testutils.helpers.analytics import assert_last_analytics_event from sentry.testutils.helpers.features import with_feature from sentry.testutils.silo import assume_test_silo_mode from sentry.types.activity import ActivityType @@ -563,16 +565,18 @@ def test_status_sync_inbound_resolve_webhook_and_sends_to_sentry_app( assert data["data"]["resolution_type"] == self.integration.provider # Verify analytics event was recorded - mock_record.assert_called_with( - "issue.resolved", - project_id=self.group.project.id, - default_user_id="Sentry Jira", - organization_id=self.group.organization.id, - group_id=self.group.id, - resolution_type="with_third_party_app", - provider="example", - issue_type=self.group.issue_type.slug, - issue_category=self.group.issue_category.name.lower(), + assert_last_analytics_event( + mock_record, + IssueResolvedEvent( + project_id=self.group.project.id, + default_user_id="Sentry Jira", + organization_id=self.group.organization.id, + group_id=self.group.id, + resolution_type="with_third_party_app", + provider="example", + issue_type=self.group.issue_type.slug, + issue_category=self.group.issue_category.name.lower(), + ), ) @patch("sentry.utils.sentry_apps.webhooks.safe_urlopen", return_value=MockResponseInstance) From 4867cdb00495ea22f5806d8e0a88cc6fd0a86c18 Mon Sep 17 00:00:00 2001 From: Fabian Schindler Date: Fri, 11 Jul 2025 17:10:01 +0200 Subject: [PATCH 7/7] fix(analytics): mypy type issues --- .../events/first_new_feedback_sent.py | 2 +- .../analytics/events/first_sourcemaps_sent.py | 2 +- src/sentry/analytics/events/issue_resolved.py | 8 ++--- .../analytics/events/onboarding_complete.py | 4 +-- src/sentry/analytics/events/plugin_enabled.py | 2 +- src/sentry/integrations/analytics.py | 34 +++++++++---------- .../integrations/tasks/create_comment.py | 1 - .../integrations/tasks/sync_status_inbound.py | 1 - .../integrations/tasks/update_comment.py | 1 - src/sentry/receivers/features.py | 4 +-- 10 files changed, 28 insertions(+), 31 deletions(-) diff --git a/src/sentry/analytics/events/first_new_feedback_sent.py b/src/sentry/analytics/events/first_new_feedback_sent.py index 9576a21b2732ed..d50d598a2a21d6 100644 --- a/src/sentry/analytics/events/first_new_feedback_sent.py +++ b/src/sentry/analytics/events/first_new_feedback_sent.py @@ -6,7 +6,7 @@ class FirstNewFeedbackSentEvent(analytics.Event): organization_id: str project_id: str platform: str | None = None - user_id: str | None = None + user_id: int | None = None analytics.register(FirstNewFeedbackSentEvent) diff --git a/src/sentry/analytics/events/first_sourcemaps_sent.py b/src/sentry/analytics/events/first_sourcemaps_sent.py index 9b7f47f70c2775..c1a1b616bbf157 100644 --- a/src/sentry/analytics/events/first_sourcemaps_sent.py +++ b/src/sentry/analytics/events/first_sourcemaps_sent.py @@ -3,7 +3,7 @@ @analytics.eventclass("first_sourcemaps.sent") class FirstSourcemapsSentEvent(analytics.Event): - user_id: str + user_id: int organization_id: str project_id: str platform: str | None = None diff --git a/src/sentry/analytics/events/issue_resolved.py b/src/sentry/analytics/events/issue_resolved.py index c102e1349281d1..675306a3bf900c 100644 --- a/src/sentry/analytics/events/issue_resolved.py +++ b/src/sentry/analytics/events/issue_resolved.py @@ -3,11 +3,11 @@ @analytics.eventclass("issue.resolved") class IssueResolvedEvent(analytics.Event): - user_id: str | None = None - project_id: str | None = None + user_id: int | None = None + project_id: int | None = None default_user_id: str - organization_id: str - group_id: str + organization_id: int + group_id: int resolution_type: str # TODO: make required once we validate that all events have this issue_category: str | None = None diff --git a/src/sentry/analytics/events/onboarding_complete.py b/src/sentry/analytics/events/onboarding_complete.py index 2d10a7d48284e7..a1bb9186c23737 100644 --- a/src/sentry/analytics/events/onboarding_complete.py +++ b/src/sentry/analytics/events/onboarding_complete.py @@ -3,8 +3,8 @@ @analytics.eventclass("onboarding.complete") class OnboardingCompleteEvent(analytics.Event): - user_id: str - organization_id: str + user_id: int + organization_id: int referrer: str diff --git a/src/sentry/analytics/events/plugin_enabled.py b/src/sentry/analytics/events/plugin_enabled.py index 97aca875e8df00..464b17b4de31e9 100644 --- a/src/sentry/analytics/events/plugin_enabled.py +++ b/src/sentry/analytics/events/plugin_enabled.py @@ -3,7 +3,7 @@ @analytics.eventclass("plugin.enabled") class PluginEnabledEvent(analytics.Event): - user_id: str + user_id: int | None organization_id: str project_id: str plugin: str diff --git a/src/sentry/integrations/analytics.py b/src/sentry/integrations/analytics.py index 8578dbe81d95af..f66e5ad76241f5 100644 --- a/src/sentry/integrations/analytics.py +++ b/src/sentry/integrations/analytics.py @@ -4,15 +4,15 @@ @analytics.eventclass("integration.added") class IntegrationAddedEvent(analytics.Event): provider: str - id: str - organization_id: str - user_id: str | None = None - default_user_id: str + id: int + organization_id: int + user_id: int | None = None + default_user_id: int @analytics.eventclass("integration.disabled.notified") class IntegrationDisabledNotified(analytics.Event): - organization_id: str + organization_id: int provider: str integration_type: str integration_id: str @@ -22,7 +22,7 @@ class IntegrationDisabledNotified(analytics.Event): @analytics.eventclass("integration.issue.created") class IntegrationIssueCreatedEvent(analytics.Event): provider: str - id: str + id: int organization_id: str user_id: str | None = None default_user_id: str @@ -31,7 +31,7 @@ class IntegrationIssueCreatedEvent(analytics.Event): @analytics.eventclass("integration.issue.linked") class IntegrationIssueLinkedEvent(analytics.Event): provider: str - id: str + id: int organization_id: str user_id: str | None = None default_user_id: str @@ -40,41 +40,41 @@ class IntegrationIssueLinkedEvent(analytics.Event): @analytics.eventclass("integration.issue.status.synced") class IntegrationIssueStatusSyncedEvent(analytics.Event): provider: str - id: str + id: int organization_id: str @analytics.eventclass("integration.issue.assignee.synced") class IntegrationIssueAssigneeSyncedEvent(analytics.Event): provider: str - id: str + id: int organization_id: str @analytics.eventclass("integration.issue.comments.synced") class IntegrationIssueCommentsSyncedEvent(analytics.Event): provider: str - id: str + id: int organization_id: str @analytics.eventclass("integration.repo.added") class IntegrationRepoAddedEvent(analytics.Event): provider: str - id: str + id: int organization_id: str @analytics.eventclass("integration.resolve.commit") class IntegrationResolveCommitEvent(analytics.Event): - provider: str - id: str + provider: str | None + id: int organization_id: str @analytics.eventclass("integration.resolve.pr") class IntegrationResolvePREvent(analytics.Event): - provider: str + provider: str | None id: str organization_id: str @@ -83,11 +83,11 @@ class IntegrationResolvePREvent(analytics.Event): class IntegrationStacktraceLinkEvent(analytics.Event): provider: str config_id: str - project_id: str - organization_id: str + project_id: int + organization_id: int filepath: str status: str - link_fetch_iterations: str + link_fetch_iterations: int platform: str | None = None diff --git a/src/sentry/integrations/tasks/create_comment.py b/src/sentry/integrations/tasks/create_comment.py index 9b5b7a79bbe2c9..34b2a58d041101 100644 --- a/src/sentry/integrations/tasks/create_comment.py +++ b/src/sentry/integrations/tasks/create_comment.py @@ -73,6 +73,5 @@ def create_comment(external_issue_id: int, user_id: int, group_note_id: int) -> provider=installation.model.provider, id=installation.model.id, organization_id=external_issue.organization_id, - user_id=user_id, ) ) diff --git a/src/sentry/integrations/tasks/sync_status_inbound.py b/src/sentry/integrations/tasks/sync_status_inbound.py index 41893edb498947..203cd58e78df3f 100644 --- a/src/sentry/integrations/tasks/sync_status_inbound.py +++ b/src/sentry/integrations/tasks/sync_status_inbound.py @@ -300,7 +300,6 @@ def sync_status_inbound( organization_id=organization_id, group_id=group.id, resolution_type="with_third_party_app", - provider=provider.key, issue_type=group.issue_type.slug, issue_category=group.issue_category.name.lower(), ) diff --git a/src/sentry/integrations/tasks/update_comment.py b/src/sentry/integrations/tasks/update_comment.py index 3f79a1db86c29a..d8b92e3634132d 100644 --- a/src/sentry/integrations/tasks/update_comment.py +++ b/src/sentry/integrations/tasks/update_comment.py @@ -73,6 +73,5 @@ def update_comment(external_issue_id: int, user_id: int, group_note_id: int) -> provider=installation.model.provider, id=installation.model.id, organization_id=external_issue.organization_id, - user_id=user_id, ) ) diff --git a/src/sentry/receivers/features.py b/src/sentry/receivers/features.py index 236f2bd67fc22f..c513caf7125cbd 100644 --- a/src/sentry/receivers/features.py +++ b/src/sentry/receivers/features.py @@ -64,6 +64,7 @@ transaction_processed, user_feedback_received, ) +from sentry.users.models.user import User from sentry.utils import metrics from sentry.utils.javascript import has_sourcemap @@ -399,7 +400,6 @@ def record_alert_rule_edited( user_id=user_id, default_user_id=default_user_id, organization_id=project.organization_id, - project_id=project.id, rule_id=rule.id, rule_type=rule_type, is_api_token=is_api_token, @@ -408,7 +408,7 @@ def record_alert_rule_edited( @plugin_enabled.connect(weak=False) -def record_plugin_enabled(plugin, project, user, **kwargs): +def record_plugin_enabled(plugin, project, user: User | None, **kwargs): analytics.record( PluginEnabledEvent( user_id=user.id if user else None,