/* Copyright 2026 Element Creations Ltd. Copyright 2024 New Vector Ltd. Copyright 2022 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE files in the repository root for full details. */ import { PureComponent, type SyntheticEvent } from "react"; import { type WebScreen as ScreenEvent } from "@matrix-org/analytics-events/types/typescript/WebScreen"; import { type Interaction as InteractionEvent } from "@matrix-org/analytics-events/types/typescript/Interaction"; import { type PinUnpinAction } from "@matrix-org/analytics-events/types/typescript/PinUnpinAction"; import { type RoomListSortingAlgorithmChanged } from "@matrix-org/analytics-events/types/typescript/RoomListSortingAlgorithmChanged"; import { type UrlPreviewRendered } from "@matrix-org/analytics-events/types/typescript/UrlPreviewRendered"; import { type SectionCreation } from "@matrix-org/analytics-events/types/typescript/SectionCreation"; import { type ExpandCollapseSection } from "@matrix-org/analytics-events/types/typescript/ExpandCollapseSection"; import PageType from "./PageTypes"; import Views from "./Views"; import { PosthogAnalytics } from "./PosthogAnalytics"; import { SortingAlgorithm } from "./stores/room-list-v3/skip-list/sorters"; import { LruCache } from "./utils/LruCache"; export type ScreenName = ScreenEvent["$current_url"]; export type InteractionName = InteractionEvent["name"]; const notLoggedInMap: Record, ScreenName> = { [Views.LOADING]: "Loading", [Views.CONFIRM_LOCK_THEFT]: "ConfirmStartup", [Views.WELCOME]: "Welcome", [Views.LOGIN]: "Login", [Views.REGISTER]: "Register", [Views.FORGOT_PASSWORD]: "ForgotPassword", [Views.COMPLETE_SECURITY]: "CompleteSecurity", [Views.E2E_SETUP]: "E2ESetup", [Views.PENDING_CLIENT_START]: "Loading", [Views.SOFT_LOGOUT]: "SoftLogout", [Views.LOCK_STOLEN]: "SessionLockStolen", }; const loggedInPageTypeMap: Record = { [PageType.HomePage]: "Home", [PageType.RoomView]: "Room", [PageType.UserView]: "User", }; const SortingAlgorithmMap: Record = { [SortingAlgorithm.Recency]: "Activity", [SortingAlgorithm.Unread]: "Unread", [SortingAlgorithm.Alphabetic]: "Alphabetic", }; export default class PosthogTrackers { private static internalInstance: PosthogTrackers; private readonly previewedEventIds = new LruCache(1000); public static get instance(): PosthogTrackers { if (!PosthogTrackers.internalInstance) { PosthogTrackers.internalInstance = new PosthogTrackers(); } return PosthogTrackers.internalInstance; } private view: Views = Views.LOADING; private pageType?: PageType | string; private override?: ScreenName; public trackPageChange(view: Views, pageType: PageType | string | 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: "$pageview", $current_url: screenName, durationMs, }); } public trackOverride(screenName: ScreenName): void { if (!screenName) return; this.override = screenName; PosthogAnalytics.instance.trackEvent({ eventName: "$pageview", $current_url: screenName, }); } public clearOverride(screenName: ScreenName): void { if (screenName !== this.override) return; this.override = undefined; this.trackPage(); } public static trackInteraction(name: InteractionName, ev?: SyntheticEvent | Event, index?: number): void { let interactionType: InteractionEvent["interactionType"]; if (ev?.type === "click") { interactionType = "Pointer"; } else if (ev?.type.startsWith("key")) { interactionType = "Keyboard"; } PosthogAnalytics.instance.trackEvent({ eventName: "Interaction", interactionType, index, name, }); } /** * Track a pin or unpin action on a message. * @param kind - Is pin or unpin. * @param from - From where the action is triggered. */ public static trackPinUnpinMessage(kind: PinUnpinAction["kind"], from: PinUnpinAction["from"]): void { PosthogAnalytics.instance.trackEvent({ eventName: "PinUnpinAction", kind, from, }); } /** * Track when the user chooses a different sorting algorithm for the room-list. * @param oldAlgorithm - The old algorithm. * @param newAlgorithm - The new algorithm. */ public static trackRoomListSortingAlgorithmChange( oldAlgorithm: SortingAlgorithm, newAlgorithm: SortingAlgorithm, ): void { PosthogAnalytics.instance.trackEvent({ eventName: "RoomListSortingAlgorithmChanged", oldAlgorithm: SortingAlgorithmMap[oldAlgorithm], newAlgorithm: SortingAlgorithmMap[newAlgorithm], }); } /** * Track if an event has had a previewed rendered in the client. * This function makes a best-effort attempt to prevent double counting. * * @param eventId EventID for deduplication. * @param isEncrypted Whether the event (and effectively the room) was encrypted. * @param previews The previews generated from the event. */ public trackUrlPreview(eventId: string, isEncrypted: boolean, previews: { image?: unknown }[]): void { // Discount any previews that we have already tracked. if (this.previewedEventIds.get(eventId)) { return; } PosthogAnalytics.instance.trackEvent({ eventName: "UrlPreviewRendered", previewKind: "LegacyCard", hasThumbnail: previews.some((p) => !!p.image), previewCount: previews.length, encryptedRoom: isEncrypted, }); this.previewedEventIds.set(eventId, true); } /** * Track when the user creates a section in the room list. * @param from The source from which the section creation was triggered. */ public static trackSectionCreation(from: SectionCreation["from"]): void { PosthogAnalytics.instance.trackEvent({ eventName: "SectionCreation", from, }); } /** * Track when the user collapses or expands a section in the room list. * @param kind Is expand or collapse. * @param from From where the action is triggered. */ public static trackCollapseOrExpandSection( kind: ExpandCollapseSection["kind"], from: ExpandCollapseSection["from"], ): void { PosthogAnalytics.instance.trackEvent({ eventName: "ExpandCollapseSection", kind, from, }); } } export class PosthogScreenTracker extends PureComponent<{ screenName: ScreenName }> { public componentDidMount(): void { PosthogTrackers.instance.trackOverride(this.props.screenName); } public componentDidUpdate(): void { // 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); } public componentWillUnmount(): void { PosthogTrackers.instance.clearOverride(this.props.screenName); } public render(): React.ReactNode { return null; // no need to render anything, we just need to hook into the React lifecycle } }