Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit a37682e

Browse files
author
Kerry Archibald
committed
record device client inforamtion events on app start
1 parent 12e3ba8 commit a37682e

File tree

4 files changed

+217
-0
lines changed

4 files changed

+217
-0
lines changed

src/DeviceListener.ts

+19
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ import { isSecureBackupRequired } from './utils/WellKnownUtils';
4040
import { ActionPayload } from "./dispatcher/payloads";
4141
import { Action } from "./dispatcher/actions";
4242
import { isLoggedIn } from "./utils/login";
43+
import SdkConfig from "./SdkConfig";
44+
import PlatformPeg from "./PlatformPeg";
45+
import { recordClientInformation } from "./utils/device/clientInformation";
4346

4447
const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
4548

@@ -78,6 +81,7 @@ export default class DeviceListener {
7881
MatrixClientPeg.get().on(RoomStateEvent.Events, this.onRoomStateEvents);
7982
this.dispatcherRef = dis.register(this.onAction);
8083
this.recheck();
84+
this.recordClientInformation();
8185
}
8286

8387
public stop() {
@@ -200,6 +204,7 @@ export default class DeviceListener {
200204
private onAction = ({ action }: ActionPayload) => {
201205
if (action !== Action.OnLoggedIn) return;
202206
this.recheck();
207+
this.recordClientInformation();
203208
};
204209

205210
// The server doesn't tell us when key backup is set up, so we poll
@@ -343,4 +348,18 @@ export default class DeviceListener {
343348
dis.dispatch({ action: Action.ReportKeyBackupNotEnabled });
344349
}
345350
};
351+
352+
private recordClientInformation = async () => {
353+
try {
354+
await recordClientInformation(
355+
MatrixClientPeg.get(),
356+
SdkConfig.get(),
357+
PlatformPeg.get(),
358+
);
359+
} catch (error) {
360+
// this is a best effort operation
361+
// log the error without rethrowing
362+
logger.error('Failed to record client information', error);
363+
}
364+
};
346365
}

src/utils/device/clientInformation.ts

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { MatrixClient } from "matrix-js-sdk/src/client";
18+
19+
import BasePlatform from "../../BasePlatform";
20+
import { IConfigOptions } from "../../IConfigOptions";
21+
22+
export type DeviceClientInformation = {
23+
name?: string;
24+
version?: string;
25+
url?: string;
26+
};
27+
28+
const formatUrl = (): string | undefined => {
29+
// don't record url for electron clients
30+
if (window.electron) {
31+
return undefined;
32+
}
33+
34+
// strip query-string and fragment from uri
35+
const url = new URL(window.location.href);
36+
37+
return [
38+
url.host,
39+
url.pathname.replace(/\/$/, ""), // Remove trailing slash if present
40+
].join("");
41+
};
42+
43+
export const getClientInformationEventType = (deviceId: string): string =>
44+
`io.element.matrix-client-information.${deviceId}`;
45+
46+
export const recordClientInformation = async (
47+
matrixClient: MatrixClient,
48+
sdkConfig: IConfigOptions,
49+
platform: BasePlatform,
50+
): Promise<void> => {
51+
const deviceId = matrixClient.getDeviceId();
52+
const { brand } = sdkConfig;
53+
const version = await platform.getAppVersion();
54+
const type = getClientInformationEventType(deviceId);
55+
const url = formatUrl();
56+
57+
await matrixClient.setAccountData(type, {
58+
name: brand,
59+
version,
60+
url,
61+
});
62+
};

test/DeviceListener-test.ts

+50
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ limitations under the License.
1818
import { EventEmitter } from "events";
1919
import { mocked } from "jest-mock";
2020
import { Room } from "matrix-js-sdk/src/matrix";
21+
import { logger } from "matrix-js-sdk/src/logger";
2122

2223
import DeviceListener from "../src/DeviceListener";
2324
import { MatrixClientPeg } from "../src/MatrixClientPeg";
@@ -27,6 +28,7 @@ import * as BulkUnverifiedSessionsToast from "../src/toasts/BulkUnverifiedSessio
2728
import { isSecretStorageBeingAccessed } from "../src/SecurityManager";
2829
import dis from "../src/dispatcher/dispatcher";
2930
import { Action } from "../src/dispatcher/actions";
31+
import { mockPlatformPeg } from "./test-utils";
3032

3133
// don't litter test console with logs
3234
jest.mock("matrix-js-sdk/src/logger");
@@ -40,6 +42,8 @@ jest.mock("../src/SecurityManager", () => ({
4042
isSecretStorageBeingAccessed: jest.fn(), accessSecretStorage: jest.fn(),
4143
}));
4244

45+
const deviceId = 'my-device-id';
46+
4347
class MockClient extends EventEmitter {
4448
getUserId = jest.fn();
4549
getKeyBackupVersion = jest.fn().mockResolvedValue(undefined);
@@ -57,6 +61,8 @@ class MockClient extends EventEmitter {
5761
downloadKeys = jest.fn();
5862
isRoomEncrypted = jest.fn();
5963
getClientWellKnown = jest.fn();
64+
getDeviceId = jest.fn().mockReturnValue(deviceId);
65+
setAccountData = jest.fn();
6066
}
6167
const mockDispatcher = mocked(dis);
6268
const flushPromises = async () => await new Promise(process.nextTick);
@@ -75,6 +81,9 @@ describe('DeviceListener', () => {
7581

7682
beforeEach(() => {
7783
jest.resetAllMocks();
84+
mockPlatformPeg({
85+
getAppVersion: jest.fn().mockResolvedValue('1.2.3'),
86+
});
7887
mockClient = new MockClient();
7988
jest.spyOn(MatrixClientPeg, 'get').mockReturnValue(mockClient);
8089
});
@@ -86,6 +95,47 @@ describe('DeviceListener', () => {
8695
return instance;
8796
};
8897

98+
describe('client information', () => {
99+
it('saves client information on start', async () => {
100+
await createAndStart();
101+
102+
expect(mockClient.setAccountData).toHaveBeenCalledWith(
103+
`io.element.matrix-client-information.${deviceId}`,
104+
{ name: 'Element', url: 'localhost', version: '1.2.3' },
105+
);
106+
});
107+
108+
it('catches error and logs when saving client information fails', async () => {
109+
const errorLogSpy = jest.spyOn(logger, 'error');
110+
const error = new Error('oups');
111+
mockClient.setAccountData.mockRejectedValue(error);
112+
113+
// doesn't throw
114+
await createAndStart();
115+
116+
expect(errorLogSpy).toHaveBeenCalledWith(
117+
'Failed to record client information',
118+
error,
119+
);
120+
});
121+
122+
it('saves client information on logged in action', async () => {
123+
const instance = await createAndStart();
124+
125+
mockClient.setAccountData.mockClear();
126+
127+
// @ts-ignore calling private function
128+
instance.onAction({ action: Action.OnLoggedIn });
129+
130+
await flushPromises();
131+
132+
expect(mockClient.setAccountData).toHaveBeenCalledWith(
133+
`io.element.matrix-client-information.${deviceId}`,
134+
{ name: 'Element', url: 'localhost', version: '1.2.3' },
135+
);
136+
});
137+
});
138+
89139
describe('recheck', () => {
90140
it('does nothing when cross signing feature is not supported', async () => {
91141
mockClient.doesServerSupportUnstableFeature.mockResolvedValue(false);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import BasePlatform from "../../../src/BasePlatform";
18+
import { IConfigOptions } from "../../../src/IConfigOptions";
19+
import { recordClientInformation } from "../../../src/utils/device/clientInformation";
20+
import { getMockClientWithEventEmitter } from "../../test-utils";
21+
22+
describe('recordClientInformation()', () => {
23+
const deviceId = 'my-device-id';
24+
const version = '1.2.3';
25+
const isElectron = window.electron;
26+
27+
const mockClient = getMockClientWithEventEmitter({
28+
getDeviceId: jest.fn().mockReturnValue(deviceId),
29+
setAccountData: jest.fn(),
30+
});
31+
32+
const sdkConfig: IConfigOptions = {
33+
brand: 'Test Brand',
34+
element_call: { url: '' },
35+
};
36+
37+
const platform = {
38+
getAppVersion: jest.fn().mockResolvedValue(version),
39+
} as unknown as BasePlatform;
40+
41+
beforeEach(() => {
42+
jest.clearAllMocks();
43+
window.electron = false;
44+
});
45+
46+
afterAll(() => {
47+
// restore global
48+
window.electron = isElectron;
49+
});
50+
51+
it('saves client information without url for electron clients', async () => {
52+
window.electron = true;
53+
54+
await recordClientInformation(
55+
mockClient,
56+
sdkConfig,
57+
platform,
58+
);
59+
60+
expect(mockClient.setAccountData).toHaveBeenCalledWith(
61+
`io.element.matrix-client-information.${deviceId}`,
62+
{
63+
name: sdkConfig.brand,
64+
version,
65+
url: undefined,
66+
},
67+
);
68+
});
69+
70+
it('saves client information with url for non-electron clients', async () => {
71+
await recordClientInformation(
72+
mockClient,
73+
sdkConfig,
74+
platform,
75+
);
76+
77+
expect(mockClient.setAccountData).toHaveBeenCalledWith(
78+
`io.element.matrix-client-information.${deviceId}`,
79+
{
80+
name: sdkConfig.brand,
81+
version,
82+
url: 'localhost',
83+
},
84+
);
85+
});
86+
});

0 commit comments

Comments
 (0)