From d0d664ef79406dd64539e00ccdd281dad6a11631 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 2 Sep 2020 19:32:07 +0200 Subject: [PATCH] Migrated VisitsParser to TS --- src/App.tsx | 3 +- src/container/index.ts | 4 +- src/utils/helpers/visits.ts | 8 +-- src/visits/services/VisitsParser.js | 75 --------------------------- src/visits/services/VisitsParser.ts | 79 +++++++++++++++++++++++++++++ src/visits/types/index.ts | 33 +++++++++++- 6 files changed, 117 insertions(+), 85 deletions(-) delete mode 100644 src/visits/services/VisitsParser.js create mode 100644 src/visits/services/VisitsParser.ts diff --git a/src/App.tsx b/src/App.tsx index 81311fd2..70709bc5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,11 +1,12 @@ import React, { useEffect, FC } from 'react'; import { Route, Switch } from 'react-router-dom'; import NotFound from './common/NotFound'; +import { ServersMap } from './servers/data'; import './App.scss'; interface AppProps { fetchServers: Function; - servers: Record; + servers: ServersMap; } const App = (MainHeader: FC, Home: FC, MenuLayout: FC, CreateServer: FC, EditServer: FC, Settings: FC) => ( diff --git a/src/container/index.ts b/src/container/index.ts index 22271324..77d2a640 100644 --- a/src/container/index.ts +++ b/src/container/index.ts @@ -13,13 +13,13 @@ import provideMercureServices from '../mercure/services/provideServices'; import provideSettingsServices from '../settings/services/provideServices'; import { ConnectDecorator } from './types'; -type ActionMap = Record; +type LazyActionMap = Record; const bottle = new Bottle(); const { container } = bottle; 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, // Wrap actual action service in a function so that it is lazily created the first time it is called [actionName]: lazyService(container, actionName), diff --git a/src/utils/helpers/visits.ts b/src/utils/helpers/visits.ts index 4bd0ab49..ea45421a 100644 --- a/src/utils/helpers/visits.ts +++ b/src/utils/helpers/visits.ts @@ -1,6 +1,7 @@ import bowser from 'bowser'; import { zipObj } from 'ramda'; import { Empty, hasValue } from '../utils'; +import { Stats, UserAgent } from '../../visits/types'; const DEFAULT = 'Others'; const BROWSERS_WHITELIST = [ @@ -17,11 +18,6 @@ const BROWSERS_WHITELIST = [ 'WeChat', ]; -interface UserAgent { - browser: string; - os: string; -} - export const parseUserAgent = (userAgent: string | Empty): UserAgent => { if (!hasValue(userAgent)) { return { browser: DEFAULT, os: DEFAULT }; @@ -42,5 +38,5 @@ export const extractDomain = (url: string | Empty): string => { return domain.split(':')[0]; }; -export const fillTheGaps = (stats: Record, labels: string[]): number[] => +export const fillTheGaps = (stats: Stats, labels: string[]): number[] => Object.values({ ...zipObj(labels, labels.map(() => 0)), ...stats }); diff --git a/src/visits/services/VisitsParser.js b/src/visits/services/VisitsParser.js deleted file mode 100644 index 83f984bb..00000000 --- a/src/visits/services/VisitsParser.js +++ /dev/null @@ -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, - }; -}); diff --git a/src/visits/services/VisitsParser.ts b/src/visits/services/VisitsParser.ts new file mode 100644 index 00000000..c905d0b1 --- /dev/null +++ b/src/visits/services/VisitsParser.ts @@ -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, 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, +})); diff --git a/src/visits/types/index.ts b/src/visits/types/index.ts index b66ee1c4..0b553d4e 100644 --- a/src/visits/types/index.ts +++ b/src/visits/types/index.ts @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; -import { ShortUrl } from '../../short-urls/data'; import { Action } from 'redux'; +import { ShortUrl } from '../../short-urls/data'; /** @deprecated Use Visit interface instead */ export const VisitType = PropTypes.shape({ @@ -59,7 +59,38 @@ export interface Visit { 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 { shortUrl: ShortUrl; visit: Visit; } + +export type Stats = Record; + +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; +}