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
This commit is contained in:
parent
191b0a1517
commit
3e4f3152bc
4 changed files with 207 additions and 7 deletions
|
@ -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"
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue