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

Commit 3e4f315

Browse files
author
Kerry
authored
Device manager - parse user agent for device information (#9352)
* record device client inforamtion events on app start * matrix-client-information -> matrix_client_information * fix types * remove another unused export * add docs link * display device client information in device details * update snapshots * integration-ish test client information in metadata * tests * fix tests * export helper * DeviceClientInformation type * Device manager - select all devices (#9330) * add device selection that does nothing * multi select and sign out of sessions * test multiple selection * fix type after rebase * select all sessions * rename type * use ExtendedDevice type everywhere * rename clientName to appName for less collision with UA parser * fix bad find and replace * rename ExtendedDeviceInfo to ExtendedDeviceAppInfo * rename DeviceType comp to DeviceTypeIcon * update tests for new required property deviceType * add stubbed user agent parsing * setup test cases * detect device type correctly * 80% working ua parser * parse asera gents for device info * combine clientName/Version into one field, remove debug from tests
1 parent 191b0a1 commit 3e4f315

File tree

4 files changed

+207
-7
lines changed

4 files changed

+207
-7
lines changed

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"@sentry/browser": "^6.11.0",
6262
"@sentry/tracing": "^6.11.0",
6363
"@types/geojson": "^7946.0.8",
64+
"@types/ua-parser-js": "^0.7.36",
6465
"await-lock": "^2.1.0",
6566
"blurhash": "^1.1.3",
6667
"browser-request": "^0.3.3",
@@ -112,6 +113,7 @@
112113
"rfc4648": "^1.4.0",
113114
"sanitize-html": "^2.3.2",
114115
"tar-js": "^0.3.0",
116+
"ua-parser-js": "^1.0.2",
115117
"url": "^0.11.0",
116118
"what-input": "^5.2.10",
117119
"zxcvbn": "^4.4.2"

src/utils/device/parseUserAgent.ts

+74-6
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17+
import UAParser from 'ua-parser-js';
18+
1719
export enum DeviceType {
1820
Desktop = 'Desktop',
1921
Mobile = 'Mobile',
@@ -26,20 +28,86 @@ export type ExtendedDeviceInformation = {
2628
deviceModel?: string;
2729
// eg Android 11
2830
deviceOperatingSystem?: string;
29-
// eg Firefox
30-
clientName?: string;
31-
// eg 1.1.0
32-
clientVersion?: string;
31+
// eg Firefox 1.1.0
32+
client?: string;
33+
};
34+
35+
// Element/1.8.21 (iPhone XS Max; iOS 15.2; Scale/3.00)
36+
const IOS_KEYWORD = "; iOS ";
37+
const BROWSER_KEYWORD = "Mozilla/";
38+
39+
const getDeviceType = (
40+
userAgent: string,
41+
device: UAParser.IDevice,
42+
browser: UAParser.IBrowser,
43+
operatingSystem: UAParser.IOS,
44+
): DeviceType => {
45+
if (browser.name === 'Electron') {
46+
return DeviceType.Desktop;
47+
}
48+
if (!!browser.name) {
49+
return DeviceType.Web;
50+
}
51+
if (
52+
device.type === 'mobile' ||
53+
operatingSystem.name?.includes('Android') ||
54+
userAgent.indexOf(IOS_KEYWORD) > -1
55+
) {
56+
return DeviceType.Mobile;
57+
}
58+
return DeviceType.Unknown;
59+
};
60+
61+
/**
62+
* Some mobile model and OS strings are not recognised
63+
* by the UA parsing library
64+
* check they exist by hand
65+
*/
66+
const checkForCustomValues = (userAgent: string): {
67+
customDeviceModel?: string;
68+
customDeviceOS?: string;
69+
} => {
70+
if (userAgent.includes(BROWSER_KEYWORD)) {
71+
return {};
72+
}
73+
74+
const mightHaveDevice = userAgent.includes('(');
75+
if (!mightHaveDevice) {
76+
return {};
77+
}
78+
const deviceInfoSegments = userAgent.substring(userAgent.indexOf('(') + 1).split('; ');
79+
const customDeviceModel = deviceInfoSegments[0] || undefined;
80+
const customDeviceOS = deviceInfoSegments[1] || undefined;
81+
return { customDeviceModel, customDeviceOS };
3382
};
3483

84+
const concatenateNameAndVersion = (name?: string, version?: string): string | undefined =>
85+
name && [name, version].filter(Boolean).join(' ');
86+
3587
export const parseUserAgent = (userAgent?: string): ExtendedDeviceInformation => {
3688
if (!userAgent) {
3789
return {
3890
deviceType: DeviceType.Unknown,
3991
};
4092
}
41-
// @TODO(kerrya) not yet implemented
93+
94+
const parser = new UAParser(userAgent);
95+
96+
const browser = parser.getBrowser();
97+
const device = parser.getDevice();
98+
const operatingSystem = parser.getOS();
99+
100+
const deviceOperatingSystem = concatenateNameAndVersion(operatingSystem.name, operatingSystem.version);
101+
const deviceModel = concatenateNameAndVersion(device.vendor, device.model);
102+
const client = concatenateNameAndVersion(browser.name, browser.major || browser.version);
103+
104+
const { customDeviceModel, customDeviceOS } = checkForCustomValues(userAgent);
105+
const deviceType = getDeviceType(userAgent, device, browser, operatingSystem);
106+
42107
return {
43-
deviceType: DeviceType.Unknown,
108+
deviceType,
109+
deviceModel: deviceModel || customDeviceModel,
110+
deviceOperatingSystem: deviceOperatingSystem || customDeviceOS,
111+
client,
44112
};
45113
};

test/utils/device/parseUserAgent-test.ts

+121-1
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,132 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import { DeviceType, parseUserAgent } from "../../../src/utils/device/parseUserAgent";
17+
import { DeviceType, ExtendedDeviceInformation, parseUserAgent } from "../../../src/utils/device/parseUserAgent";
18+
19+
const makeDeviceExtendedInfo = (
20+
deviceType: DeviceType,
21+
deviceModel?: string,
22+
deviceOperatingSystem?: string,
23+
clientName?: string,
24+
clientVersion?: string,
25+
): ExtendedDeviceInformation => ({
26+
deviceType,
27+
deviceModel,
28+
deviceOperatingSystem,
29+
client: clientName && [clientName, clientVersion].filter(Boolean).join(' '),
30+
});
31+
32+
/* eslint-disable max-len */
33+
const ANDROID_UA = [
34+
// New User Agent Implementation
35+
"Element dbg/1.5.0-dev (Xiaomi Mi 9T; Android 11; RKQ1.200826.002 test-keys; Flavour GooglePlay; MatrixAndroidSdk2 1.5.2)",
36+
"Element/1.5.0 (Samsung SM-G960F; Android 6.0.1; RKQ1.200826.002; Flavour FDroid; MatrixAndroidSdk2 1.5.2)",
37+
"Element/1.5.0 (Google Nexus 5; Android 7.0; RKQ1.200826.002 test test; Flavour FDroid; MatrixAndroidSdk2 1.5.2)",
38+
"Element/1.5.0 (Google (Nexus) 5; Android 7.0; RKQ1.200826.002 test test; Flavour FDroid; MatrixAndroidSdk2 1.5.2)",
39+
"Element/1.5.0 (Google (Nexus) (5); Android 7.0; RKQ1.200826.002 test test; Flavour FDroid; MatrixAndroidSdk2 1.5.2)",
40+
// Legacy User Agent Implementation
41+
"Element/1.0.0 (Linux; U; Android 6.0.1; SM-A510F Build/MMB29; Flavour GPlay; MatrixAndroidSdk2 1.0)",
42+
"Element/1.0.0 (Linux; Android 7.0; SM-G610M Build/NRD90M; Flavour GPlay; MatrixAndroidSdk2 1.0)",
43+
];
44+
45+
const ANDROID_EXPECTED_RESULT = [
46+
makeDeviceExtendedInfo(DeviceType.Mobile, "Xiaomi Mi 9T", "Android 11"),
47+
makeDeviceExtendedInfo(DeviceType.Mobile, "Samsung SM-G960F", "Android 6.0.1"),
48+
makeDeviceExtendedInfo(DeviceType.Mobile, "LG Nexus 5", "Android 7.0"),
49+
makeDeviceExtendedInfo(DeviceType.Mobile, "Google (Nexus) 5", "Android 7.0"),
50+
makeDeviceExtendedInfo(DeviceType.Mobile, "Google (Nexus) (5)", "Android 7.0"),
51+
makeDeviceExtendedInfo(DeviceType.Mobile, "Samsung SM-A510F", "Android 6.0.1"),
52+
makeDeviceExtendedInfo(DeviceType.Mobile, "Samsung SM-G610M", "Android 7.0"),
53+
];
54+
55+
const IOS_UA = [
56+
"Element/1.8.21 (iPhone; iOS 15.2; Scale/3.00)",
57+
"Element/1.8.21 (iPhone XS Max; iOS 15.2; Scale/3.00)",
58+
"Element/1.8.21 (iPad Pro (11-inch); iOS 15.2; Scale/3.00)",
59+
"Element/1.8.21 (iPad Pro (12.9-inch) (3rd generation); iOS 15.2; Scale/3.00)",
60+
];
61+
const IOS_EXPECTED_RESULT = [
62+
makeDeviceExtendedInfo(DeviceType.Mobile, "Apple iPhone", "iOS 15.2"),
63+
makeDeviceExtendedInfo(DeviceType.Mobile, "Apple iPhone XS Max", "iOS 15.2"),
64+
makeDeviceExtendedInfo(DeviceType.Mobile, "iPad Pro (11-inch)", "iOS 15.2"),
65+
makeDeviceExtendedInfo(DeviceType.Mobile, "iPad Pro (12.9-inch) (3rd generation)", "iOS 15.2"),
66+
];
67+
const DESKTOP_UA = [
68+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) ElementNightly/2022091301 Chrome/104.0.5112.102" +
69+
" Electron/20.1.1 Safari/537.36",
70+
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) ElementNightly/2022091301 Chrome/104.0.5112.102 Electron/20.1.1 Safari/537.36",
71+
];
72+
const DESKTOP_EXPECTED_RESULT = [
73+
makeDeviceExtendedInfo(DeviceType.Desktop, undefined, "Mac OS 10.15.7", "Electron", "20"),
74+
makeDeviceExtendedInfo(DeviceType.Desktop, undefined, "Windows 10", "Electron", "20"),
75+
];
76+
77+
const WEB_UA = [
78+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36",
79+
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36",
80+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:39.0) Gecko/20100101 Firefox/39.0",
81+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/600.3.18 (KHTML, like Gecko) Version/8.0.3 Safari/600.3.18",
82+
"Mozilla/5.0 (Windows NT 6.0; rv:40.0) Gecko/20100101 Firefox/40.0",
83+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246",
84+
// using mobile browser
85+
"Mozilla/5.0 (iPad; CPU OS 8_4_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12H321 Safari/600.1.4",
86+
"Mozilla/5.0 (iPhone; CPU iPhone OS 8_4_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12H321 Safari/600.1.4",
87+
"Mozilla/5.0 (Linux; Android 9; SM-G973U Build/PPR1.180610.011) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
88+
];
89+
90+
const WEB_EXPECTED_RESULT = [
91+
makeDeviceExtendedInfo(DeviceType.Web, undefined, "Mac OS 10.15.7", "Chrome", "104"),
92+
makeDeviceExtendedInfo(DeviceType.Web, undefined, "Windows 10", "Chrome", "104"),
93+
makeDeviceExtendedInfo(DeviceType.Web, undefined, "Mac OS 10.10", "Firefox", "39"),
94+
makeDeviceExtendedInfo(DeviceType.Web, undefined, "Mac OS 10.10.2", "Safari", "8"),
95+
makeDeviceExtendedInfo(DeviceType.Web, undefined, "Windows Vista", "Firefox", "40"),
96+
makeDeviceExtendedInfo(DeviceType.Web, undefined, "Windows 10", "Edge", "12"),
97+
// using mobile browser
98+
makeDeviceExtendedInfo(DeviceType.Web, "Apple iPad", "iOS 8.4.1", "Mobile Safari", "8"),
99+
makeDeviceExtendedInfo(DeviceType.Web, "Apple iPhone", "iOS 8.4.1", "Mobile Safari", "8"),
100+
makeDeviceExtendedInfo(DeviceType.Web, "Samsung SM-G973U", "Android 9", "Chrome", "69"),
101+
102+
];
103+
104+
const MISC_UA = [
105+
"AppleTV11,1/11.1",
106+
"Curl Client/1.0",
107+
"banana",
108+
"",
109+
];
110+
111+
const MISC_EXPECTED_RESULT = [
112+
makeDeviceExtendedInfo(DeviceType.Unknown, "Apple Apple TV", undefined, undefined, undefined),
113+
makeDeviceExtendedInfo(DeviceType.Unknown, undefined, undefined, undefined, undefined),
114+
makeDeviceExtendedInfo(DeviceType.Unknown, undefined, undefined, undefined, undefined),
115+
makeDeviceExtendedInfo(DeviceType.Unknown, undefined, undefined, undefined, undefined),
116+
];
117+
/* eslint-disable max-len */
18118

