Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 43f264c

Browse files
author
James Salter
authored
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
1 parent 684b061 commit 43f264c

File tree

7 files changed

+101
-148
lines changed

7 files changed

+101
-148
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
"katex": "^0.12.0",
8585
"linkifyjs": "^2.1.9",
8686
"lodash": "^4.17.20",
87+
"matrix-analytics-events": "https://github.com/matrix-org/matrix-analytics-events.git#1eab4356548c97722a183912fda1ceabbe8cc7c1",
8788
"maplibre-gl": "^1.15.2",
8889
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
8990
"matrix-widget-api": "^0.1.0-beta.18",

src/DecryptionFailureTracker.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,11 @@ export class DecryptionFailure {
2525
}
2626
}
2727

28-
type TrackingFn = (count: number, trackedErrCode: string) => void;
29-
type ErrCodeMapFn = (errcode: string) => string;
28+
type ErrorCode = "OlmKeysNotSentError" | "OlmIndexError" | "UnknownError" | "OlmUnspecifiedError";
29+
30+
type TrackingFn = (count: number, trackedErrCode: ErrorCode) => void;
31+
32+
export type ErrCodeMapFn = (errcode: string) => ErrorCode;
3033

3134
export class DecryptionFailureTracker {
3235
// Array of items of type DecryptionFailure. Every `CHECK_INTERVAL_MS`, this list
@@ -73,12 +76,12 @@ export class DecryptionFailureTracker {
7376
* @param {function?} errorCodeMapFn The function used to map error codes to the
7477
* trackedErrorCode. If not provided, the `.code` of errors will be used.
7578
*/
76-
constructor(private readonly fn: TrackingFn, private readonly errorCodeMapFn?: ErrCodeMapFn) {
79+
constructor(private readonly fn: TrackingFn, private readonly errorCodeMapFn: ErrCodeMapFn) {
7780
if (!fn || typeof fn !== 'function') {
7881
throw new Error('DecryptionFailureTracker requires tracking function');
7982
}
8083

81-
if (errorCodeMapFn && typeof errorCodeMapFn !== 'function') {
84+
if (typeof errorCodeMapFn !== 'function') {
8285
throw new Error('DecryptionFailureTracker second constructor argument should be a function');
8386
}
8487
}
@@ -195,7 +198,7 @@ export class DecryptionFailureTracker {
195198
public trackFailures(): void {
196199
for (const errorCode of Object.keys(this.failureCounts)) {
197200
if (this.failureCounts[errorCode] > 0) {
198-
const trackedErrorCode = this.errorCodeMapFn ? this.errorCodeMapFn(errorCode) : errorCode;
201+
const trackedErrorCode = this.errorCodeMapFn(errorCode);
199202

200203
this.fn(this.failureCounts[errorCode], trackedErrorCode);
201204
this.failureCounts[errorCode] = 0;

src/PosthogAnalytics.ts

Lines changed: 12 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,8 @@ import SettingsStore from "./settings/SettingsStore";
4040
*/
4141

4242
interface IEvent {
43-
// The event name that will be used by PostHog. Event names should use snake_case.
43+
// The event name that will be used by PostHog. Event names should use camelCase.
4444
eventName: string;
45-
46-
// The properties of the event that will be stored in PostHog. This is just a placeholder,
47-
// extending interfaces must override this with a concrete definition to do type validation.
48-
properties: {};
4945
}
5046

5147
export enum Anonymity {
@@ -54,39 +50,16 @@ export enum Anonymity {
5450
Pseudonymous
5551
}
5652

57-
// If an event extends IPseudonymousEvent, the event contains pseudonymous data
58-
// that won't be sent unless the user has explicitly consented to pseudonymous tracking.
59-
// For example, it might contain hashed user IDs or room IDs.
60-
// Such events will be automatically dropped if PosthogAnalytics.anonymity isn't set to Pseudonymous.
61-
export interface IPseudonymousEvent extends IEvent {}
62-
63-
// If an event extends IAnonymousEvent, the event strictly contains *only* anonymous data;
64-
// i.e. no identifiers that can be associated with the user.
65-
export interface IAnonymousEvent extends IEvent {}
66-
67-
export interface IRoomEvent extends IPseudonymousEvent {
68-
hashedRoomId: string;
69-
}
70-
71-
interface IPageView extends IAnonymousEvent {
72-
eventName: "$pageview";
73-
properties: {
74-
durationMs?: number;
75-
screen?: string;
76-
};
77-
}
78-
7953
const whitelistedScreens = new Set([
8054
"register", "login", "forgot_password", "soft_logout", "new", "settings", "welcome", "home", "start", "directory",
8155
"start_sso", "start_cas", "groups", "complete_security", "post_registration", "room", "user", "group",
8256
]);
8357

84-
export async function getRedactedCurrentLocation(
58+
export function getRedactedCurrentLocation(
8559
origin: string,
8660
hash: string,
8761
pathname: string,
88-
anonymity: Anonymity,
89-
): Promise<string> {
62+
): string {
9063
// Redact PII from the current location.
9164
// For known screens, assumes a URL structure of /<screen name>/might/be/pii
9265
if (origin.startsWith('file://')) {
@@ -219,13 +192,12 @@ export class PosthogAnalytics {
219192
};
220193
}
221194

222-
private async capture(eventName: string, properties: posthog.Properties) {
195+
private capture(eventName: string, properties: posthog.Properties) {
223196
if (!this.enabled) {
224197
return;
225198
}
226199
const { origin, hash, pathname } = window.location;
227-
properties['$redacted_current_url'] = await getRedactedCurrentLocation(
228-
origin, hash, pathname, this.anonymity);
200+
properties['$redacted_current_url'] = getRedactedCurrentLocation(origin, hash, pathname);
229201
this.posthog.capture(eventName, properties);
230202
}
231203

@@ -288,35 +260,13 @@ export class PosthogAnalytics {
288260
this.setAnonymity(Anonymity.Disabled);
289261
}
290262

291-
public async trackPseudonymousEvent<E extends IPseudonymousEvent>(
292-
eventName: E["eventName"],
293-
properties: E["properties"] = {},
294-
) {
295-
if (this.anonymity == Anonymity.Anonymous || this.anonymity == Anonymity.Disabled) return;
296-
await this.capture(eventName, properties);
297-
}
298-
299-
public async trackAnonymousEvent<E extends IAnonymousEvent>(
300-
eventName: E["eventName"],
301-
properties: E["properties"] = {},
302-
): Promise<void> {
303-
if (this.anonymity == Anonymity.Disabled) return;
304-
await this.capture(eventName, properties);
305-
}
306-
307-
public async trackPageView(durationMs: number): Promise<void> {
308-
const hash = window.location.hash;
309-
310-
let screen = null;
311-
const split = hash.split("/");
312-
if (split.length >= 2) {
313-
screen = split[1];
314-
}
315-
316-
await this.trackAnonymousEvent<IPageView>("$pageview", {
317-
durationMs,
318-
screen,
319-
});
263+
public trackEvent<E extends IEvent>(
264+
event: E,
265+
): void {
266+
if (this.anonymity == Anonymity.Disabled || this.anonymity == Anonymity.Anonymous) return;
267+
const eventWithoutName = { ...event };
268+
delete eventWithoutName.eventName;
269+
this.capture(event.eventName, eventWithoutName);
320270
}
321271

322272
public async updatePlatformSuperProperties(): Promise<void> {

src/components/structures/MatrixChat.tsx

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import React, { ComponentType, createRef } from 'react';
1818
import { createClient } from "matrix-js-sdk/src/matrix";
1919
import { InvalidStoreError } from "matrix-js-sdk/src/errors";
2020
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
21+
import { Error as ErrorEvent } from "matrix-analytics-events/types/typescript/Error";
22+
import { Screen as ScreenEvent } from "matrix-analytics-events/types/typescript/Screen";
2123
import { defer, IDeferred, QueryDict } from "matrix-js-sdk/src/utils";
2224

2325
// 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> {
446448
const durationMs = this.stopPageChangeTimer();
447449
Analytics.trackPageChange(durationMs);
448450
CountlyAnalytics.instance.trackPageChange(durationMs);
449-
PosthogAnalytics.instance.trackPageView(durationMs);
451+
this.trackScreenChange(durationMs);
450452
}
451453
if (this.focusComposer) {
452454
dis.fire(Action.FocusSendMessageComposer);
@@ -465,6 +467,36 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
465467
if (this.accountPasswordTimer !== null) clearTimeout(this.accountPasswordTimer);
466468
}
467469

470+
public trackScreenChange(durationMs: number): void {
471+
const notLoggedInMap = {};
472+
notLoggedInMap[Views.LOADING] = "WebLoading";
473+
notLoggedInMap[Views.WELCOME] = "WebWelcome";
474+
notLoggedInMap[Views.LOGIN] = "WebLogin";
475+
notLoggedInMap[Views.REGISTER] = "WebRegister";
476+
notLoggedInMap[Views.FORGOT_PASSWORD] = "WebForgotPassword";
477+
notLoggedInMap[Views.COMPLETE_SECURITY] = "WebCompleteSecurity";
478+
notLoggedInMap[Views.E2E_SETUP] = "WebE2ESetup";
479+
notLoggedInMap[Views.SOFT_LOGOUT] = "WebSoftLogout";
480+
481+
const loggedInPageTypeMap = {};
482+
loggedInPageTypeMap[PageType.HomePage] = "Home";
483+
loggedInPageTypeMap[PageType.RoomView] = "Room";
484+
loggedInPageTypeMap[PageType.RoomDirectory] = "RoomDirectory";
485+
loggedInPageTypeMap[PageType.UserView] = "User";
486+
loggedInPageTypeMap[PageType.GroupView] = "Group";
487+
loggedInPageTypeMap[PageType.MyGroups] = "MyGroups";
488+
489+
const screenName = this.state.view === Views.LOGGED_IN ?
490+
loggedInPageTypeMap[this.state.page_type] :
491+
notLoggedInMap[this.state.view];
492+
493+
return PosthogAnalytics.instance.trackEvent<ScreenEvent>({
494+
eventName: "Screen",
495+
screenName,
496+
durationMs,
497+
});
498+
}
499+
468500
getFallbackHsUrl() {
469501
if (this.props.serverConfig && this.props.serverConfig.isDefault) {
470502
return this.props.config.fallback_hs_url;
@@ -1595,17 +1627,22 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
15951627
const dft = new DecryptionFailureTracker((total, errorCode) => {
15961628
Analytics.trackEvent('E2E', 'Decryption failure', errorCode, String(total));
15971629
CountlyAnalytics.instance.track("decryption_failure", { errorCode }, null, { sum: total });
1630+
PosthogAnalytics.instance.trackEvent<ErrorEvent>({
1631+
eventName: "Error",
1632+
domain: "E2EE",
1633+
name: errorCode,
1634+
});
15981635
}, (errorCode) => {
15991636
// Map JS-SDK error codes to tracker codes for aggregation
16001637
switch (errorCode) {
16011638
case 'MEGOLM_UNKNOWN_INBOUND_SESSION_ID':
1602-
return 'olm_keys_not_sent_error';
1639+
return 'OlmKeysNotSentError';
16031640
case 'OLM_UNKNOWN_MESSAGE_INDEX':
1604-
return 'olm_index_error';
1641+
return 'OlmIndexError';
16051642
case undefined:
1606-
return 'unexpected_error';
1643+
return 'OlmUnspecifiedError';
16071644
default:
1608-
return 'unspecified_error';
1645+
return 'UnknownError';
16091646
}
16101647
});
16111648

test/DecryptionFailureTracker-test.js

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ describe('DecryptionFailureTracker', function() {
3939
const failedDecryptionEvent = createFailedDecryptionEvent();
4040

4141
let count = 0;
42-
const tracker = new DecryptionFailureTracker((total) => count += total);
42+
const tracker = new DecryptionFailureTracker((total) => count += total, () => "UnknownError");
4343

4444
const err = new MockDecryptionError();
4545
tracker.eventDecrypted(failedDecryptionEvent, err);
@@ -59,7 +59,7 @@ describe('DecryptionFailureTracker', function() {
5959
const decryptedEvent = createFailedDecryptionEvent();
6060
const tracker = new DecryptionFailureTracker((total) => {
6161
expect(true).toBe(false, 'should not track an event that has since been decrypted correctly');
62-
});
62+
}, () => "UnknownError");
6363

6464
const err = new MockDecryptionError();
6565
tracker.eventDecrypted(decryptedEvent, err);
@@ -81,7 +81,7 @@ describe('DecryptionFailureTracker', function() {
8181
const decryptedEvent2 = createFailedDecryptionEvent();
8282

8383
let count = 0;
84-
const tracker = new DecryptionFailureTracker((total) => count += total);
84+
const tracker = new DecryptionFailureTracker((total) => count += total, () => "UnknownError");
8585

8686
// Arbitrary number of failed decryptions for both events
8787
const err = new MockDecryptionError();
@@ -112,7 +112,7 @@ describe('DecryptionFailureTracker', function() {
112112
const decryptedEvent = createFailedDecryptionEvent();
113113

114114
let count = 0;
115-
const tracker = new DecryptionFailureTracker((total) => count += total);
115+
const tracker = new DecryptionFailureTracker((total) => count += total, () => "UnknownError");
116116

117117
// Indicate decryption
118118
const err = new MockDecryptionError();
@@ -140,7 +140,7 @@ describe('DecryptionFailureTracker', function() {
140140
const decryptedEvent = createFailedDecryptionEvent();
141141

142142
let count = 0;
143-
const tracker = new DecryptionFailureTracker((total) => count += total);
143+
const tracker = new DecryptionFailureTracker((total) => count += total, () => "UnknownError");
144144

145145
// Indicate decryption
146146
const err = new MockDecryptionError();
@@ -153,7 +153,7 @@ describe('DecryptionFailureTracker', function() {
153153
tracker.trackFailures();
154154

155155
// Simulate the browser refreshing by destroying tracker and creating a new tracker
156-
const secondTracker = new DecryptionFailureTracker((total) => count += total);
156+
const secondTracker = new DecryptionFailureTracker((total) => count += total, () => "UnknownError");
157157

158158
//secondTracker.loadTrackedEventHashMap();
159159

@@ -170,28 +170,29 @@ describe('DecryptionFailureTracker', function() {
170170
const counts = {};
171171
const tracker = new DecryptionFailureTracker(
172172
(total, errorCode) => counts[errorCode] = (counts[errorCode] || 0) + total,
173+
(error) => error === "UnknownError" ? "UnknownError" : "OlmKeysNotSentError",
173174
);
174175

175176
// One failure of ERROR_CODE_1, and effectively two for ERROR_CODE_2
176-
tracker.addDecryptionFailure(new DecryptionFailure('$event_id1', 'ERROR_CODE_1'));
177-
tracker.addDecryptionFailure(new DecryptionFailure('$event_id2', 'ERROR_CODE_2'));
178-
tracker.addDecryptionFailure(new DecryptionFailure('$event_id2', 'ERROR_CODE_2'));
179-
tracker.addDecryptionFailure(new DecryptionFailure('$event_id3', 'ERROR_CODE_2'));
177+
tracker.addDecryptionFailure(new DecryptionFailure('$event_id1', 'UnknownError'));
178+
tracker.addDecryptionFailure(new DecryptionFailure('$event_id2', 'OlmKeysNotSentError'));
179+
tracker.addDecryptionFailure(new DecryptionFailure('$event_id2', 'OlmKeysNotSentError'));
180+
tracker.addDecryptionFailure(new DecryptionFailure('$event_id3', 'OlmKeysNotSentError'));
180181

181182
// Pretend "now" is Infinity
182183
tracker.checkFailures(Infinity);
183184

184185
tracker.trackFailures();
185186

186-
expect(counts['ERROR_CODE_1']).toBe(1, 'should track one ERROR_CODE_1');
187-
expect(counts['ERROR_CODE_2']).toBe(2, 'should track two ERROR_CODE_2');
187+
expect(counts['UnknownError']).toBe(1, 'should track one UnknownError');
188+
expect(counts['OlmKeysNotSentError']).toBe(2, 'should track two OlmKeysNotSentError');
188189
});
189190

190191
it('should map error codes correctly', () => {
191192
const counts = {};
192193
const tracker = new DecryptionFailureTracker(
193194
(total, errorCode) => counts[errorCode] = (counts[errorCode] || 0) + total,
194-
(errorCode) => 'MY_NEW_ERROR_CODE',
195+
(errorCode) => 'OlmUnspecifiedError',
195196
);
196197

197198
// One failure of ERROR_CODE_1, and effectively two for ERROR_CODE_2
@@ -204,7 +205,7 @@ describe('DecryptionFailureTracker', function() {
204205

205206
tracker.trackFailures();
206207

207-
expect(counts['MY_NEW_ERROR_CODE'])
208-
.toBe(3, 'should track three MY_NEW_ERROR_CODE, got ' + counts['MY_NEW_ERROR_CODE']);
208+
expect(counts['OlmUnspecifiedError'])
209+
.toBe(3, 'should track three OlmUnspecifiedError, got ' + counts['OlmUnspecifiedError']);
209210
});
210211
});

0 commit comments

Comments
 (0)