2021-08-03 10:55:02 +00:00
|
|
|
/*
|
|
|
|
Copyright 2021 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.
|
|
|
|
*/
|
|
|
|
|
2022-03-23 10:27:28 +00:00
|
|
|
import { mocked } from "jest-mock";
|
|
|
|
import { PostHog } from "posthog-js";
|
|
|
|
|
2022-11-04 09:10:37 +00:00
|
|
|
import { Anonymity, getRedactedCurrentLocation, IPosthogEvent, PosthogAnalytics } from "../src/PosthogAnalytics";
|
2021-08-03 10:55:02 +00:00
|
|
|
import SdkConfig from "../src/SdkConfig";
|
2022-03-23 10:27:28 +00:00
|
|
|
import { getMockClientWithEventEmitter } from "./test-utils";
|
2022-11-04 09:10:37 +00:00
|
|
|
import SettingsStore from "../src/settings/SettingsStore";
|
|
|
|
import { Layout } from "../src/settings/enums/Layout";
|
|
|
|
import defaultDispatcher from "../src/dispatcher/dispatcher";
|
|
|
|
import { Action } from "../src/dispatcher/actions";
|
|
|
|
import { SettingLevel } from "../src/settings/SettingLevel";
|
2021-08-03 10:55:02 +00:00
|
|
|
|
2022-03-23 10:27:28 +00:00
|
|
|
const getFakePosthog = (): PostHog =>
|
|
|
|
({
|
|
|
|
capture: jest.fn(),
|
|
|
|
init: jest.fn(),
|
|
|
|
identify: jest.fn(),
|
|
|
|
reset: jest.fn(),
|
|
|
|
register: jest.fn(),
|
|
|
|
} as unknown as PostHog);
|
2021-08-03 10:55:02 +00:00
|
|
|
|
2023-03-01 15:23:35 +00:00
|
|
|
interface ITestEvent extends IPosthogEvent {
|
2021-12-06 10:43:42 +00:00
|
|
|
eventName: "JestTestEvents";
|
2022-11-04 09:10:37 +00:00
|
|
|
foo?: string;
|
2021-08-03 10:55:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
describe("PosthogAnalytics", () => {
|
2022-03-23 10:27:28 +00:00
|
|
|
let fakePosthog: PostHog;
|
2023-02-13 11:39:16 +00:00
|
|
|
const shaHashes: Record<string, string> = {
|
2021-08-03 10:55:02 +00:00
|
|
|
"42": "73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049",
|
|
|
|
"some": "a6b46dd0d1ae5e86cbc8f37e75ceeb6760230c1ca4ffbcb0c97b96dd7d9c464b",
|
|
|
|
"pii": "bd75b3e080945674c0351f75e0db33d1e90986fa07b318ea7edf776f5eef38d4",
|
|
|
|
"foo": "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
|
|
|
|
};
|
|
|
|
|
|
|
|
beforeEach(() => {
|
2022-03-23 10:27:28 +00:00
|
|
|
fakePosthog = getFakePosthog();
|
2021-08-03 10:55:02 +00:00
|
|
|
|
2023-04-20 08:25:53 +00:00
|
|
|
Object.defineProperty(window, "crypto", {
|
|
|
|
value: {
|
|
|
|
subtle: {
|
|
|
|
digest: async (_: AlgorithmIdentifier, encodedMessage: BufferSource) => {
|
|
|
|
const message = new TextDecoder().decode(encodedMessage);
|
|
|
|
const hexHash = shaHashes[message];
|
|
|
|
const bytes: number[] = [];
|
|
|
|
for (let c = 0; c < hexHash.length; c += 2) {
|
|
|
|
bytes.push(parseInt(hexHash.slice(c, c + 2), 16));
|
|
|
|
}
|
|
|
|
return bytes;
|
|
|
|
},
|
2021-08-03 10:55:02 +00:00
|
|
|
},
|
2023-04-20 08:25:53 +00:00
|
|
|
},
|
|
|
|
});
|
2021-08-03 10:55:02 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
afterEach(() => {
|
2023-04-20 08:25:53 +00:00
|
|
|
Object.defineProperty(window, "crypto", {
|
|
|
|
value: null,
|
|
|
|
});
|
2022-03-18 16:12:36 +00:00
|
|
|
SdkConfig.unset(); // we touch the config, so clean up
|
2021-08-03 10:55:02 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
describe("Initialisation", () => {
|
|
|
|
it("Should not be enabled without config being set", () => {
|
2022-03-18 16:12:36 +00:00
|
|
|
// force empty/invalid state for posthog options
|
|
|
|
SdkConfig.put({ brand: "Testing" });
|
2021-08-03 10:55:02 +00:00
|
|
|
const analytics = new PosthogAnalytics(fakePosthog);
|
|
|
|
expect(analytics.isEnabled()).toBe(false);
|
|
|
|
});
|
|
|
|
|
|
|
|
it("Should be enabled if config is set", () => {
|
2022-03-18 16:12:36 +00:00
|
|
|
SdkConfig.put({
|
|
|
|
brand: "Testing",
|
2021-08-03 10:55:02 +00:00
|
|
|
posthog: {
|
2022-03-18 16:12:36 +00:00
|
|
|
project_api_key: "foo",
|
|
|
|
api_host: "bar",
|
2021-08-03 10:55:02 +00:00
|
|
|
},
|
|
|
|
});
|
|
|
|
const analytics = new PosthogAnalytics(fakePosthog);
|
|
|
|
analytics.setAnonymity(Anonymity.Pseudonymous);
|
|
|
|
expect(analytics.isEnabled()).toBe(true);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe("Tracking", () => {
|
|
|
|
let analytics: PosthogAnalytics;
|
|
|
|
|
|
|
|
beforeEach(() => {
|
2022-03-18 16:12:36 +00:00
|
|
|
SdkConfig.put({
|
|
|
|
brand: "Testing",
|
2021-08-03 10:55:02 +00:00
|
|
|
posthog: {
|
2022-03-18 16:12:36 +00:00
|
|
|
project_api_key: "foo",
|
|
|
|
api_host: "bar",
|
2021-08-03 10:55:02 +00:00
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
analytics = new PosthogAnalytics(fakePosthog);
|
|
|
|
});
|
|
|
|
|
2021-12-06 10:43:42 +00:00
|
|
|
it("Should pass event to posthog", () => {
|
2021-08-03 10:55:02 +00:00
|
|
|
analytics.setAnonymity(Anonymity.Pseudonymous);
|
2021-12-06 10:43:42 +00:00
|
|
|
analytics.trackEvent<ITestEvent>({
|
|
|
|
eventName: "JestTestEvents",
|
2021-08-03 10:55:02 +00:00
|
|
|
foo: "bar",
|
|
|
|
});
|
2022-03-23 10:27:28 +00:00
|
|
|
expect(mocked(fakePosthog).capture.mock.calls[0][0]).toBe("JestTestEvents");
|
2023-02-16 09:38:44 +00:00
|
|
|
expect(mocked(fakePosthog).capture.mock.calls[0][1]!["foo"]).toEqual("bar");
|
2021-08-03 10:55:02 +00:00
|
|
|
});
|
|
|
|
|
2021-12-06 10:43:42 +00:00
|
|
|
it("Should not track events if anonymous", async () => {
|
2021-08-03 10:55:02 +00:00
|
|
|
analytics.setAnonymity(Anonymity.Anonymous);
|
2021-12-06 10:43:42 +00:00
|
|
|
await analytics.trackEvent<ITestEvent>({
|
|
|
|
eventName: "JestTestEvents",
|
2021-08-03 10:55:02 +00:00
|
|
|
foo: "bar",
|
|
|
|
});
|
2022-03-23 10:27:28 +00:00
|
|
|
expect(fakePosthog.capture).not.toHaveBeenCalled();
|
2021-08-03 10:55:02 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it("Should not track any events if disabled", async () => {
|
|
|
|
analytics.setAnonymity(Anonymity.Disabled);
|
2021-12-06 10:43:42 +00:00
|
|
|
analytics.trackEvent<ITestEvent>({
|
|
|
|
eventName: "JestTestEvents",
|
2021-08-03 10:55:02 +00:00
|
|
|
foo: "bar",
|
|
|
|
});
|
2022-03-23 10:27:28 +00:00
|
|
|
expect(fakePosthog.capture).not.toHaveBeenCalled();
|
2021-08-03 10:55:02 +00:00
|
|
|
});
|
|
|
|
|
2021-12-06 10:43:42 +00:00
|
|
|
it("Should anonymise location of a known screen", async () => {
|
|
|
|
const location = getRedactedCurrentLocation("https://foo.bar", "#/register/some/pii", "/");
|
2021-09-15 12:55:34 +00:00
|
|
|
expect(location).toBe("https://foo.bar/#/register/<redacted>");
|
2021-08-03 10:55:02 +00:00
|
|
|
});
|
|
|
|
|
2021-12-06 10:43:42 +00:00
|
|
|
it("Should anonymise location of an unknown screen", async () => {
|
|
|
|
const location = getRedactedCurrentLocation("https://foo.bar", "#/not_a_screen_name/some/pii", "/");
|
2021-09-15 12:55:34 +00:00
|
|
|
expect(location).toBe("https://foo.bar/#/<redacted_screen_name>/<redacted>");
|
2021-08-03 10:55:02 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it("Should handle an empty hash", async () => {
|
2021-12-06 10:43:42 +00:00
|
|
|
const location = getRedactedCurrentLocation("https://foo.bar", "", "/");
|
2021-08-03 10:55:02 +00:00
|
|
|
expect(location).toBe("https://foo.bar/");
|
|
|
|
});
|
|
|
|
|
|
|
|
it("Should identify the user to posthog if pseudonymous", async () => {
|
|
|
|
analytics.setAnonymity(Anonymity.Pseudonymous);
|
2022-03-23 10:27:28 +00:00
|
|
|
const client = getMockClientWithEventEmitter({
|
|
|
|
getAccountDataFromServer: jest.fn().mockResolvedValue(null),
|
|
|
|
setAccountData: jest.fn().mockResolvedValue({}),
|
|
|
|
});
|
|
|
|
await analytics.identifyUser(client, () => "analytics_id");
|
|
|
|
expect(mocked(fakePosthog).identify.mock.calls[0][0]).toBe("analytics_id");
|
2021-08-03 10:55:02 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it("Should not identify the user to posthog if anonymous", async () => {
|
|
|
|
analytics.setAnonymity(Anonymity.Anonymous);
|
2022-03-23 10:27:28 +00:00
|
|
|
const client = getMockClientWithEventEmitter({});
|
|
|
|
await analytics.identifyUser(client, () => "analytics_id");
|
|
|
|
expect(mocked(fakePosthog).identify.mock.calls.length).toBe(0);
|
2021-08-03 10:55:02 +00:00
|
|
|
});
|
2021-09-14 14:53:33 +00:00
|
|
|
|
|
|
|
it("Should identify using the server's analytics id if present", async () => {
|
|
|
|
analytics.setAnonymity(Anonymity.Pseudonymous);
|
2022-03-23 10:27:28 +00:00
|
|
|
|
|
|
|
const client = getMockClientWithEventEmitter({
|
|
|
|
getAccountDataFromServer: jest.fn().mockResolvedValue({ id: "existing_analytics_id" }),
|
|
|
|
setAccountData: jest.fn().mockResolvedValue({}),
|
|
|
|
});
|
|
|
|
await analytics.identifyUser(client, () => "new_analytics_id");
|
|
|
|
expect(mocked(fakePosthog).identify.mock.calls[0][0]).toBe("existing_analytics_id");
|
2021-09-14 14:53:33 +00:00
|
|
|
});
|
2021-08-03 10:55:02 +00:00
|
|
|
});
|
2022-11-04 09:10:37 +00:00
|
|
|
|
|
|
|
describe("WebLayout", () => {
|
|
|
|
let analytics: PosthogAnalytics;
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
SdkConfig.put({
|
|
|
|
brand: "Testing",
|
|
|
|
posthog: {
|
|
|
|
project_api_key: "foo",
|
|
|
|
api_host: "bar",
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
analytics = new PosthogAnalytics(fakePosthog);
|
|
|
|
analytics.setAnonymity(Anonymity.Pseudonymous);
|
|
|
|
});
|
|
|
|
|
|
|
|
it("should send layout IRC correctly", async () => {
|
|
|
|
await SettingsStore.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
|
|
|
|
defaultDispatcher.dispatch(
|
|
|
|
{
|
|
|
|
action: Action.SettingUpdated,
|
|
|
|
settingName: "layout",
|
|
|
|
},
|
|
|
|
true,
|
|
|
|
);
|
|
|
|
analytics.trackEvent<ITestEvent>({
|
|
|
|
eventName: "JestTestEvents",
|
|
|
|
});
|
2023-02-16 09:38:44 +00:00
|
|
|
expect(mocked(fakePosthog).capture.mock.calls[0][1]!["$set"]).toStrictEqual({
|
2022-11-04 09:10:37 +00:00
|
|
|
WebLayout: "IRC",
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it("should send layout Bubble correctly", async () => {
|
|
|
|
await SettingsStore.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble);
|
|
|
|
defaultDispatcher.dispatch(
|
|
|
|
{
|
|
|
|
action: Action.SettingUpdated,
|
|
|
|
settingName: "layout",
|
|
|
|
},
|
|
|
|
true,
|
|
|
|
);
|
|
|
|
analytics.trackEvent<ITestEvent>({
|
|
|
|
eventName: "JestTestEvents",
|
|
|
|
});
|
2023-02-16 09:38:44 +00:00
|
|
|
expect(mocked(fakePosthog).capture.mock.calls[0][1]!["$set"]).toStrictEqual({
|
2022-11-04 09:10:37 +00:00
|
|
|
WebLayout: "Bubble",
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it("should send layout Group correctly", async () => {
|
|
|
|
await SettingsStore.setValue("layout", null, SettingLevel.DEVICE, Layout.Group);
|
|
|
|
defaultDispatcher.dispatch(
|
|
|
|
{
|
|
|
|
action: Action.SettingUpdated,
|
|
|
|
settingName: "layout",
|
|
|
|
},
|
|
|
|
true,
|
|
|
|
);
|
|
|
|
analytics.trackEvent<ITestEvent>({
|
|
|
|
eventName: "JestTestEvents",
|
|
|
|
});
|
2023-02-16 09:38:44 +00:00
|
|
|
expect(mocked(fakePosthog).capture.mock.calls[0][1]!["$set"]).toStrictEqual({
|
2022-11-04 09:10:37 +00:00
|
|
|
WebLayout: "Group",
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it("should send layout Compact correctly", async () => {
|
|
|
|
await SettingsStore.setValue("layout", null, SettingLevel.DEVICE, Layout.Group);
|
|
|
|
await SettingsStore.setValue("useCompactLayout", null, SettingLevel.DEVICE, true);
|
|
|
|
defaultDispatcher.dispatch(
|
|
|
|
{
|
|
|
|
action: Action.SettingUpdated,
|
|
|
|
settingName: "useCompactLayout",
|
|
|
|
},
|
|
|
|
true,
|
|
|
|
);
|
|
|
|
analytics.trackEvent<ITestEvent>({
|
|
|
|
eventName: "JestTestEvents",
|
|
|
|
});
|
2023-02-16 09:38:44 +00:00
|
|
|
expect(mocked(fakePosthog).capture.mock.calls[0][1]!["$set"]).toStrictEqual({
|
2022-11-04 09:10:37 +00:00
|
|
|
WebLayout: "Compact",
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
2021-08-03 10:55:02 +00:00
|
|
|
});
|