diff --git a/strix/report/state.py b/strix/report/state.py index 1eff2850..6f111178 100644 --- a/strix/report/state.py +++ b/strix/report/state.py @@ -351,6 +351,15 @@ class ReportState: for key in ("requests", "input_tokens", "output_tokens", "total_tokens", "cost") } + def get_process_duration_seconds(self) -> float: + """Return this process's elapsed wall time for telemetry.""" + try: + start = datetime.fromisoformat(self.process_start_time.replace("Z", "+00:00")) + duration = (datetime.now(start.tzinfo) - start).total_seconds() + return max(0.0, duration) + except (ValueError, TypeError, AttributeError): + return 0.0 + def get_total_llm_cost(self) -> float: """Live accumulated LLM cost, independent of the persisted run-record snapshot.""" return self._llm_usage.total_cost diff --git a/strix/telemetry/posthog.py b/strix/telemetry/posthog.py index fa89fd0b..756f163c 100644 --- a/strix/telemetry/posthog.py +++ b/strix/telemetry/posthog.py @@ -1,5 +1,4 @@ import logging -from datetime import datetime from typing import TYPE_CHECKING, Any import requests @@ -105,13 +104,7 @@ def end(report_state: "ReportState", exit_reason: str = "completed") -> None: if sev in vulnerabilities_counts: vulnerabilities_counts[sev] += 1 - duration = 0.0 - try: - start = datetime.fromisoformat(report_state.process_start_time.replace("Z", "+00:00")) - end_iso = report_state.end_time or datetime.now(start.tzinfo).isoformat() - duration = (datetime.fromisoformat(end_iso.replace("Z", "+00:00")) - start).total_seconds() - except (ValueError, TypeError, AttributeError): - pass + duration = report_state.get_process_duration_seconds() llm_props: dict[str, int | float] = {} try: diff --git a/strix/telemetry/scarf.py b/strix/telemetry/scarf.py index 495c0f65..22767424 100644 --- a/strix/telemetry/scarf.py +++ b/strix/telemetry/scarf.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging import urllib.parse -from datetime import datetime from typing import TYPE_CHECKING, Any import requests @@ -114,15 +113,7 @@ def end(report_state: ReportState, exit_reason: str = "completed") -> None: if sev in vulnerabilities_counts: vulnerabilities_counts[sev] += 1 - duration = 0.0 - try: - scan_start = datetime.fromisoformat(report_state.process_start_time.replace("Z", "+00:00")) - end_iso = report_state.end_time or datetime.now(scan_start.tzinfo).isoformat() - duration = ( - datetime.fromisoformat(end_iso.replace("Z", "+00:00")) - scan_start - ).total_seconds() - except (ValueError, TypeError, AttributeError): - pass + duration = report_state.get_process_duration_seconds() llm_props: dict[str, int | float] = {} try: diff --git a/tests/test_telemetry_resume.py b/tests/test_telemetry_resume.py index 60804bc7..9a0f92fd 100644 --- a/tests/test_telemetry_resume.py +++ b/tests/test_telemetry_resume.py @@ -40,6 +40,8 @@ def test_scan_ended_reports_resumed_usage_delta( model="unknown", ) initial.record_observed_llm_cost(1.25) + initial.end_time = (datetime.now(UTC) - timedelta(hours=1)).isoformat() + initial.run_record["end_time"] = initial.end_time initial.save_run_data() resumed = ReportState(run_name="resumed") @@ -50,7 +52,6 @@ def test_scan_ended_reports_resumed_usage_delta( model="unknown", ) resumed.record_observed_llm_cost(0.75) - resumed.end_time = (datetime.now(UTC) + timedelta(seconds=2)).isoformat() sent: list[dict[str, Any]] = [] monkeypatch.setattr(telemetry, "_send", lambda _event, props: _capture(sent, props)) @@ -61,7 +62,7 @@ def test_scan_ended_reports_resumed_usage_delta( assert sent[0]["llm_output_tokens"] == 50 assert sent[0]["llm_tokens"] == 350 assert sent[0]["llm_cost"] == pytest.approx(0.75) - assert sent[0]["duration_seconds"] <= 2 + assert 0 <= sent[0]["duration_seconds"] <= 2 @pytest.mark.parametrize("telemetry", [posthog, scarf])