mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 09:30:31 +03:00
Migrated VisitsParser to TS
This commit is contained in:
parent
16d96efa4a
commit
d0d664ef79
6 changed files with 117 additions and 85 deletions
|
@ -1,11 +1,12 @@
|
||||||
import React, { useEffect, FC } from 'react';
|
import React, { useEffect, FC } from 'react';
|
||||||
import { Route, Switch } from 'react-router-dom';
|
import { Route, Switch } from 'react-router-dom';
|
||||||
import NotFound from './common/NotFound';
|
import NotFound from './common/NotFound';
|
||||||
|
import { ServersMap } from './servers/data';
|
||||||
import './App.scss';
|
import './App.scss';
|
||||||
|
|
||||||
interface AppProps {
|
interface AppProps {
|
||||||
fetchServers: Function;
|
fetchServers: Function;
|
||||||
servers: Record<string, object>;
|
servers: ServersMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
const App = (MainHeader: FC, Home: FC, MenuLayout: FC, CreateServer: FC, EditServer: FC, Settings: FC) => (
|
const App = (MainHeader: FC, Home: FC, MenuLayout: FC, CreateServer: FC, EditServer: FC, Settings: FC) => (
|
||||||
|
|
|
@ -13,13 +13,13 @@ import provideMercureServices from '../mercure/services/provideServices';
|
||||||
import provideSettingsServices from '../settings/services/provideServices';
|
import provideSettingsServices from '../settings/services/provideServices';
|
||||||
import { ConnectDecorator } from './types';
|
import { ConnectDecorator } from './types';
|
||||||
|
|
||||||
type ActionMap = Record<string, any>;
|
type LazyActionMap = Record<string, Function>;
|
||||||
|
|
||||||
const bottle = new Bottle();
|
const bottle = new Bottle();
|
||||||
const { container } = bottle;
|
const { container } = bottle;
|
||||||
|
|
||||||
const lazyService = (container: IContainer, serviceName: string) => (...args: any[]) => container[serviceName](...args);
|
const lazyService = (container: IContainer, serviceName: string) => (...args: any[]) => container[serviceName](...args);
|
||||||
const mapActionService = (map: ActionMap, actionName: string): ActionMap => ({
|
const mapActionService = (map: LazyActionMap, actionName: string): LazyActionMap => ({
|
||||||
...map,
|
...map,
|
||||||
// Wrap actual action service in a function so that it is lazily created the first time it is called
|
// Wrap actual action service in a function so that it is lazily created the first time it is called
|
||||||
[actionName]: lazyService(container, actionName),
|
[actionName]: lazyService(container, actionName),
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import bowser from 'bowser';
|
import bowser from 'bowser';
|
||||||
import { zipObj } from 'ramda';
|
import { zipObj } from 'ramda';
|
||||||
import { Empty, hasValue } from '../utils';
|
import { Empty, hasValue } from '../utils';
|
||||||
|
import { Stats, UserAgent } from '../../visits/types';
|
||||||
|
|
||||||
const DEFAULT = 'Others';
|
const DEFAULT = 'Others';
|
||||||
const BROWSERS_WHITELIST = [
|
const BROWSERS_WHITELIST = [
|
||||||
|
@ -17,11 +18,6 @@ const BROWSERS_WHITELIST = [
|
||||||
'WeChat',
|
'WeChat',
|
||||||
];
|
];
|
||||||
|
|
||||||
interface UserAgent {
|
|
||||||
browser: string;
|
|
||||||
os: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const parseUserAgent = (userAgent: string | Empty): UserAgent => {
|
export const parseUserAgent = (userAgent: string | Empty): UserAgent => {
|
||||||
if (!hasValue(userAgent)) {
|
if (!hasValue(userAgent)) {
|
||||||
return { browser: DEFAULT, os: DEFAULT };
|
return { browser: DEFAULT, os: DEFAULT };
|
||||||
|
@ -42,5 +38,5 @@ export const extractDomain = (url: string | Empty): string => {
|
||||||
return domain.split(':')[0];
|
return domain.split(':')[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fillTheGaps = (stats: Record<string, number>, labels: string[]): number[] =>
|
export const fillTheGaps = (stats: Stats, labels: string[]): number[] =>
|
||||||
Object.values({ ...zipObj(labels, labels.map(() => 0)), ...stats });
|
Object.values({ ...zipObj(labels, labels.map(() => 0)), ...stats });
|
||||||
|
|
|
@ -1,75 +0,0 @@
|
||||||
import { isNil, map } from 'ramda';
|
|
||||||
import { extractDomain, parseUserAgent } from '../../utils/helpers/visits';
|
|
||||||
import { hasValue } from '../../utils/utils';
|
|
||||||
|
|
||||||
const visitHasProperty = (visit, propertyName) => !isNil(visit) && hasValue(visit[propertyName]);
|
|
||||||
|
|
||||||
const updateOsStatsForVisit = (osStats, { os }) => {
|
|
||||||
osStats[os] = (osStats[os] || 0) + 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateBrowsersStatsForVisit = (browsersStats, { browser }) => {
|
|
||||||
browsersStats[browser] = (browsersStats[browser] || 0) + 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateReferrersStatsForVisit = (referrersStats, { referer: domain }) => {
|
|
||||||
referrersStats[domain] = (referrersStats[domain] || 0) + 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateLocationsStatsForVisit = (propertyName) => (stats, visit) => {
|
|
||||||
const hasLocationProperty = visitHasProperty(visit, propertyName);
|
|
||||||
const value = hasLocationProperty ? visit[propertyName] : 'Unknown';
|
|
||||||
|
|
||||||
stats[value] = (stats[value] || 0) + 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateCountriesStatsForVisit = updateLocationsStatsForVisit('country');
|
|
||||||
const updateCitiesStatsForVisit = updateLocationsStatsForVisit('city');
|
|
||||||
|
|
||||||
const updateCitiesForMapForVisit = (citiesForMapStats, visit) => {
|
|
||||||
if (!visitHasProperty(visit, 'city') || visit.city === 'Unknown') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { city, latitude, longitude } = visit;
|
|
||||||
const currentCity = citiesForMapStats[city] || {
|
|
||||||
cityName: city,
|
|
||||||
count: 0,
|
|
||||||
latLong: [ parseFloat(latitude), parseFloat(longitude) ],
|
|
||||||
};
|
|
||||||
|
|
||||||
currentCity.count++;
|
|
||||||
|
|
||||||
citiesForMapStats[city] = currentCity;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const processStatsFromVisits = (normalizedVisits) =>
|
|
||||||
normalizedVisits.reduce(
|
|
||||||
(stats, visit) => {
|
|
||||||
// We mutate the original object because it has a big performance impact when large data sets are processed
|
|
||||||
updateOsStatsForVisit(stats.os, visit);
|
|
||||||
updateBrowsersStatsForVisit(stats.browsers, visit);
|
|
||||||
updateReferrersStatsForVisit(stats.referrers, visit);
|
|
||||||
updateCountriesStatsForVisit(stats.countries, visit);
|
|
||||||
updateCitiesStatsForVisit(stats.cities, visit);
|
|
||||||
updateCitiesForMapForVisit(stats.citiesForMap, visit);
|
|
||||||
|
|
||||||
return stats;
|
|
||||||
},
|
|
||||||
{ os: {}, browsers: {}, referrers: {}, countries: {}, cities: {}, citiesForMap: {} },
|
|
||||||
);
|
|
||||||
|
|
||||||
export const normalizeVisits = map(({ userAgent, date, referer, visitLocation }) => {
|
|
||||||
const { browser, os } = parseUserAgent(userAgent);
|
|
||||||
|
|
||||||
return {
|
|
||||||
date,
|
|
||||||
browser,
|
|
||||||
os,
|
|
||||||
referer: extractDomain(referer),
|
|
||||||
country: (visitLocation && visitLocation.countryName) || 'Unknown',
|
|
||||||
city: (visitLocation && visitLocation.cityName) || 'Unknown',
|
|
||||||
latitude: visitLocation && visitLocation.latitude,
|
|
||||||
longitude: visitLocation && visitLocation.longitude,
|
|
||||||
};
|
|
||||||
});
|
|
79
src/visits/services/VisitsParser.ts
Normal file
79
src/visits/services/VisitsParser.ts
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import { isNil, map, reduce } from 'ramda';
|
||||||
|
import { extractDomain, parseUserAgent } from '../../utils/helpers/visits';
|
||||||
|
import { hasValue } from '../../utils/utils';
|
||||||
|
import { CityStats, NormalizedVisit, Stats, Visit, VisitsStats } from '../types';
|
||||||
|
|
||||||
|
const visitHasProperty = (visit: NormalizedVisit, propertyName: keyof NormalizedVisit) =>
|
||||||
|
!isNil(visit) && hasValue(visit[propertyName]);
|
||||||
|
|
||||||
|
const optionalNumericToNumber = (numeric: string | number | null | undefined): number => {
|
||||||
|
if (typeof numeric === 'number') {
|
||||||
|
return numeric;
|
||||||
|
}
|
||||||
|
|
||||||
|
return numeric ? parseFloat(numeric) : 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateOsStatsForVisit = (osStats: Stats, { os }: NormalizedVisit) => {
|
||||||
|
osStats[os] = (osStats[os] || 0) + 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateBrowsersStatsForVisit = (browsersStats: Stats, { browser }: NormalizedVisit) => {
|
||||||
|
browsersStats[browser] = (browsersStats[browser] || 0) + 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateReferrersStatsForVisit = (referrersStats: Stats, { referer: domain }: NormalizedVisit) => {
|
||||||
|
referrersStats[domain] = (referrersStats[domain] || 0) + 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateLocationsStatsForVisit = (propertyName: 'country' | 'city') => (stats: Stats, visit: NormalizedVisit) => {
|
||||||
|
const hasLocationProperty = visitHasProperty(visit, propertyName);
|
||||||
|
const value = hasLocationProperty ? visit[propertyName] : 'Unknown';
|
||||||
|
|
||||||
|
stats[value] = (stats[value] || 0) + 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateCountriesStatsForVisit = updateLocationsStatsForVisit('country');
|
||||||
|
const updateCitiesStatsForVisit = updateLocationsStatsForVisit('city');
|
||||||
|
|
||||||
|
const updateCitiesForMapForVisit = (citiesForMapStats: Record<string, CityStats>, visit: NormalizedVisit) => {
|
||||||
|
if (!visitHasProperty(visit, 'city') || visit.city === 'Unknown') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { city, latitude, longitude } = visit;
|
||||||
|
const currentCity = citiesForMapStats[city] || {
|
||||||
|
cityName: city,
|
||||||
|
count: 0,
|
||||||
|
latLong: [ optionalNumericToNumber(latitude), optionalNumericToNumber(longitude) ],
|
||||||
|
};
|
||||||
|
|
||||||
|
currentCity.count++;
|
||||||
|
|
||||||
|
citiesForMapStats[city] = currentCity;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const processStatsFromVisits = reduce(
|
||||||
|
(stats: VisitsStats, visit: NormalizedVisit) => {
|
||||||
|
// We mutate the original object because it has a big performance impact when large data sets are processed
|
||||||
|
updateOsStatsForVisit(stats.os, visit);
|
||||||
|
updateBrowsersStatsForVisit(stats.browsers, visit);
|
||||||
|
updateReferrersStatsForVisit(stats.referrers, visit);
|
||||||
|
updateCountriesStatsForVisit(stats.countries, visit);
|
||||||
|
updateCitiesStatsForVisit(stats.cities, visit);
|
||||||
|
updateCitiesForMapForVisit(stats.citiesForMap, visit);
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
},
|
||||||
|
{ os: {}, browsers: {}, referrers: {}, countries: {}, cities: {}, citiesForMap: {} },
|
||||||
|
);
|
||||||
|
|
||||||
|
export const normalizeVisits = map(({ userAgent, date, referer, visitLocation }: Visit): NormalizedVisit => ({
|
||||||
|
date,
|
||||||
|
...parseUserAgent(userAgent),
|
||||||
|
referer: extractDomain(referer),
|
||||||
|
country: visitLocation?.countryName ?? 'Unknown',
|
||||||
|
city: visitLocation?.cityName ?? 'Unknown',
|
||||||
|
latitude: visitLocation?.latitude,
|
||||||
|
longitude: visitLocation?.longitude,
|
||||||
|
}));
|
|
@ -1,6 +1,6 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { ShortUrl } from '../../short-urls/data';
|
|
||||||
import { Action } from 'redux';
|
import { Action } from 'redux';
|
||||||
|
import { ShortUrl } from '../../short-urls/data';
|
||||||
|
|
||||||
/** @deprecated Use Visit interface instead */
|
/** @deprecated Use Visit interface instead */
|
||||||
export const VisitType = PropTypes.shape({
|
export const VisitType = PropTypes.shape({
|
||||||
|
@ -59,7 +59,38 @@ export interface Visit {
|
||||||
visitLocation: VisitLocation | null;
|
visitLocation: VisitLocation | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserAgent {
|
||||||
|
browser: string;
|
||||||
|
os: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NormalizedVisit extends UserAgent {
|
||||||
|
date: string;
|
||||||
|
referer: string;
|
||||||
|
country: string;
|
||||||
|
city: string;
|
||||||
|
latitude?: number | null;
|
||||||
|
longitude?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreateVisit {
|
export interface CreateVisit {
|
||||||
shortUrl: ShortUrl;
|
shortUrl: ShortUrl;
|
||||||
visit: Visit;
|
visit: Visit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Stats = Record<string, number>;
|
||||||
|
|
||||||
|
export interface CityStats {
|
||||||
|
cityName: string;
|
||||||
|
count: number;
|
||||||
|
latLong: [number, number];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VisitsStats {
|
||||||
|
os: Stats;
|
||||||
|
browsers: Stats;
|
||||||
|
referrers: Stats;
|
||||||
|
countries: Stats;
|
||||||
|
cities: Stats;
|
||||||
|
citiesForMap: Record<string, CityStats>;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue