diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 1f1a018814..3e1773be29 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -134,7 +134,7 @@ export class PosthogAnalytics { private readonly enabled: boolean = false; private static _instance = null; private platformSuperProperties = {}; - private static ANALYTICS_EVENT_TYPE = "im.vector.analytics"; + public static readonly ANALYTICS_EVENT_TYPE = "im.vector.analytics"; private propertiesForNextEvent: Partial> = {}; private userPropertyCache: UserProperties = {}; private authenticationType: Signup["authenticationType"] = "Other"; diff --git a/src/models/Call.ts b/src/models/Call.ts index cfbd8d3977..2b996c96c3 100644 --- a/src/models/Call.ts +++ b/src/models/Call.ts @@ -53,6 +53,7 @@ import { getCurrentLanguage } from "../languageHandler"; import DesktopCapturerSourcePicker from "../components/views/elements/DesktopCapturerSourcePicker"; import Modal from "../Modal"; import { FontWatcher } from "../settings/watchers/FontWatcher"; +import { PosthogAnalytics } from "../PosthogAnalytics"; const TIMEOUT_MS = 16000; @@ -626,6 +627,15 @@ export class ElementCall extends Call { } private constructor(public readonly groupCall: GroupCall, client: MatrixClient) { + const accountAnalyticsData = client.getAccountData(PosthogAnalytics.ANALYTICS_EVENT_TYPE); + // The analyticsID is passed directly to element call (EC) since this codepath is only for EC and no other widget. + // We really don't want the same analyticID's for the EC and EW posthog instances (Data on posthog should be limited/anonymized as much as possible). + // This is prohibited in EC where a hashed version of the analyticsID is used for the actual posthog identification. + // We can pass the raw EW analyticsID here since we need to trust EC with not sending sensitive data to posthog (EC has access to more sensible data than the analyticsID e.g. the username) + const analyticsID: string = accountAnalyticsData?.getContent().pseudonymousAnalyticsOptIn + ? accountAnalyticsData?.getContent().id + : ""; + // Splice together the Element Call URL for this call const params = new URLSearchParams({ embed: "", @@ -637,6 +647,7 @@ export class ElementCall extends Call { baseUrl: client.baseUrl, lang: getCurrentLanguage().replace("_", "-"), fontScale: `${SettingsStore.getValue("baseFontSize") / FontWatcher.DEFAULT_SIZE}`, + analyticsID, }); // Set custom fonts diff --git a/test/models/Call-test.ts b/test/models/Call-test.ts index 301148adbf..ad6bb362dd 100644 --- a/test/models/Call-test.ts +++ b/test/models/Call-test.ts @@ -23,6 +23,7 @@ import { Room, RoomEvent } from "matrix-js-sdk/src/models/room"; import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state"; import { Widget } from "matrix-widget-api"; import { GroupCallIntent } from "matrix-js-sdk/src/webrtc/groupCall"; +import { MatrixEvent } from "matrix-js-sdk/src/matrix"; import type { Mocked } from "jest-mock"; import type { MatrixClient, IMyDevice } from "matrix-js-sdk/src/client"; @@ -40,6 +41,7 @@ import { ElementWidgetActions } from "../../src/stores/widgets/ElementWidgetActi import SettingsStore from "../../src/settings/SettingsStore"; import Modal, { IHandle } from "../../src/Modal"; import PlatformPeg from "../../src/PlatformPeg"; +import { PosthogAnalytics } from "../../src/PosthogAnalytics"; jest.spyOn(MediaDeviceHandler, "getDevices").mockResolvedValue({ [MediaDeviceKindEnum.AudioInput]: [ @@ -622,6 +624,53 @@ describe("ElementCall", () => { SettingsStore.getValue = originalGetValue; }); + + it("passes analyticsID through widget URL", async () => { + client.getAccountData.mockImplementation((eventType: string) => { + if (eventType === PosthogAnalytics.ANALYTICS_EVENT_TYPE) { + return new MatrixEvent({ content: { id: "123456789987654321", pseudonymousAnalyticsOptIn: true } }); + } + return undefined; + }); + await ElementCall.create(room); + const call = Call.get(room); + if (!(call instanceof ElementCall)) throw new Error("Failed to create call"); + + const urlParams = new URLSearchParams(new URL(call.widget.url).hash.slice(1)); + expect(urlParams.get("analyticsID")).toBe("123456789987654321"); + }); + + it("does not pass analyticsID if `pseudonymousAnalyticsOptIn` set to false", async () => { + client.getAccountData.mockImplementation((eventType: string) => { + if (eventType === PosthogAnalytics.ANALYTICS_EVENT_TYPE) { + return new MatrixEvent({ + content: { id: "123456789987654321", pseudonymousAnalyticsOptIn: false }, + }); + } + return undefined; + }); + await ElementCall.create(room); + const call = Call.get(room); + if (!(call instanceof ElementCall)) throw new Error("Failed to create call"); + + const urlParams = new URLSearchParams(new URL(call.widget.url).hash.slice(1)); + expect(urlParams.get("analyticsID")).toBe(""); + }); + + it("passes empty analyticsID if the id is not in the account data", async () => { + client.getAccountData.mockImplementation((eventType: string) => { + if (eventType === PosthogAnalytics.ANALYTICS_EVENT_TYPE) { + return new MatrixEvent({ content: {} }); + } + return undefined; + }); + await ElementCall.create(room); + const call = Call.get(room); + if (!(call instanceof ElementCall)) throw new Error("Failed to create call"); + + const urlParams = new URLSearchParams(new URL(call.widget.url).hash.slice(1)); + expect(urlParams.get("analyticsID")).toBe(""); + }); }); describe("instance in a non-video room", () => { diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index ee34e20727..6e914189a4 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -136,7 +136,7 @@ export function createTestClient(): MatrixClient { getTurnServers: jest.fn().mockReturnValue([]), getTurnServersExpiry: jest.fn().mockReturnValue(2 ^ 32), getThirdpartyUser: jest.fn().mockResolvedValue([]), - getAccountData: (type) => { + getAccountData: jest.fn().mockImplementation((type) => { return mkEvent({ user: undefined, room: undefined, @@ -144,7 +144,7 @@ export function createTestClient(): MatrixClient { event: true, content: {}, }); - }, + }), mxcUrlToHttp: (mxc) => `http://this.is.a.url/${mxc.substring(6)}`, setAccountData: jest.fn(), setRoomAccountData: jest.fn(),