Merge pull request #3586 from element-hq/toger5/otel-remove-experiment

Remove all `OpenTelementry` related code
This commit is contained in:
Timo
2026-01-05 14:55:28 +01:00
committed by GitHub
18 changed files with 8 additions and 2059 deletions

View File

@@ -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

View File

@@ -49,13 +49,6 @@
"@livekit/protocol": "^1.42.2",
"@livekit/track-processors": "^0.5.5",
"@mediapipe/tasks-vision": "^0.10.18",
"@opentelemetry/api": "^1.4.0",
"@opentelemetry/core": "^2.0.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.203.0",
"@opentelemetry/resources": "^2.0.0",
"@opentelemetry/sdk-trace-base": "^2.0.0",
"@opentelemetry/sdk-trace-web": "^2.0.0",
"@opentelemetry/semantic-conventions": "^1.25.1",
"@playwright/test": "^1.57.0",
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-slider": "^1.1.2",

View File

@@ -1,157 +0,0 @@
/*
Copyright 2023, 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import {
type SpanProcessor,
type ReadableSpan,
type Span,
} from "@opentelemetry/sdk-trace-base";
import { hrTimeToMilliseconds } from "@opentelemetry/core";
import { logger } from "matrix-js-sdk/lib/logger";
import { PosthogAnalytics } from "./PosthogAnalytics";
interface PrevCall {
callId: string;
hangupTs: number;
}
/**
* The maximum time between hanging up and joining the same call that we would
* consider a 'rejoin' on the user's part.
*/
const maxRejoinMs = 2 * 60 * 1000; // 2 minutes
/**
* Span processor that extracts certain metrics from spans to send to PostHog
*/
export class PosthogSpanProcessor implements SpanProcessor {
public async forceFlush(): Promise<void> {}
public onStart(span: Span): void {
// Hack: Yield to allow attributes to be set before processing
try {
switch (span.name) {
case "matrix.groupCallMembership":
this.onGroupCallMembershipStart(span);
return;
case "matrix.groupCallMembership.summaryReport":
this.onSummaryReportStart(span);
return;
}
} catch (e) {
// log to avoid tripping @typescript-eslint/no-unused-vars
logger.debug(e);
}
}
public onEnd(span: ReadableSpan): void {
switch (span.name) {
case "matrix.groupCallMembership":
this.onGroupCallMembershipEnd(span);
return;
}
}
private get prevCall(): PrevCall | null {
// This is stored in localStorage so we can remember the previous call
// across app restarts
const data = localStorage.getItem("matrix-prev-call");
if (data === null) return null;
try {
return JSON.parse(data);
} catch (e) {
logger.warn("Invalid prev call data", data, "error:", e);
return null;
}
}
private set prevCall(data: PrevCall | null) {
localStorage.setItem("matrix-prev-call", JSON.stringify(data));
}
private onGroupCallMembershipStart(span: ReadableSpan): void {
const prevCall = this.prevCall;
const newCallId = span.attributes["matrix.confId"] as string;
// If the user joined the same call within a short time frame, log this as a
// rejoin. This is interesting as a call quality metric, since rejoins may
// indicate that users had to intervene to make the product work.
if (prevCall !== null && newCallId === prevCall.callId) {
const duration = hrTimeToMilliseconds(span.startTime) - prevCall.hangupTs;
if (duration <= maxRejoinMs) {
PosthogAnalytics.instance.trackEvent({
eventName: "Rejoin",
callId: prevCall.callId,
rejoinDuration: duration,
});
}
}
}
private onGroupCallMembershipEnd(span: ReadableSpan): void {
this.prevCall = {
callId: span.attributes["matrix.confId"] as string,
hangupTs: hrTimeToMilliseconds(span.endTime),
};
}
private onSummaryReportStart(span: ReadableSpan): void {
// Searching for an event like this:
// matrix.stats.summary
// matrix.stats.summary.percentageReceivedAudioMedia: 0.75
// matrix.stats.summary.percentageReceivedMedia: 1
// matrix.stats.summary.percentageReceivedVideoMedia: 0.75
// matrix.stats.summary.maxJitter: 100
// matrix.stats.summary.maxPacketLoss: 20
const event = span.events.find((e) => e.name === "matrix.stats.summary");
if (event !== undefined) {
const attributes = event.attributes;
if (attributes) {
const mediaReceived = `${attributes["matrix.stats.summary.percentageReceivedMedia"]}`;
const videoReceived = `${attributes["matrix.stats.summary.percentageReceivedVideoMedia"]}`;
const audioReceived = `${attributes["matrix.stats.summary.percentageReceivedAudioMedia"]}`;
const maxJitter = `${attributes["matrix.stats.summary.maxJitter"]}`;
const maxPacketLoss = `${attributes["matrix.stats.summary.maxPacketLoss"]}`;
const peerConnections = `${attributes["matrix.stats.summary.peerConnections"]}`;
const percentageConcealedAudio = `${attributes["matrix.stats.summary.percentageConcealedAudio"]}`;
const opponentUsersInCall = `${attributes["matrix.stats.summary.opponentUsersInCall"]}`;
const opponentDevicesInCall = `${attributes["matrix.stats.summary.opponentDevicesInCall"]}`;
const diffDevicesToPeerConnections = `${attributes["matrix.stats.summary.diffDevicesToPeerConnections"]}`;
const ratioPeerConnectionToDevices = `${attributes["matrix.stats.summary.ratioPeerConnectionToDevices"]}`;
PosthogAnalytics.instance.trackEvent(
{
eventName: "MediaReceived",
callId: span.attributes["matrix.confId"] as string,
mediaReceived: mediaReceived,
audioReceived: audioReceived,
videoReceived: videoReceived,
maxJitter: maxJitter,
maxPacketLoss: maxPacketLoss,
peerConnections: peerConnections,
percentageConcealedAudio: percentageConcealedAudio,
opponentUsersInCall: opponentUsersInCall,
opponentDevicesInCall: opponentDevicesInCall,
diffDevicesToPeerConnections: diffDevicesToPeerConnections,
ratioPeerConnectionToDevices: ratioPeerConnectionToDevices,
},
// Send instantly because the window might be closing
{ send_instantly: true },
);
}
}
}
/**
* Shutdown the processor.
*/
public async shutdown(): Promise<void> {
return Promise.resolve();
}
}

