Files
shlink-web-client/src/visits/services/VisitsParser.ts
2021-02-27 20:03:51 +01:00

95 lines
3.5 KiB
TypeScript

import { isNil, map } from 'ramda';
import { extractDomain, parseUserAgent } from '../../utils/helpers/visits';
import { hasValue } from '../../utils/utils';
import { CityStats, NormalizedVisit, Stats, Visit, VisitsStats } from '../types';
import { isOrphanVisit } from '../types/helpers';
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 = (visits: NormalizedVisit[]) => visits.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((visit: Visit): NormalizedVisit => {
const { userAgent, date, referer, visitLocation } = visit;
const common = {
date,
...parseUserAgent(userAgent),
referer: extractDomain(referer),
country: visitLocation?.countryName || 'Unknown', // eslint-disable-line @typescript-eslint/prefer-nullish-coalescing
city: visitLocation?.cityName || 'Unknown', // eslint-disable-line @typescript-eslint/prefer-nullish-coalescing
latitude: visitLocation?.latitude,
longitude: visitLocation?.longitude,
};
if (!isOrphanVisit(visit)) {
return common;
}
return { ...common, type: visit.type, visitedUrl: visit.visitedUrl };
});
export interface VisitsParser {
processStatsFromVisits: (normalizedVisits: NormalizedVisit[]) => VisitsStats;
normalizeVisits: (visits: Visit[]) => NormalizedVisit[];
}