Integrate analytics stubs (#7186)

* Add matrix-analytics-events as a dependency
* Make IEvent look like a stub definition
* Update pageview tracking to track screens, using a hypothetical definition of a screen event
* Remove distinction between pseudo and anon tracking, will need to rework it considering stubs
This commit is contained in:
James Salter 2021-12-06 21:43:42 +11:00 committed by GitHub
parent 684b0617ae
commit 43f264ccfc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 101 additions and 148 deletions

View file

@ -25,8 +25,11 @@ export class DecryptionFailure {
}
}
type TrackingFn = (count: number, trackedErrCode: string) => void;
type ErrCodeMapFn = (errcode: string) => string;
type ErrorCode = "OlmKeysNotSentError" | "OlmIndexError" | "UnknownError" | "OlmUnspecifiedError";
type TrackingFn = (count: number, trackedErrCode: ErrorCode) => void;
export type ErrCodeMapFn = (errcode: string) => ErrorCode;
export class DecryptionFailureTracker {
// Array of items of type DecryptionFailure. Every `CHECK_INTERVAL_MS`, this list
@ -73,12 +76,12 @@ export class DecryptionFailureTracker {
* @param {function?} errorCodeMapFn The function used to map error codes to the
* trackedErrorCode. If not provided, the `.code` of errors will be used.
*/
constructor(private readonly fn: TrackingFn, private readonly errorCodeMapFn?: ErrCodeMapFn) {
constructor(private readonly fn: TrackingFn, private readonly errorCodeMapFn: ErrCodeMapFn) {
if (!fn || typeof fn !== 'function') {
throw new Error('DecryptionFailureTracker requires tracking function');
}
if (errorCodeMapFn && typeof errorCodeMapFn !== 'function') {
if (typeof errorCodeMapFn !== 'function') {
throw new Error('DecryptionFailureTracker second constructor argument should be a function');
}
}
@ -195,7 +198,7 @@ export class DecryptionFailureTracker {
public trackFailures(): void {
for (const errorCode of Object.keys(this.failureCounts)) {
if (this.failureCounts[errorCode] > 0) {
const trackedErrorCode = this.errorCodeMapFn ? this.errorCodeMapFn(errorCode) : errorCode;
const trackedErrorCode = this.errorCodeMapFn(errorCode);
this.fn(this.failureCounts[errorCode], trackedErrorCode);
this.failureCounts[errorCode] = 0;

View file

@ -40,12 +40,8 @@ import SettingsStore from "./settings/SettingsStore";
*/
interface IEvent {
// The event name that will be used by PostHog. Event names should use snake_case.
// The event name that will be used by PostHog. Event names should use camelCase.
eventName: string;
// The properties of the event that will be stored in PostHog. This is just a placeholder,
// extending interfaces must override this with a concrete definition to do type validation.
properties: {};
}
export enum Anonymity {
@ -54,39 +50,16 @@ export enum Anonymity {
Pseudonymous
}
// 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, 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;
// i.e. no identifiers that can be associated with the user.
export interface IAnonymousEvent extends IEvent {}
export interface IRoomEvent extends IPseudonymousEvent {
hashedRoomId: string;
}
interface IPageView extends IAnonymousEvent {
eventName: "$pageview";
properties: {
durationMs?: number;
screen?: string;
};
}
const whitelistedScreens = 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(
export function getRedactedCurrentLocation(
origin: string,
hash: string,
pathname: string,
anonymity: Anonymity,
): Promise<string> {
): string {
// Redact PII from the current location.
// For known screens, assumes a URL structure of /<screen name>/might/be/pii
if (origin.startsWith('file://')) {
@ -219,13 +192,12 @@ export class PosthogAnalytics {
};
}
private async capture(eventName: string, properties: posthog.Properties) {
private 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);
properties['$redacted_current_url'] = getRedactedCurrentLocation(origin, hash, pathname);
this.posthog.capture(eventName, properties);
}
@ -288,35 +260,13 @@ export class PosthogAnalytics {
this.setAnonymity(Anonymity.Disabled);
}
public async trackPseudonymousEvent<E extends IPseudonymousEvent>(
eventName: E["eventName"],
properties: E["properties"] = {},
) {
if (this.anonymity == Anonymity.Anonymous || this.anonymity == Anonymity.Disabled) return;
await this.capture(eventName, properties);
}
public async trackAnonymousEvent<E extends IAnonymousEvent>(
eventName: E["eventName"],
properties: E["properties"] = {},
): Promise<void> {
if (this.anonymity == Anonymity.Disabled) return;
await this.capture(eventName, properties);
}
public async trackPageView(durationMs: number): Promise<void> {
const hash = window.location.hash;
let screen = null;
const split = hash.split("/");
if (split.length >= 2) {
screen = split[1];
}
await this.trackAnonymousEvent<IPageView>("$pageview", {
durationMs,
screen,
});
public trackEvent<E extends IEvent>(
event: E,
): void {
if (this.anonymity == Anonymity.Disabled || this.anonymity == Anonymity.Anonymous) return;
const eventWithoutName = { ...event };
delete eventWithoutName.eventName;
this.capture(event.eventName, eventWithoutName);
}
public async updatePlatformSuperProperties(): Promise<void> {

View file

@ -18,6 +18,8 @@ import React, { ComponentType, createRef } from 'react';
import { createClient } from "matrix-js-sdk/src/matrix";
import { InvalidStoreError } from "matrix-js-sdk/src/errors";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Error as ErrorEvent } from "matrix-analytics-events/types/typescript/Error";
import { Screen as ScreenEvent } from "matrix-analytics-events/types/typescript/Screen";
import { defer, IDeferred, QueryDict } from "matrix-js-sdk/src/utils";
// focus-visible is a Polyfill for the :focus-visible CSS pseudo-attribute used by _AccessibleButton.scss
@ -446,7 +448,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
const durationMs = this.stopPageChangeTimer();
Analytics.trackPageChange(durationMs);
CountlyAnalytics.instance.trackPageChange(durationMs);
PosthogAnalytics.instance.trackPageView(durationMs);
this.trackScreenChange(durationMs);
}
if (this.focusComposer) {
dis.fire(Action.FocusSendMessageComposer);
@ -465,6 +467,36 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
if (this.accountPasswordTimer !== null) clearTimeout(this.accountPasswordTimer);
}
public trackScreenChange(durationMs: number): void {
const notLoggedInMap = {};
notLoggedInMap[Views.LOADING] = "WebLoading";
notLoggedInMap[Views.WELCOME] = "WebWelcome";
notLoggedInMap[Views.LOGIN] = "WebLogin";
notLoggedInMap[Views.REGISTER] = "WebRegister";
notLoggedInMap[Views.FORGOT_PASSWORD] = "WebForgotPassword";
notLoggedInMap[Views.COMPLETE_SECURITY] = "WebCompleteSecurity";
notLoggedInMap[Views.E2E_SETUP] = "WebE2ESetup";
notLoggedInMap[Views.SOFT_LOGOUT] = "WebSoftLogout";
const loggedInPageTypeMap = {};
loggedInPageTypeMap[PageType.HomePage] = "Home";
loggedInPageTypeMap[PageType.RoomView] = "Room";
loggedInPageTypeMap[PageType.RoomDirectory] = "RoomDirectory";
loggedInPageTypeMap[PageType.UserView] = "User";
loggedInPageTypeMap[PageType.GroupView] = "Group";
loggedInPageTypeMap[PageType.MyGroups] = "MyGroups";
const screenName = this.state.view === Views.LOGGED_IN ?
loggedInPageTypeMap[this.state.page_type] :
notLoggedInMap[this.state.view];
return PosthogAnalytics.instance.trackEvent<ScreenEvent>({
eventName: "Screen",
screenName,
durationMs,
});
}
getFallbackHsUrl() {
if (this.props.serverConfig && this.props.serverConfig.isDefault) {
return this.props.config.fallback_hs_url;
@ -1595,17 +1627,22 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
const dft = new DecryptionFailureTracker((total, errorCode) => {
Analytics.trackEvent('E2E', 'Decryption failure', errorCode, String(total));
CountlyAnalytics.instance.track("decryption_failure", { errorCode }, null, { sum: total });
PosthogAnalytics.instance.trackEvent<ErrorEvent>({
eventName: "Error",
domain: "E2EE",
name: errorCode,
});
}, (errorCode) => {
// Map JS-SDK error codes to tracker codes for aggregation
switch (errorCode) {
case 'MEGOLM_UNKNOWN_INBOUND_SESSION_ID':
return 'olm_keys_not_sent_error';
return 'OlmKeysNotSentError';
case 'OLM_UNKNOWN_MESSAGE_INDEX':
return 'olm_index_error';
return 'OlmIndexError';
case undefined:
return 'unexpected_error';
return 'OlmUnspecifiedError';
default:
return 'unspecified_error';
return 'UnknownError';
}
});