diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 93b84ff305..20c4ff2f33 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -136,6 +136,9 @@ export class PosthogAnalytics { private authenticationType: Signup["authenticationType"] = "Other"; private watchSettingRef?: string; + // Will be set when the matrixClient is passed to the analytics object (e.g. on login). + private currentCryptoBackend?: "Rust" | "Legacy" = undefined; + public static get instance(): PosthogAnalytics { if (!this._instance) { this._instance = new PosthogAnalytics(posthog); @@ -170,6 +173,7 @@ export class PosthogAnalytics { SettingsStore.monitorSetting("layout", null); SettingsStore.monitorSetting("useCompactLayout", null); this.onLayoutUpdated(); + this.updateCryptoSuperProperty(); } private onLayoutUpdated = (): void => { @@ -278,6 +282,8 @@ export class PosthogAnalytics { this.registerSuperProperties(this.platformSuperProperties); } this.anonymity = anonymity; + // update anyhow, no-op if not enabled or Disabled. + this.updateCryptoSuperProperty(); } private static getRandomAnalyticsId(): string { @@ -367,7 +373,28 @@ export class PosthogAnalytics { this.registerSuperProperties(this.platformSuperProperties); } + private updateCryptoSuperProperty(): void { + if (!this.enabled || this.anonymity === Anonymity.Disabled) return; + // Update super property for cryptoSDK in posthog. + // This property will be subsequently passed in every event. + if (this.currentCryptoBackend) { + this.registerSuperProperties({ cryptoSDK: this.currentCryptoBackend }); + } + } + public async updateAnonymityFromSettings(client: MatrixClient, pseudonymousOptIn: boolean): Promise { + // Temporary until we have migration code to switch crypto sdk. + if (client.getCrypto()) { + const cryptoVersion = client.getCrypto()!.getVersion(); + // version for rust is something like "Rust SDK 0.6.0 (9c6b550), Vodozemac 0.5.0" + // for legacy it will be 'Olm x.x.x" + if (cryptoVersion.includes("Rust SDK")) { + this.currentCryptoBackend = "Rust"; + } else { + this.currentCryptoBackend = "Legacy"; + } + } + // Update this.anonymity based on the user's analytics opt-in settings const anonymity = pseudonymousOptIn ? Anonymity.Pseudonymous : Anonymity.Disabled; this.setAnonymity(anonymity); @@ -379,7 +406,8 @@ export class PosthogAnalytics { } if (anonymity !== Anonymity.Disabled) { - await PosthogAnalytics.instance.updatePlatformSuperProperties(); + await this.updatePlatformSuperProperties(); + this.updateCryptoSuperProperty(); } } diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index 1ba7c01d53..d73ee1ec66 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -16,6 +16,7 @@ limitations under the License. import { mocked } from "jest-mock"; import { PostHog } from "posthog-js"; +import { CryptoApi, MatrixClient } from "matrix-js-sdk/src/matrix"; import { Anonymity, getRedactedCurrentLocation, IPosthogEvent, PosthogAnalytics } from "../src/PosthogAnalytics"; import SdkConfig from "../src/SdkConfig"; @@ -37,6 +38,7 @@ const getFakePosthog = (): PostHog => persistence: { get_user_state: jest.fn(), }, + identifyUser: jest.fn(), } as unknown as PostHog); interface ITestEvent extends IPosthogEvent { @@ -274,4 +276,78 @@ describe("PosthogAnalytics", () => { }); }); }); + + describe("CryptoSdk", () => { + let analytics: PosthogAnalytics; + const getFakeClient = (): MatrixClient => + ({ + getCrypto: jest.fn(), + setAccountData: jest.fn(), + // just fake return an `im.vector.analytics` content + getAccountDataFromServer: jest.fn().mockReturnValue({ + id: "0000000", + pseudonymousAnalyticsOptIn: true, + }), + } as unknown as MatrixClient); + + beforeEach(async () => { + SdkConfig.put({ + brand: "Testing", + posthog: { + project_api_key: "foo", + api_host: "bar", + }, + }); + + analytics = new PosthogAnalytics(fakePosthog); + }); + + // `updateAnonymityFromSettings` is called On page load / login / account data change. + // We manually call it so we can test the behaviour. + async function simulateLogin(rustBackend: boolean, pseudonymous = true) { + // To simulate a switch we call updateAnonymityFromSettings. + // As per documentation this function is called On login. + const mockClient = getFakeClient(); + mocked(mockClient.getCrypto).mockReturnValue({ + getVersion: () => { + return rustBackend ? "Rust SDK 0.6.0 (9c6b550), Vodozemac 0.5.0" : "Olm 3.2.0"; + }, + } as unknown as CryptoApi); + await analytics.updateAnonymityFromSettings(mockClient, pseudonymous); + } + + it("should send rust cryptoSDK superProperty correctly", async () => { + analytics.setAnonymity(Anonymity.Pseudonymous); + + await simulateLogin(false); + + expect(mocked(fakePosthog).register.mock.lastCall![0]["cryptoSDK"]).toStrictEqual("Legacy"); + }); + + it("should send Legacy cryptoSDK superProperty correctly", async () => { + analytics.setAnonymity(Anonymity.Pseudonymous); + + await simulateLogin(false); + + // Super Properties are properties associated with events that are set once and then sent with every capture call. + // They are set using posthog.register + expect(mocked(fakePosthog).register.mock.lastCall![0]["cryptoSDK"]).toStrictEqual("Legacy"); + }); + + it("should send cryptoSDK superProperty when enabling analytics", async () => { + analytics.setAnonymity(Anonymity.Disabled); + + await simulateLogin(true, false); + + // This initial call is due to the call to register platformSuperProperties + // The important thing is that the cryptoSDK superProperty is not set. + expect(mocked(fakePosthog).register.mock.lastCall![0]).toStrictEqual({}); + + // switching to pseudonymous should ensure that the cryptoSDK superProperty is set correctly + analytics.setAnonymity(Anonymity.Pseudonymous); + // Super Properties are properties associated with events that are set once and then sent with every capture call. + // They are set using posthog.register + expect(mocked(fakePosthog).register.mock.lastCall![0]["cryptoSDK"]).toStrictEqual("Rust"); + }); + }); });