diff --git a/src/sentry/api/endpoints/codeowners/__init__.py b/src/sentry/api/endpoints/codeowners/__init__.py index 3194d482423741..7a96a4a60d165d 100644 --- a/src/sentry/api/endpoints/codeowners/__init__.py +++ b/src/sentry/api/endpoints/codeowners/__init__.py @@ -8,6 +8,7 @@ from rest_framework.request import Request from sentry import analytics, features +from sentry.api.endpoints.codeowners.analytics import CodeOwnersMaxLengthExceeded from sentry.api.serializers.rest_framework.base import CamelSnakeModelSerializer from sentry.api.validators.project_codeowners import validate_codeowners_associations from sentry.integrations.models.repository_project_path_config import RepositoryProjectPathConfig @@ -56,8 +57,9 @@ def validate(self, attrs: Mapping[str, Any]) -> Mapping[str, Any]: max_length = self.get_max_length() if len(attrs["raw"]) > max_length and len(existing_raw) <= max_length: analytics.record( - "codeowners.max_length_exceeded", - organization_id=self.context["project"].organization.id, + CodeOwnersMaxLengthExceeded( + organization_id=self.context["project"].organization.id, + ) ) raise serializers.ValidationError( {"raw": f"Raw needs to be <= {max_length} characters in length"} diff --git a/src/sentry/api/endpoints/codeowners/analytics.py b/src/sentry/api/endpoints/codeowners/analytics.py index 0453a6802631cc..9ea47c3294eb10 100644 --- a/src/sentry/api/endpoints/codeowners/analytics.py +++ b/src/sentry/api/endpoints/codeowners/analytics.py @@ -1,10 +1,9 @@ from sentry import analytics +@analytics.eventclass("codeowners.max_length_exceeded") class CodeOwnersMaxLengthExceeded(analytics.Event): - type = "codeowners.max_length_exceeded" - - attributes = (analytics.Attribute("organization_id"),) + organization_id: str analytics.register(CodeOwnersMaxLengthExceeded) diff --git a/src/sentry/integrations/discord/analytics.py b/src/sentry/integrations/discord/analytics.py index f0bcc61291b346..6cfbb85bf05c82 100644 --- a/src/sentry/integrations/discord/analytics.py +++ b/src/sentry/integrations/discord/analytics.py @@ -1,65 +1,50 @@ from sentry import analytics +@analytics.eventclass("integrations.discord.notification_sent") class DiscordIntegrationNotificationSent(analytics.Event): - type = "integrations.discord.notification_sent" - - attributes = ( - analytics.Attribute("organization_id"), - analytics.Attribute("project_id"), - analytics.Attribute("category"), - analytics.Attribute("group_id"), - analytics.Attribute("notification_uuid"), - analytics.Attribute("alert_id", required=False), - ) + organization_id: str + project_id: str + category: str + group_id: str + notification_uuid: str + alert_id: str | None = None +@analytics.eventclass("integrations.discord.command_interaction") class DiscordIntegrationCommandInteractionReceived(analytics.Event): - type = "integrations.discord.command_interaction" - - attributes = (analytics.Attribute("command_name"),) + command_name: str +@analytics.eventclass("integrations.discord.identity_linked") class DiscordIntegrationIdentityLinked(analytics.Event): - type = "integrations.discord.identity_linked" - - attributes = ( - analytics.Attribute("provider"), - analytics.Attribute("actor_id"), - analytics.Attribute("actor_type"), - ) + provider: str + actor_id: str + actor_type: str +@analytics.eventclass("integrations.discord.identity_unlinked") class DiscordIntegrationIdentityUnlinked(analytics.Event): - type = "integrations.discord.identity_unlinked" - - attributes = ( - analytics.Attribute("provider"), - analytics.Attribute("actor_id"), - analytics.Attribute("actor_type"), - ) + provider: str + actor_id: str + actor_type: str +@analytics.eventclass("integrations.discord.message_interaction") class DiscordIntegrationMessageInteractionReceived(analytics.Event): - type = "integrations.discord.message_interaction" - - attributes = (analytics.Attribute("custom_id"),) + custom_id: str +@analytics.eventclass("integrations.discord.assign") class DiscordIntegrationAssign(analytics.Event): - type = "integrations.discord.assign" - - attributes = (analytics.Attribute("actor_id"),) + actor_id: str +@analytics.eventclass("integrations.discord.status") class DiscordIntegrationStatus(analytics.Event): - type = "integrations.discord.status" - - attributes = ( - analytics.Attribute("organization_id"), - analytics.Attribute("user_id"), - analytics.Attribute("status"), - ) + organization_id: str + user_id: str + status: str analytics.register(DiscordIntegrationCommandInteractionReceived) diff --git a/src/sentry/integrations/discord/webhooks/base.py b/src/sentry/integrations/discord/webhooks/base.py index f647e6694960d9..6a9e93a7651119 100644 --- a/src/sentry/integrations/discord/webhooks/base.py +++ b/src/sentry/integrations/discord/webhooks/base.py @@ -11,6 +11,10 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, all_silo_endpoint +from sentry.integrations.discord.analytics import ( + DiscordIntegrationCommandInteractionReceived, + DiscordIntegrationMessageInteractionReceived, +) from sentry.integrations.discord.requests.base import DiscordRequest, DiscordRequestError from sentry.integrations.discord.webhooks.command import DiscordCommandHandler from sentry.integrations.discord.webhooks.message_component import DiscordMessageComponentHandler @@ -58,15 +62,17 @@ def post(self, request: Request) -> HttpResponse: elif discord_request.is_command(): analytics.record( - "integrations.discord.command_interaction", - command_name=discord_request.get_command_name(), + DiscordIntegrationCommandInteractionReceived( + command_name=discord_request.get_command_name(), + ) ) return DiscordCommandHandler(discord_request).handle() elif discord_request.is_message_component(): analytics.record( - "integrations.discord.message_interaction", - custom_id=discord_request.get_component_custom_id(), + DiscordIntegrationMessageInteractionReceived( + custom_id=discord_request.get_component_custom_id(), + ) ) return DiscordMessageComponentHandler(discord_request).handle() diff --git a/src/sentry/integrations/discord/webhooks/message_component.py b/src/sentry/integrations/discord/webhooks/message_component.py index 04f4691617166c..147c857acab5dc 100644 --- a/src/sentry/integrations/discord/webhooks/message_component.py +++ b/src/sentry/integrations/discord/webhooks/message_component.py @@ -7,6 +7,7 @@ from sentry import analytics from sentry.api.helpers.group_index.update import update_groups +from sentry.integrations.discord.analytics import DiscordIntegrationAssign, DiscordIntegrationStatus from sentry.integrations.discord.message_builder.base.base import DiscordMessageBuilder from sentry.integrations.discord.message_builder.base.component import ( DiscordComponentCustomIds as CustomIds, @@ -175,8 +176,9 @@ def assign(self) -> Response: assert self.request.user is not None analytics.record( - "integrations.discord.assign", - actor_id=self.request.user.id, + DiscordIntegrationAssign( + actor_id=self.request.user.id, + ) ) message = DiscordMessageBuilder( @@ -241,10 +243,11 @@ def archive(self) -> Response: def update_group(self, data: Mapping[str, object]) -> None: if self.group: analytics.record( - "integrations.discord.status", - organization_id=self.group.organization.id, - user_id=self.user.id, - status=data, + DiscordIntegrationStatus( + organization_id=self.group.organization.id, + user_id=self.user.id, + status=data, + ) ) update_groups( request=self.request.request, groups=[self.group], user=self.user, data=data diff --git a/src/sentry/integrations/messaging/linkage.py b/src/sentry/integrations/messaging/linkage.py index 722b1282475268..1213933a84c999 100644 --- a/src/sentry/integrations/messaging/linkage.py +++ b/src/sentry/integrations/messaging/linkage.py @@ -20,6 +20,7 @@ from sentry.integrations.models.external_actor import ExternalActor from sentry.integrations.models.integration import Integration from sentry.integrations.services.integration import RpcIntegration, integration_service +from sentry.integrations.slack.analytics import IntegrationIdentityLinked from sentry.integrations.types import ExternalProviderEnum, ExternalProviders from sentry.integrations.utils.identities import get_identity_or_404 from sentry.models.organizationmember import OrganizationMember @@ -492,10 +493,11 @@ def execute( ) analytics.record( - "integrations.identity_linked", - provider=self.provider_slug, - actor_id=team.id, - actor_type="team", + IntegrationIdentityLinked( + provider=self.provider_slug, + actor_id=team.id, + actor_type="team", + ) ) if not created: diff --git a/src/sentry/integrations/slack/analytics.py b/src/sentry/integrations/slack/analytics.py index 157a8f382c2302..ada5578fcfc9bb 100644 --- a/src/sentry/integrations/slack/analytics.py +++ b/src/sentry/integrations/slack/analytics.py @@ -1,77 +1,58 @@ from sentry import analytics +@analytics.eventclass("integrations.slack.assign") class SlackIntegrationAssign(analytics.Event): - type = "integrations.slack.assign" - - attributes = (analytics.Attribute("actor_id", required=False),) + actor_id: str | None = None +@analytics.eventclass("integrations.slack.status") class SlackIntegrationStatus(analytics.Event): - type = "integrations.slack.status" - - attributes = ( - analytics.Attribute("organization_id"), - analytics.Attribute("status"), - analytics.Attribute("resolve_type", required=False), - analytics.Attribute("user_id", required=False), - ) + organization_id: str + status: str + resolve_type: str | None = None + user_id: str | None = None +@analytics.eventclass("integrations.slack.notification_sent") class SlackIntegrationNotificationSent(analytics.Event): - type = "integrations.slack.notification_sent" - - attributes = ( - analytics.Attribute("organization_id"), - analytics.Attribute("project_id", required=False), - analytics.Attribute("category"), - analytics.Attribute("actor_id", required=False), - analytics.Attribute("user_id", required=False), - analytics.Attribute("group_id", required=False), - analytics.Attribute("notification_uuid"), - analytics.Attribute("alert_id", required=False), - analytics.Attribute("actor_type", required=False), - ) - - + organization_id: str + project_id: str | None = None + category: str + actor_id: str | None = None + user_id: str | None = None + group_id: str | None = None + notification_uuid: str + alert_id: str | None = None + actor_type: str | None = None + + +@analytics.eventclass("integrations.identity_linked") class IntegrationIdentityLinked(analytics.Event): - type = "integrations.identity_linked" - - attributes = ( - analytics.Attribute("provider"), - analytics.Attribute("actor_id"), - analytics.Attribute("actor_type"), - ) + provider: str + actor_id: str + actor_type: str +@analytics.eventclass("integrations.slack.chart_unfurl") class IntegrationSlackChartUnfurl(analytics.Event): - type = "integrations.slack.chart_unfurl" - - attributes = ( - analytics.Attribute("user_id", required=False), - analytics.Attribute("organization_id"), - analytics.Attribute("unfurls_count", type=int), - ) + user_id: str | None = None + organization_id: str + unfurls_count: int +@analytics.eventclass("integrations.slack.chart_unfurl_action") class IntegrationSlackLinkIdentity(analytics.Event): - type = "integrations.slack.chart_unfurl_action" - - attributes = ( - analytics.Attribute("organization_id"), - analytics.Attribute("action"), - ) + organization_id: str + action: str +@analytics.eventclass("integrations.slack.approve_member_invitation") class IntegrationSlackApproveMemberInvitation(analytics.Event): - type = "integrations.slack.approve_member_invitation" - - attributes = ( - analytics.Attribute("organization_id"), - analytics.Attribute("actor_id"), - analytics.Attribute("invitation_type"), - analytics.Attribute("invited_member_id"), - ) + organization_id: str + actor_id: str + invitation_type: str + invited_member_id: str class IntegrationSlackRejectMemberInvitation(IntegrationSlackApproveMemberInvitation): diff --git a/src/sentry/integrations/slack/unfurl/discover.py b/src/sentry/integrations/slack/unfurl/discover.py index 658d58c2365237..5dbe2f24f2240e 100644 --- a/src/sentry/integrations/slack/unfurl/discover.py +++ b/src/sentry/integrations/slack/unfurl/discover.py @@ -21,6 +21,7 @@ ) from sentry.integrations.models.integration import Integration from sentry.integrations.services.integration import integration_service +from sentry.integrations.slack.analytics import IntegrationSlackChartUnfurl from sentry.integrations.slack.message_builder.discover import SlackDiscoverMessageBuilder from sentry.integrations.slack.spec import SlackMessagingSpec from sentry.integrations.slack.unfurl.types import Handler, UnfurlableUrl, UnfurledUrl @@ -298,10 +299,11 @@ def _unfurl_discover( first_org_integration = org_integrations[0] if len(org_integrations) > 0 else None if first_org_integration is not None and hasattr(first_org_integration, "id"): analytics.record( - "integrations.slack.chart_unfurl", - organization_id=first_org_integration.organization_id, - user_id=user.id if user else None, - unfurls_count=len(unfurls), + IntegrationSlackChartUnfurl( + organization_id=first_org_integration.organization_id, + user_id=user.id if user else None, + unfurls_count=len(unfurls), + ) ) return unfurls diff --git a/src/sentry/integrations/slack/webhooks/action.py b/src/sentry/integrations/slack/webhooks/action.py index b99c84c9dece46..4979d2a5034ea0 100644 --- a/src/sentry/integrations/slack/webhooks/action.py +++ b/src/sentry/integrations/slack/webhooks/action.py @@ -31,6 +31,11 @@ MessagingInteractionType, ) from sentry.integrations.services.integration import integration_service +from sentry.integrations.slack.analytics import ( + IntegrationSlackLinkIdentity, + SlackIntegrationAssign, + SlackIntegrationStatus, +) from sentry.integrations.slack.message_builder.issues import SlackIssuesMessageBuilder from sentry.integrations.slack.requests.action import SlackActionRequest from sentry.integrations.slack.requests.base import SlackRequestError @@ -287,7 +292,7 @@ def on_assign( }, request, ) - analytics.record("integrations.slack.assign", actor_id=user.id) + analytics.record(SlackIntegrationAssign(actor_id=user.id)) def on_status( self, @@ -320,11 +325,12 @@ def on_status( update_group(group, user, status, request) analytics.record( - "integrations.slack.status", - organization_id=group.project.organization.id, - status=status["status"], - resolve_type=resolve_type, - user_id=user.id, + SlackIntegrationStatus( + organization_id=group.project.organization.id, + status=status["status"], + resolve_type=resolve_type, + user_id=user.id, + ) ) def _handle_group_actions( @@ -521,9 +527,10 @@ def handle_unfurl(self, slack_request: SlackActionRequest, action: str) -> Respo ) if len(organization_integrations) > 0: analytics.record( - "integrations.slack.chart_unfurl_action", - organization_id=organization_integrations[0].id, - action=action, + IntegrationSlackLinkIdentity( + organization_id=organization_integrations[0].id, + action=action, + ) ) payload = {"delete_original": "true"} try: diff --git a/src/sentry/integrations/slack/webhooks/event.py b/src/sentry/integrations/slack/webhooks/event.py index f87688406af3ce..d14e18acbc6bf7 100644 --- a/src/sentry/integrations/slack/webhooks/event.py +++ b/src/sentry/integrations/slack/webhooks/event.py @@ -15,6 +15,7 @@ from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import all_silo_endpoint from sentry.integrations.services.integration import integration_service +from sentry.integrations.slack.analytics import IntegrationSlackChartUnfurl from sentry.integrations.slack.message_builder.help import SlackHelpMessageBuilder from sentry.integrations.slack.message_builder.prompt import SlackPromptLinkMessageBuilder from sentry.integrations.slack.requests.base import SlackDMRequest, SlackRequestError @@ -189,9 +190,10 @@ def on_link_shared(self, request: Request, slack_request: SlackDMRequest) -> boo and features.has("organizations:discover-basic", organization, actor=request.user) ): analytics.record( - "integrations.slack.chart_unfurl", - organization_id=organization.id, - unfurls_count=0, + IntegrationSlackChartUnfurl( + organization_id=organization.id, + unfurls_count=0, + ) ) self.prompt_link(slack_request) return True diff --git a/src/sentry/models/projectcodeowners.py b/src/sentry/models/projectcodeowners.py index c0c13fc0391512..55d07df8315777 100644 --- a/src/sentry/models/projectcodeowners.py +++ b/src/sentry/models/projectcodeowners.py @@ -9,6 +9,7 @@ from rest_framework.exceptions import ValidationError from sentry import analytics +from sentry.api.endpoints.codeowners.analytics import CodeOwnersMaxLengthExceeded from sentry.backup.scopes import RelocationScope from sentry.db.models import FlexibleForeignKey, JSONField, Model, region_silo_model, sane_repr from sentry.issues.ownership.grammar import ( @@ -106,8 +107,9 @@ def update_schema(self, organization: Organization, raw: str | None = None) -> N if len(self.raw) > MAX_RAW_LENGTH: analytics.record( - "codeowners.max_length_exceeded", - organization_id=organization.id, + CodeOwnersMaxLengthExceeded( + organization_id=organization.id, + ) ) logger.warning({"raw": f"Raw needs to be <= {MAX_RAW_LENGTH} characters in length"}) return diff --git a/src/sentry/users/models/identity.py b/src/sentry/users/models/identity.py index 0a727d10656200..12da89e3c9d4ec 100644 --- a/src/sentry/users/models/identity.py +++ b/src/sentry/users/models/identity.py @@ -20,6 +20,7 @@ ) from sentry.db.models.fields.jsonfield import JSONField from sentry.db.models.manager.base import BaseManager +from sentry.integrations.slack.analytics import IntegrationIdentityLinked from sentry.integrations.types import ExternalProviders from sentry.users.services.user import RpcUser @@ -107,12 +108,13 @@ def link_identity( return self.reattach(idp, external_id, user, defaults) analytics.record( - "integrations.identity_linked", - provider="slack", - # Note that prior to circa March 2023 this was user.actor_id. It changed - # when actor ids were no longer stable between regions for the same user - actor_id=user.id, - actor_type="user", + IntegrationIdentityLinked( + provider="slack", + # Note that prior to circa March 2023 this was user.actor_id. It changed + # when actor ids were no longer stable between regions for the same user + actor_id=user.id, + actor_type="user", + ) ) return identity