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
pull/28217/head
Kerry 2022-10-06 10:06:29 +02:00 committed by GitHub
parent 191b0a1517
commit 3e4f3152bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 207 additions and 7 deletions

View File

@ -61,6 +61,7 @@
"@sentry/browser": "^6.11.0", "@sentry/browser": "^6.11.0",
"@sentry/tracing": "^6.11.0", "@sentry/tracing": "^6.11.0",
"@types/geojson": "^7946.0.8", "@types/geojson": "^7946.0.8",
"@types/ua-parser-js": "^0.7.36",
"await-lock": "^2.1.0", "await-lock": "^2.1.0",
"blurhash": "^1.1.3", "blurhash": "^1.1.3",
"browser-request": "^0.3.3", "browser-request": "^0.3.3",
@ -112,6 +113,7 @@
"rfc4648": "^1.4.0", "rfc4648": "^1.4.0",
"sanitize-html": "^2.3.2", "sanitize-html": "^2.3.2",
"tar-js": "^0.3.0", "tar-js": "^0.3.0",
"ua-parser-js": "^1.0.2",
"url": "^0.11.0", "url": "^0.11.0",
"what-input": "^5.2.10", "what-input": "^5.2.10",
"zxcvbn": "^4.4.2" "zxcvbn": "^4.4.2"

View File

@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import UAParser from 'ua-parser-js';
export enum DeviceType { export enum DeviceType {
Desktop = 'Desktop', Desktop = 'Desktop',
Mobile = 'Mobile', Mobile = 'Mobile',
@ -26,20 +28,86 @@ export type ExtendedDeviceInformation = {
deviceModel?: string; deviceModel?: string;
// eg Android 11 // eg Android 11
deviceOperatingSystem?: string; deviceOperatingSystem?: string;
// eg Firefox // eg Firefox 1.1.0
clientName?: string; client?: string;
// eg 1.1.0
clientVersion?: 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 => { export const parseUserAgent = (userAgent?: string): ExtendedDeviceInformation => {
if (!userAgent) { if (!userAgent) {
return { return {
deviceType: DeviceType.Unknown, 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 { return {
deviceType: DeviceType.Unknown, deviceType,
deviceModel: deviceModel || customDeviceModel,
deviceOperatingSystem: deviceOperatingSystem || customDeviceOS,
client,
}; };
}; };

View File

@ -14,7 +14,107 @@ See the License for the specific language governing permissions and
limitations under the License. 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()', () => { describe('parseUserAgent()', () => {
it('returns deviceType unknown when user agent is falsy', () => { it('returns deviceType unknown when user agent is falsy', () => {
@ -22,4 +122,24 @@ describe('parseUserAgent()', () => {
deviceType: DeviceType.Unknown, 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);
}); });

View File

@ -2325,6 +2325,11 @@
dependencies: dependencies:
"@types/jest" "*" "@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@*": "@types/yargs-parser@*":
version "21.0.0" version "21.0.0"
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" 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:
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.31.tgz#649a656b191dffab4f21d5e053e27ca17cbff5c6" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.31.tgz#649a656b191dffab4f21d5e053e27ca17cbff5c6"
integrity sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ== 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: unbox-primitive@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e"