From a52251befab5db50bd4c2373406c301a27e5d265 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 3 Apr 2023 20:57:03 -0400 Subject: [PATCH] Track call rejoins Call rejoins will be one of the KPIs we track in PostHog to measure call quality. I've also reverted the previous behavior which logged all OpenTelemetry spans to PostHog, since we should only be sending small, anonymized bits of data there. --- src/analytics/OtelPosthogExporter.ts | 106 +++++++++++++++++++-------- src/analytics/PosthogAnalytics.ts | 16 ---- 2 files changed, 75 insertions(+), 47 deletions(-) diff --git a/src/analytics/OtelPosthogExporter.ts b/src/analytics/OtelPosthogExporter.ts index 8f9ba26b..c624a007 100644 --- a/src/analytics/OtelPosthogExporter.ts +++ b/src/analytics/OtelPosthogExporter.ts @@ -16,12 +16,29 @@ limitations under the License. import { SpanExporter, ReadableSpan } from "@opentelemetry/sdk-trace-base"; import { ExportResult, ExportResultCode } from "@opentelemetry/core"; +import { logger } from "matrix-js-sdk/src/logger"; +import { HrTime } from "@opentelemetry/api"; import { PosthogAnalytics } from "./PosthogAnalytics"; +interface PrevCall { + callId: string; + hangupTs: number; +} + +function hrTimeToMs(time: HrTime): number { + return time[0] * 1000 + time[1] * 0.000001; +} + /** - * This is implementation of {@link SpanExporter} that sends spans - * to Posthog + * 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 + +/** + * This is implementation of {@link SpanExporter} that extracts certain metrics + * from spans to send to PostHog */ export class PosthogSpanExporter implements SpanExporter { /** @@ -33,41 +50,68 @@ export class PosthogSpanExporter implements SpanExporter { spans: ReadableSpan[], resultCallback: (result: ExportResult) => void ): Promise { - console.log("POSTHOGEXPORTER", spans); - for (const span of spans) { - const sendInstantly = [ - "otel_callEnded", - "otel_otherSentInstantlyEventName", - ].includes(span.name); - - for (const spanEvent of span.events) { - await PosthogAnalytics.instance.trackFromSpan( - { - eventName: spanEvent.name, - ...spanEvent.attributes, - }, - { - send_instantly: sendInstantly, - } - ); - } - - await PosthogAnalytics.instance.trackFromSpan( - { eventName: span.name, ...span.attributes }, - { - send_instantly: sendInstantly, + await Promise.all( + spans.map((span) => { + switch (span.name) { + case "matrix.groupCallMembership": + return this.exportGroupCallMembershipSpan(span); + // TBD if there are other spans that we want to process for export to + // PostHog } - ); - resultCallback({ code: ExportResultCode.SUCCESS }); + }) + ); + + resultCallback({ code: ExportResultCode.SUCCESS }); + } + + 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); + return null; } } + + private set prevCall(data: PrevCall | null) { + localStorage.setItem("matrix-prev-call", JSON.stringify(data)); + } + + async exportGroupCallMembershipSpan(span: ReadableSpan): Promise { + const prevCall = this.prevCall; + const newPrevCall = (this.prevCall = { + callId: span.attributes["matrix.confId"] as string, + hangupTs: hrTimeToMs(span.endTime), + }); + + // 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 && newPrevCall.callId === prevCall.callId) { + const duration = hrTimeToMs(span.startTime) - prevCall.hangupTs; + if (duration <= maxRejoinMs) { + PosthogAnalytics.instance.trackEvent( + { + eventName: "Rejoin", + callId: prevCall.callId, + rejoinDuration: duration, + }, + // Send instantly because the window might be closing + { send_instantly: true } + ); + } + } + } + /** * Shutdown the exporter. */ shutdown(): Promise { - console.log("POSTHOGEXPORTER shutdown of otelPosthogExporter"); - return new Promise((resolve, _reject) => { - resolve(); - }); + return Promise.resolve(); } } diff --git a/src/analytics/PosthogAnalytics.ts b/src/analytics/PosthogAnalytics.ts index 718a49c2..e2e8fdae 100644 --- a/src/analytics/PosthogAnalytics.ts +++ b/src/analytics/PosthogAnalytics.ts @@ -385,22 +385,6 @@ export class PosthogAnalytics { this.capture(eventName, properties, options); } - public async trackFromSpan( - { eventName, ...properties }, - options?: CaptureOptions - ): Promise { - if (this.identificationPromise) { - // only make calls to posthog after the identificaion is done - await this.identificationPromise; - } - if ( - this.anonymity == Anonymity.Disabled || - this.anonymity == Anonymity.Anonymous - ) - return; - this.capture(eventName, properties, options); - } - public startListeningToSettingsChanges(): void { // Listen to account data changes from sync so we can observe changes to relevant flags and update. // This is called -