Add analytics tracking for URL previews (#32659)

* Add analytics tracking.

* fix import

* fix other import too

* fixup type

* Add test case

* Add better testing

* make it happier

* update lock
This commit is contained in:
Will Hunt
2026-03-30 10:10:46 +00:00
committed by GitHub
parent f28fca76eb
commit e0cf78b5b8
7 changed files with 151 additions and 4 deletions
+8
View File
@@ -164,7 +164,9 @@ export class PosthogAnalytics {
dis.register(this.onAction);
SettingsStore.monitorSetting("layout", null);
SettingsStore.monitorSetting("useCompactLayout", null);
SettingsStore.monitorSetting("urlPreviewsEnabled", null);
this.onLayoutUpdated();
this.onUrlPreviewSettingUpdated(SettingsStore.getValue("urlPreviewsEnabled"));
this.updateCryptoSuperProperty();
}
@@ -188,11 +190,17 @@ export class PosthogAnalytics {
this.setProperty("WebLayout", layout);
};
private readonly onUrlPreviewSettingUpdated = (value: boolean): void => {
this.setProperty("URLPreviewsEnabled", value);
};
private onAction = (payload: ActionPayload): void => {
if (payload.action !== Action.SettingUpdated) return;
const settingsPayload = payload as SettingUpdatedPayload;
if (["layout", "useCompactLayout"].includes(settingsPayload.settingName)) {
this.onLayoutUpdated();
} else if (settingsPayload.settingName === "urlPreviewsEnabled" && !settingsPayload.roomId) {
this.onUrlPreviewSettingUpdated(settingsPayload.newValue as boolean);
}
};
+29
View File
@@ -1,4 +1,5 @@
/*
Copyright 2026 Element Creations Ltd.
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
@@ -11,11 +12,14 @@ import { type WebScreen as ScreenEvent } from "@matrix-org/analytics-events/type
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 UrlPreview } from "@element-hq/web-shared-components";
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"];
@@ -49,6 +53,8 @@ const SortingAlgorithmMap: Record<SortingAlgorithm, RoomListSortingAlgorithmChan
export default class PosthogTrackers {
private static internalInstance: PosthogTrackers;
private readonly previewedEventIds = new LruCache<string, true>(1000);
public static get instance(): PosthogTrackers {
if (!PosthogTrackers.internalInstance) {
PosthogTrackers.internalInstance = new PosthogTrackers();
@@ -136,6 +142,29 @@ export default class PosthogTrackers {
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: UrlPreview[]): void {
// Discount any previews that we have already tracked.
if (this.previewedEventIds.get(eventId)) {
return;
}
PosthogAnalytics.instance.trackEvent<UrlPreviewRendered>({
eventName: "UrlPreviewRendered",
previewKind: "LegacyCard",
hasThumbnail: previews.some((p) => !!p.image),
previewCount: previews.length,
encryptedRoom: isEncrypted,
});
this.previewedEventIds.set(eventId, true);
}
}
export class PosthogScreenTracker extends PureComponent<{ screenName: ScreenName }> {
@@ -14,6 +14,7 @@ import {
useCreateAutoDisposedViewModel,
EventContentBodyView,
LINKIFIED_DATA_ATTRIBUTE,
useViewModel,
} from "@element-hq/web-shared-components";
import { logger as rootLogger } from "matrix-js-sdk/src/logger";
@@ -39,6 +40,7 @@ import { UrlPreviewGroupViewModel } from "../../../viewmodels/message-body/UrlPr
import { useMediaVisible } from "../../../hooks/useMediaVisible.ts";
import ImageView from "../elements/ImageView.tsx";
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext.tsx";
import PosthogTrackers from "../../../PosthogTrackers.ts";
const logger = rootLogger.getChild("TextualBody");
@@ -427,5 +429,14 @@ export default function TextualBody(props: IBodyProps): React.ReactElement {
})();
}, [vm, props.showUrlPreview, mediaVisible]);
const { previews } = useViewModel(vm);
useEffect(() => {
if (previews.length === 0) {
return;
}
PosthogTrackers.instance.trackUrlPreview(props.mxEvent.getId()!, props.mxEvent.isEncrypted(), previews);
}, [props.mxEvent, previews]);
return <InnerTextualBody urlPreviewViewModel={vm} {...props} />;
}
@@ -14,12 +14,14 @@ import {
import { logger as rootLogger } from "matrix-js-sdk/src/logger";
import { type IPreviewUrlResponse, type MatrixClient, MatrixError, type MatrixEvent } from "matrix-js-sdk/src/matrix";
import { decode } from "html-entities";
import { type UrlPreviewVisibilityChanged } from "@matrix-org/analytics-events/types/typescript/UrlPreviewVisibilityChanged";
import { isPermalinkHost } from "../../utils/permalinks/Permalinks";
import { mediaFromMxc } from "../../customisations/Media";
import PlatformPeg from "../../PlatformPeg";
import { thumbHeight } from "../../ImageUtils";
import SettingsStore from "../../settings/SettingsStore";
import { PosthogAnalytics } from "../../PosthogAnalytics";
const logger = rootLogger.getChild("UrlPreviewGroupViewModel");
@@ -404,6 +406,13 @@ export class UrlPreviewGroupViewModel
// FIXME: persist this somewhere smarter than local storage
globalThis.localStorage?.setItem(this.storageKey, "1");
this.urlPreviewEnabledByUser = false;
PosthogAnalytics.instance.trackEvent<UrlPreviewVisibilityChanged>({
eventName: "UrlPreviewVisibilityChanged",
previewKind: "LegacyCard",
hasThumbnail: this.snapshot.current.previews.some((p) => !!p.image),
previewCount: this.snapshot.current.previews.length,
visible: this.urlPreviewEnabledByUser,
});
return this.computeSnapshot();
};