diff --git a/src/sentry/analytics/events/project_created.py b/src/sentry/analytics/events/project_created.py index 91aaca260475c8..19b15c0d57ddeb 100644 --- a/src/sentry/analytics/events/project_created.py +++ b/src/sentry/analytics/events/project_created.py @@ -8,6 +8,7 @@ class ProjectCreatedEvent(analytics.Event): analytics.Attribute("user_id", required=False), analytics.Attribute("default_user_id"), analytics.Attribute("organization_id"), + analytics.Attribute("origin", required=False), analytics.Attribute("project_id"), analytics.Attribute("platform", required=False), analytics.Attribute("updated_empty_state", required=False), diff --git a/src/sentry/api/endpoints/organization_projects_experiment.py b/src/sentry/api/endpoints/organization_projects_experiment.py index 075357d98607cc..6de8c1c85f2ee4 100644 --- a/src/sentry/api/endpoints/organization_projects_experiment.py +++ b/src/sentry/api/endpoints/organization_projects_experiment.py @@ -184,14 +184,14 @@ def post(self, request: Request, organization: Organization) -> Response: "organization": team.organization, "target_object": project.id, } - - if request.data.get("origin"): + origin = request.data.get("origin") + if origin: self.create_audit_entry( **common_audit_data, event=audit_log.get_event_id("PROJECT_ADD_WITH_ORIGIN"), data={ **project.get_audit_log_data(), - "origin": request.data.get("origin"), + "origin": origin, }, ) else: @@ -205,6 +205,7 @@ def post(self, request: Request, organization: Organization) -> Response: project=project, user=request.user, default_rules=result.get("default_rules", True), + origin=origin, sender=self, ) self.create_audit_entry( diff --git a/src/sentry/api/endpoints/team_projects.py b/src/sentry/api/endpoints/team_projects.py index 761ff09dce16d1..e9549ed3519a06 100644 --- a/src/sentry/api/endpoints/team_projects.py +++ b/src/sentry/api/endpoints/team_projects.py @@ -230,13 +230,14 @@ def post(self, request: Request, team: Team) -> Response: "target_object": project.id, } - if request.data.get("origin"): + origin = request.data.get("origin") + if origin: self.create_audit_entry( **common_audit_data, event=audit_log.get_event_id("PROJECT_ADD_WITH_ORIGIN"), data={ **project.get_audit_log_data(), - "origin": request.data.get("origin"), + "origin": origin, }, ) else: @@ -250,6 +251,7 @@ def post(self, request: Request, team: Team) -> Response: project=project, user=request.user, default_rules=result.get("default_rules", True), + origin=origin, sender=self, ) diff --git a/src/sentry/receivers/onboarding.py b/src/sentry/receivers/onboarding.py index d704d3064fac5d..c55d6c5bfcf06b 100644 --- a/src/sentry/receivers/onboarding.py +++ b/src/sentry/receivers/onboarding.py @@ -58,7 +58,7 @@ @project_created.connect(weak=False) -def record_new_project(project, user=None, user_id=None, **kwargs): +def record_new_project(project, user=None, user_id=None, origin=None, **kwargs): scope = sentry_sdk.get_current_scope() scope.set_extra("project_id", project.id) @@ -90,6 +90,7 @@ def record_new_project(project, user=None, user_id=None, **kwargs): user_id=user_id, default_user_id=default_user_id, organization_id=project.organization_id, + origin=origin, project_id=project.id, platform=project.platform, updated_empty_state=features.has( diff --git a/tests/sentry/api/endpoints/test_organization_projects_experiment.py b/tests/sentry/api/endpoints/test_organization_projects_experiment.py index c8d8e72e965195..f8a4eb18a5f73f 100644 --- a/tests/sentry/api/endpoints/test_organization_projects_experiment.py +++ b/tests/sentry/api/endpoints/test_organization_projects_experiment.py @@ -1,5 +1,6 @@ import re -from unittest.mock import patch +from unittest import mock +from unittest.mock import Mock, patch from django.utils.text import slugify @@ -14,6 +15,7 @@ from sentry.models.project import Project from sentry.models.rule import Rule from sentry.models.team import Team +from sentry.signals import project_created from sentry.testutils.cases import APITestCase from sentry.testutils.helpers.features import with_feature @@ -282,3 +284,36 @@ def test_disable_member_project_creation(self): name="foo", status_code=201, ) + + @with_feature(["organizations:team-roles"]) + @patch( + "sentry.api.endpoints.organization_projects_experiment.OrganizationProjectsExperimentEndpoint.create_audit_entry" + ) + def test_create_project_with_origin(self, create_audit_entry): + signal_handler = Mock() + project_created.connect(signal_handler) + + response = self.get_success_response( + self.organization.slug, + name="foo", + origin="ui", + status_code=201, + ) + + project = Project.objects.get(id=response.data["id"]) + # Verify audit log was created + create_audit_entry.assert_any_call( + request=mock.ANY, + organization=self.organization, + target_object=project.id, + event=1154, + data={ + **project.get_audit_log_data(), + "origin": "ui", + }, + ) + + # Verify origin is passed to project_created signal + assert signal_handler.call_count == 1 + assert signal_handler.call_args[1]["origin"] == "ui" + project_created.disconnect(signal_handler) diff --git a/tests/sentry/api/endpoints/test_team_projects.py b/tests/sentry/api/endpoints/test_team_projects.py index 68ee6340996203..1b813828759fe5 100644 --- a/tests/sentry/api/endpoints/test_team_projects.py +++ b/tests/sentry/api/endpoints/test_team_projects.py @@ -8,7 +8,7 @@ from sentry.models.project import Project from sentry.models.rule import Rule from sentry.notifications.types import FallthroughChoiceType -from sentry.signals import alert_rule_created +from sentry.signals import alert_rule_created, project_created from sentry.slug.errors import DEFAULT_SLUG_ERROR_MESSAGE from sentry.testutils.cases import APITestCase from sentry.testutils.helpers.options import override_options @@ -356,6 +356,9 @@ def test_builtin_symbol_sources_not_electron(self): @patch("sentry.api.endpoints.team_projects.TeamProjectsEndpoint.create_audit_entry") def test_create_project_with_origin(self, create_audit_entry): + signal_handler = Mock() + project_created.connect(signal_handler) + response = self.get_success_response( self.organization.slug, self.team.slug, @@ -379,3 +382,8 @@ def test_create_project_with_origin(self, create_audit_entry): "origin": "ui", }, ) + + # Verify origin is passed to project_created signal + assert signal_handler.call_count == 1 + assert signal_handler.call_args[1]["origin"] == "ui" + project_created.disconnect(signal_handler) diff --git a/tests/sentry/receivers/test_onboarding.py b/tests/sentry/receivers/test_onboarding.py index aa6883fdd9dd96..82b153226b5fc2 100644 --- a/tests/sentry/receivers/test_onboarding.py +++ b/tests/sentry/receivers/test_onboarding.py @@ -137,6 +137,32 @@ def test_project_created(self): ) assert task is not None + @patch("sentry.analytics.record") + def test_project_created_with_origin(self, record_analytics): + project = self.create_project() + project_created.send( + project=project, user=self.user, default_rules=False, sender=type(project), origin="ui" + ) + + task = OrganizationOnboardingTask.objects.get( + organization=self.organization, + task=OnboardingTask.FIRST_PROJECT, + status=OnboardingTaskStatus.COMPLETE, + ) + assert task is not None + + # Verify origin is passed to analytics event + record_analytics.assert_called_with( + "project.created", + user_id=self.user.id, + default_user_id=self.organization.default_owner_id, + organization_id=self.organization.id, + project_id=project.id, + platform=project.platform, + updated_empty_state=False, + origin="ui", + ) + def test_first_event_received(self): now = timezone.now() project = self.create_project(first_event=now, platform="javascript") @@ -834,6 +860,7 @@ def test_new_onboarding_complete(self, record_analytics): project_id=project.id, platform=project.platform, updated_empty_state=False, + origin=None, ) # Set up tracing