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

Commit

Permalink
Device manager - parse user agent for device information (#9352)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Kerry authored Oct 6, 2022
1 parent 191b0a1 commit 3e4f315
Show file tree
Hide file tree
Showing 4 changed files with 207 additions and 7 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"@sentry/browser": "^6.11.0",
"@sentry/tracing": "^6.11.0",
"@types/geojson": "^7946.0.8",
"@types/ua-parser-js": "^0.7.36",
"await-lock": "^2.1.0",
"blurhash": "^1.1.3",
"browser-request": "^0.3.3",
Expand Down Expand Up @@ -112,6 +113,7 @@
"rfc4648": "^1.4.0",
"sanitize-html": "^2.3.2",
"tar-js": "^0.3.0",
"ua-parser-js": "^1.0.2",
"url": "^0.11.0",
"what-input": "^5.2.10",
"zxcvbn": "^4.4.2"
Expand Down
80 changes: 74 additions & 6 deletions src/utils/device/parseUserAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import UAParser from 'ua-parser-js';

export enum DeviceType {
Desktop = 'Desktop',
Mobile = 'Mobile',
Expand All @@ -26,20 +28,86 @@ export type ExtendedDeviceInformation = {
deviceModel?: string;
// eg Android 11
deviceOperatingSystem?: string;
// eg Firefox
clientName?: string;
// eg 1.1.0
clientVersion?: string;
// eg Firefox 1.1.0
client?: string;
};

// Element/1.8.21 (iPhone XS Max; iOS 15.2; Scale/3.00)
const IOS_KEYWORD = "; iOS ";
const BROWSER_KEYWORD = "Mozilla/";

const getDeviceType = (
userAgent: string,
device: UAParser.IDevice,
browser: UAParser.IBrowser,
operatingSystem: UAParser.IOS,
): DeviceType => {
if (browser.name === 'Electron') {
return DeviceType.Desktop;
}
if (!!browser.name) {
return DeviceType.Web;
}
if (
device.type === 'mobile' ||
operatingSystem.name?.includes('Android') ||
userAgent.indexOf(IOS_KEYWORD) > -1
) {
return DeviceType.Mobile;
}
return DeviceType.Unknown;
};

/**
* Some mobile model and OS strings are not recognised
* by the UA parsing library
* check they exist by hand
*/
const checkForCustomValues = (userAgent: string): {
customDeviceModel?: string;
customDeviceOS?: string;
} => {
if (userAgent.includes(BROWSER_KEYWORD)) {
return {};
}

const mightHaveDevice = userAgent.includes('(');
if (!mightHaveDevice) {
return {};
}
const deviceInfoSegments = userAgent.substring(userAgent.indexOf('(') + 1).split('; ');
const customDeviceModel = deviceInfoSegments[0] || undefined;
const customDeviceOS = deviceInfoSegments[1] || undefined;
return { customDeviceModel, customDeviceOS };
};

const concatenateNameAndVersion = (name?: string, version?: string): string | undefined =>
name && [name, version].filter(Boolean).join(' ');

export const parseUserAgent = (userAgent?: string): ExtendedDeviceInformation => {
if (!userAgent) {
return {
deviceType: DeviceType.Unknown,
};
}
// @TODO(kerrya) not yet implemented

const parser = new UAParser(userAgent);

const browser = parser.getBrowser();
const device = parser.getDevice();
const operatingSystem = parser.getOS();

const deviceOperatingSystem = concatenateNameAndVersion(operatingSystem.name, operatingSystem.version);
const deviceModel = concatenateNameAndVersion(device.vendor, device.model);
const client = concatenateNameAndVersion(browser.name, browser.major || browser.version);

const { customDeviceModel, customDeviceOS } = checkForCustomValues(userAgent);
const deviceType = getDeviceType(userAgent, device, browser, operatingSystem);

return {
deviceType: DeviceType.Unknown,
deviceType,
deviceModel: deviceModel || customDeviceModel,
deviceOperatingSystem: deviceOperatingSystem || customDeviceOS,
client,
};
};
122 changes: 121 additions & 1 deletion test/utils/device/parseUserAgent-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,132 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import { DeviceType, parseUserAgent } from "../../../src/utils/device/parseUserAgent";
import { DeviceType, ExtendedDeviceInformation, parseUserAgent } from "../../../src/utils/device/parseUserAgent";

const makeDeviceExtendedInfo = (
deviceType: DeviceType,
deviceModel?: string,
deviceOperatingSystem?: string,
clientName?: string,
clientVersion?: string,
): ExtendedDeviceInformation => ({
deviceType,
deviceModel,
deviceOperatingSystem,
client: clientName && [clientName, clientVersion].filter(Boolean).join(' '),
});

/* eslint-disable max-len */
const ANDROID_UA = [
// New User Agent Implementation
"Element dbg/1.5.0-dev (Xiaomi Mi 9T; Android 11; RKQ1.200826.002 test-keys; Flavour GooglePlay; MatrixAndroidSdk2 1.5.2)",
"Element/1.5.0 (Samsung SM-G960F; Android 6.0.1; RKQ1.200826.002; Flavour FDroid; MatrixAndroidSdk2 1.5.2)",
"Element/1.5.0 (Google Nexus 5; Android 7.0; RKQ1.200826.002 test test; Flavour FDroid; MatrixAndroidSdk2 1.5.2)",
"Element/1.5.0 (Google (Nexus) 5; Android 7.0; RKQ1.200826.002 test test; Flavour FDroid; MatrixAndroidSdk2 1.5.2)",
"Element/1.5.0 (Google (Nexus) (5); Android 7.0; RKQ1.200826.002 test test; Flavour FDroid; MatrixAndroidSdk2 1.5.2)",
// Legacy User Agent Implementation
"Element/1.0.0 (Linux; U; Android 6.0.1; SM-A510F Build/MMB29; Flavour GPlay; MatrixAndroidSdk2 1.0)",
"Element/1.0.0 (Linux; Android 7.0; SM-G610M Build/NRD90M; Flavour GPlay; MatrixAndroidSdk2 1.0)",
];

const ANDROID_EXPECTED_RESULT = [
makeDeviceExtendedInfo(DeviceType.Mobile, "Xiaomi Mi 9T", "Android 11"),
makeDeviceExtendedInfo(DeviceType.Mobile, "Samsung SM-G960F", "Android 6.0.1"),
makeDeviceExtendedInfo(DeviceType.Mobile, "LG Nexus 5", "Android 7.0"),
makeDeviceExtendedInfo(DeviceType.Mobile, "Google (Nexus) 5", "Android 7.0"),
makeDeviceExtendedInfo(DeviceType.Mobile, "Google (Nexus) (5)", "Android 7.0"),
makeDeviceExtendedInfo(DeviceType.Mobile, "Samsung SM-A510F", "Android 6.0.1"),
makeDeviceExtendedInfo(DeviceType.Mobile, "Samsung SM-G610M", "Android 7.0"),
];

const IOS_UA = [
"Element/1.8.21 (iPhone; iOS 15.2; Scale/3.00)",
"Element/1.8.21 (iPhone XS Max; iOS 15.2; Scale/3.00)",
"Element/1.8.21 (iPad Pro (11-inch); iOS 15.2; Scale/3.00)",
"Element/1.8.21 (iPad Pro (12.9-inch) (3rd generation); iOS 15.2; Scale/3.00)",
];
const IOS_EXPECTED_RESULT = [
makeDeviceExtendedInfo(DeviceType.Mobile, "Apple iPhone", "iOS 15.2"),
makeDeviceExtendedInfo(DeviceType.Mobile, "Apple iPhone XS Max", "iOS 15.2"),
makeDeviceExtendedInfo(DeviceType.Mobile, "iPad Pro (11-inch)", "iOS 15.2"),
makeDeviceExtendedInfo(DeviceType.Mobile, "iPad Pro (12.9-inch) (3rd generation)", "iOS 15.2"),
];
const DESKTOP_UA = [
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) ElementNightly/2022091301 Chrome/104.0.5112.102" +
" Electron/20.1.1 Safari/537.36",
"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",
];
const DESKTOP_EXPECTED_RESULT = [
makeDeviceExtendedInfo(DeviceType.Desktop, undefined, "Mac OS 10.15.7", "Electron", "20"),
makeDeviceExtendedInfo(DeviceType.Desktop, undefined, "Windows 10", "Electron", "20"),
];

const WEB_UA = [
"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",
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:39.0) Gecko/20100101 Firefox/39.0",
"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",
"Mozilla/5.0 (Windows NT 6.0; rv:40.0) Gecko/20100101 Firefox/40.0",
"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",
// using mobile browser
"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",
"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",
"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",
];

