remove span processor endpoints
This commit is contained in:
@@ -1,157 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2023, 2024 New Vector Ltd.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
Please see LICENSE in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
type SpanProcessor,
|
|
||||||
type ReadableSpan,
|
|
||||||
type Span,
|
|
||||||
} from "@opentelemetry/sdk-trace-base";
|
|
||||||
import { hrTimeToMilliseconds } from "@opentelemetry/core";
|
|
||||||
import { logger } from "matrix-js-sdk/lib/logger";
|
|
||||||
|
|
||||||
import { PosthogAnalytics } from "./PosthogAnalytics";
|
|
||||||
|
|
||||||
interface PrevCall {
|
|
||||||
callId: string;
|
|
||||||
hangupTs: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The maximum time between hanging up and joining the same call that we would
|
|
||||||
* consider a 'rejoin' on the user's part.
|
|
||||||
*/
|
|
||||||
const maxRejoinMs = 2 * 60 * 1000; // 2 minutes
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Span processor that extracts certain metrics from spans to send to PostHog
|
|
||||||
*/
|
|
||||||
export class PosthogSpanProcessor implements SpanProcessor {
|
|
||||||
public async forceFlush(): Promise<void> {}
|
|
||||||
|
|
||||||
public onStart(span: Span): void {
|
|
||||||
// Hack: Yield to allow attributes to be set before processing
|
|
||||||
try {
|
|
||||||
switch (span.name) {
|
|
||||||
case "matrix.groupCallMembership":
|
|
||||||
this.onGroupCallMembershipStart(span);
|
|
||||||
return;
|
|
||||||
case "matrix.groupCallMembership.summaryReport":
|
|
||||||
this.onSummaryReportStart(span);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// log to avoid tripping @typescript-eslint/no-unused-vars
|
|
||||||
logger.debug(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public onEnd(span: ReadableSpan): void {
|
|
||||||
switch (span.name) {
|
|
||||||
case "matrix.groupCallMembership":
|
|
||||||
this.onGroupCallMembershipEnd(span);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private get prevCall(): PrevCall | null {
|
|
||||||
// This is stored in localStorage so we can remember the previous call
|
|
||||||
// across app restarts
|
|
||||||
const data = localStorage.getItem("matrix-prev-call");
|
|
||||||
if (data === null) return null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
return JSON.parse(data);
|
|
||||||
} catch (e) {
|
|
||||||
logger.warn("Invalid prev call data", data, "error:", e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private set prevCall(data: PrevCall | null) {
|
|
||||||
localStorage.setItem("matrix-prev-call", JSON.stringify(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
private onGroupCallMembershipStart(span: ReadableSpan): void {
|
|
||||||
const prevCall = this.prevCall;
|
|
||||||
const newCallId = span.attributes["matrix.confId"] as string;
|
|
||||||
|
|
||||||
// If the user joined the same call within a short time frame, log this as a
|
|
||||||
// rejoin. This is interesting as a call quality metric, since rejoins may
|
|
||||||
// indicate that users had to intervene to make the product work.
|
|
||||||
if (prevCall !== null && newCallId === prevCall.callId) {
|
|
||||||
const duration = hrTimeToMilliseconds(span.startTime) - prevCall.hangupTs;
|
|
||||||
if (duration <= maxRejoinMs) {
|
|
||||||
PosthogAnalytics.instance.trackEvent({
|
|
||||||
eventName: "Rejoin",
|
|
||||||
callId: prevCall.callId,
|
|
||||||
rejoinDuration: duration,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onGroupCallMembershipEnd(span: ReadableSpan): void {
|
|
||||||
this.prevCall = {
|
|
||||||
callId: span.attributes["matrix.confId"] as string,
|
|
||||||
hangupTs: hrTimeToMilliseconds(span.endTime),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private onSummaryReportStart(span: ReadableSpan): void {
|
|
||||||
// Searching for an event like this:
|
|
||||||
// matrix.stats.summary
|
|
||||||
// matrix.stats.summary.percentageReceivedAudioMedia: 0.75
|
|
||||||
// matrix.stats.summary.percentageReceivedMedia: 1
|
|
||||||
// matrix.stats.summary.percentageReceivedVideoMedia: 0.75
|
|
||||||
// matrix.stats.summary.maxJitter: 100
|
|
||||||
// matrix.stats.summary.maxPacketLoss: 20
|
|
||||||
const event = span.events.find((e) => e.name === "matrix.stats.summary");
|
|
||||||
if (event !== undefined) {
|
|
||||||
const attributes = event.attributes;
|
|
||||||
if (attributes) {
|
|
||||||
const mediaReceived = `${attributes["matrix.stats.summary.percentageReceivedMedia"]}`;
|
|
||||||
const videoReceived = `${attributes["matrix.stats.summary.percentageReceivedVideoMedia"]}`;
|
|
||||||
const audioReceived = `${attributes["matrix.stats.summary.percentageReceivedAudioMedia"]}`;
|
|
||||||
const maxJitter = `${attributes["matrix.stats.summary.maxJitter"]}`;
|
|
||||||
const maxPacketLoss = `${attributes["matrix.stats.summary.maxPacketLoss"]}`;
|
|
||||||
const peerConnections = `${attributes["matrix.stats.summary.peerConnections"]}`;
|
|
||||||
const percentageConcealedAudio = `${attributes["matrix.stats.summary.percentageConcealedAudio"]}`;
|
|
||||||
const opponentUsersInCall = `${attributes["matrix.stats.summary.opponentUsersInCall"]}`;
|
|
||||||
const opponentDevicesInCall = `${attributes["matrix.stats.summary.opponentDevicesInCall"]}`;
|
|
||||||
const diffDevicesToPeerConnections = `${attributes["matrix.stats.summary.diffDevicesToPeerConnections"]}`;
|
|
||||||
const ratioPeerConnectionToDevices = `${attributes["matrix.stats.summary.ratioPeerConnectionToDevices"]}`;
|
|
||||||
|
|
||||||
PosthogAnalytics.instance.trackEvent(
|
|
||||||
{
|
|
||||||
eventName: "MediaReceived",
|
|
||||||
callId: span.attributes["matrix.confId"] as string,
|
|
||||||
mediaReceived: mediaReceived,
|
|
||||||
audioReceived: audioReceived,
|
|
||||||
videoReceived: videoReceived,
|
|
||||||
maxJitter: maxJitter,
|
|
||||||
maxPacketLoss: maxPacketLoss,
|
|
||||||
peerConnections: peerConnections,
|
|
||||||
percentageConcealedAudio: percentageConcealedAudio,
|
|
||||||
opponentUsersInCall: opponentUsersInCall,
|
|
||||||
opponentDevicesInCall: opponentDevicesInCall,
|
|
||||||
diffDevicesToPeerConnections: diffDevicesToPeerConnections,
|
|
||||||
ratioPeerConnectionToDevices: ratioPeerConnectionToDevices,
|
|
||||||
},
|
|
||||||
// Send instantly because the window might be closing
|
|
||||||
{ send_instantly: true },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shutdown the processor.
|
|
||||||
*/
|
|
||||||
public async shutdown(): Promise<void> {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2023, 2024 New Vector Ltd.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
Please see LICENSE in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { type AttributeValue, type Attributes } from "@opentelemetry/api";
|
|
||||||
import { hrTimeToMicroseconds } from "@opentelemetry/core";
|
|
||||||
import {
|
|
||||||
type SpanProcessor,
|
|
||||||
type ReadableSpan,
|
|
||||||
type Span,
|
|
||||||
} from "@opentelemetry/sdk-trace-base";
|
|
||||||
|
|
||||||
const dumpAttributes = (
|
|
||||||
attr: Attributes,
|
|
||||||
): {
|
|
||||||
key: string;
|
|
||||||
type:
|
|
||||||
| "string"
|
|
||||||
| "number"
|
|
||||||
| "bigint"
|
|
||||||
| "boolean"
|
|
||||||
| "symbol"
|
|
||||||
| "undefined"
|
|
||||||
| "object"
|
|
||||||
| "function";
|
|
||||||
value: AttributeValue | undefined;
|
|
||||||
}[] =>
|
|
||||||
Object.entries(attr).map(([key, value]) => ({
|
|
||||||
key,
|
|
||||||
type: typeof value,
|
|
||||||
value,
|
|
||||||
}));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exports spans on demand to the Jaeger JSON format, which can be attached to
|
|
||||||
* rageshakes and loaded into analysis tools like Jaeger and Stalk.
|
|
||||||
*/
|
|
||||||
export class RageshakeSpanProcessor implements SpanProcessor {
|
|
||||||
private readonly spans: ReadableSpan[] = [];
|
|
||||||
|
|
||||||
public async forceFlush(): Promise<void> {}
|
|
||||||
|
|
||||||
public onStart(span: Span): void {
|
|
||||||
this.spans.push(span);
|
|
||||||
}
|
|
||||||
|
|
||||||
public onEnd(): void {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dumps the spans collected so far as Jaeger-compatible JSON.
|
|
||||||
*/
|
|
||||||
public dump(): string {
|
|
||||||
const now = Date.now() * 1000; // Jaeger works in microseconds
|
|
||||||
const traces = new Map<string, ReadableSpan[]>();
|
|
||||||
|
|
||||||
// Organize spans by their trace IDs
|
|
||||||
for (const span of this.spans) {
|
|
||||||
const traceId = span.spanContext().traceId;
|
|
||||||
let trace = traces.get(traceId);
|
|
||||||
|
|
||||||
if (trace === undefined) {
|
|
||||||
trace = [];
|
|
||||||
traces.set(traceId, trace);
|
|
||||||
}
|
|
||||||
|
|
||||||
trace.push(span);
|
|
||||||
}
|
|
||||||
|
|
||||||
const processId = "p1";
|
|
||||||
const processes = {
|
|
||||||
[processId]: {
|
|
||||||
serviceName: "element-call",
|
|
||||||
tags: [],
|
|
||||||
},
|
|
||||||
warnings: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
return JSON.stringify({
|
|
||||||
// Honestly not sure what some of these fields mean, I just know that
|
|
||||||
// they're present in Jaeger JSON exports
|
|
||||||
total: 0,
|
|
||||||
limit: 0,
|
|
||||||
offset: 0,
|
|
||||||
errors: null,
|
|
||||||
data: [...traces.entries()].map(([traceId, spans]) => ({
|
|
||||||
traceID: traceId,
|
|
||||||
warnings: null,
|
|
||||||
processes,
|
|
||||||
spans: spans.map((span) => {
|
|
||||||
const ctx = span.spanContext();
|
|
||||||
const startTime = hrTimeToMicroseconds(span.startTime);
|
|
||||||
// If the span has not yet ended, pretend that it ends now
|
|
||||||
const duration =
|
|
||||||
span.duration[0] === -1
|
|
||||||
? now - startTime
|
|
||||||
: hrTimeToMicroseconds(span.duration);
|
|
||||||
|
|
||||||
return {
|
|
||||||
traceID: traceId,
|
|
||||||
spanID: ctx.spanId,
|
|
||||||
operationName: span.name,
|
|
||||||
processID: processId,
|
|
||||||
warnings: null,
|
|
||||||
startTime,
|
|
||||||
duration,
|
|
||||||
references:
|
|
||||||
span.parentSpanContext?.spanId === undefined
|
|
||||||
? []
|
|
||||||
: [
|
|
||||||
{
|
|
||||||
refType: "CHILD_OF",
|
|
||||||
traceID: traceId,
|
|
||||||
spanID: span.parentSpanContext?.spanId,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
tags: dumpAttributes(span.attributes),
|
|
||||||
logs: span.events.map((event) => ({
|
|
||||||
timestamp: hrTimeToMicroseconds(event.time),
|
|
||||||
// The name of the event is in the "event" field, aparently.
|
|
||||||
fields: [
|
|
||||||
...dumpAttributes(event.attributes ?? {}),
|
|
||||||
{ key: "event", type: "string", value: event.name },
|
|
||||||
],
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async shutdown(): Promise<void> {}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user