Add report percentages to dashboard and details view (#5923)
* Update report percentages on dashboard and details view * Add percentages to Countries, Regions, and Cities reports * Add percentages to Channels, Sources, and UTM reports * Add percentages to top pages, entry pages, and exit pages reports * Update tests to include percentages * Change dashboard copy from title case to sentence case * Update details modal style * Make animations snappier * Introduce max height to modal and make inner content scrollable * Improve modal mobile design - Enable horizontal scroll for details modal on mobile - Add responsive spacing and positioning to modal * Added mobile tap behavior to external link in list report * Show tooltips only when in comparison mode or when the number is abbreviated * remove previously added showTooltip prop - This isn't needed anymore since we now handle the tooltip logic in the MetricValue component * Show long format upon hovering detailed view metrics * Added mobile tapping behaviour to detailed view * Added percentages to all detailed views * Add mobile swipe-to-close behavior for modal * Adjust sensitivity of modal drag to close * Use hammerjs for swipe-to-close modal behaviour * Prevent dragging if gesture starts inside table * Show 2 decimal places for percentages < 0.1% across dashboard * Adjust dark mode styles * Add hover effect to external link icon * Update tests to expect two-decimal percentages * Undo hammer install and revert to old modal styling * Remove CR and % columns from goals and custom props reports on dashboard, and show on hover in detailed view * Remove unused constants * Undo conversion rate on hover behaviour - Unlike percentages, CR should show permanently. * Show percentages permanently in custom props detailed view * Adjust width of conversion metrics column * Updated metric-value test * Update top-bar test * Added changelog entry * Fix test expectations for percentages with imported data - Update tests to expect correct percentages (≤100%) when imported data is included. These tests will fail until the percentage calculation bug is fixed, documenting the expected behavior. * Add imported_visitors to tests to ensure correct total_visitors calculation * Correct imported_visitors count in test
This commit is contained in:
parent
6446e15871
commit
dfeda94e06
|
|
@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- A visitor percentage breakdown is now shown on all reports, both on the dashboard and in the detailed breakdown
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,7 @@
|
||||||
--color-gray-950: var(--color-zinc-950);
|
--color-gray-950: var(--color-zinc-950);
|
||||||
|
|
||||||
/* Custom gray shades from config (override some zinc values) */
|
/* Custom gray shades from config (override some zinc values) */
|
||||||
|
--color-gray-75: rgb(247 247 248);
|
||||||
--color-gray-150: rgb(236 236 238);
|
--color-gray-150: rgb(236 236 238);
|
||||||
--color-gray-750: rgb(50 50 54);
|
--color-gray-750: rgb(50 50 54);
|
||||||
--color-gray-825: rgb(35 35 38);
|
--color-gray-825: rgb(35 35 38);
|
||||||
|
|
@ -294,16 +295,12 @@ blockquote {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-striped tbody tr:nth-child(odd) {
|
.table-striped tbody tr:nth-child(odd) td {
|
||||||
background-color: var(--color-gray-100);
|
background-color: var(--color-gray-75);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .table-striped tbody tr:nth-child(odd) {
|
.dark .table-striped tbody tr:nth-child(odd) td {
|
||||||
background-color: var(--color-gray-800);
|
background-color: var(--color-gray-850);
|
||||||
}
|
|
||||||
|
|
||||||
.dark .table-striped tbody tr:nth-child(even) {
|
|
||||||
background-color: var(--color-gray-900);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-enter {
|
.fade-enter {
|
||||||
|
|
|
||||||
|
|
@ -32,33 +32,6 @@
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal__container {
|
|
||||||
background-color: #fff;
|
|
||||||
padding: 1rem 2rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin: 50px auto;
|
|
||||||
box-sizing: border-box;
|
|
||||||
min-height: 509px;
|
|
||||||
transition: height 200ms ease-in;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal__close {
|
|
||||||
position: fixed;
|
|
||||||
color: #b8c2cc;
|
|
||||||
font-size: 48px;
|
|
||||||
font-weight: bold;
|
|
||||||
top: 12px;
|
|
||||||
right: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal__close::before {
|
|
||||||
content: '\2715';
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal__content {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes mm-fade-in {
|
@keyframes mm-fade-in {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ export const SearchInput = ({
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={isFocused ? placeholderFocused : placeholderUnfocused}
|
placeholder={isFocused ? placeholderFocused : placeholderUnfocused}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'dark:text-gray-100 block border-gray-300 dark:border-gray-750 rounded-md dark:bg-gray-750 w-48 dark:placeholder:text-gray-400 focus:outline-none focus:ring-3 focus:ring-indigo-500/20 dark:focus:ring-indigo-500/25 focus:border-indigo-500',
|
'text-sm dark:text-gray-100 block border-gray-300 dark:border-gray-750 rounded-md dark:bg-gray-750 max-w-64 w-full dark:placeholder:text-gray-400 focus:outline-none focus:ring-3 focus:ring-indigo-500/20 dark:focus:ring-indigo-500/25 focus:border-indigo-500',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
onChange={debouncedOnSearchInputChange}
|
onChange={debouncedOnSearchInputChange}
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,18 @@ export const SortButton = ({
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={toggleSort}
|
onClick={toggleSort}
|
||||||
className={classNames('group', 'hover:underline', 'relative')}
|
className={classNames(
|
||||||
|
'group',
|
||||||
|
'hover:text-gray-700 dark:hover:text-gray-200 transition-colors duration-100',
|
||||||
|
'relative'
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<span
|
<span
|
||||||
title={next.hint}
|
title={next.hint}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'absolute',
|
'absolute',
|
||||||
'rounded inline-block h-4 w-4',
|
'rounded inline-block size-4',
|
||||||
'ml-1',
|
'ml-1',
|
||||||
{
|
{
|
||||||
[SortDirection.asc]: 'rotate-180',
|
[SortDirection.asc]: 'rotate-180',
|
||||||
|
|
@ -30,9 +34,8 @@ export const SortButton = ({
|
||||||
}[sortDirection ?? next.direction],
|
}[sortDirection ?? next.direction],
|
||||||
!sortDirection && 'opacity-0',
|
!sortDirection && 'opacity-0',
|
||||||
!sortDirection && 'group-hover:opacity-100',
|
!sortDirection && 'group-hover:opacity-100',
|
||||||
sortDirection &&
|
'group-hover:bg-gray-100 dark:group-hover:bg-gray-900',
|
||||||
'group-hover:bg-gray-100 dark:group-hover:bg-gray-900',
|
'transition-all duration-100'
|
||||||
'transition'
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
↓
|
↓
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ export type ColumnConfiguraton<T extends Record<string, unknown>> = {
|
||||||
/**
|
/**
|
||||||
* Function used to transform the value found at item[key] for the cell. Superseded by renderItem if present. @example 1120 => "1.1k"
|
* Function used to transform the value found at item[key] for the cell. Superseded by renderItem if present. @example 1120 => "1.1k"
|
||||||
*/
|
*/
|
||||||
renderValue?: (item: T) => ReactNode
|
renderValue?: (item: T, isRowHovered?: boolean) => ReactNode
|
||||||
/** Function used to create richer cells */
|
/** Function used to create richer cells */
|
||||||
renderItem?: (item: T) => ReactNode
|
renderItem?: (item: T) => ReactNode
|
||||||
}
|
}
|
||||||
|
|
@ -38,7 +38,7 @@ export const TableHeaderCell = ({
|
||||||
return (
|
return (
|
||||||
<th
|
<th
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'p-2 text-xs font-bold text-gray-500 dark:text-gray-400 tracking-wide',
|
'p-2 text-xs font-semibold text-gray-500 dark:text-gray-400',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
align={align}
|
align={align}
|
||||||
|
|
@ -58,7 +58,13 @@ export const TableCell = ({
|
||||||
align?: 'left' | 'right'
|
align?: 'left' | 'right'
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<td className={classNames('p-2 font-medium', className)} align={align}>
|
<td
|
||||||
|
className={classNames(
|
||||||
|
'p-2 font-medium first:rounded-s-sm last:rounded-e-sm',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
align={align}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</td>
|
</td>
|
||||||
)
|
)
|
||||||
|
|
@ -68,15 +74,42 @@ export const ItemRow = <T extends Record<string, string | number | ReactNode>>({
|
||||||
rowIndex,
|
rowIndex,
|
||||||
pageIndex,
|
pageIndex,
|
||||||
item,
|
item,
|
||||||
columns
|
columns,
|
||||||
|
tappedRowName,
|
||||||
|
onRowTap
|
||||||
}: {
|
}: {
|
||||||
rowIndex: number
|
rowIndex: number
|
||||||
pageIndex?: number
|
pageIndex?: number
|
||||||
item: T
|
item: T
|
||||||
columns: ColumnConfiguraton<T>[]
|
columns: ColumnConfiguraton<T>[]
|
||||||
|
tappedRowName?: string | null
|
||||||
|
onRowTap?: (rowName: string | null) => void
|
||||||
}) => {
|
}) => {
|
||||||
|
const [isHovered, setIsHovered] = React.useState(false)
|
||||||
|
|
||||||
|
const rowName = (item as unknown as { name: string }).name
|
||||||
|
const isTapped = tappedRowName === rowName
|
||||||
|
const isRowActive = isHovered || isTapped
|
||||||
|
|
||||||
|
const handleRowClick = (e: React.MouseEvent) => {
|
||||||
|
if (window.innerWidth < 768 && !(e.target as HTMLElement).closest('a')) {
|
||||||
|
if (onRowTap) {
|
||||||
|
if (isTapped) {
|
||||||
|
onRowTap(null)
|
||||||
|
} else {
|
||||||
|
onRowTap(rowName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr className="text-sm dark:text-gray-200">
|
<tr
|
||||||
|
className="group text-sm dark:text-gray-200 md:cursor-default cursor-pointer"
|
||||||
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
|
onClick={handleRowClick}
|
||||||
|
>
|
||||||
{columns.map(({ key, width, align, renderValue, renderItem }) => (
|
{columns.map(({ key, width, align, renderValue, renderItem }) => (
|
||||||
<TableCell
|
<TableCell
|
||||||
key={`${(pageIndex ?? null) === null ? '' : `page_${pageIndex}_`}row_${rowIndex}_${String(key)}`}
|
key={`${(pageIndex ?? null) === null ? '' : `page_${pageIndex}_`}row_${rowIndex}_${String(key)}`}
|
||||||
|
|
@ -86,7 +119,7 @@ export const ItemRow = <T extends Record<string, string | number | ReactNode>>({
|
||||||
{renderItem
|
{renderItem
|
||||||
? renderItem(item)
|
? renderItem(item)
|
||||||
: renderValue
|
: renderValue
|
||||||
? renderValue(item)
|
? renderValue(item, isRowActive)
|
||||||
: (item[key] ?? '')}
|
: (item[key] ?? '')}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
))}
|
))}
|
||||||
|
|
@ -101,6 +134,8 @@ export const Table = <T extends Record<string, string | number | ReactNode>>({
|
||||||
columns: ColumnConfiguraton<T>[]
|
columns: ColumnConfiguraton<T>[]
|
||||||
data: T[] | { pages: T[][] }
|
data: T[] | { pages: T[][] }
|
||||||
}) => {
|
}) => {
|
||||||
|
const [tappedRowName, setTappedRowName] = React.useState<string | null>(null)
|
||||||
|
|
||||||
const renderColumnLabel = (column: ColumnConfiguraton<T>) => {
|
const renderColumnLabel = (column: ColumnConfiguraton<T>) => {
|
||||||
if (column.metricWarning) {
|
if (column.metricWarning) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -125,13 +160,13 @@ export const Table = <T extends Record<string, string | number | ReactNode>>({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<table className="w-max overflow-x-auto md:w-full table-striped table-fixed">
|
<table className="border-collapse table-striped table-fixed w-max min-w-full">
|
||||||
<thead>
|
<thead className="sticky top-0 bg-white dark:bg-gray-900 z-10">
|
||||||
<tr className="text-xs font-bold text-gray-500 dark:text-gray-400">
|
<tr className="text-xs font-semibold text-gray-500 dark:text-gray-400">
|
||||||
{columns.map((column) => (
|
{columns.map((column) => (
|
||||||
<TableHeaderCell
|
<TableHeaderCell
|
||||||
key={`header_${String(column.key)}`}
|
key={`header_${String(column.key)}`}
|
||||||
className={classNames('p-2 tracking-wide', column.width)}
|
className={classNames('p-2', column.width)}
|
||||||
align={column.align}
|
align={column.align}
|
||||||
>
|
>
|
||||||
{column.onSort ? (
|
{column.onSort ? (
|
||||||
|
|
@ -156,6 +191,8 @@ export const Table = <T extends Record<string, string | number | ReactNode>>({
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowIndex={rowIndex}
|
rowIndex={rowIndex}
|
||||||
key={rowIndex}
|
key={rowIndex}
|
||||||
|
tappedRowName={tappedRowName}
|
||||||
|
onRowTap={setTappedRowName}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
: data.pages.map((page, pageIndex) =>
|
: data.pages.map((page, pageIndex) =>
|
||||||
|
|
@ -166,6 +203,8 @@ export const Table = <T extends Record<string, string | number | ReactNode>>({
|
||||||
rowIndex={rowIndex}
|
rowIndex={rowIndex}
|
||||||
pageIndex={pageIndex}
|
pageIndex={pageIndex}
|
||||||
key={`page_${pageIndex}_row_${rowIndex}`}
|
key={`page_${pageIndex}_row_${rowIndex}`}
|
||||||
|
tappedRowName={tappedRowName}
|
||||||
|
onRowTap={setTappedRowName}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,7 @@ const Items = ({
|
||||||
<SearchInput
|
<SearchInput
|
||||||
searchRef={searchRef}
|
searchRef={searchRef}
|
||||||
placeholderUnfocused="Press / to search"
|
placeholderUnfocused="Press / to search"
|
||||||
className="ml-auto w-full py-1 text-sm"
|
className="ml-auto w-full py-1"
|
||||||
onSearch={handleSearchInput}
|
onSearch={handleSearchInput}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ export const SearchableSegmentsSection = ({
|
||||||
<SearchInput
|
<SearchInput
|
||||||
searchRef={searchRef}
|
searchRef={searchRef}
|
||||||
placeholderUnfocused="Press / to search"
|
placeholderUnfocused="Press / to search"
|
||||||
className="ml-auto w-full py-1 text-sm"
|
className="ml-auto w-full py-1"
|
||||||
onSearch={handleSearchInput}
|
onSearch={handleSearchInput}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ test('user can open and close filters dropdown', async () => {
|
||||||
'Location',
|
'Location',
|
||||||
'Screen size',
|
'Screen size',
|
||||||
'Browser',
|
'Browser',
|
||||||
'Operating System',
|
'Operating system',
|
||||||
'Goal'
|
'Goal'
|
||||||
])
|
])
|
||||||
await userEvent.click(toggleFilters)
|
await userEvent.click(toggleFilters)
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ export default function Bar({
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full relative" style={style}>
|
<div className="w-full h-full relative" style={style}>
|
||||||
<div
|
<div
|
||||||
className={`absolute top-0 left-0 h-full rounded-sm transition-colors duration-150 ${bg || ''}`}
|
className={`absolute top-0 left-0 h-full rounded-sm ${bg || ''}`}
|
||||||
style={{ width: `${width}%` }}
|
style={{ width: `${width}%` }}
|
||||||
></div>
|
></div>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ export default function Conversions({ afterFetchData, onGoalFilterClick }) {
|
||||||
path: conversionsRoute.path,
|
path: conversionsRoute.path,
|
||||||
search: (search) => search
|
search: (search) => search
|
||||||
}}
|
}}
|
||||||
color="bg-red-50 group-hover:bg-red-100"
|
color="bg-red-50 group-hover/row:bg-red-100"
|
||||||
colMinWidth={90}
|
colMinWidth={90}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,6 @@ function SpecialPropBreakdown({ prop, afterFetchData }) {
|
||||||
search: (search) => search
|
search: (search) => search
|
||||||
}}
|
}}
|
||||||
getExternalLinkUrl={getExternalLinkUrlFactory()}
|
getExternalLinkUrl={getExternalLinkUrlFactory()}
|
||||||
maybeHideDetails={true}
|
|
||||||
color="bg-red-50"
|
color="bg-red-50"
|
||||||
colMinWidth={90}
|
colMinWidth={90}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,8 @@ export const PROPS = 'props'
|
||||||
export const FUNNELS = 'funnels'
|
export const FUNNELS = 'funnels'
|
||||||
|
|
||||||
export const sectionTitles = {
|
export const sectionTitles = {
|
||||||
[CONVERSIONS]: 'Goal Conversions',
|
[CONVERSIONS]: 'Goal conversions',
|
||||||
[PROPS]: 'Custom Properties',
|
[PROPS]: 'Custom properties',
|
||||||
[FUNNELS]: 'Funnels'
|
[FUNNELS]: 'Funnels'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -137,8 +137,7 @@ export default function Properties({ afterFetchData }) {
|
||||||
params: { propKey },
|
params: { propKey },
|
||||||
search: (search) => search
|
search: (search) => search
|
||||||
}}
|
}}
|
||||||
maybeHideDetails={true}
|
color="bg-red-50 group-hover/row:bg-red-100"
|
||||||
color="bg-red-50 group-hover:bg-red-100"
|
|
||||||
colMinWidth={90}
|
colMinWidth={90}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -76,8 +76,9 @@ function Browsers({ afterFetchData }) {
|
||||||
function chooseMetrics() {
|
function chooseMetrics() {
|
||||||
return [
|
return [
|
||||||
metrics.createVisitors({ meta: { plot: true } }),
|
metrics.createVisitors({ meta: { plot: true } }),
|
||||||
hasConversionGoalFilter(query) && metrics.createConversionRate(),
|
!hasConversionGoalFilter(query) &&
|
||||||
!hasConversionGoalFilter(query) && metrics.createPercentage()
|
metrics.createPercentage({ meta: { showOnHover: true } }),
|
||||||
|
hasConversionGoalFilter(query) && metrics.createConversionRate()
|
||||||
].filter((metric) => !!metric)
|
].filter((metric) => !!metric)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -121,8 +122,9 @@ function BrowserVersions({ afterFetchData }) {
|
||||||
function chooseMetrics() {
|
function chooseMetrics() {
|
||||||
return [
|
return [
|
||||||
metrics.createVisitors({ meta: { plot: true } }),
|
metrics.createVisitors({ meta: { plot: true } }),
|
||||||
hasConversionGoalFilter(query) && metrics.createConversionRate(),
|
!hasConversionGoalFilter(query) &&
|
||||||
!hasConversionGoalFilter(query) && metrics.createPercentage()
|
metrics.createPercentage({ meta: { showOnHover: true } }),
|
||||||
|
hasConversionGoalFilter(query) && metrics.createConversionRate()
|
||||||
].filter((metric) => !!metric)
|
].filter((metric) => !!metric)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -187,9 +189,11 @@ function OperatingSystems({ afterFetchData }) {
|
||||||
function chooseMetrics() {
|
function chooseMetrics() {
|
||||||
return [
|
return [
|
||||||
metrics.createVisitors({ meta: { plot: true } }),
|
metrics.createVisitors({ meta: { plot: true } }),
|
||||||
hasConversionGoalFilter(query) && metrics.createConversionRate(),
|
|
||||||
!hasConversionGoalFilter(query) &&
|
!hasConversionGoalFilter(query) &&
|
||||||
metrics.createPercentage({ meta: { hiddenonMobile: true } })
|
metrics.createPercentage({
|
||||||
|
meta: { showOnHover: true, hiddenOnMobile: true }
|
||||||
|
}),
|
||||||
|
hasConversionGoalFilter(query) && metrics.createConversionRate()
|
||||||
].filter((metric) => !!metric)
|
].filter((metric) => !!metric)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -238,8 +242,9 @@ function OperatingSystemVersions({ afterFetchData }) {
|
||||||
function chooseMetrics() {
|
function chooseMetrics() {
|
||||||
return [
|
return [
|
||||||
metrics.createVisitors({ meta: { plot: true } }),
|
metrics.createVisitors({ meta: { plot: true } }),
|
||||||
hasConversionGoalFilter(query) && metrics.createConversionRate(),
|
!hasConversionGoalFilter(query) &&
|
||||||
!hasConversionGoalFilter(query) && metrics.createPercentage()
|
metrics.createPercentage({ meta: { showOnHover: true } }),
|
||||||
|
hasConversionGoalFilter(query) && metrics.createConversionRate()
|
||||||
].filter((metric) => !!metric)
|
].filter((metric) => !!metric)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -281,8 +286,9 @@ function ScreenSizes({ afterFetchData }) {
|
||||||
function chooseMetrics() {
|
function chooseMetrics() {
|
||||||
return [
|
return [
|
||||||
metrics.createVisitors({ meta: { plot: true } }),
|
metrics.createVisitors({ meta: { plot: true } }),
|
||||||
hasConversionGoalFilter(query) && metrics.createConversionRate(),
|
!hasConversionGoalFilter(query) &&
|
||||||
!hasConversionGoalFilter(query) && metrics.createPercentage()
|
metrics.createPercentage({ meta: { showOnHover: true } }),
|
||||||
|
hasConversionGoalFilter(query) && metrics.createConversionRate()
|
||||||
].filter((metric) => !!metric)
|
].filter((metric) => !!metric)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -432,7 +438,7 @@ export default function Devices() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="group/report overflow-x-hidden">
|
||||||
<div className="flex justify-between w-full">
|
<div className="flex justify-between w-full">
|
||||||
<div className="flex gap-x-1">
|
<div className="flex gap-x-1">
|
||||||
<h3 className="font-bold dark:text-gray-100">Devices</h3>
|
<h3 className="font-bold dark:text-gray-100">Devices</h3>
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
export const METRIC_LABELS = {
|
export const METRIC_LABELS = {
|
||||||
visitors: 'Visitors',
|
visitors: 'Visitors',
|
||||||
pageviews: 'Pageviews',
|
pageviews: 'Pageviews',
|
||||||
events: 'Total Conversions',
|
events: 'Total conversions',
|
||||||
views_per_visit: 'Views per Visit',
|
views_per_visit: 'Views per visit',
|
||||||
visits: 'Visits',
|
visits: 'Visits',
|
||||||
bounce_rate: 'Bounce Rate',
|
bounce_rate: 'Bounce rate',
|
||||||
visit_duration: 'Visit Duration',
|
visit_duration: 'Visit duration',
|
||||||
conversions: 'Converted Visitors',
|
conversions: 'Converted visitors',
|
||||||
conversion_rate: 'Conversion Rate',
|
conversion_rate: 'Conversion rate',
|
||||||
average_revenue: 'Average Revenue',
|
average_revenue: 'Average revenue',
|
||||||
total_revenue: 'Total Revenue',
|
total_revenue: 'Total revenue',
|
||||||
scroll_depth: 'Scroll Depth',
|
scroll_depth: 'Scroll depth',
|
||||||
time_on_page: 'Time on Page'
|
time_on_page: 'Time on page'
|
||||||
}
|
}
|
||||||
|
|
||||||
function plottable(dataArray) {
|
function plottable(dataArray) {
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,8 @@ function Countries({ query, site, onClick, afterFetchData }) {
|
||||||
function chooseMetrics() {
|
function chooseMetrics() {
|
||||||
return [
|
return [
|
||||||
metrics.createVisitors({ meta: { plot: true } }),
|
metrics.createVisitors({ meta: { plot: true } }),
|
||||||
|
!hasConversionGoalFilter(query) &&
|
||||||
|
metrics.createPercentage({ meta: { showOnHover: true } }),
|
||||||
hasConversionGoalFilter(query) && metrics.createConversionRate()
|
hasConversionGoalFilter(query) && metrics.createConversionRate()
|
||||||
].filter((metric) => !!metric)
|
].filter((metric) => !!metric)
|
||||||
}
|
}
|
||||||
|
|
@ -54,7 +56,7 @@ function Countries({ query, site, onClick, afterFetchData }) {
|
||||||
search: (search) => search
|
search: (search) => search
|
||||||
}}
|
}}
|
||||||
renderIcon={renderIcon}
|
renderIcon={renderIcon}
|
||||||
color="bg-orange-50 group-hover:bg-orange-100"
|
color="bg-orange-50 group-hover/row:bg-orange-100"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -79,6 +81,8 @@ function Regions({ query, site, onClick, afterFetchData }) {
|
||||||
function chooseMetrics() {
|
function chooseMetrics() {
|
||||||
return [
|
return [
|
||||||
metrics.createVisitors({ meta: { plot: true } }),
|
metrics.createVisitors({ meta: { plot: true } }),
|
||||||
|
!hasConversionGoalFilter(query) &&
|
||||||
|
metrics.createPercentage({ meta: { showOnHover: true } }),
|
||||||
hasConversionGoalFilter(query) && metrics.createConversionRate()
|
hasConversionGoalFilter(query) && metrics.createConversionRate()
|
||||||
].filter((metric) => !!metric)
|
].filter((metric) => !!metric)
|
||||||
}
|
}
|
||||||
|
|
@ -93,7 +97,7 @@ function Regions({ query, site, onClick, afterFetchData }) {
|
||||||
metrics={chooseMetrics()}
|
metrics={chooseMetrics()}
|
||||||
detailsLinkProps={{ path: regionsRoute.path, search: (search) => search }}
|
detailsLinkProps={{ path: regionsRoute.path, search: (search) => search }}
|
||||||
renderIcon={renderIcon}
|
renderIcon={renderIcon}
|
||||||
color="bg-orange-50 group-hover:bg-orange-100"
|
color="bg-orange-50 group-hover/row:bg-orange-100"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -118,6 +122,8 @@ function Cities({ query, site, afterFetchData }) {
|
||||||
function chooseMetrics() {
|
function chooseMetrics() {
|
||||||
return [
|
return [
|
||||||
metrics.createVisitors({ meta: { plot: true } }),
|
metrics.createVisitors({ meta: { plot: true } }),
|
||||||
|
!hasConversionGoalFilter(query) &&
|
||||||
|
metrics.createPercentage({ meta: { showOnHover: true } }),
|
||||||
hasConversionGoalFilter(query) && metrics.createConversionRate()
|
hasConversionGoalFilter(query) && metrics.createConversionRate()
|
||||||
].filter((metric) => !!metric)
|
].filter((metric) => !!metric)
|
||||||
}
|
}
|
||||||
|
|
@ -131,7 +137,7 @@ function Cities({ query, site, afterFetchData }) {
|
||||||
metrics={chooseMetrics()}
|
metrics={chooseMetrics()}
|
||||||
detailsLinkProps={{ path: citiesRoute.path, search: (search) => search }}
|
detailsLinkProps={{ path: citiesRoute.path, search: (search) => search }}
|
||||||
renderIcon={renderIcon}
|
renderIcon={renderIcon}
|
||||||
color="bg-orange-50 group-hover:bg-orange-100"
|
color="bg-orange-50 group-hover/row:bg-orange-100"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -247,7 +253,7 @@ class Locations extends React.Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="group/report overflow-x-hidden">
|
||||||
<div className="w-full flex justify-between">
|
<div className="w-full flex justify-between">
|
||||||
<div className="flex gap-x-1">
|
<div className="flex gap-x-1">
|
||||||
<h3 className="font-bold dark:text-gray-100">
|
<h3 className="font-bold dark:text-gray-100">
|
||||||
|
|
|
||||||
|
|
@ -185,7 +185,7 @@ const WorldMap = ({
|
||||||
path: countriesRoute.path,
|
path: countriesRoute.path,
|
||||||
search: (search: Record<string, unknown>) => search
|
search: (search: Record<string, unknown>) => search
|
||||||
}}
|
}}
|
||||||
className={undefined}
|
className="mt-3"
|
||||||
onClick={undefined}
|
onClick={undefined}
|
||||||
/>
|
/>
|
||||||
{site.isDbip && <GeolocationNotice />}
|
{site.isDbip && <GeolocationNotice />}
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,14 @@ import {
|
||||||
useRememberOrderBy
|
useRememberOrderBy
|
||||||
} from '../../hooks/use-order-by'
|
} from '../../hooks/use-order-by'
|
||||||
import { Metric } from '../reports/metrics'
|
import { Metric } from '../reports/metrics'
|
||||||
|
import * as metricsModule from '../reports/metrics'
|
||||||
import { BreakdownResultMeta, DashboardQuery } from '../../query'
|
import { BreakdownResultMeta, DashboardQuery } from '../../query'
|
||||||
import { ColumnConfiguraton } from '../../components/table'
|
import { ColumnConfiguraton } from '../../components/table'
|
||||||
import { BreakdownTable } from './breakdown-table'
|
import { BreakdownTable } from './breakdown-table'
|
||||||
import { useSiteContext } from '../../site-context'
|
import { useSiteContext } from '../../site-context'
|
||||||
import { DrilldownLink, FilterInfo } from '../../components/drilldown-link'
|
import { DrilldownLink, FilterInfo } from '../../components/drilldown-link'
|
||||||
import { SharedReportProps } from '../reports/list'
|
import { SharedReportProps } from '../reports/list'
|
||||||
|
import { hasConversionGoalFilter } from '../../util/filters'
|
||||||
|
|
||||||
export type ReportInfo = {
|
export type ReportInfo = {
|
||||||
/** Title of the report to render on the top left. */
|
/** Title of the report to render on the top left. */
|
||||||
|
|
@ -35,6 +37,8 @@ type BreakdownModalProps = {
|
||||||
/** Function that must return a new query that contains appropriate search filter for searchValue param. */
|
/** Function that must return a new query that contains appropriate search filter for searchValue param. */
|
||||||
addSearchFilter?: (q: DashboardQuery, searchValue: string) => DashboardQuery
|
addSearchFilter?: (q: DashboardQuery, searchValue: string) => DashboardQuery
|
||||||
searchEnabled?: boolean
|
searchEnabled?: boolean
|
||||||
|
/** When true, keep the percentage metric as a permanently visible, sortable column. */
|
||||||
|
showPercentageColumn?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -62,6 +66,7 @@ export default function BreakdownModal<TListItem extends { name: string }>({
|
||||||
renderIcon,
|
renderIcon,
|
||||||
getExternalLinkUrl,
|
getExternalLinkUrl,
|
||||||
searchEnabled = true,
|
searchEnabled = true,
|
||||||
|
showPercentageColumn = false,
|
||||||
afterFetchData,
|
afterFetchData,
|
||||||
afterFetchNextPage,
|
afterFetchNextPage,
|
||||||
addSearchFilter,
|
addSearchFilter,
|
||||||
|
|
@ -71,20 +76,28 @@ export default function BreakdownModal<TListItem extends { name: string }>({
|
||||||
const { query } = useQueryContext()
|
const { query } = useQueryContext()
|
||||||
const [meta, setMeta] = useState<BreakdownResultMeta | null>(null)
|
const [meta, setMeta] = useState<BreakdownResultMeta | null>(null)
|
||||||
|
|
||||||
|
const breakdownMetrics = useMemo(() => {
|
||||||
|
const hasPercentage = metrics.some((m) => m.key === 'percentage')
|
||||||
|
if (!hasPercentage && !hasConversionGoalFilter(query)) {
|
||||||
|
return [...metrics, metricsModule.createPercentage()]
|
||||||
|
}
|
||||||
|
return metrics
|
||||||
|
}, [metrics, query])
|
||||||
|
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const defaultOrderBy = getStoredOrderBy({
|
const defaultOrderBy = getStoredOrderBy({
|
||||||
domain: site.domain,
|
domain: site.domain,
|
||||||
reportInfo,
|
reportInfo,
|
||||||
metrics,
|
metrics: breakdownMetrics,
|
||||||
fallbackValue: reportInfo.defaultOrder ? [reportInfo.defaultOrder] : []
|
fallbackValue: reportInfo.defaultOrder ? [reportInfo.defaultOrder] : []
|
||||||
})
|
})
|
||||||
const { orderBy, orderByDictionary, toggleSortByMetric } = useOrderBy({
|
const { orderBy, orderByDictionary, toggleSortByMetric } = useOrderBy({
|
||||||
metrics,
|
metrics: breakdownMetrics,
|
||||||
defaultOrderBy
|
defaultOrderBy
|
||||||
})
|
})
|
||||||
useRememberOrderBy({
|
useRememberOrderBy({
|
||||||
effectiveOrderBy: orderBy,
|
effectiveOrderBy: orderBy,
|
||||||
metrics,
|
metrics: breakdownMetrics,
|
||||||
reportInfo
|
reportInfo
|
||||||
})
|
})
|
||||||
const apiState = usePaginatedGetAPI<
|
const apiState = usePaginatedGetAPI<
|
||||||
|
|
@ -125,7 +138,7 @@ export default function BreakdownModal<TListItem extends { name: string }>({
|
||||||
{
|
{
|
||||||
label: reportInfo.dimensionLabel,
|
label: reportInfo.dimensionLabel,
|
||||||
key: 'name',
|
key: 'name',
|
||||||
width: 'w-48 md:w-full flex items-center break-all',
|
width: 'w-40 md:w-48',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
renderItem: (item) => (
|
renderItem: (item) => (
|
||||||
<NameCell
|
<NameCell
|
||||||
|
|
@ -136,29 +149,39 @@ export default function BreakdownModal<TListItem extends { name: string }>({
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
...metrics.map(
|
...breakdownMetrics
|
||||||
(m): ColumnConfiguraton<TListItem> => ({
|
.filter((m) => showPercentageColumn || m.key !== 'percentage')
|
||||||
label: m.renderLabel(query),
|
.map(
|
||||||
key: m.key,
|
(m): ColumnConfiguraton<TListItem> => ({
|
||||||
width: m.width,
|
label: m.renderLabel(query),
|
||||||
align: 'right',
|
key: m.key,
|
||||||
metricWarning: getMetricWarning(m, meta),
|
width: m.width,
|
||||||
renderValue: (item) => m.renderValue(item, meta),
|
align: 'right',
|
||||||
onSort: m.sortable ? () => toggleSortByMetric(m) : undefined,
|
metricWarning: getMetricWarning(m, meta),
|
||||||
sortDirection: orderByDictionary[m.key]
|
renderValue: (item, isRowHovered) =>
|
||||||
})
|
m.renderValue(
|
||||||
)
|
showPercentageColumn && m.key === 'visitors'
|
||||||
|
? { ...item, percentage: null }
|
||||||
|
: item,
|
||||||
|
meta,
|
||||||
|
{ detailedView: true, isRowHovered }
|
||||||
|
),
|
||||||
|
onSort: m.sortable ? () => toggleSortByMetric(m) : undefined,
|
||||||
|
sortDirection: orderByDictionary[m.key]
|
||||||
|
})
|
||||||
|
)
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
reportInfo.dimensionLabel,
|
reportInfo.dimensionLabel,
|
||||||
metrics,
|
breakdownMetrics,
|
||||||
getFilterInfo,
|
getFilterInfo,
|
||||||
query,
|
query,
|
||||||
orderByDictionary,
|
orderByDictionary,
|
||||||
toggleSortByMetric,
|
toggleSortByMetric,
|
||||||
renderIcon,
|
renderIcon,
|
||||||
getExternalLinkUrl,
|
getExternalLinkUrl,
|
||||||
meta
|
meta,
|
||||||
|
showPercentageColumn
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -190,7 +213,7 @@ const NameCell = <TListItem extends { name: string }>({
|
||||||
renderIcon?: (item: TListItem) => ReactNode
|
renderIcon?: (item: TListItem) => ReactNode
|
||||||
getExternalLinkUrl?: (listItem: TListItem) => string
|
getExternalLinkUrl?: (listItem: TListItem) => string
|
||||||
}) => (
|
}) => (
|
||||||
<>
|
<div className="max-w-full break-all flex items-center">
|
||||||
{typeof renderIcon === 'function' && renderIcon(item)}
|
{typeof renderIcon === 'function' && renderIcon(item)}
|
||||||
<DrilldownLink
|
<DrilldownLink
|
||||||
path={rootRoute.path}
|
path={rootRoute.path}
|
||||||
|
|
@ -203,7 +226,7 @@ const NameCell = <TListItem extends { name: string }>({
|
||||||
{typeof getExternalLinkUrl === 'function' && (
|
{typeof getExternalLinkUrl === 'function' && (
|
||||||
<ExternalLinkIcon url={getExternalLinkUrl(item)} />
|
<ExternalLinkIcon url={getExternalLinkUrl(item)} />
|
||||||
)}
|
)}
|
||||||
</>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
const ExternalLinkIcon = ({ url }: { url?: string }) =>
|
const ExternalLinkIcon = ({ url }: { url?: string }) =>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import React, { ReactNode, useRef } from 'react'
|
import React, { ReactNode, useRef } from 'react'
|
||||||
|
import { XMarkIcon } from '@heroicons/react/20/solid'
|
||||||
|
|
||||||
import { SearchInput } from '../../components/search-input'
|
import { SearchInput } from '../../components/search-input'
|
||||||
import { ColumnConfiguraton, Table } from '../../components/table'
|
import { ColumnConfiguraton, Table } from '../../components/table'
|
||||||
import RocketIcon from './rocket-icon'
|
import RocketIcon from './rocket-icon'
|
||||||
import { QueryStatus } from '@tanstack/react-query'
|
import { QueryStatus } from '@tanstack/react-query'
|
||||||
|
import { useAppNavigate } from '../../navigation/use-app-navigate'
|
||||||
const MIN_HEIGHT_PX = 500
|
import { rootRoute } from '../../router'
|
||||||
|
|
||||||
export const BreakdownTable = <TListItem extends { name: string }>({
|
export const BreakdownTable = <TListItem extends { name: string }>({
|
||||||
title,
|
title,
|
||||||
|
|
@ -19,7 +20,8 @@ export const BreakdownTable = <TListItem extends { name: string }>({
|
||||||
data,
|
data,
|
||||||
status,
|
status,
|
||||||
error,
|
error,
|
||||||
displayError
|
displayError,
|
||||||
|
onClose
|
||||||
}: {
|
}: {
|
||||||
title: ReactNode
|
title: ReactNode
|
||||||
onSearch?: (input: string) => void
|
onSearch?: (input: string) => void
|
||||||
|
|
@ -34,28 +36,42 @@ export const BreakdownTable = <TListItem extends { name: string }>({
|
||||||
error?: Error | null
|
error?: Error | null
|
||||||
/** Controls whether the component displays API request errors or ignores them. */
|
/** Controls whether the component displays API request errors or ignores them. */
|
||||||
displayError?: boolean
|
displayError?: boolean
|
||||||
|
onClose?: () => void
|
||||||
}) => {
|
}) => {
|
||||||
const searchRef = useRef<HTMLInputElement>(null)
|
const searchRef = useRef<HTMLInputElement>(null)
|
||||||
|
const navigate = useAppNavigate()
|
||||||
|
const handleClose =
|
||||||
|
onClose ?? (() => navigate({ path: rootRoute.path, search: (s) => s }))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full">
|
<>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center gap-4">
|
||||||
<div className="flex items-center gap-x-2">
|
<div className="flex items-center gap-4 w-full">
|
||||||
<h1 className="text-xl font-bold dark:text-gray-100">{title}</h1>
|
<h1 className="shrink-0 mb-0.5 text-base md:text-lg font-bold dark:text-gray-100">
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
{!isPending && isFetching && <SmallLoadingSpinner />}
|
{!isPending && isFetching && <SmallLoadingSpinner />}
|
||||||
|
{!!onSearch && (
|
||||||
|
<SearchInput
|
||||||
|
searchRef={searchRef}
|
||||||
|
onSearch={onSearch}
|
||||||
|
className={
|
||||||
|
displayError && status === 'error' ? 'pointer-events-none' : ''
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!!onSearch && (
|
<button
|
||||||
<SearchInput
|
type="button"
|
||||||
searchRef={searchRef}
|
onClick={handleClose}
|
||||||
onSearch={onSearch}
|
aria-label="Close modal"
|
||||||
className={
|
className="text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
|
||||||
displayError && status === 'error' ? 'pointer-events-none' : ''
|
>
|
||||||
}
|
<XMarkIcon className="size-5" />
|
||||||
/>
|
</button>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="my-4 border-b border-gray-300 dark:border-gray-700"></div>
|
<div className="my-3 md:my-4 border-b border-gray-250 dark:border-gray-750"></div>
|
||||||
<div style={{ minHeight: `${MIN_HEIGHT_PX}px` }}>
|
<div className="flex-1 overflow-auto pr-4 -mr-4">
|
||||||
{displayError && status === 'error' && <ErrorMessage error={error} />}
|
{displayError && status === 'error' && <ErrorMessage error={error} />}
|
||||||
{isPending && <InitialLoadingSpinner />}
|
{isPending && <InitialLoadingSpinner />}
|
||||||
{data && <Table<TListItem> data={data} columns={columns} />}
|
{data && <Table<TListItem> data={data} columns={columns} />}
|
||||||
|
|
@ -66,15 +82,12 @@ export const BreakdownTable = <TListItem extends { name: string }>({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const InitialLoadingSpinner = () => (
|
const InitialLoadingSpinner = () => (
|
||||||
<div
|
<div className="w-full h-full flex flex-col justify-center">
|
||||||
className="w-full h-full flex flex-col justify-center"
|
|
||||||
style={{ minHeight: `${MIN_HEIGHT_PX}px` }}
|
|
||||||
>
|
|
||||||
<div className="mx-auto loading">
|
<div className="mx-auto loading">
|
||||||
<div />
|
<div />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -88,10 +101,7 @@ const SmallLoadingSpinner = () => (
|
||||||
)
|
)
|
||||||
|
|
||||||
const ErrorMessage = ({ error }: { error?: unknown }) => (
|
const ErrorMessage = ({ error }: { error?: unknown }) => (
|
||||||
<div
|
<div className="grid grid-rows-2 text-gray-700 dark:text-gray-300">
|
||||||
className="grid grid-rows-2 text-gray-700 dark:text-gray-300"
|
|
||||||
style={{ height: `${MIN_HEIGHT_PX}px` }}
|
|
||||||
>
|
|
||||||
<div className="text-center self-end">
|
<div className="text-center self-end">
|
||||||
<RocketIcon />
|
<RocketIcon />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ function ConversionsModal() {
|
||||||
const site = useSiteContext()
|
const site = useSiteContext()
|
||||||
|
|
||||||
const reportInfo = {
|
const reportInfo = {
|
||||||
title: 'Goal Conversions',
|
title: 'Goal conversions',
|
||||||
dimension: 'goal',
|
dimension: 'goal',
|
||||||
endpoint: url.apiPath(site, '/conversions'),
|
endpoint: url.apiPath(site, '/conversions'),
|
||||||
dimensionLabel: 'Goal'
|
dimensionLabel: 'Goal'
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ function BrowserVersionsModal() {
|
||||||
const site = useSiteContext()
|
const site = useSiteContext()
|
||||||
|
|
||||||
const reportInfo = {
|
const reportInfo = {
|
||||||
title: 'Browser Versions',
|
title: 'Browser versions',
|
||||||
dimension: 'browser_version',
|
dimension: 'browser_version',
|
||||||
endpoint: url.apiPath(site, '/browser-versions'),
|
endpoint: url.apiPath(site, '/browser-versions'),
|
||||||
dimensionLabel: 'Browser version',
|
dimensionLabel: 'Browser version',
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ export default function chooseMetrics(query, site) {
|
||||||
metrics.createTotalVisitors(),
|
metrics.createTotalVisitors(),
|
||||||
metrics.createVisitors({
|
metrics.createVisitors({
|
||||||
renderLabel: (_query) => 'Conversions',
|
renderLabel: (_query) => 'Conversions',
|
||||||
width: 'w-28'
|
width: 'w-32 md:w-28'
|
||||||
}),
|
}),
|
||||||
metrics.createConversionRate(),
|
metrics.createConversionRate(),
|
||||||
showRevenueMetrics && metrics.createTotalRevenue(),
|
showRevenueMetrics && metrics.createTotalRevenue(),
|
||||||
|
|
@ -26,7 +26,7 @@ export default function chooseMetrics(query, site) {
|
||||||
return [
|
return [
|
||||||
metrics.createVisitors({
|
metrics.createVisitors({
|
||||||
renderLabel: (_query) => 'Current visitors',
|
renderLabel: (_query) => 'Current visitors',
|
||||||
width: 'w-36'
|
width: 'w-32'
|
||||||
}),
|
}),
|
||||||
metrics.createPercentage()
|
metrics.createPercentage()
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ function OperatingSystemVersionsModal() {
|
||||||
const site = useSiteContext()
|
const site = useSiteContext()
|
||||||
|
|
||||||
const reportInfo = {
|
const reportInfo = {
|
||||||
title: 'Operating System Versions',
|
title: 'Operating system versions',
|
||||||
dimension: 'os_version',
|
dimension: 'os_version',
|
||||||
endpoint: url.apiPath(site, '/operating-system-versions'),
|
endpoint: url.apiPath(site, '/operating-system-versions'),
|
||||||
dimensionLabel: 'Operating system version',
|
dimensionLabel: 'Operating system version',
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ function OperatingSystemsModal() {
|
||||||
const site = useSiteContext()
|
const site = useSiteContext()
|
||||||
|
|
||||||
const reportInfo = {
|
const reportInfo = {
|
||||||
title: 'Operating Systems',
|
title: 'Operating systems',
|
||||||
dimension: 'os',
|
dimension: 'os',
|
||||||
endpoint: url.apiPath(site, '/operating-systems'),
|
endpoint: url.apiPath(site, '/operating-systems'),
|
||||||
dimensionLabel: 'Operating system',
|
dimensionLabel: 'Operating system',
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ function ScreenSizesModal() {
|
||||||
const site = useSiteContext()
|
const site = useSiteContext()
|
||||||
|
|
||||||
const reportInfo = {
|
const reportInfo = {
|
||||||
title: 'Screen Sizes',
|
title: 'Screen sizes',
|
||||||
dimension: 'screen',
|
dimension: 'screen',
|
||||||
endpoint: url.apiPath(site, '/screen-sizes'),
|
endpoint: url.apiPath(site, '/screen-sizes'),
|
||||||
dimensionLabel: 'Screen size',
|
dimensionLabel: 'Screen size',
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ function EntryPagesModal() {
|
||||||
const showRevenueMetrics = BUILD_EXTRA && revenueAvailable(query, site)
|
const showRevenueMetrics = BUILD_EXTRA && revenueAvailable(query, site)
|
||||||
|
|
||||||
const reportInfo = {
|
const reportInfo = {
|
||||||
title: 'Entry Pages',
|
title: 'Entry pages',
|
||||||
dimension: 'entry_page',
|
dimension: 'entry_page',
|
||||||
endpoint: url.apiPath(site, '/entry-pages'),
|
endpoint: url.apiPath(site, '/entry-pages'),
|
||||||
dimensionLabel: 'Entry page',
|
dimensionLabel: 'Entry page',
|
||||||
|
|
@ -67,7 +67,7 @@ function EntryPagesModal() {
|
||||||
return [
|
return [
|
||||||
metrics.createVisitors({
|
metrics.createVisitors({
|
||||||
renderLabel: (_query) => 'Current visitors',
|
renderLabel: (_query) => 'Current visitors',
|
||||||
width: 'w-36'
|
width: 'w-32'
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -75,8 +75,8 @@ function EntryPagesModal() {
|
||||||
return [
|
return [
|
||||||
metrics.createVisitors({ renderLabel: (_query) => 'Visitors' }),
|
metrics.createVisitors({ renderLabel: (_query) => 'Visitors' }),
|
||||||
metrics.createVisits({
|
metrics.createVisits({
|
||||||
renderLabel: (_query) => 'Total Entrances',
|
renderLabel: (_query) => 'Total entrances',
|
||||||
width: 'w-36'
|
width: 'w-32'
|
||||||
}),
|
}),
|
||||||
metrics.createBounceRate(),
|
metrics.createBounceRate(),
|
||||||
metrics.createVisitDuration()
|
metrics.createVisitDuration()
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ function ExitPagesModal() {
|
||||||
const showRevenueMetrics = BUILD_EXTRA && revenueAvailable(query, site)
|
const showRevenueMetrics = BUILD_EXTRA && revenueAvailable(query, site)
|
||||||
|
|
||||||
const reportInfo = {
|
const reportInfo = {
|
||||||
title: 'Exit Pages',
|
title: 'Exit pages',
|
||||||
dimension: 'exit_page',
|
dimension: 'exit_page',
|
||||||
endpoint: url.apiPath(site, '/exit-pages'),
|
endpoint: url.apiPath(site, '/exit-pages'),
|
||||||
dimensionLabel: 'Page url',
|
dimensionLabel: 'Page url',
|
||||||
|
|
@ -64,7 +64,7 @@ function ExitPagesModal() {
|
||||||
return [
|
return [
|
||||||
metrics.createVisitors({
|
metrics.createVisitors({
|
||||||
renderLabel: (_query) => 'Current visitors',
|
renderLabel: (_query) => 'Current visitors',
|
||||||
width: 'w-36'
|
width: 'w-32'
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -75,7 +75,8 @@ function ExitPagesModal() {
|
||||||
sortable: true
|
sortable: true
|
||||||
}),
|
}),
|
||||||
metrics.createVisits({
|
metrics.createVisits({
|
||||||
renderLabel: (_query) => 'Total Exits',
|
renderLabel: (_query) => 'Total exits',
|
||||||
|
width: 'w-32',
|
||||||
sortable: true
|
sortable: true
|
||||||
}),
|
}),
|
||||||
metrics.createExitRate()
|
metrics.createExitRate()
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { XMarkIcon } from '@heroicons/react/20/solid'
|
||||||
import { useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
|
|
||||||
import Modal from './modal'
|
import Modal from './modal'
|
||||||
|
|
@ -68,6 +69,7 @@ class FilterModal extends React.Component {
|
||||||
)
|
)
|
||||||
|
|
||||||
this.handleKeydown = this.handleKeydown.bind(this)
|
this.handleKeydown = this.handleKeydown.bind(this)
|
||||||
|
this.closeModal = this.closeModal.bind(this)
|
||||||
this.state = {
|
this.state = {
|
||||||
query,
|
query,
|
||||||
filterState,
|
filterState,
|
||||||
|
|
@ -108,6 +110,13 @@ class FilterModal extends React.Component {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
closeModal() {
|
||||||
|
this.props.navigate({
|
||||||
|
path: rootRoute.path,
|
||||||
|
search: (search) => search
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
selectFiltersAndCloseModal(filters) {
|
selectFiltersAndCloseModal(filters) {
|
||||||
this.props.navigate({
|
this.props.navigate({
|
||||||
path: rootRoute.path,
|
path: rootRoute.path,
|
||||||
|
|
@ -169,13 +178,23 @@ class FilterModal extends React.Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Modal maxWidth="460px">
|
<Modal maxWidth="460px" onClose={this.closeModal}>
|
||||||
<h1 className="text-xl font-bold dark:text-gray-100">
|
<div className="flex items-center justify-between gap-3">
|
||||||
Filter by {formatFilterGroup(this.props.modalType)}
|
<h1 className="text-base md:text-lg font-bold dark:text-gray-100">
|
||||||
</h1>
|
Filter by {formatFilterGroup(this.props.modalType)}
|
||||||
|
</h1>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={this.closeModal}
|
||||||
|
aria-label="Close modal"
|
||||||
|
className="text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
|
||||||
|
>
|
||||||
|
<XMarkIcon className="size-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 border-b border-gray-300 dark:border-gray-700"></div>
|
<div className="mt-2 md:mt-4 border-b border-gray-300 dark:border-gray-700"></div>
|
||||||
<main className="modal__content">
|
<main>
|
||||||
<form
|
<form
|
||||||
className="flex flex-col"
|
className="flex flex-col"
|
||||||
onSubmit={this.handleSubmit.bind(this)}
|
onSubmit={this.handleSubmit.bind(this)}
|
||||||
|
|
@ -192,7 +211,7 @@ class FilterModal extends React.Component {
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<div className="mt-6 flex gap-x-4 items-center justify-start">
|
<div className="mt-6 mb-3 flex gap-x-4 items-center justify-start">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="button !px-3"
|
className="button !px-3"
|
||||||
|
|
|
||||||
|
|
@ -12,21 +12,21 @@ import { SortDirection } from '../../hooks/use-order-by'
|
||||||
|
|
||||||
const VIEWS = {
|
const VIEWS = {
|
||||||
countries: {
|
countries: {
|
||||||
title: 'Top Countries',
|
title: 'Top countries',
|
||||||
dimension: 'country',
|
dimension: 'country',
|
||||||
endpoint: '/countries',
|
endpoint: '/countries',
|
||||||
dimensionLabel: 'Country',
|
dimensionLabel: 'Country',
|
||||||
defaultOrder: ['visitors', SortDirection.desc]
|
defaultOrder: ['visitors', SortDirection.desc]
|
||||||
},
|
},
|
||||||
regions: {
|
regions: {
|
||||||
title: 'Top Regions',
|
title: 'Top regions',
|
||||||
dimension: 'region',
|
dimension: 'region',
|
||||||
endpoint: '/regions',
|
endpoint: '/regions',
|
||||||
dimensionLabel: 'Region',
|
dimensionLabel: 'Region',
|
||||||
defaultOrder: ['visitors', SortDirection.desc]
|
defaultOrder: ['visitors', SortDirection.desc]
|
||||||
},
|
},
|
||||||
cities: {
|
cities: {
|
||||||
title: 'Top Cities',
|
title: 'Top cities',
|
||||||
dimension: 'city',
|
dimension: 'city',
|
||||||
endpoint: '/cities',
|
endpoint: '/cities',
|
||||||
dimensionLabel: 'City',
|
dimensionLabel: 'City',
|
||||||
|
|
@ -88,7 +88,7 @@ function LocationsModal({ currentView }) {
|
||||||
return [
|
return [
|
||||||
metrics.createVisitors({
|
metrics.createVisitors({
|
||||||
renderLabel: (_query) => 'Current visitors',
|
renderLabel: (_query) => 'Current visitors',
|
||||||
width: 'w-36'
|
width: 'w-32'
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,8 @@ import { createPortal } from 'react-dom'
|
||||||
import { isModifierPressed, isTyping, Keybind } from '../../keybinding'
|
import { isModifierPressed, isTyping, Keybind } from '../../keybinding'
|
||||||
import { rootRoute } from '../../router'
|
import { rootRoute } from '../../router'
|
||||||
import { useAppNavigate } from '../../navigation/use-app-navigate'
|
import { useAppNavigate } from '../../navigation/use-app-navigate'
|
||||||
|
|
||||||
// This corresponds to the 'md' breakpoint on TailwindCSS.
|
|
||||||
const MD_WIDTH = 768
|
|
||||||
// We assume that the dashboard is by default opened on a desktop. This is also a fall-back for when, for any reason, the width is not ascertained.
|
// We assume that the dashboard is by default opened on a desktop. This is also a fall-back for when, for any reason, the width is not ascertained.
|
||||||
const DEFAULT_WIDTH = 1080
|
const DEFAULT_WIDTH = 1080
|
||||||
|
|
||||||
class Modal extends React.Component {
|
class Modal extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
@ -27,26 +23,21 @@ class Modal extends React.Component {
|
||||||
window.addEventListener('resize', this.handleResize, false)
|
window.addEventListener('resize', this.handleResize, false)
|
||||||
this.handleResize()
|
this.handleResize()
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
document.body.style.overflow = null
|
document.body.style.overflow = null
|
||||||
document.body.style.height = null
|
document.body.style.height = null
|
||||||
document.removeEventListener('mousedown', this.handleClickOutside)
|
document.removeEventListener('mousedown', this.handleClickOutside)
|
||||||
window.removeEventListener('resize', this.handleResize, false)
|
window.removeEventListener('resize', this.handleResize, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClickOutside(e) {
|
handleClickOutside(e) {
|
||||||
if (this.node.current.contains(e.target)) {
|
if (this.node.current.contains(e.target)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.onClose()
|
this.props.onClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
handleResize() {
|
handleResize() {
|
||||||
this.setState({ viewport: window.innerWidth })
|
this.setState({ viewport: window.innerWidth })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description
|
* @description
|
||||||
* Decide whether to set max-width, and if so, to what.
|
* Decide whether to set max-width, and if so, to what.
|
||||||
|
|
@ -56,12 +47,11 @@ class Modal extends React.Component {
|
||||||
*/
|
*/
|
||||||
getStyle() {
|
getStyle() {
|
||||||
const { maxWidth } = this.props
|
const { maxWidth } = this.props
|
||||||
const { viewport } = this.state
|
|
||||||
const styleObject = {}
|
const styleObject = {}
|
||||||
if (maxWidth) {
|
if (maxWidth) {
|
||||||
styleObject.maxWidth = maxWidth
|
styleObject.maxWidth = maxWidth
|
||||||
} else {
|
} else {
|
||||||
styleObject.width = viewport <= MD_WIDTH ? 'min-content' : '860px'
|
styleObject.maxWidth = '880px'
|
||||||
}
|
}
|
||||||
return styleObject
|
return styleObject
|
||||||
}
|
}
|
||||||
|
|
@ -78,16 +68,17 @@ class Modal extends React.Component {
|
||||||
/>
|
/>
|
||||||
<div className="modal is-open" onClick={this.props.onClick}>
|
<div className="modal is-open" onClick={this.props.onClick}>
|
||||||
<div className="modal__overlay">
|
<div className="modal__overlay">
|
||||||
<button className="modal__close"></button>
|
<div className="[--gap:1rem] sm:[--gap:2rem] md:[--gap:4rem] flex h-full w-full items-center md:items-start justify-center p-[var(--gap)] box-border">
|
||||||
<div
|
<div
|
||||||
ref={this.node}
|
ref={this.node}
|
||||||
className="modal__container dark:bg-gray-900 focus:outline-hidden"
|
className="max-h-[calc(100dvh_-_var(--gap)*2)] min-h-[66vh] md:min-h-120 w-full flex flex-col bg-white p-3 md:px-6 md:py-4 overflow-hidden box-border transition-[height] duration-200 ease-in shadow-2xl rounded-lg dark:bg-gray-900 focus:outline-hidden"
|
||||||
style={this.getStyle()}
|
style={this.getStyle()}
|
||||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<FocusOnMount focusableRef={this.node} />
|
<FocusOnMount focusableRef={this.node} />
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ function PagesModal() {
|
||||||
const showRevenueMetrics = BUILD_EXTRA && revenueAvailable(query, site)
|
const showRevenueMetrics = BUILD_EXTRA && revenueAvailable(query, site)
|
||||||
|
|
||||||
const reportInfo = {
|
const reportInfo = {
|
||||||
title: 'Top Pages',
|
title: 'Top pages',
|
||||||
dimension: 'page',
|
dimension: 'page',
|
||||||
endpoint: url.apiPath(site, '/pages'),
|
endpoint: url.apiPath(site, '/pages'),
|
||||||
dimensionLabel: 'Page url',
|
dimensionLabel: 'Page url',
|
||||||
|
|
@ -67,7 +67,7 @@ function PagesModal() {
|
||||||
return [
|
return [
|
||||||
metrics.createVisitors({
|
metrics.createVisitors({
|
||||||
renderLabel: (_query) => 'Current visitors',
|
renderLabel: (_query) => 'Current visitors',
|
||||||
width: 'w-36'
|
width: 'w-32'
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ function PropsModal() {
|
||||||
const showRevenueMetrics = BUILD_EXTRA && revenueAvailable(query, site)
|
const showRevenueMetrics = BUILD_EXTRA && revenueAvailable(query, site)
|
||||||
|
|
||||||
const reportInfo = {
|
const reportInfo = {
|
||||||
title: specialTitleWhenGoalFilter(query, 'Custom Property Breakdown'),
|
title: specialTitleWhenGoalFilter(query, 'Custom property breakdown'),
|
||||||
dimension: propKey,
|
dimension: propKey,
|
||||||
endpoint: url.apiPath(
|
endpoint: url.apiPath(
|
||||||
site,
|
site,
|
||||||
|
|
@ -71,6 +71,7 @@ function PropsModal() {
|
||||||
metrics={chooseMetrics()}
|
metrics={chooseMetrics()}
|
||||||
getFilterInfo={getFilterInfo}
|
getFilterInfo={getFilterInfo}
|
||||||
addSearchFilter={addSearchFilter}
|
addSearchFilter={addSearchFilter}
|
||||||
|
showPercentageColumn
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ function ReferrerDrilldownModal() {
|
||||||
return [
|
return [
|
||||||
metrics.createVisitors({
|
metrics.createVisitors({
|
||||||
renderLabel: (_query) => 'Current visitors',
|
renderLabel: (_query) => 'Current visitors',
|
||||||
width: 'w-36'
|
width: 'w-32'
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import { SourceFavicon } from '../sources/source-favicon'
|
||||||
const VIEWS = {
|
const VIEWS = {
|
||||||
sources: {
|
sources: {
|
||||||
info: {
|
info: {
|
||||||
title: 'Top Sources',
|
title: 'Top sources',
|
||||||
dimension: 'source',
|
dimension: 'source',
|
||||||
endpoint: '/sources',
|
endpoint: '/sources',
|
||||||
dimensionLabel: 'Source',
|
dimensionLabel: 'Source',
|
||||||
|
|
@ -33,7 +33,7 @@ const VIEWS = {
|
||||||
},
|
},
|
||||||
channels: {
|
channels: {
|
||||||
info: {
|
info: {
|
||||||
title: 'Top Acquisition Channels',
|
title: 'Top acquisition channels',
|
||||||
dimension: 'channel',
|
dimension: 'channel',
|
||||||
endpoint: '/channels',
|
endpoint: '/channels',
|
||||||
dimensionLabel: 'Channel',
|
dimensionLabel: 'Channel',
|
||||||
|
|
@ -42,46 +42,46 @@ const VIEWS = {
|
||||||
},
|
},
|
||||||
utm_mediums: {
|
utm_mediums: {
|
||||||
info: {
|
info: {
|
||||||
title: 'Top UTM Mediums',
|
title: 'Top UTM mediums',
|
||||||
dimension: 'utm_medium',
|
dimension: 'utm_medium',
|
||||||
endpoint: '/utm_mediums',
|
endpoint: '/utm_mediums',
|
||||||
dimensionLabel: 'UTM Medium',
|
dimensionLabel: 'UTM medium',
|
||||||
defaultOrder: ['visitors', SortDirection.desc]
|
defaultOrder: ['visitors', SortDirection.desc]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
utm_sources: {
|
utm_sources: {
|
||||||
info: {
|
info: {
|
||||||
title: 'Top UTM Sources',
|
title: 'Top UTM sources',
|
||||||
dimension: 'utm_source',
|
dimension: 'utm_source',
|
||||||
endpoint: '/utm_sources',
|
endpoint: '/utm_sources',
|
||||||
dimensionLabel: 'UTM Source',
|
dimensionLabel: 'UTM source',
|
||||||
defaultOrder: ['visitors', SortDirection.desc]
|
defaultOrder: ['visitors', SortDirection.desc]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
utm_campaigns: {
|
utm_campaigns: {
|
||||||
info: {
|
info: {
|
||||||
title: 'Top UTM Campaigns',
|
title: 'Top UTM campaigns',
|
||||||
dimension: 'utm_campaign',
|
dimension: 'utm_campaign',
|
||||||
endpoint: '/utm_campaigns',
|
endpoint: '/utm_campaigns',
|
||||||
dimensionLabel: 'UTM Campaign',
|
dimensionLabel: 'UTM campaign',
|
||||||
defaultOrder: ['visitors', SortDirection.desc]
|
defaultOrder: ['visitors', SortDirection.desc]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
utm_contents: {
|
utm_contents: {
|
||||||
info: {
|
info: {
|
||||||
title: 'Top UTM Contents',
|
title: 'Top UTM contents',
|
||||||
dimension: 'utm_content',
|
dimension: 'utm_content',
|
||||||
endpoint: '/utm_contents',
|
endpoint: '/utm_contents',
|
||||||
dimensionLabel: 'UTM Content',
|
dimensionLabel: 'UTM content',
|
||||||
defaultOrder: ['visitors', SortDirection.desc]
|
defaultOrder: ['visitors', SortDirection.desc]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
utm_terms: {
|
utm_terms: {
|
||||||
info: {
|
info: {
|
||||||
title: 'Top UTM Terms',
|
title: 'Top UTM terms',
|
||||||
dimension: 'utm_term',
|
dimension: 'utm_term',
|
||||||
endpoint: '/utm_terms',
|
endpoint: '/utm_terms',
|
||||||
dimensionLabel: 'UTM Term',
|
dimensionLabel: 'UTM term',
|
||||||
defaultOrder: ['visitors', SortDirection.desc]
|
defaultOrder: ['visitors', SortDirection.desc]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -140,7 +140,7 @@ function SourcesModal({ currentView }) {
|
||||||
return [
|
return [
|
||||||
metrics.createVisitors({
|
metrics.createVisitors({
|
||||||
renderLabel: (_query) => 'Current visitors',
|
renderLabel: (_query) => 'Current visitors',
|
||||||
width: 'w-36'
|
width: 'w-32'
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,10 +33,12 @@ function EntryPages({ afterFetchData }) {
|
||||||
function chooseMetrics() {
|
function chooseMetrics() {
|
||||||
return [
|
return [
|
||||||
metrics.createVisitors({
|
metrics.createVisitors({
|
||||||
defaultLabel: 'Unique Entrances',
|
defaultLabel: 'Unique entrances',
|
||||||
width: 'w-36',
|
width: 'w-36',
|
||||||
meta: { plot: true }
|
meta: { plot: true }
|
||||||
}),
|
}),
|
||||||
|
!hasConversionGoalFilter(query) &&
|
||||||
|
metrics.createPercentage({ meta: { showOnHover: true } }),
|
||||||
hasConversionGoalFilter(query) && metrics.createConversionRate()
|
hasConversionGoalFilter(query) && metrics.createConversionRate()
|
||||||
].filter((metric) => !!metric)
|
].filter((metric) => !!metric)
|
||||||
}
|
}
|
||||||
|
|
@ -53,7 +55,7 @@ function EntryPages({ afterFetchData }) {
|
||||||
search: (search) => search
|
search: (search) => search
|
||||||
}}
|
}}
|
||||||
getExternalLinkUrl={getExternalLinkUrl}
|
getExternalLinkUrl={getExternalLinkUrl}
|
||||||
color="bg-orange-50 group-hover:bg-orange-100"
|
color="bg-orange-50 group-hover/row:bg-orange-100"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -79,10 +81,12 @@ function ExitPages({ afterFetchData }) {
|
||||||
function chooseMetrics() {
|
function chooseMetrics() {
|
||||||
return [
|
return [
|
||||||
metrics.createVisitors({
|
metrics.createVisitors({
|
||||||
defaultLabel: 'Unique Exits',
|
defaultLabel: 'Unique exits',
|
||||||
width: 'w-36',
|
width: 'w-36',
|
||||||
meta: { plot: true }
|
meta: { plot: true }
|
||||||
}),
|
}),
|
||||||
|
!hasConversionGoalFilter(query) &&
|
||||||
|
metrics.createPercentage({ meta: { showOnHover: true } }),
|
||||||
hasConversionGoalFilter(query) && metrics.createConversionRate()
|
hasConversionGoalFilter(query) && metrics.createConversionRate()
|
||||||
].filter((metric) => !!metric)
|
].filter((metric) => !!metric)
|
||||||
}
|
}
|
||||||
|
|
@ -99,7 +103,7 @@ function ExitPages({ afterFetchData }) {
|
||||||
search: (search) => search
|
search: (search) => search
|
||||||
}}
|
}}
|
||||||
getExternalLinkUrl={getExternalLinkUrl}
|
getExternalLinkUrl={getExternalLinkUrl}
|
||||||
color="bg-orange-50 group-hover:bg-orange-100"
|
color="bg-orange-50 group-hover/row:bg-orange-100"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -125,6 +129,8 @@ function TopPages({ afterFetchData }) {
|
||||||
function chooseMetrics() {
|
function chooseMetrics() {
|
||||||
return [
|
return [
|
||||||
metrics.createVisitors({ meta: { plot: true } }),
|
metrics.createVisitors({ meta: { plot: true } }),
|
||||||
|
!hasConversionGoalFilter(query) &&
|
||||||
|
metrics.createPercentage({ meta: { showOnHover: true } }),
|
||||||
hasConversionGoalFilter(query) && metrics.createConversionRate()
|
hasConversionGoalFilter(query) && metrics.createConversionRate()
|
||||||
].filter((metric) => !!metric)
|
].filter((metric) => !!metric)
|
||||||
}
|
}
|
||||||
|
|
@ -141,15 +147,15 @@ function TopPages({ afterFetchData }) {
|
||||||
search: (search) => search
|
search: (search) => search
|
||||||
}}
|
}}
|
||||||
getExternalLinkUrl={getExternalLinkUrl}
|
getExternalLinkUrl={getExternalLinkUrl}
|
||||||
color="bg-orange-50 group-hover:bg-orange-100"
|
color="bg-orange-50 group-hover/row:bg-orange-100"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const labelFor = {
|
const labelFor = {
|
||||||
pages: 'Top Pages',
|
pages: 'Top pages',
|
||||||
'entry-pages': 'Entry Pages',
|
'entry-pages': 'Entry pages',
|
||||||
'exit-pages': 'Exit Pages'
|
'exit-pages': 'Exit pages'
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Pages() {
|
export default function Pages() {
|
||||||
|
|
@ -187,7 +193,7 @@ export default function Pages() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="group/report overflow-x-hidden">
|
||||||
{/* Header Container */}
|
{/* Header Container */}
|
||||||
<div className="w-full flex justify-between">
|
<div className="w-full flex justify-between">
|
||||||
<div className="flex gap-x-1">
|
<div className="flex gap-x-1">
|
||||||
|
|
@ -201,9 +207,9 @@ export default function Pages() {
|
||||||
</div>
|
</div>
|
||||||
<TabWrapper>
|
<TabWrapper>
|
||||||
{[
|
{[
|
||||||
{ label: 'Top Pages', value: 'pages' },
|
{ label: 'Top pages', value: 'pages' },
|
||||||
{ label: 'Entry Pages', value: 'entry-pages' },
|
{ label: 'Entry pages', value: 'entry-pages' },
|
||||||
{ label: 'Exit Pages', value: 'exit-pages' }
|
{ label: 'Exit pages', value: 'exit-pages' }
|
||||||
].map(({ value, label }) => (
|
].map(({ value, label }) => (
|
||||||
<TabButton
|
<TabButton
|
||||||
active={mode === value}
|
active={mode === value}
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ it('renders tilde for no change', () => {
|
||||||
|
|
||||||
const arrowElement = screen.getByTestId('change-arrow')
|
const arrowElement = screen.getByTestId('change-arrow')
|
||||||
|
|
||||||
expect(arrowElement).toHaveTextContent('〰 0%')
|
expect(arrowElement).toHaveTextContent('0%')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('inverts colors for positive bounce_rate change', () => {
|
it('inverts colors for positive bounce_rate change', () => {
|
||||||
|
|
|
||||||
|
|
@ -15,24 +15,22 @@ export function ChangeArrow({
|
||||||
className: string
|
className: string
|
||||||
hideNumber?: boolean
|
hideNumber?: boolean
|
||||||
}) {
|
}) {
|
||||||
const formattedChange = hideNumber
|
|
||||||
? null
|
|
||||||
: ` ${numberShortFormatter(Math.abs(change))}%`
|
|
||||||
|
|
||||||
let icon = null
|
let icon = null
|
||||||
const arrowClassName = classNames(
|
const arrowClassName = classNames(
|
||||||
color(change, metric),
|
color(change, metric),
|
||||||
'inline-block h-3 w-3 stroke-[1px] stroke-current'
|
'mb-0.5 inline-block size-3 stroke-[1px] stroke-current'
|
||||||
)
|
)
|
||||||
|
|
||||||
if (change > 0) {
|
if (change > 0) {
|
||||||
icon = <ArrowUpRightIcon className={arrowClassName} />
|
icon = <ArrowUpRightIcon className={arrowClassName} />
|
||||||
} else if (change < 0) {
|
} else if (change < 0) {
|
||||||
icon = <ArrowDownRightIcon className={arrowClassName} />
|
icon = <ArrowDownRightIcon className={arrowClassName} />
|
||||||
} else if (change === 0 && !hideNumber) {
|
|
||||||
icon = <>〰</>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formattedChange = hideNumber
|
||||||
|
? null
|
||||||
|
: `${icon ? ' ' : ''}${numberShortFormatter(Math.abs(change))}%`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={className} data-testid="change-arrow">
|
<span className={className} data-testid="change-arrow">
|
||||||
{icon}
|
{icon}
|
||||||
|
|
|
||||||
|
|
@ -26,27 +26,34 @@ const COL_MIN_WIDTH = 70
|
||||||
|
|
||||||
function ExternalLink<T>({
|
function ExternalLink<T>({
|
||||||
item,
|
item,
|
||||||
getExternalLinkUrl
|
getExternalLinkUrl,
|
||||||
|
isTapped
|
||||||
}: {
|
}: {
|
||||||
item: T
|
item: T
|
||||||
getExternalLinkUrl?: (item: T) => string
|
getExternalLinkUrl?: (item: T) => string
|
||||||
|
isTapped?: boolean
|
||||||
}) {
|
}) {
|
||||||
const dest = getExternalLinkUrl && getExternalLinkUrl(item)
|
const dest = getExternalLinkUrl && getExternalLinkUrl(item)
|
||||||
if (dest) {
|
if (dest) {
|
||||||
|
const className = isTapped
|
||||||
|
? 'visible md:invisible md:group-hover/row:visible'
|
||||||
|
: 'invisible md:group-hover/row:visible'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a
|
<a target="_blank" rel="noreferrer" href={dest} className={className}>
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
href={dest}
|
|
||||||
className="w-4 h-4 invisible group-hover:visible"
|
|
||||||
>
|
|
||||||
<svg
|
<svg
|
||||||
className="inline w-full h-full ml-1 -mt-1 text-gray-600 dark:text-gray-400"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="currentColor"
|
fill="none"
|
||||||
viewBox="0 0 20 20"
|
viewBox="0 0 24 24"
|
||||||
|
className="inline size-3.5 mb-0.5 text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200"
|
||||||
>
|
>
|
||||||
<path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z"></path>
|
<path
|
||||||
<path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z"></path>
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
d="M9 5H5a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-4M12 12l9-9-.303.303M14 3h7v7"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
)
|
)
|
||||||
|
|
@ -88,11 +95,6 @@ type ListReportProps = {
|
||||||
colMinWidth?: number
|
colMinWidth?: number
|
||||||
/** Navigation props to be passed to "More" link, if any. */
|
/** Navigation props to be passed to "More" link, if any. */
|
||||||
detailsLinkProps?: AppNavigationLinkProps
|
detailsLinkProps?: AppNavigationLinkProps
|
||||||
/** Set this to `true` if the details button should be hidden on
|
|
||||||
* the condition that there are less than MAX_ITEMS entries in the list (i.e. nothing
|
|
||||||
* more to show).
|
|
||||||
*/
|
|
||||||
maybeHideDetails?: boolean
|
|
||||||
/** Function with additional action to be taken when a list entry is clicked. */
|
/** Function with additional action to be taken when a list entry is clicked. */
|
||||||
onClick?: () => void
|
onClick?: () => void
|
||||||
/** Color of the comparison bars in light-mode. */
|
/** Color of the comparison bars in light-mode. */
|
||||||
|
|
@ -114,7 +116,6 @@ export default function ListReport<
|
||||||
colMinWidth = COL_MIN_WIDTH,
|
colMinWidth = COL_MIN_WIDTH,
|
||||||
afterFetchData,
|
afterFetchData,
|
||||||
detailsLinkProps,
|
detailsLinkProps,
|
||||||
maybeHideDetails,
|
|
||||||
onClick,
|
onClick,
|
||||||
color,
|
color,
|
||||||
getFilterInfo,
|
getFilterInfo,
|
||||||
|
|
@ -129,6 +130,7 @@ export default function ListReport<
|
||||||
meta: BreakdownResultMeta | null
|
meta: BreakdownResultMeta | null
|
||||||
}>({ loading: true, list: null, meta: null })
|
}>({ loading: true, list: null, meta: null })
|
||||||
const [visible, setVisible] = useState(false)
|
const [visible, setVisible] = useState(false)
|
||||||
|
const [tappedRow, setTappedRow] = useState<string | null>(null)
|
||||||
|
|
||||||
const isRealtime = isRealTimeDashboard(query)
|
const isRealtime = isRealTimeDashboard(query)
|
||||||
const goalFilterApplied = hasConversionGoalFilter(query)
|
const goalFilterApplied = hasConversionGoalFilter(query)
|
||||||
|
|
@ -194,6 +196,38 @@ export default function ListReport<
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showOnHoverClass(metric: Metric, listItemName: string) {
|
||||||
|
if (!metric.meta.showOnHover) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// On mobile: show if row is tapped, hide otherwise
|
||||||
|
// On desktop: slide in from right when hovering
|
||||||
|
if (tappedRow === listItemName) {
|
||||||
|
return 'translate-x-0 opacity-100 transition-all duration-150'
|
||||||
|
} else {
|
||||||
|
return 'translate-x-[100%] opacity-0 transition-all duration-150 md:group-hover/report:translate-x-0 md:group-hover/report:opacity-100'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function slideLeftClass(
|
||||||
|
metricIndex: number,
|
||||||
|
showOnHoverIndex: number,
|
||||||
|
hasShowOnHoverMetric: boolean,
|
||||||
|
listItemName: string
|
||||||
|
) {
|
||||||
|
// Columns before the showOnHover column should slide left when it appears
|
||||||
|
if (!hasShowOnHoverMetric || metricIndex >= showOnHoverIndex) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tappedRow === listItemName) {
|
||||||
|
return 'transition-transform duration-150 translate-x-0'
|
||||||
|
} else {
|
||||||
|
return 'transition-transform duration-150 translate-x-[100%] md:group-hover/report:translate-x-0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function renderReport() {
|
function renderReport() {
|
||||||
if (state.list && state.list.length > 0) {
|
if (state.list && state.list.length > 0) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -206,16 +240,14 @@ export default function ListReport<
|
||||||
</FlipMove>
|
</FlipMove>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!!detailsLinkProps &&
|
{!!detailsLinkProps && !state.loading && (
|
||||||
!state.loading &&
|
<MoreLink
|
||||||
!(maybeHideDetails && !(state.list.length >= MAX_ITEMS)) && (
|
onClick={undefined}
|
||||||
<MoreLink
|
className={'mt-3'}
|
||||||
onClick={undefined}
|
linkProps={detailsLinkProps}
|
||||||
className={'mt-2'}
|
list={state.list}
|
||||||
linkProps={detailsLinkProps}
|
/>
|
||||||
list={state.list}
|
)}
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -223,20 +255,22 @@ export default function ListReport<
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderReportHeader() {
|
function renderReportHeader() {
|
||||||
const metricLabels = getAvailableMetrics().map((metric) => {
|
const metricLabels = getAvailableMetrics()
|
||||||
return (
|
.filter((metric) => !metric.meta.showOnHover)
|
||||||
<div
|
.map((metric) => {
|
||||||
key={metric.key}
|
return (
|
||||||
className={`${metric.key} text-right ${hiddenOnMobileClass(metric)}`}
|
<div
|
||||||
style={{ minWidth: colMinWidth }}
|
key={metric.key}
|
||||||
>
|
className={`${metric.key} text-right ${hiddenOnMobileClass(metric)}`}
|
||||||
{metric.renderLabel(query)}
|
style={{ minWidth: colMinWidth }}
|
||||||
</div>
|
>
|
||||||
)
|
{metric.renderLabel(query)}
|
||||||
})
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pt-3 w-full text-xs font-bold tracking-wide text-gray-500 flex items-center dark:text-gray-400">
|
<div className="pt-3 w-full text-xs font-semibold text-gray-500 flex items-center dark:text-gray-400">
|
||||||
<span className="grow truncate">{keyLabel}</span>
|
<span className="grow truncate">{keyLabel}</span>
|
||||||
{metricLabels}
|
{metricLabels}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -244,11 +278,22 @@ export default function ListReport<
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRow(listItem: TListItem) {
|
function renderRow(listItem: TListItem) {
|
||||||
|
const handleRowClick = (e: React.MouseEvent) => {
|
||||||
|
if (window.innerWidth < 768 && !(e.target as HTMLElement).closest('a')) {
|
||||||
|
if (tappedRow === listItem.name) {
|
||||||
|
setTappedRow(null)
|
||||||
|
} else {
|
||||||
|
setTappedRow(listItem.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={listItem.name} style={{ minHeight: ROW_HEIGHT }}>
|
<div key={listItem.name} style={{ minHeight: ROW_HEIGHT }}>
|
||||||
<div
|
<div
|
||||||
className="group flex w-full items-center hover:bg-gray-100/60 dark:hover:bg-gray-850 rounded-sm transition-colors duration-150"
|
className="group/row flex w-full items-center hover:bg-gray-100/60 dark:hover:bg-gray-850 rounded-sm md:cursor-default cursor-pointer"
|
||||||
style={{ marginTop: ROW_GAP_HEIGHT }}
|
style={{ marginTop: ROW_GAP_HEIGHT }}
|
||||||
|
onClick={handleRowClick}
|
||||||
>
|
>
|
||||||
{renderBarFor(listItem)}
|
{renderBarFor(listItem)}
|
||||||
{renderMetricValuesFor(listItem)}
|
{renderMetricValuesFor(listItem)}
|
||||||
|
|
@ -258,7 +303,7 @@ export default function ListReport<
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderBarFor(listItem: TListItem) {
|
function renderBarFor(listItem: TListItem) {
|
||||||
const lightBackground = color || 'bg-green-50 group-hover:bg-green-100'
|
const lightBackground = color || 'bg-green-50 group-hover/row:bg-green-100'
|
||||||
const metricToPlot = metrics.find((metric) => metric.meta.plot)?.key
|
const metricToPlot = metrics.find((metric) => metric.meta.plot)?.key
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -267,10 +312,10 @@ export default function ListReport<
|
||||||
maxWidthDeduction={undefined}
|
maxWidthDeduction={undefined}
|
||||||
count={listItem[metricToPlot]}
|
count={listItem[metricToPlot]}
|
||||||
all={state.list}
|
all={state.list}
|
||||||
bg={`${lightBackground} dark:bg-gray-500/15 dark:group-hover:bg-gray-500/30`}
|
bg={`${lightBackground} dark:bg-gray-500/15 dark:group-hover/row:bg-gray-500/30`}
|
||||||
plot={metricToPlot}
|
plot={metricToPlot}
|
||||||
>
|
>
|
||||||
<div className="flex justify-start px-2 py-1.5 group text-sm dark:text-gray-300 relative z-9 break-all w-full">
|
<div className="flex justify-start items-center gap-x-1.5 px-2 py-1.5 text-sm dark:text-gray-300 relative z-9 break-all w-full">
|
||||||
<DrilldownLink
|
<DrilldownLink
|
||||||
filterInfo={getFilterInfo(listItem)}
|
filterInfo={getFilterInfo(listItem)}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
|
@ -285,6 +330,7 @@ export default function ListReport<
|
||||||
<ExternalLink
|
<ExternalLink
|
||||||
item={listItem}
|
item={listItem}
|
||||||
getExternalLinkUrl={getExternalLinkUrl}
|
getExternalLinkUrl={getExternalLinkUrl}
|
||||||
|
isTapped={tappedRow === listItem.name}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Bar>
|
</Bar>
|
||||||
|
|
@ -299,19 +345,36 @@ export default function ListReport<
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMetricValuesFor(listItem: TListItem) {
|
function renderMetricValuesFor(listItem: TListItem) {
|
||||||
return getAvailableMetrics().map((metric) => {
|
const availableMetrics = getAvailableMetrics()
|
||||||
return (
|
const showOnHoverIndex = availableMetrics.findIndex(
|
||||||
<div
|
(m) => m.meta.showOnHover
|
||||||
key={`${listItem.name}__${metric.key}`}
|
)
|
||||||
className={`text-right ${hiddenOnMobileClass(metric)}`}
|
const hasShowOnHoverMetric = showOnHoverIndex !== -1
|
||||||
style={{ width: colMinWidth, minWidth: colMinWidth }}
|
|
||||||
>
|
return (
|
||||||
<span className="font-medium text-sm dark:text-gray-200 text-right">
|
<>
|
||||||
{metric.renderValue(listItem, state.meta)}
|
{availableMetrics.map((metric, index) => {
|
||||||
</span>
|
const isShowOnHover = metric.meta.showOnHover
|
||||||
</div>
|
|
||||||
)
|
return (
|
||||||
})
|
<div
|
||||||
|
key={`${listItem.name}__${metric.key}`}
|
||||||
|
className={`text-right ${hiddenOnMobileClass(metric)} ${showOnHoverClass(metric, listItem.name)} ${slideLeftClass(index, showOnHoverIndex, hasShowOnHoverMetric, listItem.name)}`}
|
||||||
|
style={{ width: colMinWidth, minWidth: colMinWidth }}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`font-medium text-sm text-right ${isShowOnHover ? 'text-gray-500 group-hover/row:text-gray-800 dark:group-hover/row:text-gray-200' : 'text-gray-800 dark:text-gray-200'}`}
|
||||||
|
>
|
||||||
|
{metric.renderValue(listItem, state.meta, {
|
||||||
|
detailedView: false,
|
||||||
|
isRowHovered: false
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderLoading() {
|
function renderLoading() {
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,9 @@ const REVENUE = { long: '$1,659.50', short: '$1.7K' }
|
||||||
|
|
||||||
describe('single value', () => {
|
describe('single value', () => {
|
||||||
it('renders small value', async () => {
|
it('renders small value', async () => {
|
||||||
await renderWithTooltip(<MetricValue {...valueProps('visitors', 10)} />)
|
render(<MetricValue {...valueProps('visitors', 10)} />)
|
||||||
|
|
||||||
expect(screen.getByTestId('metric-value')).toHaveTextContent('10')
|
expect(screen.getByTestId('metric-value')).toHaveTextContent('10')
|
||||||
expect(screen.getByRole('tooltip')).toHaveTextContent('10')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders large value', async () => {
|
it('renders large value', async () => {
|
||||||
|
|
@ -25,23 +24,19 @@ describe('single value', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders percentages', async () => {
|
it('renders percentages', async () => {
|
||||||
await renderWithTooltip(<MetricValue {...valueProps('bounce_rate', 5.3)} />)
|
render(<MetricValue {...valueProps('bounce_rate', 5.3)} />)
|
||||||
|
|
||||||
expect(screen.getByTestId('metric-value')).toHaveTextContent('5.3%')
|
expect(screen.getByTestId('metric-value')).toHaveTextContent('5.3%')
|
||||||
expect(screen.getByRole('tooltip')).toHaveTextContent('5.3%')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders durations', async () => {
|
it('renders durations', async () => {
|
||||||
await renderWithTooltip(
|
render(<MetricValue {...valueProps('visit_duration', 60)} />)
|
||||||
<MetricValue {...valueProps('visit_duration', 60)} />
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(screen.getByTestId('metric-value')).toHaveTextContent('1m 00s')
|
expect(screen.getByTestId('metric-value')).toHaveTextContent('1m 00s')
|
||||||
expect(screen.getByRole('tooltip')).toHaveTextContent('1m 00s')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders with custom formatter', async () => {
|
it('renders with custom formatter', async () => {
|
||||||
await renderWithTooltip(
|
render(
|
||||||
<MetricValue
|
<MetricValue
|
||||||
{...valueProps('test_money', 5.3)}
|
{...valueProps('test_money', 5.3)}
|
||||||
formatter={(value) => `${value}$`}
|
formatter={(value) => `${value}$`}
|
||||||
|
|
@ -49,7 +44,6 @@ describe('single value', () => {
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(screen.getByTestId('metric-value')).toHaveTextContent('5.3$')
|
expect(screen.getByTestId('metric-value')).toHaveTextContent('5.3$')
|
||||||
expect(screen.getByRole('tooltip')).toHaveTextContent('5.3$')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders revenue properly', async () => {
|
it('renders revenue properly', async () => {
|
||||||
|
|
@ -80,9 +74,8 @@ describe('comparisons', () => {
|
||||||
expect(screen.getByRole('tooltip')).toHaveTextContent(
|
expect(screen.getByRole('tooltip')).toHaveTextContent(
|
||||||
[
|
[
|
||||||
'10 visitors',
|
'10 visitors',
|
||||||
'↑ 100%',
|
|
||||||
'01 Aug - 31 Aug',
|
'01 Aug - 31 Aug',
|
||||||
'vs',
|
'↑ 100%',
|
||||||
'5 visitors',
|
'5 visitors',
|
||||||
'01 July - 31 July'
|
'01 July - 31 July'
|
||||||
].join('')
|
].join('')
|
||||||
|
|
@ -98,9 +91,8 @@ describe('comparisons', () => {
|
||||||
expect(screen.getByRole('tooltip')).toHaveTextContent(
|
expect(screen.getByRole('tooltip')).toHaveTextContent(
|
||||||
[
|
[
|
||||||
'5 visitors',
|
'5 visitors',
|
||||||
'↓ 50%',
|
|
||||||
'01 Aug - 31 Aug',
|
'01 Aug - 31 Aug',
|
||||||
'vs',
|
'↓ 50%',
|
||||||
'10 visitors',
|
'10 visitors',
|
||||||
'01 July - 31 July'
|
'01 July - 31 July'
|
||||||
].join('')
|
].join('')
|
||||||
|
|
@ -116,9 +108,8 @@ describe('comparisons', () => {
|
||||||
expect(screen.getByRole('tooltip')).toHaveTextContent(
|
expect(screen.getByRole('tooltip')).toHaveTextContent(
|
||||||
[
|
[
|
||||||
'10 visitors',
|
'10 visitors',
|
||||||
'〰 0%',
|
|
||||||
'01 Aug - 31 Aug',
|
'01 Aug - 31 Aug',
|
||||||
'vs',
|
'0%',
|
||||||
'10 visitors',
|
'10 visitors',
|
||||||
'01 July - 31 July'
|
'01 July - 31 July'
|
||||||
].join('')
|
].join('')
|
||||||
|
|
@ -136,9 +127,8 @@ describe('comparisons', () => {
|
||||||
expect(screen.getByRole('tooltip')).toHaveTextContent(
|
expect(screen.getByRole('tooltip')).toHaveTextContent(
|
||||||
[
|
[
|
||||||
'10 conversions',
|
'10 conversions',
|
||||||
'〰 0%',
|
|
||||||
'01 Aug - 31 Aug',
|
'01 Aug - 31 Aug',
|
||||||
'vs',
|
'0%',
|
||||||
'10 conversions',
|
'10 conversions',
|
||||||
'01 July - 31 July'
|
'01 July - 31 July'
|
||||||
].join('')
|
].join('')
|
||||||
|
|
@ -154,14 +144,7 @@ describe('comparisons', () => {
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(screen.getByRole('tooltip')).toHaveTextContent(
|
expect(screen.getByRole('tooltip')).toHaveTextContent(
|
||||||
[
|
['10% ', '01 Aug - 31 Aug', '0%', '10% ', '01 July - 31 July'].join('')
|
||||||
'10% ',
|
|
||||||
'〰 0%',
|
|
||||||
'01 Aug - 31 Aug',
|
|
||||||
'vs',
|
|
||||||
'10% ',
|
|
||||||
'01 July - 31 July'
|
|
||||||
].join('')
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -177,9 +160,8 @@ describe('comparisons', () => {
|
||||||
expect(screen.getByRole('tooltip')).toHaveTextContent(
|
expect(screen.getByRole('tooltip')).toHaveTextContent(
|
||||||
[
|
[
|
||||||
'10$ test',
|
'10$ test',
|
||||||
'↑ 100%',
|
|
||||||
'01 Aug - 31 Aug',
|
'01 Aug - 31 Aug',
|
||||||
'vs',
|
'↑ 100%',
|
||||||
'5$ test',
|
'5$ test',
|
||||||
'01 July - 31 July'
|
'01 July - 31 July'
|
||||||
].join('')
|
].join('')
|
||||||
|
|
@ -200,9 +182,8 @@ describe('comparisons', () => {
|
||||||
expect(screen.getByRole('tooltip')).toHaveTextContent(
|
expect(screen.getByRole('tooltip')).toHaveTextContent(
|
||||||
[
|
[
|
||||||
'$1,659.50 average_revenue',
|
'$1,659.50 average_revenue',
|
||||||
'〰 0%',
|
|
||||||
'01 Aug - 31 Aug',
|
'01 Aug - 31 Aug',
|
||||||
'vs',
|
'0%',
|
||||||
'$1,659.50 average_revenue',
|
'$1,659.50 average_revenue',
|
||||||
'01 July - 31 July'
|
'01 July - 31 July'
|
||||||
].join('')
|
].join('')
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useMemo } from 'react'
|
import React, { useMemo, useRef, useEffect } from 'react'
|
||||||
import { Metric } from '../../../types/query-api'
|
import { Metric } from '../../../types/query-api'
|
||||||
import { Tooltip } from '../../util/tooltip'
|
import { Tooltip } from '../../util/tooltip'
|
||||||
import { ChangeArrow } from './change-arrow'
|
import { ChangeArrow } from './change-arrow'
|
||||||
|
|
@ -36,23 +36,84 @@ export default function MetricValue(props: {
|
||||||
renderLabel: (query: DashboardQuery) => string
|
renderLabel: (query: DashboardQuery) => string
|
||||||
formatter?: (value: ValueType) => string
|
formatter?: (value: ValueType) => string
|
||||||
meta: BreakdownResultMeta | null
|
meta: BreakdownResultMeta | null
|
||||||
|
detailedView?: boolean
|
||||||
|
isRowHovered?: boolean
|
||||||
}) {
|
}) {
|
||||||
const { query } = useQueryContext()
|
const { query } = useQueryContext()
|
||||||
|
const portalRef = useRef<HTMLElement | null>(null)
|
||||||
|
|
||||||
const { metric, listItem } = props
|
useEffect(() => {
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
portalRef.current = document.body
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const { metric, listItem, detailedView = false, isRowHovered = false } = props
|
||||||
const { value, comparison } = useMemo(
|
const { value, comparison } = useMemo(
|
||||||
() => valueRenderProps(listItem, metric),
|
() => valueRenderProps(listItem, metric),
|
||||||
[listItem, metric]
|
[listItem, metric]
|
||||||
)
|
)
|
||||||
const metricLabel = useMemo(() => props.renderLabel(query), [query, props])
|
const metricLabel = useMemo(() => props.renderLabel(query), [query, props])
|
||||||
const shortFormatter = props.formatter ?? MetricFormatterShort[metric]
|
const shortFormatter = props.formatter ?? MetricFormatterShort[metric]
|
||||||
|
const longFormatter = props.formatter ?? MetricFormatterLong[metric]
|
||||||
|
|
||||||
|
const isAbbreviated = useMemo(() => {
|
||||||
|
if (value === null) return false
|
||||||
|
return shortFormatter(value) !== longFormatter(value)
|
||||||
|
}, [value, shortFormatter, longFormatter])
|
||||||
|
|
||||||
|
const showTooltip = detailedView
|
||||||
|
? !!comparison
|
||||||
|
: !!comparison || isAbbreviated
|
||||||
|
|
||||||
|
const shouldShowLongFormat =
|
||||||
|
detailedView && !comparison && isRowHovered && isAbbreviated
|
||||||
|
const displayFormatter = shouldShowLongFormat ? longFormatter : shortFormatter
|
||||||
|
|
||||||
|
const percentageValue = listItem['percentage' as Metric]
|
||||||
|
const shouldShowPercentage =
|
||||||
|
detailedView &&
|
||||||
|
metric === 'visitors' &&
|
||||||
|
isRowHovered &&
|
||||||
|
percentageValue != null
|
||||||
|
const percentageFormatter = MetricFormatterShort['percentage']
|
||||||
|
const percentageDisplay = shouldShowPercentage
|
||||||
|
? percentageFormatter(percentageValue)
|
||||||
|
: null
|
||||||
|
|
||||||
if (value === null && (!comparison || comparison.value === null)) {
|
if (value === null && (!comparison || comparison.value === null)) {
|
||||||
return <span data-testid="metric-value">{shortFormatter(value)}</span>
|
return <span data-testid="metric-value">{displayFormatter(value)}</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
const valueContent = (
|
||||||
|
<span
|
||||||
|
className={showTooltip ? 'cursor-default' : ''}
|
||||||
|
data-testid="metric-value"
|
||||||
|
>
|
||||||
|
{percentageDisplay && (
|
||||||
|
<span className="mr-3 text-gray-500 dark:text-gray-400">
|
||||||
|
{percentageDisplay}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{displayFormatter(value)}
|
||||||
|
{comparison ? (
|
||||||
|
<ChangeArrow
|
||||||
|
change={comparison.change}
|
||||||
|
metric={metric}
|
||||||
|
className="inline-block pl-1 w-4"
|
||||||
|
hideNumber
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!showTooltip) {
|
||||||
|
return valueContent
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
containerRef={portalRef as React.RefObject<HTMLElement>}
|
||||||
info={
|
info={
|
||||||
<ComparisonTooltipContent
|
<ComparisonTooltipContent
|
||||||
value={value}
|
value={value}
|
||||||
|
|
@ -62,17 +123,7 @@ export default function MetricValue(props: {
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span className="cursor-default" data-testid="metric-value">
|
{valueContent}
|
||||||
{shortFormatter(value)}
|
|
||||||
{comparison ? (
|
|
||||||
<ChangeArrow
|
|
||||||
change={comparison.change}
|
|
||||||
metric={metric}
|
|
||||||
className="inline-block pl-1 w-4"
|
|
||||||
hideNumber
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -106,34 +157,34 @@ function ComparisonTooltipContent({
|
||||||
return (
|
return (
|
||||||
<div className="text-left whitespace-nowrap py-1 space-y-2">
|
<div className="text-left whitespace-nowrap py-1 space-y-2">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center">
|
<div className="flex gap-x-4">
|
||||||
<span className="font-bold text-base">
|
<div className="flex flex-col">
|
||||||
{longFormatter(value)} {label}
|
<span className="font-medium text-sm/6 text-white">
|
||||||
</span>
|
{longFormatter(value)} {label}
|
||||||
|
</span>
|
||||||
|
<div className="font-normal text-xs text-white">
|
||||||
|
{meta.date_range_label}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<ChangeArrow
|
<ChangeArrow
|
||||||
metric={metric}
|
metric={metric}
|
||||||
change={comparison.change}
|
change={comparison.change}
|
||||||
className="pl-4 text-xs text-gray-100"
|
className="text-xs/6 font-medium text-white"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="font-normal text-xs">{meta.date_range_label}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div>vs</div>
|
<div className="w-full border-t border-gray-600"></div>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-bold text-base">
|
<div className="font-medium text-sm/6 text-gray-300/80">
|
||||||
{longFormatter(comparison.value)} {label}
|
{longFormatter(comparison.value)} {label}
|
||||||
</div>
|
</div>
|
||||||
<div className="font-normal text-xs">
|
<div className="font-normal text-xs text-gray-300/80">
|
||||||
{meta.comparison_date_range_label}
|
{meta.comparison_date_range_label}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return (
|
return <div className="whitespace-nowrap">{longFormatter(value)}</div>
|
||||||
<div className="whitespace-nowrap">
|
|
||||||
{longFormatter(value)} {label}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,8 @@ export class Metric {
|
||||||
this.renderValue = this.renderValue.bind(this)
|
this.renderValue = this.renderValue.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
renderValue(listItem, meta) {
|
renderValue(listItem, meta, options = {}) {
|
||||||
|
const { detailedView = false, isRowHovered = false } = options
|
||||||
return (
|
return (
|
||||||
<MetricValue
|
<MetricValue
|
||||||
listItem={listItem}
|
listItem={listItem}
|
||||||
|
|
@ -51,6 +52,8 @@ export class Metric {
|
||||||
renderLabel={this.renderLabel}
|
renderLabel={this.renderLabel}
|
||||||
meta={meta}
|
meta={meta}
|
||||||
formatter={this.formatter}
|
formatter={this.formatter}
|
||||||
|
detailedView={detailedView}
|
||||||
|
isRowHovered={isRowHovered}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -85,7 +88,7 @@ export const createVisitors = (props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Metric({
|
return new Metric({
|
||||||
width: 'w-24',
|
width: 'w-36',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
...props,
|
...props,
|
||||||
key: 'visitors',
|
key: 'visitors',
|
||||||
|
|
@ -96,7 +99,7 @@ export const createVisitors = (props) => {
|
||||||
export const createConversionRate = (props) => {
|
export const createConversionRate = (props) => {
|
||||||
const renderLabel = (_query) => 'CR'
|
const renderLabel = (_query) => 'CR'
|
||||||
return new Metric({
|
return new Metric({
|
||||||
width: 'w-24',
|
width: 'w-28 md:w-24',
|
||||||
...props,
|
...props,
|
||||||
key: 'conversion_rate',
|
key: 'conversion_rate',
|
||||||
renderLabel,
|
renderLabel,
|
||||||
|
|
@ -116,13 +119,13 @@ export const createPercentage = (props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createEvents = (props) => {
|
export const createEvents = (props) => {
|
||||||
return new Metric({ width: 'w-24', ...props, key: 'events', sortable: true })
|
return new Metric({ width: 'w-28', ...props, key: 'events', sortable: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createTotalRevenue = (props) => {
|
export const createTotalRevenue = (props) => {
|
||||||
const renderLabel = (_query) => 'Revenue'
|
const renderLabel = (_query) => 'Revenue'
|
||||||
return new Metric({
|
return new Metric({
|
||||||
width: 'w-24',
|
width: 'w-32',
|
||||||
...props,
|
...props,
|
||||||
key: 'total_revenue',
|
key: 'total_revenue',
|
||||||
renderLabel,
|
renderLabel,
|
||||||
|
|
@ -133,7 +136,7 @@ export const createTotalRevenue = (props) => {
|
||||||
export const createAverageRevenue = (props) => {
|
export const createAverageRevenue = (props) => {
|
||||||
const renderLabel = (_query) => 'Average'
|
const renderLabel = (_query) => 'Average'
|
||||||
return new Metric({
|
return new Metric({
|
||||||
width: 'w-24',
|
width: 'w-28',
|
||||||
...props,
|
...props,
|
||||||
key: 'average_revenue',
|
key: 'average_revenue',
|
||||||
renderLabel,
|
renderLabel,
|
||||||
|
|
@ -142,9 +145,9 @@ export const createAverageRevenue = (props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createTotalVisitors = (props) => {
|
export const createTotalVisitors = (props) => {
|
||||||
const renderLabel = (_query) => 'Total Visitors'
|
const renderLabel = (_query) => 'Total visitors'
|
||||||
return new Metric({
|
return new Metric({
|
||||||
width: 'w-28',
|
width: 'w-32',
|
||||||
...props,
|
...props,
|
||||||
key: 'total_visitors',
|
key: 'total_visitors',
|
||||||
renderLabel,
|
renderLabel,
|
||||||
|
|
@ -157,9 +160,9 @@ export const createVisits = (props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createVisitDuration = (props) => {
|
export const createVisitDuration = (props) => {
|
||||||
const renderLabel = (_query) => 'Visit Duration'
|
const renderLabel = (_query) => 'Visit duration'
|
||||||
return new Metric({
|
return new Metric({
|
||||||
width: 'w-36',
|
width: 'w-28 md:w-24',
|
||||||
...props,
|
...props,
|
||||||
key: 'visit_duration',
|
key: 'visit_duration',
|
||||||
renderLabel,
|
renderLabel,
|
||||||
|
|
@ -168,9 +171,9 @@ export const createVisitDuration = (props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createBounceRate = (props) => {
|
export const createBounceRate = (props) => {
|
||||||
const renderLabel = (_query) => 'Bounce Rate'
|
const renderLabel = (_query) => 'Bounce rate'
|
||||||
return new Metric({
|
return new Metric({
|
||||||
width: 'w-28',
|
width: 'w-28 md:w-24',
|
||||||
...props,
|
...props,
|
||||||
key: 'bounce_rate',
|
key: 'bounce_rate',
|
||||||
renderLabel,
|
renderLabel,
|
||||||
|
|
@ -190,9 +193,9 @@ export const createPageviews = (props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createTimeOnPage = (props) => {
|
export const createTimeOnPage = (props) => {
|
||||||
const renderLabel = (_query) => 'Time on Page'
|
const renderLabel = (_query) => 'Time on page'
|
||||||
return new Metric({
|
return new Metric({
|
||||||
width: 'w-32',
|
width: 'w-28 md:w-24',
|
||||||
...props,
|
...props,
|
||||||
key: 'time_on_page',
|
key: 'time_on_page',
|
||||||
renderLabel,
|
renderLabel,
|
||||||
|
|
@ -201,9 +204,9 @@ export const createTimeOnPage = (props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createExitRate = (props) => {
|
export const createExitRate = (props) => {
|
||||||
const renderLabel = (_query) => 'Exit Rate'
|
const renderLabel = (_query) => 'Exit rate'
|
||||||
return new Metric({
|
return new Metric({
|
||||||
width: 'w-28',
|
width: 'w-28 md:w-24',
|
||||||
...props,
|
...props,
|
||||||
key: 'exit_rate',
|
key: 'exit_rate',
|
||||||
renderLabel,
|
renderLabel,
|
||||||
|
|
@ -212,9 +215,9 @@ export const createExitRate = (props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createScrollDepth = (props) => {
|
export const createScrollDepth = (props) => {
|
||||||
const renderLabel = (_query) => 'Scroll Depth'
|
const renderLabel = (_query) => 'Scroll depth'
|
||||||
return new Metric({
|
return new Metric({
|
||||||
width: 'w-28',
|
width: 'w-28 md:w-24',
|
||||||
...props,
|
...props,
|
||||||
key: 'scroll_depth',
|
key: 'scroll_depth',
|
||||||
renderLabel,
|
renderLabel,
|
||||||
|
|
|
||||||
|
|
@ -149,7 +149,7 @@ export function SearchTerms() {
|
||||||
path: referrersGoogleRoute.path,
|
path: referrersGoogleRoute.path,
|
||||||
search: (search: Record<string, unknown>) => search
|
search: (search: Record<string, unknown>) => search
|
||||||
}}
|
}}
|
||||||
className="w-full mt-2"
|
className="w-full mt-3"
|
||||||
onClick={undefined}
|
onClick={undefined}
|
||||||
/>
|
/>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
|
|
||||||
|
|
@ -27,26 +27,26 @@ import { DropdownTabButton, TabButton, TabWrapper } from '../../components/tabs'
|
||||||
|
|
||||||
const UTM_TAGS = {
|
const UTM_TAGS = {
|
||||||
utm_medium: {
|
utm_medium: {
|
||||||
title: 'UTM Mediums',
|
title: 'UTM mediums',
|
||||||
label: 'Medium',
|
label: 'Medium',
|
||||||
endpoint: '/utm_mediums'
|
endpoint: '/utm_mediums'
|
||||||
},
|
},
|
||||||
utm_source: {
|
utm_source: {
|
||||||
title: 'UTM Sources',
|
title: 'UTM sources',
|
||||||
label: 'Source',
|
label: 'Source',
|
||||||
endpoint: '/utm_sources'
|
endpoint: '/utm_sources'
|
||||||
},
|
},
|
||||||
utm_campaign: {
|
utm_campaign: {
|
||||||
title: 'UTM Campaigns',
|
title: 'UTM campaigns',
|
||||||
label: 'Campaign',
|
label: 'Campaign',
|
||||||
endpoint: '/utm_campaigns'
|
endpoint: '/utm_campaigns'
|
||||||
},
|
},
|
||||||
utm_content: {
|
utm_content: {
|
||||||
title: 'UTM Contents',
|
title: 'UTM contents',
|
||||||
label: 'Content',
|
label: 'Content',
|
||||||
endpoint: '/utm_contents'
|
endpoint: '/utm_contents'
|
||||||
},
|
},
|
||||||
utm_term: { title: 'UTM Terms', label: 'Term', endpoint: '/utm_terms' }
|
utm_term: { title: 'UTM terms', label: 'Term', endpoint: '/utm_terms' }
|
||||||
}
|
}
|
||||||
|
|
||||||
function AllSources({ afterFetchData }) {
|
function AllSources({ afterFetchData }) {
|
||||||
|
|
@ -70,6 +70,8 @@ function AllSources({ afterFetchData }) {
|
||||||
function chooseMetrics() {
|
function chooseMetrics() {
|
||||||
return [
|
return [
|
||||||
metrics.createVisitors({ meta: { plot: true } }),
|
metrics.createVisitors({ meta: { plot: true } }),
|
||||||
|
!hasConversionGoalFilter(query) &&
|
||||||
|
metrics.createPercentage({ meta: { showOnHover: true } }),
|
||||||
hasConversionGoalFilter(query) && metrics.createConversionRate()
|
hasConversionGoalFilter(query) && metrics.createConversionRate()
|
||||||
].filter((metric) => !!metric)
|
].filter((metric) => !!metric)
|
||||||
}
|
}
|
||||||
|
|
@ -83,7 +85,7 @@ function AllSources({ afterFetchData }) {
|
||||||
metrics={chooseMetrics()}
|
metrics={chooseMetrics()}
|
||||||
detailsLinkProps={{ path: sourcesRoute.path, search: (search) => search }}
|
detailsLinkProps={{ path: sourcesRoute.path, search: (search) => search }}
|
||||||
renderIcon={renderIcon}
|
renderIcon={renderIcon}
|
||||||
color="bg-blue-50 group-hover:bg-blue-100"
|
color="bg-blue-50 group-hover/row:bg-blue-100"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -106,6 +108,8 @@ function Channels({ onClick, afterFetchData }) {
|
||||||
function chooseMetrics() {
|
function chooseMetrics() {
|
||||||
return [
|
return [
|
||||||
metrics.createVisitors({ meta: { plot: true } }),
|
metrics.createVisitors({ meta: { plot: true } }),
|
||||||
|
!hasConversionGoalFilter(query) &&
|
||||||
|
metrics.createPercentage({ meta: { showOnHover: true } }),
|
||||||
hasConversionGoalFilter(query) && metrics.createConversionRate()
|
hasConversionGoalFilter(query) && metrics.createConversionRate()
|
||||||
].filter((metric) => !!metric)
|
].filter((metric) => !!metric)
|
||||||
}
|
}
|
||||||
|
|
@ -122,7 +126,7 @@ function Channels({ onClick, afterFetchData }) {
|
||||||
path: channelsRoute.path,
|
path: channelsRoute.path,
|
||||||
search: (search) => search
|
search: (search) => search
|
||||||
}}
|
}}
|
||||||
color="bg-blue-50 group-hover:bg-blue-100"
|
color="bg-blue-50 group-hover/row:bg-blue-100"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -154,6 +158,8 @@ function UTMSources({ tab, afterFetchData }) {
|
||||||
function chooseMetrics() {
|
function chooseMetrics() {
|
||||||
return [
|
return [
|
||||||
metrics.createVisitors({ meta: { plot: true } }),
|
metrics.createVisitors({ meta: { plot: true } }),
|
||||||
|
!hasConversionGoalFilter(query) &&
|
||||||
|
metrics.createPercentage({ meta: { showOnHover: true } }),
|
||||||
hasConversionGoalFilter(query) && metrics.createConversionRate()
|
hasConversionGoalFilter(query) && metrics.createConversionRate()
|
||||||
].filter((metric) => !!metric)
|
].filter((metric) => !!metric)
|
||||||
}
|
}
|
||||||
|
|
@ -166,14 +172,14 @@ function UTMSources({ tab, afterFetchData }) {
|
||||||
keyLabel={utmTag.label}
|
keyLabel={utmTag.label}
|
||||||
metrics={chooseMetrics()}
|
metrics={chooseMetrics()}
|
||||||
detailsLinkProps={{ path: route?.path, search: (search) => search }}
|
detailsLinkProps={{ path: route?.path, search: (search) => search }}
|
||||||
color="bg-blue-50 group-hover:bg-blue-100"
|
color="bg-blue-50 group-hover/row:bg-blue-100"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const labelFor = {
|
const labelFor = {
|
||||||
channels: 'Top Channels',
|
channels: 'Top channels',
|
||||||
all: 'Top Sources'
|
all: 'Top sources'
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [key, utm_tag] of Object.entries(UTM_TAGS)) {
|
for (const [key, utm_tag] of Object.entries(UTM_TAGS)) {
|
||||||
|
|
@ -241,7 +247,7 @@ export default function SourceList() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="group/report overflow-x-hidden">
|
||||||
{/* Header Container */}
|
{/* Header Container */}
|
||||||
<div className="w-full flex justify-between">
|
<div className="w-full flex justify-between">
|
||||||
<div className="flex gap-x-1">
|
<div className="flex gap-x-1">
|
||||||
|
|
|
||||||
|
|
@ -291,23 +291,23 @@ export const formattedFilters = {
|
||||||
prop_value: 'Value',
|
prop_value: 'Value',
|
||||||
source: 'Source',
|
source: 'Source',
|
||||||
channel: 'Channel',
|
channel: 'Channel',
|
||||||
utm_medium: 'UTM Medium',
|
utm_medium: 'UTM medium',
|
||||||
utm_source: 'UTM Source',
|
utm_source: 'UTM source',
|
||||||
utm_campaign: 'UTM Campaign',
|
utm_campaign: 'UTM campaign',
|
||||||
utm_content: 'UTM Content',
|
utm_content: 'UTM content',
|
||||||
utm_term: 'UTM Term',
|
utm_term: 'UTM term',
|
||||||
referrer: 'Referrer URL',
|
referrer: 'Referrer URL',
|
||||||
screen: 'Screen size',
|
screen: 'Screen size',
|
||||||
browser: 'Browser',
|
browser: 'Browser',
|
||||||
browser_version: 'Browser Version',
|
browser_version: 'Browser version',
|
||||||
os: 'Operating System',
|
os: 'Operating system',
|
||||||
os_version: 'Operating System Version',
|
os_version: 'Operating system version',
|
||||||
country: 'Country',
|
country: 'Country',
|
||||||
region: 'Region',
|
region: 'Region',
|
||||||
city: 'City',
|
city: 'City',
|
||||||
page: 'Page',
|
page: 'Page',
|
||||||
hostname: 'Hostname',
|
hostname: 'Hostname',
|
||||||
entry_page: 'Entry Page',
|
entry_page: 'Entry page',
|
||||||
exit_page: 'Exit Page',
|
exit_page: 'Exit page',
|
||||||
segment: 'Segment'
|
segment: 'Segment'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,11 @@ export function durationFormatter(duration: number): string {
|
||||||
|
|
||||||
export function percentageFormatter(number: number | null): string {
|
export function percentageFormatter(number: number | null): string {
|
||||||
if (typeof number === 'number') {
|
if (typeof number === 'number') {
|
||||||
return number + '%'
|
if (Math.abs(number) > 0 && Math.abs(number) < 0.1) {
|
||||||
|
return number.toFixed(2) + '%'
|
||||||
|
} else {
|
||||||
|
return number.toFixed(1).replace(/\.0$/, '') + '%'
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return '-'
|
return '-'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,16 +26,14 @@ export function Tooltip({
|
||||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(
|
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
const [arrowElement, setArrowElement] = useState<HTMLDivElement | null>(null)
|
|
||||||
|
|
||||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||||
placement: 'top',
|
placement: 'top',
|
||||||
modifiers: [
|
modifiers: [
|
||||||
{ name: 'arrow', options: { element: arrowElement } },
|
|
||||||
{
|
{
|
||||||
name: 'offset',
|
name: 'offset',
|
||||||
options: {
|
options: {
|
||||||
offset: [0, 4]
|
offset: [0, 6]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
...(boundary
|
...(boundary
|
||||||
|
|
@ -67,8 +65,6 @@ export function Tooltip({
|
||||||
popperStyle={styles.popper}
|
popperStyle={styles.popper}
|
||||||
popperAttributes={attributes.popper}
|
popperAttributes={attributes.popper}
|
||||||
setPopperElement={setPopperElement}
|
setPopperElement={setPopperElement}
|
||||||
setArrowElement={setArrowElement}
|
|
||||||
arrowStyle={styles.arrow}
|
|
||||||
>
|
>
|
||||||
{info}
|
{info}
|
||||||
</TooltipMessage>
|
</TooltipMessage>
|
||||||
|
|
@ -82,16 +78,12 @@ function TooltipMessage({
|
||||||
popperStyle,
|
popperStyle,
|
||||||
popperAttributes,
|
popperAttributes,
|
||||||
setPopperElement,
|
setPopperElement,
|
||||||
setArrowElement,
|
|
||||||
arrowStyle,
|
|
||||||
children
|
children
|
||||||
}: {
|
}: {
|
||||||
containerRef?: RefObject<HTMLElement>
|
containerRef?: RefObject<HTMLElement>
|
||||||
popperStyle: CSSProperties
|
popperStyle: CSSProperties
|
||||||
arrowStyle: CSSProperties
|
|
||||||
popperAttributes?: Record<string, string>
|
popperAttributes?: Record<string, string>
|
||||||
setPopperElement: (element: HTMLDivElement) => void
|
setPopperElement: (element: HTMLDivElement) => void
|
||||||
setArrowElement: (element: HTMLDivElement) => void
|
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
}) {
|
}) {
|
||||||
const messageElement = (
|
const messageElement = (
|
||||||
|
|
@ -99,15 +91,10 @@ function TooltipMessage({
|
||||||
ref={setPopperElement}
|
ref={setPopperElement}
|
||||||
style={popperStyle}
|
style={popperStyle}
|
||||||
{...popperAttributes}
|
{...popperAttributes}
|
||||||
className="z-50 p-2 rounded-sm text-sm text-gray-100 font-bold bg-gray-800 dark:bg-gray-700"
|
className="z-[999] px-2 py-1 rounded-sm text-sm text-gray-100 font-medium bg-gray-800 dark:bg-gray-700"
|
||||||
role="tooltip"
|
role="tooltip"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<div
|
|
||||||
ref={setArrowElement}
|
|
||||||
style={arrowStyle}
|
|
||||||
className="tooltip-arrow"
|
|
||||||
></div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
if (containerRef) {
|
if (containerRef) {
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
|
||||||
phx-target="#funnel-form"
|
phx-target="#funnel-form"
|
||||||
phx-click-away="cancel-add-funnel"
|
phx-click-away="cancel-add-funnel"
|
||||||
onkeydown="return event.key != 'Enter';"
|
onkeydown="return event.key != 'Enter';"
|
||||||
class="bg-white dark:bg-gray-900 shadow-md rounded px-8 pt-6 pb-8 mb-4 mt-8"
|
class="bg-white dark:bg-gray-900 shadow-2xl rounded-lg px-8 pt-6 pb-8 mb-4 mt-8"
|
||||||
>
|
>
|
||||||
<.title class="mb-6">
|
<.title class="mb-6">
|
||||||
{if @funnel, do: "Edit", else: "Add"} funnel
|
{if @funnel, do: "Edit", else: "Add"} funnel
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ defmodule Plausible.Stats.SQL.SpecialMetrics do
|
||||||
|> select_merge_as([], %{
|
|> select_merge_as([], %{
|
||||||
percentage:
|
percentage:
|
||||||
fragment(
|
fragment(
|
||||||
"if(? > 0, round(? / ? * 100, 1), null)",
|
"if(? > 0, round(? / ? * 100, 2), null)",
|
||||||
selected_as(:total_visitors),
|
selected_as(:total_visitors),
|
||||||
selected_as(:visitors),
|
selected_as(:visitors),
|
||||||
selected_as(:total_visitors)
|
selected_as(:total_visitors)
|
||||||
|
|
|
||||||
|
|
@ -475,7 +475,9 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||||
pagination = parse_pagination(params)
|
pagination = parse_pagination(params)
|
||||||
|
|
||||||
extra_metrics =
|
extra_metrics =
|
||||||
if params["detailed"], do: [:bounce_rate, :visit_duration], else: []
|
if params["detailed"],
|
||||||
|
do: [:percentage, :bounce_rate, :visit_duration],
|
||||||
|
else: [:percentage]
|
||||||
|
|
||||||
metrics =
|
metrics =
|
||||||
breakdown_metrics(query,
|
breakdown_metrics(query,
|
||||||
|
|
@ -513,7 +515,9 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||||
pagination = parse_pagination(params)
|
pagination = parse_pagination(params)
|
||||||
|
|
||||||
extra_metrics =
|
extra_metrics =
|
||||||
if params["detailed"], do: [:bounce_rate, :visit_duration], else: []
|
if params["detailed"],
|
||||||
|
do: [:percentage, :bounce_rate, :visit_duration],
|
||||||
|
else: [:percentage]
|
||||||
|
|
||||||
metrics =
|
metrics =
|
||||||
breakdown_metrics(query,
|
breakdown_metrics(query,
|
||||||
|
|
@ -606,7 +610,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||||
|
|
||||||
metrics =
|
metrics =
|
||||||
breakdown_metrics(query,
|
breakdown_metrics(query,
|
||||||
extra_metrics: [:bounce_rate, :visit_duration],
|
extra_metrics: [:percentage, :bounce_rate, :visit_duration],
|
||||||
include_revenue?: !!params["detailed"]
|
include_revenue?: !!params["detailed"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -641,7 +645,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||||
|
|
||||||
metrics =
|
metrics =
|
||||||
breakdown_metrics(query,
|
breakdown_metrics(query,
|
||||||
extra_metrics: [:bounce_rate, :visit_duration],
|
extra_metrics: [:percentage, :bounce_rate, :visit_duration],
|
||||||
include_revenue?: !!params["detailed"]
|
include_revenue?: !!params["detailed"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -676,7 +680,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||||
|
|
||||||
metrics =
|
metrics =
|
||||||
breakdown_metrics(query,
|
breakdown_metrics(query,
|
||||||
extra_metrics: [:bounce_rate, :visit_duration],
|
extra_metrics: [:percentage, :bounce_rate, :visit_duration],
|
||||||
include_revenue?: !!params["detailed"]
|
include_revenue?: !!params["detailed"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -711,7 +715,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||||
|
|
||||||
metrics =
|
metrics =
|
||||||
breakdown_metrics(query,
|
breakdown_metrics(query,
|
||||||
extra_metrics: [:bounce_rate, :visit_duration],
|
extra_metrics: [:percentage, :bounce_rate, :visit_duration],
|
||||||
include_revenue?: !!params["detailed"]
|
include_revenue?: !!params["detailed"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -746,7 +750,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||||
|
|
||||||
metrics =
|
metrics =
|
||||||
breakdown_metrics(query,
|
breakdown_metrics(query,
|
||||||
extra_metrics: [:bounce_rate, :visit_duration],
|
extra_metrics: [:percentage, :bounce_rate, :visit_duration],
|
||||||
include_revenue?: !!params["detailed"]
|
include_revenue?: !!params["detailed"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -781,7 +785,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||||
|
|
||||||
metrics =
|
metrics =
|
||||||
breakdown_metrics(query,
|
breakdown_metrics(query,
|
||||||
extra_metrics: [:bounce_rate, :visit_duration],
|
extra_metrics: [:percentage, :bounce_rate, :visit_duration],
|
||||||
include_revenue?: !!params["detailed"]
|
include_revenue?: !!params["detailed"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -873,7 +877,9 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||||
pagination = parse_pagination(params)
|
pagination = parse_pagination(params)
|
||||||
|
|
||||||
extra_metrics =
|
extra_metrics =
|
||||||
if params["detailed"], do: [:bounce_rate, :visit_duration], else: []
|
if params["detailed"],
|
||||||
|
do: [:percentage, :bounce_rate, :visit_duration],
|
||||||
|
else: [:percentage]
|
||||||
|
|
||||||
metrics =
|
metrics =
|
||||||
breakdown_metrics(query,
|
breakdown_metrics(query,
|
||||||
|
|
@ -902,9 +908,9 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||||
|
|
||||||
extra_metrics =
|
extra_metrics =
|
||||||
if params["detailed"] do
|
if params["detailed"] do
|
||||||
[:pageviews, :bounce_rate, :time_on_page, :scroll_depth]
|
[:percentage, :pageviews, :bounce_rate, :time_on_page, :scroll_depth]
|
||||||
else
|
else
|
||||||
[]
|
[:percentage]
|
||||||
end
|
end
|
||||||
|
|
||||||
metrics =
|
metrics =
|
||||||
|
|
@ -947,7 +953,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||||
|
|
||||||
metrics =
|
metrics =
|
||||||
breakdown_metrics(query,
|
breakdown_metrics(query,
|
||||||
extra_metrics: [:visits, :visit_duration, :bounce_rate],
|
extra_metrics: [:percentage, :visits, :visit_duration, :bounce_rate],
|
||||||
include_revenue?: !!params["detailed"]
|
include_revenue?: !!params["detailed"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -990,9 +996,9 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||||
|
|
||||||
extra_metrics =
|
extra_metrics =
|
||||||
if TableDecider.sessions_join_events?(query) do
|
if TableDecider.sessions_join_events?(query) do
|
||||||
[:visits]
|
[:percentage, :visits]
|
||||||
else
|
else
|
||||||
[:visits, :exit_rate]
|
[:percentage, :visits, :exit_rate]
|
||||||
end
|
end
|
||||||
|
|
||||||
metrics =
|
metrics =
|
||||||
|
|
@ -1099,7 +1105,12 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||||
params = Map.put(params, "property", "visit:region")
|
params = Map.put(params, "property", "visit:region")
|
||||||
query = Query.from(site, params, debug_metadata(conn))
|
query = Query.from(site, params, debug_metadata(conn))
|
||||||
pagination = parse_pagination(params)
|
pagination = parse_pagination(params)
|
||||||
metrics = breakdown_metrics(query, include_revenue?: !!params["detailed"])
|
|
||||||
|
metrics =
|
||||||
|
breakdown_metrics(query,
|
||||||
|
extra_metrics: [:percentage],
|
||||||
|
include_revenue?: !!params["detailed"]
|
||||||
|
)
|
||||||
|
|
||||||
%{results: results, meta: meta} = Stats.breakdown(site, query, metrics, pagination)
|
%{results: results, meta: meta} = Stats.breakdown(site, query, metrics, pagination)
|
||||||
|
|
||||||
|
|
@ -1140,7 +1151,12 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||||
params = Map.put(params, "property", "visit:city")
|
params = Map.put(params, "property", "visit:city")
|
||||||
query = Query.from(site, params, debug_metadata(conn))
|
query = Query.from(site, params, debug_metadata(conn))
|
||||||
pagination = parse_pagination(params)
|
pagination = parse_pagination(params)
|
||||||
metrics = breakdown_metrics(query, include_revenue?: !!params["detailed"])
|
|
||||||
|
metrics =
|
||||||
|
breakdown_metrics(query,
|
||||||
|
extra_metrics: [:percentage],
|
||||||
|
include_revenue?: !!params["detailed"]
|
||||||
|
)
|
||||||
|
|
||||||
%{results: results, meta: meta} = Stats.breakdown(site, query, metrics, pagination)
|
%{results: results, meta: meta} = Stats.breakdown(site, query, metrics, pagination)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -163,7 +163,7 @@ defmodule PlausibleWeb.Live.Components.Modal do
|
||||||
|
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
class = [
|
class = [
|
||||||
"md:w-1/2 w-full max-w-md mx-auto bg-white dark:bg-gray-900 shadow-xl rounded-lg px-8 pt-6 pb-8 top-24",
|
"md:w-1/2 w-full max-w-md mx-auto bg-white dark:bg-gray-900 shadow-2xl rounded-lg px-8 pt-6 pb-8 top-24",
|
||||||
assigns.class
|
assigns.class
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -280,7 +280,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController.QueryComparisonsTest do
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"dimensions" => ["Firefox"],
|
"dimensions" => ["Firefox"],
|
||||||
"metrics" => [2, 33.3],
|
"metrics" => [2, 33.33],
|
||||||
"comparison" => %{
|
"comparison" => %{
|
||||||
"dimensions" => ["Firefox"],
|
"dimensions" => ["Firefox"],
|
||||||
"metrics" => [4, 50.0],
|
"metrics" => [4, 50.0],
|
||||||
|
|
@ -304,11 +304,11 @@ defmodule PlausibleWeb.Api.ExternalStatsController.QueryComparisonsTest do
|
||||||
assert json_response(conn2, 200)["results"] == [
|
assert json_response(conn2, 200)["results"] == [
|
||||||
%{
|
%{
|
||||||
"dimensions" => ["Safari"],
|
"dimensions" => ["Safari"],
|
||||||
"metrics" => [1, 16.7],
|
"metrics" => [1, 16.67],
|
||||||
"comparison" => %{
|
"comparison" => %{
|
||||||
"dimensions" => ["Safari"],
|
"dimensions" => ["Safari"],
|
||||||
"metrics" => [3, 37.5],
|
"metrics" => [3, 37.5],
|
||||||
"change" => [-67, -55]
|
"change" => [-67, -56]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -361,7 +361,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController.QueryComparisonsTest do
|
||||||
assert json_response(conn, 200)["results"] == [
|
assert json_response(conn, 200)["results"] == [
|
||||||
%{
|
%{
|
||||||
"dimensions" => ["Chrome"],
|
"dimensions" => ["Chrome"],
|
||||||
"metrics" => [2, 66.7],
|
"metrics" => [2, 66.67],
|
||||||
"comparison" => %{
|
"comparison" => %{
|
||||||
"dimensions" => ["Chrome"],
|
"dimensions" => ["Chrome"],
|
||||||
"metrics" => [40, 40.0],
|
"metrics" => [40, 40.0],
|
||||||
|
|
@ -370,7 +370,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController.QueryComparisonsTest do
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"dimensions" => ["Firefox"],
|
"dimensions" => ["Firefox"],
|
||||||
"metrics" => [1, 33.3],
|
"metrics" => [1, 33.33],
|
||||||
"comparison" => %{
|
"comparison" => %{
|
||||||
"dimensions" => ["Firefox"],
|
"dimensions" => ["Firefox"],
|
||||||
"metrics" => [50, 50.0],
|
"metrics" => [50, 50.0],
|
||||||
|
|
|
||||||
|
|
@ -2045,8 +2045,8 @@ defmodule PlausibleWeb.Api.ExternalStatsController.QueryTest do
|
||||||
|
|
||||||
assert results == [
|
assert results == [
|
||||||
%{"dimensions" => [unquote(value1)], "metrics" => [3, 50]},
|
%{"dimensions" => [unquote(value1)], "metrics" => [3, 50]},
|
||||||
%{"dimensions" => [unquote(value2)], "metrics" => [2, 33.3]},
|
%{"dimensions" => [unquote(value2)], "metrics" => [2, 33.33]},
|
||||||
%{"dimensions" => [unquote(blank_value)], "metrics" => [1, 16.7]}
|
%{"dimensions" => [unquote(blank_value)], "metrics" => [1, 16.67]}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ defmodule PlausibleWeb.Api.StatsController.BrowsersTest do
|
||||||
conn = get(conn, "/api/stats/#{site.domain}/browsers?period=day")
|
conn = get(conn, "/api/stats/#{site.domain}/browsers?period=day")
|
||||||
|
|
||||||
assert json_response(conn, 200)["results"] == [
|
assert json_response(conn, 200)["results"] == [
|
||||||
%{"name" => "Chrome", "visitors" => 2, "percentage" => 66.7},
|
%{"name" => "Chrome", "visitors" => 2, "percentage" => 66.67},
|
||||||
%{"name" => "Firefox", "visitors" => 1, "percentage" => 33.3}
|
%{"name" => "Firefox", "visitors" => 1, "percentage" => 33.33}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -132,8 +132,8 @@ defmodule PlausibleWeb.Api.StatsController.BrowsersTest do
|
||||||
conn2 = get(conn, "/api/stats/#{site.domain}/browsers?period=day&with_imported=true")
|
conn2 = get(conn, "/api/stats/#{site.domain}/browsers?period=day&with_imported=true")
|
||||||
|
|
||||||
assert json_response(conn2, 200)["results"] == [
|
assert json_response(conn2, 200)["results"] == [
|
||||||
%{"name" => "Chrome", "visitors" => 2, "percentage" => 66.7},
|
%{"name" => "Chrome", "visitors" => 2, "percentage" => 66.67},
|
||||||
%{"name" => "Firefox", "visitors" => 1, "percentage" => 33.3}
|
%{"name" => "Firefox", "visitors" => 1, "percentage" => 33.33}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -211,7 +211,7 @@ defmodule PlausibleWeb.Api.StatsController.BrowsersTest do
|
||||||
%{
|
%{
|
||||||
"name" => "Chrome",
|
"name" => "Chrome",
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"percentage" => 66.7,
|
"percentage" => 66.67,
|
||||||
"comparison" => %{
|
"comparison" => %{
|
||||||
"visitors" => 0,
|
"visitors" => 0,
|
||||||
"percentage" => 0.0,
|
"percentage" => 0.0,
|
||||||
|
|
@ -221,7 +221,7 @@ defmodule PlausibleWeb.Api.StatsController.BrowsersTest do
|
||||||
%{
|
%{
|
||||||
"name" => "Firefox",
|
"name" => "Firefox",
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"percentage" => 33.3,
|
"percentage" => 33.33,
|
||||||
"comparison" => %{
|
"comparison" => %{
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"percentage" => 50.0,
|
"percentage" => 50.0,
|
||||||
|
|
@ -257,7 +257,7 @@ defmodule PlausibleWeb.Api.StatsController.BrowsersTest do
|
||||||
%{
|
%{
|
||||||
"name" => "Chrome",
|
"name" => "Chrome",
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"percentage" => 66.7,
|
"percentage" => 66.67,
|
||||||
"comparison" => %{
|
"comparison" => %{
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"percentage" => 25.0,
|
"percentage" => 25.0,
|
||||||
|
|
@ -452,14 +452,14 @@ defmodule PlausibleWeb.Api.StatsController.BrowsersTest do
|
||||||
"name" => "Chrome 78.0",
|
"name" => "Chrome 78.0",
|
||||||
"version" => "78.0",
|
"version" => "78.0",
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"percentage" => 66.7,
|
"percentage" => 66.67,
|
||||||
"browser" => "Chrome"
|
"browser" => "Chrome"
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"name" => "Chrome 77.0",
|
"name" => "Chrome 77.0",
|
||||||
"version" => "77.0",
|
"version" => "77.0",
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"percentage" => 33.3,
|
"percentage" => 33.33,
|
||||||
"browser" => "Chrome"
|
"browser" => "Chrome"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -38,8 +38,20 @@ defmodule PlausibleWeb.Api.StatsController.CitiesTest do
|
||||||
conn = get(conn, "/api/stats/#{site.domain}/cities?period=day")
|
conn = get(conn, "/api/stats/#{site.domain}/cities?period=day")
|
||||||
|
|
||||||
assert json_response(conn, 200)["results"] == [
|
assert json_response(conn, 200)["results"] == [
|
||||||
%{"code" => 588_409, "country_flag" => "🇪🇪", "name" => "Tallinn", "visitors" => 3},
|
%{
|
||||||
%{"code" => 591_632, "country_flag" => "🇪🇪", "name" => "Kärdla", "visitors" => 2}
|
"code" => 588_409,
|
||||||
|
"country_flag" => "🇪🇪",
|
||||||
|
"name" => "Tallinn",
|
||||||
|
"visitors" => 3,
|
||||||
|
"percentage" => 60.0
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
"code" => 591_632,
|
||||||
|
"country_flag" => "🇪🇪",
|
||||||
|
"name" => "Kärdla",
|
||||||
|
"visitors" => 2,
|
||||||
|
"percentage" => 40.0
|
||||||
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -48,7 +60,13 @@ defmodule PlausibleWeb.Api.StatsController.CitiesTest do
|
||||||
conn = get(conn, "/api/stats/#{site.domain}/cities?period=day&filters=#{filters}")
|
conn = get(conn, "/api/stats/#{site.domain}/cities?period=day&filters=#{filters}")
|
||||||
|
|
||||||
assert json_response(conn, 200)["results"] == [
|
assert json_response(conn, 200)["results"] == [
|
||||||
%{"code" => 591_632, "country_flag" => "🇪🇪", "name" => "Kärdla", "visitors" => 2}
|
%{
|
||||||
|
"code" => 591_632,
|
||||||
|
"country_flag" => "🇪🇪",
|
||||||
|
"name" => "Kärdla",
|
||||||
|
"visitors" => 2,
|
||||||
|
"percentage" => 100.0
|
||||||
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -62,8 +80,20 @@ defmodule PlausibleWeb.Api.StatsController.CitiesTest do
|
||||||
conn = get(conn, "/api/stats/#{site.domain}/cities?period=day&with_imported=true")
|
conn = get(conn, "/api/stats/#{site.domain}/cities?period=day&with_imported=true")
|
||||||
|
|
||||||
assert json_response(conn, 200)["results"] == [
|
assert json_response(conn, 200)["results"] == [
|
||||||
%{"code" => 588_409, "country_flag" => "🇪🇪", "name" => "Tallinn", "visitors" => 4},
|
%{
|
||||||
%{"code" => 591_632, "country_flag" => "🇪🇪", "name" => "Kärdla", "visitors" => 2}
|
"code" => 588_409,
|
||||||
|
"country_flag" => "🇪🇪",
|
||||||
|
"name" => "Tallinn",
|
||||||
|
"visitors" => 4,
|
||||||
|
"percentage" => 66.67
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
"code" => 591_632,
|
||||||
|
"country_flag" => "🇪🇪",
|
||||||
|
"name" => "Kärdla",
|
||||||
|
"visitors" => 2,
|
||||||
|
"percentage" => 33.33
|
||||||
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ defmodule PlausibleWeb.Api.StatsController.CountriesTest do
|
||||||
"name" => "Estonia",
|
"name" => "Estonia",
|
||||||
"flag" => "🇪🇪",
|
"flag" => "🇪🇪",
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"percentage" => 66.7
|
"percentage" => 66.67
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"code" => "GB",
|
"code" => "GB",
|
||||||
|
|
@ -31,7 +31,7 @@ defmodule PlausibleWeb.Api.StatsController.CountriesTest do
|
||||||
"name" => "United Kingdom",
|
"name" => "United Kingdom",
|
||||||
"flag" => "🇬🇧",
|
"flag" => "🇬🇧",
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"percentage" => 33.3
|
"percentage" => 33.33
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -89,13 +89,13 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"name" => "K2sna Kalle",
|
"name" => "K2sna Kalle",
|
||||||
"events" => 2,
|
"events" => 2,
|
||||||
"percentage" => 66.7
|
"percentage" => 66.67
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"name" => "(none)",
|
"name" => "(none)",
|
||||||
"events" => 1,
|
"events" => 1,
|
||||||
"percentage" => 33.3
|
"percentage" => 33.33
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -135,7 +135,7 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"name" => "Teet",
|
"name" => "Teet",
|
||||||
"events" => 2,
|
"events" => 2,
|
||||||
"percentage" => 33.3
|
"percentage" => 33.33
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -144,7 +144,7 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"name" => "(none)",
|
"name" => "(none)",
|
||||||
"events" => 1,
|
"events" => 1,
|
||||||
"percentage" => 16.7
|
"percentage" => 16.67
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -1082,13 +1082,13 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"name" => "K2sna Kalle",
|
"name" => "K2sna Kalle",
|
||||||
"events" => 2,
|
"events" => 2,
|
||||||
"percentage" => 66.7
|
"percentage" => 66.67
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"name" => "Sipsik",
|
"name" => "Sipsik",
|
||||||
"events" => 1,
|
"events" => 1,
|
||||||
"percentage" => 33.3
|
"percentage" => 33.33
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -1121,13 +1121,13 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"name" => "bar",
|
"name" => "bar",
|
||||||
"events" => 2,
|
"events" => 2,
|
||||||
"percentage" => 66.7
|
"percentage" => 66.67
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"name" => "foobar",
|
"name" => "foobar",
|
||||||
"events" => 1,
|
"events" => 1,
|
||||||
"percentage" => 33.3
|
"percentage" => 33.33
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -154,6 +154,15 @@ defmodule PlausibleWeb.Api.StatsController.ImportedTest do
|
||||||
)
|
)
|
||||||
])
|
])
|
||||||
|
|
||||||
|
populate_stats(site, import_id, [
|
||||||
|
build(:imported_visitors, date: ~D[2021-01-01]),
|
||||||
|
build(:imported_visitors, date: ~D[2021-01-01]),
|
||||||
|
build(:imported_visitors, date: ~D[2021-01-01]),
|
||||||
|
build(:imported_visitors, date: ~D[2021-01-01]),
|
||||||
|
build(:imported_visitors, date: ~D[2021-01-31]),
|
||||||
|
build(:imported_visitors, date: ~D[2021-01-31])
|
||||||
|
])
|
||||||
|
|
||||||
import_data(
|
import_data(
|
||||||
[
|
[
|
||||||
%{
|
%{
|
||||||
|
|
@ -280,11 +289,11 @@ defmodule PlausibleWeb.Api.StatsController.ImportedTest do
|
||||||
|> Enum.sort()
|
|> Enum.sort()
|
||||||
|
|
||||||
assert results == [
|
assert results == [
|
||||||
%{"name" => "A Nice Newsletter", "visitors" => 1},
|
%{"name" => "A Nice Newsletter", "visitors" => 1, "percentage" => 11.11},
|
||||||
%{"name" => "Direct / None", "visitors" => 1},
|
%{"name" => "Direct / None", "visitors" => 1, "percentage" => 11.11},
|
||||||
%{"name" => "DuckDuckGo", "visitors" => 2},
|
%{"name" => "DuckDuckGo", "visitors" => 2, "percentage" => 22.22},
|
||||||
%{"name" => "Google", "visitors" => 4},
|
%{"name" => "Google", "visitors" => 4, "percentage" => 44.44},
|
||||||
%{"name" => "Twitter", "visitors" => 1}
|
%{"name" => "Twitter", "visitors" => 1, "percentage" => 11.11}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -415,10 +424,10 @@ defmodule PlausibleWeb.Api.StatsController.ImportedTest do
|
||||||
|> Enum.sort()
|
|> Enum.sort()
|
||||||
|
|
||||||
assert results == [
|
assert results == [
|
||||||
%{"name" => "(not set)", "visitors" => 1},
|
%{"name" => "(not set)", "visitors" => 1, "percentage" => 33.33},
|
||||||
%{"name" => "Direct", "visitors" => 2},
|
%{"name" => "Direct", "visitors" => 2, "percentage" => 66.67},
|
||||||
%{"name" => "Organic Search", "visitors" => 3},
|
%{"name" => "Organic Search", "visitors" => 3, "percentage" => 100.0},
|
||||||
%{"name" => "Paid Search", "visitors" => 2}
|
%{"name" => "Paid Search", "visitors" => 2, "percentage" => 66.67}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -492,8 +501,9 @@ defmodule PlausibleWeb.Api.StatsController.ImportedTest do
|
||||||
%{
|
%{
|
||||||
"bounce_rate" => 100.0,
|
"bounce_rate" => 100.0,
|
||||||
"name" => "social",
|
"name" => "social",
|
||||||
"visit_duration" => 20,
|
"visit_duration" => 20.0,
|
||||||
"visitors" => 3
|
"visitors" => 3,
|
||||||
|
"percentage" => 100.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -581,13 +591,15 @@ defmodule PlausibleWeb.Api.StatsController.ImportedTest do
|
||||||
"name" => "august",
|
"name" => "august",
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"bounce_rate" => 50.0,
|
"bounce_rate" => 50.0,
|
||||||
"visit_duration" => 50.0
|
"visit_duration" => 50.0,
|
||||||
|
"percentage" => 50.0
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"name" => "profile",
|
"name" => "profile",
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"bounce_rate" => 100.0,
|
"bounce_rate" => 100.0,
|
||||||
"visit_duration" => 50.0
|
"visit_duration" => 50.0,
|
||||||
|
"percentage" => 50.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -676,13 +688,15 @@ defmodule PlausibleWeb.Api.StatsController.ImportedTest do
|
||||||
"name" => "Sweden",
|
"name" => "Sweden",
|
||||||
"visitors" => 3,
|
"visitors" => 3,
|
||||||
"bounce_rate" => 67.0,
|
"bounce_rate" => 67.0,
|
||||||
"visit_duration" => 33.0
|
"visit_duration" => 33.0,
|
||||||
|
"percentage" => 60.0
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"name" => "oat milk",
|
"name" => "oat milk",
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"bounce_rate" => 100.0,
|
"bounce_rate" => 100.0,
|
||||||
"visit_duration" => 50.0
|
"visit_duration" => 50.0,
|
||||||
|
"percentage" => 40.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -770,13 +784,15 @@ defmodule PlausibleWeb.Api.StatsController.ImportedTest do
|
||||||
"name" => "ad",
|
"name" => "ad",
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"bounce_rate" => 100.0,
|
"bounce_rate" => 100.0,
|
||||||
"visit_duration" => 50.0
|
"visit_duration" => 50.0,
|
||||||
|
"percentage" => 50.0
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"name" => "blog",
|
"name" => "blog",
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"bounce_rate" => 50.0,
|
"bounce_rate" => 50.0,
|
||||||
"visit_duration" => 50.0
|
"visit_duration" => 50.0,
|
||||||
|
"percentage" => 50.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -801,6 +817,13 @@ defmodule PlausibleWeb.Api.StatsController.ImportedTest do
|
||||||
)
|
)
|
||||||
])
|
])
|
||||||
|
|
||||||
|
populate_stats(site, import_id, [
|
||||||
|
build(:imported_visitors, date: ~D[2021-01-01]),
|
||||||
|
build(:imported_visitors, date: ~D[2021-01-01]),
|
||||||
|
build(:imported_visitors, date: ~D[2021-01-01]),
|
||||||
|
build(:imported_visitors, date: ~D[2021-01-01])
|
||||||
|
])
|
||||||
|
|
||||||
import_data(
|
import_data(
|
||||||
[
|
[
|
||||||
%{
|
%{
|
||||||
|
|
@ -877,12 +900,13 @@ defmodule PlausibleWeb.Api.StatsController.ImportedTest do
|
||||||
|
|
||||||
assert json_response(conn, 200)["results"] == [
|
assert json_response(conn, 200)["results"] == [
|
||||||
%{
|
%{
|
||||||
"bounce_rate" => 0,
|
"bounce_rate" => 0.0,
|
||||||
"time_on_page" => 60,
|
"time_on_page" => 60,
|
||||||
"visitors" => 3,
|
"visitors" => 3,
|
||||||
"pageviews" => 4,
|
"pageviews" => 4,
|
||||||
"scroll_depth" => nil,
|
"scroll_depth" => nil,
|
||||||
"name" => "/some-other-page"
|
"name" => "/some-other-page",
|
||||||
|
"percentage" => 60.0
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"bounce_rate" => 25.0,
|
"bounce_rate" => 25.0,
|
||||||
|
|
@ -890,7 +914,8 @@ defmodule PlausibleWeb.Api.StatsController.ImportedTest do
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"pageviews" => 2,
|
"pageviews" => 2,
|
||||||
"scroll_depth" => nil,
|
"scroll_depth" => nil,
|
||||||
"name" => "/"
|
"name" => "/",
|
||||||
|
"percentage" => 40.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -960,12 +985,19 @@ defmodule PlausibleWeb.Api.StatsController.ImportedTest do
|
||||||
)
|
)
|
||||||
|
|
||||||
assert json_response(conn, 200)["results"] == [
|
assert json_response(conn, 200)["results"] == [
|
||||||
%{"code" => 588_335, "name" => "Tartu", "visitors" => 1, "country_flag" => "🇪🇪"},
|
%{
|
||||||
|
"code" => 588_335,
|
||||||
|
"name" => "Tartu",
|
||||||
|
"visitors" => 1,
|
||||||
|
"country_flag" => "🇪🇪",
|
||||||
|
"percentage" => 50.0
|
||||||
|
},
|
||||||
%{
|
%{
|
||||||
"code" => 2_650_225,
|
"code" => 2_650_225,
|
||||||
"name" => "Edinburgh",
|
"name" => "Edinburgh",
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"country_flag" => "🇬🇧"
|
"country_flag" => "🇬🇧",
|
||||||
|
"percentage" => 50.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ defmodule PlausibleWeb.Api.StatsController.OperatingSystemsTest do
|
||||||
conn = get(conn, "/api/stats/#{site.domain}/operating-systems?period=day")
|
conn = get(conn, "/api/stats/#{site.domain}/operating-systems?period=day")
|
||||||
|
|
||||||
assert json_response(conn, 200)["results"] == [
|
assert json_response(conn, 200)["results"] == [
|
||||||
%{"name" => "Mac", "visitors" => 2, "percentage" => 66.7},
|
%{"name" => "Mac", "visitors" => 2, "percentage" => 66.67},
|
||||||
%{"name" => "Android", "visitors" => 1, "percentage" => 33.3}
|
%{"name" => "Android", "visitors" => 1, "percentage" => 33.33}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -175,8 +175,8 @@ defmodule PlausibleWeb.Api.StatsController.OperatingSystemsTest do
|
||||||
conn1 = get(conn, "/api/stats/#{site.domain}/operating-systems?period=day")
|
conn1 = get(conn, "/api/stats/#{site.domain}/operating-systems?period=day")
|
||||||
|
|
||||||
assert json_response(conn1, 200)["results"] == [
|
assert json_response(conn1, 200)["results"] == [
|
||||||
%{"name" => "Mac", "visitors" => 2, "percentage" => 66.7},
|
%{"name" => "Mac", "visitors" => 2, "percentage" => 66.67},
|
||||||
%{"name" => "Android", "visitors" => 1, "percentage" => 33.3}
|
%{"name" => "Android", "visitors" => 1, "percentage" => 33.33}
|
||||||
]
|
]
|
||||||
|
|
||||||
conn2 =
|
conn2 =
|
||||||
|
|
@ -357,14 +357,14 @@ defmodule PlausibleWeb.Api.StatsController.OperatingSystemsTest do
|
||||||
%{
|
%{
|
||||||
"name" => "Mac 10.16",
|
"name" => "Mac 10.16",
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"percentage" => 66.7,
|
"percentage" => 66.67,
|
||||||
"os" => "Mac",
|
"os" => "Mac",
|
||||||
"version" => "10.16"
|
"version" => "10.16"
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"name" => "Mac 10.15",
|
"name" => "Mac 10.15",
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"percentage" => 33.3,
|
"percentage" => 33.33,
|
||||||
"os" => "Mac",
|
"os" => "Mac",
|
||||||
"version" => "10.15"
|
"version" => "10.15"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,9 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
conn = get(conn, "/api/stats/#{site.domain}/pages?period=day")
|
conn = get(conn, "/api/stats/#{site.domain}/pages?period=day")
|
||||||
|
|
||||||
assert json_response(conn, 200)["results"] == [
|
assert json_response(conn, 200)["results"] == [
|
||||||
%{"visitors" => 3, "name" => "/"},
|
%{"visitors" => 3, "name" => "/", "percentage" => 50.0},
|
||||||
%{"visitors" => 2, "name" => "/register"},
|
%{"visitors" => 2, "name" => "/register", "percentage" => 33.33},
|
||||||
%{"visitors" => 1, "name" => "/contact"}
|
%{"visitors" => 1, "name" => "/contact", "percentage" => 16.67}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -46,18 +46,18 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
conn = get(conn1, "/api/stats/#{site.domain}/pages?period=day&filters=#{filters}")
|
conn = get(conn1, "/api/stats/#{site.domain}/pages?period=day&filters=#{filters}")
|
||||||
|
|
||||||
assert json_response(conn, 200)["results"] == [
|
assert json_response(conn, 200)["results"] == [
|
||||||
%{"visitors" => 3, "name" => "/"},
|
%{"visitors" => 3, "name" => "/", "percentage" => 50.0},
|
||||||
%{"visitors" => 2, "name" => "/register"},
|
%{"visitors" => 2, "name" => "/register", "percentage" => 33.33},
|
||||||
%{"visitors" => 1, "name" => "/contact"},
|
%{"visitors" => 1, "name" => "/contact", "percentage" => 16.67},
|
||||||
%{"visitors" => 1, "name" => "/landing"}
|
%{"visitors" => 1, "name" => "/landing", "percentage" => 16.67}
|
||||||
]
|
]
|
||||||
|
|
||||||
filters = Jason.encode!([[:is, "event:hostname", ["d.example.com"]]])
|
filters = Jason.encode!([[:is, "event:hostname", ["d.example.com"]]])
|
||||||
conn = get(conn1, "/api/stats/#{site.domain}/pages?period=day&filters=#{filters}")
|
conn = get(conn1, "/api/stats/#{site.domain}/pages?period=day&filters=#{filters}")
|
||||||
|
|
||||||
assert json_response(conn, 200)["results"] == [
|
assert json_response(conn, 200)["results"] == [
|
||||||
%{"visitors" => 2, "name" => "/register"},
|
%{"visitors" => 2, "name" => "/register", "percentage" => 66.67},
|
||||||
%{"visitors" => 1, "name" => "/"}
|
%{"visitors" => 1, "name" => "/", "percentage" => 33.33}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -80,7 +80,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
conn = get(conn, "/api/stats/#{site.domain}/pages?period=day&filters=#{filters}")
|
conn = get(conn, "/api/stats/#{site.domain}/pages?period=day&filters=#{filters}")
|
||||||
|
|
||||||
assert json_response(conn, 200)["results"] == [
|
assert json_response(conn, 200)["results"] == [
|
||||||
%{"visitors" => 1, "name" => "/blog/john-1"}
|
%{"visitors" => 1, "name" => "/blog/john-1", "percentage" => 100.0}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -106,8 +106,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
conn = get(conn, "/api/stats/#{site.domain}/pages?period=day&filters=#{filters}")
|
conn = get(conn, "/api/stats/#{site.domain}/pages?period=day&filters=#{filters}")
|
||||||
|
|
||||||
assert json_response(conn, 200)["results"] == [
|
assert json_response(conn, 200)["results"] == [
|
||||||
%{"visitors" => 1, "name" => "/"},
|
%{"visitors" => 1, "name" => "/", "percentage" => 50.0},
|
||||||
%{"visitors" => 1, "name" => "/blog/other-post"}
|
%{"visitors" => 1, "name" => "/blog/other-post", "percentage" => 50.0}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -143,8 +143,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
conn = get(conn, "/api/stats/#{site.domain}/pages?period=day&filters=#{filters}")
|
conn = get(conn, "/api/stats/#{site.domain}/pages?period=day&filters=#{filters}")
|
||||||
|
|
||||||
assert json_response(conn, 200)["results"] == [
|
assert json_response(conn, 200)["results"] == [
|
||||||
%{"visitors" => 1, "name" => "/1"},
|
%{"visitors" => 1, "name" => "/1", "percentage" => 50.0},
|
||||||
%{"visitors" => 1, "name" => "/2"}
|
%{"visitors" => 1, "name" => "/2", "percentage" => 50.0}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -185,9 +185,9 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
conn = get(conn, "/api/stats/#{site.domain}/pages?period=day&filters=#{filters}")
|
conn = get(conn, "/api/stats/#{site.domain}/pages?period=day&filters=#{filters}")
|
||||||
|
|
||||||
assert json_response(conn, 200)["results"] == [
|
assert json_response(conn, 200)["results"] == [
|
||||||
%{"visitors" => 1, "name" => "/1"},
|
%{"visitors" => 1, "name" => "/1", "percentage" => 33.33},
|
||||||
%{"visitors" => 1, "name" => "/2"},
|
%{"visitors" => 1, "name" => "/2", "percentage" => 33.33},
|
||||||
%{"visitors" => 1, "name" => "/6"}
|
%{"visitors" => 1, "name" => "/6", "percentage" => 33.33}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -228,7 +228,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
conn = get(conn, "/api/stats/#{site.domain}/pages?period=day&filters=#{filters}")
|
conn = get(conn, "/api/stats/#{site.domain}/pages?period=day&filters=#{filters}")
|
||||||
|
|
||||||
assert json_response(conn, 200)["results"] == [
|
assert json_response(conn, 200)["results"] == [
|
||||||
%{"visitors" => 1, "name" => "/1"}
|
%{"visitors" => 1, "name" => "/1", "percentage" => 100.0}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -319,7 +319,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"pageviews" => 2,
|
"pageviews" => 2,
|
||||||
"bounce_rate" => 0,
|
"bounce_rate" => 0,
|
||||||
"time_on_page" => 315,
|
"time_on_page" => 315,
|
||||||
"scroll_depth" => 0
|
"scroll_depth" => 0,
|
||||||
|
"percentage" => 100.0
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"name" => "/blog/john-1",
|
"name" => "/blog/john-1",
|
||||||
|
|
@ -327,7 +328,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"pageviews" => 1,
|
"pageviews" => 1,
|
||||||
"bounce_rate" => 0,
|
"bounce_rate" => 0,
|
||||||
"time_on_page" => 60,
|
"time_on_page" => 60,
|
||||||
"scroll_depth" => 0
|
"scroll_depth" => 0,
|
||||||
|
"percentage" => 50.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -419,7 +421,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"pageviews" => 2,
|
"pageviews" => 2,
|
||||||
"bounce_rate" => 0,
|
"bounce_rate" => 0,
|
||||||
"time_on_page" => 120,
|
"time_on_page" => 120,
|
||||||
"scroll_depth" => 0
|
"scroll_depth" => 0,
|
||||||
|
"percentage" => 100.0
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"name" => "/blog/other-post",
|
"name" => "/blog/other-post",
|
||||||
|
|
@ -427,7 +430,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"pageviews" => 1,
|
"pageviews" => 1,
|
||||||
"bounce_rate" => 0,
|
"bounce_rate" => 0,
|
||||||
"time_on_page" => 30,
|
"time_on_page" => 30,
|
||||||
"scroll_depth" => 0
|
"scroll_depth" => 0,
|
||||||
|
"percentage" => 50.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -500,7 +504,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"pageviews" => 2,
|
"pageviews" => 2,
|
||||||
"bounce_rate" => 50,
|
"bounce_rate" => 50,
|
||||||
"time_on_page" => 45,
|
"time_on_page" => 45,
|
||||||
"scroll_depth" => 0
|
"scroll_depth" => 0,
|
||||||
|
"percentage" => 100.0
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"name" => "/blog/other-post",
|
"name" => "/blog/other-post",
|
||||||
|
|
@ -508,7 +513,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"pageviews" => 1,
|
"pageviews" => 1,
|
||||||
"bounce_rate" => 0,
|
"bounce_rate" => 0,
|
||||||
"time_on_page" => 30,
|
"time_on_page" => 30,
|
||||||
"scroll_depth" => 0
|
"scroll_depth" => 0,
|
||||||
|
"percentage" => 50.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -589,7 +595,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"pageviews" => 2,
|
"pageviews" => 2,
|
||||||
"bounce_rate" => 100,
|
"bounce_rate" => 100,
|
||||||
"time_on_page" => 30,
|
"time_on_page" => 30,
|
||||||
"scroll_depth" => 0
|
"scroll_depth" => 0,
|
||||||
|
"percentage" => 100.0
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"name" => "/blog/john-1",
|
"name" => "/blog/john-1",
|
||||||
|
|
@ -597,7 +604,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"pageviews" => 1,
|
"pageviews" => 1,
|
||||||
"bounce_rate" => 0,
|
"bounce_rate" => 0,
|
||||||
"time_on_page" => 60,
|
"time_on_page" => 60,
|
||||||
"scroll_depth" => 0
|
"scroll_depth" => 0,
|
||||||
|
"percentage" => 50.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -645,7 +653,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
assert json_response(conn, 200)["results"] == [
|
assert json_response(conn, 200)["results"] == [
|
||||||
%{
|
%{
|
||||||
"name" => "/firefox",
|
"name" => "/firefox",
|
||||||
"visitors" => 2
|
"visitors" => 2,
|
||||||
|
"percentage" => 100.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -685,7 +694,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
assert json_response(conn, 200)["results"] == [
|
assert json_response(conn, 200)["results"] == [
|
||||||
%{
|
%{
|
||||||
"name" => "/safari",
|
"name" => "/safari",
|
||||||
"visitors" => 1
|
"visitors" => 1,
|
||||||
|
"percentage" => 100.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -758,7 +768,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"pageviews" => 3,
|
"pageviews" => 3,
|
||||||
"bounce_rate" => 50,
|
"bounce_rate" => 50,
|
||||||
"time_on_page" => 90,
|
"time_on_page" => 90,
|
||||||
"scroll_depth" => 0
|
"scroll_depth" => 0,
|
||||||
|
"percentage" => 100.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -831,7 +842,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"pageviews" => 2,
|
"pageviews" => 2,
|
||||||
"bounce_rate" => 0,
|
"bounce_rate" => 0,
|
||||||
"time_on_page" => 60,
|
"time_on_page" => 60,
|
||||||
"scroll_depth" => 25
|
"scroll_depth" => 25,
|
||||||
|
"percentage" => 66.67
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"name" => "/blog",
|
"name" => "/blog",
|
||||||
|
|
@ -839,14 +851,16 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"pageviews" => 4,
|
"pageviews" => 4,
|
||||||
"bounce_rate" => 33,
|
"bounce_rate" => 33,
|
||||||
"time_on_page" => 80,
|
"time_on_page" => 80,
|
||||||
"scroll_depth" => 60
|
"scroll_depth" => 60,
|
||||||
|
"percentage" => 100.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "calculates scroll_depth from native and imported data combined", %{
|
test "calculates scroll_depth from native and imported data combined", %{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
site: site
|
site: site,
|
||||||
|
site_import: site_import
|
||||||
} do
|
} do
|
||||||
populate_stats(site, [
|
populate_stats(site, [
|
||||||
build(:pageview,
|
build(:pageview,
|
||||||
|
|
@ -873,6 +887,12 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
)
|
)
|
||||||
])
|
])
|
||||||
|
|
||||||
|
populate_stats(site, site_import.id, [
|
||||||
|
build(:imported_visitors, date: ~D[2020-01-01]),
|
||||||
|
build(:imported_visitors, date: ~D[2020-01-01]),
|
||||||
|
build(:imported_visitors, date: ~D[2020-01-01])
|
||||||
|
])
|
||||||
|
|
||||||
conn =
|
conn =
|
||||||
get(
|
get(
|
||||||
conn,
|
conn,
|
||||||
|
|
@ -886,14 +906,16 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"pageviews" => 4,
|
"pageviews" => 4,
|
||||||
"bounce_rate" => 100,
|
"bounce_rate" => 100,
|
||||||
"time_on_page" => 28,
|
"time_on_page" => 28,
|
||||||
"scroll_depth" => 50
|
"scroll_depth" => 50,
|
||||||
|
"percentage" => 100.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "handles missing scroll_depth data from native and imported sources", %{
|
test "handles missing scroll_depth data from native and imported sources", %{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
site: site
|
site: site,
|
||||||
|
site_import: site_import
|
||||||
} do
|
} do
|
||||||
populate_stats(site, [
|
populate_stats(site, [
|
||||||
build(:pageview,
|
build(:pageview,
|
||||||
|
|
@ -942,6 +964,12 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
)
|
)
|
||||||
])
|
])
|
||||||
|
|
||||||
|
populate_stats(
|
||||||
|
site,
|
||||||
|
site_import.id,
|
||||||
|
for(_ <- 1..24, do: build(:imported_visitors, date: ~D[2020-01-01]))
|
||||||
|
)
|
||||||
|
|
||||||
conn =
|
conn =
|
||||||
get(
|
get(
|
||||||
conn,
|
conn,
|
||||||
|
|
@ -955,7 +983,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"pageviews" => 5,
|
"pageviews" => 5,
|
||||||
"bounce_rate" => 0,
|
"bounce_rate" => 0,
|
||||||
"time_on_page" => 48,
|
"time_on_page" => 48,
|
||||||
"scroll_depth" => 50
|
"scroll_depth" => 50,
|
||||||
|
"percentage" => 20.0
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"name" => "/native-only",
|
"name" => "/native-only",
|
||||||
|
|
@ -963,7 +992,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"pageviews" => 1,
|
"pageviews" => 1,
|
||||||
"bounce_rate" => 0,
|
"bounce_rate" => 0,
|
||||||
"time_on_page" => 60,
|
"time_on_page" => 60,
|
||||||
"scroll_depth" => 40
|
"scroll_depth" => 40,
|
||||||
|
"percentage" => 4.0
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"name" => "/imported-only",
|
"name" => "/imported-only",
|
||||||
|
|
@ -971,7 +1001,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"pageviews" => 30,
|
"pageviews" => 30,
|
||||||
"bounce_rate" => 0,
|
"bounce_rate" => 0,
|
||||||
"time_on_page" => 30,
|
"time_on_page" => 30,
|
||||||
"scroll_depth" => 10
|
"scroll_depth" => 10,
|
||||||
|
"percentage" => 80.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -1017,7 +1048,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"pageviews" => 160,
|
"pageviews" => 160,
|
||||||
"bounce_rate" => 0,
|
"bounce_rate" => 0,
|
||||||
"time_on_page" => 60,
|
"time_on_page" => 60,
|
||||||
"scroll_depth" => 10
|
"scroll_depth" => 10,
|
||||||
|
"percentage" => nil
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -1097,7 +1129,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"pageviews" => 3,
|
"pageviews" => 3,
|
||||||
"bounce_rate" => 50,
|
"bounce_rate" => 50,
|
||||||
"time_on_page" => 75,
|
"time_on_page" => 75,
|
||||||
"scroll_depth" => 0
|
"scroll_depth" => 0,
|
||||||
|
"percentage" => 66.67
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"name" => "/about",
|
"name" => "/about",
|
||||||
|
|
@ -1105,7 +1138,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"pageviews" => 1,
|
"pageviews" => 1,
|
||||||
"bounce_rate" => 100,
|
"bounce_rate" => 100,
|
||||||
"time_on_page" => 30,
|
"time_on_page" => 30,
|
||||||
"scroll_depth" => 0
|
"scroll_depth" => 0,
|
||||||
|
"percentage" => 33.33
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -1185,7 +1219,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"pageviews" => 3,
|
"pageviews" => 3,
|
||||||
"bounce_rate" => 50,
|
"bounce_rate" => 50,
|
||||||
"time_on_page" => 75,
|
"time_on_page" => 75,
|
||||||
"scroll_depth" => 0
|
"scroll_depth" => 0,
|
||||||
|
"percentage" => 100.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -1265,7 +1300,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"pageviews" => 2,
|
"pageviews" => 2,
|
||||||
"bounce_rate" => 100,
|
"bounce_rate" => 100,
|
||||||
"time_on_page" => 30,
|
"time_on_page" => 30,
|
||||||
"scroll_depth" => 0
|
"scroll_depth" => 0,
|
||||||
|
"percentage" => 66.67
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"name" => "/blog/post-1",
|
"name" => "/blog/post-1",
|
||||||
|
|
@ -1273,7 +1309,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"pageviews" => 1,
|
"pageviews" => 1,
|
||||||
"bounce_rate" => 0,
|
"bounce_rate" => 0,
|
||||||
"time_on_page" => 60,
|
"time_on_page" => 60,
|
||||||
"scroll_depth" => 0
|
"scroll_depth" => 0,
|
||||||
|
"percentage" => 33.33
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"name" => "/blog/post-2",
|
"name" => "/blog/post-2",
|
||||||
|
|
@ -1281,7 +1318,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"pageviews" => 1,
|
"pageviews" => 1,
|
||||||
"bounce_rate" => 0,
|
"bounce_rate" => 0,
|
||||||
"time_on_page" => 30,
|
"time_on_page" => 30,
|
||||||
"scroll_depth" => 0
|
"scroll_depth" => 0,
|
||||||
|
"percentage" => 33.33
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -1339,7 +1377,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"pageviews" => 1,
|
"pageviews" => 1,
|
||||||
"bounce_rate" => 0,
|
"bounce_rate" => 0,
|
||||||
"time_on_page" => 60,
|
"time_on_page" => 60,
|
||||||
"scroll_depth" => 0
|
"scroll_depth" => 0,
|
||||||
|
"percentage" => 100.0
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"name" => "/blog/(/post-2",
|
"name" => "/blog/(/post-2",
|
||||||
|
|
@ -1347,7 +1386,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"pageviews" => 1,
|
"pageviews" => 1,
|
||||||
"bounce_rate" => 0,
|
"bounce_rate" => 0,
|
||||||
"time_on_page" => 30,
|
"time_on_page" => 30,
|
||||||
"scroll_depth" => 0
|
"scroll_depth" => 0,
|
||||||
|
"percentage" => 100.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -1427,7 +1467,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"pageviews" => 2,
|
"pageviews" => 2,
|
||||||
"bounce_rate" => 50,
|
"bounce_rate" => 50,
|
||||||
"time_on_page" => 600,
|
"time_on_page" => 600,
|
||||||
"scroll_depth" => 0
|
"scroll_depth" => 0,
|
||||||
|
"percentage" => 100.0
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"name" => "/about",
|
"name" => "/about",
|
||||||
|
|
@ -1435,7 +1476,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"pageviews" => 1,
|
"pageviews" => 1,
|
||||||
"bounce_rate" => 0,
|
"bounce_rate" => 0,
|
||||||
"time_on_page" => 30,
|
"time_on_page" => 30,
|
||||||
"scroll_depth" => 0
|
"scroll_depth" => 0,
|
||||||
|
"percentage" => 50.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -1455,17 +1497,17 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
conn1 = get(conn, "/api/stats/#{site.domain}/pages?period=day")
|
conn1 = get(conn, "/api/stats/#{site.domain}/pages?period=day")
|
||||||
|
|
||||||
assert json_response(conn1, 200)["results"] == [
|
assert json_response(conn1, 200)["results"] == [
|
||||||
%{"visitors" => 3, "name" => "/"},
|
%{"visitors" => 3, "name" => "/", "percentage" => 50.0},
|
||||||
%{"visitors" => 2, "name" => "/register"},
|
%{"visitors" => 2, "name" => "/register", "percentage" => 33.33},
|
||||||
%{"visitors" => 1, "name" => "/contact"}
|
%{"visitors" => 1, "name" => "/contact", "percentage" => 16.67}
|
||||||
]
|
]
|
||||||
|
|
||||||
conn2 = get(conn, "/api/stats/#{site.domain}/pages?period=day&with_imported=true")
|
conn2 = get(conn, "/api/stats/#{site.domain}/pages?period=day&with_imported=true")
|
||||||
|
|
||||||
assert json_response(conn2, 200)["results"] == [
|
assert json_response(conn2, 200)["results"] == [
|
||||||
%{"visitors" => 4, "name" => "/"},
|
%{"visitors" => 4, "name" => "/", "percentage" => 66.67},
|
||||||
%{"visitors" => 3, "name" => "/register"},
|
%{"visitors" => 3, "name" => "/register", "percentage" => 50.0},
|
||||||
%{"visitors" => 1, "name" => "/contact"}
|
%{"visitors" => 1, "name" => "/contact", "percentage" => 16.67}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -1559,7 +1601,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"pageviews" => 2,
|
"pageviews" => 2,
|
||||||
"name" => "/",
|
"name" => "/",
|
||||||
"scroll_depth" => 0
|
"scroll_depth" => 0,
|
||||||
|
"percentage" => 100.0
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"bounce_rate" => 0,
|
"bounce_rate" => 0,
|
||||||
|
|
@ -1567,7 +1610,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"pageviews" => 1,
|
"pageviews" => 1,
|
||||||
"name" => "/some-other-page",
|
"name" => "/some-other-page",
|
||||||
"scroll_depth" => 0
|
"scroll_depth" => 0,
|
||||||
|
"percentage" => 50.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -1608,7 +1652,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"pageviews" => 2,
|
"pageviews" => 2,
|
||||||
"time_on_page" => nil,
|
"time_on_page" => nil,
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"scroll_depth" => nil
|
"scroll_depth" => nil,
|
||||||
|
"percentage" => 100.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -1744,7 +1789,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"pageviews" => 3,
|
"pageviews" => 3,
|
||||||
"time_on_page" => 435,
|
"time_on_page" => 435,
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"scroll_depth" => 0
|
"scroll_depth" => 0,
|
||||||
|
"percentage" => 100.0
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"bounce_rate" => 0,
|
"bounce_rate" => 0,
|
||||||
|
|
@ -1752,14 +1798,16 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"pageviews" => 1,
|
"pageviews" => 1,
|
||||||
"time_on_page" => 120,
|
"time_on_page" => 120,
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"scroll_depth" => 0
|
"scroll_depth" => 0,
|
||||||
|
"percentage" => 50.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "calculates bounce rate and time on page for pages with imported data", %{
|
test "calculates bounce rate and time on page for pages with imported data", %{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
site: site
|
site: site,
|
||||||
|
site_import: site_import
|
||||||
} do
|
} do
|
||||||
populate_stats(site, [
|
populate_stats(site, [
|
||||||
build(:pageview,
|
build(:pageview,
|
||||||
|
|
@ -1815,6 +1863,12 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
)
|
)
|
||||||
])
|
])
|
||||||
|
|
||||||
|
populate_stats(site, site_import.id, [
|
||||||
|
build(:imported_visitors, date: ~D[2021-01-01]),
|
||||||
|
build(:imported_visitors, date: ~D[2021-01-01]),
|
||||||
|
build(:imported_visitors, date: ~D[2021-01-01])
|
||||||
|
])
|
||||||
|
|
||||||
conn =
|
conn =
|
||||||
get(
|
get(
|
||||||
conn,
|
conn,
|
||||||
|
|
@ -1828,7 +1882,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"visitors" => 3,
|
"visitors" => 3,
|
||||||
"pageviews" => 3,
|
"pageviews" => 3,
|
||||||
"scroll_depth" => 0,
|
"scroll_depth" => 0,
|
||||||
"name" => "/"
|
"name" => "/",
|
||||||
|
"percentage" => 60.0
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"bounce_rate" => 0,
|
"bounce_rate" => 0,
|
||||||
|
|
@ -1836,7 +1891,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"pageviews" => 2,
|
"pageviews" => 2,
|
||||||
"scroll_depth" => 0,
|
"scroll_depth" => 0,
|
||||||
"name" => "/some-other-page"
|
"name" => "/some-other-page",
|
||||||
|
"percentage" => 40.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -1851,8 +1907,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
conn = get(conn, "/api/stats/#{site.domain}/pages?period=realtime")
|
conn = get(conn, "/api/stats/#{site.domain}/pages?period=realtime")
|
||||||
|
|
||||||
assert json_response(conn, 200)["results"] == [
|
assert json_response(conn, 200)["results"] == [
|
||||||
%{"visitors" => 2, "name" => "/page1"},
|
%{"visitors" => 2, "name" => "/page1", "percentage" => 66.67},
|
||||||
%{"visitors" => 1, "name" => "/page2"}
|
%{"visitors" => 1, "name" => "/page2", "percentage" => 33.33}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -1926,7 +1982,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"pageviews" => 4,
|
"pageviews" => 4,
|
||||||
"time_on_page" => 90.0,
|
"time_on_page" => 90.0,
|
||||||
"visitors" => 4,
|
"visitors" => 4,
|
||||||
"scroll_depth" => 0
|
"scroll_depth" => 0,
|
||||||
|
"percentage" => 100.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -1989,7 +2046,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"pageviews" => 4,
|
"pageviews" => 4,
|
||||||
"time_on_page" => 90.0,
|
"time_on_page" => 90.0,
|
||||||
"visitors" => 4,
|
"visitors" => 4,
|
||||||
"scroll_depth" => 0
|
"scroll_depth" => 0,
|
||||||
|
"percentage" => 80.0
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"bounce_rate" => 100,
|
"bounce_rate" => 100,
|
||||||
|
|
@ -1997,7 +2055,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"pageviews" => 1,
|
"pageviews" => 1,
|
||||||
"time_on_page" => 10.0,
|
"time_on_page" => 10.0,
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"scroll_depth" => nil
|
"scroll_depth" => nil,
|
||||||
|
"percentage" => 20.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -2055,20 +2114,22 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
|
|
||||||
assert json_response(conn, 200)["results"] == [
|
assert json_response(conn, 200)["results"] == [
|
||||||
%{
|
%{
|
||||||
"bounce_rate" => 50,
|
"bounce_rate" => 50.0,
|
||||||
"name" => "/aaa",
|
"name" => "/aaa",
|
||||||
"pageviews" => 4,
|
"pageviews" => 4,
|
||||||
"time_on_page" => 90.0,
|
"time_on_page" => 90,
|
||||||
"visitors" => 4,
|
"visitors" => 4,
|
||||||
"scroll_depth" => 0
|
"scroll_depth" => 0,
|
||||||
|
"percentage" => 80.0
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"bounce_rate" => 100,
|
"bounce_rate" => 100.0,
|
||||||
"name" => "/a",
|
"name" => "/a",
|
||||||
"pageviews" => 1,
|
"pageviews" => 1,
|
||||||
"time_on_page" => 10.0,
|
"time_on_page" => 10,
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"scroll_depth" => nil
|
"scroll_depth" => nil,
|
||||||
|
"percentage" => 20.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -2103,24 +2164,27 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
%{
|
%{
|
||||||
"bounce_rate" => 100,
|
"bounce_rate" => 100,
|
||||||
"comparison" => %{
|
"comparison" => %{
|
||||||
"bounce_rate" => 0,
|
"bounce_rate" => 0.0,
|
||||||
"pageviews" => 0,
|
"pageviews" => 0,
|
||||||
"time_on_page" => nil,
|
"time_on_page" => nil,
|
||||||
"visitors" => 0,
|
"visitors" => 0,
|
||||||
"scroll_depth" => nil,
|
"scroll_depth" => nil,
|
||||||
|
"percentage" => 0.0,
|
||||||
"change" => %{
|
"change" => %{
|
||||||
"bounce_rate" => nil,
|
"bounce_rate" => nil,
|
||||||
"pageviews" => 100,
|
"pageviews" => 100,
|
||||||
"time_on_page" => nil,
|
"time_on_page" => nil,
|
||||||
"visitors" => 100,
|
"visitors" => 100,
|
||||||
"scroll_depth" => nil
|
"scroll_depth" => nil,
|
||||||
|
"percentage" => 100
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name" => "/page2",
|
"name" => "/page2",
|
||||||
"pageviews" => 2,
|
"pageviews" => 2,
|
||||||
"time_on_page" => nil,
|
"time_on_page" => nil,
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"scroll_depth" => nil
|
"scroll_depth" => nil,
|
||||||
|
"percentage" => 66.67
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"bounce_rate" => 100,
|
"bounce_rate" => 100,
|
||||||
|
|
@ -2129,18 +2193,21 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"time_on_page" => nil,
|
"time_on_page" => nil,
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"scroll_depth" => nil,
|
"scroll_depth" => nil,
|
||||||
|
"percentage" => 33.33,
|
||||||
"comparison" => %{
|
"comparison" => %{
|
||||||
"bounce_rate" => 100,
|
"bounce_rate" => 100,
|
||||||
"pageviews" => 1,
|
"pageviews" => 1,
|
||||||
"time_on_page" => nil,
|
"time_on_page" => nil,
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"scroll_depth" => nil,
|
"scroll_depth" => nil,
|
||||||
|
"percentage" => 100.0,
|
||||||
"change" => %{
|
"change" => %{
|
||||||
"bounce_rate" => 0,
|
"bounce_rate" => 0,
|
||||||
"pageviews" => 0,
|
"pageviews" => 0,
|
||||||
"time_on_page" => nil,
|
"time_on_page" => nil,
|
||||||
"visitors" => 0,
|
"visitors" => 0,
|
||||||
"scroll_depth" => nil
|
"scroll_depth" => nil,
|
||||||
|
"percentage" => -67
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2177,7 +2244,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"pageviews" => 3,
|
"pageviews" => 3,
|
||||||
"time_on_page" => nil,
|
"time_on_page" => nil,
|
||||||
"visitors" => 3,
|
"visitors" => 3,
|
||||||
"scroll_depth" => nil
|
"scroll_depth" => nil,
|
||||||
|
"percentage" => 50.0
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"bounce_rate" => 100,
|
"bounce_rate" => 100,
|
||||||
|
|
@ -2185,7 +2253,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"pageviews" => 2,
|
"pageviews" => 2,
|
||||||
"time_on_page" => nil,
|
"time_on_page" => nil,
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"scroll_depth" => nil
|
"scroll_depth" => nil,
|
||||||
|
"percentage" => 33.33
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"bounce_rate" => 100,
|
"bounce_rate" => 100,
|
||||||
|
|
@ -2193,7 +2262,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"pageviews" => 1,
|
"pageviews" => 1,
|
||||||
"time_on_page" => nil,
|
"time_on_page" => nil,
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"scroll_depth" => nil
|
"scroll_depth" => nil,
|
||||||
|
"percentage" => 16.67
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -2241,14 +2311,16 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"visits" => 2,
|
"visits" => 2,
|
||||||
"name" => "/page1",
|
"name" => "/page1",
|
||||||
"visit_duration" => 0,
|
"visit_duration" => 0,
|
||||||
"bounce_rate" => 100
|
"bounce_rate" => 100,
|
||||||
|
"percentage" => 66.67
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"visits" => 2,
|
"visits" => 2,
|
||||||
"name" => "/page2",
|
"name" => "/page2",
|
||||||
"visit_duration" => 450,
|
"visit_duration" => 450,
|
||||||
"bounce_rate" => 50
|
"bounce_rate" => 50,
|
||||||
|
"percentage" => 33.33
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -2299,14 +2371,16 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"visits" => 1,
|
"visits" => 1,
|
||||||
"name" => "/blog",
|
"name" => "/blog",
|
||||||
"visit_duration" => 60,
|
"visit_duration" => 60,
|
||||||
"bounce_rate" => 0
|
"bounce_rate" => 0,
|
||||||
|
"percentage" => 50.0
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"visits" => 1,
|
"visits" => 1,
|
||||||
"name" => "/blog/john-2",
|
"name" => "/blog/john-2",
|
||||||
"visit_duration" => 0,
|
"visit_duration" => 0,
|
||||||
"bounce_rate" => 100
|
"bounce_rate" => 100,
|
||||||
|
"percentage" => 50.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -2356,14 +2430,16 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"visits" => 2,
|
"visits" => 2,
|
||||||
"name" => "/page1",
|
"name" => "/page1",
|
||||||
"visit_duration" => 0,
|
"visit_duration" => 0,
|
||||||
"bounce_rate" => 100
|
"bounce_rate" => 100,
|
||||||
|
"percentage" => 66.67
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"visits" => 2,
|
"visits" => 2,
|
||||||
"name" => "/page2",
|
"name" => "/page2",
|
||||||
"visit_duration" => 450,
|
"visit_duration" => 450,
|
||||||
"bounce_rate" => 50
|
"bounce_rate" => 50,
|
||||||
|
"percentage" => 33.33
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -2379,14 +2455,16 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"visits" => 5,
|
"visits" => 5,
|
||||||
"name" => "/page2",
|
"name" => "/page2",
|
||||||
"visit_duration" => 240.0,
|
"visit_duration" => 240.0,
|
||||||
"bounce_rate" => 20
|
"bounce_rate" => 20.0,
|
||||||
|
"percentage" => 60.0
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"visits" => 2,
|
"visits" => 2,
|
||||||
"name" => "/page1",
|
"name" => "/page1",
|
||||||
"visit_duration" => 0,
|
"visit_duration" => 0.0,
|
||||||
"bounce_rate" => 100
|
"bounce_rate" => 100.0,
|
||||||
|
"percentage" => 40.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -2444,14 +2522,16 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"visit_duration" => 0,
|
"visit_duration" => 0,
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"visits" => 1,
|
"visits" => 1,
|
||||||
"bounce_rate" => 100
|
"bounce_rate" => 100,
|
||||||
|
"percentage" => 50.0
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"name" => "/page2",
|
"name" => "/page2",
|
||||||
"visit_duration" => 0,
|
"visit_duration" => 0,
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"visits" => 1,
|
"visits" => 1,
|
||||||
"bounce_rate" => 100
|
"bounce_rate" => 100,
|
||||||
|
"percentage" => 50.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -2610,21 +2690,24 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"name" => "/a",
|
"name" => "/a",
|
||||||
"visits" => 10,
|
"visits" => 10,
|
||||||
"visitors" => 6,
|
"visitors" => 6,
|
||||||
"bounce_rate" => 10
|
"bounce_rate" => 10.0,
|
||||||
|
"percentage" => 66.67
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"visit_duration" => 50.0,
|
"visit_duration" => 50.0,
|
||||||
"name" => "/bbb",
|
"name" => "/bbb",
|
||||||
"visits" => 2,
|
"visits" => 2,
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"bounce_rate" => 0
|
"bounce_rate" => 0.0,
|
||||||
|
"percentage" => 22.22
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"visit_duration" => 0,
|
"visit_duration" => 0,
|
||||||
"name" => "/aaa",
|
"name" => "/aaa",
|
||||||
"visits" => 1,
|
"visits" => 1,
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"bounce_rate" => 100
|
"bounce_rate" => 100.0,
|
||||||
|
"percentage" => 11.11
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -2658,8 +2741,20 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
conn = get(conn, "/api/stats/#{site.domain}/exit-pages?period=day&date=2021-01-01")
|
conn = get(conn, "/api/stats/#{site.domain}/exit-pages?period=day&date=2021-01-01")
|
||||||
|
|
||||||
assert json_response(conn, 200)["results"] == [
|
assert json_response(conn, 200)["results"] == [
|
||||||
%{"name" => "/page1", "visitors" => 2, "visits" => 2, "exit_rate" => 66.7},
|
%{
|
||||||
%{"name" => "/page2", "visitors" => 1, "visits" => 1, "exit_rate" => 100}
|
"name" => "/page1",
|
||||||
|
"visitors" => 2,
|
||||||
|
"visits" => 2,
|
||||||
|
"exit_rate" => 66.7,
|
||||||
|
"percentage" => 66.67
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
"name" => "/page2",
|
||||||
|
"visitors" => 1,
|
||||||
|
"visits" => 1,
|
||||||
|
"exit_rate" => 100,
|
||||||
|
"percentage" => 33.33
|
||||||
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -2692,8 +2787,20 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
)
|
)
|
||||||
|
|
||||||
assert json_response(conn, 200)["results"] == [
|
assert json_response(conn, 200)["results"] == [
|
||||||
%{"name" => "/page2", "visitors" => 1, "visits" => 1, "exit_rate" => 100},
|
%{
|
||||||
%{"name" => "/page1", "visitors" => 2, "visits" => 2, "exit_rate" => 66.7}
|
"name" => "/page2",
|
||||||
|
"visitors" => 1,
|
||||||
|
"visits" => 1,
|
||||||
|
"exit_rate" => 100.0,
|
||||||
|
"percentage" => 33.33
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
"name" => "/page1",
|
||||||
|
"visitors" => 2,
|
||||||
|
"visits" => 2,
|
||||||
|
"exit_rate" => 66.7,
|
||||||
|
"percentage" => 66.67
|
||||||
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -2740,7 +2847,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
|
|
||||||
# We're going to only join sessions where the entry hostname matches the filter
|
# We're going to only join sessions where the entry hostname matches the filter
|
||||||
assert json_response(conn, 200)["results"] ==
|
assert json_response(conn, 200)["results"] ==
|
||||||
[%{"name" => "/page1", "visitors" => 1, "visits" => 1}]
|
[%{"name" => "/page1", "visitors" => 1, "visits" => 1, "percentage" => 100.0}]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns top exit pages filtered by custom pageview props", %{conn: conn, site: site} do
|
test "returns top exit pages filtered by custom pageview props", %{conn: conn, site: site} do
|
||||||
|
|
@ -2778,7 +2885,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
)
|
)
|
||||||
|
|
||||||
assert json_response(conn, 200)["results"] == [
|
assert json_response(conn, 200)["results"] == [
|
||||||
%{"name" => "/", "visitors" => 1, "visits" => 1}
|
%{"name" => "/", "visitors" => 1, "visits" => 1, "percentage" => 100.0}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -2822,8 +2929,20 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
conn1 = get(conn, "/api/stats/#{site.domain}/exit-pages?period=day&date=2021-01-01")
|
conn1 = get(conn, "/api/stats/#{site.domain}/exit-pages?period=day&date=2021-01-01")
|
||||||
|
|
||||||
assert json_response(conn1, 200)["results"] == [
|
assert json_response(conn1, 200)["results"] == [
|
||||||
%{"name" => "/page1", "visitors" => 2, "visits" => 2, "exit_rate" => 66.7},
|
%{
|
||||||
%{"name" => "/page2", "visitors" => 1, "visits" => 1, "exit_rate" => 100}
|
"name" => "/page1",
|
||||||
|
"visitors" => 2,
|
||||||
|
"visits" => 2,
|
||||||
|
"exit_rate" => 66.7,
|
||||||
|
"percentage" => 66.67
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
"name" => "/page2",
|
||||||
|
"visitors" => 1,
|
||||||
|
"visits" => 1,
|
||||||
|
"exit_rate" => 100.0,
|
||||||
|
"percentage" => 33.33
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
conn2 =
|
conn2 =
|
||||||
|
|
@ -2837,9 +2956,16 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"name" => "/page2",
|
"name" => "/page2",
|
||||||
"visitors" => 3,
|
"visitors" => 3,
|
||||||
"visits" => 4,
|
"visits" => 4,
|
||||||
"exit_rate" => 80.0
|
"exit_rate" => 80.0,
|
||||||
|
"percentage" => 60.0
|
||||||
},
|
},
|
||||||
%{"name" => "/page1", "visitors" => 2, "visits" => 2, "exit_rate" => 66.7}
|
%{
|
||||||
|
"name" => "/page1",
|
||||||
|
"visitors" => 2,
|
||||||
|
"visits" => 2,
|
||||||
|
"exit_rate" => 66.7,
|
||||||
|
"percentage" => 40.0
|
||||||
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -2938,8 +3064,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
)
|
)
|
||||||
|
|
||||||
assert json_response(conn, 200)["results"] == [
|
assert json_response(conn, 200)["results"] == [
|
||||||
%{"name" => "/exit1", "visitors" => 1, "visits" => 1},
|
%{"name" => "/exit1", "visitors" => 1, "visits" => 1, "percentage" => 50.0},
|
||||||
%{"name" => "/exit2", "visitors" => 1, "visits" => 1}
|
%{"name" => "/exit2", "visitors" => 1, "visits" => 1, "percentage" => 50.0}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -2996,19 +3122,22 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||||
"exit_rate" => 50.0,
|
"exit_rate" => 50.0,
|
||||||
"name" => "/a",
|
"name" => "/a",
|
||||||
"visits" => 10,
|
"visits" => 10,
|
||||||
"visitors" => 6
|
"visitors" => 6,
|
||||||
|
"percentage" => 66.67
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"exit_rate" => 100.0,
|
"exit_rate" => 100.0,
|
||||||
"name" => "/bbb",
|
"name" => "/bbb",
|
||||||
"visits" => 2,
|
"visits" => 2,
|
||||||
"visitors" => 2
|
"visitors" => 2,
|
||||||
|
"percentage" => 22.22
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"exit_rate" => 100.0,
|
"exit_rate" => 100.0,
|
||||||
"name" => "/aaa",
|
"name" => "/aaa",
|
||||||
"visits" => 1,
|
"visits" => 1,
|
||||||
"visitors" => 1
|
"visitors" => 1,
|
||||||
|
"percentage" => 11.11
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -38,8 +38,20 @@ defmodule PlausibleWeb.Api.StatsController.RegionsTest do
|
||||||
conn = get(conn, "/api/stats/#{site.domain}/regions?period=day")
|
conn = get(conn, "/api/stats/#{site.domain}/regions?period=day")
|
||||||
|
|
||||||
assert json_response(conn, 200)["results"] == [
|
assert json_response(conn, 200)["results"] == [
|
||||||
%{"code" => "EE-37", "country_flag" => "🇪🇪", "name" => "Harjumaa", "visitors" => 3},
|
%{
|
||||||
%{"code" => "EE-39", "country_flag" => "🇪🇪", "name" => "Hiiumaa", "visitors" => 2}
|
"code" => "EE-37",
|
||||||
|
"country_flag" => "🇪🇪",
|
||||||
|
"name" => "Harjumaa",
|
||||||
|
"visitors" => 3,
|
||||||
|
"percentage" => 60.0
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
"code" => "EE-39",
|
||||||
|
"country_flag" => "🇪🇪",
|
||||||
|
"name" => "Hiiumaa",
|
||||||
|
"visitors" => 2,
|
||||||
|
"percentage" => 40.0
|
||||||
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -48,7 +60,13 @@ defmodule PlausibleWeb.Api.StatsController.RegionsTest do
|
||||||
conn = get(conn, "/api/stats/#{site.domain}/regions?period=day&filters=#{filters}")
|
conn = get(conn, "/api/stats/#{site.domain}/regions?period=day&filters=#{filters}")
|
||||||
|
|
||||||
assert json_response(conn, 200)["results"] == [
|
assert json_response(conn, 200)["results"] == [
|
||||||
%{"code" => "EE-39", "country_flag" => "🇪🇪", "name" => "Hiiumaa", "visitors" => 2}
|
%{
|
||||||
|
"code" => "EE-39",
|
||||||
|
"country_flag" => "🇪🇪",
|
||||||
|
"name" => "Hiiumaa",
|
||||||
|
"visitors" => 2,
|
||||||
|
"percentage" => 100.0
|
||||||
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ defmodule PlausibleWeb.Api.StatsController.ScreenSizesTest do
|
||||||
conn = get(conn, "/api/stats/#{site.domain}/screen-sizes?period=day")
|
conn = get(conn, "/api/stats/#{site.domain}/screen-sizes?period=day")
|
||||||
|
|
||||||
assert json_response(conn, 200)["results"] == [
|
assert json_response(conn, 200)["results"] == [
|
||||||
%{"name" => "Desktop", "visitors" => 2, "percentage" => 66.7},
|
%{"name" => "Desktop", "visitors" => 2, "percentage" => 66.67},
|
||||||
%{"name" => "Laptop", "visitors" => 1, "percentage" => 33.3}
|
%{"name" => "Laptop", "visitors" => 1, "percentage" => 33.33}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -47,14 +47,14 @@ defmodule PlausibleWeb.Api.StatsController.ScreenSizesTest do
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"bounce_rate" => 50,
|
"bounce_rate" => 50,
|
||||||
"visit_duration" => 300,
|
"visit_duration" => 300,
|
||||||
"percentage" => 66.7
|
"percentage" => 66.67
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"name" => "Laptop",
|
"name" => "Laptop",
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"bounce_rate" => 100,
|
"bounce_rate" => 100,
|
||||||
"visit_duration" => 0,
|
"visit_duration" => 0,
|
||||||
"percentage" => 33.3
|
"percentage" => 33.33
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -212,8 +212,8 @@ defmodule PlausibleWeb.Api.StatsController.ScreenSizesTest do
|
||||||
conn1 = get(conn, "/api/stats/#{site.domain}/screen-sizes?period=day")
|
conn1 = get(conn, "/api/stats/#{site.domain}/screen-sizes?period=day")
|
||||||
|
|
||||||
assert json_response(conn1, 200)["results"] == [
|
assert json_response(conn1, 200)["results"] == [
|
||||||
%{"name" => "Desktop", "visitors" => 2, "percentage" => 66.7},
|
%{"name" => "Desktop", "visitors" => 2, "percentage" => 66.67},
|
||||||
%{"name" => "Laptop", "visitors" => 1, "percentage" => 33.3}
|
%{"name" => "Laptop", "visitors" => 1, "percentage" => 33.33}
|
||||||
]
|
]
|
||||||
|
|
||||||
conn2 = get(conn, "/api/stats/#{site.domain}/screen-sizes?period=day&with_imported=true")
|
conn2 = get(conn, "/api/stats/#{site.domain}/screen-sizes?period=day&with_imported=true")
|
||||||
|
|
@ -320,8 +320,8 @@ defmodule PlausibleWeb.Api.StatsController.ScreenSizesTest do
|
||||||
conn = get(conn, "/api/stats/#{site.domain}/screen-sizes?period=day&filters=#{filters}")
|
conn = get(conn, "/api/stats/#{site.domain}/screen-sizes?period=day&filters=#{filters}")
|
||||||
|
|
||||||
assert json_response(conn, 200)["results"] == [
|
assert json_response(conn, 200)["results"] == [
|
||||||
%{"name" => "Desktop", "visitors" => 2, "percentage" => 66.7},
|
%{"name" => "Desktop", "visitors" => 2, "percentage" => 66.67},
|
||||||
%{"name" => "Mobile", "visitors" => 1, "percentage" => 33.3}
|
%{"name" => "Mobile", "visitors" => 1, "percentage" => 33.33}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,9 +34,9 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
conn = get(conn, "/api/stats/#{site.domain}/sources?period=day")
|
conn = get(conn, "/api/stats/#{site.domain}/sources?period=day")
|
||||||
|
|
||||||
assert json_response(conn, 200)["results"] == [
|
assert json_response(conn, 200)["results"] == [
|
||||||
%{"name" => "Google", "visitors" => 3},
|
%{"name" => "Google", "visitors" => 3, "percentage" => 50.0},
|
||||||
%{"name" => "DuckDuckGo", "visitors" => 2},
|
%{"name" => "DuckDuckGo", "visitors" => 2, "percentage" => 33.33},
|
||||||
%{"name" => "Direct / None", "visitors" => 1}
|
%{"name" => "Direct / None", "visitors" => 1, "percentage" => 16.67}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -84,8 +84,8 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
)
|
)
|
||||||
|
|
||||||
assert json_response(conn, 200)["results"] == [
|
assert json_response(conn, 200)["results"] == [
|
||||||
%{"name" => "Google", "visitors" => 2},
|
%{"name" => "Google", "visitors" => 2, "percentage" => 66.67},
|
||||||
%{"name" => "DuckDuckGo", "visitors" => 1}
|
%{"name" => "DuckDuckGo", "visitors" => 1, "percentage" => 33.33}
|
||||||
]
|
]
|
||||||
|
|
||||||
assert json_response(conn, 200)["meta"] == %{
|
assert json_response(conn, 200)["meta"] == %{
|
||||||
|
|
@ -142,8 +142,8 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
)
|
)
|
||||||
|
|
||||||
assert json_response(conn, 200)["results"] == [
|
assert json_response(conn, 200)["results"] == [
|
||||||
%{"name" => "Google", "visitors" => 2},
|
%{"name" => "Google", "visitors" => 2, "percentage" => 66.67},
|
||||||
%{"name" => "DuckDuckGo", "visitors" => 1}
|
%{"name" => "DuckDuckGo", "visitors" => 1, "percentage" => 33.33}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -192,8 +192,8 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
)
|
)
|
||||||
|
|
||||||
assert json_response(conn, 200)["results"] == [
|
assert json_response(conn, 200)["results"] == [
|
||||||
%{"name" => "Facebook", "visitors" => 2},
|
%{"name" => "Facebook", "visitors" => 2, "percentage" => 66.67},
|
||||||
%{"name" => "DuckDuckGo", "visitors" => 1}
|
%{"name" => "DuckDuckGo", "visitors" => 1, "percentage" => 33.33}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -246,8 +246,8 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
)
|
)
|
||||||
|
|
||||||
assert json_response(conn, 200)["results"] == [
|
assert json_response(conn, 200)["results"] == [
|
||||||
%{"name" => "Google", "visitors" => 2},
|
%{"name" => "Google", "visitors" => 2, "percentage" => 66.67},
|
||||||
%{"name" => "DuckDuckGo", "visitors" => 1}
|
%{"name" => "DuckDuckGo", "visitors" => 1, "percentage" => 33.33}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -262,6 +262,9 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
])
|
])
|
||||||
|
|
||||||
populate_stats(site, [
|
populate_stats(site, [
|
||||||
|
build(:imported_visitors),
|
||||||
|
build(:imported_visitors),
|
||||||
|
build(:imported_visitors),
|
||||||
build(:imported_sources,
|
build(:imported_sources,
|
||||||
source: "Google",
|
source: "Google",
|
||||||
visitors: 2
|
visitors: 2
|
||||||
|
|
@ -275,15 +278,15 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
conn1 = get(conn, "/api/stats/#{site.domain}/sources?period=day")
|
conn1 = get(conn, "/api/stats/#{site.domain}/sources?period=day")
|
||||||
|
|
||||||
assert json_response(conn1, 200)["results"] == [
|
assert json_response(conn1, 200)["results"] == [
|
||||||
%{"name" => "Google", "visitors" => 2},
|
%{"name" => "Google", "visitors" => 2, "percentage" => 66.67},
|
||||||
%{"name" => "DuckDuckGo", "visitors" => 1}
|
%{"name" => "DuckDuckGo", "visitors" => 1, "percentage" => 33.33}
|
||||||
]
|
]
|
||||||
|
|
||||||
conn2 = get(conn, "/api/stats/#{site.domain}/sources?period=day&with_imported=true")
|
conn2 = get(conn, "/api/stats/#{site.domain}/sources?period=day&with_imported=true")
|
||||||
|
|
||||||
assert json_response(conn2, 200)["results"] == [
|
assert json_response(conn2, 200)["results"] == [
|
||||||
%{"name" => "Google", "visitors" => 4},
|
%{"name" => "Google", "visitors" => 4, "percentage" => 66.67},
|
||||||
%{"name" => "DuckDuckGo", "visitors" => 2}
|
%{"name" => "DuckDuckGo", "visitors" => 2, "percentage" => 33.33}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -319,13 +322,15 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
"name" => "DuckDuckGo",
|
"name" => "DuckDuckGo",
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"bounce_rate" => 100,
|
"bounce_rate" => 100,
|
||||||
"visit_duration" => 0
|
"visit_duration" => 0,
|
||||||
|
"percentage" => 50.0
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"name" => "Google",
|
"name" => "Google",
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"bounce_rate" => 0,
|
"bounce_rate" => 0,
|
||||||
"visit_duration" => 900
|
"visit_duration" => 900,
|
||||||
|
"percentage" => 50.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -355,6 +360,9 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
])
|
])
|
||||||
|
|
||||||
populate_stats(site, [
|
populate_stats(site, [
|
||||||
|
build(:imported_visitors, date: ~D[2021-01-01]),
|
||||||
|
build(:imported_visitors, date: ~D[2021-01-01]),
|
||||||
|
build(:imported_visitors, date: ~D[2021-01-01]),
|
||||||
build(:imported_sources,
|
build(:imported_sources,
|
||||||
source: "Google",
|
source: "Google",
|
||||||
date: ~D[2021-01-01],
|
date: ~D[2021-01-01],
|
||||||
|
|
@ -384,13 +392,15 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
"name" => "DuckDuckGo",
|
"name" => "DuckDuckGo",
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"bounce_rate" => 100,
|
"bounce_rate" => 100,
|
||||||
"visit_duration" => 0
|
"visit_duration" => 0,
|
||||||
|
"percentage" => 50.0
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"name" => "Google",
|
"name" => "Google",
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"bounce_rate" => 0,
|
"bounce_rate" => 0,
|
||||||
"visit_duration" => 900
|
"visit_duration" => 900,
|
||||||
|
"percentage" => 50.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -404,14 +414,16 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
%{
|
%{
|
||||||
"name" => "Google",
|
"name" => "Google",
|
||||||
"visitors" => 3,
|
"visitors" => 3,
|
||||||
"bounce_rate" => 25,
|
"bounce_rate" => 25.0,
|
||||||
"visit_duration" => 450.0
|
"visit_duration" => 450.0,
|
||||||
|
"percentage" => 60.0
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"name" => "DuckDuckGo",
|
"name" => "DuckDuckGo",
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"bounce_rate" => 50,
|
"bounce_rate" => 50.0,
|
||||||
"visit_duration" => 50
|
"visit_duration" => 50.0,
|
||||||
|
"percentage" => 40.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -438,8 +450,8 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
conn = get(conn, "/api/stats/#{site.domain}/sources?period=realtime")
|
conn = get(conn, "/api/stats/#{site.domain}/sources?period=realtime")
|
||||||
|
|
||||||
assert json_response(conn, 200)["results"] == [
|
assert json_response(conn, 200)["results"] == [
|
||||||
%{"name" => "Google", "visitors" => 2},
|
%{"name" => "Google", "visitors" => 2, "percentage" => 66.67},
|
||||||
%{"name" => "DuckDuckGo", "visitors" => 1}
|
%{"name" => "DuckDuckGo", "visitors" => 1, "percentage" => 33.33}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -468,7 +480,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
conn1 = get(conn, "/api/stats/#{site.domain}/sources?period=day&limit=1&page=2")
|
conn1 = get(conn, "/api/stats/#{site.domain}/sources?period=day&limit=1&page=2")
|
||||||
|
|
||||||
assert json_response(conn1, 200)["results"] == [
|
assert json_response(conn1, 200)["results"] == [
|
||||||
%{"name" => "DuckDuckGo", "visitors" => 1}
|
%{"name" => "DuckDuckGo", "visitors" => 1, "percentage" => 33.33}
|
||||||
]
|
]
|
||||||
|
|
||||||
conn2 =
|
conn2 =
|
||||||
|
|
@ -478,7 +490,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
)
|
)
|
||||||
|
|
||||||
assert json_response(conn2, 200)["results"] == [
|
assert json_response(conn2, 200)["results"] == [
|
||||||
%{"name" => "Google", "visitors" => 2}
|
%{"name" => "Google", "visitors" => 2, "percentage" => 66.67}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -502,8 +514,8 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
conn = get(conn, "/api/stats/#{site.domain}/sources?period=day&filters=#{filters}")
|
conn = get(conn, "/api/stats/#{site.domain}/sources?period=day&filters=#{filters}")
|
||||||
|
|
||||||
assert json_response(conn, 200)["results"] == [
|
assert json_response(conn, 200)["results"] == [
|
||||||
%{"name" => "Google", "visitors" => 2},
|
%{"name" => "Google", "visitors" => 2, "percentage" => 66.67},
|
||||||
%{"name" => "DuckDuckGo", "visitors" => 1}
|
%{"name" => "DuckDuckGo", "visitors" => 1, "percentage" => 33.33}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -527,8 +539,8 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
conn = get(conn, "/api/stats/#{site.domain}/sources?period=day&filters=#{filters}")
|
conn = get(conn, "/api/stats/#{site.domain}/sources?period=day&filters=#{filters}")
|
||||||
|
|
||||||
assert json_response(conn, 200)["results"] == [
|
assert json_response(conn, 200)["results"] == [
|
||||||
%{"name" => "Google", "visitors" => 2},
|
%{"name" => "Google", "visitors" => 2, "percentage" => 66.67},
|
||||||
%{"name" => "DuckDuckGo", "visitors" => 1}
|
%{"name" => "DuckDuckGo", "visitors" => 1, "percentage" => 33.33}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -543,9 +555,9 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
conn = get(conn, "/api/stats/#{site.domain}/sources?order_by=#{order_by}&period=day")
|
conn = get(conn, "/api/stats/#{site.domain}/sources?order_by=#{order_by}&period=day")
|
||||||
|
|
||||||
assert json_response(conn, 200)["results"] == [
|
assert json_response(conn, 200)["results"] == [
|
||||||
%{"name" => "C", "visitors" => 1},
|
%{"name" => "C", "visitors" => 1, "percentage" => 33.33},
|
||||||
%{"name" => "B", "visitors" => 1},
|
%{"name" => "B", "visitors" => 1, "percentage" => 33.33},
|
||||||
%{"name" => "A", "visitors" => 1}
|
%{"name" => "A", "visitors" => 1, "percentage" => 33.33}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -593,10 +605,34 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
)
|
)
|
||||||
|
|
||||||
assert json_response(conn1, 200)["results"] == [
|
assert json_response(conn1, 200)["results"] == [
|
||||||
%{"name" => "Z", "visitors" => 1, "bounce_rate" => 100, "visit_duration" => 0},
|
%{
|
||||||
%{"name" => "A", "visitors" => 2, "bounce_rate" => 100, "visit_duration" => 0},
|
"name" => "Z",
|
||||||
%{"name" => "C", "visitors" => 1, "bounce_rate" => 0, "visit_duration" => 30},
|
"visitors" => 1,
|
||||||
%{"name" => "B", "visitors" => 1, "bounce_rate" => 0, "visit_duration" => 45}
|
"bounce_rate" => 100,
|
||||||
|
"visit_duration" => 0,
|
||||||
|
"percentage" => 25.0
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
"name" => "A",
|
||||||
|
"visitors" => 2,
|
||||||
|
"bounce_rate" => 100,
|
||||||
|
"visit_duration" => 0,
|
||||||
|
"percentage" => 50.0
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
"name" => "C",
|
||||||
|
"visitors" => 1,
|
||||||
|
"bounce_rate" => 0,
|
||||||
|
"visit_duration" => 30,
|
||||||
|
"percentage" => 25.0
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
"name" => "B",
|
||||||
|
"visitors" => 1,
|
||||||
|
"bounce_rate" => 0,
|
||||||
|
"visit_duration" => 45,
|
||||||
|
"percentage" => 25.0
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
order_by_flipped = Jason.encode!([["visit_duration", "desc"], ["visit:source", "asc"]])
|
order_by_flipped = Jason.encode!([["visit_duration", "desc"], ["visit:source", "asc"]])
|
||||||
|
|
@ -608,10 +644,34 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
)
|
)
|
||||||
|
|
||||||
assert json_response(conn2, 200)["results"] == [
|
assert json_response(conn2, 200)["results"] == [
|
||||||
%{"name" => "B", "visitors" => 1, "bounce_rate" => 0, "visit_duration" => 45},
|
%{
|
||||||
%{"name" => "C", "visitors" => 1, "bounce_rate" => 0, "visit_duration" => 30},
|
"name" => "B",
|
||||||
%{"name" => "A", "visitors" => 2, "bounce_rate" => 100, "visit_duration" => 0},
|
"visitors" => 1,
|
||||||
%{"name" => "Z", "visitors" => 1, "bounce_rate" => 100, "visit_duration" => 0}
|
"bounce_rate" => 0,
|
||||||
|
"visit_duration" => 45,
|
||||||
|
"percentage" => 25.0
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
"name" => "C",
|
||||||
|
"visitors" => 1,
|
||||||
|
"bounce_rate" => 0,
|
||||||
|
"visit_duration" => 30,
|
||||||
|
"percentage" => 25.0
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
"name" => "A",
|
||||||
|
"visitors" => 2,
|
||||||
|
"bounce_rate" => 100,
|
||||||
|
"visit_duration" => 0,
|
||||||
|
"percentage" => 50.0
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
"name" => "Z",
|
||||||
|
"visitors" => 1,
|
||||||
|
"bounce_rate" => 100,
|
||||||
|
"visit_duration" => 0,
|
||||||
|
"percentage" => 25.0
|
||||||
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -651,11 +711,18 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"bounce_rate" => 100,
|
"bounce_rate" => 100,
|
||||||
"visit_duration" => 0,
|
"visit_duration" => 0,
|
||||||
|
"percentage" => 66.67,
|
||||||
"comparison" => %{
|
"comparison" => %{
|
||||||
"visitors" => 0,
|
"visitors" => 0,
|
||||||
"bounce_rate" => 0,
|
"bounce_rate" => 0,
|
||||||
"visit_duration" => nil,
|
"visit_duration" => nil,
|
||||||
"change" => %{"visitors" => 100, "bounce_rate" => nil, "visit_duration" => nil}
|
"percentage" => 0.0,
|
||||||
|
"change" => %{
|
||||||
|
"visitors" => 100,
|
||||||
|
"bounce_rate" => nil,
|
||||||
|
"visit_duration" => nil,
|
||||||
|
"percentage" => 100
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
|
|
@ -663,11 +730,18 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"bounce_rate" => 100,
|
"bounce_rate" => 100,
|
||||||
"visit_duration" => 0,
|
"visit_duration" => 0,
|
||||||
|
"percentage" => 33.33,
|
||||||
"comparison" => %{
|
"comparison" => %{
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"bounce_rate" => 100,
|
"bounce_rate" => 100,
|
||||||
"visit_duration" => 0,
|
"visit_duration" => 0,
|
||||||
"change" => %{"visitors" => 0, "bounce_rate" => 0, "visit_duration" => 0}
|
"percentage" => 100.0,
|
||||||
|
"change" => %{
|
||||||
|
"visitors" => 0,
|
||||||
|
"bounce_rate" => 0,
|
||||||
|
"visit_duration" => 0,
|
||||||
|
"percentage" => -67
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -896,13 +970,15 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
"name" => "Paid Social",
|
"name" => "Paid Social",
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"bounce_rate" => 100,
|
"bounce_rate" => 100,
|
||||||
"visit_duration" => 0
|
"visit_duration" => 0,
|
||||||
|
"percentage" => 66.67
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"name" => "Organic Search",
|
"name" => "Organic Search",
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"bounce_rate" => 0,
|
"bounce_rate" => 0,
|
||||||
"visit_duration" => 900
|
"visit_duration" => 900,
|
||||||
|
"percentage" => 33.33
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -1087,13 +1163,15 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
"name" => "email",
|
"name" => "email",
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"bounce_rate" => 100,
|
"bounce_rate" => 100,
|
||||||
"visit_duration" => 0
|
"visit_duration" => 0,
|
||||||
|
"percentage" => 50.0
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"name" => "social",
|
"name" => "social",
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"bounce_rate" => 0,
|
"bounce_rate" => 0,
|
||||||
"visit_duration" => 900
|
"visit_duration" => 900,
|
||||||
|
"percentage" => 50.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1108,13 +1186,15 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
"name" => "email",
|
"name" => "email",
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"bounce_rate" => 50,
|
"bounce_rate" => 50,
|
||||||
"visit_duration" => 50
|
"visit_duration" => 50,
|
||||||
|
"percentage" => 50.0
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"name" => "social",
|
"name" => "social",
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"bounce_rate" => 50,
|
"bounce_rate" => 50,
|
||||||
"visit_duration" => 800.0
|
"visit_duration" => 800.0,
|
||||||
|
"percentage" => 50.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -1167,7 +1247,8 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
"name" => "social",
|
"name" => "social",
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"bounce_rate" => 0,
|
"bounce_rate" => 0,
|
||||||
"visit_duration" => 900
|
"visit_duration" => 900,
|
||||||
|
"percentage" => 100.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1181,8 +1262,9 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
%{
|
%{
|
||||||
"name" => "social",
|
"name" => "social",
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"bounce_rate" => 50,
|
"bounce_rate" => 50.0,
|
||||||
"visit_duration" => 800.0
|
"visit_duration" => 800.0,
|
||||||
|
"percentage" => 100.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -1348,13 +1430,15 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
"name" => "august",
|
"name" => "august",
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"bounce_rate" => 100,
|
"bounce_rate" => 100,
|
||||||
"visit_duration" => 0
|
"visit_duration" => 0,
|
||||||
|
"percentage" => 66.67
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"name" => "profile",
|
"name" => "profile",
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"bounce_rate" => 0,
|
"bounce_rate" => 0,
|
||||||
"visit_duration" => 900
|
"visit_duration" => 900,
|
||||||
|
"percentage" => 33.33
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1369,13 +1453,15 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
"name" => "august",
|
"name" => "august",
|
||||||
"visitors" => 3,
|
"visitors" => 3,
|
||||||
"bounce_rate" => 67,
|
"bounce_rate" => 67,
|
||||||
"visit_duration" => 300
|
"visit_duration" => 300,
|
||||||
|
"percentage" => 60.0
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"name" => "profile",
|
"name" => "profile",
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"bounce_rate" => 50,
|
"bounce_rate" => 50,
|
||||||
"visit_duration" => 800.0
|
"visit_duration" => 800.0,
|
||||||
|
"percentage" => 40.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -1432,7 +1518,8 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
"name" => "profile",
|
"name" => "profile",
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"bounce_rate" => 0,
|
"bounce_rate" => 0,
|
||||||
"visit_duration" => 900
|
"visit_duration" => 900,
|
||||||
|
"percentage" => 100.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1446,8 +1533,9 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
%{
|
%{
|
||||||
"name" => "profile",
|
"name" => "profile",
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"bounce_rate" => 50,
|
"bounce_rate" => 50.0,
|
||||||
"visit_duration" => 800.0
|
"visit_duration" => 800.0,
|
||||||
|
"percentage" => 100.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -1594,13 +1682,15 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
"name" => "newsletter",
|
"name" => "newsletter",
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"bounce_rate" => 100,
|
"bounce_rate" => 100,
|
||||||
"visit_duration" => 0
|
"visit_duration" => 0,
|
||||||
|
"percentage" => 66.67
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"name" => "Twitter",
|
"name" => "Twitter",
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"bounce_rate" => 0,
|
"bounce_rate" => 0,
|
||||||
"visit_duration" => 900
|
"visit_duration" => 900,
|
||||||
|
"percentage" => 33.33
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -1766,13 +1856,15 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
"name" => "Sweden",
|
"name" => "Sweden",
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"bounce_rate" => 100,
|
"bounce_rate" => 100,
|
||||||
"visit_duration" => 0
|
"visit_duration" => 0,
|
||||||
|
"percentage" => 66.67
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"name" => "oat milk",
|
"name" => "oat milk",
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"bounce_rate" => 0,
|
"bounce_rate" => 0,
|
||||||
"visit_duration" => 900
|
"visit_duration" => 900,
|
||||||
|
"percentage" => 33.33
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1786,14 +1878,16 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
%{
|
%{
|
||||||
"name" => "Sweden",
|
"name" => "Sweden",
|
||||||
"visitors" => 3,
|
"visitors" => 3,
|
||||||
"bounce_rate" => 67,
|
"bounce_rate" => 67.0,
|
||||||
"visit_duration" => 300
|
"visit_duration" => 300.0,
|
||||||
|
"percentage" => 60.0
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"name" => "oat milk",
|
"name" => "oat milk",
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"bounce_rate" => 50,
|
"bounce_rate" => 50.0,
|
||||||
"visit_duration" => 800.0
|
"visit_duration" => 800.0,
|
||||||
|
"percentage" => 40.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -1850,7 +1944,8 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
"name" => "oat milk",
|
"name" => "oat milk",
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"bounce_rate" => 0,
|
"bounce_rate" => 0,
|
||||||
"visit_duration" => 900
|
"visit_duration" => 900,
|
||||||
|
"percentage" => 100.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1864,8 +1959,9 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
%{
|
%{
|
||||||
"name" => "oat milk",
|
"name" => "oat milk",
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"bounce_rate" => 50,
|
"bounce_rate" => 50.0,
|
||||||
"visit_duration" => 800.0
|
"visit_duration" => 800.0,
|
||||||
|
"percentage" => 100.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -2031,13 +2127,15 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
"name" => "blog",
|
"name" => "blog",
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"bounce_rate" => 100,
|
"bounce_rate" => 100,
|
||||||
"visit_duration" => 0
|
"visit_duration" => 0,
|
||||||
|
"percentage" => 66.67
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"name" => "ad",
|
"name" => "ad",
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"bounce_rate" => 0,
|
"bounce_rate" => 0,
|
||||||
"visit_duration" => 900
|
"visit_duration" => 900,
|
||||||
|
"percentage" => 33.33
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -2051,14 +2149,16 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
%{
|
%{
|
||||||
"name" => "blog",
|
"name" => "blog",
|
||||||
"visitors" => 3,
|
"visitors" => 3,
|
||||||
"bounce_rate" => 67,
|
"bounce_rate" => 67.0,
|
||||||
"visit_duration" => 300
|
"visit_duration" => 300.0,
|
||||||
|
"percentage" => 60.0
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"name" => "ad",
|
"name" => "ad",
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"bounce_rate" => 50,
|
"bounce_rate" => 50.0,
|
||||||
"visit_duration" => 800.0
|
"visit_duration" => 800.0,
|
||||||
|
"percentage" => 40.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -2115,7 +2215,8 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
"name" => "ad",
|
"name" => "ad",
|
||||||
"visitors" => 1,
|
"visitors" => 1,
|
||||||
"bounce_rate" => 0,
|
"bounce_rate" => 0,
|
||||||
"visit_duration" => 900
|
"visit_duration" => 900,
|
||||||
|
"percentage" => 100.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -2129,8 +2230,9 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
%{
|
%{
|
||||||
"name" => "ad",
|
"name" => "ad",
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"bounce_rate" => 50,
|
"bounce_rate" => 50.0,
|
||||||
"visit_duration" => 800.0
|
"visit_duration" => 800.0,
|
||||||
|
"percentage" => 100.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
@ -2637,8 +2739,8 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
)
|
)
|
||||||
|
|
||||||
assert json_response(conn, 200)["results"] == [
|
assert json_response(conn, 200)["results"] == [
|
||||||
%{"name" => "10words.com", "visitors" => 2},
|
%{"name" => "10words.com", "visitors" => 2, "percentage" => 66.67},
|
||||||
%{"name" => "10words.com/page1", "visitors" => 1}
|
%{"name" => "10words.com/page1", "visitors" => 1, "percentage" => 33.33}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -2683,7 +2785,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
)
|
)
|
||||||
|
|
||||||
assert json_response(conn, 200)["results"] == [
|
assert json_response(conn, 200)["results"] == [
|
||||||
%{"name" => "example.com/page1", "visitors" => 1}
|
%{"name" => "example.com/page1", "visitors" => 1, "percentage" => 100.0}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -2724,7 +2826,8 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||||
"name" => "10words.com",
|
"name" => "10words.com",
|
||||||
"visitors" => 2,
|
"visitors" => 2,
|
||||||
"bounce_rate" => 50.0,
|
"bounce_rate" => 50.0,
|
||||||
"visit_duration" => 450
|
"visit_duration" => 450,
|
||||||
|
"percentage" => 100.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -584,10 +584,10 @@ defmodule PlausibleWeb.StatsControllerTest do
|
||||||
assert result == [
|
assert result == [
|
||||||
["property", "value", "visitors", "events", "percentage"],
|
["property", "value", "visitors", "events", "percentage"],
|
||||||
["author", "(none)", "3", "4", "50.0"],
|
["author", "(none)", "3", "4", "50.0"],
|
||||||
["author", "uku", "2", "2", "33.3"],
|
["author", "uku", "2", "2", "33.33"],
|
||||||
["author", "marko", "1", "1", "16.7"],
|
["author", "marko", "1", "1", "16.67"],
|
||||||
["logged_in", "(none)", "5", "5", "83.3"],
|
["logged_in", "(none)", "5", "5", "83.33"],
|
||||||
["logged_in", "true", "1", "2", "16.7"],
|
["logged_in", "true", "1", "2", "16.67"],
|
||||||
[""]
|
[""]
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue