diff --git a/src/sentry/event_manager.py b/src/sentry/event_manager.py index 957ed8b6636913..201c0d8270b656 100644 --- a/src/sentry/event_manager.py +++ b/src/sentry/event_manager.py @@ -105,16 +105,14 @@ from sentry.plugins.base import plugins from sentry.quotas.base import index_data_category from sentry.receivers.features import record_event_processed -from sentry.receivers.onboarding import ( - record_first_insight_span, - record_first_transaction, - record_release_received, -) +from sentry.receivers.onboarding import record_release_received from sentry.reprocessing2 import is_reprocessed_event from sentry.seer.signed_seer_api import make_signed_seer_api_request from sentry.signals import ( first_event_received, first_event_with_minified_stack_trace_received, + first_insight_span_received, + first_transaction_received, issue_unresolved, ) from sentry.tasks.process_buffer import buffer_incr @@ -136,6 +134,7 @@ from sentry.utils.outcomes import Outcome, track_outcome from sentry.utils.performance_issues.performance_detection import detect_performance_problems from sentry.utils.performance_issues.performance_problem import PerformanceProblem +from sentry.utils.projectflags import set_project_flag_and_signal from sentry.utils.safe import get_path, safe_execute, setdefault_path, trim from sentry.utils.sdk import set_span_data from sentry.utils.tag_normalization import normalized_sdk_tag_from_event @@ -220,27 +219,6 @@ def plugin_is_regression(group: Group, event: BaseEvent) -> bool: return True -def get_project_insight_flag(project: Project, module: InsightModules): - if module == InsightModules.HTTP: - return project.flags.has_insights_http - elif module == InsightModules.DB: - return project.flags.has_insights_db - elif module == InsightModules.ASSETS: - return project.flags.has_insights_assets - elif module == InsightModules.APP_START: - return project.flags.has_insights_app_start - elif module == InsightModules.SCREEN_LOAD: - return project.flags.has_insights_screen_load - elif module == InsightModules.VITAL: - return project.flags.has_insights_vitals - elif module == InsightModules.CACHE: - return project.flags.has_insights_caches - elif module == InsightModules.QUEUE: - return project.flags.has_insights_queues - elif module == InsightModules.LLM_MONITORING: - return project.flags.has_insights_llm_monitoring - - def has_pending_commit_resolution(group: Group) -> bool: """ Checks that the most recent commit that fixes a group has had a chance to release @@ -569,8 +547,11 @@ def save_error_events( has_event_minified_stack_trace(job["event"]) and not project.flags.has_minified_stack_trace ): - first_event_with_minified_stack_trace_received.send_robust( - project=project, event=job["event"], sender=Project + set_project_flag_and_signal( + project, + "has_minified_stack_trace", + first_event_with_minified_stack_trace_received, + event=job["event"], ) if is_reprocessed: @@ -2475,6 +2456,19 @@ def _detect_performance_problems(jobs: Sequence[Job], projects: ProjectsMapping) ) +INSIGHT_MODULE_TO_PROJECT_FLAG_NAME: dict[InsightModules, str] = { + InsightModules.HTTP: "has_insights_http", + InsightModules.DB: "has_insights_db", + InsightModules.ASSETS: "has_insights_assets", + InsightModules.APP_START: "has_insights_app_start", + InsightModules.SCREEN_LOAD: "has_insights_screen_load", + InsightModules.VITAL: "has_insights_vitals", + InsightModules.CACHE: "has_insights_caches", + InsightModules.QUEUE: "has_insights_queues", + InsightModules.LLM_MONITORING: "has_insights_llm_monitoring", +} + + @sentry_sdk.tracing.trace def _record_transaction_info( jobs: Sequence[Job], projects: ProjectsMapping, skip_send_first_transaction: bool @@ -2490,12 +2484,22 @@ def _record_transaction_info( record_event_processed(project, event) if not skip_send_first_transaction: - record_first_transaction(project, event.datetime) + set_project_flag_and_signal( + project, + "has_transactions", + first_transaction_received, + event=event, + ) spans = job["data"]["spans"] for module, is_module in INSIGHT_MODULE_FILTERS.items(): - if not get_project_insight_flag(project, module) and is_module(spans): - record_first_insight_span(project, module) + if is_module(spans): + set_project_flag_and_signal( + project, + INSIGHT_MODULE_TO_PROJECT_FLAG_NAME[module], + first_insight_span_received, + module=module, + ) if job["release"]: environment = job["data"].get("environment") or None # coorce "" to None diff --git a/src/sentry/feedback/usecases/create_feedback.py b/src/sentry/feedback/usecases/create_feedback.py index 698959ca96ac7e..1d5b2d0ba6045e 100644 --- a/src/sentry/feedback/usecases/create_feedback.py +++ b/src/sentry/feedback/usecases/create_feedback.py @@ -25,6 +25,7 @@ from sentry.types.group import GroupSubStatus from sentry.utils import metrics from sentry.utils.outcomes import Outcome, track_outcome +from sentry.utils.projectflags import set_project_flag_and_signal from sentry.utils.safe import get_path logger = logging.getLogger(__name__) @@ -369,17 +370,10 @@ def create_feedback_issue( validate_issue_platform_event_schema(event_fixed) # Analytics - if not project.flags.has_feedbacks: - first_feedback_received.send_robust(project=project, sender=Project) + set_project_flag_and_signal(project, "has_feedbacks", first_feedback_received) - if ( - source - in [ - FeedbackCreationSource.NEW_FEEDBACK_ENVELOPE, - ] - and not project.flags.has_new_feedbacks - ): - first_new_feedback_received.send_robust(project=project, sender=Project) + if source == FeedbackCreationSource.NEW_FEEDBACK_ENVELOPE: + set_project_flag_and_signal(project, "has_new_feedbacks", first_new_feedback_received) # Send to issue platform for processing. produce_occurrence_to_kafka( diff --git a/src/sentry/monitors/utils.py b/src/sentry/monitors/utils.py index 1554ca21adf690..72ef4035c6b8e2 100644 --- a/src/sentry/monitors/utils.py +++ b/src/sentry/monitors/utils.py @@ -23,6 +23,7 @@ from sentry.users.models.user import User from sentry.utils.audit import create_audit_entry, create_system_audit_entry from sentry.utils.auth import AuthenticatedHttpRequest +from sentry.utils.projectflags import set_project_flag_and_signal def signal_first_checkin(project: Project, monitor: Monitor): @@ -30,18 +31,20 @@ def signal_first_checkin(project: Project, monitor: Monitor): # Backfill users that already have cron monitors check_and_signal_first_monitor_created(project, None, False) transaction.on_commit( - lambda: first_cron_checkin_received.send_robust( - project=project, monitor_id=str(monitor.guid), sender=Project + lambda: set_project_flag_and_signal( + project, + "has_new_feedbacks", + first_cron_checkin_received, + monitor_id=str(monitor.guid), ), router.db_for_write(Project), ) def check_and_signal_first_monitor_created(project: Project, user, from_upsert: bool): - if not project.flags.has_cron_monitors: - first_cron_monitor_created.send_robust( - project=project, user=user, from_upsert=from_upsert, sender=Project - ) + set_project_flag_and_signal( + project, "has_cron_monitors", first_cron_monitor_created, user=user, from_upsert=from_upsert + ) def signal_monitor_created(project: Project, user, from_upsert: bool, monitor: Monitor, request): diff --git a/src/sentry/profiles/task.py b/src/sentry/profiles/task.py index 63f0e3903195f4..395380f0e9e5fb 100644 --- a/src/sentry/profiles/task.py +++ b/src/sentry/profiles/task.py @@ -53,6 +53,7 @@ from sentry.utils.kafka_config import get_kafka_producer_cluster_options, get_topic_definition from sentry.utils.locking import UnableToAcquireLock from sentry.utils.outcomes import Outcome, track_outcome +from sentry.utils.projectflags import set_project_flag_and_signal from sentry.utils.sdk import set_span_data REVERSE_DEVICE_CLASS = {next(iter(tags)): label for label, tags in DEVICE_CLASS.items()} @@ -276,8 +277,7 @@ def process_profile_task( if sampled: with metrics.timer("process_profile.track_outcome.accepted"): - if not project.flags.has_profiles: - first_profile_received.send_robust(project=project, sender=Project) + set_project_flag_and_signal(project, "has_profiles", first_profile_received) try: if quotas.backend.should_emit_profile_duration_outcome( organization=organization, profile=profile diff --git a/src/sentry/receivers/onboarding.py b/src/sentry/receivers/onboarding.py index 40efedf8e46587..dc7500326c17cb 100644 --- a/src/sentry/receivers/onboarding.py +++ b/src/sentry/receivers/onboarding.py @@ -9,7 +9,6 @@ from django.utils import timezone as django_timezone from sentry import analytics -from sentry.constants import InsightModules from sentry.integrations.base import IntegrationDomain, get_integration_types from sentry.integrations.services.integration import RpcIntegration, integration_service from sentry.models.organization import Organization @@ -180,21 +179,12 @@ def record_first_event(project, event, **kwargs): @first_transaction_received.connect(weak=False, dispatch_uid="onboarding.record_first_transaction") -def _record_first_transaction(project, event, **kwargs): - return record_first_transaction(project, event.datetime, **kwargs) - - -def record_first_transaction(project, datetime, **kwargs): - if project.flags.has_transactions: - return - - project.update(flags=F("flags").bitor(Project.flags.has_transactions)) - +def record_first_transaction(project, event, **kwargs): OrganizationOnboardingTask.objects.record( organization_id=project.organization_id, task=OnboardingTask.FIRST_TRANSACTION, status=OnboardingTaskStatus.COMPLETE, - date_completed=datetime, + date_completed=event.datetime, ) analytics.record( @@ -208,8 +198,6 @@ def record_first_transaction(project, datetime, **kwargs): @first_profile_received.connect(weak=False, dispatch_uid="onboarding.record_first_profile") def record_first_profile(project, **kwargs): - project.update(flags=F("flags").bitor(Project.flags.has_profiles)) - analytics.record( "first_profile.sent", user_id=get_owner_id(project), @@ -246,8 +234,6 @@ def record_first_replay(project, **kwargs): @first_flag_received.connect(weak=False, dispatch_uid="onboarding.record_first_flag") def record_first_flag(project, **kwargs): - project.update(flags=F("flags").bitor(Project.flags.has_flags)) - analytics.record( "first_flag.sent", organization_id=project.organization_id, @@ -258,8 +244,6 @@ def record_first_flag(project, **kwargs): @first_feedback_received.connect(weak=False, dispatch_uid="onboarding.record_first_feedback") def record_first_feedback(project, **kwargs): - project.update(flags=F("flags").bitor(Project.flags.has_feedbacks)) - analytics.record( "first_feedback.sent", user_id=get_owner_id(project), @@ -273,8 +257,6 @@ def record_first_feedback(project, **kwargs): weak=False, dispatch_uid="onboarding.record_first_new_feedback" ) def record_first_new_feedback(project, **kwargs): - project.update(flags=F("flags").bitor(Project.flags.has_new_feedbacks)) - analytics.record( "first_new_feedback.sent", user_id=get_owner_id(project), @@ -286,16 +268,13 @@ def record_first_new_feedback(project, **kwargs): @first_cron_monitor_created.connect(weak=False, dispatch_uid="onboarding.record_first_cron_monitor") def record_first_cron_monitor(project, user, from_upsert, **kwargs): - updated = project.update(flags=F("flags").bitor(Project.flags.has_cron_monitors)) - - if updated: - analytics.record( - "first_cron_monitor.created", - user_id=get_owner_id(project, user), - organization_id=project.organization_id, - project_id=project.id, - from_upsert=from_upsert, - ) + analytics.record( + "first_cron_monitor.created", + user_id=get_owner_id(project, user), + organization_id=project.organization_id, + project_id=project.id, + from_upsert=from_upsert, + ) @cron_monitor_created.connect(weak=False, dispatch_uid="onboarding.record_cron_monitor_created") @@ -313,8 +292,6 @@ def record_cron_monitor_created(project, user, from_upsert, **kwargs): weak=False, dispatch_uid="onboarding.record_first_cron_checkin" ) def record_first_cron_checkin(project, monitor_id, **kwargs): - project.update(flags=F("flags").bitor(Project.flags.has_cron_checkins)) - analytics.record( "first_cron_checkin.sent", user_id=get_owner_id(project), @@ -324,30 +301,10 @@ def record_first_cron_checkin(project, monitor_id, **kwargs): ) +@first_insight_span_received.connect( + weak=False, dispatch_uid="onboarding.record_first_insight_span" +) def record_first_insight_span(project, module, **kwargs): - flag = None - if module == InsightModules.HTTP: - flag = Project.flags.has_insights_http - elif module == InsightModules.DB: - flag = Project.flags.has_insights_db - elif module == InsightModules.ASSETS: - flag = Project.flags.has_insights_assets - elif module == InsightModules.APP_START: - flag = Project.flags.has_insights_app_start - elif module == InsightModules.SCREEN_LOAD: - flag = Project.flags.has_insights_screen_load - elif module == InsightModules.VITAL: - flag = Project.flags.has_insights_vitals - elif module == InsightModules.CACHE: - flag = Project.flags.has_insights_caches - elif module == InsightModules.QUEUE: - flag = Project.flags.has_insights_queues - elif module == InsightModules.LLM_MONITORING: - flag = Project.flags.has_insights_llm_monitoring - - if flag is not None: - project.update(flags=F("flags").bitor(flag)) - analytics.record( "first_insight_span.sent", user_id=get_owner_id(project), @@ -358,9 +315,6 @@ def record_first_insight_span(project, module, **kwargs): ) -first_insight_span_received.connect(record_first_insight_span, weak=False) - - # TODO (mifu67): update this to use the new org member invite model @member_invited.connect(weak=False, dispatch_uid="onboarding.record_member_invited") def record_member_invited(member, user, **kwargs): @@ -426,28 +380,16 @@ def record_event_with_first_minified_stack_trace_for_project(project, event, **k ) return - # First, only enter this logic if we've never seen a minified stack trace before - if not project.flags.has_minified_stack_trace: - # Next, attempt to update the flag, but ONLY if the flag is currently not set. - # The number of affected rows tells us whether we succeeded or not. If we didn't, then skip sending the event. - # This guarantees us that this analytics event will only be ever sent once. - affected = Project.objects.filter( - id=project.id, flags=F("flags").bitand(~Project.flags.has_minified_stack_trace) - ).update(flags=F("flags").bitor(Project.flags.has_minified_stack_trace)) - - if ( - project.date_added > START_DATE_TRACKING_FIRST_EVENT_WITH_MINIFIED_STACK_TRACE_PER_PROJ - and affected > 0 - ): - analytics.record( - "first_event_with_minified_stack_trace_for_project.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), - ) + if project.date_added > START_DATE_TRACKING_FIRST_EVENT_WITH_MINIFIED_STACK_TRACE_PER_PROJ: + analytics.record( + "first_event_with_minified_stack_trace_for_project.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), + ) @event_processed.connect(weak=False, dispatch_uid="onboarding.record_sourcemaps_received") diff --git a/src/sentry/replays/usecases/ingest/__init__.py b/src/sentry/replays/usecases/ingest/__init__.py index 1e44af9acde5e6..b62dab27dab32c 100644 --- a/src/sentry/replays/usecases/ingest/__init__.py +++ b/src/sentry/replays/usecases/ingest/__init__.py @@ -40,6 +40,7 @@ from sentry.signals import first_replay_received from sentry.utils import json, metrics from sentry.utils.outcomes import Outcome, track_outcome +from sentry.utils.projectflags import set_project_flag_and_signal CACHE_TIMEOUT = 3600 COMMIT_FREQUENCY_SEC = 1 @@ -120,8 +121,7 @@ def _track_initial_segment_event( key_id: int | None, received: int, ) -> None: - if not project.flags.has_replays: - first_replay_received.send_robust(project=project, sender=Project) + set_project_flag_and_signal(project, "has_replays", first_replay_received) track_outcome( org_id=org_id, diff --git a/src/sentry/signals.py b/src/sentry/signals.py index 441c221875c010..43f89383bbe45b 100644 --- a/src/sentry/signals.py +++ b/src/sentry/signals.py @@ -102,7 +102,7 @@ def _log_robust_failure(self, receiver: object, err: Exception) -> None: event_accepted = BetterSignal() # ["ip", "data", "project"] # Organization Onboarding Signals -project_created = BetterSignal() # ["project", "user", "user_id", "default_rules"] +project_created = BetterSignal() # ["project", "user", "default_rules"] project_transferred = BetterSignal() # ["old_org_id", "project"] first_event_received = BetterSignal() # ["project", "event"] diff --git a/src/sentry/spans/consumers/process_segments/message.py b/src/sentry/spans/consumers/process_segments/message.py index 0457f36af38112..37b512b414830f 100644 --- a/src/sentry/spans/consumers/process_segments/message.py +++ b/src/sentry/spans/consumers/process_segments/message.py @@ -1,4 +1,5 @@ import logging +import types import uuid from copy import deepcopy from typing import Any, cast @@ -8,7 +9,7 @@ from sentry import options from sentry.constants import INSIGHT_MODULE_FILTERS from sentry.dynamic_sampling.rules.helpers.latest_releases import record_latest_release -from sentry.event_manager import get_project_insight_flag +from sentry.event_manager import INSIGHT_MODULE_TO_PROJECT_FLAG_NAME from sentry.issues.grouptype import PerformanceStreamedSpansGroupTypeExperimental from sentry.issues.issue_occurrence import IssueOccurrence from sentry.issues.producer import PayloadType, produce_occurrence_to_kafka @@ -18,11 +19,8 @@ from sentry.models.releaseenvironment import ReleaseEnvironment from sentry.models.releaseprojectenvironment import ReleaseProjectEnvironment from sentry.receivers.features import record_generic_event_processed -from sentry.receivers.onboarding import ( - record_first_insight_span, - record_first_transaction, - record_release_received, -) +from sentry.receivers.onboarding import record_release_received +from sentry.signals import first_insight_span_received, first_transaction_received from sentry.spans.consumers.process_segments.enrichment import ( match_schemas, set_exclusive_time, @@ -33,6 +31,7 @@ from sentry.utils import metrics from sentry.utils.dates import to_datetime from sentry.utils.performance_issues.performance_detection import detect_performance_problems +from sentry.utils.projectflags import set_project_flag_and_signal logger = logging.getLogger(__name__) @@ -259,8 +258,21 @@ def _record_signals(segment_span: Span, spans: list[Span], project: Project) -> environment=sentry_tags.get("environment"), ) - record_first_transaction(project, to_datetime(segment_span["end_timestamp_precise"])) + # signal expects an event like object with a datetime attribute + event_like = types.SimpleNamespace(datetime=to_datetime(segment_span["end_timestamp_precise"])) + + set_project_flag_and_signal( + project, + "has_transactions", + first_transaction_received, + event=event_like, + ) for module, is_module in INSIGHT_MODULE_FILTERS.items(): - if not get_project_insight_flag(project, module) and is_module(spans): - record_first_insight_span(project, module) + if is_module(spans): + set_project_flag_and_signal( + project, + INSIGHT_MODULE_TO_PROJECT_FLAG_NAME[module], + first_insight_span_received, + module=module, + ) diff --git a/src/sentry/tasks/post_process.py b/src/sentry/tasks/post_process.py index 77cb168fe171a8..d3f08f634e4313 100644 --- a/src/sentry/tasks/post_process.py +++ b/src/sentry/tasks/post_process.py @@ -1542,8 +1542,8 @@ def detect_base_urls_for_uptime(job: PostProcessJob): def check_if_flags_sent(job: PostProcessJob) -> None: - from sentry.models.project import Project from sentry.signals import first_flag_received + from sentry.utils.projectflags import set_project_flag_and_signal event = job["event"] project = event.project @@ -1552,8 +1552,7 @@ def check_if_flags_sent(job: PostProcessJob) -> None: if flag_context: metrics.incr("feature_flags.event_has_flags_context") metrics.distribution("feature_flags.num_flags_sent", len(flag_context)) - if not project.flags.has_flags: - first_flag_received.send_robust(project=project, sender=Project) + set_project_flag_and_signal(project, "has_flags", first_flag_received) def kick_off_seer_automation(job: PostProcessJob) -> None: diff --git a/src/sentry/utils/projectflags.py b/src/sentry/utils/projectflags.py new file mode 100644 index 00000000000000..07b6cb23557925 --- /dev/null +++ b/src/sentry/utils/projectflags.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from django.db.models import F +from django.dispatch import Signal + +from sentry.models.project import Project + + +def set_project_flag_and_signal(project: Project, flag_name: str, signal: Signal, **kwargs) -> bool: + """ + Helper function to set a project flag and send a signal. + Returns True if the flag was set, False if it was already set. + """ + flag = getattr(Project.flags, flag_name) + + # if the flag is already set, we don't need to do anything + # and we can return early + if getattr(project.flags, flag_name): + return False + + setattr(project.flags, flag_name, True) + updated = project.update(flags=F("flags").bitor(flag)) + signal.send_robust(project=project, sender=Project, **kwargs) + return bool(updated) diff --git a/tests/sentry/event_manager/test_event_manager.py b/tests/sentry/event_manager/test_event_manager.py index 21a3697745a24f..ee480330387414 100644 --- a/tests/sentry/event_manager/test_event_manager.py +++ b/tests/sentry/event_manager/test_event_manager.py @@ -67,6 +67,7 @@ from sentry.models.releasecommit import ReleaseCommit from sentry.models.releaseprojectenvironment import ReleaseProjectEnvironment from sentry.projectoptions.defaults import DEFAULT_GROUPING_CONFIG +from sentry.signals import first_insight_span_received, first_transaction_received from sentry.spans.grouping.utils import hash_values from sentry.testutils.asserts import assert_mock_called_once_with_partial from sentry.testutils.cases import ( @@ -1660,19 +1661,25 @@ def test_transaction_sampler_and_receive(self) -> None: manager.normalize() manager.save(self.project.id) - @patch("sentry.event_manager.record_first_transaction") - @patch("sentry.event_manager.record_first_insight_span") @patch("sentry.event_manager.record_release_received") @patch("sentry.ingest.transaction_clusterer.datasource.redis._record_sample") def test_transaction_sampler_and_receive_mock_called( self, mock_record_sample: mock.MagicMock, mock_record_release: mock.MagicMock, - mock_record_insight: mock.MagicMock, - mock_record_transaction: mock.MagicMock, ) -> None: self.project.update(flags=F("flags").bitand(~Project.flags.has_transactions)) + with ( + patch( + "sentry.receivers.onboarding.record_first_transaction", # autospec=True + ) as mock_record_transaction, + patch( + "sentry.receivers.onboarding.record_first_insight_span", # autospec=True + ) as mock_record_insight, + ): + first_transaction_received.connect(mock_record_transaction, weak=False) + first_insight_span_received.connect(mock_record_insight, weak=False) manager = EventManager( make_event( **{ @@ -1737,8 +1744,18 @@ def test_transaction_sampler_and_receive_mock_called( event = manager.save(self.project.id) mock_record_release.assert_called_once_with(self.project, "foo@1.0.0") - mock_record_insight.assert_called_once_with(self.project, InsightModules.DB) - mock_record_transaction.assert_called_once_with(self.project, event.datetime) + mock_record_insight.assert_called_once_with( + signal=first_insight_span_received, + sender=Project, + project=self.project, + module=InsightModules.DB, + ) + mock_record_transaction.assert_called_once_with( + signal=first_transaction_received, + sender=Project, + project=self.project, + event=event, + ) assert mock_record_sample.mock_calls == [ mock.call(ClustererNamespace.TRANSACTIONS, self.project, "wait") ] diff --git a/tests/sentry/receivers/test_onboarding.py b/tests/sentry/receivers/test_onboarding.py index 42fb96590cfbb6..01f465ece50c82 100644 --- a/tests/sentry/receivers/test_onboarding.py +++ b/tests/sentry/receivers/test_onboarding.py @@ -288,8 +288,6 @@ def test_first_transaction_received(self): assert task is not None - assert project.flags.has_transactions - def test_member_invited(self): user = self.create_user(email="test@example.org") member = self.create_member( diff --git a/tests/sentry/utils/test_projectflags.py b/tests/sentry/utils/test_projectflags.py new file mode 100644 index 00000000000000..8a9ad61f6ea253 --- /dev/null +++ b/tests/sentry/utils/test_projectflags.py @@ -0,0 +1,39 @@ +from unittest.mock import Mock, patch + +from django.db.models import F + +from sentry.models.project import Project +from sentry.signals import BetterSignal +from sentry.testutils.cases import TestCase +from sentry.utils.projectflags import set_project_flag_and_signal + +test_signal = BetterSignal() + + +class SetProjectFlagsAndSignalTest(TestCase): + @patch.object(test_signal, "send_robust") + def test_basic(self, mock_send_robust: Mock): + assert not self.project.flags.has_transactions + assert set_project_flag_and_signal(self.project, "has_transactions", test_signal) == 1 + mock_send_robust.assert_called_once_with(project=self.project, sender=Project) + + assert self.project.flags.has_transactions + + @patch.object(test_signal, "send_robust") + def test_flag_already_set(self, mock_send_robust: Mock): + self.project.update(flags=F("flags").bitor(Project.flags.has_transactions)) + assert self.project.flags.has_transactions + assert set_project_flag_and_signal(self.project, "has_transactions", test_signal) == 0 + mock_send_robust.assert_not_called() + assert self.project.flags.has_transactions + + @patch.object(test_signal, "send_robust") + def test_signal_kwargs(self, mock_send_robust: Mock): + assert not self.project.flags.has_transactions + assert ( + set_project_flag_and_signal(self.project, "has_transactions", test_signal, a=1, b="xyz") + == 1 + ) + mock_send_robust.assert_called_once_with(project=self.project, sender=Project, a=1, b="xyz") + + assert self.project.flags.has_transactions