2021-07-21 12:48:10 +00:00
|
|
|
import posthog, { PostHog } from 'posthog-js';
|
2021-07-21 06:40:39 +00:00
|
|
|
import SdkConfig from './SdkConfig';
|
|
|
|
|
2021-07-21 07:23:42 +00:00
|
|
|
interface IEvent {
|
|
|
|
// The event name that will be used by PostHog.
|
|
|
|
// TODO: standard format (camel case? snake? UpperCase?)
|
|
|
|
eventName: string;
|
|
|
|
|
|
|
|
// The properties of the event that will be stored in PostHog.
|
2021-07-21 06:40:39 +00:00
|
|
|
properties: {}
|
|
|
|
}
|
|
|
|
|
2021-07-21 12:48:10 +00:00
|
|
|
export enum Anonymity {
|
|
|
|
Anonymous,
|
|
|
|
Pseudonymous
|
|
|
|
}
|
|
|
|
|
2021-07-21 07:23:42 +00:00
|
|
|
// 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.
|
|
|
|
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.
|
|
|
|
export interface IAnonymousEvent extends IEvent {}
|
|
|
|
|
|
|
|
export interface IRoomEvent extends IPseudonymousEvent {
|
|
|
|
hashedRoomId: string
|
|
|
|
}
|
|
|
|
|
2021-07-21 10:23:42 +00:00
|
|
|
export interface IOnboardingLoginBegin extends IAnonymousEvent {
|
2021-07-21 06:40:39 +00:00
|
|
|
key: "onboarding_login_begin",
|
|
|
|
}
|
|
|
|
|
|
|
|
const hashHex = async (input: string): Promise<string> => {
|
|
|
|
const buf = new TextEncoder().encode(input);
|
|
|
|
const digestBuf = await window.crypto.subtle.digest("sha-256", buf);
|
|
|
|
return [...new Uint8Array(digestBuf)].map((b: number) => b.toString(16).padStart(2, "0")).join("");
|
|
|
|
};
|
|
|
|
|
2021-07-21 12:48:10 +00:00
|
|
|
const knownScreens = new Set([
|
|
|
|
"register", "login", "forgot_password", "soft_logout", "new", "settings", "welcome", "home", "start", "directory",
|
|
|
|
"start_sso", "start_cas", "groups", "complete_security", "post_registration", "room", "user", "group",
|
|
|
|
]);
|
|
|
|
|
|
|
|
export async function getRedactedCurrentLocation(origin: string, hash: string, pathname: string, anonymity: Anonymity) {
|
|
|
|
// Redact PII from the current location.
|
|
|
|
// If anonymous is true, redact entirely, if false, substitute it with a hash.
|
|
|
|
// For known screens, assumes a URL structure of /<screen name>/might/be/pii
|
|
|
|
if (origin.startsWith('file://')) {
|
|
|
|
pathname = "/<redacted_file_scheme_url>/";
|
|
|
|
}
|
|
|
|
|
|
|
|
let [_, screen, ...parts] = hash.split("/");
|
|
|
|
|
|
|
|
if (!knownScreens.has(screen)) {
|
|
|
|
screen = "<redacted_screen_name>";
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let i = 0; i < parts.length; i++) {
|
|
|
|
parts[i] = anonymity === Anonymity.Anonymous ? `<redacted>` : await hashHex(parts[i]);
|
|
|
|
}
|
|
|
|
|
|
|
|
const hashStr = `${_}/${screen}/${parts.join("/")}`;
|
|
|
|
return origin + pathname + hashStr;
|
|
|
|
}
|
|
|
|
|
2021-07-21 06:40:39 +00:00
|
|
|
export class PosthogAnalytics {
|
|
|
|
private onlyTrackAnonymousEvents = false;
|
|
|
|
private initialised = false;
|
2021-07-21 12:48:10 +00:00
|
|
|
private posthog?: PostHog = null;
|
|
|
|
private redactedCurrentLocation = null;
|
2021-07-21 06:40:39 +00:00
|
|
|
|
|
|
|
private static _instance = null;
|
|
|
|
|
|
|
|
public static instance(): PosthogAnalytics {
|
2021-07-21 10:23:18 +00:00
|
|
|
if (!this._instance) {
|
2021-07-21 06:40:39 +00:00
|
|
|
this._instance = new PosthogAnalytics(posthog);
|
|
|
|
}
|
|
|
|
return this._instance;
|
|
|
|
}
|
|
|
|
|
2021-07-21 12:48:10 +00:00
|
|
|
constructor(posthog: PostHog) {
|
2021-07-21 06:40:39 +00:00
|
|
|
this.posthog = posthog;
|
|
|
|
}
|
|
|
|
|
2021-07-21 12:48:10 +00:00
|
|
|
public async init(onlyTrackAnonymousEvents: boolean) {
|
2021-07-21 06:40:39 +00:00
|
|
|
if (Boolean(navigator.doNotTrack === "1")) {
|
|
|
|
this.initialised = false;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.onlyTrackAnonymousEvents = onlyTrackAnonymousEvents;
|
2021-07-21 12:48:10 +00:00
|
|
|
|
2021-07-21 06:40:39 +00:00
|
|
|
const posthogConfig = SdkConfig.get()["posthog"];
|
|
|
|
if (posthogConfig) {
|
2021-07-21 12:48:10 +00:00
|
|
|
// Update the redacted current location before initialising posthog, as posthog.init triggers
|
|
|
|
// an immediate pageview event which calls the sanitize_properties callback
|
|
|
|
await this.updateRedactedCurrentLocation();
|
|
|
|
|
|
|
|
this.posthog.init(posthogConfig.projectApiKey, {
|
|
|
|
api_host: posthogConfig.apiHost,
|
|
|
|
autocapture: false,
|
|
|
|
mask_all_text: true,
|
|
|
|
mask_all_element_attributes: true,
|
|
|
|
sanitize_properties: this.sanitizeProperties.bind(this),
|
|
|
|
});
|
2021-07-21 06:40:39 +00:00
|
|
|
this.initialised = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-21 12:48:10 +00:00
|
|
|
private async updateRedactedCurrentLocation() {
|
|
|
|
// TODO only calculate this when the location changes as its expensive
|
|
|
|
const { origin, hash, pathname } = window.location;
|
|
|
|
this.redactedCurrentLocation = await getRedactedCurrentLocation(
|
|
|
|
origin, hash, pathname, this.onlyTrackAnonymousEvents ? Anonymity.Anonymous : Anonymity.Pseudonymous);
|
|
|
|
}
|
|
|
|
|
|
|
|
private sanitizeProperties(properties: posthog.Properties, _: string): posthog.Properties {
|
|
|
|
// Sanitize posthog's built in properties which leak PII e.g. url reporting
|
|
|
|
// see utils.js _.info.properties in posthog-js
|
|
|
|
|
|
|
|
// this.redactedCurrentLocation needs to have been updated prior to reaching this point as
|
|
|
|
// updating it involves async, which this callback is not
|
|
|
|
properties['$current_url'] = this.redactedCurrentLocation;
|
|
|
|
|
|
|
|
if (this.onlyTrackAnonymousEvents) {
|
|
|
|
// drop referrer information for anonymous users
|
|
|
|
properties['$referrer'] = null;
|
|
|
|
properties['$referring_domain'] = null;
|
|
|
|
properties['$initial_referrer'] = null;
|
|
|
|
properties['$initial_referring_domain'] = null;
|
|
|
|
|
|
|
|
// drop device ID, which is a UUID persisted in local storage
|
|
|
|
properties['$device_id'] = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return properties;
|
|
|
|
}
|
|
|
|
|
2021-07-21 07:38:58 +00:00
|
|
|
public async identifyUser(userId: string) {
|
|
|
|
if (this.onlyTrackAnonymousEvents) return;
|
|
|
|
this.posthog.identify(await hashHex(userId));
|
|
|
|
}
|
|
|
|
|
2021-07-21 06:40:39 +00:00
|
|
|
public isInitialised(): boolean {
|
|
|
|
return this.initialised;
|
|
|
|
}
|
|
|
|
|
|
|
|
public setOnlyTrackAnonymousEvents(enabled: boolean) {
|
|
|
|
this.onlyTrackAnonymousEvents = enabled;
|
|
|
|
}
|
|
|
|
|
2021-07-21 12:48:10 +00:00
|
|
|
private async capture(eventName: string, properties: posthog.Properties, anonymity: Anonymity) {
|
|
|
|
if (!this.initialised) return;
|
|
|
|
await this.updateRedactedCurrentLocation(anonymity);
|
|
|
|
this.posthog.capture(eventName, properties);
|
|
|
|
}
|
|
|
|
|
|
|
|
public async trackPseudonymousEvent<E extends IPseudonymousEvent>(
|
2021-07-21 07:23:42 +00:00
|
|
|
eventName: E["eventName"],
|
2021-07-21 06:40:39 +00:00
|
|
|
properties: E["properties"],
|
|
|
|
) {
|
2021-07-21 07:23:42 +00:00
|
|
|
if (this.onlyTrackAnonymousEvents) return;
|
2021-07-21 12:48:10 +00:00
|
|
|
this.capture(eventName, properties, Anonymity.Pseudonyomous);
|
2021-07-21 07:23:42 +00:00
|
|
|
}
|
2021-07-21 06:40:39 +00:00
|
|
|
|
2021-07-21 12:48:10 +00:00
|
|
|
public async trackAnonymousEvent<E extends IAnonymousEvent>(
|
2021-07-21 07:23:42 +00:00
|
|
|
eventName: E["eventName"],
|
|
|
|
properties: E["properties"],
|
|
|
|
) {
|
2021-07-21 12:48:10 +00:00
|
|
|
this.capture(eventName, properties, Anonymity.Anonymous);
|
2021-07-21 06:40:39 +00:00
|
|
|
}
|
|
|
|
|
2021-07-21 07:23:42 +00:00
|
|
|
public async trackRoomEvent<E extends IRoomEvent>(
|
|
|
|
eventName: E["eventName"],
|
2021-07-21 06:40:39 +00:00
|
|
|
roomId: string,
|
2021-07-21 07:23:42 +00:00
|
|
|
properties: Omit<E["properties"], "roomId">,
|
2021-07-21 06:40:39 +00:00
|
|
|
) {
|
|
|
|
const updatedProperties = {
|
|
|
|
...properties,
|
|
|
|
hashedRoomId: roomId ? await hashHex(roomId) : null,
|
|
|
|
};
|
2021-07-21 07:23:42 +00:00
|
|
|
this.trackPseudonymousEvent(eventName, updatedProperties);
|
2021-07-21 06:40:39 +00:00
|
|
|
}
|
|
|
|
}
|
2021-07-21 07:42:29 +00:00
|
|
|
|
2021-07-21 10:23:55 +00:00
|
|
|
export function getAnalytics(): PosthogAnalytics {
|
2021-07-21 07:42:29 +00:00
|
|
|
return PosthogAnalytics.instance();
|
|
|
|
}
|