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

Device manager - parse user agent for device information #9352

Merged
merged 37 commits into from
Oct 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
a37682e
record device client inforamtion events on app start
Sep 23, 2022
94b027b
matrix-client-information -> matrix_client_information
Sep 26, 2022
593465a
fix types
Sep 26, 2022
3f616af
Merge branch 'develop' into psg-633/save-device-client-info
Sep 26, 2022
c858656
remove another unused export
Sep 26, 2022
950f61c
add docs link
Sep 27, 2022
fb95964
Merge branch 'develop' into psg-633/save-device-client-info
Sep 27, 2022
258d364
display device client information in device details
Sep 23, 2022
a3cf263
update snapshots
Sep 23, 2022
d316ec6
integration-ish test client information in metadata
Sep 23, 2022
f24e983
tests
Sep 23, 2022
e79934f
fix tests
Sep 26, 2022
9e86e9e
export helper
Sep 26, 2022
ad81d2c
DeviceClientInformation type
Sep 26, 2022
b97ff34
Merge branch 'develop' into psg-682/display-client-info
Sep 30, 2022
d1e3b73
Merge branch 'develop' into psg-682/display-client-info
Oct 4, 2022
0e273d9
Device manager - select all devices (#9330)
Oct 4, 2022
ae76c77
rename type
Oct 4, 2022
295c6d7
use ExtendedDevice type everywhere
Oct 4, 2022
b733f61
rename clientName to appName for less collision with UA parser
Oct 4, 2022
605019c
fix bad find and replace
Oct 4, 2022
0bec4bd
Merge branch 'psg-632/device-man-type-shuffle' into psg-632/device-ma…
Oct 4, 2022
b8c3ea1
rename ExtendedDeviceInfo to ExtendedDeviceAppInfo
Oct 4, 2022
a01c3ca
Merge branch 'psg-632/device-man-type-shuffle' into psg-632/device-ma…
Oct 4, 2022
2569c0c
rename DeviceType comp to DeviceTypeIcon
Oct 4, 2022
82dbfb6
update tests for new required property deviceType
Oct 4, 2022
b10c197
add stubbed user agent parsing
Oct 4, 2022
680fb21
Merge branch 'develop' into psg-682/display-client-info
Oct 4, 2022
eef014a
Merge branch 'psg-682/display-client-info' into psg-632/device-man-pa…
Oct 4, 2022
a7a878a
setup test cases
Oct 4, 2022
7f82b8f
detect device type correctly
Oct 4, 2022
f942ab8
80% working ua parser
Oct 4, 2022
6766985
Merge branch 'develop' into psg-632/device-man-parse-ua
Oct 4, 2022
ebf3da9
Merge branch 'psg-632/device-man-parse-ua' into psg-632/dm-ua-parser-…
Oct 4, 2022
83d2f93
parse asera gents for device info
Oct 5, 2022
4b5fa8e
combine clientName/Version into one field, remove debug from tests
Oct 5, 2022
16c292e
Merge branch 'develop' into psg-632/dm-ua-parser-bowser
Oct 5, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(' ');
Comment on lines +84 to +85
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const concatenateNameAndVersion = (name?: string, version?: string): string | undefined =>
name && [name, version].filter(Boolean).join(' ');
const concatenateNameAndVersion = (name?: string, version?: string): string | undefined => {
return 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 = [
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// 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