View File

@@ -1,135 +0,0 @@
/*
Copyright 2023, 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { type AttributeValue, type Attributes } from "@opentelemetry/api";
import { hrTimeToMicroseconds } from "@opentelemetry/core";
import {
type SpanProcessor,
type ReadableSpan,
type Span,
} from "@opentelemetry/sdk-trace-base";
const dumpAttributes = (
attr: Attributes,
): {
key: string;
type:
| "string"
| "number"
| "bigint"
| "boolean"
| "symbol"
| "undefined"
| "object"
| "function";
value: AttributeValue | undefined;
}[] =>
Object.entries(attr).map(([key, value]) => ({
key,
type: typeof value,
value,
}));
/**
* Exports spans on demand to the Jaeger JSON format, which can be attached to
* rageshakes and loaded into analysis tools like Jaeger and Stalk.
*/
export class RageshakeSpanProcessor implements SpanProcessor {
private readonly spans: ReadableSpan[] = [];
public async forceFlush(): Promise<void> {}
public onStart(span: Span): void {
this.spans.push(span);
}
public onEnd(): void {}
/**
* Dumps the spans collected so far as Jaeger-compatible JSON.
*/
public dump(): string {
const now = Date.now() * 1000; // Jaeger works in microseconds
const traces = new Map<string, ReadableSpan[]>();
// Organize spans by their trace IDs
for (const span of this.spans) {
const traceId = span.spanContext().traceId;
let trace = traces.get(traceId);
if (trace === undefined) {
trace = [];
traces.set(traceId, trace);
}
trace.push(span);
}
const processId = "p1";
const processes = {
[processId]: {
serviceName: "element-call",
tags: [],
},
warnings: null,
};
return JSON.stringify({
// Honestly not sure what some of these fields mean, I just know that
// they're present in Jaeger JSON exports
total: 0,
limit: 0,
offset: 0,
errors: null,
data: [...traces.entries()].map(([traceId, spans]) => ({
traceID: traceId,
warnings: null,
processes,
spans: spans.map((span) => {
const ctx = span.spanContext();
const startTime = hrTimeToMicroseconds(span.startTime);
// If the span has not yet ended, pretend that it ends now
const duration =
span.duration[0] === -1
? now - startTime
: hrTimeToMicroseconds(span.duration);
return {
traceID: traceId,
spanID: ctx.spanId,
operationName: span.name,
processID: processId,
warnings: null,
startTime,
duration,
references:
span.parentSpanContext?.spanId === undefined
? []
: [
{
refType: "CHILD_OF",
traceID: traceId,
spanID: span.parentSpanContext?.spanId,
},
],
tags: dumpAttributes(span.attributes),
logs: span.events.map((event) => ({
timestamp: hrTimeToMicroseconds(event.time),
// The name of the event is in the "event" field, aparently.
fields: [
...dumpAttributes(event.attributes ?? {}),
{ key: "event", type: "string", value: event.name },
],
})),
};
}),
})),
});
}
public async shutdown(): Promise<void> {}
}

