From a6df687196916cb3b31a4fa0cc52cbebed1bb939 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 09:54:37 +0100 Subject: [PATCH] Tidy up interface and add some comments --- src/PosthogAnalytics.ts | 102 ++++++++++++++++++++++------------ test/PosthogAnalytics-test.ts | 2 +- 2 files changed, 66 insertions(+), 38 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index fa530b5309..6329598685 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -20,11 +20,12 @@ export enum Anonymity { // If an event extends IPseudonymousEvent, the event contains pseudonymous data // that won't be sent unless the user has explicitly consented to pseudonymous tracking. -// For example, hashed user IDs or room IDs. +// For example, it might contain hashed user IDs or room IDs. +// Such events will be automatically dropped if PosthogAnalytics.anonymity isn't set to Pseudonymous. export interface IPseudonymousEvent extends IEvent {} -// If an event extends IAnonymousEvent, the event strictly contains *only* anonymous data which -// may be sent without explicit user consent. +// If an event extends IAnonymousEvent, the event strictly contains *only* anonymous data; +// i.e. no identifiers that can be associated with the user. export interface IAnonymousEvent extends IEvent {} export interface IRoomEvent extends IPseudonymousEvent { @@ -83,6 +84,23 @@ export async function getRedactedCurrentLocation(origin: string, hash: string, p } export class PosthogAnalytics { + /* Wrapper for Posthog analytics. + * + * 3 modes of anonymity are supported, governed by this.anonymity + * - Anonymity.Disabled means *no data* is passed to posthog + * - Anonymity.Anonymous means all identifers will be redacted before being passed to posthog + * - Anonymity.Pseudonymous means all identifiers will be hashed via SHA-256 before being passed + * to Posthog + * + * To update anonymity, call updateAnonymityFromSettings() or you can set it directly via setAnonymity(). + * + * To pass an event to Posthog: + * + * 1. Declare a type for the event, extending IAnonymousEvent, IPseudonymousEvent or IRoomEvent. + * 2. Call the appropriate track*() method. Pseudonymous events will be dropped when anonymity is + * Anonymous or Disabled; Anonymous events will be dropped when anonymity is Disabled. + */ + private anonymity = Anonymity.Anonymous; private posthog?: PostHog = null; // set true during the constructor if posthog config is present, otherwise false @@ -156,9 +174,9 @@ export class PosthogAnalytics { // "Send anonymous usage data which helps us improve Element. This will use a cookie." const analyticsOptIn = SettingsStore.getValue("analyticsOptIn"); - // "Send pseudonymous usage data which helps us improve Element. This will use a cookie." + // (proposed wording) "Send pseudonymous usage data which helps us improve Element. This will use a cookie." // - // Currently, this is only a labs flag, for testing purposes. + // TODO: Currently, this is only a labs flag, for testing purposes. const pseudonumousOptIn = SettingsStore.getValue("feature_pseudonymousAnalyticsOptIn"); let anonymity; @@ -173,23 +191,46 @@ export class PosthogAnalytics { return anonymity; } - public async identifyUser(userId: string) { - if (this.anonymity == Anonymity.Pseudonymous) { - this.posthog.identify(await hashHex(userId)); - } - } - private registerSuperProperties(properties) { if (this.enabled) { this.posthog.register(properties); } } + private static async getPlatformProperties() { + const platform = PlatformPeg.get(); + let appVersion; + try { + appVersion = await platform.getAppVersion(); + } catch (e) { + // this happens if no version is set i.e. in dev + appVersion = "unknown"; + } + + return { + appVersion, + appPlatform: platform.getHumanReadableName(), + }; + } + + private async capture(eventName: string, properties: posthog.Properties) { + if (!this.enabled) { + return; + } + const { origin, hash, pathname } = window.location; + properties['$redacted_current_url'] = await getRedactedCurrentLocation( + origin, hash, pathname, this.anonymity); + this.posthog.capture(eventName, properties); + } + public isEnabled() { return this.enabled; } public setAnonymity(anonymity: Anonymity) { + // Update this.anonymity. + // This is public for testing purposes, typically you want to call updateAnonymityFromSettings + // to ensure this value is in step with the user's settings. if (this.enabled && (anonymity == Anonymity.Disabled || anonymity == Anonymity.Anonymous)) { // when transitioning to Disabled or Anonymous ensure we clear out any prior state // set in posthog e.g. distinct ID @@ -200,6 +241,12 @@ export class PosthogAnalytics { this.anonymity = anonymity; } + public async identifyUser(userId: string) { + if (this.anonymity == Anonymity.Pseudonymous) { + this.posthog.identify(await hashHex(userId)); + } + } + public getAnonymity() { return this.anonymity; } @@ -211,16 +258,6 @@ export class PosthogAnalytics { this.setAnonymity(Anonymity.Anonymous); } - private async capture(eventName: string, properties: posthog.Properties) { - if (!this.enabled) { - return; - } - const { origin, hash, pathname } = window.location; - properties['$redacted_current_url'] = await getRedactedCurrentLocation( - origin, hash, pathname, this.anonymity); - this.posthog.capture(eventName, properties); - } - public async trackPseudonymousEvent( eventName: E["eventName"], properties: E["properties"], @@ -256,27 +293,18 @@ export class PosthogAnalytics { } public async updatePlatformSuperProperties() { + // Update super properties in posthog with our platform (app version, platform). + // These properties will be subsequently passed in every event. + // + // This only needs to be done once per page lifetime. Note that getPlatformProperties + // is async and can involve a network request if we are running in a browser. this.platformSuperProperties = await PosthogAnalytics.getPlatformProperties(); this.registerSuperProperties(this.platformSuperProperties); } - private static async getPlatformProperties() { - const platform = PlatformPeg.get(); - let appVersion; - try { - appVersion = await platform.getAppVersion(); - } catch (e) { - // this happens if no version is set i.e. in dev - appVersion = "unknown"; - } - - return { - appVersion, - appPlatform: platform.getHumanReadableName(), - }; - } - public async updateAnonymityFromSettings(userId?: string) { + // Update this.anonymity based on the user's analytics opt-in settings + // Identify the user (via hashed user ID) to posthog if anonymity is pseudonmyous this.setAnonymity(PosthogAnalytics.getAnonymityFromSettings()); if (userId && this.getAnonymity() == Anonymity.Pseudonymous) { await this.identifyUser(userId); diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index f726fe0b13..a0cfec2406 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -172,7 +172,7 @@ bd75b3e080945674c0351f75e0db33d1e90986fa07b318ea7edf776f5eef38d4`); expect(location).toBe("https://foo.bar/#///"); }); - it("Should currently handle an empty hash", async () => { + it("Should handle an empty hash", async () => { const location = await getRedactedCurrentLocation( "https://foo.bar", "", "/", Anonymity.Anonymous); expect(location).toBe("https://foo.bar/");