Merge pull request #247 from acelaya-forks/feature/user-agent-improvements

Feature/user agent improvements
This commit is contained in:
Alejandro Celaya 2020-04-10 20:06:57 +02:00 committed by GitHub
commit d231ed3ede
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 64 additions and 59 deletions

View file

@ -40,6 +40,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
#### Fixed #### Fixed
* [#243](https://github.com/shlinkio/shlink-web-client/issues/243) Fixed loading state and resetting on short URL creation form. * [#243](https://github.com/shlinkio/shlink-web-client/issues/243) Fixed loading state and resetting on short URL creation form.
* [#239](https://github.com/shlinkio/shlink-web-client/issues/239) Fixed how user agents are parsed, reducing false results.
## 2.3.1 - 2020-02-08 ## 2.3.1 - 2020-02-08

5
package-lock.json generated
View file

@ -3460,6 +3460,11 @@
"resolved": "https://registry.npmjs.org/bottlejs/-/bottlejs-1.7.2.tgz", "resolved": "https://registry.npmjs.org/bottlejs/-/bottlejs-1.7.2.tgz",
"integrity": "sha512-voMPQ+g8/4GBiDE5lp43fgfoBn7Q5fZI7k3ye8ErrPoHzuuRS0Gff9lZYeyalh86uz5P304iZ14wNAM+3TZguQ==" "integrity": "sha512-voMPQ+g8/4GBiDE5lp43fgfoBn7Q5fZI7k3ye8ErrPoHzuuRS0Gff9lZYeyalh86uz5P304iZ14wNAM+3TZguQ=="
}, },
"bowser": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.9.0.tgz",
"integrity": "sha512-2ld76tuLBNFekRgmJfT2+3j5MIrP6bFict8WAIT3beq+srz1gcKNAdNKMqHqauQt63NmAa88HfP1/Ypa9Er3HA=="
},
"boxen": { "boxen": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz", "resolved": "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz",

View file

@ -33,6 +33,7 @@
"axios": "^0.19.0", "axios": "^0.19.0",
"bootstrap": "^4.3.1", "bootstrap": "^4.3.1",
"bottlejs": "^1.7.2", "bottlejs": "^1.7.2",
"bowser": "^2.9.0",
"chart.js": "^2.8.0", "chart.js": "^2.8.0",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"compare-versions": "^3.5.1", "compare-versions": "^3.5.1",

View file

@ -1,51 +1,29 @@
import bowser from 'bowser';
import { hasValue } from '../utils'; import { hasValue } from '../utils';
const DEFAULT = 'Others'; const DEFAULT = 'Others';
const BROWSERS_WHITELIST = [
'Android Browser',
'Chrome',
'Chromium',
'Firefox',
'Internet Explorer',
'Microsoft Edge',
'Opera',
'Safari',
'Samsung Internet for Android',
'Vivaldi',
'WeChat',
];
export const osFromUserAgent = (userAgent) => { export const parseUserAgent = (userAgent) => {
if (!hasValue(userAgent)) { if (!hasValue(userAgent)) {
return DEFAULT; return { browser: DEFAULT, os: DEFAULT };
} }
const lowerUserAgent = userAgent.toLowerCase(); const { browser: { name: browser }, os: { name: os } } = bowser.parse(userAgent);
switch (true) { return { os: os || DEFAULT, browser: browser && BROWSERS_WHITELIST.includes(browser) ? browser : DEFAULT };
case lowerUserAgent.includes('linux'):
return 'Linux';
case lowerUserAgent.includes('windows'):
return 'Windows';
case lowerUserAgent.includes('mac'):
return 'MacOS';
case lowerUserAgent.includes('mobi'):
return 'Mobile';
default:
return DEFAULT;
}
};
export const browserFromUserAgent = (userAgent) => {
if (!hasValue(userAgent)) {
return DEFAULT;
}
const lowerUserAgent = userAgent.toLowerCase();
switch (true) {
case lowerUserAgent.includes('opera') || lowerUserAgent.includes('opr'):
return 'Opera';
case lowerUserAgent.includes('firefox'):
return 'Firefox';
case lowerUserAgent.includes('chrome'):
return 'Chrome';
case lowerUserAgent.includes('safari'):
return 'Safari';
case lowerUserAgent.includes('edg'):
return 'Microsoft Edge';
case lowerUserAgent.includes('msie'):
return 'Internet Explorer';
default:
return DEFAULT;
}
}; };
export const extractDomain = (url) => { export const extractDomain = (url) => {

View file

@ -24,12 +24,16 @@ const generateGraphData = (title, isBarChart, labels, data, highlightedData) =>
data, data,
backgroundColor: isBarChart ? 'rgba(70, 150, 229, 0.4)' : [ backgroundColor: isBarChart ? 'rgba(70, 150, 229, 0.4)' : [
'#97BBCD', '#97BBCD',
'#DCDCDC',
'#F7464A', '#F7464A',
'#46BFBD', '#46BFBD',
'#FDB45C', '#FDB45C',
'#949FB1', '#949FB1',
'#4D5360', '#57A773',
'#414066',
'#08B2E3',
'#B6C454',
'#DCDCDC',
'#463730',
], ],
borderColor: isBarChart ? 'rgba(70, 150, 229, 1)' : 'white', borderColor: isBarChart ? 'rgba(70, 150, 229, 1)' : 'white',
borderWidth: 2, borderWidth: 2,
@ -47,6 +51,14 @@ const generateGraphData = (title, isBarChart, labels, data, highlightedData) =>
const dropLabelIfHidden = (label) => label.startsWith('hidden') ? '' : label; const dropLabelIfHidden = (label) => label.startsWith('hidden') ? '' : label;
const determineHeight = (isBarChart, labels) => {
if (!isBarChart && labels.length > 8) {
return 200;
}
return isBarChart && labels.length > 20 ? labels.length * 8 : null;
};
const renderGraph = (title, isBarChart, stats, max, highlightedStats, onClick) => { const renderGraph = (title, isBarChart, stats, max, highlightedStats, onClick) => {
const hasHighlightedStats = highlightedStats && Object.keys(highlightedStats).length > 0; const hasHighlightedStats = highlightedStats && Object.keys(highlightedStats).length > 0;
const Component = isBarChart ? HorizontalBar : Doughnut; const Component = isBarChart ? HorizontalBar : Doughnut;
@ -84,7 +96,7 @@ const renderGraph = (title, isBarChart, stats, max, highlightedStats, onClick) =
}), }),
}; };
const graphData = generateGraphData(title, isBarChart, labels, data, highlightedData); const graphData = generateGraphData(title, isBarChart, labels, data, highlightedData);
const height = isBarChart && labels.length > 20 ? labels.length * 8 : null; const height = determineHeight(isBarChart, labels);
// Provide a key based on the height, so that every time the dataset changes, a new graph is rendered // Provide a key based on the height, so that every time the dataset changes, a new graph is rendered
return ( return (

View file

@ -198,7 +198,7 @@ const ShortUrlVisits = ({ processStatsFromVisits, normalizeVisits }, OpenMapModa
</div> </div>
<div className="col-lg-5 col-xl-6 mt-4 mt-lg-0"> <div className="col-lg-5 col-xl-6 mt-4 mt-lg-0">
{showTableControls && ( {showTableControls && (
<span className={classNames({ 'row flex-row-reverse': isMobileDevice })}> <span className={classNames({ row: isMobileDevice })}>
<span className={classNames({ 'col-6': isMobileDevice })}> <span className={classNames({ 'col-6': isMobileDevice })}>
<Button outline color="primary" block={isMobileDevice} onClick={toggleTable}> <Button outline color="primary" block={isMobileDevice} onClick={toggleTable}>
{showTable ? 'Hide' : 'Show'} table {showTable ? 'Hide' : 'Show'} table

View file

@ -1,5 +1,5 @@
import { isNil, map } from 'ramda'; import { isNil, map } from 'ramda';
import { browserFromUserAgent, extractDomain, osFromUserAgent } from '../../utils/helpers/visits'; import { extractDomain, parseUserAgent } from '../../utils/helpers/visits';
import { hasValue } from '../../utils/utils'; import { hasValue } from '../../utils/utils';
const visitHasProperty = (visit, propertyName) => !isNil(visit) && hasValue(visit[propertyName]); const visitHasProperty = (visit, propertyName) => !isNil(visit) && hasValue(visit[propertyName]);
@ -59,13 +59,17 @@ export const processStatsFromVisits = (normalizedVisits) =>
{ os: {}, browsers: {}, referrers: {}, countries: {}, cities: {}, citiesForMap: {} } { os: {}, browsers: {}, referrers: {}, countries: {}, cities: {}, citiesForMap: {} }
); );
export const normalizeVisits = map(({ userAgent, date, referer, visitLocation }) => ({ export const normalizeVisits = map(({ userAgent, date, referer, visitLocation }) => {
date, const { browser, os } = parseUserAgent(userAgent);
browser: browserFromUserAgent(userAgent),
os: osFromUserAgent(userAgent), return {
referer: extractDomain(referer), date,
country: (visitLocation && visitLocation.countryName) || 'Unknown', browser,
city: (visitLocation && visitLocation.cityName) || 'Unknown', os,
latitude: visitLocation && visitLocation.latitude, referer: extractDomain(referer),
longitude: visitLocation && visitLocation.longitude, country: (visitLocation && visitLocation.countryName) || 'Unknown',
})); city: (visitLocation && visitLocation.cityName) || 'Unknown',
latitude: visitLocation && visitLocation.latitude,
longitude: visitLocation && visitLocation.longitude,
};
});

View file

@ -31,12 +31,16 @@ describe('<GraphCard />', () => {
expect(datasets).toHaveLength(1); expect(datasets).toHaveLength(1);
expect(backgroundColor).toEqual([ expect(backgroundColor).toEqual([
'#97BBCD', '#97BBCD',
'#DCDCDC',
'#F7464A', '#F7464A',
'#46BFBD', '#46BFBD',
'#FDB45C', '#FDB45C',
'#949FB1', '#949FB1',
'#4D5360', '#57A773',
'#414066',
'#08B2E3',
'#B6C454',
'#DCDCDC',
'#463730',
]); ]);
expect(borderColor).toEqual('white'); expect(borderColor).toEqual('white');
expect(legend).toEqual({ position: 'right' }); expect(legend).toEqual({ position: 'right' });

View file

@ -56,7 +56,7 @@ describe('VisitsParser', () => {
expect(os).toEqual({ expect(os).toEqual({
Linux: 3, Linux: 3,
Windows: 1, Windows: 1,
MacOS: 1, macOS: 1,
}); });
}); });
@ -137,7 +137,7 @@ describe('VisitsParser', () => {
}, },
{ {
browser: 'Firefox', browser: 'Firefox',
os: 'MacOS', os: 'macOS',
referer: 'google.com', referer: 'google.com',
country: 'United States', country: 'United States',
city: 'New York', city: 'New York',