/* Copyright 2023, 2024 New Vector Ltd. SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ import opentelemetry, { type Span, type Attributes, type Context, } from "@opentelemetry/api"; import { type GroupCall, type MatrixClient, type MatrixEvent, type RoomMember, } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import { type CallError, type CallState, type MatrixCall, type VoipEvent, } from "matrix-js-sdk/src/webrtc/call"; import { type CallsByUserAndDevice, type GroupCallError, GroupCallEvent, type GroupCallStatsReport, } from "matrix-js-sdk/src/webrtc/groupCall"; import { type ConnectionStatsReport, type ByteSentStatsReport, type SummaryStatsReport, type CallFeedReport, } from "matrix-js-sdk/src/webrtc/stats/statsReport"; import { ElementCallOpenTelemetry } from "./otel"; import { ObjectFlattener } from "./ObjectFlattener"; import { OTelCall } from "./OTelCall"; /** * Represent the span of time which we intend to be joined to a group call */ export class OTelGroupCallMembership { private callMembershipSpan?: Span; private groupCallContext?: Context; private myUserId = "unknown"; private myDeviceId: string; private myMember?: RoomMember; private callsByCallId = new Map(); private statsReportSpan: { span: Span | undefined; stats: OTelStatsReportEvent[]; }; private readonly speakingSpans = new Map>(); public constructor( private groupCall: GroupCall, client: MatrixClient, ) { const clientId = client.getUserId(); if (clientId) { this.myUserId = clientId; const myMember = groupCall.room.getMember(clientId); if (myMember) { this.myMember = myMember; } } this.myDeviceId = client.getDeviceId() || "unknown"; this.statsReportSpan = { span: undefined, stats: [] }; this.groupCall.on(GroupCallEvent.CallsChanged, this.onCallsChanged); } public dispose(): void { this.groupCall.removeListener( GroupCallEvent.CallsChanged, this.onCallsChanged, ); } public onJoinCall(): void { if (!ElementCallOpenTelemetry.instance) return; if (this.callMembershipSpan !== undefined) { logger.warn("Call membership span is already started"); return; } // Create the main span that tracks the time we intend to be in the call this.callMembershipSpan = ElementCallOpenTelemetry.instance.tracer.startSpan( "matrix.groupCallMembership", ); this.callMembershipSpan.setAttribute( "matrix.confId", this.groupCall.groupCallId, ); this.callMembershipSpan.setAttribute("matrix.userId", this.myUserId); this.callMembershipSpan.setAttribute("matrix.deviceId", this.myDeviceId); this.callMembershipSpan.setAttribute( "matrix.displayName", this.myMember ? this.myMember.name : "unknown-name", ); this.groupCallContext = opentelemetry.trace.setSpan( opentelemetry.context.active(), this.callMembershipSpan, ); this.callMembershipSpan?.addEvent("matrix.joinCall"); } public onLeaveCall(): void { if (this.callMembershipSpan === undefined) { logger.warn("Call membership span is already ended"); return; } this.callMembershipSpan.addEvent("matrix.leaveCall"); // and end the span to indicate we've left this.callMembershipSpan.end(); this.callMembershipSpan = undefined; this.groupCallContext = undefined; } public onUpdateRoomState(event: MatrixEvent): void { if ( !event || (!event.getType().startsWith("m.call") && !event.getType().startsWith("org.matrix.msc3401.call")) ) { return; } this.callMembershipSpan?.addEvent( `matrix.roomStateEvent_${event.getType()}`, ObjectFlattener.flattenVoipEvent(event.getContent()), ); } public onCallsChanged(calls: CallsByUserAndDevice): void { for (const [userId, userCalls] of calls.entries()) { for (const [deviceId, call] of userCalls.entries()) { if (!this.callsByCallId.has(call.callId)) { if (ElementCallOpenTelemetry.instance) { const span = ElementCallOpenTelemetry.instance.tracer.startSpan( `matrix.call`, undefined, this.groupCallContext, ); // XXX: anonymity span.setAttribute("matrix.call.target.userId", userId); span.setAttribute("matrix.call.target.deviceId", deviceId); const displayName = this.groupCall.room.getMember(userId)?.name ?? "unknown"; span.setAttribute("matrix.call.target.displayName", displayName); this.callsByCallId.set( call.callId, new OTelCall(userId, deviceId, call, span), ); } } } } for (const callTrackingInfo of this.callsByCallId.values()) { const userCalls = calls.get(callTrackingInfo.userId); if ( !userCalls || !userCalls.has(callTrackingInfo.deviceId) || userCalls.get(callTrackingInfo.deviceId)?.callId !== callTrackingInfo.call.callId ) { callTrackingInfo.end(); this.callsByCallId.delete(callTrackingInfo.call.callId); } } } public onCallStateChange(call: MatrixCall, newState: CallState): void { const callTrackingInfo = this.callsByCallId.get(call.callId); if (!callTrackingInfo) { logger.error(`Got call state change for unknown call ID ${call.callId}`); return; } callTrackingInfo.span.addEvent("matrix.call.stateChange", { state: newState, }); } public onSendEvent(call: MatrixCall, event: VoipEvent): void { const eventType = event.eventType as string; if ( !eventType.startsWith("m.call") && !eventType.startsWith("org.matrix.call") ) return; const callTrackingInfo = this.callsByCallId.get(call.callId); if (!callTrackingInfo) { logger.error(`Got call send event for unknown call ID ${call.callId}`); return; } if (event.type === "toDevice") { callTrackingInfo.span.addEvent( `matrix.sendToDeviceEvent_${event.eventType}`, ObjectFlattener.flattenVoipEvent(event), ); } else if (event.type === "sendEvent") { callTrackingInfo.span.addEvent( `matrix.sendToRoomEvent_${event.eventType}`, ObjectFlattener.flattenVoipEvent(event), ); } } public onReceivedVoipEvent(event: MatrixEvent): void { // These come straight from CallEventHandler so don't have // a call already associated (in principle we could receive // events for calls we don't know about). const callId = event.getContent().call_id; if (!callId) { this.callMembershipSpan?.addEvent("matrix.receive_voip_event_no_callid", { "sender.userId": event.getSender(), }); logger.error("Received call event with no call ID!"); return; } const call = this.callsByCallId.get(callId); if (!call) { this.callMembershipSpan?.addEvent( "matrix.receive_voip_event_unknown_callid", { "sender.userId": event.getSender(), }, ); logger.error("Received call event for unknown call ID " + callId); return; } call.span.addEvent("matrix.receive_voip_event", { "sender.userId": event.getSender(), ...ObjectFlattener.flattenVoipEvent(event.getContent()), }); } public onToggleMicrophoneMuted(newValue: boolean): void { this.callMembershipSpan?.addEvent("matrix.toggleMicMuted", { "matrix.microphone.muted": newValue, }); } public onSetMicrophoneMuted(setMuted: boolean): void { this.callMembershipSpan?.addEvent("matrix.setMicMuted", { "matrix.microphone.muted": setMuted, }); } public onToggleLocalVideoMuted(newValue: boolean): void { this.callMembershipSpan?.addEvent("matrix.toggleVidMuted", { "matrix.video.muted": newValue, }); } public onSetLocalVideoMuted(setMuted: boolean): void { this.callMembershipSpan?.addEvent("matrix.setVidMuted", { "matrix.video.muted": setMuted, }); } public onToggleScreensharing(newValue: boolean): void { this.callMembershipSpan?.addEvent("matrix.setVidMuted", { "matrix.screensharing.enabled": newValue, }); } public onSpeaking( member: RoomMember, deviceId: string, speaking: boolean, ): void { if (speaking) { // Ensure that there's an audio activity span for this speaker let deviceMap = this.speakingSpans.get(member); if (deviceMap === undefined) { deviceMap = new Map(); this.speakingSpans.set(member, deviceMap); } if (!deviceMap.has(deviceId)) { const span = ElementCallOpenTelemetry.instance.tracer.startSpan( "matrix.audioActivity", undefined, this.groupCallContext, ); span.setAttribute("matrix.userId", member.userId); span.setAttribute("matrix.displayName", member.rawDisplayName); deviceMap.set(deviceId, span); } } else { // End the audio activity span for this speaker, if any const deviceMap = this.speakingSpans.get(member); deviceMap?.get(deviceId)?.end(); deviceMap?.delete(deviceId); if (deviceMap?.size === 0) this.speakingSpans.delete(member); } } public onCallError(error: CallError, call: MatrixCall): void { const callTrackingInfo = this.callsByCallId.get(call.callId); if (!callTrackingInfo) { logger.error(`Got error for unknown call ID ${call.callId}`); return; } callTrackingInfo.span.recordException(error); } public onGroupCallError(error: GroupCallError): void { this.callMembershipSpan?.recordException(error); } public onUndecryptableToDevice(event: MatrixEvent): void { this.callMembershipSpan?.addEvent("matrix.toDevice.undecryptable", { "sender.userId": event.getSender(), }); } public onCallFeedStatsReport( report: GroupCallStatsReport, ): void { if (!ElementCallOpenTelemetry.instance) return; let call: OTelCall | undefined; const callId = report.report?.callId; if (callId) { call = this.callsByCallId.get(callId); } if (!call) { this.callMembershipSpan?.addEvent( OTelStatsReportType.CallFeedReport + "_unknown_callId", { "call.callId": callId, "call.opponentMemberId": report.report?.opponentMemberId ? report.report?.opponentMemberId : "unknown", }, ); logger.error( `Received ${OTelStatsReportType.CallFeedReport} with unknown call ID: ${callId}`, ); return; } else { call.onCallFeedStats(report.report.callFeeds); call.onTransceiverStats(report.report.transceiver); } } public onConnectionStatsReport( statsReport: GroupCallStatsReport, ): void { this.buildCallStatsSpan( OTelStatsReportType.ConnectionReport, statsReport.report, ); } public onByteSentStatsReport( statsReport: GroupCallStatsReport, ): void { this.buildCallStatsSpan( OTelStatsReportType.ByteSentReport, statsReport.report, ); } public buildCallStatsSpan( type: OTelStatsReportType, report: ByteSentStatsReport | ConnectionStatsReport, ): void { if (!ElementCallOpenTelemetry.instance) return; let call: OTelCall | undefined; const callId = report?.callId; if (callId) { call = this.callsByCallId.get(callId); } if (!call) { this.callMembershipSpan?.addEvent(type + "_unknown_callid", { "call.callId": callId, "call.opponentMemberId": report.opponentMemberId ? report.opponentMemberId : "unknown", }); logger.error(`Received ${type} with unknown call ID: ${callId}`); return; } const data = ObjectFlattener.flattenReportObject(type, report); const ctx = opentelemetry.trace.setSpan( opentelemetry.context.active(), call.span, ); const options = { links: [ { context: call.span.spanContext(), }, ], }; const span = ElementCallOpenTelemetry.instance.tracer.startSpan( type, options, ctx, ); span.setAttribute("matrix.callId", callId ?? "unknown"); span.setAttribute( "matrix.opponentMemberId", report.opponentMemberId ? report.opponentMemberId : "unknown", ); span.addEvent("matrix.call.connection_stats_event", data); span.end(); } public onSummaryStatsReport( statsReport: GroupCallStatsReport, ): void { if (!ElementCallOpenTelemetry.instance) return; const type = OTelStatsReportType.SummaryReport; const data = ObjectFlattener.flattenSummaryStatsReportObject(statsReport); if (this.statsReportSpan.span === undefined && this.callMembershipSpan) { const ctx = opentelemetry.trace.setSpan( opentelemetry.context.active(), this.callMembershipSpan, ); const span = ElementCallOpenTelemetry.instance?.tracer.startSpan( "matrix.groupCallMembership.summaryReport", undefined, ctx, ); if (span === undefined) { return; } span.setAttribute("matrix.confId", this.groupCall.groupCallId); span.setAttribute("matrix.userId", this.myUserId); span.setAttribute( "matrix.displayName", this.myMember ? this.myMember.name : "unknown-name", ); span.addEvent(type, data); span.end(); } } } interface OTelStatsReportEvent { type: OTelStatsReportType; data: Attributes; } enum OTelStatsReportType { ConnectionReport = "matrix.call.stats.connection", ByteSentReport = "matrix.call.stats.byteSent", SummaryReport = "matrix.stats.summary", CallFeedReport = "matrix.stats.call_feed", }