mirror of
https://github.com/vector-im/element-call.git
synced 2026-03-19 06:20:25 +00:00
Merge branch 'livekit' into renovate/all-minor-patch
This commit is contained in:
@@ -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<void> {}
|
||||
|
||||
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<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
@@ -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<void> {}
|
||||
|
||||
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<string, ReadableSpan[]>();
|
||||
|
||||
// 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<void> {}
|
||||
}
|
||||
@@ -34,8 +34,8 @@ const getRoomSharedKeyLocalStorageKey = (roomId: string): string =>
|
||||
`room-shared-key-${roomId}`;
|
||||
|
||||
/**
|
||||
* An upto-date shared key for the room. Either from local storage or the value from `setInitialValue`.
|
||||
* @param roomId
|
||||
* An up-to-date shared key for the room. Either from local storage or the value from `setInitialValue`.
|
||||
* @param roomId The room ID we want the shared key for.
|
||||
* @param setInitialValue The value we get from the URL. The hook will overwrite the local storage value with this.
|
||||
* @returns [roomSharedKey, setRoomSharedKey] like a react useState hook.
|
||||
*/
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -166,7 +166,11 @@ interface StereoPanAudioTrackProps {
|
||||
* It main purpose is to remount the AudioTrack component when switching from
|
||||
* audioContext to normal audio playback.
|
||||
* As of now the AudioTrack component does not support adding audio nodes while being mounted.
|
||||
* @param param0
|
||||
* @param props The component props
|
||||
* @param props.trackRef The track reference
|
||||
* @param props.muted If the track should be muted
|
||||
* @param props.audioContext The audio context to use
|
||||
* @param props.audioNodes The audio nodes to use
|
||||
* @returns
|
||||
*/
|
||||
function AudioTrackWithAudioNodes({
|
||||
|
||||
@@ -63,9 +63,9 @@ export type OpenIDClientParts = Pick<
|
||||
* Gets a bearer token from the homeserver and then use it to authenticate
|
||||
* to the matrix RTC backend in order to get acces to the SFU.
|
||||
* It has built-in retry for calls to the homeserver with a backoff policy.
|
||||
* @param client
|
||||
* @param serviceUrl
|
||||
* @param matrixRoomId
|
||||
* @param client The Matrix client
|
||||
* @param serviceUrl The URL of the livekit SFU service
|
||||
* @param matrixRoomId The Matrix room ID for which to get the SFU config
|
||||
* @returns Object containing the token information
|
||||
* @throws FailToGetOpenIdToken
|
||||
*/
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<string, OTelCall>();
|
||||
private statsReportSpan: {
|
||||
span: Span | undefined;
|
||||
stats: OTelStatsReportEvent[];
|
||||
};
|
||||
private readonly speakingSpans = new Map<RoomMember, Map<string, Span>>();
|
||||
|
||||
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<CallFeedReport>,
|
||||
): 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<ConnectionStatsReport>,
|
||||
): void {
|
||||
this.buildCallStatsSpan(
|
||||
OTelStatsReportType.ConnectionReport,
|
||||
statsReport.report,
|
||||
);
|
||||
}
|
||||
|
||||
public onByteSentStatsReport(
|
||||
statsReport: GroupCallStatsReport<ByteSentStatsReport>,
|
||||
): 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<SummaryStatsReport>,
|
||||
): 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",
|
||||
}
|
||||
@@ -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<ConnectionStatsReport> = {
|
||||
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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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<ByteSentStatsReport>,
|
||||
): Attributes {
|
||||
const flatObject = {};
|
||||
ObjectFlattener.flattenObjectRecursive(
|
||||
statsReport.report,
|
||||
flatObject,
|
||||
"matrix.stats.bytesSent.",
|
||||
0,
|
||||
);
|
||||
return flatObject;
|
||||
}
|
||||
|
||||
public static flattenSummaryStatsReportObject(
|
||||
statsReport: GroupCallStatsReport<SummaryStatsReport>,
|
||||
): 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<string, unknown>, // 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
117
src/otel/otel.ts
117
src/otel/otel.ts
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -135,10 +135,10 @@ export class ReactionsReader {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetchest any hand wave reactions by the given sender on the given
|
||||
* Fetches any hand wave reactions by the given sender on the given
|
||||
* membership event.
|
||||
* @param membershipEventId
|
||||
* @param expectedSender
|
||||
* @param membershipEventId - The user membership event id.
|
||||
* @param expectedSender - The expected sender of the reaction.
|
||||
* @returns A MatrixEvent if one was found.
|
||||
*/
|
||||
private getLastReactionEvent(
|
||||
|
||||
@@ -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";
|
||||
@@ -189,7 +188,6 @@ export interface InCallViewProps {
|
||||
matrixRoom: MatrixRoom;
|
||||
muteStates: MuteStates;
|
||||
header: HeaderStyle;
|
||||
otelGroupCallMembership?: OTelGroupCallMembership;
|
||||
onShareClick: (() => void) | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -106,22 +106,18 @@ async function joinRoomAfterInvite(
|
||||
|
||||
export class CallTerminatedMessage extends Error {
|
||||
/**
|
||||
* Creates a new CallTerminatedMessage.
|
||||
*
|
||||
* @param icon The icon to display with the message
|
||||
* @param messageTitle The title of the call ended screen message (translated)
|
||||
* @param messageBody The message explaining the kind of termination
|
||||
* (kick, ban, knock reject, etc.) (translated)
|
||||
* @param reason The user-provided reason for the termination (kick/ban)
|
||||
*/
|
||||
public constructor(
|
||||
/**
|
||||
* The icon to display with the message.
|
||||
*/
|
||||
public readonly icon: ComponentType<SVGAttributes<SVGElement>>,
|
||||
messageTitle: string,
|
||||
/**
|
||||
* The message explaining the kind of termination (kick, ban, knock reject,
|
||||
* etc.) (translated)
|
||||
*/
|
||||
public readonly messageBody: string,
|
||||
/**
|
||||
* The user-provided reason for the termination (kick/ban)
|
||||
*/
|
||||
public readonly reason?: string,
|
||||
) {
|
||||
super(messageTitle);
|
||||
|
||||
@@ -99,7 +99,7 @@ class ConsoleLogger extends EventEmitter {
|
||||
|
||||
/**
|
||||
* Returns the log lines to flush to disk and empties the internal log buffer
|
||||
* @return {string} \n delimited log lines
|
||||
* @return \n delimited log lines
|
||||
*/
|
||||
public popLogs(): string {
|
||||
const logsToFlush = this.logs;
|
||||
@@ -109,7 +109,7 @@ class ConsoleLogger extends EventEmitter {
|
||||
|
||||
/**
|
||||
* Returns lines currently in the log buffer without removing them
|
||||
* @return {string} \n delimited log lines
|
||||
* @return \n delimited log lines
|
||||
*/
|
||||
public peekLogs(): string {
|
||||
return this.logs;
|
||||
@@ -139,7 +139,7 @@ class IndexedDBLogStore {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Promise} Resolves when the store is ready.
|
||||
* @return Resolves when the store is ready.
|
||||
*/
|
||||
public async connect(): Promise<void> {
|
||||
const req = this.indexedDB.open("logs");
|
||||
@@ -219,7 +219,7 @@ class IndexedDBLogStore {
|
||||
* This guarantees that we will always eventually do a flush when flush() is
|
||||
* called.
|
||||
*
|
||||
* @return {Promise} Resolved when the logs have been flushed.
|
||||
* @return Resolved when the logs have been flushed.
|
||||
*/
|
||||
public flush = async (): Promise<void> => {
|
||||
// check if a flush() operation is ongoing
|
||||
@@ -270,7 +270,7 @@ class IndexedDBLogStore {
|
||||
* returned are deleted at the same time, so this can be called at startup
|
||||
* to do house-keeping to keep the logs from growing too large.
|
||||
*
|
||||
* @return {Promise<Object[]>} Resolves to an array of objects. The array is
|
||||
* @return Resolves to an array of objects. The array is
|
||||
* sorted in time (oldest first) based on when the log file was created (the
|
||||
* log ID). The objects have said log ID in an "id" field and "lines" which
|
||||
* is a big string with all the new-line delimited logs.
|
||||
@@ -421,12 +421,12 @@ class IndexedDBLogStore {
|
||||
|
||||
/**
|
||||
* Helper method to collect results from a Cursor and promiseify it.
|
||||
* @param {ObjectStore|Index} store The store to perform openCursor on.
|
||||
* @param {IDBKeyRange=} keyRange Optional key range to apply on the cursor.
|
||||
* @param {Function} resultMapper A function which is repeatedly called with a
|
||||
* @param store - The store to perform openCursor on.
|
||||
* @param keyRange - Optional key range to apply on the cursor.
|
||||
* @param resultMapper - A function which is repeatedly called with a
|
||||
* Cursor.
|
||||
* Return the data you want to keep.
|
||||
* @return {Promise<T[]>} Resolves to an array of whatever you returned from
|
||||
* @return Resolves to an array of whatever you returned from
|
||||
* resultMapper.
|
||||
*/
|
||||
async function selectQuery<T>(
|
||||
@@ -464,9 +464,7 @@ declare global {
|
||||
/**
|
||||
* Configure rage shaking support for sending bug reports.
|
||||
* Modifies globals.
|
||||
* @param {boolean} setUpPersistence When true (default), the persistence will
|
||||
* be set up immediately for the logs.
|
||||
* @return {Promise} Resolves when set up.
|
||||
* @return Resolves when set up.
|
||||
*/
|
||||
export async function init(): Promise<void> {
|
||||
global.mx_rage_logger = new ConsoleLogger();
|
||||
@@ -503,7 +501,7 @@ export async function init(): Promise<void> {
|
||||
/**
|
||||
* Try to start up the rageshake storage for logs. If not possible (client unsupported)
|
||||
* then this no-ops.
|
||||
* @return {Promise} Resolves when complete.
|
||||
* @return Resolves when complete.
|
||||
*/
|
||||
async function tryInitStorage(): Promise<void> {
|
||||
if (global.mx_rage_initStoragePromise) {
|
||||
@@ -536,7 +534,7 @@ async function tryInitStorage(): Promise<void> {
|
||||
/**
|
||||
* Get a recent snapshot of the logs, ready for attaching to a bug report
|
||||
*
|
||||
* @return {LogEntry[]} list of log data
|
||||
* @return list of log data
|
||||
*/
|
||||
export async function getLogsForReport(): Promise<LogEntry[]> {
|
||||
if (!global.mx_rage_logger) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -81,7 +81,7 @@ export interface Props {
|
||||
localUser: { deviceId: string; userId: string };
|
||||
}
|
||||
/**
|
||||
* @returns {callPickupState$, autoLeave$}
|
||||
* @returns two observables:
|
||||
* `callPickupState$` The current call pickup state of the call.
|
||||
* - "unknown": The client has not yet sent the notification event. We don't know if it will because it first needs to send its own membership.
|
||||
* Then we can conclude if we were the first one to join or not.
|
||||
|
||||
@@ -138,7 +138,16 @@ interface Props {
|
||||
* We want
|
||||
* - a publisher
|
||||
* -
|
||||
* @param param0
|
||||
* @param props The properties required to create the local membership.
|
||||
* @param props.scope The observable scope to use.
|
||||
* @param props.connectionManager The connection manager to get connections from.
|
||||
* @param props.createPublisherFactory Factory to create a publisher once we have a connection.
|
||||
* @param props.joinMatrixRTC Callback to join the matrix RTC session once we have a transport.
|
||||
* @param props.homeserverConnected The homeserver connected state.
|
||||
* @param props.localTransport$ The local transport to use for publishing.
|
||||
* @param props.logger The logger to use.
|
||||
* @param props.muteStates The mute states for video and audio.
|
||||
* @param props.matrixRTCSession The matrix RTC session to join.
|
||||
* @returns
|
||||
* - publisher: The handle to create tracks and publish them to the room.
|
||||
* - connected$: the current connection state. Including matrix server and livekit server connection. (only considering the livekit server we are using for our own media publication)
|
||||
@@ -676,9 +685,11 @@ interface EnterRTCSessionOptions {
|
||||
* - Delay events management
|
||||
* - Handles retries (fails only after several attempts)
|
||||
*
|
||||
* @param rtcSession
|
||||
* @param transport
|
||||
* @param options
|
||||
* @param rtcSession - The MatrixRTCSession to join.
|
||||
* @param transport - The LivekitTransport to use for this session.
|
||||
* @param options - Options for entering the RTC session.
|
||||
* @param options.encryptMedia - Whether to encrypt media.
|
||||
* @param options.matrixRTCMode - The Matrix RTC mode to use.
|
||||
* @throws If the widget could not send ElementWidgetActions.JoinCall action.
|
||||
*/
|
||||
// Exported for unit testing
|
||||
|
||||
@@ -143,7 +143,7 @@ export class Publisher {
|
||||
this.logger.debug("createAndSetupTracks called");
|
||||
const lkRoom = this.connection.livekitRoom;
|
||||
// Observe mute state changes and update LiveKit microphone/camera states accordingly
|
||||
this.observeMuteStates(this.scope);
|
||||
this.observeMuteStates();
|
||||
|
||||
// Check if audio and/or video is enabled. We only create tracks if enabled,
|
||||
// because it could prompt for permission, and we don't want to do that unnecessarily.
|
||||
@@ -356,10 +356,9 @@ export class Publisher {
|
||||
|
||||
/**
|
||||
* Observe changes in the mute states and update the LiveKit room accordingly.
|
||||
* @param scope
|
||||
* @private
|
||||
*/
|
||||
private observeMuteStates(scope: ObservableScope): void {
|
||||
private observeMuteStates(): void {
|
||||
const lkRoom = this.connection.livekitRoom;
|
||||
this.muteStates.audio.setHandler(async (enable) => {
|
||||
try {
|
||||
|
||||
@@ -218,7 +218,7 @@ export class Connection {
|
||||
*
|
||||
* @param opts - Connection options {@link ConnectionOpts}.
|
||||
*
|
||||
* @param logger
|
||||
* @param logger - The logger to use.
|
||||
*/
|
||||
public constructor(opts: ConnectionOpts, logger: Logger) {
|
||||
this.logger = logger.getChild("[Connection]");
|
||||
|
||||
@@ -43,11 +43,11 @@ export class ECConnectionFactory implements ConnectionFactory {
|
||||
* @param client - The OpenID client parts for authentication, needed to get openID and JWT tokens.
|
||||
* @param devices - Used for video/audio out/in capture options.
|
||||
* @param processorState$ - Effects like background blur (only for publishing connection?)
|
||||
* @param livekitKeyProvider
|
||||
* @param livekitKeyProvider - Optional key provider for end-to-end encryption.
|
||||
* @param controlledAudioDevices - Option to indicate whether audio output device is controlled externally (native mobile app).
|
||||
* @param livekitRoomFactory - Optional factory function (for testing) to create LivekitRoom instances. If not provided, a default factory is used.
|
||||
* @param echoCancellation - Whether to enable echo cancellation for audio capture.
|
||||
* @param noiseSuppression - Whether to enable noise suppression for audio capture.
|
||||
* @param livekitRoomFactory - Optional factory function (for testing) to create LivekitRoom instances. If not provided, a default factory is used.
|
||||
*/
|
||||
public constructor(
|
||||
private client: OpenIDClientParts,
|
||||
|
||||
@@ -76,9 +76,11 @@ export interface IConnectionManager {
|
||||
|
||||
/**
|
||||
* Crete a `ConnectionManager`
|
||||
* @param scope the observable scope used by this object.
|
||||
* @param connectionFactory used to create new connections.
|
||||
* @param _transportsSubscriptions$ A list of Behaviors each containing a LIST of LivekitTransport.
|
||||
* @param props - Configuration object
|
||||
* @param props.scope - The observable scope used by this object
|
||||
* @param props.connectionFactory - Used to create new connections
|
||||
* @param props.inputTransports$ - A list of Behaviors each containing a LIST of LivekitTransport.
|
||||
* @param props.logger - The logger to use
|
||||
* Each of these behaviors can be interpreted as subscribed list of transports.
|
||||
*
|
||||
* Using `registerTransports` independent external modules can control what connections
|
||||
|
||||
@@ -22,9 +22,12 @@ import * as controls from "./controls";
|
||||
* Play a sound though a given AudioContext. Will take
|
||||
* care of connecting the correct buffer and gating
|
||||
* through gain.
|
||||
* @param volume The volume to play at.
|
||||
* @param ctx The context to play through.
|
||||
* @param buffer The buffer to play.
|
||||
* @param volume The volume to play at.
|
||||
* @param stereoPan The stereo pan to apply.
|
||||
* @param delayS Delay in seconds before starting playing.
|
||||
* @param abort Optional AbortController that can be used to stop playback.
|
||||
* @returns A promise that resolves when the sound has finished playing.
|
||||
*/
|
||||
async function playSound(
|
||||
@@ -55,9 +58,11 @@ async function playSound(
|
||||
* Play a sound though a given AudioContext, looping until stopped. Will take
|
||||
* care of connecting the correct buffer and gating
|
||||
* through gain.
|
||||
* @param volume The volume to play at.
|
||||
* @param ctx The context to play through.
|
||||
* @param buffer The buffer to play.
|
||||
* @param volume The volume to play at.
|
||||
* @param stereoPan The stereo pan to apply.
|
||||
* @param delayS Delay in seconds between each loop.
|
||||
* @returns A function used to end the sound. This function will return a promise when the sound has stopped.
|
||||
*/
|
||||
function playSoundLooping(
|
||||
@@ -120,7 +125,7 @@ interface UseAudioContext<S extends string> {
|
||||
/**
|
||||
* Add an audio context which can be used to play
|
||||
* a set of preloaded sounds.
|
||||
* @param props
|
||||
* @param props The properties for the audio context.
|
||||
* @returns Either an instance that can be used to play sounds, or null if not ready.
|
||||
*/
|
||||
export function useAudioContext<S extends string>(
|
||||
|
||||
@@ -77,6 +77,13 @@ export function shouldDisambiguate(
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates a display name for a member, optionally disambiguating it.
|
||||
* @param member - The member to calculate the display name for.
|
||||
* @param member.rawDisplayName - The raw display name of the member
|
||||
* @param member.userId - The user ID of the member
|
||||
* @param disambiguate - Whether to disambiguate the display name.
|
||||
*/
|
||||
export function calculateDisplayName(
|
||||
member: { rawDisplayName?: string; userId: string },
|
||||
disambiguate: boolean,
|
||||
|
||||
@@ -57,9 +57,16 @@ export class ElementCallError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration problem due to no MatrixRTC backend/SFU is exposed via .well-known and no fallback configured.
|
||||
*/
|
||||
export class MatrixRTCTransportMissingError extends ElementCallError {
|
||||
public domain: string;
|
||||
|
||||
/**
|
||||
* Creates an instance of MatrixRTCTransportMissingError.
|
||||
* @param domain - The domain where the MatrixRTC transport is missing.
|
||||
*/
|
||||
public constructor(domain: string) {
|
||||
super(
|
||||
t("error.call_is_not_supported"),
|
||||
@@ -75,6 +82,9 @@ export class MatrixRTCTransportMissingError extends ElementCallError {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error indicating that the connection to the call was lost and could not be re-established.
|
||||
*/
|
||||
export class ConnectionLostError extends ElementCallError {
|
||||
public constructor() {
|
||||
super(
|
||||
@@ -86,7 +96,16 @@ export class ConnectionLostError extends ElementCallError {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error indicating a failure in the membership manager causing the join call
|
||||
* operation to fail.
|
||||
*/
|
||||
export class MembershipManagerError extends ElementCallError {
|
||||
/**
|
||||
* Creates an instance of MembershipManagerError.
|
||||
*
|
||||
* @param error - The underlying error that caused the membership manager failure.
|
||||
*/
|
||||
public constructor(error: Error) {
|
||||
super(
|
||||
t("error.membership_manager"),
|
||||
@@ -98,6 +117,9 @@ export class MembershipManagerError extends ElementCallError {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error indicating that end-to-end encryption is not supported in the current environment.
|
||||
*/
|
||||
export class E2EENotSupportedError extends ElementCallError {
|
||||
public constructor() {
|
||||
super(
|
||||
@@ -109,7 +131,14 @@ export class E2EENotSupportedError extends ElementCallError {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error indicating an unknown issue occurred during a call operation.
|
||||
*/
|
||||
export class UnknownCallError extends ElementCallError {
|
||||
/**
|
||||
* Creates an instance of UnknownCallError.
|
||||
* @param error - The underlying error that caused the unknown issue.
|
||||
*/
|
||||
public constructor(error: Error) {
|
||||
super(
|
||||
t("error.generic"),
|
||||
@@ -122,7 +151,14 @@ export class UnknownCallError extends ElementCallError {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error indicating a failure to obtain an OpenID token.
|
||||
*/
|
||||
export class FailToGetOpenIdToken extends ElementCallError {
|
||||
/**
|
||||
* Creates an instance of FailToGetOpenIdToken.
|
||||
* @param error - The underlying error that caused the failure.
|
||||
*/
|
||||
public constructor(error: Error) {
|
||||
super(
|
||||
t("error.generic"),
|
||||
@@ -135,7 +171,14 @@ export class FailToGetOpenIdToken extends ElementCallError {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error indicating a failure to start publishing on a LiveKit connection.
|
||||
*/
|
||||
export class FailToStartLivekitConnection extends ElementCallError {
|
||||
/**
|
||||
* Creates an instance of FailToStartLivekitConnection.
|
||||
* @param e - An optional error message providing additional context.
|
||||
*/
|
||||
public constructor(e?: string) {
|
||||
super(
|
||||
t("error.failed_to_start_livekit"),
|
||||
@@ -146,6 +189,9 @@ export class FailToStartLivekitConnection extends ElementCallError {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error indicating that a LiveKit's server has hit its track limits.
|
||||
*/
|
||||
export class InsufficientCapacityError extends ElementCallError {
|
||||
public constructor() {
|
||||
super(
|
||||
@@ -157,6 +203,10 @@ export class InsufficientCapacityError extends ElementCallError {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error indicating that room creation is restricted by the SFU.
|
||||
* Only authorized users can create rooms, so the room must exist before connecting (done by the auth jwt service)
|
||||
*/
|
||||
export class SFURoomCreationRestrictedError extends ElementCallError {
|
||||
public constructor() {
|
||||
super(
|
||||
|
||||
@@ -188,7 +188,6 @@ function fullAliasFromRoomName(roomName: string, client: MatrixClient): string {
|
||||
* Applies some basic sanitisation to a room name that the user
|
||||
* has given us
|
||||
* @param input The room name from the user
|
||||
* @param client A matrix client object
|
||||
*/
|
||||
export function sanitiseRoomNameInput(input: string): string {
|
||||
// check to see if the user has entered a fully qualified room
|
||||
@@ -304,8 +303,9 @@ export async function createRoom(
|
||||
/**
|
||||
* Returns an absolute URL to that will load Element Call with the given room
|
||||
* @param roomId ID of the room
|
||||
* @param roomName Name of the room
|
||||
* @param encryptionSystem what encryption (or EncryptionSystem.Unencrypted) the room uses
|
||||
* @param roomName Name of the room
|
||||
* @param viaServers Optional list of servers to include as 'via' parameters in the URL
|
||||
*/
|
||||
export function getAbsoluteRoomUrl(
|
||||
roomId: string,
|
||||
@@ -321,8 +321,9 @@ export function getAbsoluteRoomUrl(
|
||||
/**
|
||||
* Returns a relative URL to that will load Element Call with the given room
|
||||
* @param roomId ID of the room
|
||||
* @param roomName Name of the room
|
||||
* @param encryptionSystem what encryption (or EncryptionSystem.Unencrypted) the room uses
|
||||
* @param roomName Name of the room
|
||||
* @param viaServers Optional list of servers to include as 'via' parameters in the URL
|
||||
*/
|
||||
export function getRelativeRoomUrl(
|
||||
roomId: string,
|
||||
|
||||
@@ -8,6 +8,7 @@ Please see LICENSE in the repository root for full details.
|
||||
/**
|
||||
* Finds a media device with label matching 'deviceName'
|
||||
* @param deviceName The label of the device to look for
|
||||
* @param kind The kind of media device to look for
|
||||
* @param devices The list of devices to search
|
||||
* @returns A matching media device or undefined if no matching device was found
|
||||
*/
|
||||
|
||||
@@ -135,7 +135,6 @@ interface ItemHandle<Data, Item> {
|
||||
* requested at a later time, and destroyed (have their scope ended) when the
|
||||
* key is no longer requested.
|
||||
*
|
||||
* @param input$ The input value to be mapped.
|
||||
* @param generator A generator function yielding a tuple of keys and the
|
||||
* currently associated data for each item that it wants to exist.
|
||||
* @param factory A function constructing an individual item, given the item's key,
|
||||
|
||||
Reference in New Issue
Block a user