const WEB_EXPECTED_RESULT = [
makeDeviceExtendedInfo(DeviceType.Web, undefined, "Mac OS 10.15.7", "Chrome", "104"),
makeDeviceExtendedInfo(DeviceType.Web, undefined, "Windows 10", "Chrome", "104"),
makeDeviceExtendedInfo(DeviceType.Web, undefined, "Mac OS 10.10", "Firefox", "39"),
makeDeviceExtendedInfo(DeviceType.Web, undefined, "Mac OS 10.10.2", "Safari", "8"),
makeDeviceExtendedInfo(DeviceType.Web, undefined, "Windows Vista", "Firefox", "40"),
makeDeviceExtendedInfo(DeviceType.Web, undefined, "Windows 10", "Edge", "12"),
// using mobile browser
makeDeviceExtendedInfo(DeviceType.Web, "Apple iPad", "iOS 8.4.1", "Mobile Safari", "8"),
makeDeviceExtendedInfo(DeviceType.Web, "Apple iPhone", "iOS 8.4.1", "Mobile Safari", "8"),
makeDeviceExtendedInfo(DeviceType.Web, "Samsung SM-G973U", "Android 9", "Chrome", "69"),

];

const MISC_UA = [
"AppleTV11,1/11.1",
"Curl Client/1.0",
"banana",
"",
];

