Merge pull request #983 from robintown/rejoin-analytics
Track call rejoins
This commit is contained in:
commit
61a0534984
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…
Reference in a new issue