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.
This commit is contained in:
parent
e870188be3
commit
a52251befa
2 changed files with 75 additions and 47 deletions
|
@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
console.log("POSTHOGEXPORTER shutdown of otelPosthogExporter");
|
||||
return new Promise<void>((resolve, _reject) => {
|
||||
resolve();
|
||||
});
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -385,22 +385,6 @@ export class PosthogAnalytics {
|
|||
this.capture(eventName, properties, options);
|
||||
}
|
||||
|
||||
public async trackFromSpan(
|
||||
{ eventName, ...properties },
|
||||
options?: CaptureOptions
|
||||
): Promise<void> {
|
||||
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 -
|
||||
|
|
Loading…
Add table
Reference in a new issue