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:
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
@@ -205,6 +206,10 @@ describe("PosthogAnalytics", () => {
|
||||
analytics.setAnonymity(Anonymity.Pseudonymous);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
SettingsStore.reset();
|
||||
});
|
||||
|
||||
it("should send layout IRC correctly", async () => {
|
||||
await SettingsStore.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
|
||||
defaultDispatcher.dispatch(
|
||||
@@ -217,7 +222,7 @@ describe("PosthogAnalytics", () => {
|
||||
analytics.trackEvent<ITestEvent>({
|
||||
eventName: "JestTestEvents",
|
||||
});
|
||||
expect(mocked(fakePosthog).capture.mock.calls[0][1]!["$set"]).toStrictEqual({
|
||||
expect(mocked(fakePosthog).capture.mock.calls[0][1]!["$set"]).toMatchObject({
|
||||
WebLayout: "IRC",
|
||||
});
|
||||
});
|
||||
@@ -234,7 +239,7 @@ describe("PosthogAnalytics", () => {
|
||||
analytics.trackEvent<ITestEvent>({
|
||||
eventName: "JestTestEvents",
|
||||
});
|
||||
expect(mocked(fakePosthog).capture.mock.calls[0][1]!["$set"]).toStrictEqual({
|
||||
expect(mocked(fakePosthog).capture.mock.calls[0][1]!["$set"]).toMatchObject({
|
||||
WebLayout: "Bubble",
|
||||
});
|
||||
});
|
||||
@@ -251,7 +256,7 @@ describe("PosthogAnalytics", () => {
|
||||
analytics.trackEvent<ITestEvent>({
|
||||
eventName: "JestTestEvents",
|
||||
});
|
||||
expect(mocked(fakePosthog).capture.mock.calls[0][1]!["$set"]).toStrictEqual({
|
||||
expect(mocked(fakePosthog).capture.mock.calls[0][1]!["$set"]).toMatchObject({
|
||||
WebLayout: "Group",
|
||||
});
|
||||
});
|
||||
@@ -269,12 +274,51 @@ describe("PosthogAnalytics", () => {
|
||||
analytics.trackEvent<ITestEvent>({
|
||||
eventName: "JestTestEvents",
|
||||
});
|
||||
expect(mocked(fakePosthog).capture.mock.calls[0][1]!["$set"]).toStrictEqual({
|
||||
console.log(mocked(fakePosthog).capture.mock.calls[0]);
|
||||
expect(mocked(fakePosthog).capture.mock.calls[0][1]!["$set"]).toMatchObject({
|
||||
WebLayout: "Compact",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("UrlPreviews", () => {
|
||||
let analytics: PosthogAnalytics;
|
||||
|
||||
beforeEach(() => {
|
||||
SdkConfig.put({
|
||||
brand: "Testing",
|
||||
posthog: {
|
||||
project_api_key: "foo",
|
||||
api_host: "bar",
|
||||
},
|
||||
});
|
||||
|
||||
analytics = new PosthogAnalytics(fakePosthog);
|
||||
analytics.setAnonymity(Anonymity.Pseudonymous);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
SdkConfig.reset();
|
||||
});
|
||||
|
||||
it("should set UrlPreviewsEnabled on change", async () => {
|
||||
defaultDispatcher.dispatch(
|
||||
{
|
||||
action: Action.SettingUpdated,
|
||||
settingName: "urlPreviewsEnabled",
|
||||
newValue: true,
|
||||
},
|
||||
true,
|
||||
);
|
||||
analytics.trackEvent<ITestEvent>({
|
||||
eventName: "JestTestEvents",
|
||||
});
|
||||
expect(mocked(fakePosthog).capture.mock.calls[0][1]!["$set"]).toMatchObject({
|
||||
URLPreviewsEnabled: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("CryptoSdk", () => {
|
||||
let analytics: PosthogAnalytics;
|
||||
const getFakeClient = (): MatrixClient =>
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
|
||||
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 { PosthogAnalytics } from "../../src/PosthogAnalytics";
|
||||
import PosthogTrackers from "../../src/PosthogTrackers";
|
||||
|
||||
describe("PosthogTrackers", () => {
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("tracks URL Previews", () => {
|
||||
jest.spyOn(PosthogAnalytics.instance, "trackEvent");
|
||||
const tracker = new PosthogTrackers();
|
||||
tracker.trackUrlPreview("$123456", false, [
|
||||
{
|
||||
title: "A preview",
|
||||
image: {
|
||||
imageThumb: "abc",
|
||||
imageFull: "abc",
|
||||
},
|
||||
link: "a-link",
|
||||
},
|
||||
]);
|
||||
tracker.trackUrlPreview("$123456", false, [
|
||||
{
|
||||
title: "A second preview",
|
||||
link: "a-link",
|
||||
},
|
||||
]);
|
||||
// Ignores subsequent calls.
|
||||
expect(PosthogAnalytics.instance.trackEvent).toHaveBeenCalledWith({
|
||||
eventName: "UrlPreviewRendered",
|
||||
previewKind: "LegacyCard",
|
||||
hasThumbnail: true,
|
||||
previewCount: 1,
|
||||
encryptedRoom: false,
|
||||
});
|
||||
expect(PosthogAnalytics.instance.trackEvent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
Generated
+1
@@ -5979,6 +5979,7 @@ packages:
|
||||
'@xmldom/xmldom@0.8.11':
|
||||
resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
deprecated: this version has critical issues, please update to the latest version
|
||||
|
||||
'@xtuc/ieee754@1.2.0':
|
||||
resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==}
|
||||
|
||||
Reference in New Issue
Block a user