mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 17:40:23 +03:00
Merge pull request #441 from acelaya-forks/feature/bots-support
Feature/bots support
This commit is contained in:
commit
a0ab9533cb
19 changed files with 145 additions and 47 deletions
|
@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
* `SHLINK_SERVER_NAME`: A name you want to give to this server. Defaults to *Shlink* if not provided.
|
* `SHLINK_SERVER_NAME`: A name you want to give to this server. Defaults to *Shlink* if not provided.
|
||||||
|
|
||||||
* [#432](https://github.com/shlinkio/shlink-web-client/pull/432) Added support to provide the `servers.json` file inside a a `conf.d` folder.
|
* [#432](https://github.com/shlinkio/shlink-web-client/pull/432) Added support to provide the `servers.json` file inside a a `conf.d` folder.
|
||||||
|
* [#440](https://github.com/shlinkio/shlink-web-client/pull/440) Added hint of what visits come potentially from a bot, in the visits table, when consuming Shlink >=2.7.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* *Nothing*
|
* *Nothing*
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
FROM node:14.15-alpine as node
|
FROM node:14.17-alpine as node
|
||||||
COPY . /shlink-web-client
|
COPY . /shlink-web-client
|
||||||
ARG VERSION="latest"
|
ARG VERSION="latest"
|
||||||
ENV VERSION ${VERSION}
|
ENV VERSION ${VERSION}
|
||||||
RUN cd /shlink-web-client && \
|
RUN cd /shlink-web-client && \
|
||||||
npm install && npm run build -- ${VERSION} --no-dist
|
npm install && npm run build -- ${VERSION} --no-dist
|
||||||
|
|
||||||
FROM nginx:1.19.6-alpine
|
FROM nginx:1.21-alpine
|
||||||
LABEL maintainer="Alejandro Celaya <alejandro@alejandrocelaya.com>"
|
LABEL maintainer="Alejandro Celaya <alejandro@alejandrocelaya.com>"
|
||||||
RUN rm -r /usr/share/nginx/html && rm /etc/nginx/conf.d/default.conf
|
RUN rm -r /usr/share/nginx/html && rm /etc/nginx/conf.d/default.conf
|
||||||
COPY config/docker/nginx.conf /etc/nginx/conf.d/default.conf
|
COPY config/docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
COPY scripts/docker/servers.json_from_env.sh /docker-entrypoint.d/30-shlink-servers-json.sh
|
COPY scripts/docker/servers_from_env.sh /docker-entrypoint.d/30-shlink-servers-json.sh
|
||||||
COPY --from=node /shlink-web-client/build /usr/share/nginx/html
|
COPY --from=node /shlink-web-client/build /usr/share/nginx/html
|
||||||
|
|
|
@ -3,7 +3,7 @@ version: '3'
|
||||||
services:
|
services:
|
||||||
shlink_web_client_node:
|
shlink_web_client_node:
|
||||||
container_name: shlink_web_client_node
|
container_name: shlink_web_client_node
|
||||||
image: node:14.15-alpine
|
image: node:14.17-alpine
|
||||||
command: /bin/sh -c "cd /home/shlink/www && npm install && npm run start"
|
command: /bin/sh -c "cd /home/shlink/www && npm install && npm run start"
|
||||||
volumes:
|
volumes:
|
||||||
- ./:/home/shlink/www
|
- ./:/home/shlink/www
|
||||||
|
|
|
@ -23,3 +23,5 @@ export const supportsOrphanVisits = supportsShortUrlTitle;
|
||||||
export const supportsQrCodeMargin = supportsShortUrlTitle;
|
export const supportsQrCodeMargin = supportsShortUrlTitle;
|
||||||
|
|
||||||
export const supportsTagsInPatch = supportsShortUrlTitle;
|
export const supportsTagsInPatch = supportsShortUrlTitle;
|
||||||
|
|
||||||
|
export const supportsBotVisits = serverMatchesVersions({ minVersion: '2.7.0' });
|
||||||
|
|
|
@ -2,17 +2,16 @@ import { RouteComponentProps } from 'react-router';
|
||||||
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
||||||
import { ShlinkVisitsParams } from '../api/types';
|
import { ShlinkVisitsParams } from '../api/types';
|
||||||
import { Topics } from '../mercure/helpers/Topics';
|
import { Topics } from '../mercure/helpers/Topics';
|
||||||
import { Settings } from '../settings/reducers/settings';
|
|
||||||
import VisitsStats from './VisitsStats';
|
import VisitsStats from './VisitsStats';
|
||||||
import { OrphanVisitsHeader } from './OrphanVisitsHeader';
|
import { OrphanVisitsHeader } from './OrphanVisitsHeader';
|
||||||
import { NormalizedVisit, VisitsInfo } from './types';
|
import { NormalizedVisit, VisitsInfo } from './types';
|
||||||
import { VisitsExporter } from './services/VisitsExporter';
|
import { VisitsExporter } from './services/VisitsExporter';
|
||||||
|
import { CommonVisitsProps } from './types/CommonVisitsProps';
|
||||||
|
|
||||||
export interface OrphanVisitsProps extends RouteComponentProps {
|
export interface OrphanVisitsProps extends CommonVisitsProps, RouteComponentProps {
|
||||||
getOrphanVisits: (params: ShlinkVisitsParams) => void;
|
getOrphanVisits: (params: ShlinkVisitsParams) => void;
|
||||||
orphanVisits: VisitsInfo;
|
orphanVisits: VisitsInfo;
|
||||||
cancelGetOrphanVisits: () => void;
|
cancelGetOrphanVisits: () => void;
|
||||||
settings: Settings;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OrphanVisits = ({ exportVisits }: VisitsExporter) => boundToMercureHub(({
|
export const OrphanVisits = ({ exportVisits }: VisitsExporter) => boundToMercureHub(({
|
||||||
|
@ -22,6 +21,7 @@ export const OrphanVisits = ({ exportVisits }: VisitsExporter) => boundToMercure
|
||||||
orphanVisits,
|
orphanVisits,
|
||||||
cancelGetOrphanVisits,
|
cancelGetOrphanVisits,
|
||||||
settings,
|
settings,
|
||||||
|
selectedServer,
|
||||||
}: OrphanVisitsProps) => {
|
}: OrphanVisitsProps) => {
|
||||||
const exportCsv = (visits: NormalizedVisit[]) => exportVisits('orphan_visits.csv', visits);
|
const exportCsv = (visits: NormalizedVisit[]) => exportVisits('orphan_visits.csv', visits);
|
||||||
|
|
||||||
|
@ -33,6 +33,7 @@ export const OrphanVisits = ({ exportVisits }: VisitsExporter) => boundToMercure
|
||||||
baseUrl={url}
|
baseUrl={url}
|
||||||
settings={settings}
|
settings={settings}
|
||||||
exportCsv={exportCsv}
|
exportCsv={exportCsv}
|
||||||
|
selectedServer={selectedServer}
|
||||||
isOrphanVisits
|
isOrphanVisits
|
||||||
>
|
>
|
||||||
<OrphanVisitsHeader orphanVisits={orphanVisits} goBack={goBack} />
|
<OrphanVisitsHeader orphanVisits={orphanVisits} goBack={goBack} />
|
||||||
|
|
|
@ -5,20 +5,19 @@ import { ShlinkVisitsParams } from '../api/types';
|
||||||
import { parseQuery } from '../utils/helpers/query';
|
import { parseQuery } from '../utils/helpers/query';
|
||||||
import { Topics } from '../mercure/helpers/Topics';
|
import { Topics } from '../mercure/helpers/Topics';
|
||||||
import { ShortUrlDetail } from '../short-urls/reducers/shortUrlDetail';
|
import { ShortUrlDetail } from '../short-urls/reducers/shortUrlDetail';
|
||||||
import { Settings } from '../settings/reducers/settings';
|
|
||||||
import { ShortUrlVisits as ShortUrlVisitsState } from './reducers/shortUrlVisits';
|
import { ShortUrlVisits as ShortUrlVisitsState } from './reducers/shortUrlVisits';
|
||||||
import ShortUrlVisitsHeader from './ShortUrlVisitsHeader';
|
import ShortUrlVisitsHeader from './ShortUrlVisitsHeader';
|
||||||
import VisitsStats from './VisitsStats';
|
import VisitsStats from './VisitsStats';
|
||||||
import { VisitsExporter } from './services/VisitsExporter';
|
import { VisitsExporter } from './services/VisitsExporter';
|
||||||
import { NormalizedVisit } from './types';
|
import { NormalizedVisit } from './types';
|
||||||
|
import { CommonVisitsProps } from './types/CommonVisitsProps';
|
||||||
|
|
||||||
export interface ShortUrlVisitsProps extends RouteComponentProps<{ shortCode: string }> {
|
export interface ShortUrlVisitsProps extends CommonVisitsProps, RouteComponentProps<{ shortCode: string }> {
|
||||||
getShortUrlVisits: (shortCode: string, query?: ShlinkVisitsParams) => void;
|
getShortUrlVisits: (shortCode: string, query?: ShlinkVisitsParams) => void;
|
||||||
shortUrlVisits: ShortUrlVisitsState;
|
shortUrlVisits: ShortUrlVisitsState;
|
||||||
getShortUrlDetail: Function;
|
getShortUrlDetail: Function;
|
||||||
shortUrlDetail: ShortUrlDetail;
|
shortUrlDetail: ShortUrlDetail;
|
||||||
cancelGetShortUrlVisits: () => void;
|
cancelGetShortUrlVisits: () => void;
|
||||||
settings: Settings;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ShortUrlVisits = ({ exportVisits }: VisitsExporter) => boundToMercureHub(({
|
const ShortUrlVisits = ({ exportVisits }: VisitsExporter) => boundToMercureHub(({
|
||||||
|
@ -31,6 +30,7 @@ const ShortUrlVisits = ({ exportVisits }: VisitsExporter) => boundToMercureHub((
|
||||||
getShortUrlDetail,
|
getShortUrlDetail,
|
||||||
cancelGetShortUrlVisits,
|
cancelGetShortUrlVisits,
|
||||||
settings,
|
settings,
|
||||||
|
selectedServer,
|
||||||
}: ShortUrlVisitsProps) => {
|
}: ShortUrlVisitsProps) => {
|
||||||
const { shortCode } = params;
|
const { shortCode } = params;
|
||||||
const { domain } = parseQuery<{ domain?: string }>(search);
|
const { domain } = parseQuery<{ domain?: string }>(search);
|
||||||
|
@ -53,6 +53,7 @@ const ShortUrlVisits = ({ exportVisits }: VisitsExporter) => boundToMercureHub((
|
||||||
domain={domain}
|
domain={domain}
|
||||||
settings={settings}
|
settings={settings}
|
||||||
exportCsv={exportCsv}
|
exportCsv={exportCsv}
|
||||||
|
selectedServer={selectedServer}
|
||||||
>
|
>
|
||||||
<ShortUrlVisitsHeader shortUrlDetail={shortUrlDetail} shortUrlVisits={shortUrlVisits} goBack={goBack} />
|
<ShortUrlVisitsHeader shortUrlDetail={shortUrlDetail} shortUrlVisits={shortUrlVisits} goBack={goBack} />
|
||||||
</VisitsStats>
|
</VisitsStats>
|
||||||
|
|
|
@ -3,18 +3,17 @@ import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
||||||
import ColorGenerator from '../utils/services/ColorGenerator';
|
import ColorGenerator from '../utils/services/ColorGenerator';
|
||||||
import { ShlinkVisitsParams } from '../api/types';
|
import { ShlinkVisitsParams } from '../api/types';
|
||||||
import { Topics } from '../mercure/helpers/Topics';
|
import { Topics } from '../mercure/helpers/Topics';
|
||||||
import { Settings } from '../settings/reducers/settings';
|
|
||||||
import { TagVisits as TagVisitsState } from './reducers/tagVisits';
|
import { TagVisits as TagVisitsState } from './reducers/tagVisits';
|
||||||
import TagVisitsHeader from './TagVisitsHeader';
|
import TagVisitsHeader from './TagVisitsHeader';
|
||||||
import VisitsStats from './VisitsStats';
|
import VisitsStats from './VisitsStats';
|
||||||
import { VisitsExporter } from './services/VisitsExporter';
|
import { VisitsExporter } from './services/VisitsExporter';
|
||||||
import { NormalizedVisit } from './types';
|
import { NormalizedVisit } from './types';
|
||||||
|
import { CommonVisitsProps } from './types/CommonVisitsProps';
|
||||||
|
|
||||||
export interface TagVisitsProps extends RouteComponentProps<{ tag: string }> {
|
export interface TagVisitsProps extends CommonVisitsProps, RouteComponentProps<{ tag: string }> {
|
||||||
getTagVisits: (tag: string, query: any) => void;
|
getTagVisits: (tag: string, query: any) => void;
|
||||||
tagVisits: TagVisitsState;
|
tagVisits: TagVisitsState;
|
||||||
cancelGetTagVisits: () => void;
|
cancelGetTagVisits: () => void;
|
||||||
settings: Settings;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const TagVisits = (colorGenerator: ColorGenerator, { exportVisits }: VisitsExporter) => boundToMercureHub(({
|
const TagVisits = (colorGenerator: ColorGenerator, { exportVisits }: VisitsExporter) => boundToMercureHub(({
|
||||||
|
@ -24,6 +23,7 @@ const TagVisits = (colorGenerator: ColorGenerator, { exportVisits }: VisitsExpor
|
||||||
tagVisits,
|
tagVisits,
|
||||||
cancelGetTagVisits,
|
cancelGetTagVisits,
|
||||||
settings,
|
settings,
|
||||||
|
selectedServer,
|
||||||
}: TagVisitsProps) => {
|
}: TagVisitsProps) => {
|
||||||
const { tag } = params;
|
const { tag } = params;
|
||||||
const loadVisits = (params: ShlinkVisitsParams) => getTagVisits(tag, params);
|
const loadVisits = (params: ShlinkVisitsParams) => getTagVisits(tag, params);
|
||||||
|
@ -37,6 +37,7 @@ const TagVisits = (colorGenerator: ColorGenerator, { exportVisits }: VisitsExpor
|
||||||
baseUrl={url}
|
baseUrl={url}
|
||||||
settings={settings}
|
settings={settings}
|
||||||
exportCsv={exportCsv}
|
exportCsv={exportCsv}
|
||||||
|
selectedServer={selectedServer}
|
||||||
>
|
>
|
||||||
<TagVisitsHeader tagVisits={tagVisits} goBack={goBack} colorGenerator={colorGenerator} />
|
<TagVisitsHeader tagVisits={tagVisits} goBack={goBack} colorGenerator={colorGenerator} />
|
||||||
</VisitsStats>
|
</VisitsStats>
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { DateInterval, DateRange, intervalToDateRange } from '../utils/dates/typ
|
||||||
import { Result } from '../utils/Result';
|
import { Result } from '../utils/Result';
|
||||||
import { ShlinkApiError } from '../api/ShlinkApiError';
|
import { ShlinkApiError } from '../api/ShlinkApiError';
|
||||||
import { Settings } from '../settings/reducers/settings';
|
import { Settings } from '../settings/reducers/settings';
|
||||||
|
import { SelectedServer } from '../servers/data';
|
||||||
import SortableBarGraph from './helpers/SortableBarGraph';
|
import SortableBarGraph from './helpers/SortableBarGraph';
|
||||||
import GraphCard from './helpers/GraphCard';
|
import GraphCard from './helpers/GraphCard';
|
||||||
import LineChartCard from './helpers/LineChartCard';
|
import LineChartCard from './helpers/LineChartCard';
|
||||||
|
@ -23,13 +24,14 @@ import { NormalizedOrphanVisit, NormalizedVisit, OrphanVisitType, VisitsInfo } f
|
||||||
import OpenMapModalBtn from './helpers/OpenMapModalBtn';
|
import OpenMapModalBtn from './helpers/OpenMapModalBtn';
|
||||||
import { processStatsFromVisits } from './services/VisitsParser';
|
import { processStatsFromVisits } from './services/VisitsParser';
|
||||||
import { OrphanVisitTypeDropdown } from './helpers/OrphanVisitTypeDropdown';
|
import { OrphanVisitTypeDropdown } from './helpers/OrphanVisitTypeDropdown';
|
||||||
import './VisitsStats.scss';
|
|
||||||
import { HighlightableProps, highlightedVisitsToStats, normalizeAndFilterVisits } from './types/helpers';
|
import { HighlightableProps, highlightedVisitsToStats, normalizeAndFilterVisits } from './types/helpers';
|
||||||
|
import './VisitsStats.scss';
|
||||||
|
|
||||||
export interface VisitsStatsProps {
|
export interface VisitsStatsProps {
|
||||||
getVisits: (params: Partial<ShlinkVisitsParams>) => void;
|
getVisits: (params: Partial<ShlinkVisitsParams>) => void;
|
||||||
visitsInfo: VisitsInfo;
|
visitsInfo: VisitsInfo;
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
|
selectedServer: SelectedServer;
|
||||||
cancelGetVisits: () => void;
|
cancelGetVisits: () => void;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
domain?: string;
|
domain?: string;
|
||||||
|
@ -67,9 +69,18 @@ const VisitsNavLink: FC<VisitsNavLinkProps & { to: string }> = ({ subPath, title
|
||||||
</NavLink>
|
</NavLink>
|
||||||
);
|
);
|
||||||
|
|
||||||
const VisitsStats: FC<VisitsStatsProps> = (
|
const VisitsStats: FC<VisitsStatsProps> = ({
|
||||||
{ children, visitsInfo, getVisits, cancelGetVisits, baseUrl, domain, settings, exportCsv, isOrphanVisits = false },
|
children,
|
||||||
) => {
|
visitsInfo,
|
||||||
|
getVisits,
|
||||||
|
cancelGetVisits,
|
||||||
|
baseUrl,
|
||||||
|
domain,
|
||||||
|
settings,
|
||||||
|
exportCsv,
|
||||||
|
selectedServer,
|
||||||
|
isOrphanVisits = false,
|
||||||
|
}) => {
|
||||||
const initialInterval: DateInterval = settings.visits?.defaultInterval ?? 'last30Days';
|
const initialInterval: DateInterval = settings.visits?.defaultInterval ?? 'last30Days';
|
||||||
const [ dateRange, setDateRange ] = useState<DateRange>(intervalToDateRange(initialInterval));
|
const [ dateRange, setDateRange ] = useState<DateRange>(intervalToDateRange(initialInterval));
|
||||||
const [ highlightedVisits, setHighlightedVisits ] = useState<NormalizedVisit[]>([]);
|
const [ highlightedVisits, setHighlightedVisits ] = useState<NormalizedVisit[]>([]);
|
||||||
|
@ -243,6 +254,7 @@ const VisitsStats: FC<VisitsStatsProps> = (
|
||||||
selectedVisits={highlightedVisits}
|
selectedVisits={highlightedVisits}
|
||||||
setSelectedVisits={setSelectedVisits}
|
setSelectedVisits={setSelectedVisits}
|
||||||
isOrphanVisits={isOrphanVisits}
|
isOrphanVisits={isOrphanVisits}
|
||||||
|
selectedServer={selectedServer}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
|
@ -6,24 +6,29 @@ import {
|
||||||
faCaretDown as caretDownIcon,
|
faCaretDown as caretDownIcon,
|
||||||
faCaretUp as caretUpIcon,
|
faCaretUp as caretUpIcon,
|
||||||
faCheck as checkIcon,
|
faCheck as checkIcon,
|
||||||
|
faRobot as botIcon,
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { UncontrolledTooltip } from 'reactstrap';
|
||||||
import SimplePaginator from '../common/SimplePaginator';
|
import SimplePaginator from '../common/SimplePaginator';
|
||||||
import SearchField from '../utils/SearchField';
|
import SearchField from '../utils/SearchField';
|
||||||
import { determineOrderDir, OrderDir } from '../utils/utils';
|
import { determineOrderDir, OrderDir } from '../utils/utils';
|
||||||
import { prettify } from '../utils/helpers/numbers';
|
import { prettify } from '../utils/helpers/numbers';
|
||||||
|
import { supportsBotVisits } from '../utils/helpers/features';
|
||||||
|
import { SelectedServer } from '../servers/data';
|
||||||
import { NormalizedOrphanVisit, NormalizedVisit } from './types';
|
import { NormalizedOrphanVisit, NormalizedVisit } from './types';
|
||||||
import './VisitsTable.scss';
|
import './VisitsTable.scss';
|
||||||
|
|
||||||
interface VisitsTableProps {
|
export interface VisitsTableProps {
|
||||||
visits: NormalizedVisit[];
|
visits: NormalizedVisit[];
|
||||||
selectedVisits?: NormalizedVisit[];
|
selectedVisits?: NormalizedVisit[];
|
||||||
setSelectedVisits: (visits: NormalizedVisit[]) => void;
|
setSelectedVisits: (visits: NormalizedVisit[]) => void;
|
||||||
matchMedia?: (query: string) => MediaQueryList;
|
matchMedia?: (query: string) => MediaQueryList;
|
||||||
isOrphanVisits?: boolean;
|
isOrphanVisits?: boolean;
|
||||||
|
selectedServer: SelectedServer;
|
||||||
}
|
}
|
||||||
|
|
||||||
type OrderableFields = 'date' | 'country' | 'city' | 'browser' | 'os' | 'referer' | 'visitedUrl';
|
type OrderableFields = 'date' | 'country' | 'city' | 'browser' | 'os' | 'referer' | 'visitedUrl' | 'potentialBot';
|
||||||
|
|
||||||
interface Order {
|
interface Order {
|
||||||
field?: OrderableFields;
|
field?: OrderableFields;
|
||||||
|
@ -58,6 +63,7 @@ const VisitsTable = ({
|
||||||
visits,
|
visits,
|
||||||
selectedVisits = [],
|
selectedVisits = [],
|
||||||
setSelectedVisits,
|
setSelectedVisits,
|
||||||
|
selectedServer,
|
||||||
matchMedia = window.matchMedia,
|
matchMedia = window.matchMedia,
|
||||||
isOrphanVisits = false,
|
isOrphanVisits = false,
|
||||||
}: VisitsTableProps) => {
|
}: VisitsTableProps) => {
|
||||||
|
@ -69,10 +75,11 @@ const VisitsTable = ({
|
||||||
const [ order, setOrder ] = useState<Order>({ field: undefined, dir: undefined });
|
const [ order, setOrder ] = useState<Order>({ field: undefined, dir: undefined });
|
||||||
const resultSet = useMemo(() => calculateVisits(visits, searchTerm, order), [ searchTerm, order ]);
|
const resultSet = useMemo(() => calculateVisits(visits, searchTerm, order), [ searchTerm, order ]);
|
||||||
const isFirstLoad = useRef(true);
|
const isFirstLoad = useRef(true);
|
||||||
|
|
||||||
const [ page, setPage ] = useState(1);
|
const [ page, setPage ] = useState(1);
|
||||||
const end = page * PAGE_SIZE;
|
const end = page * PAGE_SIZE;
|
||||||
const start = end - PAGE_SIZE;
|
const start = end - PAGE_SIZE;
|
||||||
|
const supportsBots = supportsBotVisits(selectedServer);
|
||||||
|
const fullSizeColSpan = 7 + Number(supportsBots) + Number(isOrphanVisits);
|
||||||
|
|
||||||
const orderByColumn = (field: OrderableFields) =>
|
const orderByColumn = (field: OrderableFields) =>
|
||||||
() => setOrder({ field, dir: determineOrderDir(field, order.field, order.dir) });
|
() => setOrder({ field, dir: determineOrderDir(field, order.field, order.dir) });
|
||||||
|
@ -102,13 +109,19 @@ const VisitsTable = ({
|
||||||
<thead className="visits-table__header">
|
<thead className="visits-table__header">
|
||||||
<tr>
|
<tr>
|
||||||
<th
|
<th
|
||||||
className="visits-table__header-cell visits-table__sticky text-center"
|
className={`${headerCellsClass} text-center`}
|
||||||
onClick={() => setSelectedVisits(
|
onClick={() => setSelectedVisits(
|
||||||
selectedVisits.length < resultSet.total ? resultSet.visitsGroups.flat() : [],
|
selectedVisits.length < resultSet.total ? resultSet.visitsGroups.flat() : [],
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={checkIcon} className={classNames({ 'text-primary': selectedVisits.length > 0 })} />
|
<FontAwesomeIcon icon={checkIcon} className={classNames({ 'text-primary': selectedVisits.length > 0 })} />
|
||||||
</th>
|
</th>
|
||||||
|
{supportsBots && (
|
||||||
|
<th className={`${headerCellsClass} text-center`} onClick={orderByColumn('potentialBot')}>
|
||||||
|
<FontAwesomeIcon icon={botIcon} />
|
||||||
|
{renderOrderIcon('potentialBot')}
|
||||||
|
</th>
|
||||||
|
)}
|
||||||
<th className={headerCellsClass} onClick={orderByColumn('date')}>
|
<th className={headerCellsClass} onClick={orderByColumn('date')}>
|
||||||
Date
|
Date
|
||||||
{renderOrderIcon('date')}
|
{renderOrderIcon('date')}
|
||||||
|
@ -141,7 +154,7 @@ const VisitsTable = ({
|
||||||
)}
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={isOrphanVisits ? 8 : 7} className="p-0">
|
<td colSpan={fullSizeColSpan} className="p-0">
|
||||||
<SearchField noBorder large={false} onChange={setSearchTerm} />
|
<SearchField noBorder large={false} onChange={setSearchTerm} />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -149,7 +162,7 @@ const VisitsTable = ({
|
||||||
<tbody>
|
<tbody>
|
||||||
{!resultSet.visitsGroups[page - 1]?.length && (
|
{!resultSet.visitsGroups[page - 1]?.length && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={isOrphanVisits ? 8 : 7} className="text-center">
|
<td colSpan={fullSizeColSpan} className="text-center">
|
||||||
No visits found with current filtering
|
No visits found with current filtering
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -169,6 +182,18 @@ const VisitsTable = ({
|
||||||
<td className="text-center">
|
<td className="text-center">
|
||||||
{isSelected && <FontAwesomeIcon icon={checkIcon} className="text-primary" />}
|
{isSelected && <FontAwesomeIcon icon={checkIcon} className="text-primary" />}
|
||||||
</td>
|
</td>
|
||||||
|
{supportsBots && (
|
||||||
|
<td className="text-center">
|
||||||
|
{visit.potentialBot && (
|
||||||
|
<>
|
||||||
|
<FontAwesomeIcon icon={botIcon} id={`botIcon${index}`} />
|
||||||
|
<UncontrolledTooltip placement="right" target={`botIcon${index}`}>
|
||||||
|
Potentially a visit from a bot or crawler
|
||||||
|
</UncontrolledTooltip>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
<td>
|
<td>
|
||||||
<Moment format="YYYY-MM-DD HH:mm">{visit.date}</Moment>
|
<Moment format="YYYY-MM-DD HH:mm">{visit.date}</Moment>
|
||||||
</td>
|
</td>
|
||||||
|
@ -185,7 +210,7 @@ const VisitsTable = ({
|
||||||
{resultSet.total > PAGE_SIZE && (
|
{resultSet.total > PAGE_SIZE && (
|
||||||
<tfoot>
|
<tfoot>
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={isOrphanVisits ? 8 : 7} className="visits-table__footer-cell visits-table__sticky">
|
<td colSpan={fullSizeColSpan} className="visits-table__footer-cell visits-table__sticky">
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<SimplePaginator
|
<SimplePaginator
|
||||||
|
|
|
@ -81,9 +81,10 @@ export const processStatsFromVisits = (visits: NormalizedVisit[]) => visits.redu
|
||||||
);
|
);
|
||||||
|
|
||||||
export const normalizeVisits = map((visit: Visit): NormalizedVisit => {
|
export const normalizeVisits = map((visit: Visit): NormalizedVisit => {
|
||||||
const { userAgent, date, referer, visitLocation } = visit;
|
const { userAgent, date, referer, visitLocation, potentialBot = false } = visit;
|
||||||
const common = {
|
const common = {
|
||||||
date,
|
date,
|
||||||
|
potentialBot,
|
||||||
...parseUserAgent(userAgent),
|
...parseUserAgent(userAgent),
|
||||||
referer: extractDomain(referer),
|
referer: extractDomain(referer),
|
||||||
country: visitLocation?.countryName || 'Unknown', // eslint-disable-line @typescript-eslint/prefer-nullish-coalescing
|
country: visitLocation?.countryName || 'Unknown', // eslint-disable-line @typescript-eslint/prefer-nullish-coalescing
|
||||||
|
|
|
@ -18,19 +18,19 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
|
|
||||||
bottle.serviceFactory('ShortUrlVisits', ShortUrlVisits, 'VisitsExporter');
|
bottle.serviceFactory('ShortUrlVisits', ShortUrlVisits, 'VisitsExporter');
|
||||||
bottle.decorator('ShortUrlVisits', connect(
|
bottle.decorator('ShortUrlVisits', connect(
|
||||||
[ 'shortUrlVisits', 'shortUrlDetail', 'mercureInfo', 'settings' ],
|
[ 'shortUrlVisits', 'shortUrlDetail', 'mercureInfo', 'settings', 'selectedServer' ],
|
||||||
[ 'getShortUrlVisits', 'getShortUrlDetail', 'cancelGetShortUrlVisits', 'createNewVisits', 'loadMercureInfo' ],
|
[ 'getShortUrlVisits', 'getShortUrlDetail', 'cancelGetShortUrlVisits', 'createNewVisits', 'loadMercureInfo' ],
|
||||||
));
|
));
|
||||||
|
|
||||||
bottle.serviceFactory('TagVisits', TagVisits, 'ColorGenerator', 'VisitsExporter');
|
bottle.serviceFactory('TagVisits', TagVisits, 'ColorGenerator', 'VisitsExporter');
|
||||||
bottle.decorator('TagVisits', connect(
|
bottle.decorator('TagVisits', connect(
|
||||||
[ 'tagVisits', 'mercureInfo', 'settings' ],
|
[ 'tagVisits', 'mercureInfo', 'settings', 'selectedServer' ],
|
||||||
[ 'getTagVisits', 'cancelGetTagVisits', 'createNewVisits', 'loadMercureInfo' ],
|
[ 'getTagVisits', 'cancelGetTagVisits', 'createNewVisits', 'loadMercureInfo' ],
|
||||||
));
|
));
|
||||||
|
|
||||||
bottle.serviceFactory('OrphanVisits', OrphanVisits, 'VisitsExporter');
|
bottle.serviceFactory('OrphanVisits', OrphanVisits, 'VisitsExporter');
|
||||||
bottle.decorator('OrphanVisits', connect(
|
bottle.decorator('OrphanVisits', connect(
|
||||||
[ 'orphanVisits', 'mercureInfo', 'settings' ],
|
[ 'orphanVisits', 'mercureInfo', 'settings', 'selectedServer' ],
|
||||||
[ 'getOrphanVisits', 'cancelGetOrphanVisits', 'createNewVisits', 'loadMercureInfo' ],
|
[ 'getOrphanVisits', 'cancelGetOrphanVisits', 'createNewVisits', 'loadMercureInfo' ],
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|
7
src/visits/types/CommonVisitsProps.ts
Normal file
7
src/visits/types/CommonVisitsProps.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { SelectedServer } from '../../servers/data';
|
||||||
|
import { Settings } from '../../settings/reducers/settings';
|
||||||
|
|
||||||
|
export interface CommonVisitsProps {
|
||||||
|
selectedServer: SelectedServer;
|
||||||
|
settings: Settings;
|
||||||
|
}
|
|
@ -38,6 +38,7 @@ export interface RegularVisit {
|
||||||
date: string;
|
date: string;
|
||||||
userAgent: string;
|
userAgent: string;
|
||||||
visitLocation: VisitLocation | null;
|
visitLocation: VisitLocation | null;
|
||||||
|
potentialBot?: boolean; // Optional only when using Shlink older than v2.7
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OrphanVisit extends RegularVisit {
|
export interface OrphanVisit extends RegularVisit {
|
||||||
|
@ -59,6 +60,7 @@ export interface NormalizedRegularVisit extends UserAgent {
|
||||||
city: string;
|
city: string;
|
||||||
latitude?: number | null;
|
latitude?: number | null;
|
||||||
longitude?: number | null;
|
longitude?: number | null;
|
||||||
|
potentialBot: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NormalizedOrphanVisit extends NormalizedRegularVisit {
|
export interface NormalizedOrphanVisit extends NormalizedRegularVisit {
|
||||||
|
|
|
@ -9,6 +9,7 @@ import VisitsStats from '../../src/visits/VisitsStats';
|
||||||
import { OrphanVisitsHeader } from '../../src/visits/OrphanVisitsHeader';
|
import { OrphanVisitsHeader } from '../../src/visits/OrphanVisitsHeader';
|
||||||
import { Settings } from '../../src/settings/reducers/settings';
|
import { Settings } from '../../src/settings/reducers/settings';
|
||||||
import { VisitsExporter } from '../../src/visits/services/VisitsExporter';
|
import { VisitsExporter } from '../../src/visits/services/VisitsExporter';
|
||||||
|
import { SelectedServer } from '../../src/servers/data';
|
||||||
|
|
||||||
describe('<OrphanVisits />', () => {
|
describe('<OrphanVisits />', () => {
|
||||||
it('wraps visits stats and header', () => {
|
it('wraps visits stats and header', () => {
|
||||||
|
@ -28,6 +29,7 @@ describe('<OrphanVisits />', () => {
|
||||||
location={Mock.all<Location>()}
|
location={Mock.all<Location>()}
|
||||||
match={Mock.of<match>({ url: 'the_base_url' })}
|
match={Mock.of<match>({ url: 'the_base_url' })}
|
||||||
settings={Mock.all<Settings>()}
|
settings={Mock.all<Settings>()}
|
||||||
|
selectedServer={Mock.all<SelectedServer>()}
|
||||||
/>,
|
/>,
|
||||||
).dive();
|
).dive();
|
||||||
const stats = wrapper.find(VisitsStats);
|
const stats = wrapper.find(VisitsStats);
|
||||||
|
|
|
@ -10,6 +10,7 @@ import LineChartCard from '../../src/visits/helpers/LineChartCard';
|
||||||
import VisitsTable from '../../src/visits/VisitsTable';
|
import VisitsTable from '../../src/visits/VisitsTable';
|
||||||
import { Result } from '../../src/utils/Result';
|
import { Result } from '../../src/utils/Result';
|
||||||
import { Settings } from '../../src/settings/reducers/settings';
|
import { Settings } from '../../src/settings/reducers/settings';
|
||||||
|
import { SelectedServer } from '../../src/servers/data';
|
||||||
|
|
||||||
describe('<VisitStats />', () => {
|
describe('<VisitStats />', () => {
|
||||||
const visits = [ Mock.all<Visit>(), Mock.all<Visit>(), Mock.all<Visit>() ];
|
const visits = [ Mock.all<Visit>(), Mock.all<Visit>(), Mock.all<Visit>() ];
|
||||||
|
@ -27,6 +28,7 @@ describe('<VisitStats />', () => {
|
||||||
baseUrl={''}
|
baseUrl={''}
|
||||||
settings={Mock.all<Settings>()}
|
settings={Mock.all<Settings>()}
|
||||||
exportCsv={exportCsv}
|
exportCsv={exportCsv}
|
||||||
|
selectedServer={Mock.all<SelectedServer>()}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,43 +1,62 @@
|
||||||
import { shallow, ShallowWrapper } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import { Mock } from 'ts-mockery';
|
import { Mock } from 'ts-mockery';
|
||||||
import VisitsTable from '../../src/visits/VisitsTable';
|
import VisitsTable, { VisitsTableProps } from '../../src/visits/VisitsTable';
|
||||||
import { rangeOf } from '../../src/utils/utils';
|
import { rangeOf } from '../../src/utils/utils';
|
||||||
import SimplePaginator from '../../src/common/SimplePaginator';
|
import SimplePaginator from '../../src/common/SimplePaginator';
|
||||||
import SearchField from '../../src/utils/SearchField';
|
import SearchField from '../../src/utils/SearchField';
|
||||||
import { NormalizedVisit } from '../../src/visits/types';
|
import { NormalizedVisit } from '../../src/visits/types';
|
||||||
|
import { ReachableServer, SelectedServer } from '../../src/servers/data';
|
||||||
|
import { SemVer } from '../../src/utils/helpers/version';
|
||||||
|
|
||||||
describe('<VisitsTable />', () => {
|
describe('<VisitsTable />', () => {
|
||||||
const matchMedia = () => Mock.of<MediaQueryList>({ matches: false });
|
const matchMedia = () => Mock.of<MediaQueryList>({ matches: false });
|
||||||
const setSelectedVisits = jest.fn();
|
const setSelectedVisits = jest.fn();
|
||||||
let wrapper: ShallowWrapper;
|
let wrapper: ShallowWrapper;
|
||||||
const createWrapper = (visits: NormalizedVisit[], selectedVisits: NormalizedVisit[] = [], isOrphanVisits = false) => {
|
const wrapperFactory = (props: Partial<VisitsTableProps> = {}) => {
|
||||||
wrapper = shallow(
|
wrapper = shallow(
|
||||||
<VisitsTable
|
<VisitsTable
|
||||||
visits={visits}
|
visits={[]}
|
||||||
selectedVisits={selectedVisits}
|
selectedServer={Mock.all<SelectedServer>()}
|
||||||
setSelectedVisits={setSelectedVisits}
|
{...props}
|
||||||
matchMedia={matchMedia}
|
matchMedia={matchMedia}
|
||||||
isOrphanVisits={isOrphanVisits}
|
setSelectedVisits={setSelectedVisits}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
return wrapper;
|
return wrapper;
|
||||||
};
|
};
|
||||||
|
const createWrapper = (visits: NormalizedVisit[], selectedVisits: NormalizedVisit[] = []) => wrapperFactory(
|
||||||
|
{ visits, selectedVisits },
|
||||||
|
);
|
||||||
|
const createOrphanVisitsWrapper = (isOrphanVisits: boolean, version: SemVer) => wrapperFactory({
|
||||||
|
isOrphanVisits,
|
||||||
|
selectedServer: Mock.of<ReachableServer>({ printableVersion: version, version }),
|
||||||
|
});
|
||||||
|
const createServerVersionWrapper = (version: SemVer) => wrapperFactory({
|
||||||
|
selectedServer: Mock.of<ReachableServer>({ printableVersion: version, version }),
|
||||||
|
});
|
||||||
|
const createWrapperWithBots = () => wrapperFactory({
|
||||||
|
selectedServer: Mock.of<ReachableServer>({ printableVersion: '2.7.0', version: '2.7.0' }),
|
||||||
|
visits: [
|
||||||
|
Mock.of<NormalizedVisit>({ potentialBot: false }),
|
||||||
|
Mock.of<NormalizedVisit>({ potentialBot: true }),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(jest.resetAllMocks);
|
afterEach(jest.resetAllMocks);
|
||||||
afterEach(() => wrapper?.unmount());
|
afterEach(() => wrapper?.unmount());
|
||||||
|
|
||||||
it('renders columns as expected', () => {
|
it.each([
|
||||||
const wrapper = createWrapper([]);
|
[ '2.6.0' as SemVer, [ 'Date', 'Country', 'City', 'Browser', 'OS', 'Referrer' ]],
|
||||||
|
[ '2.7.0' as SemVer, [ 'fa-robot', 'Date', 'Country', 'City', 'Browser', 'OS', 'Referrer' ]],
|
||||||
|
])('renders columns as expected', (version, expectedColumns) => {
|
||||||
|
const wrapper = createServerVersionWrapper(version);
|
||||||
const th = wrapper.find('thead').find('th');
|
const th = wrapper.find('thead').find('th');
|
||||||
|
|
||||||
expect(th).toHaveLength(7);
|
expect(th).toHaveLength(expectedColumns.length + 1);
|
||||||
expect(th.at(1).text()).toContain('Date');
|
expectedColumns.forEach((column, index) => {
|
||||||
expect(th.at(2).text()).toContain('Country');
|
expect(th.at(index + 1).html()).toContain(column);
|
||||||
expect(th.at(3).text()).toContain('City');
|
});
|
||||||
expect(th.at(4).text()).toContain('Browser');
|
|
||||||
expect(th.at(5).text()).toContain('OS');
|
|
||||||
expect(th.at(6).text()).toContain('Referrer');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows warning when no visits are found', () => {
|
it('shows warning when no visits are found', () => {
|
||||||
|
@ -137,10 +156,12 @@ describe('<VisitsTable />', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
[ true, 8 ],
|
[ true, '2.6.0' as SemVer, 8 ],
|
||||||
[ false, 7 ],
|
[ false, '2.6.0' as SemVer, 7 ],
|
||||||
])('displays proper amount of columns for orphan and non-orphan visits', (isOrphanVisits, expectedCols) => {
|
[ true, '2.7.0' as SemVer, 9 ],
|
||||||
const wrapper = createWrapper([], [], isOrphanVisits);
|
[ false, '2.7.0' as SemVer, 8 ],
|
||||||
|
])('displays proper amount of columns for orphan and non-orphan visits', (isOrphanVisits, version, expectedCols) => {
|
||||||
|
const wrapper = createOrphanVisitsWrapper(isOrphanVisits, version);
|
||||||
const rowsWithColspan = wrapper.find('[colSpan]');
|
const rowsWithColspan = wrapper.find('[colSpan]');
|
||||||
const cols = wrapper.find('th');
|
const cols = wrapper.find('th');
|
||||||
|
|
||||||
|
@ -148,4 +169,12 @@ describe('<VisitsTable />', () => {
|
||||||
expect(rowsWithColspan).toHaveLength(2);
|
expect(rowsWithColspan).toHaveLength(2);
|
||||||
rowsWithColspan.forEach((row) => expect(row.prop('colSpan')).toEqual(expectedCols));
|
rowsWithColspan.forEach((row) => expect(row.prop('colSpan')).toEqual(expectedCols));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('displays bots icon when a visit is a potential bot', () => {
|
||||||
|
const wrapper = createWrapperWithBots();
|
||||||
|
const rows = wrapper.find('tbody').find('tr');
|
||||||
|
|
||||||
|
expect(rows.at(0).find('td').at(1).text()).not.toContain('FontAwesomeIcon');
|
||||||
|
expect(rows.at(1).find('td').at(1).text()).toContain('FontAwesomeIcon');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -39,6 +39,7 @@ describe('VisitsExporter', () => {
|
||||||
longitude: 0,
|
longitude: 0,
|
||||||
os: 'os',
|
os: 'os',
|
||||||
referer: 'referer',
|
referer: 'referer',
|
||||||
|
potentialBot: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,7 @@ describe('VisitsParser', () => {
|
||||||
}),
|
}),
|
||||||
Mock.of<Visit>({
|
Mock.of<Visit>({
|
||||||
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36 OPR/38.0.2220.41',
|
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36 OPR/38.0.2220.41',
|
||||||
|
potentialBot: true,
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
const orphanVisits: OrphanVisit[] = [
|
const orphanVisits: OrphanVisit[] = [
|
||||||
|
@ -61,6 +62,7 @@ describe('VisitsParser', () => {
|
||||||
Mock.of<OrphanVisit>({
|
Mock.of<OrphanVisit>({
|
||||||
type: 'regular_404',
|
type: 'regular_404',
|
||||||
visitedUrl: 'bar',
|
visitedUrl: 'bar',
|
||||||
|
potentialBot: true,
|
||||||
}),
|
}),
|
||||||
Mock.of<OrphanVisit>({
|
Mock.of<OrphanVisit>({
|
||||||
type: 'invalid_short_url',
|
type: 'invalid_short_url',
|
||||||
|
@ -73,6 +75,7 @@ describe('VisitsParser', () => {
|
||||||
latitude: 123.45,
|
latitude: 123.45,
|
||||||
longitude: -543.21,
|
longitude: -543.21,
|
||||||
},
|
},
|
||||||
|
potentialBot: false,
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -176,6 +179,7 @@ describe('VisitsParser', () => {
|
||||||
date: undefined,
|
date: undefined,
|
||||||
latitude: 123.45,
|
latitude: 123.45,
|
||||||
longitude: -543.21,
|
longitude: -543.21,
|
||||||
|
potentialBot: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
browser: 'Firefox',
|
browser: 'Firefox',
|
||||||
|
@ -186,6 +190,7 @@ describe('VisitsParser', () => {
|
||||||
date: undefined,
|
date: undefined,
|
||||||
latitude: 1029,
|
latitude: 1029,
|
||||||
longitude: 6758,
|
longitude: 6758,
|
||||||
|
potentialBot: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
browser: 'Chrome',
|
browser: 'Chrome',
|
||||||
|
@ -196,6 +201,7 @@ describe('VisitsParser', () => {
|
||||||
date: undefined,
|
date: undefined,
|
||||||
latitude: undefined,
|
latitude: undefined,
|
||||||
longitude: undefined,
|
longitude: undefined,
|
||||||
|
potentialBot: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
browser: 'Chrome',
|
browser: 'Chrome',
|
||||||
|
@ -206,6 +212,7 @@ describe('VisitsParser', () => {
|
||||||
date: undefined,
|
date: undefined,
|
||||||
latitude: 123.45,
|
latitude: 123.45,
|
||||||
longitude: -543.21,
|
longitude: -543.21,
|
||||||
|
potentialBot: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
browser: 'Opera',
|
browser: 'Opera',
|
||||||
|
@ -216,6 +223,7 @@ describe('VisitsParser', () => {
|
||||||
date: undefined,
|
date: undefined,
|
||||||
latitude: undefined,
|
latitude: undefined,
|
||||||
longitude: undefined,
|
longitude: undefined,
|
||||||
|
potentialBot: true,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
@ -233,6 +241,7 @@ describe('VisitsParser', () => {
|
||||||
longitude: 6758,
|
longitude: 6758,
|
||||||
type: 'base_url',
|
type: 'base_url',
|
||||||
visitedUrl: 'foo',
|
visitedUrl: 'foo',
|
||||||
|
potentialBot: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'regular_404',
|
type: 'regular_404',
|
||||||
|
@ -245,6 +254,7 @@ describe('VisitsParser', () => {
|
||||||
longitude: undefined,
|
longitude: undefined,
|
||||||
os: 'Others',
|
os: 'Others',
|
||||||
referer: 'Direct',
|
referer: 'Direct',
|
||||||
|
potentialBot: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
browser: 'Chrome',
|
browser: 'Chrome',
|
||||||
|
@ -257,6 +267,7 @@ describe('VisitsParser', () => {
|
||||||
longitude: -543.21,
|
longitude: -543.21,
|
||||||
type: 'invalid_short_url',
|
type: 'invalid_short_url',
|
||||||
visitedUrl: 'bar',
|
visitedUrl: 'bar',
|
||||||
|
potentialBot: false,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue