diff --git a/package.json b/package.json index 147675ce28..bc657beede 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/PageTypes.ts b/src/PageTypes.ts index 73967f351e..0e4944efc9 100644 --- a/src/PageTypes.ts +++ b/src/PageTypes.ts @@ -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", diff --git a/src/PosthogTrackers.ts b/src/PosthogTrackers.ts new file mode 100644 index 0000000000..4e13bf2a80 --- /dev/null +++ b/src/PosthogTrackers.ts @@ -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, 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.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({ + eventName: "$screen", + screenName, + durationMs, + }); + } + + public trackOverride(screenName: ScreenName): void { + if (!screenName) return; + this.override = screenName; + PosthogAnalytics.instance.trackEvent({ + 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 + } +} diff --git a/src/Views.ts b/src/Views.ts new file mode 100644 index 0000000000..4c734f7bba --- /dev/null +++ b/src/Views.ts @@ -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; diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 8e967169da..667d59dd31 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -630,10 +630,6 @@ class LoggedInView extends React.Component { pageElement = ; break; - case PageTypes.RoomDirectory: - // handled by MatrixChat for now - break; - case PageTypes.HomePage: pageElement = ; break; diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 58d5094f7c..58b937b87b 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -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 { 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 { if (this.accountPasswordTimer !== null) clearTimeout(this.accountPasswordTimer); } - public trackScreenChange(durationMs: number): void { - const notLoggedInMap: Partial> = {}; - 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> = {}; - 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({ - 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(); diff --git a/src/components/structures/RoomDirectory.tsx b/src/components/structures/RoomDirectory.tsx index 10349d8e95..22e9fb759e 100644 --- a/src/components/structures/RoomDirectory.tsx +++ b/src/components/structures/RoomDirectory.tsx @@ -811,6 +811,7 @@ export default class RoomDirectory extends React.Component { hasCancel={true} onFinished={this.onFinished} title={title} + screenName="RoomDirectory" >
{ explanation } diff --git a/src/components/structures/TabbedView.tsx b/src/components/structures/TabbedView.tsx index 8287189c2c..ea116b7d8e 100644 --- a/src/components/structures/TabbedView.tsx +++ b/src/components/structures/TabbedView.tsx @@ -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 { 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 { return (
+
{ labels }
diff --git a/src/components/views/dialogs/BaseDialog.tsx b/src/components/views/dialogs/BaseDialog.tsx index 52773c13b9..bbb9266084 100644 --- a/src/components/views/dialogs/BaseDialog.tsx +++ b/src/components/views/dialogs/BaseDialog.tsx @@ -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 { return ( + { } return ( - +
to prevent Enter triggering submission, to further prevent accidents return ( -

{ _t( diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index ee82ae1bea..37301c26b9 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -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

{ dialogContent } diff --git a/src/components/views/dialogs/RoomSettingsDialog.tsx b/src/components/views/dialogs/RoomSettingsDialog.tsx index 67dc4e07d5..7490dfe5a7 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.tsx +++ b/src/components/views/dialogs/RoomSettingsDialog.tsx @@ -154,6 +154,7 @@ export default class RoomSettingsDialog extends React.Component
diff --git a/src/components/views/dialogs/UserSettingsDialog.tsx b/src/components/views/dialogs/UserSettingsDialog.tsx index de438c42ee..5ca0601655 100644 --- a/src/components/views/dialogs/UserSettingsDialog.tsx +++ b/src/components/views/dialogs/UserSettingsDialog.tsx @@ -94,12 +94,14 @@ export default class UserSettingsDialog extends React.Component _td("General"), "mx_UserSettingsDialog_settingsIcon", , + "WebUserSettingsGeneral", )); tabs.push(new Tab( UserTab.Appearance, _td("Appearance"), "mx_UserSettingsDialog_appearanceIcon", , + "WebUserSettingsAppearance", )); if (SettingsStore.getValue(UIFeature.Flair)) { tabs.push(new Tab( @@ -107,6 +109,7 @@ export default class UserSettingsDialog extends React.Component _td("Flair"), "mx_UserSettingsDialog_flairIcon", , + "WebUserSettingFlair", )); } tabs.push(new Tab( @@ -114,24 +117,28 @@ export default class UserSettingsDialog extends React.Component _td("Notifications"), "mx_UserSettingsDialog_bellIcon", , + "WebUserSettingsNotifications", )); tabs.push(new Tab( UserTab.Preferences, _td("Preferences"), "mx_UserSettingsDialog_preferencesIcon", , + "WebUserSettingsPreferences", )); tabs.push(new Tab( UserTab.Keyboard, _td("Keyboard"), "mx_UserSettingsDialog_keyboardIcon", , + "WebUserSettingsKeyboard", )); tabs.push(new Tab( UserTab.Sidebar, _td("Sidebar"), "mx_UserSettingsDialog_sidebarIcon", , + "WebUserSettingsSidebar", )); if (SettingsStore.getValue(UIFeature.Voip)) { @@ -140,6 +147,7 @@ export default class UserSettingsDialog extends React.Component _td("Voice & Video"), "mx_UserSettingsDialog_voiceIcon", , + "WebUserSettingsVoiceVideo", )); } @@ -148,6 +156,7 @@ export default class UserSettingsDialog extends React.Component _td("Security & Privacy"), "mx_UserSettingsDialog_securityIcon", , + "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 _td("Labs"), "mx_UserSettingsDialog_labsIcon", , + "WebUserSettingsLabs", )); } if (this.state.mjolnirEnabled) { @@ -166,6 +176,7 @@ export default class UserSettingsDialog extends React.Component _td("Ignored users"), "mx_UserSettingsDialog_mjolnirIcon", , + "WebUserSettingMjolnir", )); } tabs.push(new Tab( @@ -173,6 +184,7 @@ export default class UserSettingsDialog extends React.Component _td("Help & About"), "mx_UserSettingsDialog_helpIcon", this.props.onFinished(true)} />, + "WebUserSettingsHelpAbout", )); return tabs; diff --git a/src/components/views/rooms/SearchBar.tsx b/src/components/views/rooms/SearchBar.tsx index c8b03b99d6..05aceb4d10 100644 --- a/src/components/views/rooms/SearchBar.tsx +++ b/src/components/views/rooms/SearchBar.tsx @@ -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 { return ( <> +
+