diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 067c5246..11116ed2 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -8,7 +8,7 @@ Please see LICENSE in the repository root for full details. `; module.exports = { - plugins: ["matrix-org", "rxjs"], + plugins: ["matrix-org", "rxjs", "jsdoc"], extends: [ "plugin:matrix-org/react", "plugin:matrix-org/a11y", @@ -26,6 +26,13 @@ module.exports = { node: true, }, rules: { + "jsdoc/no-types": "error", + "jsdoc/empty-tags": "error", + "jsdoc/check-property-names": "error", + "jsdoc/check-values": "error", + "jsdoc/check-param-names": "warn", + // "jsdoc/require-param": "warn", + "jsdoc/require-param-description": "warn", "matrix-org/require-copyright-header": ["error", COPYRIGHT_HEADER], "jsx-a11y/media-has-caption": "off", "react/display-name": "error", @@ -75,6 +82,23 @@ module.exports = { "no-console": ["error"], }, }, + { + files: [ + "**/*.test.ts", + "**/*.test.tsx", + "**/test.ts", + "**/test.tsx", + "**/test-**", + ], + rules: { + "jsdoc/no-types": "off", + "jsdoc/empty-tags": "off", + "jsdoc/check-property-names": "off", + "jsdoc/check-values": "off", + "jsdoc/check-param-names": "off", + "jsdoc/require-param-description": "off", + }, + }, ], settings: { react: { diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 6a5c090e..7f2c58fe 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -61,6 +61,7 @@ jobs: docker_tags: | type=sha,format=short,event=branch type=raw,value=${{ github.event.release.tag_name }} + type=raw,value=latest # Like before, using ${{ env.VERSION }} above doesn't work add_docker_release_note: needs: publish_docker diff --git a/config/otel_dev/README.md b/config/otel_dev/README.md index ea6c09a2..87c3da93 100644 --- a/config/otel_dev/README.md +++ b/config/otel_dev/README.md @@ -1,5 +1,12 @@ # OpenTelemetry Collector for development +## Edit: + +Open telemetry has been removed in: https://github.com/element-hq/element-call/pull/3586 +Check this PR to get back the implementation or to use it as reference to add it back. + +--- + This directory contains a docker compose file that starts a jaeger all-in-one instance with an in-memory database, along with a standalone OpenTelemetry collector that forwards traces into the jaeger. Jaeger has a built-in OpenTelemetry collector, but it can't be diff --git a/package.json b/package.json index 71a8c4e0..944fc43b 100644 --- a/package.json +++ b/package.json @@ -49,13 +49,6 @@ "@livekit/protocol": "^1.42.2", "@livekit/track-processors": "^0.6.0 || ^0.7.1", "@mediapipe/tasks-vision": "^0.10.18", - "@opentelemetry/api": "^1.4.0", - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/exporter-trace-otlp-http": "^0.208.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.57.0", "@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-slider": "^1.1.2", @@ -94,6 +87,7 @@ "eslint-config-prettier": "^10.0.0", "eslint-plugin-deprecate": "^0.8.2", "eslint-plugin-import": "^2.26.0", + "eslint-plugin-jsdoc": "^61.5.0", "eslint-plugin-jsx-a11y": "^6.5.1", "eslint-plugin-matrix-org": "2.1.0", "eslint-plugin-react": "^7.29.4", diff --git a/playwright/fixtures/widget-user.ts b/playwright/fixtures/widget-user.ts index 6236928c..f1f738b7 100644 --- a/playwright/fixtures/widget-user.ts +++ b/playwright/fixtures/widget-user.ts @@ -67,7 +67,6 @@ const CONFIG_JSON = { /** * Set the Element Call URL in the dev tool settings using `window.mxSettingsStore` via `page.evaluate`. - * @param page */ const setDevToolElementCallDevUrl = process.env.USE_DOCKER ? async (page: Page): Promise => { diff --git a/src/analytics/PosthogSpanProcessor.ts b/src/analytics/PosthogSpanProcessor.ts deleted file mode 100644 index a0046200..00000000 --- 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 eca657db..00000000 --- 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 {} -} diff --git a/src/e2ee/sharedKeyManagement.ts b/src/e2ee/sharedKeyManagement.ts index c68ba453..18d007e2 100644 --- a/src/e2ee/sharedKeyManagement.ts +++ b/src/e2ee/sharedKeyManagement.ts @@ -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. */ diff --git a/src/initializer.tsx b/src/initializer.tsx index 267d818a..419b1291 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/livekit/MatrixAudioRenderer.tsx b/src/livekit/MatrixAudioRenderer.tsx index 5b1149e9..741529b8 100644 --- a/src/livekit/MatrixAudioRenderer.tsx +++ b/src/livekit/MatrixAudioRenderer.tsx @@ -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({ diff --git a/src/livekit/openIDSFU.ts b/src/livekit/openIDSFU.ts index b3c07397..34c98a88 100644 --- a/src/livekit/openIDSFU.ts +++ b/src/livekit/openIDSFU.ts @@ -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 */ diff --git a/src/otel/OTelCall.ts b/src/otel/OTelCall.ts deleted file mode 100644 index e70cedf2..00000000 --- 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 69e41547..00000000 --- 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 59c780a5..00000000 --- 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 c81acd4f..00000000 --- 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 675d793e..00000000 --- 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 668b989c..00000000 --- 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 5685617c..00000000 --- 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 a963c743..00000000 --- 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 0bf0573f..00000000 --- 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 915c3d58..00000000 --- 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/reactions/ReactionsReader.ts b/src/reactions/ReactionsReader.ts index 74b47c77..7ce59812 100644 --- a/src/reactions/ReactionsReader.ts +++ b/src/reactions/ReactionsReader.ts @@ -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( diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 07adacbc..c6f22b43 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"; @@ -189,7 +188,6 @@ export interface InCallViewProps { matrixRoom: MatrixRoom; muteStates: MuteStates; header: HeaderStyle; - otelGroupCallMembership?: OTelGroupCallMembership; onShareClick: (() => void) | null; } diff --git a/src/room/useLoadGroupCall.ts b/src/room/useLoadGroupCall.ts index ab6ccf64..2cd0d40b 100644 --- a/src/room/useLoadGroupCall.ts +++ b/src/room/useLoadGroupCall.ts @@ -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>, 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); diff --git a/src/settings/rageshake.ts b/src/settings/rageshake.ts index 6c1a0f61..26d0839b 100644 --- a/src/settings/rageshake.ts +++ b/src/settings/rageshake.ts @@ -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 { 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 => { // 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} 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} Resolves to an array of whatever you returned from + * @return Resolves to an array of whatever you returned from * resultMapper. */ async function selectQuery( @@ -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 { global.mx_rage_logger = new ConsoleLogger(); @@ -503,7 +501,7 @@ export async function init(): Promise { /** * 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 { if (global.mx_rage_initStoragePromise) { @@ -536,7 +534,7 @@ async function tryInitStorage(): Promise { /** * 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 { if (!global.mx_rage_logger) { diff --git a/src/settings/submit-rageshake.ts b/src/settings/submit-rageshake.ts index bfd55126..276d8e60 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) { diff --git a/src/state/CallViewModel/CallNotificationLifecycle.ts b/src/state/CallViewModel/CallNotificationLifecycle.ts index 2a0bf2f1..d90f35ba 100644 --- a/src/state/CallViewModel/CallNotificationLifecycle.ts +++ b/src/state/CallViewModel/CallNotificationLifecycle.ts @@ -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. diff --git a/src/state/CallViewModel/localMember/LocalMember.ts b/src/state/CallViewModel/localMember/LocalMember.ts index 6d28bc56..dc22db23 100644 --- a/src/state/CallViewModel/localMember/LocalMember.ts +++ b/src/state/CallViewModel/localMember/LocalMember.ts @@ -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 diff --git a/src/state/CallViewModel/localMember/Publisher.ts b/src/state/CallViewModel/localMember/Publisher.ts index 3cb3bd04..27c53726 100644 --- a/src/state/CallViewModel/localMember/Publisher.ts +++ b/src/state/CallViewModel/localMember/Publisher.ts @@ -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 { diff --git a/src/state/CallViewModel/remoteMembers/Connection.ts b/src/state/CallViewModel/remoteMembers/Connection.ts index cf92e2a6..41dfe665 100644 --- a/src/state/CallViewModel/remoteMembers/Connection.ts +++ b/src/state/CallViewModel/remoteMembers/Connection.ts @@ -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]"); diff --git a/src/state/CallViewModel/remoteMembers/ConnectionFactory.ts b/src/state/CallViewModel/remoteMembers/ConnectionFactory.ts index 48e5b8d8..c3364059 100644 --- a/src/state/CallViewModel/remoteMembers/ConnectionFactory.ts +++ b/src/state/CallViewModel/remoteMembers/ConnectionFactory.ts @@ -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, diff --git a/src/state/CallViewModel/remoteMembers/ConnectionManager.ts b/src/state/CallViewModel/remoteMembers/ConnectionManager.ts index c1b4af59..101e34ed 100644 --- a/src/state/CallViewModel/remoteMembers/ConnectionManager.ts +++ b/src/state/CallViewModel/remoteMembers/ConnectionManager.ts @@ -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 diff --git a/src/useAudioContext.tsx b/src/useAudioContext.tsx index 59334dda..4d08dde8 100644 --- a/src/useAudioContext.tsx +++ b/src/useAudioContext.tsx @@ -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 { /** * 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( diff --git a/src/utils/displayname.ts b/src/utils/displayname.ts index 5ab5de9b..bc49b29e 100644 --- a/src/utils/displayname.ts +++ b/src/utils/displayname.ts @@ -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, diff --git a/src/utils/errors.ts b/src/utils/errors.ts index bb37754a..cddf90de 100644 --- a/src/utils/errors.ts +++ b/src/utils/errors.ts @@ -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( diff --git a/src/utils/matrix.ts b/src/utils/matrix.ts index 0a2b5c1a..4e3ae3c3 100644 --- a/src/utils/matrix.ts +++ b/src/utils/matrix.ts @@ -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, diff --git a/src/utils/media.ts b/src/utils/media.ts index cdd81aa7..3750aa4e 100644 --- a/src/utils/media.ts +++ b/src/utils/media.ts @@ -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 */ diff --git a/src/utils/observable.ts b/src/utils/observable.ts index a6dafea3..9739353f 100644 --- a/src/utils/observable.ts +++ b/src/utils/observable.ts @@ -135,7 +135,6 @@ interface ItemHandle { * 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, diff --git a/yarn.lock b/yarn.lock index 40e6a8cf..b350926d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2430,6 +2430,26 @@ __metadata: languageName: node linkType: hard +"@es-joy/jsdoccomment@npm:~0.76.0": + version: 0.76.0 + resolution: "@es-joy/jsdoccomment@npm:0.76.0" + dependencies: + "@types/estree": "npm:^1.0.8" + "@typescript-eslint/types": "npm:^8.46.0" + comment-parser: "npm:1.4.1" + esquery: "npm:^1.6.0" + jsdoc-type-pratt-parser: "npm:~6.10.0" + checksum: 10c0/8fe4edec7d60562787ea8c77193ebe8737a9e28ec3143d383506b63890d0ffd45a2813e913ad1f00f227cb10e3a1fb913e5a696b33d499dc564272ff1a6f3fdb + languageName: node + linkType: hard + +"@es-joy/resolve.exports@npm:1.2.0": + version: 1.2.0 + resolution: "@es-joy/resolve.exports@npm:1.2.0" + checksum: 10c0/7e4713471f5eccb17a925a12415a2d9e372a42376813a19f6abd9c35e8d01ab1403777265817da67c6150cffd4f558d9ad51e26a8de6911dad89d9cb7eedacd8 + languageName: node + linkType: hard + "@esbuild/aix-ppc64@npm:0.25.1": version: 0.25.1 resolution: "@esbuild/aix-ppc64@npm:0.25.1" @@ -2874,6 +2894,15 @@ __metadata: languageName: node linkType: hard +"@floating-ui/core@npm:^1.7.3": + version: 1.7.3 + resolution: "@floating-ui/core@npm:1.7.3" + dependencies: + "@floating-ui/utils": "npm:^0.2.10" + checksum: 10c0/edfc23800122d81df0df0fb780b7328ae6c5f00efbb55bd48ea340f4af8c5b3b121ceb4bb81220966ab0f87b443204d37105abdd93d94846468be3243984144c + languageName: node + linkType: hard + "@floating-ui/dom@npm:1.6.13, @floating-ui/dom@npm:^1.0.0": version: 1.6.13 resolution: "@floating-ui/dom@npm:1.6.13" @@ -2884,6 +2913,16 @@ __metadata: languageName: node linkType: hard +"@floating-ui/dom@npm:1.7.4": + version: 1.7.4 + resolution: "@floating-ui/dom@npm:1.7.4" + dependencies: + "@floating-ui/core": "npm:^1.7.3" + "@floating-ui/utils": "npm:^0.2.10" + checksum: 10c0/da6166c25f9b0729caa9f498685a73a0e28251613b35d27db8de8014bc9d045158a23c092b405321a3d67c2064909b6e2a7e6c1c9cc0f62967dca5779f5aef30 + languageName: node + linkType: hard + "@floating-ui/react-dom@npm:^2.0.0, @floating-ui/react-dom@npm:^2.1.2": version: 2.1.2 resolution: "@floating-ui/react-dom@npm:2.1.2" @@ -2910,6 +2949,13 @@ __metadata: languageName: node linkType: hard +"@floating-ui/utils@npm:^0.2.10": + version: 0.2.10 + resolution: "@floating-ui/utils@npm:0.2.10" + checksum: 10c0/e9bc2a1730ede1ee25843937e911ab6e846a733a4488623cd353f94721b05ec2c9ec6437613a2ac9379a94c2fd40c797a2ba6fa1df2716f5ce4aa6ddb1cf9ea4 + languageName: node + linkType: hard + "@floating-ui/utils@npm:^0.2.9": version: 0.2.9 resolution: "@floating-ui/utils@npm:0.2.9" @@ -3167,7 +3213,7 @@ __metadata: languageName: node linkType: hard -"@livekit/components-core@npm:0.12.11, @livekit/components-core@npm:^0.12.0": +"@livekit/components-core@npm:0.12.11": version: 0.12.11 resolution: "@livekit/components-core@npm:0.12.11" dependencies: @@ -3181,6 +3227,20 @@ __metadata: languageName: node linkType: hard +"@livekit/components-core@npm:^0.12.0": + version: 0.12.12 + resolution: "@livekit/components-core@npm:0.12.12" + dependencies: + "@floating-ui/dom": "npm:1.7.4" + loglevel: "npm:1.9.1" + rxjs: "npm:7.8.2" + peerDependencies: + livekit-client: ^2.15.14 + tslib: ^2.6.2 + checksum: 10c0/788ae01fa6c58a0edbd629f4195f2f3a7bc94660d2fb729af8b27cab2b151abe36cd0a666989811c6187e51d32c847119853010a82be55844750ab3978079c38 + languageName: node + linkType: hard + "@livekit/components-react@npm:^2.0.0": version: 2.9.16 resolution: "@livekit/components-react@npm:2.9.16" @@ -3434,146 +3494,6 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/api-logs@npm:0.208.0": - version: 0.208.0 - resolution: "@opentelemetry/api-logs@npm:0.208.0" - dependencies: - "@opentelemetry/api": "npm:^1.3.0" - checksum: 10c0/dc1fbee6219df4166509f43b74ea936bb18b6d594565b0bcf56b654a1c958b50d6046b8739dc36c98149fe890c02150ff3814e963f5ea439a07ff3c562555b99 - 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.2.0, @opentelemetry/core@npm:^2.0.0": - version: 2.2.0 - resolution: "@opentelemetry/core@npm:2.2.0" - dependencies: - "@opentelemetry/semantic-conventions": "npm:^1.29.0" - peerDependencies: - "@opentelemetry/api": ">=1.0.0 <1.10.0" - checksum: 10c0/f618b63f2f560d052791d2406b1411722aa4b0585031242e6906f869f0a707ffe725c4b29bf18aed1f202e1ab5dfc3a9f769c517ac8521338b33ac8c4265fba9 - languageName: node - linkType: hard - -"@opentelemetry/exporter-trace-otlp-http@npm:^0.208.0": - version: 0.208.0 - resolution: "@opentelemetry/exporter-trace-otlp-http@npm:0.208.0" - dependencies: - "@opentelemetry/core": "npm:2.2.0" - "@opentelemetry/otlp-exporter-base": "npm:0.208.0" - "@opentelemetry/otlp-transformer": "npm:0.208.0" - "@opentelemetry/resources": "npm:2.2.0" - "@opentelemetry/sdk-trace-base": "npm:2.2.0" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10c0/5e901388febb6e797aa7ed1705373df322df4ba47eaf545a85bb4e1e3b3056993a455d9d6a68e94dc57e6c8112d580129b374bf2982595244edf664663b93e66 - languageName: node - linkType: hard - -"@opentelemetry/otlp-exporter-base@npm:0.208.0": - version: 0.208.0 - resolution: "@opentelemetry/otlp-exporter-base@npm:0.208.0" - dependencies: - "@opentelemetry/core": "npm:2.2.0" - "@opentelemetry/otlp-transformer": "npm:0.208.0" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10c0/c2b2014da16e2a2be0ebe525b1a62b3e64e286fc9c2575444e4c75bbe0060a83762172180dc7a97cdaaaa8c6765076073edea30340459fc1820cd43468ff98b0 - languageName: node - linkType: hard - -"@opentelemetry/otlp-transformer@npm:0.208.0": - version: 0.208.0 - resolution: "@opentelemetry/otlp-transformer@npm:0.208.0" - dependencies: - "@opentelemetry/api-logs": "npm:0.208.0" - "@opentelemetry/core": "npm:2.2.0" - "@opentelemetry/resources": "npm:2.2.0" - "@opentelemetry/sdk-logs": "npm:0.208.0" - "@opentelemetry/sdk-metrics": "npm:2.2.0" - "@opentelemetry/sdk-trace-base": "npm:2.2.0" - protobufjs: "npm:^7.3.0" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10c0/70c04b2a52f0b2f8aece25ad21401c32ed3136ccd6e82b767d570a24d5456a5ded206ed4cc60ebc09eac08a4aa9c03bc8dcbf10730e491f1af3e7768c361ac12 - languageName: node - linkType: hard - -"@opentelemetry/resources@npm:2.2.0, @opentelemetry/resources@npm:^2.0.0": - version: 2.2.0 - resolution: "@opentelemetry/resources@npm:2.2.0" - dependencies: - "@opentelemetry/core": "npm:2.2.0" - "@opentelemetry/semantic-conventions": "npm:^1.29.0" - peerDependencies: - "@opentelemetry/api": ">=1.3.0 <1.10.0" - checksum: 10c0/f08fa69ccccb6d14b6932fabe6f8e097c0dfc41ae8f4c0f6c54fb04bc3d9c04e742da3e22d7240d74b585287101126d97a0da192b493a9724dc07a56ca1b77e0 - languageName: node - linkType: hard - -"@opentelemetry/sdk-logs@npm:0.208.0": - version: 0.208.0 - resolution: "@opentelemetry/sdk-logs@npm:0.208.0" - dependencies: - "@opentelemetry/api-logs": "npm:0.208.0" - "@opentelemetry/core": "npm:2.2.0" - "@opentelemetry/resources": "npm:2.2.0" - peerDependencies: - "@opentelemetry/api": ">=1.4.0 <1.10.0" - checksum: 10c0/a167ee7d2818e435ff7480836461f94543e4e39f0e8e8013d462c635def9b960dcf1a29e5536743946b51ef13b764f518d9edb511e89bc1e8995acc96f54241f - languageName: node - linkType: hard - -"@opentelemetry/sdk-metrics@npm:2.2.0": - version: 2.2.0 - resolution: "@opentelemetry/sdk-metrics@npm:2.2.0" - dependencies: - "@opentelemetry/core": "npm:2.2.0" - "@opentelemetry/resources": "npm:2.2.0" - peerDependencies: - "@opentelemetry/api": ">=1.9.0 <1.10.0" - checksum: 10c0/a2668f9ef937123552a5ab96ec23675931ae7d3223ec7a31c8aac95fbbfb0b03a54a873f17f2356b04db7031421e7e3d7e3bf9d96d9069a0b97c680a2c158bc4 - languageName: node - linkType: hard - -"@opentelemetry/sdk-trace-base@npm:2.2.0, @opentelemetry/sdk-trace-base@npm:^2.0.0": - version: 2.2.0 - resolution: "@opentelemetry/sdk-trace-base@npm:2.2.0" - dependencies: - "@opentelemetry/core": "npm:2.2.0" - "@opentelemetry/resources": "npm:2.2.0" - "@opentelemetry/semantic-conventions": "npm:^1.29.0" - peerDependencies: - "@opentelemetry/api": ">=1.3.0 <1.10.0" - checksum: 10c0/a67715b71d7253cd61ea79954f56491796ac7a660d03d5381fd81defd4546042bb465b27e1b6eee4b1ed32c00305a5349a16d04fd44314c9a1d371a0a638107a - languageName: node - linkType: hard - -"@opentelemetry/sdk-trace-web@npm:^2.0.0": - version: 2.2.0 - resolution: "@opentelemetry/sdk-trace-web@npm:2.2.0" - dependencies: - "@opentelemetry/core": "npm:2.2.0" - "@opentelemetry/sdk-trace-base": "npm:2.2.0" - peerDependencies: - "@opentelemetry/api": ">=1.0.0 <1.10.0" - checksum: 10c0/002296bb929d4992575415ea93c2a68411cd5018c6e7fa27b7cd48e78741b7eddfb87eb0903e7ca42740acb04ac4e8508d00f7651bdc63b433055af955201d31 - languageName: node - linkType: hard - -"@opentelemetry/semantic-conventions@npm:^1.25.1, @opentelemetry/semantic-conventions@npm:^1.29.0": - version: 1.38.0 - resolution: "@opentelemetry/semantic-conventions@npm:1.38.0" - checksum: 10c0/ae93e39ac18bf47df2b11d43e9a0dc1673b9d33e5f1e7f357c92968e6329fb9a67cf8a447e9a7150948ee3f8178b38274db365b8fa775a8c54802e0c6ccdd2ca - languageName: node - linkType: hard - "@oxc-resolver/binding-android-arm-eabi@npm:11.16.2": version: 11.16.2 resolution: "@oxc-resolver/binding-android-arm-eabi@npm:11.16.2" @@ -3878,79 +3798,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" @@ -5403,6 +5250,13 @@ __metadata: languageName: node linkType: hard +"@sindresorhus/base62@npm:^1.0.0": + version: 1.0.0 + resolution: "@sindresorhus/base62@npm:1.0.0" + checksum: 10c0/9a14df0f058fdf4731c30f0f05728a4822144ee42236030039d7fa5a1a1072c2879feba8091fd4a17c8922d1056bc07bada77c31fddc3e15836fc05a266fd918 + languageName: node + linkType: hard + "@stylistic/eslint-plugin@npm:^3.0.0": version: 3.1.0 resolution: "@stylistic/eslint-plugin@npm:3.1.0" @@ -5684,7 +5538,7 @@ __metadata: languageName: node linkType: hard -"@types/estree@npm:1.0.8": +"@types/estree@npm:1.0.8, @types/estree@npm:^1.0.8": version: 1.0.8 resolution: "@types/estree@npm:1.0.8" checksum: 10c0/39d34d1afaa338ab9763f37ad6066e3f349444f9052b9676a7cc0252ef9485a41c6d81c9c4e0d26e9077993354edf25efc853f3224dd4b447175ef62bdcc86a5 @@ -5767,7 +5621,7 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=13.7.0": +"@types/node@npm:*": version: 25.0.3 resolution: "@types/node@npm:25.0.3" dependencies: @@ -6006,7 +5860,7 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/types@npm:8.51.0, @typescript-eslint/types@npm:^8.51.0": +"@typescript-eslint/types@npm:8.51.0, @typescript-eslint/types@npm:^8.46.0, @typescript-eslint/types@npm:^8.51.0": version: 8.51.0 resolution: "@typescript-eslint/types@npm:8.51.0" checksum: 10c0/eb3473d0bb71eb886438f35887b620ffadae7853b281752a40c73158aee644d136adeb82549be7d7c30f346fe888b2e979dff7e30e67b35377e8281018034529 @@ -6372,6 +6226,15 @@ __metadata: languageName: node linkType: hard +"acorn@npm:^8.15.0": + version: 8.15.0 + resolution: "acorn@npm:8.15.0" + bin: + acorn: bin/acorn + checksum: 10c0/dec73ff59b7d6628a01eebaece7f2bdb8bb62b9b5926dcad0f8931f2b8b79c2be21f6c68ac095592adb5adb15831a3635d9343e6a91d028bbe85d564875ec3ec + languageName: node + linkType: hard + "acorn@npm:^8.9.0": version: 8.12.1 resolution: "acorn@npm:8.12.1" @@ -6481,6 +6344,13 @@ __metadata: languageName: node linkType: hard +"are-docs-informative@npm:^0.0.2": + version: 0.0.2 + resolution: "are-docs-informative@npm:0.0.2" + checksum: 10c0/f0326981bd699c372d268b526b170a28f2e1aec2cf99d7de0686083528427ecdf6ae41fef5d9988e224a5616298af747ad8a76e7306b0a7c97cc085a99636d60 + languageName: node + linkType: hard + "argparse@npm:^2.0.1": version: 2.0.1 resolution: "argparse@npm:2.0.1" @@ -7561,6 +7431,13 @@ __metadata: languageName: node linkType: hard +"comment-parser@npm:1.4.1": + version: 1.4.1 + resolution: "comment-parser@npm:1.4.1" + checksum: 10c0/d6c4be3f5be058f98b24f2d557f745d8fe1cc9eb75bebbdccabd404a0e1ed41563171b16285f593011f8b6a5ec81f564fb1f2121418ac5cbf0f49255bf0840dd + languageName: node + linkType: hard + "common-tags@npm:^1.8.0": version: 1.8.2 resolution: "common-tags@npm:1.8.2" @@ -7971,6 +7848,18 @@ __metadata: languageName: node linkType: hard +"debug@npm:^4.4.3": + version: 4.4.3 + resolution: "debug@npm:4.4.3" + dependencies: + ms: "npm:^2.1.3" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 10c0/d79136ec6c83ecbefd0f6a5593da6a9c91ec4d7ddc4b54c883d6e71ec9accb5f67a1a5e96d00a328196b5b5c86d365e98d8a3a70856aaf16b4e7b1985e67f5a6 + languageName: node + linkType: hard + "decamelize@npm:^1.2.0": version: 1.2.0 resolution: "decamelize@npm:1.2.0" @@ -8302,13 +8191,6 @@ __metadata: "@livekit/protocol": "npm:^1.42.2" "@livekit/track-processors": "npm:^0.6.0 || ^0.7.1" "@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.208.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.57.0" "@radix-ui/react-dialog": "npm:^1.0.4" "@radix-ui/react-slider": "npm:^1.1.2" @@ -8347,6 +8229,7 @@ __metadata: eslint-config-prettier: "npm:^10.0.0" eslint-plugin-deprecate: "npm:^0.8.2" eslint-plugin-import: "npm:^2.26.0" + eslint-plugin-jsdoc: "npm:^61.5.0" eslint-plugin-jsx-a11y: "npm:^6.5.1" eslint-plugin-matrix-org: "npm:2.1.0" eslint-plugin-react: "npm:^7.29.4" @@ -9010,6 +8893,30 @@ __metadata: languageName: node linkType: hard +"eslint-plugin-jsdoc@npm:^61.5.0": + version: 61.5.0 + resolution: "eslint-plugin-jsdoc@npm:61.5.0" + dependencies: + "@es-joy/jsdoccomment": "npm:~0.76.0" + "@es-joy/resolve.exports": "npm:1.2.0" + are-docs-informative: "npm:^0.0.2" + comment-parser: "npm:1.4.1" + debug: "npm:^4.4.3" + escape-string-regexp: "npm:^4.0.0" + espree: "npm:^10.4.0" + esquery: "npm:^1.6.0" + html-entities: "npm:^2.6.0" + object-deep-merge: "npm:^2.0.0" + parse-imports-exports: "npm:^0.2.4" + semver: "npm:^7.7.3" + spdx-expression-parse: "npm:^4.0.0" + to-valid-identifier: "npm:^1.0.0" + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 + checksum: 10c0/fabb04f6efe58a167a0839d3c05676a76080c6e91d98a269fa768c1bfd835aa0ded5822d400da2874216177044d2d227ebe241d73e923f3fe1c08bafd19cfd3d + languageName: node + linkType: hard + "eslint-plugin-jsx-a11y@npm:^6.5.1": version: 6.10.2 resolution: "eslint-plugin-jsx-a11y@npm:6.10.2" @@ -9244,6 +9151,17 @@ __metadata: languageName: node linkType: hard +"espree@npm:^10.4.0": + version: 10.4.0 + resolution: "espree@npm:10.4.0" + dependencies: + acorn: "npm:^8.15.0" + acorn-jsx: "npm:^5.3.2" + eslint-visitor-keys: "npm:^4.2.1" + checksum: 10c0/c63fe06131c26c8157b4083313cb02a9a54720a08e21543300e55288c40e06c3fc284bdecf108d3a1372c5934a0a88644c98714f38b6ae8ed272b40d9ea08d6b + languageName: node + linkType: hard + "espree@npm:^9.6.0, espree@npm:^9.6.1": version: 9.6.1 resolution: "espree@npm:9.6.1" @@ -10137,6 +10055,13 @@ __metadata: languageName: node linkType: hard +"html-entities@npm:^2.6.0": + version: 2.6.0 + resolution: "html-entities@npm:2.6.0" + checksum: 10c0/7c8b15d9ea0cd00dc9279f61bab002ba6ca8a7a0f3c36ed2db3530a67a9621c017830d1d2c1c65beb9b8e3436ea663e9cf8b230472e0e413359399413b27c8b7 + languageName: node + linkType: hard + "html-escaper@npm:^2.0.0": version: 2.0.2 resolution: "html-escaper@npm:2.0.2" @@ -10931,6 +10856,13 @@ __metadata: languageName: node linkType: hard +"jsdoc-type-pratt-parser@npm:~6.10.0": + version: 6.10.0 + resolution: "jsdoc-type-pratt-parser@npm:6.10.0" + checksum: 10c0/8ea395df0cae0e41d4bdba5f8d81b8d3e467fe53d1e4182a5d4e653235a5f17d60ed137343d68dbc74fa10e767f1c58fb85b1f6d5489c2cf16fc7216cc6d3e1a + languageName: node + linkType: hard + "jsdom@npm:^26.0.0": version: 26.1.0 resolution: "jsdom@npm:26.1.0" @@ -11245,13 +11177,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" @@ -11878,6 +11803,13 @@ __metadata: languageName: node linkType: hard +"object-deep-merge@npm:^2.0.0": + version: 2.0.0 + resolution: "object-deep-merge@npm:2.0.0" + checksum: 10c0/69e8741131ad49fa8720fb96007a3c82dca1119b5d874151d2ecbcc3b44ccd46e8553c7a30b0abcba752c099ba361bbba97f33a68c9ae54c57eed7be116ffc97 + languageName: node + linkType: hard + "object-inspect@npm:^1.13.3": version: 1.13.3 resolution: "object-inspect@npm:1.13.3" @@ -12212,6 +12144,15 @@ __metadata: languageName: node linkType: hard +"parse-imports-exports@npm:^0.2.4": + version: 0.2.4 + resolution: "parse-imports-exports@npm:0.2.4" + dependencies: + parse-statements: "npm:1.0.11" + checksum: 10c0/51b729037208abdf65c4a1f8e9ed06f4e7ccd907c17c668a64db54b37d95bb9e92081f8b16e4133e14102af3cb4e89870975b6ad661b4d654e9ec8f4fb5c77d6 + languageName: node + linkType: hard + "parse-json@npm:^5.0.0, parse-json@npm:^5.2.0": version: 5.2.0 resolution: "parse-json@npm:5.2.0" @@ -12224,6 +12165,13 @@ __metadata: languageName: node linkType: hard +"parse-statements@npm:1.0.11": + version: 1.0.11 + resolution: "parse-statements@npm:1.0.11" + checksum: 10c0/48960e085019068a5f5242e875fd9d21ec87df2e291acf5ad4e4887b40eab6929a8c8d59542acb85a6497e870c5c6a24f5ab7f980ef5f907c14cc5f7984a93f3 + languageName: node + linkType: hard + "parse5-htmlparser2-tree-adapter@npm:^7.0.0": version: 7.1.0 resolution: "parse5-htmlparser2-tree-adapter@npm:7.1.0" @@ -12948,26 +12896,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" @@ -13476,6 +13404,13 @@ __metadata: languageName: node linkType: hard +"reserved-identifiers@npm:^1.0.0": + version: 1.2.0 + resolution: "reserved-identifiers@npm:1.2.0" + checksum: 10c0/b82651b12e6c608e80463c3753d275bc20fd89294d0415f04e670aeec3611ae3582ddc19e8fedd497e7d0bcbfaddab6a12823ec86e855b1e6a245e0a734eb43d + languageName: node + linkType: hard + "resolve-from@npm:^4.0.0": version: 4.0.0 resolution: "resolve-from@npm:4.0.0" @@ -13936,6 +13871,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:^7.7.3": + version: 7.7.3 + resolution: "semver@npm:7.7.3" + bin: + semver: bin/semver.js + checksum: 10c0/4afe5c986567db82f44c8c6faef8fe9df2a9b1d98098fc1721f57c696c4c21cebd572f297fc21002f81889492345b8470473bc6f4aff5fb032a6ea59ea2bc45e + languageName: node + linkType: hard + "set-blocking@npm:^2.0.0": version: 2.0.0 resolution: "set-blocking@npm:2.0.0" @@ -14204,6 +14148,16 @@ __metadata: languageName: node linkType: hard +"spdx-expression-parse@npm:^4.0.0": + version: 4.0.0 + resolution: "spdx-expression-parse@npm:4.0.0" + dependencies: + spdx-exceptions: "npm:^2.1.0" + spdx-license-ids: "npm:^3.0.0" + checksum: 10c0/965c487e77f4fb173f1c471f3eef4eb44b9f0321adc7f93d95e7620da31faa67d29356eb02523cd7df8a7fc1ec8238773cdbf9e45bd050329d2b26492771b736 + languageName: node + linkType: hard + "spdx-license-ids@npm:^3.0.0": version: 3.0.20 resolution: "spdx-license-ids@npm:3.0.20" @@ -14711,6 +14665,16 @@ __metadata: languageName: node linkType: hard +"to-valid-identifier@npm:^1.0.0": + version: 1.0.0 + resolution: "to-valid-identifier@npm:1.0.0" + dependencies: + "@sindresorhus/base62": "npm:^1.0.0" + reserved-identifiers: "npm:^1.0.0" + checksum: 10c0/569b49f43b5aaaa20677e67f0f1cdcff344855149934cfb80c793c7ac7c30e191b224bc81cab40fb57641af9ca73795c78053c164a2addc617671e2d22c13a4a + languageName: node + linkType: hard + "toggle-selection@npm:^1.0.6": version: 1.0.6 resolution: "toggle-selection@npm:1.0.6" @@ -14950,7 +14914,7 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^5.0.4, typescript@npm:^5.8.3": +"typescript@npm:^5.0.4": version: 5.9.3 resolution: "typescript@npm:5.9.3" bin: @@ -14960,7 +14924,17 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@npm%3A^5.0.4#optional!builtin, typescript@patch:typescript@npm%3A^5.8.3#optional!builtin": +"typescript@npm:^5.8.3": + version: 5.8.3 + resolution: "typescript@npm:5.8.3" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10c0/5f8bb01196e542e64d44db3d16ee0e4063ce4f3e3966df6005f2588e86d91c03e1fb131c2581baf0fb65ee79669eea6e161cd448178986587e9f6844446dbb48 + languageName: node + linkType: hard + +"typescript@patch:typescript@npm%3A^5.0.4#optional!builtin": version: 5.9.3 resolution: "typescript@patch:typescript@npm%3A5.9.3#optional!builtin::version=5.9.3&hash=5786d5" bin: @@ -14970,6 +14944,16 @@ __metadata: languageName: node linkType: hard +"typescript@patch:typescript@npm%3A^5.8.3#optional!builtin": + version: 5.8.3 + resolution: "typescript@patch:typescript@npm%3A5.8.3#optional!builtin::version=5.8.3&hash=5786d5" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10c0/39117e346ff8ebd87ae1510b3a77d5d92dae5a89bde588c747d25da5c146603a99c8ee588c7ef80faaf123d89ed46f6dbd918d534d641083177d5fac38b8a1cb + languageName: node + linkType: hard + "unbox-primitive@npm:^1.1.0": version: 1.1.0 resolution: "unbox-primitive@npm:1.1.0"