const MISC_EXPECTED_RESULT = [
makeDeviceExtendedInfo(DeviceType.Unknown, "Apple Apple TV", undefined, undefined, undefined),
makeDeviceExtendedInfo(DeviceType.Unknown, undefined, undefined, undefined, undefined),
makeDeviceExtendedInfo(DeviceType.Unknown, undefined, undefined, undefined, undefined),
makeDeviceExtendedInfo(DeviceType.Unknown, undefined, undefined, undefined, undefined),
];
/* eslint-disable max-len */

describe('parseUserAgent()', () => {
it('returns deviceType unknown when user agent is falsy', () => {
expect(parseUserAgent(undefined)).toEqual({
deviceType: DeviceType.Unknown,
});
});

type TestCase = [string, ExtendedDeviceInformation];

const testPlatform = (platform: string, userAgents: string[], results: ExtendedDeviceInformation[]): void => {
const testCases: TestCase[] = userAgents.map((userAgent, index) => [userAgent, results[index]]);

describe(platform, () => {
it.each(
testCases,
)('Parses user agent correctly - %s', (userAgent, expectedResult) => {
expect(parseUserAgent(userAgent)).toEqual(expectedResult);
});
});
};

testPlatform('Android', ANDROID_UA, ANDROID_EXPECTED_RESULT);
testPlatform('iOS', IOS_UA, IOS_EXPECTED_RESULT);
testPlatform('Desktop', DESKTOP_UA, DESKTOP_EXPECTED_RESULT);
testPlatform('Web', WEB_UA, WEB_EXPECTED_RESULT);
testPlatform('Misc', MISC_UA, MISC_EXPECTED_RESULT);
});
10 changes: 10 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2325,6 +2325,11 @@
dependencies:
"@types/jest" "*"

"@types/ua-parser-js@^0.7.36":
version "0.7.36"
resolved "https://registry.yarnpkg.com/@types/ua-parser-js/-/ua-parser-js-0.7.36.tgz#9bd0b47f26b5a3151be21ba4ce9f5fa457c5f190"
integrity sha512-N1rW+njavs70y2cApeIw1vLMYXRwfBy+7trgavGuuTfOd7j1Yh7QTRc/yqsPl6ncokt72ZXuxEU0PiCp9bSwNQ==

"@types/yargs-parser@*":
version "21.0.0"
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b"
Expand Down Expand Up @@ -9197,6 +9202,11 @@ ua-parser-js@^0.7.30:
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.31.tgz#649a656b191dffab4f21d5e053e27ca17cbff5c6"
integrity sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==

ua-parser-js@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.2.tgz#e2976c34dbfb30b15d2c300b2a53eac87c57a775"
integrity sha512-00y/AXhx0/SsnI51fTc0rLRmafiGOM4/O+ny10Ps7f+j/b8p/ZY11ytMgznXkOVo4GQ+KwQG5UQLkLGirsACRg==

unbox-primitive@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e"
Expand Down

0 comments on commit 3e4f315

Please sign in to comment.