mirror of
https://github.com/plausible/analytics.git
synced 2024-11-22 18:52:38 +03:00
Add Details modals for all Devices reports (#4419)
* return concatenated browser names and versions directly from the API * return concatenated os names and versions directly from the API * add detailed views for all device reports * extract put_combined_name_with_version function * return only version under the name key when detailed=true * update changelog * add more metrics into detailed views of Devices reports * split up different devices modals into separate files
This commit is contained in:
parent
21372b2a7b
commit
1f8662438d
@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file.
|
||||
### Added
|
||||
- Support contains filter for goals
|
||||
- UI to edit funnels
|
||||
- Add Details views for browsers, browser versions, os-s, os versions, and screen sizes reports
|
||||
- Add a search functionality in all Details views, except for Countries, Regions, and Cities
|
||||
- Icons for browsers plausible/analytics#4239
|
||||
- Automatic custom property selection in the dashboard Properties report
|
||||
|
@ -10,6 +10,11 @@ import PagesModal from './stats/modals/pages'
|
||||
import EntryPagesModal from './stats/modals/entry-pages'
|
||||
import ExitPagesModal from './stats/modals/exit-pages'
|
||||
import LocationsModal from './stats/modals/locations-modal'
|
||||
import BrowsersModal from './stats/modals/devices/browsers-modal'
|
||||
import BrowserVersionsModal from './stats/modals/devices/browser-versions-modal'
|
||||
import OperatingSystemsModal from './stats/modals/devices/operating-systems-modal'
|
||||
import OperatingSystemVersionsModal from './stats/modals/devices/operating-system-versions-modal'
|
||||
import ScreenSizesModal from './stats/modals/devices/screen-sizes'
|
||||
import PropsModal from './stats/modals/props'
|
||||
import ConversionsModal from './stats/modals/conversions'
|
||||
import FilterModal from './stats/modals/filter-modal'
|
||||
@ -106,6 +111,31 @@ export const citiesRoute = {
|
||||
element: <LocationsModal currentView="cities" />
|
||||
}
|
||||
|
||||
export const browsersRoute = {
|
||||
path: 'browsers',
|
||||
element: <BrowsersModal />
|
||||
}
|
||||
|
||||
export const browserVersionsRoute = {
|
||||
path: 'browser-versions',
|
||||
element: <BrowserVersionsModal />
|
||||
}
|
||||
|
||||
export const operatingSystemsRoute = {
|
||||
path: 'operating-systems',
|
||||
element: <OperatingSystemsModal />
|
||||
}
|
||||
|
||||
export const operatingSystemVersionsRoute = {
|
||||
path: 'operating-system-versions',
|
||||
element: <OperatingSystemVersionsModal />
|
||||
}
|
||||
|
||||
export const screenSizesRoute = {
|
||||
path: 'screen-sizes',
|
||||
element: <ScreenSizesModal />
|
||||
}
|
||||
|
||||
export const conversionsRoute = {
|
||||
path: 'conversions',
|
||||
element: <ConversionsModal />
|
||||
@ -150,6 +180,11 @@ export function createAppRouter(site) {
|
||||
countriesRoute,
|
||||
regionsRoute,
|
||||
citiesRoute,
|
||||
browsersRoute,
|
||||
browserVersionsRoute,
|
||||
operatingSystemsRoute,
|
||||
operatingSystemVersionsRoute,
|
||||
screenSizesRoute,
|
||||
conversionsRoute,
|
||||
customPropsRoute,
|
||||
filterRoute,
|
||||
|
@ -8,6 +8,13 @@ import * as url from '../../util/url';
|
||||
import ImportedQueryUnsupportedWarning from '../imported-query-unsupported-warning';
|
||||
import { useQueryContext } from '../../query-context';
|
||||
import { useSiteContext } from '../../site-context';
|
||||
import {
|
||||
browsersRoute,
|
||||
browserVersionsRoute,
|
||||
operatingSystemsRoute,
|
||||
operatingSystemVersionsRoute,
|
||||
screenSizesRoute
|
||||
} from '../../router';
|
||||
|
||||
// Icons copied from https://github.com/alrra/browser-logos
|
||||
const BROWSER_ICONS = {
|
||||
@ -31,7 +38,7 @@ const BROWSER_ICONS = {
|
||||
'vivo Browser': 'vivo.png'
|
||||
}
|
||||
|
||||
function browserIconFor(browser) {
|
||||
export function browserIconFor(browser) {
|
||||
const filename = BROWSER_ICONS[browser] || 'fallback.svg'
|
||||
|
||||
return (
|
||||
@ -76,6 +83,7 @@ function Browsers({ afterFetchData }) {
|
||||
keyLabel="Browser"
|
||||
metrics={chooseMetrics()}
|
||||
renderIcon={renderIcon}
|
||||
detailsLinkProps={{ path: browsersRoute.path, search: (search) => search }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -85,13 +93,6 @@ function BrowserVersions({ afterFetchData }) {
|
||||
const site = useSiteContext();
|
||||
function fetchData() {
|
||||
return api.get(url.apiPath(site, '/browser-versions'), query)
|
||||
.then(res => {
|
||||
return {
|
||||
...res, results: res.results.map((row => {
|
||||
return { ...row, name: `${row.browser} ${row.name}`, version: row.name }
|
||||
}))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function renderIcon(listItem) {
|
||||
@ -124,6 +125,7 @@ function BrowserVersions({ afterFetchData }) {
|
||||
keyLabel="Browser version"
|
||||
metrics={chooseMetrics()}
|
||||
renderIcon={renderIcon}
|
||||
detailsLinkProps={{ path: browserVersionsRoute.path, search: (search) => search }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -148,7 +150,7 @@ const OS_ICONS = {
|
||||
'FreeBSD': 'freebsd.png',
|
||||
}
|
||||
|
||||
function osIconFor(os) {
|
||||
export function osIconFor(os) {
|
||||
const filename = OS_ICONS[os] || 'fallback.svg'
|
||||
|
||||
return (
|
||||
@ -193,6 +195,7 @@ function OperatingSystems({ afterFetchData }) {
|
||||
renderIcon={renderIcon}
|
||||
keyLabel="Operating system"
|
||||
metrics={chooseMetrics()}
|
||||
detailsLinkProps={{ path: operatingSystemsRoute.path, search: (search) => search }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -203,13 +206,6 @@ function OperatingSystemVersions({ afterFetchData }) {
|
||||
|
||||
function fetchData() {
|
||||
return api.get(url.apiPath(site, '/operating-system-versions'), query)
|
||||
.then(res => {
|
||||
return {
|
||||
...res, results: res.results.map((row => {
|
||||
return { ...row, name: `${row.os} ${row.name}`, version: row.name }
|
||||
}))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function renderIcon(listItem) {
|
||||
@ -242,6 +238,7 @@ function OperatingSystemVersions({ afterFetchData }) {
|
||||
getFilterFor={getFilterFor}
|
||||
keyLabel="Operating System Version"
|
||||
metrics={chooseMetrics()}
|
||||
detailsLinkProps={{ path: operatingSystemVersionsRoute.path, search: (search) => search }}
|
||||
/>
|
||||
)
|
||||
|
||||
@ -255,10 +252,8 @@ function ScreenSizes({ afterFetchData }) {
|
||||
return api.get(url.apiPath(site, '/screen-sizes'), query)
|
||||
}
|
||||
|
||||
function renderIcon(screenSize) {
|
||||
return (
|
||||
<span className="mr-1.5">{iconFor(screenSize.name)}</span>
|
||||
)
|
||||
function renderIcon(listItem) {
|
||||
return screenSizeIconFor(listItem.name)
|
||||
}
|
||||
|
||||
function getFilterFor(listItem) {
|
||||
@ -284,30 +279,25 @@ function ScreenSizes({ afterFetchData }) {
|
||||
keyLabel="Screen size"
|
||||
metrics={chooseMetrics()}
|
||||
renderIcon={renderIcon}
|
||||
detailsLinkProps={{ path: screenSizesRoute.path, search: (search) => search }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function iconFor(screenSize) {
|
||||
export function screenSizeIconFor(screenSize) {
|
||||
let svg = null
|
||||
|
||||
if (screenSize === 'Mobile') {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="-mt-px feather"><rect x="5" y="2" width="14" height="20" rx="2" ry="2" /><line x1="12" y1="18" x2="12" y2="18" /></svg>
|
||||
)
|
||||
svg = <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="-mt-px feather"><rect x="5" y="2" width="14" height="20" rx="2" ry="2" /><line x1="12" y1="18" x2="12" y2="18" /></svg>
|
||||
} else if (screenSize === 'Tablet') {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="-mt-px feather"><rect x="4" y="2" width="16" height="20" rx="2" ry="2" transform="rotate(180 12 12)" /><line x1="12" y1="18" x2="12" y2="18" /></svg>
|
||||
)
|
||||
svg = <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="-mt-px feather"><rect x="4" y="2" width="16" height="20" rx="2" ry="2" transform="rotate(180 12 12)" /><line x1="12" y1="18" x2="12" y2="18" /></svg>
|
||||
} else if (screenSize === 'Laptop') {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="-mt-px feather"><rect x="2" y="3" width="20" height="14" rx="2" ry="2" /><line x1="2" y1="20" x2="22" y2="20" /></svg>
|
||||
)
|
||||
svg = <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="-mt-px feather"><rect x="2" y="3" width="20" height="14" rx="2" ry="2" /><line x1="2" y1="20" x2="22" y2="20" /></svg>
|
||||
} else if (screenSize === 'Desktop') {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="-mt-px feather"><rect x="2" y="3" width="20" height="14" rx="2" ry="2" /><line x1="8" y1="21" x2="16" y2="21" /><line x1="12" y1="17" x2="12" y2="21" /></svg>
|
||||
)
|
||||
} else if (screenSize === '(not set)') {
|
||||
return null
|
||||
svg = <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="-mt-px feather"><rect x="2" y="3" width="20" height="14" rx="2" ry="2" /><line x1="8" y1="21" x2="16" y2="21" /><line x1="12" y1="17" x2="12" y2="21" /></svg>
|
||||
}
|
||||
|
||||
return <span className="mr-1.5">{svg}</span>
|
||||
}
|
||||
|
||||
export default function Devices() {
|
||||
|
@ -0,0 +1,48 @@
|
||||
import React, { useCallback } from "react";
|
||||
import Modal from './../modal';
|
||||
import { addFilter } from '../../../query'
|
||||
import BreakdownModal from "./../breakdown-modal";
|
||||
import * as url from '../../../util/url';
|
||||
import { useQueryContext } from "../../../query-context";
|
||||
import { useSiteContext } from "../../../site-context";
|
||||
import { browserIconFor } from "../../devices";
|
||||
import chooseMetrics from './choose-metrics';
|
||||
|
||||
function BrowserVersionsModal() {
|
||||
const { query } = useQueryContext();
|
||||
const site = useSiteContext();
|
||||
|
||||
const reportInfo = {
|
||||
title: 'Browser Versions',
|
||||
dimension: 'browser_version',
|
||||
endpoint: url.apiPath(site, '/browser-versions'),
|
||||
dimensionLabel: 'Browser version'
|
||||
}
|
||||
|
||||
const getFilterInfo = useCallback((listItem) => {
|
||||
return {
|
||||
prefix: reportInfo.dimension,
|
||||
filter: ["is", reportInfo.dimension, [listItem.name]]
|
||||
}
|
||||
}, [reportInfo.dimension])
|
||||
|
||||
const addSearchFilter = useCallback((query, searchString) => {
|
||||
return addFilter(query, ['contains', reportInfo.dimension, [searchString]])
|
||||
}, [reportInfo.dimension])
|
||||
|
||||
const renderIcon = useCallback((listItem) => browserIconFor(listItem.browser), [])
|
||||
|
||||
return (
|
||||
<Modal>
|
||||
<BreakdownModal
|
||||
reportInfo={reportInfo}
|
||||
metrics={chooseMetrics(query)}
|
||||
getFilterInfo={getFilterInfo}
|
||||
addSearchFilter={addSearchFilter}
|
||||
renderIcon={renderIcon}
|
||||
/>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default BrowserVersionsModal
|
48
assets/js/dashboard/stats/modals/devices/browsers-modal.js
Normal file
48
assets/js/dashboard/stats/modals/devices/browsers-modal.js
Normal file
@ -0,0 +1,48 @@
|
||||
import React, { useCallback } from "react";
|
||||
import Modal from './../modal';
|
||||
import { addFilter } from '../../../query'
|
||||
import BreakdownModal from "./../breakdown-modal";
|
||||
import * as url from '../../../util/url';
|
||||
import { useQueryContext } from "../../../query-context";
|
||||
import { useSiteContext } from "../../../site-context";
|
||||
import { browserIconFor } from "../../devices";
|
||||
import chooseMetrics from './choose-metrics';
|
||||
|
||||
function BrowsersModal() {
|
||||
const { query } = useQueryContext();
|
||||
const site = useSiteContext();
|
||||
|
||||
const reportInfo = {
|
||||
title: 'Browsers',
|
||||
dimension: 'browser',
|
||||
endpoint: url.apiPath(site, '/browsers'),
|
||||
dimensionLabel: 'Browser'
|
||||
}
|
||||
|
||||
const getFilterInfo = useCallback((listItem) => {
|
||||
return {
|
||||
prefix: reportInfo.dimension,
|
||||
filter: ["is", reportInfo.dimension, [listItem.name]]
|
||||
}
|
||||
}, [reportInfo.dimension])
|
||||
|
||||
const addSearchFilter = useCallback((query, searchString) => {
|
||||
return addFilter(query, ['contains', reportInfo.dimension, [searchString]])
|
||||
}, [reportInfo.dimension])
|
||||
|
||||
const renderIcon = useCallback((listItem) => browserIconFor(listItem.name), [])
|
||||
|
||||
return (
|
||||
<Modal>
|
||||
<BreakdownModal
|
||||
reportInfo={reportInfo}
|
||||
metrics={chooseMetrics(query)}
|
||||
getFilterInfo={getFilterInfo}
|
||||
addSearchFilter={addSearchFilter}
|
||||
renderIcon={renderIcon}
|
||||
/>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default BrowsersModal
|
26
assets/js/dashboard/stats/modals/devices/choose-metrics.js
Normal file
26
assets/js/dashboard/stats/modals/devices/choose-metrics.js
Normal file
@ -0,0 +1,26 @@
|
||||
import { hasGoalFilter, isRealTimeDashboard } from "../../../util/filters";
|
||||
import * as metrics from '../../reports/metrics'
|
||||
|
||||
export default function chooseMetrics(query) {
|
||||
if (hasGoalFilter(query)) {
|
||||
return [
|
||||
metrics.createTotalVisitors(),
|
||||
metrics.createVisitors({ renderLabel: (_query) => 'Conversions' }),
|
||||
metrics.createConversionRate()
|
||||
]
|
||||
}
|
||||
|
||||
if (isRealTimeDashboard(query)) {
|
||||
return [
|
||||
metrics.createVisitors({ renderLabel: (_query) => 'Current visitors' }),
|
||||
metrics.createPercentage()
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
metrics.createVisitors({ renderLabel: (_query) => "Visitors" }),
|
||||
metrics.createPercentage(),
|
||||
metrics.createBounceRate(),
|
||||
metrics.createVisitDuration()
|
||||
]
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
import React, { useCallback } from "react";
|
||||
import Modal from './../modal';
|
||||
import { addFilter } from '../../../query'
|
||||
import BreakdownModal from "./../breakdown-modal";
|
||||
import * as url from '../../../util/url';
|
||||
import { useQueryContext } from "../../../query-context";
|
||||
import { useSiteContext } from "../../../site-context";
|
||||
import { osIconFor } from "../../devices";
|
||||
import chooseMetrics from './choose-metrics';
|
||||
|
||||
function OperatingSystemVersionsModal() {
|
||||
const { query } = useQueryContext();
|
||||
const site = useSiteContext();
|
||||
|
||||
const reportInfo = {
|
||||
title: 'Operating System Versions',
|
||||
dimension: 'os_version',
|
||||
endpoint: url.apiPath(site, '/operating-system-versions'),
|
||||
dimensionLabel: 'Operating system version'
|
||||
}
|
||||
|
||||
const getFilterInfo = useCallback((listItem) => {
|
||||
return {
|
||||
prefix: reportInfo.dimension,
|
||||
filter: ["is", reportInfo.dimension, [listItem.name]]
|
||||
}
|
||||
}, [reportInfo.dimension])
|
||||
|
||||
const addSearchFilter = useCallback((query, searchString) => {
|
||||
return addFilter(query, ['contains', reportInfo.dimension, [searchString]])
|
||||
}, [reportInfo.dimension])
|
||||
|
||||
const renderIcon = useCallback((listItem) => osIconFor(listItem.os), [])
|
||||
|
||||
return (
|
||||
<Modal>
|
||||
<BreakdownModal
|
||||
reportInfo={reportInfo}
|
||||
metrics={chooseMetrics(query)}
|
||||
getFilterInfo={getFilterInfo}
|
||||
addSearchFilter={addSearchFilter}
|
||||
renderIcon={renderIcon}
|
||||
/>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default OperatingSystemVersionsModal
|
@ -0,0 +1,48 @@
|
||||
import React, { useCallback } from "react";
|
||||
import Modal from './../modal';
|
||||
import { addFilter } from '../../../query'
|
||||
import BreakdownModal from "./../breakdown-modal";
|
||||
import * as url from '../../../util/url';
|
||||
import { useQueryContext } from "../../../query-context";
|
||||
import { useSiteContext } from "../../../site-context";
|
||||
import { osIconFor } from "../../devices";
|
||||
import chooseMetrics from './choose-metrics';
|
||||
|
||||
function OperatingSystemsModal() {
|
||||
const { query } = useQueryContext();
|
||||
const site = useSiteContext();
|
||||
|
||||
const reportInfo = {
|
||||
title: 'Operating Systems',
|
||||
dimension: 'os',
|
||||
endpoint: url.apiPath(site, '/operating-systems'),
|
||||
dimensionLabel: 'Operating system'
|
||||
}
|
||||
|
||||
const getFilterInfo = useCallback((listItem) => {
|
||||
return {
|
||||
prefix: reportInfo.dimension,
|
||||
filter: ["is", reportInfo.dimension, [listItem.name]]
|
||||
}
|
||||
}, [reportInfo.dimension])
|
||||
|
||||
const addSearchFilter = useCallback((query, searchString) => {
|
||||
return addFilter(query, ['contains', reportInfo.dimension, [searchString]])
|
||||
}, [reportInfo.dimension])
|
||||
|
||||
const renderIcon = useCallback((listItem) => osIconFor(listItem.name), [])
|
||||
|
||||
return (
|
||||
<Modal>
|
||||
<BreakdownModal
|
||||
reportInfo={reportInfo}
|
||||
metrics={chooseMetrics(query)}
|
||||
getFilterInfo={getFilterInfo}
|
||||
addSearchFilter={addSearchFilter}
|
||||
renderIcon={renderIcon}
|
||||
/>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default OperatingSystemsModal
|
43
assets/js/dashboard/stats/modals/devices/screen-sizes.js
Normal file
43
assets/js/dashboard/stats/modals/devices/screen-sizes.js
Normal file
@ -0,0 +1,43 @@
|
||||
import React, { useCallback } from "react";
|
||||
import Modal from './../modal';
|
||||
import BreakdownModal from "./../breakdown-modal";
|
||||
import * as url from '../../../util/url';
|
||||
import { useQueryContext } from "../../../query-context";
|
||||
import { useSiteContext } from "../../../site-context";
|
||||
import { screenSizeIconFor } from "../../devices";
|
||||
import chooseMetrics from './choose-metrics';
|
||||
|
||||
function ScreenSizesModal() {
|
||||
const { query } = useQueryContext();
|
||||
const site = useSiteContext();
|
||||
|
||||
const reportInfo = {
|
||||
title: 'Screen Sizes',
|
||||
dimension: 'screen',
|
||||
endpoint: url.apiPath(site, '/screen-sizes'),
|
||||
dimensionLabel: 'Screen size'
|
||||
}
|
||||
|
||||
const getFilterInfo = useCallback((listItem) => {
|
||||
return {
|
||||
prefix: reportInfo.dimension,
|
||||
filter: ["is", reportInfo.dimension, [listItem.name]]
|
||||
}
|
||||
}, [reportInfo.dimension])
|
||||
|
||||
const renderIcon = useCallback((listItem) => screenSizeIconFor(listItem.name), [])
|
||||
|
||||
return (
|
||||
<Modal>
|
||||
<BreakdownModal
|
||||
reportInfo={reportInfo}
|
||||
metrics={chooseMetrics(query)}
|
||||
getFilterInfo={getFilterInfo}
|
||||
searchEnabled={false}
|
||||
renderIcon={renderIcon}
|
||||
/>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default ScreenSizesModal
|
@ -12,6 +12,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
require Logger
|
||||
|
||||
@revenue_metrics on_ee(do: Plausible.Stats.Goal.Revenue.revenue_metrics(), else: [])
|
||||
@not_set "(not set)"
|
||||
|
||||
plug(:date_validation_plug)
|
||||
|
||||
@ -1095,7 +1096,11 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
params = Map.put(params, "property", "visit:browser")
|
||||
query = Query.from(site, params)
|
||||
pagination = parse_pagination(params)
|
||||
metrics = breakdown_metrics(query, [:percentage])
|
||||
|
||||
extra_metrics =
|
||||
if params["detailed"], do: [:bounce_rate, :visit_duration], else: []
|
||||
|
||||
metrics = breakdown_metrics(query, extra_metrics ++ [:percentage])
|
||||
|
||||
browsers =
|
||||
Stats.breakdown(site, query, metrics, pagination)
|
||||
@ -1122,29 +1127,36 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
params = Map.put(params, "property", "visit:browser_version")
|
||||
query = Query.from(site, params)
|
||||
pagination = parse_pagination(params)
|
||||
metrics = breakdown_metrics(query, [:percentage])
|
||||
|
||||
versions =
|
||||
extra_metrics =
|
||||
if params["detailed"], do: [:bounce_rate, :visit_duration], else: []
|
||||
|
||||
metrics = breakdown_metrics(query, extra_metrics ++ [:percentage])
|
||||
|
||||
results =
|
||||
Stats.breakdown(site, query, metrics, pagination)
|
||||
|> transform_keys(%{browser_version: :name})
|
||||
|> transform_keys(%{browser_version: :version})
|
||||
|
||||
if params["csv"] do
|
||||
if Query.get_filter(query, "event:goal") do
|
||||
versions
|
||||
|> transform_keys(%{
|
||||
name: :version,
|
||||
browser: :name,
|
||||
visitors: :conversions
|
||||
})
|
||||
results
|
||||
|> transform_keys(%{browser: :name, visitors: :conversions})
|
||||
|> to_csv([:name, :version, :conversions, :conversion_rate])
|
||||
else
|
||||
versions
|
||||
|> transform_keys(%{name: :version, browser: :name})
|
||||
results
|
||||
|> transform_keys(%{browser: :name})
|
||||
|> to_csv([:name, :version, :visitors])
|
||||
end
|
||||
else
|
||||
results =
|
||||
if params["detailed"] do
|
||||
transform_keys(results, %{version: :name})
|
||||
else
|
||||
Enum.map(results, &put_combined_name_with_version(&1, :browser))
|
||||
end
|
||||
|
||||
json(conn, %{
|
||||
results: versions,
|
||||
results: results,
|
||||
skip_imported_reason: query.skip_imported_reason
|
||||
})
|
||||
end
|
||||
@ -1155,7 +1167,11 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
params = Map.put(params, "property", "visit:os")
|
||||
query = Query.from(site, params)
|
||||
pagination = parse_pagination(params)
|
||||
metrics = breakdown_metrics(query, [:percentage])
|
||||
|
||||
extra_metrics =
|
||||
if params["detailed"], do: [:bounce_rate, :visit_duration], else: []
|
||||
|
||||
metrics = breakdown_metrics(query, extra_metrics ++ [:percentage])
|
||||
|
||||
systems =
|
||||
Stats.breakdown(site, query, metrics, pagination)
|
||||
@ -1182,25 +1198,36 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
params = Map.put(params, "property", "visit:os_version")
|
||||
query = Query.from(site, params)
|
||||
pagination = parse_pagination(params)
|
||||
metrics = breakdown_metrics(query, [:percentage])
|
||||
|
||||
versions =
|
||||
extra_metrics =
|
||||
if params["detailed"], do: [:bounce_rate, :visit_duration], else: []
|
||||
|
||||
metrics = breakdown_metrics(query, extra_metrics ++ [:percentage])
|
||||
|
||||
results =
|
||||
Stats.breakdown(site, query, metrics, pagination)
|
||||
|> transform_keys(%{os_version: :name})
|
||||
|> transform_keys(%{os_version: :version})
|
||||
|
||||
if params["csv"] do
|
||||
if Query.get_filter(query, "event:goal") do
|
||||
versions
|
||||
|> transform_keys(%{name: :version, os: :name, visitors: :conversions})
|
||||
results
|
||||
|> transform_keys(%{os: :name, visitors: :conversions})
|
||||
|> to_csv([:name, :version, :conversions, :conversion_rate])
|
||||
else
|
||||
versions
|
||||
|> transform_keys(%{name: :version, os: :name})
|
||||
results
|
||||
|> transform_keys(%{os: :name})
|
||||
|> to_csv([:name, :version, :visitors])
|
||||
end
|
||||
else
|
||||
results =
|
||||
if params["detailed"] do
|
||||
transform_keys(results, %{version: :name})
|
||||
else
|
||||
Enum.map(results, &put_combined_name_with_version(&1, :os))
|
||||
end
|
||||
|
||||
json(conn, %{
|
||||
results: versions,
|
||||
results: results,
|
||||
skip_imported_reason: query.skip_imported_reason
|
||||
})
|
||||
end
|
||||
@ -1211,7 +1238,11 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
params = Map.put(params, "property", "visit:device")
|
||||
query = Query.from(site, params)
|
||||
pagination = parse_pagination(params)
|
||||
metrics = breakdown_metrics(query, [:percentage])
|
||||
|
||||
extra_metrics =
|
||||
if params["detailed"], do: [:bounce_rate, :visit_duration], else: []
|
||||
|
||||
metrics = breakdown_metrics(query, extra_metrics ++ [:percentage])
|
||||
|
||||
sizes =
|
||||
Stats.breakdown(site, query, metrics, pagination)
|
||||
@ -1539,4 +1570,14 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
[:visitors] ++ extra_metrics
|
||||
end
|
||||
end
|
||||
|
||||
def put_combined_name_with_version(row, name_key) do
|
||||
name =
|
||||
case {row[name_key], row.version} do
|
||||
{@not_set, @not_set} -> @not_set
|
||||
{browser_or_os, version} -> "#{browser_or_os} #{version}"
|
||||
end
|
||||
|
||||
Map.put(row, :name, name)
|
||||
end
|
||||
end
|
||||
|
@ -225,17 +225,19 @@ defmodule PlausibleWeb.Api.StatsController.BrowsersTest do
|
||||
|> Map.get("results")
|
||||
|
||||
assert %{
|
||||
"name" => "Chrome 110",
|
||||
"browser" => "Chrome",
|
||||
"conversion_rate" => 66.7,
|
||||
"name" => "110",
|
||||
"version" => "110",
|
||||
"total_visitors" => 3,
|
||||
"visitors" => 2
|
||||
} == List.first(json_response)
|
||||
|
||||
assert %{
|
||||
"name" => "Firefox 121",
|
||||
"browser" => "Firefox",
|
||||
"conversion_rate" => 100.0,
|
||||
"name" => "121",
|
||||
"version" => "121",
|
||||
"total_visitors" => 1,
|
||||
"visitors" => 1
|
||||
} == List.last(json_response)
|
||||
@ -258,8 +260,44 @@ defmodule PlausibleWeb.Api.StatsController.BrowsersTest do
|
||||
)
|
||||
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "78.0", "visitors" => 2, "percentage" => 66.7, "browser" => "Chrome"},
|
||||
%{"name" => "77.0", "visitors" => 1, "percentage" => 33.3, "browser" => "Chrome"}
|
||||
%{
|
||||
"name" => "Chrome 78.0",
|
||||
"version" => "78.0",
|
||||
"visitors" => 2,
|
||||
"percentage" => 66.7,
|
||||
"browser" => "Chrome"
|
||||
},
|
||||
%{
|
||||
"name" => "Chrome 77.0",
|
||||
"version" => "77.0",
|
||||
"visitors" => 1,
|
||||
"percentage" => 33.3,
|
||||
"browser" => "Chrome"
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
test "returns only version under the name key (+ additional metrics) when 'detailed' is true in params",
|
||||
%{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
populate_stats(site, [build(:pageview, browser: "Chrome", browser_version: "78.0")])
|
||||
|
||||
filters = Jason.encode!(%{browser: "Chrome"})
|
||||
|
||||
conn =
|
||||
get(conn, "/api/stats/#{site.domain}/browser-versions?filters=#{filters}&detailed=true")
|
||||
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "78.0",
|
||||
"browser" => "Chrome",
|
||||
"visitors" => 1,
|
||||
"bounce_rate" => 100,
|
||||
"visit_duration" => 0,
|
||||
"percentage" => 100.0
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
@ -281,7 +319,8 @@ defmodule PlausibleWeb.Api.StatsController.BrowsersTest do
|
||||
"name" => "(not set)",
|
||||
"visitors" => 1,
|
||||
"percentage" => 100,
|
||||
"browser" => "(not set)"
|
||||
"browser" => "(not set)",
|
||||
"version" => "(not set)"
|
||||
}
|
||||
]
|
||||
end
|
||||
@ -323,13 +362,32 @@ defmodule PlausibleWeb.Api.StatsController.BrowsersTest do
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"browser" => "(not set)",
|
||||
"version" => "(not set)",
|
||||
"name" => "(not set)",
|
||||
"visitors" => 10,
|
||||
"percentage" => 50.0
|
||||
},
|
||||
%{"browser" => "Chrome", "name" => "121", "visitors" => 6, "percentage" => 30.0},
|
||||
%{"browser" => "Firefox", "name" => "121", "visitors" => 3, "percentage" => 15.0},
|
||||
%{"browser" => "Chrome", "name" => "110", "visitors" => 1, "percentage" => 5.0}
|
||||
%{
|
||||
"browser" => "Chrome",
|
||||
"version" => "121",
|
||||
"name" => "Chrome 121",
|
||||
"visitors" => 6,
|
||||
"percentage" => 30.0
|
||||
},
|
||||
%{
|
||||
"browser" => "Firefox",
|
||||
"version" => "121",
|
||||
"name" => "Firefox 121",
|
||||
"visitors" => 3,
|
||||
"percentage" => 15.0
|
||||
},
|
||||
%{
|
||||
"browser" => "Chrome",
|
||||
"version" => "110",
|
||||
"name" => "Chrome 110",
|
||||
"visitors" => 1,
|
||||
"percentage" => 5.0
|
||||
}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
@ -246,8 +246,49 @@ defmodule PlausibleWeb.Api.StatsController.OperatingSystemsTest do
|
||||
)
|
||||
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "10.16", "visitors" => 2, "percentage" => 66.7, "os" => "Mac"},
|
||||
%{"name" => "10.15", "visitors" => 1, "percentage" => 33.3, "os" => "Mac"}
|
||||
%{
|
||||
"name" => "Mac 10.16",
|
||||
"visitors" => 2,
|
||||
"percentage" => 66.7,
|
||||
"os" => "Mac",
|
||||
"version" => "10.16"
|
||||
},
|
||||
%{
|
||||
"name" => "Mac 10.15",
|
||||
"visitors" => 1,
|
||||
"percentage" => 33.3,
|
||||
"os" => "Mac",
|
||||
"version" => "10.15"
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
test "returns only version under the name key (+ additional metrics) when 'detailed' is true in params",
|
||||
%{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
populate_stats(site, [
|
||||
build(:pageview, operating_system: "Mac", operating_system_version: "14")
|
||||
])
|
||||
|
||||
filters = Jason.encode!(%{os: "Mac"})
|
||||
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
"/api/stats/#{site.domain}/operating-system-versions?filters=#{filters}&detailed=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "14",
|
||||
"os" => "Mac",
|
||||
"visitors" => 1,
|
||||
"bounce_rate" => 100,
|
||||
"visit_duration" => 0,
|
||||
"percentage" => 100.0
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
@ -289,12 +330,31 @@ defmodule PlausibleWeb.Api.StatsController.OperatingSystemsTest do
|
||||
%{
|
||||
"os" => "(not set)",
|
||||
"name" => "(not set)",
|
||||
"version" => "(not set)",
|
||||
"visitors" => 10,
|
||||
"percentage" => 50.0
|
||||
},
|
||||
%{"os" => "Mac", "name" => "11", "visitors" => 6, "percentage" => 30.0},
|
||||
%{"os" => "Windows", "name" => "11", "visitors" => 3, "percentage" => 15.0},
|
||||
%{"os" => "Mac", "name" => "12", "visitors" => 1, "percentage" => 5.0}
|
||||
%{
|
||||
"os" => "Mac",
|
||||
"name" => "Mac 11",
|
||||
"version" => "11",
|
||||
"visitors" => 6,
|
||||
"percentage" => 30.0
|
||||
},
|
||||
%{
|
||||
"os" => "Windows",
|
||||
"name" => "Windows 11",
|
||||
"version" => "11",
|
||||
"visitors" => 3,
|
||||
"percentage" => 15.0
|
||||
},
|
||||
%{
|
||||
"os" => "Mac",
|
||||
"name" => "Mac 12",
|
||||
"version" => "12",
|
||||
"visitors" => 1,
|
||||
"percentage" => 5.0
|
||||
}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
@ -19,6 +19,38 @@ defmodule PlausibleWeb.Api.StatsController.ScreenSizesTest do
|
||||
]
|
||||
end
|
||||
|
||||
test "returns bounce_rate and visit_duration when detailed=true", %{conn: conn, site: site} do
|
||||
populate_stats(site, [
|
||||
build(:pageview, user_id: 123, timestamp: ~N[2021-01-01 12:00:00], screen_size: "Desktop"),
|
||||
build(:pageview, user_id: 123, timestamp: ~N[2021-01-01 12:10:00], screen_size: "Desktop"),
|
||||
build(:pageview, timestamp: ~N[2021-01-01 12:00:00], screen_size: "Desktop"),
|
||||
build(:pageview, timestamp: ~N[2021-01-01 12:00:00], screen_size: "Laptop")
|
||||
])
|
||||
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
"/api/stats/#{site.domain}/screen-sizes?period=day&date=2021-01-01&detailed=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "Desktop",
|
||||
"visitors" => 2,
|
||||
"bounce_rate" => 50,
|
||||
"visit_duration" => 300,
|
||||
"percentage" => 66.7
|
||||
},
|
||||
%{
|
||||
"name" => "Laptop",
|
||||
"visitors" => 1,
|
||||
"bounce_rate" => 100,
|
||||
"visit_duration" => 0,
|
||||
"percentage" => 33.3
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
test "returns screen sizes for user making multiple sessions", %{conn: conn, site: site} do
|
||||
populate_stats(site, [
|
||||
build(:pageview,
|
||||
|
Loading…
Reference in New Issue
Block a user