From 794df27a2e6050b46ccb04ce74f31583eacf5cb5 Mon Sep 17 00:00:00 2001 From: Alicia Matsumoto Date: Thu, 20 Mar 2025 16:23:18 -0400 Subject: [PATCH 1/3] extend signal stats per signal id --- src/dispatch/entity/service.py | 5 ++ src/dispatch/plugins/dispatch_slack/fields.py | 1 + src/dispatch/signal/service.py | 9 ++- src/dispatch/signal/views.py | 28 ++++++- tests/signal/test_signal_data_service.py | 75 +++++++++++++++++++ 5 files changed, 115 insertions(+), 3 deletions(-) diff --git a/src/dispatch/entity/service.py b/src/dispatch/entity/service.py index 3628811c5797..ae6a5e8694f5 100644 --- a/src/dispatch/entity/service.py +++ b/src/dispatch/entity/service.py @@ -84,6 +84,11 @@ def get_all_by_signal(*, db_session: Session, signal_id: int) -> list[Entity]: ) + +# todo amats - get_all_desc_by_signal_and_type. +# descending entity TYPES, and then descending entities within each type + + def get_all_desc_by_signal(*, db_session: Session, signal_id: int) -> list[Entity]: """Gets all entities for a specific signal in descending order.""" return ( diff --git a/src/dispatch/plugins/dispatch_slack/fields.py b/src/dispatch/plugins/dispatch_slack/fields.py index d911f6326a31..ff47c7acdfd4 100644 --- a/src/dispatch/plugins/dispatch_slack/fields.py +++ b/src/dispatch/plugins/dispatch_slack/fields.py @@ -682,6 +682,7 @@ def entity_select( return multi_select_block( placeholder="Select Entities", + # todo: use option groups here instead. options=entity_options[:100], # Limit the entities to the first 100 most recent action_id=action_id, block_id=block_id, diff --git a/src/dispatch/signal/service.py b/src/dispatch/signal/service.py index fe2419b0f0c7..ca745e357611 100644 --- a/src/dispatch/signal/service.py +++ b/src/dispatch/signal/service.py @@ -971,9 +971,13 @@ def get_cases_for_signal_by_resolution_reason( def get_signal_stats( - *, db_session: Session, entity_value: str, entity_type_id: int, num_days: int | None + *, db_session: Session, entity_value: str, entity_type_id: int, signal_id: int | None = None, num_days: int | None = None ) -> Optional[SignalStats]: - """Gets a signal statistics for a given named entity and type.""" + """ + Gets signal statistics for a given named entity and type. + + If signal_id is provided, only returns stats for that specific signal definition. + """ entity_subquery = ( db_session.query( func.jsonb_build_array( @@ -1027,6 +1031,7 @@ def get_signal_stats( and_( Entity.value == entity_value, Entity.entity_type_id == entity_type_id, + SignalInstance.signal_id == signal_id if signal_id else True, SignalInstance.created_at >= date_threshold if date_threshold else True, ) ) diff --git a/src/dispatch/signal/views.py b/src/dispatch/signal/views.py index 276a497630ff..c05f33397802 100644 --- a/src/dispatch/signal/views.py +++ b/src/dispatch/signal/views.py @@ -290,7 +290,7 @@ def return_signal_stats( entity_type_id: int = Query(..., description="The ID of the entity type"), num_days: int = Query(None, description="The number of days to look back"), ): - """Gets a signal statistics given a named entity and entity type id.""" + """Gets signal statistics given a named entity and entity type id.""" signal_data = get_signal_stats( db_session=db_session, entity_value=entity_value, @@ -300,6 +300,32 @@ def return_signal_stats( return signal_data +@router.get("/{signal_id}/stats", response_model=SignalStats) +def return_single_signal_stats( + db_session: DbSession, + signal_id: Union[str, PrimaryKey], + entity_value: str = Query(..., description="The name of the entity"), + entity_type_id: int = Query(..., description="The ID of the entity type"), + num_days: int = Query(None, description="The number of days to look back"), +): + """Gets signal statistics for a specific signal given a named entity and entity type id.""" + signal = get_by_primary_or_external_id(db_session=db_session, signal_id=signal_id) + if not signal: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=[{"msg": "A signal with this id does not exist."}], + ) + + signal_data = get_signal_stats( + db_session=db_session, + entity_value=entity_value, + entity_type_id=entity_type_id, + signal_id=signal.id, + num_days=num_days, + ) + return signal_data + + @router.get("/{signal_id}", response_model=SignalRead) def get_signal(db_session: DbSession, signal_id: Union[str, PrimaryKey]): """Gets a signal by its id.""" diff --git a/tests/signal/test_signal_data_service.py b/tests/signal/test_signal_data_service.py index 914594dbd782..34b4261da779 100644 --- a/tests/signal/test_signal_data_service.py +++ b/tests/signal/test_signal_data_service.py @@ -161,3 +161,78 @@ def test_get_signal_stats_not_found(session, entity_type): assert signal_data.num_signal_instances_snoozed == 0 assert signal_data.num_snoozes_active == 0 assert signal_data.num_snoozes_expired == 0 + + +def test_get_signal_stats_with_signal_id_filter(session, entity, entity_type): + """Test get_signal_stats with a specific signal ID filter.""" + from dispatch.signal.service import get_signal_stats + from dispatch.signal.models import Signal, SignalInstance + + # Setup: Create two different signals + signal1 = Signal(name="Test Signal 1", variant="test-signal-1", project_id=1) + signal2 = Signal(name="Test Signal 2", variant="test-signal-2", project_id=1) + session.add_all([signal1, signal2]) + session.flush() + + # Associate entity with entity type + entity.entity_type = entity_type + + # Create a signal instance for signal1 + signal_instance1 = SignalInstance(signal=signal1, project_id=1) + signal_instance1.entities.append(entity) + + # Create two signal instances for signal2 + signal_instance2 = SignalInstance(signal=signal2, project_id=1) + signal_instance2.entities.append(entity) + signal_instance3 = SignalInstance(signal=signal2, project_id=1) + signal_instance3.entities.append(entity) + + session.add_all([signal_instance1, signal_instance2, signal_instance3]) + session.commit() + + # Execute: Call the service function without signal_id (should count both signals) + signal_data_all = get_signal_stats( + db_session=session, + entity_value=entity.value, + entity_type_id=entity_type.id, + num_days=None, + ) + + # Execute: Call the service function with signal_id for signal1 + signal_data_signal1 = get_signal_stats( + db_session=session, + entity_value=entity.value, + entity_type_id=entity_type.id, + signal_id=signal1.id, + num_days=None, + ) + + # Execute: Call the service function with signal_id for signal2 + signal_data_signal2 = get_signal_stats( + db_session=session, + entity_value=entity.value, + entity_type_id=entity_type.id, + signal_id=signal2.id, + num_days=None, + ) + + # Assert: Without signal_id filter, we should count both instances + assert ( + signal_data_all.num_signal_instances_alerted + + signal_data_all.num_signal_instances_snoozed + == 3 + ) + + # Assert: With signal1 filter, we should count only signal_instance1 + assert ( + signal_data_signal1.num_signal_instances_alerted + + signal_data_signal1.num_signal_instances_snoozed + == 1 + ) + + # Assert: With signal2 filter, we should count only signal_instance2 + assert ( + signal_data_signal2.num_signal_instances_alerted + + signal_data_signal2.num_signal_instances_snoozed + == 2 + ) From 0a8b600a291574143b5f899cdc4f48de7d9fa3a4 Mon Sep 17 00:00:00 2001 From: Alicia Matsumoto Date: Thu, 20 Mar 2025 16:24:32 -0400 Subject: [PATCH 2/3] remove debugging statements --- src/dispatch/entity/service.py | 5 ----- src/dispatch/plugins/dispatch_slack/fields.py | 1 - 2 files changed, 6 deletions(-) diff --git a/src/dispatch/entity/service.py b/src/dispatch/entity/service.py index ae6a5e8694f5..3628811c5797 100644 --- a/src/dispatch/entity/service.py +++ b/src/dispatch/entity/service.py @@ -84,11 +84,6 @@ def get_all_by_signal(*, db_session: Session, signal_id: int) -> list[Entity]: ) - -# todo amats - get_all_desc_by_signal_and_type. -# descending entity TYPES, and then descending entities within each type - - def get_all_desc_by_signal(*, db_session: Session, signal_id: int) -> list[Entity]: """Gets all entities for a specific signal in descending order.""" return ( diff --git a/src/dispatch/plugins/dispatch_slack/fields.py b/src/dispatch/plugins/dispatch_slack/fields.py index ff47c7acdfd4..d911f6326a31 100644 --- a/src/dispatch/plugins/dispatch_slack/fields.py +++ b/src/dispatch/plugins/dispatch_slack/fields.py @@ -682,7 +682,6 @@ def entity_select( return multi_select_block( placeholder="Select Entities", - # todo: use option groups here instead. options=entity_options[:100], # Limit the entities to the first 100 most recent action_id=action_id, block_id=block_id, From af9db5dd2dea33c9fb1544342644b13eb40399a8 Mon Sep 17 00:00:00 2001 From: Alicia Matsumoto Date: Fri, 21 Mar 2025 09:29:29 -0400 Subject: [PATCH 3/3] rename signal data test file to signal stats --- .../{test_signal_data_service.py => test_signal_stats_service.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/signal/{test_signal_data_service.py => test_signal_stats_service.py} (100%) diff --git a/tests/signal/test_signal_data_service.py b/tests/signal/test_signal_stats_service.py similarity index 100% rename from tests/signal/test_signal_data_service.py rename to tests/signal/test_signal_stats_service.py