diff --git a/strix/report/state.py b/strix/report/state.py index 3dbc9772..e4d1a16b 100644 --- a/strix/report/state.py +++ b/strix/report/state.py @@ -135,6 +135,9 @@ class ReportState: self._sarif_repo_ctx: dict[str, Any] | None = None self._sarif_repo_ctx_ready: bool = False + self.posthog_scan_ended_sent: bool = False + self.scarf_scan_ended_sent: bool = False + def get_run_dir(self) -> Path: if self._run_dir is None: run_dir_name = self.run_name if self.run_name else self.run_id diff --git a/strix/telemetry/posthog.py b/strix/telemetry/posthog.py index d1a9b460..566fed9b 100644 --- a/strix/telemetry/posthog.py +++ b/strix/telemetry/posthog.py @@ -82,6 +82,10 @@ def finding(severity: str) -> None: def end(report_state: "ReportState", exit_reason: str = "completed") -> None: + if report_state.posthog_scan_ended_sent: + return + report_state.posthog_scan_ended_sent = True + vulnerabilities_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0} for v in report_state.vulnerability_reports: sev = v.get("severity", "info").lower() diff --git a/strix/telemetry/scarf.py b/strix/telemetry/scarf.py index 037494bb..d3d3684e 100644 --- a/strix/telemetry/scarf.py +++ b/strix/telemetry/scarf.py @@ -85,6 +85,10 @@ def finding(severity: str) -> None: def end(report_state: ReportState, exit_reason: str = "completed") -> None: + if report_state.scarf_scan_ended_sent: + return + report_state.scarf_scan_ended_sent = True + vulnerabilities_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0} for v in report_state.vulnerability_reports: sev = v.get("severity", "info").lower() diff --git a/tests/test_telemetry.py b/tests/test_telemetry.py new file mode 100644 index 00000000..c43776bc --- /dev/null +++ b/tests/test_telemetry.py @@ -0,0 +1,39 @@ +"""Tests for scan telemetry emission.""" + +from __future__ import annotations + +from typing import Any + +import pytest + +from strix.report.state import ReportState +from strix.telemetry import posthog, scarf + + +@pytest.mark.parametrize( + ("backend", "sent_attribute"), + [ + (posthog, "posthog_scan_ended_sent"), + (scarf, "scarf_scan_ended_sent"), + ], +) +def test_scan_ended_is_sent_once_per_backend( + monkeypatch: pytest.MonkeyPatch, + backend: Any, + sent_attribute: str, +) -> None: + state = ReportState(run_name="test-run") + sent_events: list[tuple[str, dict[str, Any]]] = [] + monkeypatch.setattr( + backend, + "_send", + lambda event, properties: sent_events.append((event, properties)), + ) + + backend.end(state, exit_reason="finished_by_tool") + backend.end(state, exit_reason="user_exit") + + assert getattr(state, sent_attribute) is True + assert len(sent_events) == 1 + assert sent_events[0][0] == "scan_ended" + assert sent_events[0][1]["exit_reason"] == "finished_by_tool"