Emit session dropped event when stuck workflow is cleaned up

What does this MR do and why?

Emits agent_platform_session_dropped when CleanStuckWorkflowsService force-fails a stuck Duo Agent Platform session, so those sessions stop disappearing from completion-rate reporting.

CleanStuckWorkflowsService calls w.drop directly on the model. Every other transition to failed goes through UpdateWorkflowStatusService, which is the only place that emits the terminal analytics event. So a stuck-cleaned session was recorded as created plus started, and then nothing.

That matters because agent_platform_session_dropped is AI usage event id 20, which populates dropped_event_at in the ClickHouse agent_platform_sessions materialized view backing the completion_rate metric. The service does already emit cleanup_stuck_agent_platform_session, but that event is not in the Gitlab::Tracking::AiTracking registry and AiTracking.track_event returns early on unregistered events, so it reaches Snowplow only and never contributes a terminal state.

Net effect of this change: stuck sessions are now counted as dropped, and the completion rate reflects them.

Scope notes

  • No new event definition. ee/config/events/agent_platform_session_dropped.yml already exists.
  • No ClickHouse migration. agent_platform_sessions_mv already maps event = 20 to dropped_event_at and already filters event IN (8, 9, 19, 20, 21, 22).
  • No double counting. If the executor later PATCHes drop on an already-failed session, UpdateWorkflowStatusService returns early before emitting, and the MV's anyIf aggregation would collapse duplicates regardless.
  • This makes stuck sessions countable going forward. It does not retroactively explain the whole unaccounted bucket in the dashboards, which is also fed by sessions still in flight at query time and by abandoned input_required/paused sessions (those transitions emit no analytics event either). Historical numbers will not reconcile.

Redis counter batching (added after review)

The execute loop is now wrapped in Gitlab::InternalEvents.with_batched_redis_writes. Every internal event gets two default total-counter selection rules regardless of metric definitions (Gitlab::Tracking::EventDefinition#find_event_selection_rules), so track_fail does 4 Redis increments per workflow, up from 2 before this MR.

flush_redis_batch issues one write per unique key, and the keys are {event_counters}_<event_name> plus a week suffix, so they are identical for every workflow in a run. Batching per workflow would therefore re-flush the same 4 keys each time and save nothing; batching around the loop collapses 4 × N into 4. Verified locally: 3 stale sessions produce 12 deferred increments and 4 flushed keys.

Trade-off worth knowing: counter increments now flush once at the end of the run, so a hard kill mid-run loses that run's increments rather than just the tail. Sidekiq shuts down gracefully and the flush is in an ensure, and these are non-critical total counters, so this seems an acceptable trade.

Known gap, deliberately not fixed here

Concerns::WorkflowEventTracking#track_workflow_event passes only user and project, so namespace-level sessions lose namespace attribution. That is pre-existing and shared with UpdateWorkflowStatusService and CreateWorkflowService, so changing it would alter data for five other events. I reused the helper for consistency and added a spec asserting the current behaviour so it stays a known gap rather than an accident. Happy to split a follow-up if agent execution wants it fixed.

Why no changelog

Internal analytics instrumentation only. No user-facing behaviour change.

How to set up and validate locally

bundle exec rspec ee/spec/services/ai/duo_workflows/clean_stuck_workflows_service_spec.rb
bundle exec rspec ee/spec/workers/ai/duo_workflows/fail_stuck_workflows_worker_spec.rb

To confirm the assertions are not vacuous, stash the service change and re-run: three examples fail without it.

Manual check in a GDK:

  1. Create a project-level workflow and start it.
  2. Backdate updated_at past 30 minutes.
  3. Run Ai::DuoWorkflows::FailStuckWorkflowsWorker.new.perform.
  4. Confirm an Ai::UsageEvent row exists with event: :agent_platform_session_dropped. Before this change there is none.

References

Edited by Peter Gumeson

Merge request reports

Loading
Loading