Skip to content

Commit

Permalink
fix: add tests for browser and browser version detection (#870)
Browse files Browse the repository at this point in the history
* chore: test browser/version code

* another failing test

* fix

* fix test cases

* fix

* fix

* fix
  • Loading branch information
pauldambra authored Oct 31, 2023
1 parent 24fcf16 commit dc19279
Show file tree
Hide file tree
Showing 2 changed files with 252 additions and 48 deletions.
229 changes: 208 additions & 21 deletions src/__tests__/utils/event-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,216 @@ import * as globals from '../../utils/globals'
jest.mock('../../utils/globals')

describe(`event-utils`, () => {
it('should have $host and $pathname in properties', () => {
const properties = _info.properties()
expect(properties['$current_url']).toBeDefined()
expect(properties['$host']).toBeDefined()
expect(properties['$pathname']).toBeDefined()
})
describe('properties', () => {
it('should have $host and $pathname in properties', () => {
const properties = _info.properties()
expect(properties['$current_url']).toBeDefined()
expect(properties['$host']).toBeDefined()
expect(properties['$pathname']).toBeDefined()
})

it('should have user agent in properties', () => {
// TS doesn't like it but we can assign userAgent
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
globals['userAgent'] = 'blah'
const properties = _info.properties()
expect(properties['$raw_user_agent']).toBe('blah')
})

it('should have user agent in properties', () => {
// TS doesn't like it but we can assign userAgent
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
globals['userAgent'] = 'blah'
const properties = _info.properties()
expect(properties['$raw_user_agent']).toBe('blah')
it('should truncate very long user agents in properties', () => {
// TS doesn't like it but we can assign userAgent
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
globals['userAgent'] = 'a'.repeat(1001)
const properties = _info.properties()
expect(properties['$raw_user_agent'].length).toBe(1000)
expect(properties['$raw_user_agent'].substring(995)).toBe('aa...')
})
})

it('should truncate very long user agents in properties', () => {
// TS doesn't like it but we can assign userAgent
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
globals['userAgent'] = 'a'.repeat(1001)
const properties = _info.properties()
expect(properties['$raw_user_agent'].length).toBe(1000)
expect(properties['$raw_user_agent'].substring(995)).toBe('aa...')
describe('user agent', () => {
// can use https://user-agents.net/ or $raw_user_agent property on events to get new test cases
const browserTestcases: {
name: string
userAgent: string
vendor: string
expectedVersion: number | null
expectedBrowser: string
}[] = [
{
name: 'Chrome 91',
userAgent:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
vendor: '',
expectedVersion: 91.0,
expectedBrowser: 'Chrome',
},
{
name: 'Firefox 89',
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
vendor: '',
expectedVersion: 89.0,
expectedBrowser: 'Firefox',
},
{
name: 'unknown browser',
userAgent: 'UnknownBrowser/5.0',
vendor: '',
expectedVersion: null,
expectedBrowser: '',
},
{
name: 'invalid chrome',
userAgent:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome Safari/537.36',
vendor: '',
expectedVersion: null,
expectedBrowser: 'Chrome',
},
{
name: 'Internet Explorer Mobile',
userAgent:
'Mozilla/5.0 (Windows Phone 8.1; ARM; Trident/7.0; Touch; rv:11.0; IEMobile/11.0; NOKIA; 909) like Gecko',
vendor: '',
expectedVersion: 11.0,
expectedBrowser: 'Internet Explorer Mobile',
},
{
name: 'Microsoft Edge 44',
userAgent:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edge/44.17763.831.0',
vendor: '',
expectedVersion: 44.17763,
expectedBrowser: 'Microsoft Edge',
},
{
name: 'Chrome 21 iOS',
userAgent:
'Mozilla/5.0 (iPhone; U; CPU iPhone OS 5_1_1 like Mac OS X; en) AppleWebKit/534.46.0 (KHTML, like Gecko) CriOS/21.0.1180.82 Mobile/9B206 Safari/7534.48.3',
vendor: '',
expectedVersion: 21.0,
expectedBrowser: 'Chrome iOS',
},
{
name: 'UC Browser',
userAgent:
'Mozilla/5.0 (Linux; U; Android 4.2.2; en-US; Micromax A116 Build/JDQ39) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 UCBrowser/10.7.5.658 U3/0.8.0 Mobile Safari/534.30',
vendor: '',
expectedVersion: 10.7,
expectedBrowser: 'UC Browser',
},
{
name: 'Safari',
userAgent:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15',
vendor: 'Apple',
expectedVersion: 17.1,
expectedBrowser: 'Safari',
},
{
name: 'Opera',
userAgent:
'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.69 Safari/537.36 OPR/34.0.2036.25',
vendor: '',
expectedVersion: 34.0,
expectedBrowser: 'Opera',
},
{
name: 'Firefox iOS',
userAgent:
'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) FxiOS/8.3b5826 Mobile/14E5239e Safari/602.1.50',
vendor: '',
expectedVersion: 8.3,
expectedBrowser: 'Firefox iOS',
},
{
name: 'Konqueror (lowercase)',
userAgent: 'Mozilla/5.0 (X11; U; U; DragonFly amd64) KIO/5.97 konqueror/22.08.0',
vendor: '',
expectedVersion: 22.08,
expectedBrowser: 'Konqueror',
},
{
name: 'Konqueror (uppercase)',
userAgent: 'Mozilla/5.0 (X11; Linux i686) KHTML/5.20 (like Gecko) Konqueror/5.20',
vendor: '',
expectedVersion: 5.2,
expectedBrowser: 'Konqueror',
},
{
name: 'BlackBerry Bold 9790',
userAgent:
'Mozilla/5.0 (BlackBerry; U; BlackBerry 9790; es) AppleWebKit/534.11+ (KHTML, like Gecko) Version/7.1.0.569 Mobile Safari/534.11+',
vendor: '',
// TODO should we match 9790 the model and not 7.1 the browser version?
expectedVersion: 9790,
expectedBrowser: 'BlackBerry',
},
{
name: 'BlackBerry BB10 version v10.1',
userAgent:
'Mozilla/5.0 (BB10; Kbd) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.1.0.1720 Mobile Safari/537.10+',
vendor: '',
expectedVersion: 10.1,
expectedBrowser: 'BlackBerry',
},
{
name: 'Android Mobile',
userAgent:
'Mozilla/5.0 (Linux; StarOS Must use __system_property_read_callback() to read; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.127 Mobile Safari/537.36',
vendor: '',
// TODO should we detect this as Chrome or Android Mobile?
expectedVersion: 100,
expectedBrowser: 'Chrome',
},
{
name: 'Samsung Internet',
userAgent:
'Mozilla/5.0 (Linux; Android 5.0.2; SAMSUNG SM-T550 Build/LRX22G) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/3.5 Chrome/38.0.2125.102 Safari/537.36',
vendor: '',
expectedVersion: 3.5,
expectedBrowser: 'Samsung Internet',
},
{
name: 'Internet Explorer',
userAgent: 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Trident/6.0)',
vendor: '',
expectedVersion: 10.0,
expectedBrowser: 'Internet Explorer',
},
{
name: 'mobile safari (with vendor)',
userAgent:
'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1',
vendor: 'Apple',
expectedVersion: 16.6,
expectedBrowser: 'Mobile Safari',
},
{
name: 'mobile safari (without vendor)',
userAgent:
'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1',
vendor: '', // vendor is deprecated, and we see this user agent not matching in the wild
expectedVersion: 16.6,
expectedBrowser: 'Mobile Safari',
},
{
name: 'firefox for ios',
userAgent:
'Mozilla/5.0 (iPhone; CPU iPhone OS 15_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/106.0 Mobile/15E148 Safari/605.1.15',
vendor: '',
expectedVersion: 106.0,
expectedBrowser: 'Firefox iOS',
},
]

test.each(browserTestcases)('browser version %s', ({ userAgent, vendor, expectedVersion }) => {
expect(_info.browserVersion(userAgent, vendor, '')).toBe(expectedVersion)
})

test.each(browserTestcases)('browser %s', ({ userAgent, vendor, expectedBrowser }) => {
expect(_info.browser(userAgent, vendor, '')).toBe(expectedBrowser)
})
})
})
71 changes: 44 additions & 27 deletions src/utils/event-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ import Config from '../config'
import { _each, _extend, _includes, _strip_empty_properties, _timestamp } from './index'
import { document, userAgent } from './globals'

/**
* Safari detection turns out to be complicted. For e.g. https://stackoverflow.com/a/29696509
* We can be slightly loose because some options have been ruled out (e.g. firefox on iOS)
* before this check is made
*/
function isSafari(userAgent: string): boolean {
return _includes(userAgent, 'Safari') && !_includes(userAgent, 'Chrome') && !_includes(userAgent, 'Android')
}

export const _info = {
campaignParams: function (customParams?: string[]): Record<string, any> {
const campaign_keywords = [
Expand Down Expand Up @@ -68,7 +77,7 @@ export const _info = {
* The order of the checks are important since many user agents
* include key words used in later checks.
*/
browser: function (user_agent: string, vendor: string, opera?: any): string {
browser: function (user_agent: string, vendor: string | undefined, opera?: any): string {
vendor = vendor || '' // vendor is undefined for at least IE9
if (opera || _includes(user_agent, ' OPR/')) {
if (_includes(user_agent, 'Mini')) {
Expand All @@ -94,14 +103,14 @@ export const _info = {
return 'UC Browser'
} else if (_includes(user_agent, 'FxiOS')) {
return 'Firefox iOS'
} else if (_includes(vendor, 'Apple')) {
} else if (_includes(vendor, 'Apple') || isSafari(user_agent)) {
if (_includes(user_agent, 'Mobile')) {
return 'Mobile Safari'
}
return 'Safari'
} else if (_includes(user_agent, 'Android')) {
return 'Android Mobile'
} else if (_includes(user_agent, 'Konqueror')) {
} else if (_includes(user_agent, 'Konqueror') || _includes(user_agent, 'konqueror')) {
return 'Konqueror'
} else if (_includes(user_agent, 'Firefox')) {
return 'Firefox'
Expand All @@ -118,36 +127,44 @@ export const _info = {
* This function detects which browser version is running this script,
* parsing major and minor version (e.g., 42.1). User agent strings from:
* http://www.useragentstring.com/pages/useragentstring.php
*
* `navigator.vendor` is passed in and used to help with detecting certain browsers
* NB `navigator.vendor` is deprecated and not present in every browser
*/
browserVersion: function (userAgent: string, vendor: string, opera: string): number | null {
browserVersion: function (userAgent: string, vendor: string | undefined, opera: string): number | null {
const browser = _info.browser(userAgent, vendor, opera)
const versionRegexs = {
'Internet Explorer Mobile': /rv:(\d+(\.\d+)?)/,
'Microsoft Edge': /Edge?\/(\d+(\.\d+)?)/,
Chrome: /Chrome\/(\d+(\.\d+)?)/,
'Chrome iOS': /CriOS\/(\d+(\.\d+)?)/,
'UC Browser': /(UCBrowser|UCWEB)\/(\d+(\.\d+)?)/,
Safari: /Version\/(\d+(\.\d+)?)/,
'Mobile Safari': /Version\/(\d+(\.\d+)?)/,
Opera: /(Opera|OPR)\/(\d+(\.\d+)?)/,
Firefox: /Firefox\/(\d+(\.\d+)?)/,
'Firefox iOS': /FxiOS\/(\d+(\.\d+)?)/,
Konqueror: /Konqueror:(\d+(\.\d+)?)/,
BlackBerry: /BlackBerry (\d+(\.\d+)?)/,
'Android Mobile': /android\s(\d+(\.\d+)?)/,
'Samsung Internet': /SamsungBrowser\/(\d+(\.\d+)?)/,
'Internet Explorer': /(rv:|MSIE )(\d+(\.\d+)?)/,
Mozilla: /rv:(\d+(\.\d+)?)/,
const versionRegexes: Record<string, RegExp[]> = {
'Internet Explorer Mobile': [/rv:(\d+(\.\d+)?)/],
'Microsoft Edge': [/Edge?\/(\d+(\.\d+)?)/],
Chrome: [/Chrome\/(\d+(\.\d+)?)/],
'Chrome iOS': [/CriOS\/(\d+(\.\d+)?)/],
'UC Browser': [/(UCBrowser|UCWEB)\/(\d+(\.\d+)?)/],
Safari: [/Version\/(\d+(\.\d+)?)/],
'Mobile Safari': [/Version\/(\d+(\.\d+)?)/],
Opera: [/(Opera|OPR)\/(\d+(\.\d+)?)/],
Firefox: [/Firefox\/(\d+(\.\d+)?)/],
'Firefox iOS': [/FxiOS\/(\d+(\.\d+)?)/],
Konqueror: [/Konqueror[:/]?(\d+(\.\d+)?)/i],
// not every blackberry user agent has the version after the name
BlackBerry: [/BlackBerry (\d+(\.\d+)?)/, /Version\/(\d+(\.\d+)?)/],
'Android Mobile': [/android\s(\d+(\.\d+)?)/],
'Samsung Internet': [/SamsungBrowser\/(\d+(\.\d+)?)/],
'Internet Explorer': [/(rv:|MSIE )(\d+(\.\d+)?)/],
Mozilla: [/rv:(\d+(\.\d+)?)/],
}
const regex: RegExp | undefined = versionRegexs[browser as keyof typeof versionRegexs]
if (_isUndefined(regex)) {
const regexes: RegExp[] | undefined = versionRegexes[browser as keyof typeof versionRegexes]
if (_isUndefined(regexes)) {
return null
}
const matches = userAgent.match(regex)
if (!matches) {
return null

for (let i = 0; i < regexes.length; i++) {
const regex = regexes[i]
const matches = userAgent.match(regex)
if (matches) {
return parseFloat(matches[matches.length - 2])
}
}
return parseFloat(matches[matches.length - 2])
return null
},

browserLanguage: function (): string {
Expand Down

0 comments on commit dc19279

Please sign in to comment.