Skip to content

feat(llc): Client call event reporting (join telemetry)#1268

Open
Brazol wants to merge 2 commits into
mainfrom
feat/client-side-events
Open

feat(llc): Client call event reporting (join telemetry)#1268
Brazol wants to merge 2 commits into
mainfrom
feat/client-side-events

Conversation

@Brazol

@Brazol Brazol commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

resolving: FLU-504

This PR adds client-side call event reporting to the core client, a telemetry layer that tracks the full call-join lifecycle and reports each stage (initiated → completed with success/failure) to the backend. It gives us end-to-end visibility into where and why joins fail, correlated across the coordinator connection, the join attempt, and each peer connection.

Each stage emits an initiated event and, once resolved, a paired completed event (outcome: success|failure); the two share a stage_id.

Stage When Source
JoinInitiated call.join() is called (single-shot) Call.join()
CoordinatorWS Coordinator socket connect/reconnect (connection-scoped, carries coordinator_connect_id, no join_attempt_id) CoordinatorWebSocket connection state
CoordinatorJoin REST joinCall request Call._performJoinCallRequest
WSJoin SFU signaling WS connect + join handshake CallSession.start
PeerConnectionConnect Publisher & subscriber PC connect (per role; ICE→ICE_CONNECTIVITY_FAILED, PC→DTLS_CONNECTIVITY_FAILED; tracks was_previously_connected + ice_state) RtcManager / TracedStreamPeerConnection
FirstVideoFrame First decoded frame on a remote video track (headless RTCVideoRenderer.onFirstFrameRendered) RtcManager
FirstAudioFrame First inbound audio packets (packetsReceived > 0, fed from the existing periodic stats loop) RtcManager

