From 1fda5c79202ea51c2c007ec03aa2fafbcc28af67 Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 24 Nov 2025 10:17:29 +0100 Subject: [PATCH 1/3] remove otel to see what impact it has on tests. --- src/initializer.tsx | 11 - src/otel/OTelCall.ts | 188 ------- src/otel/OTelCallAbstractMediaStreamSpan.ts | 69 --- src/otel/OTelCallFeedMediaStreamSpan.ts | 64 --- src/otel/OTelCallMediaStreamTrackSpan.ts | 69 --- .../OTelCallTransceiverMediaStreamSpan.ts | 61 --- src/otel/OTelGroupCallMembership.ts | 477 ------------------ src/otel/ObjectFlattener.test.ts | 266 ---------- src/otel/ObjectFlattener.ts | 100 ---- src/otel/otel.test.ts | 79 --- src/otel/otel.ts | 117 ----- src/room/InCallView.tsx | 2 - src/settings/submit-rageshake.ts | 9 - 13 files changed, 1512 deletions(-) delete mode 100644 src/otel/OTelCall.ts delete mode 100644 src/otel/OTelCallAbstractMediaStreamSpan.ts delete mode 100644 src/otel/OTelCallFeedMediaStreamSpan.ts delete mode 100644 src/otel/OTelCallMediaStreamTrackSpan.ts delete mode 100644 src/otel/OTelCallTransceiverMediaStreamSpan.ts delete mode 100644 src/otel/OTelGroupCallMembership.ts delete mode 100644 src/otel/ObjectFlattener.test.ts delete mode 100644 src/otel/ObjectFlattener.ts delete mode 100644 src/otel/otel.test.ts delete mode 100644 src/otel/otel.ts diff --git a/src/initializer.tsx b/src/initializer.tsx index d0797e9d1..2ecd11625 100644 --- a/src/initializer.tsx +++ b/src/initializer.tsx @@ -26,7 +26,6 @@ import { import { getUrlParams } from "./UrlParams"; import { Config } from "./config/Config"; -import { ElementCallOpenTelemetry } from "./otel/otel"; import { platform } from "./Platform"; import { isFailure } from "./utils/fetch"; @@ -101,7 +100,6 @@ enum LoadState { class DependencyLoadStates { public config: LoadState = LoadState.None; public sentry: LoadState = LoadState.None; - public openTelemetry: LoadState = LoadState.None; public allDepsAreLoaded(): boolean { return !Object.values(this).some((s) => s !== LoadState.Loaded); @@ -266,15 +264,6 @@ export class Initializer { this.loadStates.sentry = LoadState.Loaded; } - // OpenTelemetry (also only after config loaded) - if ( - this.loadStates.openTelemetry === LoadState.None && - this.loadStates.config === LoadState.Loaded - ) { - ElementCallOpenTelemetry.globalInit(); - this.loadStates.openTelemetry = LoadState.Loaded; - } - if (this.loadStates.allDepsAreLoaded()) { // resolve if there is no dependency that is not loaded resolve(); diff --git a/src/otel/OTelCall.ts b/src/otel/OTelCall.ts deleted file mode 100644 index e70cedf2d..000000000 --- a/src/otel/OTelCall.ts +++ /dev/null @@ -1,188 +0,0 @@ -/* -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 { type Span } from "@opentelemetry/api"; -import { type MatrixCall } from "matrix-js-sdk"; -import { CallEvent } from "matrix-js-sdk/lib/webrtc/call"; -import { - type TransceiverStats, - type CallFeedStats, -} from "matrix-js-sdk/lib/webrtc/stats/statsReport"; - -import { ObjectFlattener } from "./ObjectFlattener"; -import { ElementCallOpenTelemetry } from "./otel"; -import { type OTelCallAbstractMediaStreamSpan } from "./OTelCallAbstractMediaStreamSpan"; -import { OTelCallTransceiverMediaStreamSpan } from "./OTelCallTransceiverMediaStreamSpan"; -import { OTelCallFeedMediaStreamSpan } from "./OTelCallFeedMediaStreamSpan"; - -type StreamId = string; -type MID = string; - -/** - * Tracks an individual call within a group call, either to a full-mesh peer or a focus - */ -export class OTelCall { - private readonly trackFeedSpan = new Map< - StreamId, - OTelCallAbstractMediaStreamSpan - >(); - private readonly trackTransceiverSpan = new Map< - MID, - OTelCallAbstractMediaStreamSpan - >(); - - public constructor( - public userId: string, - public deviceId: string, - public call: MatrixCall, - public span: Span, - ) { - if (call.peerConn) { - this.addCallPeerConnListeners(); - } else { - this.call.once( - CallEvent.PeerConnectionCreated, - this.addCallPeerConnListeners, - ); - } - } - - public dispose(): void { - this.call.peerConn?.removeEventListener( - "connectionstatechange", - this.onCallConnectionStateChanged, - ); - this.call.peerConn?.removeEventListener( - "signalingstatechange", - this.onCallSignalingStateChanged, - ); - this.call.peerConn?.removeEventListener( - "iceconnectionstatechange", - this.onIceConnectionStateChanged, - ); - this.call.peerConn?.removeEventListener( - "icegatheringstatechange", - this.onIceGatheringStateChanged, - ); - this.call.peerConn?.removeEventListener( - "icecandidateerror", - this.onIceCandidateError, - ); - } - - private addCallPeerConnListeners = (): void => { - this.call.peerConn?.addEventListener( - "connectionstatechange", - this.onCallConnectionStateChanged, - ); - this.call.peerConn?.addEventListener( - "signalingstatechange", - this.onCallSignalingStateChanged, - ); - this.call.peerConn?.addEventListener( - "iceconnectionstatechange", - this.onIceConnectionStateChanged, - ); - this.call.peerConn?.addEventListener( - "icegatheringstatechange", - this.onIceGatheringStateChanged, - ); - this.call.peerConn?.addEventListener( - "icecandidateerror", - this.onIceCandidateError, - ); - }; - - public onCallConnectionStateChanged = (): void => { - this.span.addEvent("matrix.call.callConnectionStateChange", { - callConnectionState: this.call.peerConn?.connectionState, - }); - }; - - public onCallSignalingStateChanged = (): void => { - this.span.addEvent("matrix.call.callSignalingStateChange", { - callSignalingState: this.call.peerConn?.signalingState, - }); - }; - - public onIceConnectionStateChanged = (): void => { - this.span.addEvent("matrix.call.iceConnectionStateChange", { - iceConnectionState: this.call.peerConn?.iceConnectionState, - }); - }; - - public onIceGatheringStateChanged = (): void => { - this.span.addEvent("matrix.call.iceGatheringStateChange", { - iceGatheringState: this.call.peerConn?.iceGatheringState, - }); - }; - - public onIceCandidateError = (ev: Event): void => { - const flatObject = {}; - ObjectFlattener.flattenObjectRecursive(ev, flatObject, "error.", 0); - - this.span.addEvent("matrix.call.iceCandidateError", flatObject); - }; - - public onCallFeedStats(callFeeds: CallFeedStats[]): void { - let prvFeeds: StreamId[] = [...this.trackFeedSpan.keys()]; - - callFeeds.forEach((feed) => { - if (!this.trackFeedSpan.has(feed.stream)) { - this.trackFeedSpan.set( - feed.stream, - new OTelCallFeedMediaStreamSpan( - ElementCallOpenTelemetry.instance, - this.span, - feed, - ), - ); - } - this.trackFeedSpan.get(feed.stream)?.update(feed); - prvFeeds = prvFeeds.filter((prvStreamId) => prvStreamId !== feed.stream); - }); - - prvFeeds.forEach((prvStreamId) => { - this.trackFeedSpan.get(prvStreamId)?.end(); - this.trackFeedSpan.delete(prvStreamId); - }); - } - - public onTransceiverStats(transceiverStats: TransceiverStats[]): void { - let prvTransSpan: MID[] = [...this.trackTransceiverSpan.keys()]; - - transceiverStats.forEach((transStats) => { - if (!this.trackTransceiverSpan.has(transStats.mid)) { - this.trackTransceiverSpan.set( - transStats.mid, - new OTelCallTransceiverMediaStreamSpan( - ElementCallOpenTelemetry.instance, - this.span, - transStats, - ), - ); - } - this.trackTransceiverSpan.get(transStats.mid)?.update(transStats); - prvTransSpan = prvTransSpan.filter( - (prvStreamId) => prvStreamId !== transStats.mid, - ); - }); - - prvTransSpan.forEach((prvMID) => { - this.trackTransceiverSpan.get(prvMID)?.end(); - this.trackTransceiverSpan.delete(prvMID); - }); - } - - public end(): void { - this.trackFeedSpan.forEach((feedSpan) => feedSpan.end()); - this.trackTransceiverSpan.forEach((transceiverSpan) => - transceiverSpan.end(), - ); - this.span.end(); - } -} diff --git a/src/otel/OTelCallAbstractMediaStreamSpan.ts b/src/otel/OTelCallAbstractMediaStreamSpan.ts deleted file mode 100644 index 69e415475..000000000 --- a/src/otel/OTelCallAbstractMediaStreamSpan.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* -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 } from "@opentelemetry/api"; -import { type TrackStats } from "matrix-js-sdk/lib/webrtc/stats/statsReport"; - -import { type ElementCallOpenTelemetry } from "./otel"; -import { OTelCallMediaStreamTrackSpan } from "./OTelCallMediaStreamTrackSpan"; - -type TrackId = string; - -export abstract class OTelCallAbstractMediaStreamSpan { - protected readonly trackSpans = new Map< - TrackId, - OTelCallMediaStreamTrackSpan - >(); - public readonly span; - - public constructor( - protected readonly oTel: ElementCallOpenTelemetry, - protected readonly callSpan: Span, - protected readonly type: string, - ) { - const ctx = opentelemetry.trace.setSpan( - opentelemetry.context.active(), - callSpan, - ); - const options = { - links: [ - { - context: callSpan.spanContext(), - }, - ], - }; - this.span = oTel.tracer.startSpan(this.type, options, ctx); - } - - protected upsertTrackSpans(tracks: TrackStats[]): void { - let prvTracks: TrackId[] = [...this.trackSpans.keys()]; - tracks.forEach((t) => { - if (!this.trackSpans.has(t.id)) { - this.trackSpans.set( - t.id, - new OTelCallMediaStreamTrackSpan(this.oTel, this.span, t), - ); - } - this.trackSpans.get(t.id)?.update(t); - prvTracks = prvTracks.filter((prvTrackId) => prvTrackId !== t.id); - }); - - prvTracks.forEach((prvTrackId) => { - this.trackSpans.get(prvTrackId)?.end(); - this.trackSpans.delete(prvTrackId); - }); - } - - public abstract update(data: object): void; - - public end(): void { - this.trackSpans.forEach((tSpan) => { - tSpan.end(); - }); - this.span.end(); - } -} diff --git a/src/otel/OTelCallFeedMediaStreamSpan.ts b/src/otel/OTelCallFeedMediaStreamSpan.ts deleted file mode 100644 index 59c780a5c..000000000 --- a/src/otel/OTelCallFeedMediaStreamSpan.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* -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 { type Span } from "@opentelemetry/api"; -import { - type CallFeedStats, - type TrackStats, -} from "matrix-js-sdk/lib/webrtc/stats/statsReport"; - -import { type ElementCallOpenTelemetry } from "./otel"; -import { OTelCallAbstractMediaStreamSpan } from "./OTelCallAbstractMediaStreamSpan"; - -export class OTelCallFeedMediaStreamSpan extends OTelCallAbstractMediaStreamSpan { - private readonly prev: { isAudioMuted: boolean; isVideoMuted: boolean }; - - public constructor( - protected readonly oTel: ElementCallOpenTelemetry, - protected readonly callSpan: Span, - callFeed: CallFeedStats, - ) { - const postFix = - callFeed.type === "local" && callFeed.prefix === "from-call-feed" - ? "(clone)" - : ""; - super(oTel, callSpan, `matrix.call.feed.${callFeed.type}${postFix}`); - this.span.setAttribute("feed.streamId", callFeed.stream); - this.span.setAttribute("feed.type", callFeed.type); - this.span.setAttribute("feed.readFrom", callFeed.prefix); - this.span.setAttribute("feed.purpose", callFeed.purpose); - this.prev = { - isAudioMuted: callFeed.isAudioMuted, - isVideoMuted: callFeed.isVideoMuted, - }; - this.span.addEvent("matrix.call.feed.initState", this.prev); - } - - public update(callFeed: CallFeedStats): void { - if (this.prev.isAudioMuted !== callFeed.isAudioMuted) { - this.span.addEvent("matrix.call.feed.audioMuted", { - isAudioMuted: callFeed.isAudioMuted, - }); - this.prev.isAudioMuted = callFeed.isAudioMuted; - } - if (this.prev.isVideoMuted !== callFeed.isVideoMuted) { - this.span.addEvent("matrix.call.feed.isVideoMuted", { - isVideoMuted: callFeed.isVideoMuted, - }); - this.prev.isVideoMuted = callFeed.isVideoMuted; - } - - const trackStats: TrackStats[] = []; - if (callFeed.video) { - trackStats.push(callFeed.video); - } - if (callFeed.audio) { - trackStats.push(callFeed.audio); - } - this.upsertTrackSpans(trackStats); - } -} diff --git a/src/otel/OTelCallMediaStreamTrackSpan.ts b/src/otel/OTelCallMediaStreamTrackSpan.ts deleted file mode 100644 index c81acd4f5..000000000 --- a/src/otel/OTelCallMediaStreamTrackSpan.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* -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 { type TrackStats } from "matrix-js-sdk/lib/webrtc/stats/statsReport"; -import opentelemetry, { type Span } from "@opentelemetry/api"; - -import { type ElementCallOpenTelemetry } from "./otel"; - -export class OTelCallMediaStreamTrackSpan { - private readonly span: Span; - private prev: TrackStats; - - public constructor( - protected readonly oTel: ElementCallOpenTelemetry, - protected readonly streamSpan: Span, - data: TrackStats, - ) { - const ctx = opentelemetry.trace.setSpan( - opentelemetry.context.active(), - streamSpan, - ); - const options = { - links: [ - { - context: streamSpan.spanContext(), - }, - ], - }; - const type = `matrix.call.track.${data.label}.${data.kind}`; - this.span = oTel.tracer.startSpan(type, options, ctx); - this.span.setAttribute("track.trackId", data.id); - this.span.setAttribute("track.kind", data.kind); - this.span.setAttribute("track.constrainDeviceId", data.constrainDeviceId); - this.span.setAttribute("track.settingDeviceId", data.settingDeviceId); - this.span.setAttribute("track.label", data.label); - - this.span.addEvent("matrix.call.track.initState", { - readyState: data.readyState, - muted: data.muted, - enabled: data.enabled, - }); - this.prev = data; - } - - public update(data: TrackStats): void { - if (this.prev.muted !== data.muted) { - this.span.addEvent("matrix.call.track.muted", { muted: data.muted }); - } - if (this.prev.enabled !== data.enabled) { - this.span.addEvent("matrix.call.track.enabled", { - enabled: data.enabled, - }); - } - if (this.prev.readyState !== data.readyState) { - this.span.addEvent("matrix.call.track.readyState", { - readyState: data.readyState, - }); - } - this.prev = data; - } - - public end(): void { - this.span.end(); - } -} diff --git a/src/otel/OTelCallTransceiverMediaStreamSpan.ts b/src/otel/OTelCallTransceiverMediaStreamSpan.ts deleted file mode 100644 index 675d793ef..000000000 --- a/src/otel/OTelCallTransceiverMediaStreamSpan.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* -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 { type Span } from "@opentelemetry/api"; -import { - type TrackStats, - type TransceiverStats, -} from "matrix-js-sdk/lib/webrtc/stats/statsReport"; - -import { type ElementCallOpenTelemetry } from "./otel"; -import { OTelCallAbstractMediaStreamSpan } from "./OTelCallAbstractMediaStreamSpan"; - -export class OTelCallTransceiverMediaStreamSpan extends OTelCallAbstractMediaStreamSpan { - private readonly prev: { - direction: string; - currentDirection: string; - }; - - public constructor( - protected readonly oTel: ElementCallOpenTelemetry, - protected readonly callSpan: Span, - stats: TransceiverStats, - ) { - super(oTel, callSpan, `matrix.call.transceiver.${stats.mid}`); - this.span.setAttribute("transceiver.mid", stats.mid); - - this.prev = { - direction: stats.direction, - currentDirection: stats.currentDirection, - }; - this.span.addEvent("matrix.call.transceiver.initState", this.prev); - } - - public update(stats: TransceiverStats): void { - if (this.prev.currentDirection !== stats.currentDirection) { - this.span.addEvent("matrix.call.transceiver.currentDirection", { - currentDirection: stats.currentDirection, - }); - this.prev.currentDirection = stats.currentDirection; - } - if (this.prev.direction !== stats.direction) { - this.span.addEvent("matrix.call.transceiver.direction", { - direction: stats.direction, - }); - this.prev.direction = stats.direction; - } - - const trackStats: TrackStats[] = []; - if (stats.sender) { - trackStats.push(stats.sender); - } - if (stats.receiver) { - trackStats.push(stats.receiver); - } - this.upsertTrackSpans(trackStats); - } -} diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts deleted file mode 100644 index 668b989cc..000000000 --- a/src/otel/OTelGroupCallMembership.ts +++ /dev/null @@ -1,477 +0,0 @@ -/* -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"; -import { logger } from "matrix-js-sdk/lib/logger"; -import { - type CallError, - type CallState, - type MatrixCall, - type VoipEvent, -} from "matrix-js-sdk/lib/webrtc/call"; -import { - type CallsByUserAndDevice, - type GroupCallError, - GroupCallEvent, - type GroupCallStatsReport, -} from "matrix-js-sdk/lib/webrtc/groupCall"; -import { - type ConnectionStatsReport, - type ByteSentStatsReport, - type SummaryStatsReport, - type CallFeedReport, -} from "matrix-js-sdk/lib/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", -} diff --git a/src/otel/ObjectFlattener.test.ts b/src/otel/ObjectFlattener.test.ts deleted file mode 100644 index 5685617c4..000000000 --- a/src/otel/ObjectFlattener.test.ts +++ /dev/null @@ -1,266 +0,0 @@ -/* -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 { type GroupCallStatsReport } from "matrix-js-sdk/lib/webrtc/groupCall"; -import { - type AudioConcealment, - type ByteSentStatsReport, - type ConnectionStatsReport, -} from "matrix-js-sdk/lib/webrtc/stats/statsReport"; -import { describe, expect, it } from "vitest"; - -import { ObjectFlattener } from "../../src/otel/ObjectFlattener"; - -describe("ObjectFlattener", () => { - const noConcealment: AudioConcealment = { - concealedAudio: 0, - totalAudioDuration: 0, - }; - - const statsReport: GroupCallStatsReport = { - report: { - callId: "callId", - opponentMemberId: "opponentMemberId", - bandwidth: { upload: 426, download: 0 }, - bitrate: { - upload: 426, - download: 0, - audio: { - upload: 124, - download: 0, - }, - video: { - upload: 302, - download: 0, - }, - }, - packetLoss: { - total: 0, - download: 0, - upload: 0, - }, - framerate: { - local: new Map([ - ["LOCAL_AUDIO_TRACK_ID", 0], - ["LOCAL_VIDEO_TRACK_ID", 30], - ]), - remote: new Map([ - ["REMOTE_AUDIO_TRACK_ID", 0], - ["REMOTE_VIDEO_TRACK_ID", 60], - ]), - }, - resolution: { - local: new Map([ - ["LOCAL_AUDIO_TRACK_ID", { height: -1, width: -1 }], - ["LOCAL_VIDEO_TRACK_ID", { height: 460, width: 780 }], - ]), - remote: new Map([ - ["REMOTE_AUDIO_TRACK_ID", { height: -1, width: -1 }], - ["REMOTE_VIDEO_TRACK_ID", { height: 960, width: 1080 }], - ]), - }, - jitter: new Map([ - ["REMOTE_AUDIO_TRACK_ID", 2], - ["REMOTE_VIDEO_TRACK_ID", 50], - ]), - codec: { - local: new Map([ - ["LOCAL_AUDIO_TRACK_ID", "opus"], - ["LOCAL_VIDEO_TRACK_ID", "v8"], - ]), - remote: new Map([ - ["REMOTE_AUDIO_TRACK_ID", "opus"], - ["REMOTE_VIDEO_TRACK_ID", "v9"], - ]), - }, - transport: [ - { - ip: "ff11::5fa:abcd:999c:c5c5:50000", - type: "udp", - localIp: "2aaa:9999:2aaa:999:8888:2aaa:2aaa:7777:50000", - isFocus: true, - localCandidateType: "host", - remoteCandidateType: "host", - networkType: "ethernet", - rtt: NaN, - }, - { - ip: "10.10.10.2:22222", - type: "tcp", - localIp: "10.10.10.100:33333", - isFocus: true, - localCandidateType: "srfx", - remoteCandidateType: "srfx", - networkType: "ethernet", - rtt: 0, - }, - ], - audioConcealment: new Map([ - ["REMOTE_AUDIO_TRACK_ID", noConcealment], - ["REMOTE_VIDEO_TRACK_ID", noConcealment], - ]), - totalAudioConcealment: noConcealment, - }, - }; - - describe("on flattenObjectRecursive", () => { - it("should flatter an Map object", () => { - const flatObject = {}; - ObjectFlattener.flattenObjectRecursive( - statsReport.report.resolution, - flatObject, - "matrix.call.stats.connection.resolution.", - 0, - ); - expect(flatObject).toEqual({ - "matrix.call.stats.connection.resolution.local.LOCAL_AUDIO_TRACK_ID.height": - -1, - "matrix.call.stats.connection.resolution.local.LOCAL_AUDIO_TRACK_ID.width": - -1, - - "matrix.call.stats.connection.resolution.local.LOCAL_VIDEO_TRACK_ID.height": 460, - "matrix.call.stats.connection.resolution.local.LOCAL_VIDEO_TRACK_ID.width": 780, - - "matrix.call.stats.connection.resolution.remote.REMOTE_AUDIO_TRACK_ID.height": - -1, - "matrix.call.stats.connection.resolution.remote.REMOTE_AUDIO_TRACK_ID.width": - -1, - - "matrix.call.stats.connection.resolution.remote.REMOTE_VIDEO_TRACK_ID.height": 960, - "matrix.call.stats.connection.resolution.remote.REMOTE_VIDEO_TRACK_ID.width": 1080, - }); - }); - it("should flatter an Array object", () => { - const flatObject = {}; - ObjectFlattener.flattenObjectRecursive( - statsReport.report.transport, - flatObject, - "matrix.call.stats.connection.transport.", - 0, - ); - expect(flatObject).toEqual({ - "matrix.call.stats.connection.transport.0.ip": - "ff11::5fa:abcd:999c:c5c5:50000", - "matrix.call.stats.connection.transport.0.type": "udp", - "matrix.call.stats.connection.transport.0.localIp": - "2aaa:9999:2aaa:999:8888:2aaa:2aaa:7777:50000", - "matrix.call.stats.connection.transport.0.isFocus": true, - "matrix.call.stats.connection.transport.0.localCandidateType": "host", - "matrix.call.stats.connection.transport.0.remoteCandidateType": "host", - "matrix.call.stats.connection.transport.0.networkType": "ethernet", - "matrix.call.stats.connection.transport.0.rtt": "NaN", - "matrix.call.stats.connection.transport.1.ip": "10.10.10.2:22222", - "matrix.call.stats.connection.transport.1.type": "tcp", - "matrix.call.stats.connection.transport.1.localIp": - "10.10.10.100:33333", - "matrix.call.stats.connection.transport.1.isFocus": true, - "matrix.call.stats.connection.transport.1.localCandidateType": "srfx", - "matrix.call.stats.connection.transport.1.remoteCandidateType": "srfx", - "matrix.call.stats.connection.transport.1.networkType": "ethernet", - "matrix.call.stats.connection.transport.1.rtt": 0, - }); - }); - }); - - describe("on flattenReportObject Connection Stats", () => { - it("should flatten a Report to otel Attributes Object", () => { - expect( - ObjectFlattener.flattenReportObject( - "matrix.call.stats.connection", - statsReport.report, - ), - ).toEqual({ - "matrix.call.stats.connection.callId": "callId", - "matrix.call.stats.connection.opponentMemberId": "opponentMemberId", - "matrix.call.stats.connection.bandwidth.download": 0, - "matrix.call.stats.connection.bandwidth.upload": 426, - "matrix.call.stats.connection.bitrate.audio.download": 0, - "matrix.call.stats.connection.bitrate.audio.upload": 124, - "matrix.call.stats.connection.bitrate.download": 0, - "matrix.call.stats.connection.bitrate.upload": 426, - "matrix.call.stats.connection.bitrate.video.download": 0, - "matrix.call.stats.connection.bitrate.video.upload": 302, - "matrix.call.stats.connection.codec.local.LOCAL_AUDIO_TRACK_ID": "opus", - "matrix.call.stats.connection.codec.local.LOCAL_VIDEO_TRACK_ID": "v8", - "matrix.call.stats.connection.codec.remote.REMOTE_AUDIO_TRACK_ID": - "opus", - "matrix.call.stats.connection.codec.remote.REMOTE_VIDEO_TRACK_ID": "v9", - "matrix.call.stats.connection.framerate.local.LOCAL_AUDIO_TRACK_ID": 0, - "matrix.call.stats.connection.framerate.local.LOCAL_VIDEO_TRACK_ID": 30, - "matrix.call.stats.connection.framerate.remote.REMOTE_AUDIO_TRACK_ID": 0, - "matrix.call.stats.connection.framerate.remote.REMOTE_VIDEO_TRACK_ID": 60, - "matrix.call.stats.connection.jitter.REMOTE_AUDIO_TRACK_ID": 2, - "matrix.call.stats.connection.jitter.REMOTE_VIDEO_TRACK_ID": 50, - "matrix.call.stats.connection.packetLoss.download": 0, - "matrix.call.stats.connection.packetLoss.total": 0, - "matrix.call.stats.connection.packetLoss.upload": 0, - "matrix.call.stats.connection.resolution.local.LOCAL_AUDIO_TRACK_ID.height": - -1, - "matrix.call.stats.connection.resolution.local.LOCAL_AUDIO_TRACK_ID.width": - -1, - "matrix.call.stats.connection.resolution.local.LOCAL_VIDEO_TRACK_ID.height": 460, - "matrix.call.stats.connection.resolution.local.LOCAL_VIDEO_TRACK_ID.width": 780, - "matrix.call.stats.connection.resolution.remote.REMOTE_AUDIO_TRACK_ID.height": - -1, - "matrix.call.stats.connection.resolution.remote.REMOTE_AUDIO_TRACK_ID.width": - -1, - "matrix.call.stats.connection.resolution.remote.REMOTE_VIDEO_TRACK_ID.height": 960, - "matrix.call.stats.connection.resolution.remote.REMOTE_VIDEO_TRACK_ID.width": 1080, - "matrix.call.stats.connection.transport.0.ip": - "ff11::5fa:abcd:999c:c5c5:50000", - "matrix.call.stats.connection.transport.0.type": "udp", - "matrix.call.stats.connection.transport.0.localIp": - "2aaa:9999:2aaa:999:8888:2aaa:2aaa:7777:50000", - "matrix.call.stats.connection.transport.0.isFocus": true, - "matrix.call.stats.connection.transport.0.localCandidateType": "host", - "matrix.call.stats.connection.transport.0.remoteCandidateType": "host", - "matrix.call.stats.connection.transport.0.networkType": "ethernet", - "matrix.call.stats.connection.transport.0.rtt": "NaN", - "matrix.call.stats.connection.transport.1.ip": "10.10.10.2:22222", - "matrix.call.stats.connection.transport.1.type": "tcp", - "matrix.call.stats.connection.transport.1.localIp": - "10.10.10.100:33333", - "matrix.call.stats.connection.transport.1.isFocus": true, - "matrix.call.stats.connection.transport.1.localCandidateType": "srfx", - "matrix.call.stats.connection.transport.1.remoteCandidateType": "srfx", - "matrix.call.stats.connection.transport.1.networkType": "ethernet", - "matrix.call.stats.connection.transport.1.rtt": 0, - "matrix.call.stats.connection.audioConcealment.REMOTE_AUDIO_TRACK_ID.concealedAudio": 0, - "matrix.call.stats.connection.audioConcealment.REMOTE_AUDIO_TRACK_ID.totalAudioDuration": 0, - "matrix.call.stats.connection.audioConcealment.REMOTE_VIDEO_TRACK_ID.concealedAudio": 0, - "matrix.call.stats.connection.audioConcealment.REMOTE_VIDEO_TRACK_ID.totalAudioDuration": 0, - "matrix.call.stats.connection.totalAudioConcealment.concealedAudio": 0, - "matrix.call.stats.connection.totalAudioConcealment.totalAudioDuration": 0, - }); - }); - }); - - describe("on flattenByteSendStatsReportObject", () => { - const byteSentStatsReport = new Map< - string, - number - >() as ByteSentStatsReport; - byteSentStatsReport.callId = "callId"; - byteSentStatsReport.opponentMemberId = "opponentMemberId"; - byteSentStatsReport.set("4aa92608-04c6-428e-8312-93e17602a959", 132093); - byteSentStatsReport.set("a08e4237-ee30-4015-a932-b676aec894b1", 913448); - - it("should flatten a Report to otel Attributes Object", () => { - expect( - ObjectFlattener.flattenReportObject( - "matrix.call.stats.bytesSend", - byteSentStatsReport, - ), - ).toEqual({ - "matrix.call.stats.bytesSend.4aa92608-04c6-428e-8312-93e17602a959": 132093, - "matrix.call.stats.bytesSend.a08e4237-ee30-4015-a932-b676aec894b1": 913448, - }); - expect(byteSentStatsReport.callId).toEqual("callId"); - expect(byteSentStatsReport.opponentMemberId).toEqual("opponentMemberId"); - }); - }); -}); diff --git a/src/otel/ObjectFlattener.ts b/src/otel/ObjectFlattener.ts deleted file mode 100644 index a963c7434..000000000 --- a/src/otel/ObjectFlattener.ts +++ /dev/null @@ -1,100 +0,0 @@ -/* -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 { type Attributes } from "@opentelemetry/api"; -import { type VoipEvent } from "matrix-js-sdk/lib/webrtc/call"; -import { type GroupCallStatsReport } from "matrix-js-sdk/lib/webrtc/groupCall"; -import { - type ByteSentStatsReport, - type ConnectionStatsReport, - type SummaryStatsReport, -} from "matrix-js-sdk/lib/webrtc/stats/statsReport"; - -export class ObjectFlattener { - public static flattenReportObject( - prefix: string, - report: ConnectionStatsReport | ByteSentStatsReport, - ): Attributes { - const flatObject = {}; - ObjectFlattener.flattenObjectRecursive(report, flatObject, `${prefix}.`, 0); - return flatObject; - } - - public static flattenByteSentStatsReportObject( - statsReport: GroupCallStatsReport, - ): Attributes { - const flatObject = {}; - ObjectFlattener.flattenObjectRecursive( - statsReport.report, - flatObject, - "matrix.stats.bytesSent.", - 0, - ); - return flatObject; - } - - public static flattenSummaryStatsReportObject( - statsReport: GroupCallStatsReport, - ): Attributes { - const flatObject = {}; - ObjectFlattener.flattenObjectRecursive( - statsReport.report, - flatObject, - "matrix.stats.summary.", - 0, - ); - return flatObject; - } - - /* Flattens out an object into a single layer with components - * of the key separated by dots - */ - public static flattenVoipEvent(event: VoipEvent): Attributes { - const flatObject = {}; - ObjectFlattener.flattenObjectRecursive( - event as unknown as Record, // XXX Types - flatObject, - "matrix.event.", - 0, - ); - - return flatObject; - } - - public static flattenObjectRecursive( - obj: object, - flatObject: Attributes, - prefix: string, - depth: number, - ): void { - if (depth > 10) - throw new Error( - "Depth limit exceeded: aborting VoipEvent recursion. Prefix is " + - prefix, - ); - let entries; - if (obj instanceof Map) { - entries = obj.entries(); - } else { - entries = Object.entries(obj); - } - for (const [k, v] of entries) { - if (["string", "number", "boolean"].includes(typeof v) || v === null) { - let value; - value = v === null ? "null" : v; - value = typeof v === "number" && Number.isNaN(v) ? "NaN" : value; - flatObject[prefix + k] = value; - } else if (typeof v === "object") { - ObjectFlattener.flattenObjectRecursive( - v, - flatObject, - prefix + k + ".", - depth + 1, - ); - } - } - } -} diff --git a/src/otel/otel.test.ts b/src/otel/otel.test.ts deleted file mode 100644 index 0bf0573f2..000000000 --- a/src/otel/otel.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* -Copyright 2025 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 { - expect, - describe, - it, - vi, - beforeEach, - beforeAll, - afterAll, -} from "vitest"; - -import { ElementCallOpenTelemetry } from "./otel"; -import { mockConfig } from "../utils/test"; - -describe("ElementCallOpenTelemetry", () => { - describe("embedded package", () => { - beforeAll(() => { - vi.stubEnv("VITE_PACKAGE", "embedded"); - }); - - beforeEach(() => { - mockConfig({}); - }); - - afterAll(() => { - vi.unstubAllEnvs(); - }); - - it("does not create instance without config value", () => { - ElementCallOpenTelemetry.globalInit(); - expect(ElementCallOpenTelemetry.instance?.isOtlpEnabled).toBe(false); - }); - - it("ignores config value and does not create instance", () => { - mockConfig({ - opentelemetry: { - collector_url: "https://collector.example.com.localhost", - }, - }); - ElementCallOpenTelemetry.globalInit(); - expect(ElementCallOpenTelemetry.instance?.isOtlpEnabled).toBe(false); - }); - }); - - describe("full package", () => { - beforeAll(() => { - vi.stubEnv("VITE_PACKAGE", "full"); - }); - - beforeEach(() => { - mockConfig({}); - }); - - afterAll(() => { - vi.unstubAllEnvs(); - }); - - it("does not create instance without config value", () => { - ElementCallOpenTelemetry.globalInit(); - expect(ElementCallOpenTelemetry.instance?.isOtlpEnabled).toBe(false); - }); - - it("creates instance with config value", () => { - mockConfig({ - opentelemetry: { - collector_url: "https://collector.example.com.localhost", - }, - }); - ElementCallOpenTelemetry.globalInit(); - expect(ElementCallOpenTelemetry.instance?.isOtlpEnabled).toBe(true); - }); - }); -}); diff --git a/src/otel/otel.ts b/src/otel/otel.ts deleted file mode 100644 index 915c3d587..000000000 --- a/src/otel/otel.ts +++ /dev/null @@ -1,117 +0,0 @@ -/* -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 { - SimpleSpanProcessor, - type SpanProcessor, -} from "@opentelemetry/sdk-trace-base"; -import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; -import { WebTracerProvider } from "@opentelemetry/sdk-trace-web"; -import opentelemetry, { type Tracer } from "@opentelemetry/api"; -import { resourceFromAttributes } from "@opentelemetry/resources"; -import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions"; -import { logger } from "matrix-js-sdk/lib/logger"; - -import { PosthogSpanProcessor } from "../analytics/PosthogSpanProcessor"; -import { Config } from "../config/Config"; -import { RageshakeSpanProcessor } from "../analytics/RageshakeSpanProcessor"; -import { getRageshakeSubmitUrl } from "../settings/submit-rageshake"; - -const SERVICE_NAME = "element-call"; - -let sharedInstance: ElementCallOpenTelemetry; - -export class ElementCallOpenTelemetry { - private _provider: WebTracerProvider; - private _tracer: Tracer; - private otlpExporter?: OTLPTraceExporter; - public readonly rageshakeProcessor?: RageshakeSpanProcessor; - - public static globalInit(): void { - // this is only supported in the full package as the is currently no support for passing in the collector URL from the widget host - const collectorUrl = - import.meta.env.VITE_PACKAGE === "full" - ? Config.get().opentelemetry?.collector_url - : undefined; - // we always enable opentelemetry in general. We only enable the OTLP - // collector if a URL is defined (and in future if another setting is defined) - // Posthog reporting is enabled or disabled - // within the posthog code. - const shouldEnableOtlp = Boolean(collectorUrl); - - if (!sharedInstance || sharedInstance.isOtlpEnabled !== shouldEnableOtlp) { - logger.info("(Re)starting OpenTelemetry debug reporting"); - sharedInstance?.dispose(); - - sharedInstance = new ElementCallOpenTelemetry( - collectorUrl, - getRageshakeSubmitUrl(), - ); - } - } - - public static get instance(): ElementCallOpenTelemetry { - return sharedInstance; - } - - private constructor( - collectorUrl: string | undefined, - rageshakeUrl: string | undefined, - ) { - const spanProcessors: SpanProcessor[] = []; - - if (collectorUrl) { - logger.info("Enabling OTLP collector with URL " + collectorUrl); - this.otlpExporter = new OTLPTraceExporter({ - url: collectorUrl, - }); - spanProcessors.push(new SimpleSpanProcessor(this.otlpExporter)); - } else { - logger.info("OTLP collector disabled"); - } - - if (rageshakeUrl) { - this.rageshakeProcessor = new RageshakeSpanProcessor(); - spanProcessors.push(this.rageshakeProcessor); - } - - spanProcessors.push(new PosthogSpanProcessor()); - - this._provider = new WebTracerProvider({ - resource: resourceFromAttributes({ - // This is how we can make Jaeger show a reasonable service in the dropdown on the left. - [ATTR_SERVICE_NAME]: SERVICE_NAME, - }), - spanProcessors, - }); - - opentelemetry.trace.setGlobalTracerProvider(this._provider); - this._tracer = opentelemetry.trace.getTracer( - // This is not the serviceName shown in jaeger - "my-element-call-otl-tracer", - ); - } - - public dispose(): void { - opentelemetry.trace.disable(); - this._provider?.shutdown().catch((e) => { - logger.error("Failed to shutdown OpenTelemetry", e); - }); - } - - public get isOtlpEnabled(): boolean { - return Boolean(this.otlpExporter); - } - - public get tracer(): Tracer { - return this._tracer; - } - - public get provider(): WebTracerProvider { - return this._provider; - } -} diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index b17d3aaee..72a35da68 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -48,7 +48,6 @@ import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts"; import { ElementWidgetActions, widget } from "../widget"; import styles from "./InCallView.module.css"; import { GridTile } from "../tile/GridTile"; -import { type OTelGroupCallMembership } from "../otel/OTelGroupCallMembership"; import { SettingsModal, defaultSettingsTab } from "../settings/SettingsModal"; import { useRageshakeRequestModal } from "../settings/submit-rageshake"; import { RageshakeRequestModal } from "./RageshakeRequestModal"; @@ -180,7 +179,6 @@ export interface InCallViewProps { matrixRoom: MatrixRoom; muteStates: MuteStates; header: HeaderStyle; - otelGroupCallMembership?: OTelGroupCallMembership; onShareClick: (() => void) | null; } diff --git a/src/settings/submit-rageshake.ts b/src/settings/submit-rageshake.ts index bfd551262..276d8e609 100644 --- a/src/settings/submit-rageshake.ts +++ b/src/settings/submit-rageshake.ts @@ -17,7 +17,6 @@ import { type CryptoApi } from "matrix-js-sdk/lib/crypto-api"; import { getLogsForReport } from "./rageshake"; import { useClient } from "../ClientContext"; import { Config } from "../config/Config"; -import { ElementCallOpenTelemetry } from "../otel/otel"; import { type RageshakeRequestModal } from "../room/RageshakeRequestModal"; import { getUrlParams } from "../UrlParams"; @@ -274,14 +273,6 @@ export function useSubmitRageshake( for (const entry of logs) { body.append("compressed-log", await gzip(entry.lines), entry.id); } - - body.append( - "file", - await gzip( - ElementCallOpenTelemetry.instance.rageshakeProcessor!.dump(), - ), - "traces.json.gz", - ); } if (opts.rageshakeRequestId) { From 2f928673831fd2730a1bb150c73e82e9433c372e Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 24 Nov 2025 12:36:36 +0100 Subject: [PATCH 2/3] remove otel deps --- package.json | 7 -- yarn.lock | 249 +-------------------------------------------------- 2 files changed, 1 insertion(+), 255 deletions(-) diff --git a/package.json b/package.json index 62ea9f4fb..75ae74e5f 100644 --- a/package.json +++ b/package.json @@ -47,13 +47,6 @@ "@livekit/protocol": "^1.42.2", "@livekit/track-processors": "^0.5.5", "@mediapipe/tasks-vision": "^0.10.18", - "@opentelemetry/api": "^1.4.0", - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/exporter-trace-otlp-http": "^0.203.0", - "@opentelemetry/resources": "^2.0.0", - "@opentelemetry/sdk-trace-base": "^2.0.0", - "@opentelemetry/sdk-trace-web": "^2.0.0", - "@opentelemetry/semantic-conventions": "^1.25.1", "@playwright/test": "^1.56.1", "@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-slider": "^1.1.2", diff --git a/yarn.lock b/yarn.lock index 97ca19859..f7b4242e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2989,146 +2989,6 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/api-logs@npm:0.203.0": - version: 0.203.0 - resolution: "@opentelemetry/api-logs@npm:0.203.0" - dependencies: - "@opentelemetry/api": "npm:^1.3.0" - checksum: 10c0/e7a0a0ff46aaeb62192a99f45ef4889222e4fea09be25cab6fea811afc2df95c02ea050b2c98dfc0fc5a6ec6a623d87096af2751fdf91ddbb3afcab61b5325da - languageName: node - linkType: hard - -"@opentelemetry/api@npm:^1.3.0, @opentelemetry/api@npm:^1.4.0": - version: 1.9.0 - resolution: "@opentelemetry/api@npm:1.9.0" - checksum: 10c0/9aae2fe6e8a3a3eeb6c1fdef78e1939cf05a0f37f8a4fae4d6bf2e09eb1e06f966ece85805626e01ba5fab48072b94f19b835449e58b6d26720ee19a58298add - languageName: node - linkType: hard - -"@opentelemetry/core@npm:2.0.1, @opentelemetry/core@npm:^2.0.0": - version: 2.0.1 - resolution: "@opentelemetry/core@npm:2.0.1" - dependencies: - "@opentelemetry/semantic-conventions": "npm:^1.29.0" - peerDependencies: - "@opentelemetry/api": ">=1.0.0 <1.10.0" - checksum: 10c0/d587b1289559757d80da98039f9f57612f84f72ec608cd665dc467c7c6c5ce3a987dfcc2c63b521c7c86ce984a2552b3ead15a0dc458de1cf6bde5cdfe4ca9d8 - languageName: node - linkType: hard - -"@opentelemetry/exporter-trace-otlp-http@npm:^0.203.0": - version: 0.203.0 - resolution: "@opentelemetry/exporter-trace-otlp-http@npm:0.203.0" - dependencies: - "@opentelemetry/core": "npm:2.0.1" - "@opentelemetry/otlp-exporter-base": "npm:0.203.0" - "@opentelemetry/otlp-transformer": "npm:0.203.0" - "@opentelemetry/resources": "npm:2.0.1" - "@opentelemetry/sdk-trace-base": "npm:2.0.1" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10c0/21a65ebc40dcab05cf11178e5037f96847ce344c4a855aac46dcab3f74982016318ee75fafdfeeb42f10b92a0a781b7cd8b2b5b036cbe53c14714fd13940142e - languageName: node - linkType: hard - -"@opentelemetry/otlp-exporter-base@npm:0.203.0": - version: 0.203.0 - resolution: "@opentelemetry/otlp-exporter-base@npm:0.203.0" - dependencies: - "@opentelemetry/core": "npm:2.0.1" - "@opentelemetry/otlp-transformer": "npm:0.203.0" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10c0/ad5b771b06b192f06f332f60701d1ad208df88a05975b16e1cdd1dff8e1cb66e775b3e9de513c2f5d48f390f25ca35411ead08ce4849c8203b86a264d34561d3 - languageName: node - linkType: hard - -"@opentelemetry/otlp-transformer@npm:0.203.0": - version: 0.203.0 - resolution: "@opentelemetry/otlp-transformer@npm:0.203.0" - dependencies: - "@opentelemetry/api-logs": "npm:0.203.0" - "@opentelemetry/core": "npm:2.0.1" - "@opentelemetry/resources": "npm:2.0.1" - "@opentelemetry/sdk-logs": "npm:0.203.0" - "@opentelemetry/sdk-metrics": "npm:2.0.1" - "@opentelemetry/sdk-trace-base": "npm:2.0.1" - protobufjs: "npm:^7.3.0" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10c0/3f7b4bfe4bcab4db434ff2c4e59b53de53642d379b80056610456d8e9ae0cbab0f8b69f088078637b7b5ceffd0ac2fda68469c5f295b1c0ac625f522f640338c - languageName: node - linkType: hard - -"@opentelemetry/resources@npm:2.0.1, @opentelemetry/resources@npm:^2.0.0": - version: 2.0.1 - resolution: "@opentelemetry/resources@npm:2.0.1" - dependencies: - "@opentelemetry/core": "npm:2.0.1" - "@opentelemetry/semantic-conventions": "npm:^1.29.0" - peerDependencies: - "@opentelemetry/api": ">=1.3.0 <1.10.0" - checksum: 10c0/96532b7553b26607a7a892d72f6b03ad12bd542dc23c95135a8ae40362da9c883c21a4cff3d2296d9e0e9bd899a5977e325ed52d83142621a8ffe81d08d99341 - languageName: node - linkType: hard - -"@opentelemetry/sdk-logs@npm:0.203.0": - version: 0.203.0 - resolution: "@opentelemetry/sdk-logs@npm:0.203.0" - dependencies: - "@opentelemetry/api-logs": "npm:0.203.0" - "@opentelemetry/core": "npm:2.0.1" - "@opentelemetry/resources": "npm:2.0.1" - peerDependencies: - "@opentelemetry/api": ">=1.4.0 <1.10.0" - checksum: 10c0/02dd9d9969628f05f71ae1d149f1aa6d1fee2dad607923a68a1cfc923e94b046dcc0e18e85e865324e3bda0cee7a5a0ba9fa0d57e4e95fa672be103e2ce60270 - languageName: node - linkType: hard - -"@opentelemetry/sdk-metrics@npm:2.0.1": - version: 2.0.1 - resolution: "@opentelemetry/sdk-metrics@npm:2.0.1" - dependencies: - "@opentelemetry/core": "npm:2.0.1" - "@opentelemetry/resources": "npm:2.0.1" - peerDependencies: - "@opentelemetry/api": ">=1.9.0 <1.10.0" - checksum: 10c0/fcf7ae23d459e5da7cb6fe150064b6dc4e11e47925b08980c3b357bd5534ad388898bbacd0ff8befef6801f43b35142dc7123f028ffde2d0fe2bd72177d07639 - languageName: node - linkType: hard - -"@opentelemetry/sdk-trace-base@npm:2.0.1, @opentelemetry/sdk-trace-base@npm:^2.0.0": - version: 2.0.1 - resolution: "@opentelemetry/sdk-trace-base@npm:2.0.1" - dependencies: - "@opentelemetry/core": "npm:2.0.1" - "@opentelemetry/resources": "npm:2.0.1" - "@opentelemetry/semantic-conventions": "npm:^1.29.0" - peerDependencies: - "@opentelemetry/api": ">=1.3.0 <1.10.0" - checksum: 10c0/4e3c733296012b758d007e9c0d8a5b175edbe9a680c73ec75303476e7982b73ad4209f1a2791c1a94c428e5a53eba6c2a72faa430c70336005aa58744d6cb37b - languageName: node - linkType: hard - -"@opentelemetry/sdk-trace-web@npm:^2.0.0": - version: 2.0.1 - resolution: "@opentelemetry/sdk-trace-web@npm:2.0.1" - dependencies: - "@opentelemetry/core": "npm:2.0.1" - "@opentelemetry/sdk-trace-base": "npm:2.0.1" - peerDependencies: - "@opentelemetry/api": ">=1.0.0 <1.10.0" - checksum: 10c0/48821b91430e24378b0b5b2632e78efdd018a3f840462a6aeba6ce318a6480bad2f623cc7f7f625a9266028ad44b78eb8456181778de6cb18725f26c44e2729b - languageName: node - linkType: hard - -"@opentelemetry/semantic-conventions@npm:^1.25.1, @opentelemetry/semantic-conventions@npm:^1.29.0": - version: 1.36.0 - resolution: "@opentelemetry/semantic-conventions@npm:1.36.0" - checksum: 10c0/edc8a6fe3ec4fc0c67ba3a92b86fb3dcc78fe1eb4f19838d8013c3232b9868540a034dd25cfe0afdd5eae752c5f0e9f42272ff46da144a2d5b35c644478e1c62 - languageName: node - linkType: hard - "@oxc-resolver/binding-darwin-arm64@npm:11.3.0": version: 11.3.0 resolution: "@oxc-resolver/binding-darwin-arm64@npm:11.3.0" @@ -3384,79 +3244,6 @@ __metadata: languageName: node linkType: hard -"@protobufjs/aspromise@npm:^1.1.1, @protobufjs/aspromise@npm:^1.1.2": - version: 1.1.2 - resolution: "@protobufjs/aspromise@npm:1.1.2" - checksum: 10c0/a83343a468ff5b5ec6bff36fd788a64c839e48a07ff9f4f813564f58caf44d011cd6504ed2147bf34835bd7a7dd2107052af755961c6b098fd8902b4f6500d0f - languageName: node - linkType: hard - -"@protobufjs/base64@npm:^1.1.2": - version: 1.1.2 - resolution: "@protobufjs/base64@npm:1.1.2" - checksum: 10c0/eec925e681081af190b8ee231f9bad3101e189abbc182ff279da6b531e7dbd2a56f1f306f37a80b1be9e00aa2d271690d08dcc5f326f71c9eed8546675c8caf6 - languageName: node - linkType: hard - -"@protobufjs/codegen@npm:^2.0.4": - version: 2.0.4 - resolution: "@protobufjs/codegen@npm:2.0.4" - checksum: 10c0/26ae337c5659e41f091606d16465bbcc1df1f37cc1ed462438b1f67be0c1e28dfb2ca9f294f39100c52161aef82edf758c95d6d75650a1ddf31f7ddee1440b43 - languageName: node - linkType: hard - -"@protobufjs/eventemitter@npm:^1.1.0": - version: 1.1.0 - resolution: "@protobufjs/eventemitter@npm:1.1.0" - checksum: 10c0/1eb0a75180e5206d1033e4138212a8c7089a3d418c6dfa5a6ce42e593a4ae2e5892c4ef7421f38092badba4040ea6a45f0928869989411001d8c1018ea9a6e70 - languageName: node - linkType: hard - -"@protobufjs/fetch@npm:^1.1.0": - version: 1.1.0 - resolution: "@protobufjs/fetch@npm:1.1.0" - dependencies: - "@protobufjs/aspromise": "npm:^1.1.1" - "@protobufjs/inquire": "npm:^1.1.0" - checksum: 10c0/cda6a3dc2d50a182c5865b160f72077aac197046600091dbb005dd0a66db9cce3c5eaed6d470ac8ed49d7bcbeef6ee5f0bc288db5ff9a70cbd003e5909065233 - languageName: node - linkType: hard - -"@protobufjs/float@npm:^1.0.2": - version: 1.0.2 - resolution: "@protobufjs/float@npm:1.0.2" - checksum: 10c0/18f2bdede76ffcf0170708af15c9c9db6259b771e6b84c51b06df34a9c339dbbeec267d14ce0bddd20acc142b1d980d983d31434398df7f98eb0c94a0eb79069 - languageName: node - linkType: hard - -"@protobufjs/inquire@npm:^1.1.0": - version: 1.1.0 - resolution: "@protobufjs/inquire@npm:1.1.0" - checksum: 10c0/64372482efcba1fb4d166a2664a6395fa978b557803857c9c03500e0ac1013eb4b1aacc9ed851dd5fc22f81583670b4f4431bae186f3373fedcfde863ef5921a - languageName: node - linkType: hard - -"@protobufjs/path@npm:^1.1.2": - version: 1.1.2 - resolution: "@protobufjs/path@npm:1.1.2" - checksum: 10c0/cece0a938e7f5dfd2fa03f8c14f2f1cf8b0d6e13ac7326ff4c96ea311effd5fb7ae0bba754fbf505312af2e38500250c90e68506b97c02360a43793d88a0d8b4 - languageName: node - linkType: hard - -"@protobufjs/pool@npm:^1.1.0": - version: 1.1.0 - resolution: "@protobufjs/pool@npm:1.1.0" - checksum: 10c0/eda2718b7f222ac6e6ad36f758a92ef90d26526026a19f4f17f668f45e0306a5bd734def3f48f51f8134ae0978b6262a5c517c08b115a551756d1a3aadfcf038 - languageName: node - linkType: hard - -"@protobufjs/utf8@npm:^1.1.0": - version: 1.1.0 - resolution: "@protobufjs/utf8@npm:1.1.0" - checksum: 10c0/a3fe31fe3fa29aa3349e2e04ee13dc170cc6af7c23d92ad49e3eeaf79b9766264544d3da824dba93b7855bd6a2982fb40032ef40693da98a136d835752beb487 - languageName: node - linkType: hard - "@radix-ui/number@npm:1.1.1": version: 1.1.1 resolution: "@radix-ui/number@npm:1.1.1" @@ -5240,7 +5027,7 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=13.7.0": +"@types/node@npm:*": version: 24.1.0 resolution: "@types/node@npm:24.1.0" dependencies: @@ -7485,13 +7272,6 @@ __metadata: "@livekit/protocol": "npm:^1.42.2" "@livekit/track-processors": "npm:^0.5.5" "@mediapipe/tasks-vision": "npm:^0.10.18" - "@opentelemetry/api": "npm:^1.4.0" - "@opentelemetry/core": "npm:^2.0.0" - "@opentelemetry/exporter-trace-otlp-http": "npm:^0.203.0" - "@opentelemetry/resources": "npm:^2.0.0" - "@opentelemetry/sdk-trace-base": "npm:^2.0.0" - "@opentelemetry/sdk-trace-web": "npm:^2.0.0" - "@opentelemetry/semantic-conventions": "npm:^1.25.1" "@playwright/test": "npm:^1.56.1" "@radix-ui/react-dialog": "npm:^1.0.4" "@radix-ui/react-slider": "npm:^1.1.2" @@ -10198,13 +9978,6 @@ __metadata: languageName: node linkType: hard -"long@npm:^5.0.0": - version: 5.3.1 - resolution: "long@npm:5.3.1" - checksum: 10c0/8726994c6359bb7162fb94563e14c3f9c0f0eeafd90ec654738f4f144a5705756d36a873c442f172ee2a4b51e08d14ab99765b49aa1fb994c5ba7fe12057bca2 - languageName: node - linkType: hard - "loose-envify@npm:^1.4.0": version: 1.4.0 resolution: "loose-envify@npm:1.4.0" @@ -11718,26 +11491,6 @@ __metadata: languageName: node linkType: hard -"protobufjs@npm:^7.3.0": - version: 7.4.0 - resolution: "protobufjs@npm:7.4.0" - dependencies: - "@protobufjs/aspromise": "npm:^1.1.2" - "@protobufjs/base64": "npm:^1.1.2" - "@protobufjs/codegen": "npm:^2.0.4" - "@protobufjs/eventemitter": "npm:^1.1.0" - "@protobufjs/fetch": "npm:^1.1.0" - "@protobufjs/float": "npm:^1.0.2" - "@protobufjs/inquire": "npm:^1.1.0" - "@protobufjs/path": "npm:^1.1.2" - "@protobufjs/pool": "npm:^1.1.0" - "@protobufjs/utf8": "npm:^1.1.0" - "@types/node": "npm:>=13.7.0" - long: "npm:^5.0.0" - checksum: 10c0/a5460a63fe596523b9a067cbce39a6b310d1a71750fda261f076535662aada97c24450e18c5bc98a27784f70500615904ff1227e1742183509f0db4fdede669b - languageName: node - linkType: hard - "proxy-from-env@npm:^1.1.0": version: 1.1.0 resolution: "proxy-from-env@npm:1.1.0" From 8bf651837139184c107582c5d74c135934455035 Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 24 Nov 2025 12:42:00 +0100 Subject: [PATCH 3/3] remove span processor endpoints --- src/analytics/PosthogSpanProcessor.ts | 157 ------------------------ src/analytics/RageshakeSpanProcessor.ts | 135 -------------------- 2 files changed, 292 deletions(-) delete mode 100644 src/analytics/PosthogSpanProcessor.ts delete mode 100644 src/analytics/RageshakeSpanProcessor.ts diff --git a/src/analytics/PosthogSpanProcessor.ts b/src/analytics/PosthogSpanProcessor.ts deleted file mode 100644 index a00462002..000000000 --- a/src/analytics/PosthogSpanProcessor.ts +++ /dev/null @@ -1,157 +0,0 @@ -/* -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 { - type SpanProcessor, - type ReadableSpan, - type Span, -} from "@opentelemetry/sdk-trace-base"; -import { hrTimeToMilliseconds } from "@opentelemetry/core"; -import { logger } from "matrix-js-sdk/lib/logger"; - -import { PosthogAnalytics } from "./PosthogAnalytics"; - -interface PrevCall { - callId: string; - hangupTs: number; -} - -/** - * The maximum time between hanging up and joining the same call that we would - * consider a 'rejoin' on the user's part. - */ -const maxRejoinMs = 2 * 60 * 1000; // 2 minutes - -/** - * Span processor that extracts certain metrics from spans to send to PostHog - */ -export class PosthogSpanProcessor implements SpanProcessor { - public async forceFlush(): Promise {} - - public onStart(span: Span): void { - // Hack: Yield to allow attributes to be set before processing - try { - switch (span.name) { - case "matrix.groupCallMembership": - this.onGroupCallMembershipStart(span); - return; - case "matrix.groupCallMembership.summaryReport": - this.onSummaryReportStart(span); - return; - } - } catch (e) { - // log to avoid tripping @typescript-eslint/no-unused-vars - logger.debug(e); - } - } - - public onEnd(span: ReadableSpan): void { - switch (span.name) { - case "matrix.groupCallMembership": - this.onGroupCallMembershipEnd(span); - return; - } - } - - private get prevCall(): PrevCall | null { - // This is stored in localStorage so we can remember the previous call - // across app restarts - const data = localStorage.getItem("matrix-prev-call"); - if (data === null) return null; - - try { - return JSON.parse(data); - } catch (e) { - logger.warn("Invalid prev call data", data, "error:", e); - return null; - } - } - - private set prevCall(data: PrevCall | null) { - localStorage.setItem("matrix-prev-call", JSON.stringify(data)); - } - - private onGroupCallMembershipStart(span: ReadableSpan): void { - const prevCall = this.prevCall; - const newCallId = span.attributes["matrix.confId"] as string; - - // If the user joined the same call within a short time frame, log this as a - // rejoin. This is interesting as a call quality metric, since rejoins may - // indicate that users had to intervene to make the product work. - if (prevCall !== null && newCallId === prevCall.callId) { - const duration = hrTimeToMilliseconds(span.startTime) - prevCall.hangupTs; - if (duration <= maxRejoinMs) { - PosthogAnalytics.instance.trackEvent({ - eventName: "Rejoin", - callId: prevCall.callId, - rejoinDuration: duration, - }); - } - } - } - - private onGroupCallMembershipEnd(span: ReadableSpan): void { - this.prevCall = { - callId: span.attributes["matrix.confId"] as string, - hangupTs: hrTimeToMilliseconds(span.endTime), - }; - } - - private onSummaryReportStart(span: ReadableSpan): void { - // Searching for an event like this: - // matrix.stats.summary - // matrix.stats.summary.percentageReceivedAudioMedia: 0.75 - // matrix.stats.summary.percentageReceivedMedia: 1 - // matrix.stats.summary.percentageReceivedVideoMedia: 0.75 - // matrix.stats.summary.maxJitter: 100 - // matrix.stats.summary.maxPacketLoss: 20 - const event = span.events.find((e) => e.name === "matrix.stats.summary"); - if (event !== undefined) { - const attributes = event.attributes; - if (attributes) { - const mediaReceived = `${attributes["matrix.stats.summary.percentageReceivedMedia"]}`; - const videoReceived = `${attributes["matrix.stats.summary.percentageReceivedVideoMedia"]}`; - const audioReceived = `${attributes["matrix.stats.summary.percentageReceivedAudioMedia"]}`; - const maxJitter = `${attributes["matrix.stats.summary.maxJitter"]}`; - const maxPacketLoss = `${attributes["matrix.stats.summary.maxPacketLoss"]}`; - const peerConnections = `${attributes["matrix.stats.summary.peerConnections"]}`; - const percentageConcealedAudio = `${attributes["matrix.stats.summary.percentageConcealedAudio"]}`; - const opponentUsersInCall = `${attributes["matrix.stats.summary.opponentUsersInCall"]}`; - const opponentDevicesInCall = `${attributes["matrix.stats.summary.opponentDevicesInCall"]}`; - const diffDevicesToPeerConnections = `${attributes["matrix.stats.summary.diffDevicesToPeerConnections"]}`; - const ratioPeerConnectionToDevices = `${attributes["matrix.stats.summary.ratioPeerConnectionToDevices"]}`; - - PosthogAnalytics.instance.trackEvent( - { - eventName: "MediaReceived", - callId: span.attributes["matrix.confId"] as string, - mediaReceived: mediaReceived, - audioReceived: audioReceived, - videoReceived: videoReceived, - maxJitter: maxJitter, - maxPacketLoss: maxPacketLoss, - peerConnections: peerConnections, - percentageConcealedAudio: percentageConcealedAudio, - opponentUsersInCall: opponentUsersInCall, - opponentDevicesInCall: opponentDevicesInCall, - diffDevicesToPeerConnections: diffDevicesToPeerConnections, - ratioPeerConnectionToDevices: ratioPeerConnectionToDevices, - }, - // Send instantly because the window might be closing - { send_instantly: true }, - ); - } - } - } - - /** - * Shutdown the processor. - */ - public async shutdown(): Promise { - return Promise.resolve(); - } -} diff --git a/src/analytics/RageshakeSpanProcessor.ts b/src/analytics/RageshakeSpanProcessor.ts deleted file mode 100644 index eca657db9..000000000 --- a/src/analytics/RageshakeSpanProcessor.ts +++ /dev/null @@ -1,135 +0,0 @@ -/* -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 { type AttributeValue, type Attributes } from "@opentelemetry/api"; -import { hrTimeToMicroseconds } from "@opentelemetry/core"; -import { - type SpanProcessor, - type ReadableSpan, - type Span, -} from "@opentelemetry/sdk-trace-base"; - -const dumpAttributes = ( - attr: Attributes, -): { - key: string; - type: - | "string" - | "number" - | "bigint" - | "boolean" - | "symbol" - | "undefined" - | "object" - | "function"; - value: AttributeValue | undefined; -}[] => - Object.entries(attr).map(([key, value]) => ({ - key, - type: typeof value, - value, - })); - -/** - * Exports spans on demand to the Jaeger JSON format, which can be attached to - * rageshakes and loaded into analysis tools like Jaeger and Stalk. - */ -export class RageshakeSpanProcessor implements SpanProcessor { - private readonly spans: ReadableSpan[] = []; - - public async forceFlush(): Promise {} - - public onStart(span: Span): void { - this.spans.push(span); - } - - public onEnd(): void {} - - /** - * Dumps the spans collected so far as Jaeger-compatible JSON. - */ - public dump(): string { - const now = Date.now() * 1000; // Jaeger works in microseconds - const traces = new Map(); - - // Organize spans by their trace IDs - for (const span of this.spans) { - const traceId = span.spanContext().traceId; - let trace = traces.get(traceId); - - if (trace === undefined) { - trace = []; - traces.set(traceId, trace); - } - - trace.push(span); - } - - const processId = "p1"; - const processes = { - [processId]: { - serviceName: "element-call", - tags: [], - }, - warnings: null, - }; - - return JSON.stringify({ - // Honestly not sure what some of these fields mean, I just know that - // they're present in Jaeger JSON exports - total: 0, - limit: 0, - offset: 0, - errors: null, - data: [...traces.entries()].map(([traceId, spans]) => ({ - traceID: traceId, - warnings: null, - processes, - spans: spans.map((span) => { - const ctx = span.spanContext(); - const startTime = hrTimeToMicroseconds(span.startTime); - // If the span has not yet ended, pretend that it ends now - const duration = - span.duration[0] === -1 - ? now - startTime - : hrTimeToMicroseconds(span.duration); - - return { - traceID: traceId, - spanID: ctx.spanId, - operationName: span.name, - processID: processId, - warnings: null, - startTime, - duration, - references: - span.parentSpanContext?.spanId === undefined - ? [] - : [ - { - refType: "CHILD_OF", - traceID: traceId, - spanID: span.parentSpanContext?.spanId, - }, - ], - tags: dumpAttributes(span.attributes), - logs: span.events.map((event) => ({ - timestamp: hrTimeToMicroseconds(event.time), - // The name of the event is in the "event" field, aparently. - fields: [ - ...dumpAttributes(event.attributes ?? {}), - { key: "event", type: "string", value: event.name }, - ], - })), - }; - }), - })), - }); - } - - public async shutdown(): Promise {} -}