View File

@@ -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();

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

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

View File

@@ -1,266 +0,0 @@
/*
Copyright 2023, 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { type GroupCallStatsReport } from "matrix-js-sdk/lib/webrtc/groupCall";
import {
type AudioConcealment,
type ByteSentStatsReport,
type ConnectionStatsReport,
} from "matrix-js-sdk/lib/webrtc/stats/statsReport";
import { describe, expect, it } from "vitest";
import { ObjectFlattener } from "../../src/otel/ObjectFlattener";
describe("ObjectFlattener", () => {
const noConcealment: AudioConcealment = {
concealedAudio: 0,
totalAudioDuration: 0,
};
const statsReport: GroupCallStatsReport<ConnectionStatsReport> = {
report: {
callId: "callId",
opponentMemberId: "opponentMemberId",
bandwidth: { upload: 426, download: 0 },
bitrate: {
upload: 426,
download: 0,
audio: {
upload: 124,
download: 0,
},
video: {
upload: 302,
download: 0,
},
},
packetLoss: {
total: 0,
download: 0,
upload: 0,
},
framerate: {
local: new Map([
["LOCAL_AUDIO_TRACK_ID", 0],
["LOCAL_VIDEO_TRACK_ID", 30],
]),
remote: new Map([
["REMOTE_AUDIO_TRACK_ID", 0],
["REMOTE_VIDEO_TRACK_ID", 60],
]),
},
resolution: {
local: new Map([
["LOCAL_AUDIO_TRACK_ID", { height: -1, width: -1 }],
["LOCAL_VIDEO_TRACK_ID", { height: 460, width: 780 }],
]),
remote: new Map([
["REMOTE_AUDIO_TRACK_ID", { height: -1, width: -1 }],
["REMOTE_VIDEO_TRACK_ID", { height: 960, width: 1080 }],
]),
},
jitter: new Map([
["REMOTE_AUDIO_TRACK_ID", 2],
["REMOTE_VIDEO_TRACK_ID", 50],
]),
codec: {
local: new Map([
["LOCAL_AUDIO_TRACK_ID", "opus"],
["LOCAL_VIDEO_TRACK_ID", "v8"],
]),
remote: new Map([
["REMOTE_AUDIO_TRACK_ID", "opus"],
["REMOTE_VIDEO_TRACK_ID", "v9"],
]),
},
transport: [
{
ip: "ff11::5fa:abcd:999c:c5c5:50000",
type: "udp",
localIp: "2aaa:9999:2aaa:999:8888:2aaa:2aaa:7777:50000",
isFocus: true,
localCandidateType: "host",
remoteCandidateType: "host",
networkType: "ethernet",
rtt: NaN,
},
{
ip: "10.10.10.2:22222",
type: "tcp",
localIp: "10.10.10.100:33333",
isFocus: true,
localCandidateType: "srfx",
remoteCandidateType: "srfx",
networkType: "ethernet",
rtt: 0,
},
],
audioConcealment: new Map([
["REMOTE_AUDIO_TRACK_ID", noConcealment],
["REMOTE_VIDEO_TRACK_ID", noConcealment],
]),
totalAudioConcealment: noConcealment,
},
};
describe("on flattenObjectRecursive", () => {
it("should flatter an Map object", () => {
const flatObject = {};
ObjectFlattener.flattenObjectRecursive(
statsReport.report.resolution,
flatObject,
"matrix.call.stats.connection.resolution.",
0,
);
expect(flatObject).toEqual({
"matrix.call.stats.connection.resolution.local.LOCAL_AUDIO_TRACK_ID.height":
-1,
"matrix.call.stats.connection.resolution.local.LOCAL_AUDIO_TRACK_ID.width":
-1,
"matrix.call.stats.connection.resolution.local.LOCAL_VIDEO_TRACK_ID.height": 460,
"matrix.call.stats.connection.resolution.local.LOCAL_VIDEO_TRACK_ID.width": 780,
"matrix.call.stats.connection.resolution.remote.REMOTE_AUDIO_TRACK_ID.height":
-1,
"matrix.call.stats.connection.resolution.remote.REMOTE_AUDIO_TRACK_ID.width":
-1,
"matrix.call.stats.connection.resolution.remote.REMOTE_VIDEO_TRACK_ID.height": 960,
"matrix.call.stats.connection.resolution.remote.REMOTE_VIDEO_TRACK_ID.width": 1080,
});
});
it("should flatter an Array object", () => {
const flatObject = {};
ObjectFlattener.flattenObjectRecursive(
statsReport.report.transport,
flatObject,
"matrix.call.stats.connection.transport.",
0,
);
expect(flatObject).toEqual({
"matrix.call.stats.connection.transport.0.ip":
"ff11::5fa:abcd:999c:c5c5:50000",
"matrix.call.stats.connection.transport.0.type": "udp",
"matrix.call.stats.connection.transport.0.localIp":
"2aaa:9999:2aaa:999:8888:2aaa:2aaa:7777:50000",
"matrix.call.stats.connection.transport.0.isFocus": true,
"matrix.call.stats.connection.transport.0.localCandidateType": "host",
"matrix.call.stats.connection.transport.0.remoteCandidateType": "host",
"matrix.call.stats.connection.transport.0.networkType": "ethernet",
"matrix.call.stats.connection.transport.0.rtt": "NaN",
"matrix.call.stats.connection.transport.1.ip": "10.10.10.2:22222",
"matrix.call.stats.connection.transport.1.type": "tcp",
"matrix.call.stats.connection.transport.1.localIp":
"10.10.10.100:33333",
"matrix.call.stats.connection.transport.1.isFocus": true,
"matrix.call.stats.connection.transport.1.localCandidateType": "srfx",
"matrix.call.stats.connection.transport.1.remoteCandidateType": "srfx",
"matrix.call.stats.connection.transport.1.networkType": "ethernet",
"matrix.call.stats.connection.transport.1.rtt": 0,
});
});
});
describe("on flattenReportObject Connection Stats", () => {
it("should flatten a Report to otel Attributes Object", () => {
expect(
ObjectFlattener.flattenReportObject(
"matrix.call.stats.connection",
statsReport.report,
),
).toEqual({
"matrix.call.stats.connection.callId": "callId",
"matrix.call.stats.connection.opponentMemberId": "opponentMemberId",
"matrix.call.stats.connection.bandwidth.download": 0,
"matrix.call.stats.connection.bandwidth.upload": 426,
"matrix.call.stats.connection.bitrate.audio.download": 0,
"matrix.call.stats.connection.bitrate.audio.upload": 124,
"matrix.call.stats.connection.bitrate.download": 0,
"matrix.call.stats.connection.bitrate.upload": 426,
"matrix.call.stats.connection.bitrate.video.download": 0,
"matrix.call.stats.connection.bitrate.video.upload": 302,
"matrix.call.stats.connection.codec.local.LOCAL_AUDIO_TRACK_ID": "opus",
"matrix.call.stats.connection.codec.local.LOCAL_VIDEO_TRACK_ID": "v8",
"matrix.call.stats.connection.codec.remote.REMOTE_AUDIO_TRACK_ID":
"opus",
"matrix.call.stats.connection.codec.remote.REMOTE_VIDEO_TRACK_ID": "v9",
"matrix.call.stats.connection.framerate.local.LOCAL_AUDIO_TRACK_ID": 0,
"matrix.call.stats.connection.framerate.local.LOCAL_VIDEO_TRACK_ID": 30,
"matrix.call.stats.connection.framerate.remote.REMOTE_AUDIO_TRACK_ID": 0,
"matrix.call.stats.connection.framerate.remote.REMOTE_VIDEO_TRACK_ID": 60,
"matrix.call.stats.connection.jitter.REMOTE_AUDIO_TRACK_ID": 2,
"matrix.call.stats.connection.jitter.REMOTE_VIDEO_TRACK_ID": 50,
"matrix.call.stats.connection.packetLoss.download": 0,
"matrix.call.stats.connection.packetLoss.total": 0,
"matrix.call.stats.connection.packetLoss.upload": 0,
"matrix.call.stats.connection.resolution.local.LOCAL_AUDIO_TRACK_ID.height":
-1,
"matrix.call.stats.connection.resolution.local.LOCAL_AUDIO_TRACK_ID.width":
-1,
"matrix.call.stats.connection.resolution.local.LOCAL_VIDEO_TRACK_ID.height": 460,
"matrix.call.stats.connection.resolution.local.LOCAL_VIDEO_TRACK_ID.width": 780,
"matrix.call.stats.connection.resolution.remote.REMOTE_AUDIO_TRACK_ID.height":
-1,
"matrix.call.stats.connection.resolution.remote.REMOTE_AUDIO_TRACK_ID.width":
-1,
"matrix.call.stats.connection.resolution.remote.REMOTE_VIDEO_TRACK_ID.height": 960,
"matrix.call.stats.connection.resolution.remote.REMOTE_VIDEO_TRACK_ID.width": 1080,
"matrix.call.stats.connection.transport.0.ip":
"ff11::5fa:abcd:999c:c5c5:50000",
"matrix.call.stats.connection.transport.0.type": "udp",
"matrix.call.stats.connection.transport.0.localIp":
"2aaa:9999:2aaa:999:8888:2aaa:2aaa:7777:50000",
"matrix.call.stats.connection.transport.0.isFocus": true,
"matrix.call.stats.connection.transport.0.localCandidateType": "host",
"matrix.call.stats.connection.transport.0.remoteCandidateType": "host",
"matrix.call.stats.connection.transport.0.networkType": "ethernet",
"matrix.call.stats.connection.transport.0.rtt": "NaN",
"matrix.call.stats.connection.transport.1.ip": "10.10.10.2:22222",
"matrix.call.stats.connection.transport.1.type": "tcp",
"matrix.call.stats.connection.transport.1.localIp":
"10.10.10.100:33333",
"matrix.call.stats.connection.transport.1.isFocus": true,
"matrix.call.stats.connection.transport.1.localCandidateType": "srfx",
"matrix.call.stats.connection.transport.1.remoteCandidateType": "srfx",
"matrix.call.stats.connection.transport.1.networkType": "ethernet",
"matrix.call.stats.connection.transport.1.rtt": 0,
"matrix.call.stats.connection.audioConcealment.REMOTE_AUDIO_TRACK_ID.concealedAudio": 0,
"matrix.call.stats.connection.audioConcealment.REMOTE_AUDIO_TRACK_ID.totalAudioDuration": 0,
"matrix.call.stats.connection.audioConcealment.REMOTE_VIDEO_TRACK_ID.concealedAudio": 0,
"matrix.call.stats.connection.audioConcealment.REMOTE_VIDEO_TRACK_ID.totalAudioDuration": 0,
"matrix.call.stats.connection.totalAudioConcealment.concealedAudio": 0,
"matrix.call.stats.connection.totalAudioConcealment.totalAudioDuration": 0,
});
});
});
describe("on flattenByteSendStatsReportObject", () => {
const byteSentStatsReport = new Map<
string,
number
>() as ByteSentStatsReport;
byteSentStatsReport.callId = "callId";
byteSentStatsReport.opponentMemberId = "opponentMemberId";
byteSentStatsReport.set("4aa92608-04c6-428e-8312-93e17602a959", 132093);
byteSentStatsReport.set("a08e4237-ee30-4015-a932-b676aec894b1", 913448);
it("should flatten a Report to otel Attributes Object", () => {
expect(
ObjectFlattener.flattenReportObject(
"matrix.call.stats.bytesSend",
byteSentStatsReport,
),
).toEqual({
"matrix.call.stats.bytesSend.4aa92608-04c6-428e-8312-93e17602a959": 132093,
"matrix.call.stats.bytesSend.a08e4237-ee30-4015-a932-b676aec894b1": 913448,
});
expect(byteSentStatsReport.callId).toEqual("callId");
expect(byteSentStatsReport.opponentMemberId).toEqual("opponentMemberId");
});
});
});

View File

@@ -1,100 +0,0 @@
/*
Copyright 2023, 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { type Attributes } from "@opentelemetry/api";
import { type VoipEvent } from "matrix-js-sdk/lib/webrtc/call";
import { type GroupCallStatsReport } from "matrix-js-sdk/lib/webrtc/groupCall";
import {
type ByteSentStatsReport,
type ConnectionStatsReport,
type SummaryStatsReport,
} from "matrix-js-sdk/lib/webrtc/stats/statsReport";
export class ObjectFlattener {
public static flattenReportObject(
prefix: string,
report: ConnectionStatsReport | ByteSentStatsReport,
): Attributes {
const flatObject = {};
ObjectFlattener.flattenObjectRecursive(report, flatObject, `${prefix}.`, 0);
return flatObject;
}
public static flattenByteSentStatsReportObject(
statsReport: GroupCallStatsReport<ByteSentStatsReport>,
): Attributes {
const flatObject = {};
ObjectFlattener.flattenObjectRecursive(
statsReport.report,
flatObject,
"matrix.stats.bytesSent.",
0,
);
return flatObject;
}
public static flattenSummaryStatsReportObject(
statsReport: GroupCallStatsReport<SummaryStatsReport>,
): Attributes {
const flatObject = {};
ObjectFlattener.flattenObjectRecursive(
statsReport.report,
flatObject,
"matrix.stats.summary.",
0,
);
return flatObject;
}
/* Flattens out an object into a single layer with components
* of the key separated by dots
*/
public static flattenVoipEvent(event: VoipEvent): Attributes {
const flatObject = {};
ObjectFlattener.flattenObjectRecursive(
event as unknown as Record<string, unknown>, // XXX Types
flatObject,
"matrix.event.",
0,
);
return flatObject;
}
public static flattenObjectRecursive(
obj: object,
flatObject: Attributes,
prefix: string,
depth: number,
): void {
if (depth > 10)
throw new Error(
"Depth limit exceeded: aborting VoipEvent recursion. Prefix is " +
prefix,
);
let entries;
if (obj instanceof Map) {
entries = obj.entries();
} else {
entries = Object.entries(obj);
}
for (const [k, v] of entries) {
if (["string", "number", "boolean"].includes(typeof v) || v === null) {
let value;
value = v === null ? "null" : v;
value = typeof v === "number" && Number.isNaN(v) ? "NaN" : value;
flatObject[prefix + k] = value;
} else if (typeof v === "object") {
ObjectFlattener.flattenObjectRecursive(
v,
flatObject,
prefix + k + ".",
depth + 1,
);
}
}
}
}

View File

@@ -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);
});
});
});

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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) {

249
yarn.lock
View File

@@ -3042,146 +3042,6 @@ __metadata:
languageName: node
linkType: hard
"@opentelemetry/api-logs@npm:0.203.0":
version: 0.203.0
resolution: "@opentelemetry/api-logs@npm:0.203.0"
dependencies:
"@opentelemetry/api": "npm:^1.3.0"
checksum: 10c0/e7a0a0ff46aaeb62192a99f45ef4889222e4fea09be25cab6fea811afc2df95c02ea050b2c98dfc0fc5a6ec6a623d87096af2751fdf91ddbb3afcab61b5325da
languageName: node
linkType: hard
"@opentelemetry/api@npm:^1.3.0, @opentelemetry/api@npm:^1.4.0":
version: 1.9.0
resolution: "@opentelemetry/api@npm:1.9.0"
checksum: 10c0/9aae2fe6e8a3a3eeb6c1fdef78e1939cf05a0f37f8a4fae4d6bf2e09eb1e06f966ece85805626e01ba5fab48072b94f19b835449e58b6d26720ee19a58298add
languageName: node
linkType: hard
"@opentelemetry/core@npm:2.0.1, @opentelemetry/core@npm:^2.0.0":
version: 2.0.1
resolution: "@opentelemetry/core@npm:2.0.1"
dependencies:
"@opentelemetry/semantic-conventions": "npm:^1.29.0"
peerDependencies:
"@opentelemetry/api": ">=1.0.0 <1.10.0"
checksum: 10c0/d587b1289559757d80da98039f9f57612f84f72ec608cd665dc467c7c6c5ce3a987dfcc2c63b521c7c86ce984a2552b3ead15a0dc458de1cf6bde5cdfe4ca9d8
languageName: node
linkType: hard
"@opentelemetry/exporter-trace-otlp-http@npm:^0.203.0":
version: 0.203.0
resolution: "@opentelemetry/exporter-trace-otlp-http@npm:0.203.0"
dependencies:
"@opentelemetry/core": "npm:2.0.1"
"@opentelemetry/otlp-exporter-base": "npm:0.203.0"
"@opentelemetry/otlp-transformer": "npm:0.203.0"
"@opentelemetry/resources": "npm:2.0.1"
"@opentelemetry/sdk-trace-base": "npm:2.0.1"
peerDependencies:
"@opentelemetry/api": ^1.3.0
checksum: 10c0/21a65ebc40dcab05cf11178e5037f96847ce344c4a855aac46dcab3f74982016318ee75fafdfeeb42f10b92a0a781b7cd8b2b5b036cbe53c14714fd13940142e
languageName: node
linkType: hard
"@opentelemetry/otlp-exporter-base@npm:0.203.0":
version: 0.203.0
resolution: "@opentelemetry/otlp-exporter-base@npm:0.203.0"
dependencies:
"@opentelemetry/core": "npm:2.0.1"
"@opentelemetry/otlp-transformer": "npm:0.203.0"
peerDependencies:
"@opentelemetry/api": ^1.3.0
checksum: 10c0/ad5b771b06b192f06f332f60701d1ad208df88a05975b16e1cdd1dff8e1cb66e775b3e9de513c2f5d48f390f25ca35411ead08ce4849c8203b86a264d34561d3
languageName: node
linkType: hard
"@opentelemetry/otlp-transformer@npm:0.203.0":
version: 0.203.0
resolution: "@opentelemetry/otlp-transformer@npm:0.203.0"
dependencies:
"@opentelemetry/api-logs": "npm:0.203.0"
"@opentelemetry/core": "npm:2.0.1"
"@opentelemetry/resources": "npm:2.0.1"
"@opentelemetry/sdk-logs": "npm:0.203.0"
"@opentelemetry/sdk-metrics": "npm:2.0.1"
"@opentelemetry/sdk-trace-base": "npm:2.0.1"
protobufjs: "npm:^7.3.0"
peerDependencies:
"@opentelemetry/api": ^1.3.0
checksum: 10c0/3f7b4bfe4bcab4db434ff2c4e59b53de53642d379b80056610456d8e9ae0cbab0f8b69f088078637b7b5ceffd0ac2fda68469c5f295b1c0ac625f522f640338c
languageName: node
linkType: hard
"@opentelemetry/resources@npm:2.0.1, @opentelemetry/resources@npm:^2.0.0":
version: 2.0.1
resolution: "@opentelemetry/resources@npm:2.0.1"
dependencies:
"@opentelemetry/core": "npm:2.0.1"
"@opentelemetry/semantic-conventions": "npm:^1.29.0"
peerDependencies:
"@opentelemetry/api": ">=1.3.0 <1.10.0"
checksum: 10c0/96532b7553b26607a7a892d72f6b03ad12bd542dc23c95135a8ae40362da9c883c21a4cff3d2296d9e0e9bd899a5977e325ed52d83142621a8ffe81d08d99341
languageName: node
linkType: hard
"@opentelemetry/sdk-logs@npm:0.203.0":
version: 0.203.0
resolution: "@opentelemetry/sdk-logs@npm:0.203.0"
dependencies:
"@opentelemetry/api-logs": "npm:0.203.0"
"@opentelemetry/core": "npm:2.0.1"
"@opentelemetry/resources": "npm:2.0.1"
peerDependencies:
"@opentelemetry/api": ">=1.4.0 <1.10.0"
checksum: 10c0/02dd9d9969628f05f71ae1d149f1aa6d1fee2dad607923a68a1cfc923e94b046dcc0e18e85e865324e3bda0cee7a5a0ba9fa0d57e4e95fa672be103e2ce60270
languageName: node
linkType: hard
"@opentelemetry/sdk-metrics@npm:2.0.1":
version: 2.0.1
resolution: "@opentelemetry/sdk-metrics@npm:2.0.1"
dependencies:
"@opentelemetry/core": "npm:2.0.1"
"@opentelemetry/resources": "npm:2.0.1"
peerDependencies:
"@opentelemetry/api": ">=1.9.0 <1.10.0"
checksum: 10c0/fcf7ae23d459e5da7cb6fe150064b6dc4e11e47925b08980c3b357bd5534ad388898bbacd0ff8befef6801f43b35142dc7123f028ffde2d0fe2bd72177d07639
languageName: node
linkType: hard
"@opentelemetry/sdk-trace-base@npm:2.0.1, @opentelemetry/sdk-trace-base@npm:^2.0.0":
version: 2.0.1
resolution: "@opentelemetry/sdk-trace-base@npm:2.0.1"
dependencies:
"@opentelemetry/core": "npm:2.0.1"
"@opentelemetry/resources": "npm:2.0.1"
"@opentelemetry/semantic-conventions": "npm:^1.29.0"
peerDependencies:
"@opentelemetry/api": ">=1.3.0 <1.10.0"
checksum: 10c0/4e3c733296012b758d007e9c0d8a5b175edbe9a680c73ec75303476e7982b73ad4209f1a2791c1a94c428e5a53eba6c2a72faa430c70336005aa58744d6cb37b
languageName: node
linkType: hard
"@opentelemetry/sdk-trace-web@npm:^2.0.0":
version: 2.0.1
resolution: "@opentelemetry/sdk-trace-web@npm:2.0.1"
dependencies:
"@opentelemetry/core": "npm:2.0.1"
"@opentelemetry/sdk-trace-base": "npm:2.0.1"
peerDependencies:
"@opentelemetry/api": ">=1.0.0 <1.10.0"
checksum: 10c0/48821b91430e24378b0b5b2632e78efdd018a3f840462a6aeba6ce318a6480bad2f623cc7f7f625a9266028ad44b78eb8456181778de6cb18725f26c44e2729b
languageName: node
linkType: hard
"@opentelemetry/semantic-conventions@npm:^1.25.1, @opentelemetry/semantic-conventions@npm:^1.29.0":
version: 1.36.0
resolution: "@opentelemetry/semantic-conventions@npm:1.36.0"
checksum: 10c0/edc8a6fe3ec4fc0c67ba3a92b86fb3dcc78fe1eb4f19838d8013c3232b9868540a034dd25cfe0afdd5eae752c5f0e9f42272ff46da144a2d5b35c644478e1c62
languageName: node
linkType: hard
"@oxc-resolver/binding-darwin-arm64@npm:11.3.0":
version: 11.3.0
resolution: "@oxc-resolver/binding-darwin-arm64@npm:11.3.0"
@@ -3437,79 +3297,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"
@@ -5332,7 +5119,7 @@ __metadata:
languageName: node
linkType: hard
"@types/node@npm:*, @types/node@npm:>=13.7.0":
"@types/node@npm:*":
version: 24.1.0
resolution: "@types/node@npm:24.1.0"
dependencies:
@@ -7874,13 +7661,6 @@ __metadata:
"@livekit/protocol": "npm:^1.42.2"
"@livekit/track-processors": "npm:^0.5.5"
"@mediapipe/tasks-vision": "npm:^0.10.18"
"@opentelemetry/api": "npm:^1.4.0"
"@opentelemetry/core": "npm:^2.0.0"
"@opentelemetry/exporter-trace-otlp-http": "npm:^0.203.0"
"@opentelemetry/resources": "npm:^2.0.0"
"@opentelemetry/sdk-trace-base": "npm:^2.0.0"
"@opentelemetry/sdk-trace-web": "npm:^2.0.0"
"@opentelemetry/semantic-conventions": "npm:^1.25.1"
"@playwright/test": "npm:^1.57.0"
"@radix-ui/react-dialog": "npm:^1.0.4"
"@radix-ui/react-slider": "npm:^1.1.2"
@@ -10762,13 +10542,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"
@@ -12460,26 +12233,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"