19119
describe('parseUserAgent()', () => {
20120
it('returns deviceType unknown when user agent is falsy', () => {
21121
expect(parseUserAgent(undefined)).toEqual({
22122
deviceType: DeviceType.Unknown,
23123
});
24124
});
125+
126+
type TestCase = [string, ExtendedDeviceInformation];
127+
128+
const testPlatform = (platform: string, userAgents: string[], results: ExtendedDeviceInformation[]): void => {
129+
const testCases: TestCase[] = userAgents.map((userAgent, index) => [userAgent, results[index]]);
130+
131+
describe(platform, () => {
132+
it.each(
133+
testCases,
134+
)('Parses user agent correctly - %s', (userAgent, expectedResult) => {
135+
expect(parseUserAgent(userAgent)).toEqual(expectedResult);
136+
});
137+
});
138+
};
139+
140+
testPlatform('Android', ANDROID_UA, ANDROID_EXPECTED_RESULT);
141+
testPlatform('iOS', IOS_UA, IOS_EXPECTED_RESULT);
142+
testPlatform('Desktop', DESKTOP_UA, DESKTOP_EXPECTED_RESULT);
143+
testPlatform('Web', WEB_UA, WEB_EXPECTED_RESULT);
144+
testPlatform('Misc', MISC_UA, MISC_EXPECTED_RESULT);
25145
});

yarn.lock

+10
Original file line numberDiff line numberDiff line change
@@ -2325,6 +2325,11 @@
23252325
dependencies:
23262326
"@types/jest" "*"
23272327

2328+
"@types/ua-parser-js@^0.7.36":
2329+
version "0.7.36"
2330+
resolved "https://registry.yarnpkg.com/@types/ua-parser-js/-/ua-parser-js-0.7.36.tgz#9bd0b47f26b5a3151be21ba4ce9f5fa457c5f190"
2331+
integrity sha512-N1rW+njavs70y2cApeIw1vLMYXRwfBy+7trgavGuuTfOd7j1Yh7QTRc/yqsPl6ncokt72ZXuxEU0PiCp9bSwNQ==
2332+
23282333
"@types/yargs-parser@*":
23292334
version "21.0.0"
23302335
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b"
@@ -9197,6 +9202,11 @@ ua-parser-js@^0.7.30:
91979202
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.31.tgz#649a656b191dffab4f21d5e053e27ca17cbff5c6"
91989203
integrity sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==
91999204

9205+
ua-parser-js@^1.0.2:
9206+
version "1.0.2"
9207+
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.2.tgz#e2976c34dbfb30b15d2c300b2a53eac87c57a775"
9208+
integrity sha512-00y/AXhx0/SsnI51fTc0rLRmafiGOM4/O+ny10Ps7f+j/b8p/ZY11ytMgznXkOVo4GQ+KwQG5UQLkLGirsACRg==
9209+
92009210
unbox-primitive@^1.0.2:
92019211
version "1.0.2"
92029212
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e"

0 commit comments

Comments
 (0)