Wire up more Posthog tracking (#7689)

This commit is contained in:
Michael Telatynski 2022-02-09 14:25:58 +00:00 committed by GitHub
parent 254dbeeccb
commit 5620b83d34
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 221 additions and 77 deletions

View file

@ -91,7 +91,7 @@
"linkifyjs": "^4.0.0-beta.4",
"lodash": "^4.17.20",
"maplibre-gl": "^1.15.2",
"matrix-analytics-events": "github:matrix-org/matrix-analytics-events.git#53844e3f6f9fefa88384a996b2bf5e60bb301b94",
"matrix-analytics-events": "github:matrix-org/matrix-analytics-events.git#035d02303996c1fa7119bd8a09f2e00db9a4c648",
"matrix-events-sdk": "^0.0.1-beta.6",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
"matrix-widget-api": "^0.1.0-beta.18",

View file

@ -19,7 +19,6 @@ limitations under the License.
enum PageType {
HomePage = "home_page",
RoomView = "room_view",
RoomDirectory = "room_directory",
UserView = "user_view",
GroupView = "group_view",
MyGroups = "my_groups",

110
src/PosthogTrackers.ts Normal file
View file

@ -0,0 +1,110 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { PureComponent } from "react";
import { Screen as ScreenEvent } from "matrix-analytics-events/types/typescript/Screen";
import PageType from "./PageTypes";
import Views from "./Views";
import { PosthogAnalytics } from "./PosthogAnalytics";
export type ScreenName = ScreenEvent["screenName"];
const notLoggedInMap: Record<Exclude<Views, Views.LOGGED_IN>, ScreenName> = {
[Views.LOADING]: "WebLoading",
[Views.WELCOME]: "Welcome",
[Views.LOGIN]: "Login",
[Views.REGISTER]: "Register",
[Views.FORGOT_PASSWORD]: "ForgotPassword",
[Views.COMPLETE_SECURITY]: "WebCompleteSecurity",
[Views.E2E_SETUP]: "WebE2ESetup",
[Views.SOFT_LOGOUT]: "WebSoftLogout",
};
const loggedInPageTypeMap: Record<PageType, ScreenName> = {
[PageType.HomePage]: "Home",
[PageType.RoomView]: "Room",
[PageType.UserView]: "User",
[PageType.GroupView]: "Group",
[PageType.MyGroups]: "MyGroups",
};
export default class PosthogTrackers {
private static internalInstance: PosthogTrackers;
public static get instance(): PosthogTrackers {
if (!PosthogTrackers.internalInstance) {
PosthogTrackers.internalInstance = new PosthogTrackers();
}
return PosthogTrackers.internalInstance;
}
private view: Views = Views.LOADING;
private pageType?: PageType = null;
private override?: ScreenName = null;
public trackPageChange(view: Views, pageType: PageType | undefined, durationMs: number): void {
this.view = view;
this.pageType = pageType;
if (this.override) return;
this.trackPage(durationMs);
}
private trackPage(durationMs?: number): void {
const screenName = this.view === Views.LOGGED_IN
? loggedInPageTypeMap[this.pageType]
: notLoggedInMap[this.view];
PosthogAnalytics.instance.trackEvent<ScreenEvent>({
eventName: "$screen",
screenName,
durationMs,
});
}
public trackOverride(screenName: ScreenName): void {
if (!screenName) return;
this.override = screenName;
PosthogAnalytics.instance.trackEvent<ScreenEvent>({
eventName: "$screen",
screenName,
});
}
public clearOverride(screenName: ScreenName): void {
if (screenName !== this.override) return;
this.override = null;
this.trackPage();
}
}
export class PosthogScreenTracker extends PureComponent<{ screenName: ScreenName }> {
componentDidMount() {
PosthogTrackers.instance.trackOverride(this.props.screenName);
}
componentDidUpdate() {
// We do not clear the old override here so that we do not send the non-override screen as a transition
PosthogTrackers.instance.trackOverride(this.props.screenName);
}
componentWillUnmount() {
PosthogTrackers.instance.clearOverride(this.props.screenName);
}
render() {
return null; // no need to render anything, we just need to hook into the React lifecycle
}
}

50
src/Views.ts Normal file
View file

@ -0,0 +1,50 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/** constants for MatrixChat.state.view */
enum Views {
// a special initial state which is only used at startup, while we are
// trying to re-animate a matrix client or register as a guest.
LOADING,
// we are showing the welcome view
WELCOME,
// we are showing the login view
LOGIN,
// we are showing the registration view
REGISTER,
// showing the 'forgot password' view
FORGOT_PASSWORD,
// showing flow to trust this new device with cross-signing
COMPLETE_SECURITY,
// flow to setup SSSS / cross-signing on this account
E2E_SETUP,
// we are logged in with an active matrix client. The logged_in state also
// includes guests users as they too are logged in at the client level.
LOGGED_IN,
// We are logged out (invalid token) but have our local state again. The user
// should log back in to rehydrate the client.
SOFT_LOGOUT,
}
export default Views;

View file

@ -630,10 +630,6 @@ class LoggedInView extends React.Component<IProps, IState> {
pageElement = <MyGroups />;
break;
case PageTypes.RoomDirectory:
// handled by MatrixChat for now
break;
case PageTypes.HomePage:
pageElement = <HomePage justRegistered={this.props.justRegistered} />;
break;

View file

@ -20,7 +20,6 @@ import { ISyncStateData, SyncState } from 'matrix-js-sdk/src/sync';
import { MatrixError } from 'matrix-js-sdk/src/http-api';
import { InvalidStoreError } from "matrix-js-sdk/src/errors";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Screen as ScreenEvent } from "matrix-analytics-events/types/typescript/Screen";
import { defer, IDeferred, QueryDict } from "matrix-js-sdk/src/utils";
import { logger } from "matrix-js-sdk/src/logger";
import { throttle } from "lodash";
@ -30,6 +29,7 @@ import 'focus-visible';
// what-input helps improve keyboard accessibility
import 'what-input';
import PosthogTrackers from '../../PosthogTrackers';
import Analytics from "../../Analytics";
import CountlyAnalytics from "../../CountlyAnalytics";
import { DecryptionFailureTracker } from "../../DecryptionFailureTracker";
@ -118,39 +118,10 @@ import AccessibleButton from "../views/elements/AccessibleButton";
import { ActionPayload } from "../../dispatcher/payloads";
import { SummarizedNotificationState } from "../../stores/notifications/SummarizedNotificationState";
import GenericToast from '../views/toasts/GenericToast';
import Views from '../../Views';
/** constants for MatrixChat.state.view */
export enum Views {
// a special initial state which is only used at startup, while we are
// trying to re-animate a matrix client or register as a guest.
LOADING,
// we are showing the welcome view
WELCOME,
// we are showing the login view
LOGIN,
// we are showing the registration view
REGISTER,
// showing the 'forgot password' view
FORGOT_PASSWORD,
// showing flow to trust this new device with cross-signing
COMPLETE_SECURITY,
// flow to setup SSSS / cross-signing on this account
E2E_SETUP,
// we are logged in with an active matrix client. The logged_in state also
// includes guests users as they too are logged in at the client level.
LOGGED_IN,
// We are logged out (invalid token) but have our local state again. The user
// should log back in to rehydrate the client.
SOFT_LOGOUT,
}
// legacy export
export { default as Views } from "../../Views";
const AUTH_SCREENS = ["register", "login", "forgot_password", "start_sso", "start_cas", "welcome"];
@ -455,7 +426,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
const durationMs = this.stopPageChangeTimer();
Analytics.trackPageChange(durationMs);
CountlyAnalytics.instance.trackPageChange(durationMs);
this.trackScreenChange(durationMs);
PosthogTrackers.instance.trackPageChange(this.state.view, this.state.page_type, durationMs);
}
if (this.focusComposer) {
dis.fire(Action.FocusSendMessageComposer);
@ -475,36 +446,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
if (this.accountPasswordTimer !== null) clearTimeout(this.accountPasswordTimer);
}
public trackScreenChange(durationMs: number): void {
const notLoggedInMap: Partial<Record<Views, ScreenEvent["screenName"]>> = {};
notLoggedInMap[Views.LOADING] = "WebLoading";
notLoggedInMap[Views.WELCOME] = "Welcome";
notLoggedInMap[Views.LOGIN] = "Login";
notLoggedInMap[Views.REGISTER] = "Register";
notLoggedInMap[Views.FORGOT_PASSWORD] = "ForgotPassword";
notLoggedInMap[Views.COMPLETE_SECURITY] = "WebCompleteSecurity";
notLoggedInMap[Views.E2E_SETUP] = "WebE2ESetup";
notLoggedInMap[Views.SOFT_LOGOUT] = "WebSoftLogout";
const loggedInPageTypeMap: Partial<Record<PageType, ScreenEvent["screenName"]>> = {};
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,
});
}
private onWindowResized = (): void => {
// XXX: This is a very unreliable way to detect whether or not the the devtools are open
this.warnInConsole();

View file

@ -811,6 +811,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
hasCancel={true}
onFinished={this.onFinished}
title={title}
screenName="RoomDirectory"
>
<div className="mx_RoomDirectory">
{ explanation }

View file

@ -24,6 +24,7 @@ import { _t } from '../../languageHandler';
import AutoHideScrollbar from './AutoHideScrollbar';
import { replaceableComponent } from "../../utils/replaceableComponent";
import AccessibleButton from "../views/elements/AccessibleButton";
import { PosthogScreenTracker, ScreenName } from "../../PosthogTrackers";
/**
* Represents a tab for the TabbedView.
@ -35,9 +36,15 @@ export class Tab {
* @param {string} label The untranslated tab label.
* @param {string} icon The class for the tab icon. This should be a simple mask.
* @param {React.ReactNode} body The JSX for the tab container.
* @param {string} screenName The screen name to report to Posthog.
*/
constructor(public id: string, public label: string, public icon: string, public body: React.ReactNode) {
}
constructor(
public readonly id: string,
public readonly label: string,
public readonly icon: string,
public readonly body: React.ReactNode,
public readonly screenName?: ScreenName,
) {}
}
export enum TabLocation {
@ -50,6 +57,7 @@ interface IProps {
initialTabId?: string;
tabLocation: TabLocation;
onChange?: (tabId: string) => void;
screenName?: ScreenName;
}
interface IState {
@ -132,7 +140,8 @@ export default class TabbedView extends React.Component<IProps, IState> {
public render(): React.ReactNode {
const labels = this.props.tabs.map(tab => this.renderTabLabel(tab));
const panel = this.renderTabPanel(this.props.tabs[this.getActiveTabIndex()]);
const tab = this.props.tabs[this.getActiveTabIndex()];
const panel = this.renderTabPanel(tab);
const tabbedViewClasses = classNames({
'mx_TabbedView': true,
@ -142,6 +151,7 @@ export default class TabbedView extends React.Component<IProps, IState> {
return (
<div className={tabbedViewClasses}>
<PosthogScreenTracker screenName={tab?.screenName ?? this.props.screenName} />
<div className="mx_TabbedView_tabLabels">
{ labels }
</div>

View file

@ -29,6 +29,7 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import Heading from '../typography/Heading';
import { IDialogProps } from "./IDialogProps";
import { PosthogScreenTracker, ScreenName } from "../../../PosthogTrackers";
interface IProps extends IDialogProps {
// Whether the dialog should have a 'close' button that will
@ -66,6 +67,9 @@ interface IProps extends IDialogProps {
titleClass?: string | string[];
headerButton?: JSX.Element;
// optional Posthog ScreenName to supply during the lifetime of this dialog
screenName?: ScreenName;
}
/*
@ -119,6 +123,7 @@ export default class BaseDialog extends React.Component<IProps> {
return (
<MatrixClientContext.Provider value={this.matrixClient}>
<PosthogScreenTracker screenName={this.props.screenName} />
<FocusLock
returnFocus={true}
lockProps={{

View file

@ -326,7 +326,12 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
}
return (
<BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished} title={title}>
<BaseDialog
className="mx_CreateRoomDialog"
onFinished={this.props.onFinished}
title={title}
screenName="CreateRoom"
>
<form onSubmit={this.onOk} onKeyDown={this.onKeyDown}>
<div className="mx_Dialog_content">
<Field

View file

@ -199,10 +199,12 @@ export default class DeactivateAccountDialog extends React.Component<IProps, ISt
// this is on purpose not a <form /> to prevent Enter triggering submission, to further prevent accidents
return (
<BaseDialog className="mx_DeactivateAccountDialog"
<BaseDialog
className="mx_DeactivateAccountDialog"
onFinished={this.props.onFinished}
titleClass="danger"
title={_t("Deactivate Account")}
screenName="DeactivateAccount"
>
<div className="mx_Dialog_content">
<p>{ _t(

View file

@ -70,6 +70,7 @@ import SpaceStore from "../../../stores/spaces/SpaceStore";
import CallHandler from "../../../CallHandler";
import UserIdentifierCustomisations from '../../../customisations/UserIdentifier';
import CopyableText from "../elements/CopyableText";
import { ScreenName } from '../../../PosthogTrackers';
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
/* eslint-disable camelcase */
@ -1307,6 +1308,13 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
selectText(e.target);
}
private get screenName(): ScreenName {
switch (this.props.kind) {
case KIND_DM:
return "StartChat";
}
}
render() {
let spinner = null;
if (this.state.busy) {
@ -1584,6 +1592,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
hasCancel={true}
onFinished={this.props.onFinished}
title={title}
screenName={this.screenName}
>
<div className='mx_InviteDialog_content'>
{ dialogContent }

View file

@ -154,6 +154,7 @@ export default class RoomSettingsDialog extends React.Component<IProps, IState>
<TabbedView
tabs={this.getTabs()}
initialTabId={this.props.initialTabId}
screenName="RoomSettings"
/>
</div>
</BaseDialog>

View file

@ -94,12 +94,14 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
_td("General"),
"mx_UserSettingsDialog_settingsIcon",
<GeneralUserSettingsTab closeSettingsFn={this.props.onFinished} />,
"WebUserSettingsGeneral",
));
tabs.push(new Tab(
UserTab.Appearance,
_td("Appearance"),
"mx_UserSettingsDialog_appearanceIcon",
<AppearanceUserSettingsTab />,
"WebUserSettingsAppearance",
));
if (SettingsStore.getValue(UIFeature.Flair)) {
tabs.push(new Tab(
@ -107,6 +109,7 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
_td("Flair"),
"mx_UserSettingsDialog_flairIcon",
<FlairUserSettingsTab />,
"WebUserSettingFlair",
));
}
tabs.push(new Tab(
@ -114,24 +117,28 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
_td("Notifications"),
"mx_UserSettingsDialog_bellIcon",
<NotificationUserSettingsTab />,
"WebUserSettingsNotifications",
));
tabs.push(new Tab(
UserTab.Preferences,
_td("Preferences"),
"mx_UserSettingsDialog_preferencesIcon",
<PreferencesUserSettingsTab closeSettingsFn={this.props.onFinished} />,
"WebUserSettingsPreferences",
));
tabs.push(new Tab(
UserTab.Keyboard,
_td("Keyboard"),
"mx_UserSettingsDialog_keyboardIcon",
<KeyboardUserSettingsTab />,
"WebUserSettingsKeyboard",
));
tabs.push(new Tab(
UserTab.Sidebar,
_td("Sidebar"),
"mx_UserSettingsDialog_sidebarIcon",
<SidebarUserSettingsTab />,
"WebUserSettingsSidebar",
));
if (SettingsStore.getValue(UIFeature.Voip)) {
@ -140,6 +147,7 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
_td("Voice & Video"),
"mx_UserSettingsDialog_voiceIcon",
<VoiceUserSettingsTab />,
"WebUserSettingsVoiceVideo",
));
}
@ -148,6 +156,7 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
_td("Security & Privacy"),
"mx_UserSettingsDialog_securityIcon",
<SecurityUserSettingsTab closeSettingsFn={this.props.onFinished} />,
"WebUserSettingsSecurityPrivacy",
));
// Show the Labs tab if enabled or if there are any active betas
if (SdkConfig.get()['showLabsSettings']
@ -158,6 +167,7 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
_td("Labs"),
"mx_UserSettingsDialog_labsIcon",
<LabsUserSettingsTab />,
"WebUserSettingsLabs",
));
}
if (this.state.mjolnirEnabled) {
@ -166,6 +176,7 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
_td("Ignored users"),
"mx_UserSettingsDialog_mjolnirIcon",
<MjolnirUserSettingsTab />,
"WebUserSettingMjolnir",
));
}
tabs.push(new Tab(
@ -173,6 +184,7 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
_td("Help & About"),
"mx_UserSettingsDialog_helpIcon",
<HelpUserSettingsTab closeSettingsFn={() => this.props.onFinished(true)} />,
"WebUserSettingsHelpAbout",
));
return tabs;

View file

@ -23,6 +23,7 @@ import { _t } from '../../../languageHandler';
import { Key } from "../../../Keyboard";
import DesktopBuildsNotice, { WarningKind } from "../elements/DesktopBuildsNotice";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { PosthogScreenTracker } from '../../../PosthogTrackers';
interface IProps {
onCancelClick: () => void;
@ -93,6 +94,7 @@ export default class SearchBar extends React.Component<IProps, IState> {
return (
<>
<PosthogScreenTracker screenName="RoomSearch" />
<div className="mx_SearchBar">
<div className="mx_SearchBar_buttons" role="radiogroup">
<AccessibleButton

View file

@ -10,6 +10,7 @@ Array [
onFinished={[MockFunction]}
title="Export Chat"
>
<PosthogScreenTracker />
<ForwardRef(FocusLockUICombination)
className="mx_ExportDialog false mx_Dialog_fixedWidth"
lockProps={

View file

@ -6249,9 +6249,9 @@ mathml-tag-names@^2.1.3:
resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3"
integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
"matrix-analytics-events@github:matrix-org/matrix-analytics-events.git#53844e3f6f9fefa88384a996b2bf5e60bb301b94":
"matrix-analytics-events@github:matrix-org/matrix-analytics-events.git#035d02303996c1fa7119bd8a09f2e00db9a4c648":
version "0.0.1"
resolved "https://codeload.github.com/matrix-org/matrix-analytics-events/tar.gz/53844e3f6f9fefa88384a996b2bf5e60bb301b94"
resolved "https://codeload.github.com/matrix-org/matrix-analytics-events/tar.gz/035d02303996c1fa7119bd8a09f2e00db9a4c648"
matrix-events-sdk@^0.0.1-beta.6:
version "0.0.1-beta.6"