Not included / follow-ups

  • MediaDevicePermission stage - deferred (needs an OS permission-status source. Flutter core doesn't own the permission request).

Summary by CodeRabbit

  • New Features

    • Added optional call and WebRTC telemetry reporting for join, reconnect, connection, and first audio/video frame events.
    • Improved reporting of call lifecycle outcomes, including success, failure, timeout, and disconnect reasons.
    • Added support for event batching with retry handling for more reliable delivery.
  • Bug Fixes

    • Better detection and reporting of connection failures and offline conditions.
    • Ensures telemetry is cleaned up when calls end or the app is disposed.

@Brazol Brazol requested a review from a team as a code owner July 3, 2026 11:28
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a client-side telemetry subsystem for tracking call join lifecycle events. New modules define event data models, error-to-failure mapping, HTTP transport with retry, and a ClientEventReporter for stage-based reporting. These are wired into StreamVideo, coordinator websocket, SFU join, call join/leave/reconnect, and WebRTC peer connection/media frame flows, with accompanying tests.

Changes

Client Event Telemetry

Layer / File(s) Summary
Telemetry event types and data models
lib/src/telemetry/client_event_types.dart, lib/src/telemetry/client_event.dart
Defines stage/type/outcome/ICE-state/failure-code/join-reason enums and ClientEvent/ClientEventIdentity/ClientEventDetails models with JSON serialization.
Error mapping and event transport
lib/src/telemetry/client_event_error_mapper.dart, lib/src/telemetry/client_event_transport.dart
Maps thrown SDK errors to ClientEventFailure codes and implements HTTP batch transport with retry/backoff and offline detection.
ClientEventReporter interface and implementation
lib/src/telemetry/client_event_reporter.dart, test/src/telemetry/client_event_reporter_test.dart
Implements the reporter interface, stateful call/stage tracking (register/begin/complete/fail/abort/dispose), a no-op variant, and unit tests.
StreamVideo and coordinator wiring
lib/src/stream_video.dart, lib/src/coordinator/open_api/coordinator_client_open_api.dart, lib/src/coordinator/open_api/coordinator_ws.dart
Constructs and disposes the reporter in StreamVideo, propagates it through buildCoordinatorClient/CoordinatorClientOpenApi, and reports coordinator websocket connection lifecycle stages.
Call join/leave and SFU join stage reporting
CHANGELOG.md, lib/src/call/call.dart, lib/src/call/session/call_session.dart
Reports joinInitiated/coordinatorJoin/wsJoin stages, forwards subscriber stats, emits newJoinAttempt on reconnect, and reports abort/unregister on leave.
Peer connection and media frame reporters
lib/src/telemetry/peer_connection_connect_reporter.dart, lib/src/telemetry/media_frame_reporter.dart, lib/src/webrtc/peer_connection.dart, lib/src/webrtc/rtc_manager.dart, lib/src/webrtc/rtc_manager_factory.dart, test/src/telemetry/*, test/test_helpers.dart
Adds ICE/DTLS connect reporting and first-frame media reporting, exposes raw state callbacks on StreamPeerConnection, wires reporters/sfuId into RtcManager, and updates test mocks.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
    participant Call
    participant ClientEventReporter
    participant CallSession
    participant CoordinatorWS
    participant ClientEventTransport

    Call->>ClientEventReporter: registerCall(cid)
    Call->>ClientEventReporter: reportEvent(joinInitiated)
    CoordinatorWS->>ClientEventReporter: beginStage(coordinatorWs)
    CoordinatorWS-->>ClientEventReporter: completeStage/failStage
    Call->>ClientEventReporter: beginStage(coordinatorJoin)
    Call-->>ClientEventReporter: completeStage(setCallSessionId)
    CallSession->>ClientEventReporter: beginStage(wsJoin)
    CallSession-->>ClientEventReporter: completeStage/failStageWithError
    ClientEventReporter->>ClientEventTransport: send(events) [fire-and-forget]
    ClientEventTransport-->>ClientEventTransport: POST with retry/backoff
    Call->>ClientEventReporter: newJoinAttempt(reason) [on reconnect]
    Call->>ClientEventReporter: abort(code) / unregisterCall(cid) [on leave]
Loading

Possibly related PRs

  • GetStream/stream-video-flutter#1263: Overlaps with telemetry instrumentation in join()/_performJoinCallRequest()/_reconnect() in call.dart via shared reconnect/rejoin strategy code paths.

Suggested reviewers: xsahil03x, renefloor

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is informative, but it does not follow the required template and is missing Goal, Implementation details, UI Changes, Testing, and checklists. Rewrite the PR description to match the template: add Goal, Implementation details, UI Changes or state none, Testing, and both checklists.
✅ Passed checks (4 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly describes the main change: client-side call event reporting for join telemetry.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/client-side-events

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 68.21053% with 151 lines in your changes missing coverage. Please review.
✅ Project coverage is 10.72%. Comparing base (a5eeb44) to head (c0cd585).

Files with missing lines Patch % Lines
...kages/stream_video/lib/src/webrtc/rtc_manager.dart 0.00% 35 Missing ⚠️
...video/lib/src/telemetry/client_event_reporter.dart 78.07% 25 Missing ⚠️
..._video/lib/src/telemetry/media_frame_reporter.dart 52.50% 19 Missing ⚠️
...o/lib/src/coordinator/open_api/coordinator_ws.dart 33.33% 16 Missing ⚠️
...ideo/lib/src/telemetry/client_event_transport.dart 63.15% 14 Missing ⚠️
...tream_video/lib/src/call/session/call_session.dart 0.00% 12 Missing ⚠️
...am_video/lib/src/telemetry/client_event_types.dart 70.00% 12 Missing ⚠️
...o/lib/src/telemetry/client_event_error_mapper.dart 64.51% 11 Missing ⚠️
packages/stream_video/lib/src/call/call.dart 85.00% 3 Missing ⚠️
packages/stream_video/lib/src/stream_video.dart 83.33% 3 Missing ⚠️
... and 1 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1268      +/-   ##
==========================================
+ Coverage   10.16%   10.72%   +0.56%     
==========================================
  Files         679      686       +7     
  Lines       49765    50240     +475     
==========================================
+ Hits         5059     5389     +330     
- Misses      44706    44851     +145     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Brazol

Brazol commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/stream_video/lib/src/call/call.dart (1)

2271-2288: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Call.end() never unregisters/aborts the call in ClientEventReporter.

The new abort(...)/unregisterCall(...) cleanup was added only to leave(). However end() (Lines 941-970) calls _disconnect(...) directly and never calls leave(), so ending a call via Call.end() (e.g. host ending the call for everyone) never calls clientEventReporter.abort/unregisterCall. The _onStateChanged handler's CallStatusDisconnected branch also only calls _clear(...), not the reporter.

Per the cited reporter snippet, unregisterCall removes the call's entry from its internal _calls map stage_id -> pending pair (both call- and connection-scoped) ... void unregisterCall(StreamCallCid cid) { abort(cid, ClientEventStandardCode.clientAborted); _calls.remove(cid.value); — skipping it means the entry (and any still-pending stages) is never cleaned up for calls ended via end(), leaking state and leaving telemetry stages unresolved.

Move the cleanup into the shared _disconnect() helper (called by both leave() and end()) instead of only leave().

🔧 Proposed fix
   Future<Result<None>> leave({DisconnectReason? reason}) async {
     _logger.i(() => '[leave] reason: $reason');
 
     final abortCode = switch (reason) {
       DisconnectReasonEnded() ||
       DisconnectReasonCallEnded() => ClientEventStandardCode.backendLeave,
       // Reconnection gave up — treat as a device-offline
       DisconnectReasonReconnectionFailed() =>
         ClientEventStandardCode.networkOffline,
       _ => ClientEventStandardCode.clientAborted,
     };
-    _streamVideo.clientEventReporter
-      ..abort(callCid, abortCode)
-      ..unregisterCall(callCid);
 
     try {
       final didDisconnect = await _disconnect(
-        sfuLeaveReason: _sfuLeaveReason(reason),
+        sfuLeaveReason: _sfuLeaveReason(reason),
+        abortCode: abortCode,
       );
-  Future<bool> _disconnect({required String sfuLeaveReason}) async {
+  Future<bool> _disconnect({
+    required String sfuLeaveReason,
+    ClientEventStandardCode abortCode = ClientEventStandardCode.clientAborted,
+  }) async {
     if (_leaveCallTriggered) {
       _logger.i(() => '[disconnect] rejected (already disconnecting)');
       return false;
     }
 
     _leaveCallTriggered = true;
+    _streamVideo.clientEventReporter
+      ..abort(callCid, abortCode)
+      ..unregisterCall(callCid);

cid value -> call state. final Map<String, _CallState> _calls = {}; ... void unregisterCall(StreamCallCid cid) { abort(cid, ClientEventStandardCode.clientAborted); _calls.remove(cid.value); _logger.d(() => '[unregisterCall] cid: ${cid.value}');

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/stream_video/lib/src/call/call.dart` around lines 2271 - 2288,
`Call.end()` is bypassing the `ClientEventReporter` cleanup that `leave()` now
performs. Move the `clientEventReporter.abort(...)` and
`clientEventReporter.unregisterCall(...)` calls into the shared
`_disconnect(...)` path in `Call`, so both `leave()` and `end()` always clean up
reporter state. Keep the abort-code selection tied to the existing
`leave({reason})` logic, and ensure `_onStateChanged`/`CallStatusDisconnected`
does not rely on `_clear(...)` alone for reporter cleanup.
🧹 Nitpick comments (2)
packages/stream_video/test/src/telemetry/media_frame_reporter_test.dart (1)

1-91: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No coverage for the video first-frame path.

All three tests exercise only onRemoteAudioTrack/onSubscriberStats (FirstAudioFrame). onRemoteVideoTrack/_reportVideo (renderer creation, onFirstFrameRendered firing, single-shot behavior, and renderer disposal) has no test coverage in this file.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/stream_video/test/src/telemetry/media_frame_reporter_test.dart`
around lines 1 - 91, Add test coverage for the video first-frame flow in
MediaFrameReporter, since this file only verifies the audio path through
onRemoteAudioTrack and onSubscriberStats. Create tests that exercise
onRemoteVideoTrack and the _reportVideo behavior, asserting that
onFirstFrameRendered emits the expected first-frame event once, that it does not
fire before a remote video track is registered, and that the renderer is
disposed after the event. Use the existing
ClientEventReporter/MediaFrameReporter setup and mirror the single-shot
assertions already used for FirstAudioFrame.
packages/stream_video/lib/src/call/session/call_session.dart (1)

452-569: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

fastReconnect() has no wsJoin stage telemetry, unlike start().

start() wraps the SFU WS connect/join-response flow with beginStage/completeStage/failStage for ClientEventStage.wsJoin, but fastReconnect() performs an equivalent SFU WS join (sfuWS.recreate(), sending the join request, waiting for SfuJoinResponseEvent) with no telemetry at all — including its own explicit failure path at Lines 482-487. Since fast-reconnect is a common, failure-prone path, consider wiring the same stage reporting here for consistent join-telemetry coverage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/stream_video/lib/src/call/session/call_session.dart` around lines
452 - 569, `fastReconnect()` is missing `wsJoin` stage telemetry for the SFU WS
recreate/join flow, unlike `start()`. Update `CallSession.fastReconnect` to wrap
the `sfuWS.recreate()`, join request send, and `SfuJoinResponseEvent` wait with
the same `beginStage`/`completeStage`/`failStage` reporting used in `start()`
for `ClientEventStage.wsJoin`, including the explicit failure return path when
`wsResult.isFailure` is true.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/stream_video/lib/src/telemetry/client_event_transport.dart`:
- Around line 60-111: The `ClientEventTransport.send` flow only catches failures
around `_httpClient.post`, so exceptions from `jsonEncode` or
`authHeadersProvider?.call()` can escape as unhandled Future errors. Move the
body/header preparation into the same protected section as the request, or wrap
the entire `send()` method in try/catch, so any encoding or auth-header failure
is logged and treated like a dropped telemetry attempt rather than breaking
`ClientEventReporter._dispatch`.

In `@packages/stream_video/lib/src/telemetry/media_frame_reporter.dart`:
- Around line 60-82: The duplicate-setup guard in media_frame_reporter.dart’s
onRemoteVideoTrack is only checked before awaiting initialize(), so a concurrent
second call for the same trackId can still overwrite _renderers and leak the
first renderer. After await renderer.initialize(), re-check _videoReported,
_disposed, and whether _renderers already contains trackId before assigning to
_renderers; if another renderer is already present, dispose the newly created
RTCVideoRenderer and return. Keep the fix localized to onRemoteVideoTrack and
preserve the existing cleanup path.
- Around line 50-56: The FirstAudioFrame check in onSubscriberStats is using
any(RtcInboundRtpAudioStream with packetsReceived > 0), which can fire for the
wrong remote audio stream when multiple tracks exist. Update the
MediaFrameReporter logic to correlate on the WebRTC receiver id from
RtcInboundRtpAudioStream.trackId instead of relying on _audioTrackId alone, and
thread that receiver id through the matching path so _reportAudio() only runs
for the intended stream.

---

Outside diff comments:
In `@packages/stream_video/lib/src/call/call.dart`:
- Around line 2271-2288: `Call.end()` is bypassing the `ClientEventReporter`
cleanup that `leave()` now performs. Move the `clientEventReporter.abort(...)`
and `clientEventReporter.unregisterCall(...)` calls into the shared
`_disconnect(...)` path in `Call`, so both `leave()` and `end()` always clean up
reporter state. Keep the abort-code selection tied to the existing
`leave({reason})` logic, and ensure `_onStateChanged`/`CallStatusDisconnected`
does not rely on `_clear(...)` alone for reporter cleanup.

---

Nitpick comments:
In `@packages/stream_video/lib/src/call/session/call_session.dart`:
- Around line 452-569: `fastReconnect()` is missing `wsJoin` stage telemetry for
the SFU WS recreate/join flow, unlike `start()`. Update
`CallSession.fastReconnect` to wrap the `sfuWS.recreate()`, join request send,
and `SfuJoinResponseEvent` wait with the same
`beginStage`/`completeStage`/`failStage` reporting used in `start()` for
`ClientEventStage.wsJoin`, including the explicit failure return path when
`wsResult.isFailure` is true.

In `@packages/stream_video/test/src/telemetry/media_frame_reporter_test.dart`:
- Around line 1-91: Add test coverage for the video first-frame flow in
MediaFrameReporter, since this file only verifies the audio path through
onRemoteAudioTrack and onSubscriberStats. Create tests that exercise
onRemoteVideoTrack and the _reportVideo behavior, asserting that
onFirstFrameRendered emits the expected first-frame event once, that it does not
fire before a remote video track is registered, and that the renderer is
disposed after the event. Use the existing
ClientEventReporter/MediaFrameReporter setup and mirror the single-shot
assertions already used for FirstAudioFrame.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c03b46e1-51ec-49ab-9be2-dad45c846a8c

📥 Commits

Reviewing files that changed from the base of the PR and between a5eeb44 and c0cd585.

📒 Files selected for processing (20)
  • packages/stream_video/CHANGELOG.md
  • packages/stream_video/lib/src/call/call.dart
  • packages/stream_video/lib/src/call/session/call_session.dart
  • packages/stream_video/lib/src/coordinator/open_api/coordinator_client_open_api.dart
  • packages/stream_video/lib/src/coordinator/open_api/coordinator_ws.dart
  • packages/stream_video/lib/src/stream_video.dart
  • packages/stream_video/lib/src/telemetry/client_event.dart
  • packages/stream_video/lib/src/telemetry/client_event_error_mapper.dart
  • packages/stream_video/lib/src/telemetry/client_event_reporter.dart
  • packages/stream_video/lib/src/telemetry/client_event_transport.dart
  • packages/stream_video/lib/src/telemetry/client_event_types.dart
  • packages/stream_video/lib/src/telemetry/media_frame_reporter.dart
  • packages/stream_video/lib/src/telemetry/peer_connection_connect_reporter.dart
  • packages/stream_video/lib/src/webrtc/peer_connection.dart
  • packages/stream_video/lib/src/webrtc/rtc_manager.dart
  • packages/stream_video/lib/src/webrtc/rtc_manager_factory.dart
  • packages/stream_video/test/src/telemetry/client_event_reporter_test.dart
  • packages/stream_video/test/src/telemetry/media_frame_reporter_test.dart
  • packages/stream_video/test/src/telemetry/peer_connection_connect_reporter_test.dart
  • packages/stream_video/test/test_helpers.dart

Comment thread packages/stream_video/lib/src/telemetry/client_event_transport.dart
Comment thread packages/stream_video/lib/src/telemetry/media_frame_reporter.dart
Comment thread packages/stream_video/lib/src/telemetry/media_frame_reporter.dart
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant