From 8bf651837139184c107582c5d74c135934455035 Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 24 Nov 2025 12:42:00 +0100 Subject: [PATCH] remove span processor endpoints --- src/analytics/PosthogSpanProcessor.ts | 157 ------------------------ src/analytics/RageshakeSpanProcessor.ts | 135 -------------------- 2 files changed, 292 deletions(-) delete mode 100644 src/analytics/PosthogSpanProcessor.ts delete mode 100644 src/analytics/RageshakeSpanProcessor.ts diff --git a/src/analytics/PosthogSpanProcessor.ts b/src/analytics/PosthogSpanProcessor.ts deleted file mode 100644 index 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 {} -}