Merge pull request #3852 from element-hq/toger5/posthog-call-ended-duration-fix
Only send posthog callEnded Event if the user has joined the call.
This commit is contained in:
@@ -0,0 +1,162 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 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 {
|
||||||
|
expect,
|
||||||
|
describe,
|
||||||
|
it,
|
||||||
|
vi,
|
||||||
|
beforeEach,
|
||||||
|
beforeAll,
|
||||||
|
afterAll,
|
||||||
|
} from "vitest";
|
||||||
|
import { logger } from "matrix-js-sdk/lib/logger";
|
||||||
|
import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc";
|
||||||
|
|
||||||
|
import { PosthogAnalytics } from "./PosthogAnalytics";
|
||||||
|
import { CallEndedTracker } from "./PosthogEvents";
|
||||||
|
import { mockConfig } from "../utils/test";
|
||||||
|
|
||||||
|
const defaultCounters = {
|
||||||
|
roomEventEncryptionKeysSent: 10,
|
||||||
|
roomEventEncryptionKeysReceived: 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultTotals = {
|
||||||
|
roomEventEncryptionKeysReceivedTotalAge: 500,
|
||||||
|
};
|
||||||
|
|
||||||
|
function createMockRtcSession(overrides?: {
|
||||||
|
counters?: Partial<typeof defaultCounters>;
|
||||||
|
totals?: Partial<typeof defaultTotals>;
|
||||||
|
}): MatrixRTCSession {
|
||||||
|
return {
|
||||||
|
statistics: {
|
||||||
|
counters: { ...defaultCounters, ...overrides?.counters },
|
||||||
|
totals: { ...defaultTotals, ...overrides?.totals },
|
||||||
|
},
|
||||||
|
} as unknown as MatrixRTCSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("CallEnded", () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
mockConfig();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
vi.spyOn(PosthogAnalytics.instance, "trackEvent").mockImplementation(
|
||||||
|
() => {},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
PosthogAnalytics.resetInstance();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("warns if startTime is missing when track is called", () => {
|
||||||
|
const warnSpy = vi.spyOn(logger, "warn");
|
||||||
|
const tracker = new CallEndedTracker();
|
||||||
|
const mockSession = createMockRtcSession();
|
||||||
|
|
||||||
|
tracker.track("test-call-id", 2, false, mockSession);
|
||||||
|
|
||||||
|
expect(warnSpy).toHaveBeenCalledWith(
|
||||||
|
"[PosthogEvents] Failed to send posthog callEnded event due to missing startTime",
|
||||||
|
);
|
||||||
|
expect(PosthogAnalytics.instance.trackEvent).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("tracks event with correct properties when startTime is set", () => {
|
||||||
|
const tracker = new CallEndedTracker();
|
||||||
|
const mockSession = createMockRtcSession();
|
||||||
|
|
||||||
|
tracker.cacheStartCall(new Date(Date.now() - 60000));
|
||||||
|
tracker.cacheParticipantCountChanged(5);
|
||||||
|
tracker.track("test-call-id", 3, true, mockSession);
|
||||||
|
|
||||||
|
expect(PosthogAnalytics.instance.trackEvent).toHaveBeenCalledWith(
|
||||||
|
{
|
||||||
|
eventName: "CallEnded",
|
||||||
|
callId: "test-call-id",
|
||||||
|
callParticipantsMax: 5,
|
||||||
|
callParticipantsOnLeave: 3,
|
||||||
|
callDuration: expect.closeTo(60, 1),
|
||||||
|
roomEventEncryptionKeysSent: 10,
|
||||||
|
roomEventEncryptionKeysReceived: 5,
|
||||||
|
roomEventEncryptionKeysReceivedAverageAge: 100,
|
||||||
|
},
|
||||||
|
{ send_instantly: true },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("tracks maxParticipantsCount correctly across multiple changes", () => {
|
||||||
|
const tracker = new CallEndedTracker();
|
||||||
|
const mockSession = createMockRtcSession();
|
||||||
|
|
||||||
|
tracker.cacheStartCall(new Date());
|
||||||
|
tracker.cacheParticipantCountChanged(3);
|
||||||
|
tracker.cacheParticipantCountChanged(7);
|
||||||
|
tracker.cacheParticipantCountChanged(2);
|
||||||
|
tracker.track("test-call-id", 1, false, mockSession);
|
||||||
|
|
||||||
|
expect(PosthogAnalytics.instance.trackEvent).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
callParticipantsMax: 7,
|
||||||
|
}),
|
||||||
|
expect.anything(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("computes roomEventEncryptionKeysReceivedAverageAge as 0 when no keys received", () => {
|
||||||
|
const tracker = new CallEndedTracker();
|
||||||
|
const mockSession = createMockRtcSession({
|
||||||
|
counters: { roomEventEncryptionKeysReceived: 0 },
|
||||||
|
});
|
||||||
|
|
||||||
|
tracker.cacheStartCall(new Date());
|
||||||
|
tracker.track("test-call-id", 1, false, mockSession);
|
||||||
|
|
||||||
|
expect(PosthogAnalytics.instance.trackEvent).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
roomEventEncryptionKeysReceivedAverageAge: 0,
|
||||||
|
}),
|
||||||
|
expect.anything(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("computes roomEventEncryptionKeysReceivedAverageAge correctly when keys are received", () => {
|
||||||
|
const tracker = new CallEndedTracker();
|
||||||
|
const mockSession = createMockRtcSession({
|
||||||
|
counters: { roomEventEncryptionKeysReceived: 4 },
|
||||||
|
totals: { roomEventEncryptionKeysReceivedTotalAge: 200 },
|
||||||
|
});
|
||||||
|
|
||||||
|
tracker.cacheStartCall(new Date());
|
||||||
|
tracker.track("test-call-id", 1, false, mockSession);
|
||||||
|
|
||||||
|
expect(PosthogAnalytics.instance.trackEvent).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
roomEventEncryptionKeysReceivedAverageAge: 50,
|
||||||
|
}),
|
||||||
|
expect.anything(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes send_instantly option correctly", () => {
|
||||||
|
const tracker = new CallEndedTracker();
|
||||||
|
const mockSession = createMockRtcSession();
|
||||||
|
|
||||||
|
tracker.cacheStartCall(new Date());
|
||||||
|
tracker.track("test-call-id", 1, false, mockSession);
|
||||||
|
|
||||||
|
expect(PosthogAnalytics.instance.trackEvent).toHaveBeenCalledWith(
|
||||||
|
expect.anything(),
|
||||||
|
{ send_instantly: false },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -27,8 +27,8 @@ interface CallEnded extends IPosthogEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class CallEndedTracker {
|
export class CallEndedTracker {
|
||||||
private cache: { startTime: Date; maxParticipantsCount: number } = {
|
private cache: { startTime?: Date; maxParticipantsCount: number } = {
|
||||||
startTime: new Date(0),
|
startTime: undefined,
|
||||||
maxParticipantsCount: 0,
|
maxParticipantsCount: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -49,6 +49,7 @@ export class CallEndedTracker {
|
|||||||
sendInstantly: boolean,
|
sendInstantly: boolean,
|
||||||
rtcSession: MatrixRTCSession,
|
rtcSession: MatrixRTCSession,
|
||||||
): void {
|
): void {
|
||||||
|
if (this.cache.startTime) {
|
||||||
PosthogAnalytics.instance.trackEvent<CallEnded>(
|
PosthogAnalytics.instance.trackEvent<CallEnded>(
|
||||||
{
|
{
|
||||||
eventName: "CallEnded",
|
eventName: "CallEnded",
|
||||||
@@ -69,6 +70,11 @@ export class CallEndedTracker {
|
|||||||
},
|
},
|
||||||
{ send_instantly: sendInstantly },
|
{ send_instantly: sendInstantly },
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
logger.warn(
|
||||||
|
"[PosthogEvents] Failed to send posthog callEnded event due to missing startTime",
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user