mirror of
https://github.com/plausible/analytics.git
synced 2024-12-23 09:33:19 +03:00
Implement filtering for imported data (#4118)
* move imported.ex to imported subfolder * move constructing base imported query into a separate module * Implement imported table deciding and filtering + tests for pages, entry_pages, exit_pages and common filter types * add top stats test with country filter * add timeseries test * Drop bounce_rate and time_on_page from imported & page-filtered Top Stats * rename field returned by top stats * turn pages into a fn comp * Move dashboard API results under a results key ...and also return the skip_imported_reason to the frontend to be used for displaying warnings. * extend ListReport component with an optional afterFetchData prop * turn Devices into a fn comp * add not_requested as a skip_imported_reason * display warning icons in the dashboard * Implement filtering suggestions and translate filter fields for imported * WIP * Improve and cover filtering suggestions with tests * Rename imported suggestions query helpers * fix screen size breakdown with screen size filter * support filtering by the same suggestion property * support location filters when fetching location suggestions * support filtering by multiple props from the same table * Implement filtering by goals * Make views per visit metric work for import entry and exit pages * Get rid of circular dependencies between Stats.Imported and Stats.Imported.Base * Clean up Query struct manipulation in Breakdown * Rename helper function for clarity * Automatically refresh query struct state after modifications * Shutup credo * display imported warning bubble in prop breakdown section * Render warning bubble for funnels whenever imported data is in the view * Transform any operator on respective goal filters * Fix percentage and conversion_rate calculation in presence of custom props * add tests for for combining page and pageview goal filters * add skip_refresh option to query tweaking functions * add imported CR support for timeseries * still show url breakdown when special goal + url in filter * rename Query.refresh * use flat_map instead of map and concat * fix darkmode color * Handle invalid imported region codes in suggestions gracefully * Add an entry to CHANGELOG.md --------- Co-authored-by: Adrian Gruntkowski <adrian.gruntkowski@gmail.com>
This commit is contained in:
parent
7cd9beac8f
commit
1d3b068233
@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file.
|
||||
### Added
|
||||
|
||||
- Snippet integration verification
|
||||
- Limited filtering support for imported data in the dashboard and via Stats API
|
||||
|
||||
### Removed
|
||||
|
||||
|
@ -31,7 +31,7 @@ function Historical(props) {
|
||||
<ComparisonInput site={props.site} query={props.query} />
|
||||
</div>
|
||||
</div>
|
||||
<VisitorGraph site={props.site} query={props.query} />
|
||||
<VisitorGraph site={props.site} query={props.query} updateImportedDataInView={props.updateImportedDataInView}/>
|
||||
|
||||
<div className="w-full md:flex">
|
||||
<div className={ statsBoxClass }>
|
||||
@ -51,7 +51,7 @@ function Historical(props) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Behaviours site={props.site} query={props.query} currentUserRole={props.currentUserRole} />
|
||||
<Behaviours site={props.site} query={props.query} currentUserRole={props.currentUserRole} importedDataInView={props.importedDataInView}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -13,8 +13,10 @@ class Dashboard extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.updateLastLoadTimestamp = this.updateLastLoadTimestamp.bind(this)
|
||||
this.updateImportedDataInView = this.updateImportedDataInView.bind(this)
|
||||
this.state = {
|
||||
query: parseQuery(props.location.search, this.props.site),
|
||||
importedDataInView: false,
|
||||
lastLoadTimestamp: new Date()
|
||||
}
|
||||
}
|
||||
@ -35,6 +37,10 @@ class Dashboard extends React.Component {
|
||||
this.setState({lastLoadTimestamp: new Date()})
|
||||
}
|
||||
|
||||
updateImportedDataInView(newBoolean) {
|
||||
this.setState({importedDataInView: newBoolean})
|
||||
}
|
||||
|
||||
render() {
|
||||
const { site, loggedIn, currentUserRole } = this.props
|
||||
const { query, lastLoadTimestamp } = this.state
|
||||
@ -42,7 +48,7 @@ class Dashboard extends React.Component {
|
||||
if (this.state.query.period === 'realtime') {
|
||||
return <Realtime site={site} loggedIn={loggedIn} currentUserRole={currentUserRole} query={query} lastLoadTimestamp={lastLoadTimestamp}/>
|
||||
} else {
|
||||
return <Historical site={site} loggedIn={loggedIn} currentUserRole={currentUserRole} query={query} lastLoadTimestamp={lastLoadTimestamp}/>
|
||||
return <Historical site={site} loggedIn={loggedIn} currentUserRole={currentUserRole} query={query} lastLoadTimestamp={lastLoadTimestamp} importedDataInView={this.state.importedDataInView} updateImportedDataInView={this.updateImportedDataInView}/>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import { CR_METRIC } from '../reports/metrics';
|
||||
import ListReport from '../reports/list';
|
||||
|
||||
export default function Conversions(props) {
|
||||
const { site, query } = props
|
||||
const { site, query, afterFetchData } = props
|
||||
|
||||
function fetchConversions() {
|
||||
return api.get(url.apiPath(site, '/conversions'), query, { limit: 9 })
|
||||
@ -23,6 +23,7 @@ export default function Conversions(props) {
|
||||
return (
|
||||
<ListReport
|
||||
fetchData={fetchConversions}
|
||||
afterFetchData={afterFetchData}
|
||||
getFilterFor={getFilterFor}
|
||||
keyLabel="Goal"
|
||||
onClick={props.onGoalFilterClick}
|
||||
|
@ -31,7 +31,7 @@ export function specialTitleWhenGoalFilter(query, defaultTitle) {
|
||||
}
|
||||
|
||||
function SpecialPropBreakdown(props) {
|
||||
const { site, query, prop } = props
|
||||
const { site, query, prop, afterFetchData } = props
|
||||
|
||||
function fetchData() {
|
||||
return api.get(url.apiPath(site, `/custom-prop-values/${prop}`), query)
|
||||
@ -55,6 +55,7 @@ function SpecialPropBreakdown(props) {
|
||||
return (
|
||||
<ListReport
|
||||
fetchData={fetchData}
|
||||
afterFetchData={afterFetchData}
|
||||
getFilterFor={getFilterFor}
|
||||
keyLabel={prop}
|
||||
metrics={[
|
||||
@ -73,12 +74,12 @@ function SpecialPropBreakdown(props) {
|
||||
}
|
||||
|
||||
export default function GoalConversions(props) {
|
||||
const {site, query} = props
|
||||
const {site, query, afterFetchData} = props
|
||||
|
||||
const specialGoal = getSpecialGoal(query)
|
||||
if (specialGoal) {
|
||||
return <SpecialPropBreakdown site={site} query={props.query} prop={specialGoal.prop} />
|
||||
return <SpecialPropBreakdown site={site} query={props.query} prop={specialGoal.prop} afterFetchData={afterFetchData} />
|
||||
} else {
|
||||
return <Conversions site={site} query={props.query} onGoalFilterClick={props.onGoalFilterClick}/>
|
||||
return <Conversions site={site} query={props.query} onGoalFilterClick={props.onGoalFilterClick} afterFetchData={afterFetchData} />
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import { Menu, Transition } from '@headlessui/react'
|
||||
import { ChevronDownIcon } from '@heroicons/react/20/solid'
|
||||
import classNames from 'classnames'
|
||||
import * as storage from '../../util/storage'
|
||||
|
||||
import ImportedQueryUnsupportedWarning from '../imported-query-unsupported-warning'
|
||||
import GoalConversions, { specialTitleWhenGoalFilter } from './goal-conversions'
|
||||
import Properties from './props'
|
||||
import { FeatureSetupNotice } from '../../components/notice'
|
||||
@ -48,6 +48,8 @@ export default function Behaviours(props) {
|
||||
|
||||
const [showingPropsForGoalFilter, setShowingPropsForGoalFilter] = useState(false)
|
||||
|
||||
const [importedQueryUnsupported, setImportedQueryUnsupported] = useState(false)
|
||||
|
||||
const onGoalFilterClick = useCallback((e) => {
|
||||
const goalName = e.target.innerHTML
|
||||
const isSpecialGoal = Object.keys(SPECIAL_GOALS).includes(goalName)
|
||||
@ -170,9 +172,14 @@ export default function Behaviours(props) {
|
||||
)
|
||||
}
|
||||
|
||||
function afterFetchData(apiResponse) {
|
||||
const unsupportedQuery = apiResponse.skip_imported_reason === 'unsupported_query'
|
||||
setImportedQueryUnsupported(unsupportedQuery && !isRealtime())
|
||||
}
|
||||
|
||||
function renderConversions() {
|
||||
if (site.hasGoals) {
|
||||
return <GoalConversions site={site} query={query} onGoalFilterClick={onGoalFilterClick} />
|
||||
return <GoalConversions site={site} query={query} onGoalFilterClick={onGoalFilterClick} afterFetchData={afterFetchData}/>
|
||||
}
|
||||
else if (adminAccess) {
|
||||
return (
|
||||
@ -224,7 +231,7 @@ export default function Behaviours(props) {
|
||||
|
||||
function renderProps() {
|
||||
if (site.hasProps && site.propsAvailable) {
|
||||
return <Properties site={site} query={query} />
|
||||
return <Properties site={site} query={query} afterFetchData={afterFetchData}/>
|
||||
} else if (adminAccess) {
|
||||
let callToAction
|
||||
|
||||
@ -330,9 +337,14 @@ export default function Behaviours(props) {
|
||||
<div className="items-start justify-between block w-full mt-6 md:flex">
|
||||
<div className="w-full p-4 bg-white rounded shadow-xl dark:bg-gray-825">
|
||||
<div className="flex justify-between w-full">
|
||||
<h3 className="font-bold dark:text-gray-100">
|
||||
{sectionTitle() + (isRealtime() ? ' (last 30min)' : '')}
|
||||
</h3>
|
||||
<div className="flex gap-x-1">
|
||||
<h3 className="font-bold dark:text-gray-100">
|
||||
{sectionTitle() + (isRealtime() ? ' (last 30min)' : '')}
|
||||
</h3>
|
||||
<ImportedQueryUnsupportedWarning condition={mode === CONVERSIONS && importedQueryUnsupported}/>
|
||||
<ImportedQueryUnsupportedWarning condition={mode === PROPS && importedQueryUnsupported} message="Imported data is unavailable in this view"/>
|
||||
<ImportedQueryUnsupportedWarning condition={mode === FUNNELS && props.importedDataInView} message="Imported data is unavailable in this view"/>
|
||||
</div>
|
||||
{tabs()}
|
||||
</div>
|
||||
{renderContent()}
|
||||
|
@ -75,6 +75,7 @@ export default function Properties(props) {
|
||||
return (
|
||||
<ListReport
|
||||
fetchData={fetchProps}
|
||||
afterFetchData={props.afterFetchData}
|
||||
getFilterFor={getFilterFor}
|
||||
keyLabel={propKey}
|
||||
metrics={[
|
||||
|
@ -1,13 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
import React, {useState} from 'react';
|
||||
import * as storage from '../../util/storage'
|
||||
import { getFiltersByKeyPrefix, isFilteringOnFixedValue } from '../../util/filters'
|
||||
import ListReport from '../reports/list'
|
||||
import * as api from '../../api'
|
||||
import * as url from '../../util/url'
|
||||
import { VISITORS_METRIC, PERCENTAGE_METRIC, maybeWithCR } from '../reports/metrics';
|
||||
import ImportedQueryUnsupportedWarning from '../imported-query-unsupported-warning';
|
||||
|
||||
function Browsers({ query, site }) {
|
||||
function Browsers({ query, site, afterFetchData }) {
|
||||
function fetchData() {
|
||||
return api.get(url.apiPath(site, '/browsers'), query)
|
||||
}
|
||||
@ -22,6 +22,7 @@ function Browsers({ query, site }) {
|
||||
return (
|
||||
<ListReport
|
||||
fetchData={fetchData}
|
||||
afterFetchData={afterFetchData}
|
||||
getFilterFor={getFilterFor}
|
||||
keyLabel="Browser"
|
||||
metrics={maybeWithCR([VISITORS_METRIC, PERCENTAGE_METRIC], query)}
|
||||
@ -30,7 +31,7 @@ function Browsers({ query, site }) {
|
||||
)
|
||||
}
|
||||
|
||||
function BrowserVersions({ query, site }) {
|
||||
function BrowserVersions({ query, site, afterFetchData }) {
|
||||
function fetchData() {
|
||||
return api.get(url.apiPath(site, '/browser-versions'), query)
|
||||
}
|
||||
@ -48,6 +49,7 @@ function BrowserVersions({ query, site }) {
|
||||
return (
|
||||
<ListReport
|
||||
fetchData={fetchData}
|
||||
afterFetchData={afterFetchData}
|
||||
getFilterFor={getFilterFor}
|
||||
keyLabel="Browser version"
|
||||
metrics={maybeWithCR([VISITORS_METRIC, PERCENTAGE_METRIC], query)}
|
||||
@ -57,7 +59,7 @@ function BrowserVersions({ query, site }) {
|
||||
|
||||
}
|
||||
|
||||
function OperatingSystems({ query, site }) {
|
||||
function OperatingSystems({ query, site, afterFetchData }) {
|
||||
function fetchData() {
|
||||
return api.get(url.apiPath(site, '/operating-systems'), query)
|
||||
}
|
||||
@ -72,6 +74,7 @@ function OperatingSystems({ query, site }) {
|
||||
return (
|
||||
<ListReport
|
||||
fetchData={fetchData}
|
||||
afterFetchData={afterFetchData}
|
||||
getFilterFor={getFilterFor}
|
||||
keyLabel="Operating system"
|
||||
metrics={maybeWithCR([VISITORS_METRIC, PERCENTAGE_METRIC], query)}
|
||||
@ -80,7 +83,7 @@ function OperatingSystems({ query, site }) {
|
||||
)
|
||||
}
|
||||
|
||||
function OperatingSystemVersions({ query, site }) {
|
||||
function OperatingSystemVersions({ query, site, afterFetchData }) {
|
||||
function fetchData() {
|
||||
return api.get(url.apiPath(site, '/operating-system-versions'), query)
|
||||
}
|
||||
@ -98,6 +101,7 @@ function OperatingSystemVersions({ query, site }) {
|
||||
return (
|
||||
<ListReport
|
||||
fetchData={fetchData}
|
||||
afterFetchData={afterFetchData}
|
||||
getFilterFor={getFilterFor}
|
||||
keyLabel="Operating System Version"
|
||||
metrics={maybeWithCR([VISITORS_METRIC, PERCENTAGE_METRIC], query)}
|
||||
@ -107,7 +111,7 @@ function OperatingSystemVersions({ query, site }) {
|
||||
|
||||
}
|
||||
|
||||
function ScreenSizes({ query, site }) {
|
||||
function ScreenSizes({ query, site, afterFetchData }) {
|
||||
function fetchData() {
|
||||
return api.get(url.apiPath(site, '/screen-sizes'), query)
|
||||
}
|
||||
@ -126,6 +130,7 @@ function ScreenSizes({ query, site }) {
|
||||
return (
|
||||
<ListReport
|
||||
fetchData={fetchData}
|
||||
afterFetchData={afterFetchData}
|
||||
getFilterFor={getFilterFor}
|
||||
keyLabel="Screen size"
|
||||
metrics={maybeWithCR([VISITORS_METRIC, PERCENTAGE_METRIC], query)}
|
||||
@ -157,45 +162,44 @@ function iconFor(screenSize) {
|
||||
}
|
||||
}
|
||||
|
||||
export default class Devices extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.tabKey = `deviceTab__${props.site.domain}`
|
||||
const storedTab = storage.getItem(this.tabKey)
|
||||
this.state = {
|
||||
mode: storedTab || 'browser'
|
||||
}
|
||||
export default function Devices(props) {
|
||||
const {site, query} = props
|
||||
const tabKey = `deviceTab__${site.domain}`
|
||||
const storedTab = storage.getItem(tabKey)
|
||||
const [mode, setMode] = useState(storedTab || 'browser')
|
||||
const [importedQueryUnsupported, setImportedQueryUnsupported] = useState(false)
|
||||
|
||||
function switchTab(mode) {
|
||||
storage.setItem(tabKey, mode)
|
||||
setMode(mode)
|
||||
}
|
||||
|
||||
setMode(mode) {
|
||||
return () => {
|
||||
storage.setItem(this.tabKey, mode)
|
||||
this.setState({ mode })
|
||||
}
|
||||
function afterFetchData(apiResponse) {
|
||||
const unsupportedQuery = apiResponse.skip_imported_reason === 'unsupported_query'
|
||||
const isRealtime = query.period === 'realtime'
|
||||
setImportedQueryUnsupported(unsupportedQuery && !isRealtime)
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
switch (this.state.mode) {
|
||||
function renderContent() {
|
||||
switch (mode) {
|
||||
case 'browser':
|
||||
if (isFilteringOnFixedValue(this.props.query, 'browser')) {
|
||||
return <BrowserVersions site={this.props.site} query={this.props.query} />
|
||||
if (isFilteringOnFixedValue(query, 'browser')) {
|
||||
return <BrowserVersions site={site} query={query} afterFetchData={afterFetchData} />
|
||||
}
|
||||
return <Browsers site={this.props.site} query={this.props.query} />
|
||||
return <Browsers site={site} query={query} afterFetchData={afterFetchData} />
|
||||
case 'os':
|
||||
if (isFilteringOnFixedValue(this.props.query, 'os')) {
|
||||
return <OperatingSystemVersions site={this.props.site} query={this.props.query} />
|
||||
if (isFilteringOnFixedValue(query, 'os')) {
|
||||
return <OperatingSystemVersions site={site} query={query} afterFetchData={afterFetchData} />
|
||||
}
|
||||
return <OperatingSystems site={this.props.site} query={this.props.query} />
|
||||
return <OperatingSystems site={site} query={query} afterFetchData={afterFetchData} />
|
||||
case 'size':
|
||||
default:
|
||||
return (
|
||||
<ScreenSizes site={this.props.site} query={this.props.query} />
|
||||
)
|
||||
return <ScreenSizes site={site} query={query} afterFetchData={afterFetchData} />
|
||||
}
|
||||
}
|
||||
|
||||
renderPill(name, mode) {
|
||||
const isActive = this.state.mode === mode
|
||||
function renderPill(name, pill) {
|
||||
const isActive = mode === pill
|
||||
|
||||
if (isActive) {
|
||||
return (
|
||||
@ -210,28 +214,29 @@ export default class Devices extends React.Component {
|
||||
return (
|
||||
<button
|
||||
className="cursor-pointer hover:text-indigo-600"
|
||||
onClick={this.setMode(mode)}
|
||||
onClick={() => switchTab(pill)}
|
||||
>
|
||||
{name}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between w-full">
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between w-full">
|
||||
<div className="flex gap-x-1">
|
||||
<h3 className="font-bold dark:text-gray-100">Devices</h3>
|
||||
<div className="flex text-xs font-medium text-gray-500 dark:text-gray-400 space-x-2">
|
||||
{this.renderPill('Browser', 'browser')}
|
||||
{this.renderPill('OS', 'os')}
|
||||
{this.renderPill('Size', 'size')}
|
||||
</div>
|
||||
<ImportedQueryUnsupportedWarning condition={importedQueryUnsupported}/>
|
||||
</div>
|
||||
<div className="flex text-xs font-medium text-gray-500 dark:text-gray-400 space-x-2">
|
||||
{renderPill('Browser', 'browser')}
|
||||
{renderPill('OS', 'os')}
|
||||
{renderPill('Size', 'size')}
|
||||
</div>
|
||||
{this.renderContent()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{renderContent()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function getSingleFilter(query, filterKey) {
|
||||
|
@ -81,6 +81,9 @@ export default function VisitorGraph(props) {
|
||||
function fetchTopStatsAndGraphData() {
|
||||
fetchTopStats(site, query)
|
||||
.then((res) => {
|
||||
if (props.updateImportedDataInView) {
|
||||
props.updateImportedDataInView(res.includes_imported)
|
||||
}
|
||||
setTopStatData(res)
|
||||
setTopStatsLoading(false)
|
||||
})
|
||||
|
@ -22,7 +22,7 @@ export default function WithImportedSwitch({site, topStatData}) {
|
||||
const isComparingImportedPeriod = isBeforeNativeStats(topStatData.comparing_from)
|
||||
|
||||
if (isQueryingImportedPeriod || isComparingImportedPeriod) {
|
||||
const withImported = topStatData.with_imported;
|
||||
const withImported = topStatData.includes_imported;
|
||||
const toggleColor = withImported ? " dark:text-gray-300 text-gray-700" : " dark:text-gray-500 text-gray-400"
|
||||
const target = url.setQuery('with_imported', (!withImported).toString())
|
||||
const tip = withImported ? "" : "do not ";
|
||||
|
@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
import { ExclamationCircleIcon } from '@heroicons/react/24/outline'
|
||||
|
||||
export default function ImportedQueryUnsupportedWarning({condition, message}) {
|
||||
const tooltipMessage = message || "Imported data is excluded due to applied filters"
|
||||
|
||||
if (condition) {
|
||||
return (
|
||||
<span tooltip={tooltipMessage}>
|
||||
<ExclamationCircleIcon className="w-6 h-6 dark:text-gray-100" />
|
||||
</span>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
@ -8,8 +8,9 @@ import {apiPath, sitePath} from '../../util/url'
|
||||
import ListReport from '../reports/list'
|
||||
import { VISITORS_METRIC, maybeWithCR } from '../reports/metrics';
|
||||
import { getFiltersByKeyPrefix } from '../../util/filters';
|
||||
import ImportedQueryUnsupportedWarning from '../imported-query-unsupported-warning';
|
||||
|
||||
function Countries({query, site, onClick}) {
|
||||
function Countries({query, site, onClick, afterFetchData}) {
|
||||
function fetchData() {
|
||||
return api.get(apiPath(site, '/countries'), query, { limit: 9 })
|
||||
}
|
||||
@ -29,6 +30,7 @@ function Countries({query, site, onClick}) {
|
||||
return (
|
||||
<ListReport
|
||||
fetchData={fetchData}
|
||||
afterFetchData={afterFetchData}
|
||||
getFilterFor={getFilterFor}
|
||||
onClick={onClick}
|
||||
keyLabel="Country"
|
||||
@ -41,7 +43,7 @@ function Countries({query, site, onClick}) {
|
||||
)
|
||||
}
|
||||
|
||||
function Regions({query, site, onClick}) {
|
||||
function Regions({query, site, onClick, afterFetchData}) {
|
||||
function fetchData() {
|
||||
return api.get(apiPath(site, '/regions'), query, {limit: 9})
|
||||
}
|
||||
@ -61,6 +63,7 @@ function Regions({query, site, onClick}) {
|
||||
return (
|
||||
<ListReport
|
||||
fetchData={fetchData}
|
||||
afterFetchData={afterFetchData}
|
||||
getFilterFor={getFilterFor}
|
||||
onClick={onClick}
|
||||
keyLabel="Region"
|
||||
@ -73,7 +76,7 @@ function Regions({query, site, onClick}) {
|
||||
)
|
||||
}
|
||||
|
||||
function Cities({query, site}) {
|
||||
function Cities({query, site, afterFetchData}) {
|
||||
function fetchData() {
|
||||
return api.get(apiPath(site, '/cities'), query, {limit: 9})
|
||||
}
|
||||
@ -93,6 +96,7 @@ function Cities({query, site}) {
|
||||
return (
|
||||
<ListReport
|
||||
fetchData={fetchData}
|
||||
afterFetchData={afterFetchData}
|
||||
getFilterFor={getFilterFor}
|
||||
keyLabel="City"
|
||||
metrics={maybeWithCR([VISITORS_METRIC], query)}
|
||||
@ -116,10 +120,12 @@ export default class Locations extends React.Component {
|
||||
super(props)
|
||||
this.onCountryFilter = this.onCountryFilter.bind(this)
|
||||
this.onRegionFilter = this.onRegionFilter.bind(this)
|
||||
this.afterFetchData = this.afterFetchData.bind(this)
|
||||
this.tabKey = `geoTab__${ props.site.domain}`
|
||||
const storedTab = storage.getItem(this.tabKey)
|
||||
this.state = {
|
||||
mode: storedTab || 'map'
|
||||
mode: storedTab || 'map',
|
||||
importedQueryUnsupported: false
|
||||
}
|
||||
}
|
||||
|
||||
@ -156,17 +162,23 @@ export default class Locations extends React.Component {
|
||||
this.setMode('cities')()
|
||||
}
|
||||
|
||||
afterFetchData(apiResponse) {
|
||||
const unsupportedQuery = apiResponse.skip_imported_reason === 'unsupported_query'
|
||||
const isRealtime = this.props.query.period === 'realtime'
|
||||
this.setState({importedQueryUnsupported: unsupportedQuery && !isRealtime})
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
switch(this.state.mode) {
|
||||
case "cities":
|
||||
return <Cities site={this.props.site} query={this.props.query} />
|
||||
return <Cities site={this.props.site} query={this.props.query} afterFetchData={this.afterFetchData} />
|
||||
case "regions":
|
||||
return <Regions onClick={this.onRegionFilter} site={this.props.site} query={this.props.query} />
|
||||
return <Regions onClick={this.onRegionFilter} site={this.props.site} query={this.props.query} afterFetchData={this.afterFetchData} />
|
||||
case "countries":
|
||||
return <Countries onClick={this.onCountryFilter('countries')} site={this.props.site} query={this.props.query} />
|
||||
return <Countries onClick={this.onCountryFilter('countries')} site={this.props.site} query={this.props.query} afterFetchData={this.afterFetchData} />
|
||||
case "map":
|
||||
default:
|
||||
return <CountriesMap onClick={this.onCountryFilter('map')} site={this.props.site} query={this.props.query}/>
|
||||
return <CountriesMap onClick={this.onCountryFilter('map')} site={this.props.site} query={this.props.query} afterFetchData={this.afterFetchData} />
|
||||
}
|
||||
}
|
||||
|
||||
@ -197,9 +209,12 @@ export default class Locations extends React.Component {
|
||||
return (
|
||||
<div>
|
||||
<div className="w-full flex justify-between">
|
||||
<h3 className="font-bold dark:text-gray-100">
|
||||
{labelFor[this.state.mode] || 'Locations'}
|
||||
</h3>
|
||||
<div className="flex gap-x-1">
|
||||
<h3 className="font-bold dark:text-gray-100">
|
||||
{labelFor[this.state.mode] || 'Locations'}
|
||||
</h3>
|
||||
<ImportedQueryUnsupportedWarning condition={this.state.importedQueryUnsupported} />
|
||||
</div>
|
||||
<div className="flex text-xs font-medium text-gray-500 dark:text-gray-400 space-x-2">
|
||||
{ this.renderPill('Map', 'map') }
|
||||
{ this.renderPill('Countries', 'countries') }
|
||||
|
@ -76,8 +76,11 @@ class Countries extends React.Component {
|
||||
fetchCountries() {
|
||||
return api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/countries`, this.props.query, {limit: 300})
|
||||
.then((response) => {
|
||||
const results = response.results ? response.results : response
|
||||
this.setState({loading: false, countries: results})
|
||||
if (this.props.afterFetchData) {
|
||||
this.props.afterFetchData(response)
|
||||
}
|
||||
|
||||
this.setState({loading: false, countries: response.results})
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -37,11 +37,10 @@ function ConversionsModal(props) {
|
||||
function fetchData() {
|
||||
api.get(url.apiPath(site, `/conversions`), query, { limit: 100, page })
|
||||
.then((response) => {
|
||||
const results = response.results ? response.results : response
|
||||
setLoading(false)
|
||||
setList(list.concat(results))
|
||||
setList(list.concat(response.results))
|
||||
setPage(page + 1)
|
||||
setMoreResultsAvailable(results.length >= 100)
|
||||
setMoreResultsAvailable(response.results.length >= 100)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -33,15 +33,13 @@ class EntryPagesModal extends React.Component {
|
||||
query,
|
||||
{ limit: 100, page }
|
||||
)
|
||||
.then((response) => {
|
||||
const results = response.results ? response.results : response
|
||||
|
||||
this.setState((state) => ({
|
||||
.then(
|
||||
(response) => this.setState((state) => ({
|
||||
loading: false,
|
||||
pages: state.pages.concat(results),
|
||||
moreResultsAvailable: results.length === 100
|
||||
pages: state.pages.concat(response.results),
|
||||
moreResultsAvailable: response.results.length === 100
|
||||
}))
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
|
@ -28,10 +28,7 @@ class ExitPagesModal extends React.Component {
|
||||
const { query, page } = this.state;
|
||||
|
||||
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/exit-pages`, query, { limit: 100, page })
|
||||
.then((response) => {
|
||||
const results = response.results ? response.results : response
|
||||
this.setState((state) => ({ loading: false, pages: state.pages.concat(results), moreResultsAvailable: results.length === 100 }))
|
||||
})
|
||||
.then((response) => this.setState((state) => ({ loading: false, pages: state.pages.concat(response.results), moreResultsAvailable: response.results.length === 100 })))
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
|
@ -30,10 +30,7 @@ class PagesModal extends React.Component {
|
||||
const { query, page } = this.state;
|
||||
|
||||
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/pages`, query, { limit: 100, page, detailed })
|
||||
.then((response) => {
|
||||
const results = response.results ? response.results : response
|
||||
this.setState((state) => ({ loading: false, pages: state.pages.concat(results), moreResultsAvailable: results.length === 100 }))
|
||||
})
|
||||
.then((response) => this.setState((state) => ({ loading: false, pages: state.pages.concat(response.results), moreResultsAvailable: response.results.length === 100 })))
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
|
@ -39,12 +39,10 @@ function PropsModal(props) {
|
||||
function fetchData() {
|
||||
api.get(url.apiPath(site, `/custom-prop-values/${propKey}`), query, { limit: 100, page })
|
||||
.then((response) => {
|
||||
const results = response.results ? response.results : response
|
||||
|
||||
setLoading(false)
|
||||
setList(list.concat(results))
|
||||
setList(list.concat(response.results))
|
||||
setPage(page + 1)
|
||||
setMoreResultsAvailable(results.length >= 100)
|
||||
setMoreResultsAvailable(response.results.length >= 100)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -21,10 +21,7 @@ class ReferrerDrilldownModal extends React.Component {
|
||||
const detailed = this.showExtra()
|
||||
|
||||
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/referrers/${this.props.match.params.referrer}`, this.state.query, {limit: 100, detailed})
|
||||
.then((response) => {
|
||||
const results = response.results ? response.results : response
|
||||
this.setState({loading: false, referrers: results})
|
||||
})
|
||||
.then((response) => this.setState({loading: false, referrers: response.results}))
|
||||
}
|
||||
|
||||
showExtra() {
|
||||
|
@ -35,10 +35,7 @@ class SourcesModal extends React.Component {
|
||||
|
||||
const detailed = this.showExtra()
|
||||
api.get(`/api/stats/${encodeURIComponent(site.domain)}/${this.currentView()}`, query, { limit: 100, page, detailed })
|
||||
.then((response) => {
|
||||
const results = response.results ? response.results : response
|
||||
this.setState({ loading: false, sources: sources.concat(results), moreResultsAvailable: results.length === 100 })
|
||||
})
|
||||
.then((response) => this.setState({ loading: false, sources: sources.concat(response.results), moreResultsAvailable: response.results.length === 100 }))
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -19,10 +19,7 @@ class ModalTable extends React.Component {
|
||||
|
||||
componentDidMount() {
|
||||
api.get(this.props.endpoint, this.state.query, {limit: 100})
|
||||
.then((response) => {
|
||||
const results = response.results ? response.results : response
|
||||
this.setState({loading: false, list: results})
|
||||
})
|
||||
.then((response) => this.setState({loading: false, list: response.results}))
|
||||
}
|
||||
|
||||
showConversionRate() {
|
||||
|
@ -1,12 +1,13 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import * as storage from '../../util/storage'
|
||||
import * as url from '../../util/url'
|
||||
import * as api from '../../api'
|
||||
import ListReport from './../reports/list'
|
||||
import { VISITORS_METRIC, maybeWithCR } from './../reports/metrics';
|
||||
import ImportedQueryUnsupportedWarning from '../imported-query-unsupported-warning';
|
||||
|
||||
function EntryPages({ query, site }) {
|
||||
function EntryPages({ query, site, afterFetchData }) {
|
||||
function fetchData() {
|
||||
return api.get(url.apiPath(site, '/entry-pages'), query, { limit: 9 })
|
||||
}
|
||||
@ -25,6 +26,7 @@ function EntryPages({ query, site }) {
|
||||
return (
|
||||
<ListReport
|
||||
fetchData={fetchData}
|
||||
afterFetchData={afterFetchData}
|
||||
getFilterFor={getFilterFor}
|
||||
keyLabel="Entry page"
|
||||
metrics={maybeWithCR([{ ...VISITORS_METRIC, label: 'Unique Entrances' }], query)}
|
||||
@ -36,7 +38,7 @@ function EntryPages({ query, site }) {
|
||||
)
|
||||
}
|
||||
|
||||
function ExitPages({ query, site }) {
|
||||
function ExitPages({ query, site, afterFetchData }) {
|
||||
function fetchData() {
|
||||
return api.get(url.apiPath(site, '/exit-pages'), query, { limit: 9 })
|
||||
}
|
||||
@ -55,6 +57,7 @@ function ExitPages({ query, site }) {
|
||||
return (
|
||||
<ListReport
|
||||
fetchData={fetchData}
|
||||
afterFetchData={afterFetchData}
|
||||
getFilterFor={getFilterFor}
|
||||
keyLabel="Exit page"
|
||||
metrics={maybeWithCR([{ ...VISITORS_METRIC, label: "Unique Exits" }], query)}
|
||||
@ -66,7 +69,7 @@ function ExitPages({ query, site }) {
|
||||
)
|
||||
}
|
||||
|
||||
function TopPages({ query, site }) {
|
||||
function TopPages({ query, site, afterFetchData }) {
|
||||
function fetchData() {
|
||||
return api.get(url.apiPath(site, '/pages'), query, { limit: 9 })
|
||||
}
|
||||
@ -85,6 +88,7 @@ function TopPages({ query, site }) {
|
||||
return (
|
||||
<ListReport
|
||||
fetchData={fetchData}
|
||||
afterFetchData={afterFetchData}
|
||||
getFilterFor={getFilterFor}
|
||||
keyLabel="Page"
|
||||
metrics={maybeWithCR([VISITORS_METRIC], query)}
|
||||
@ -102,38 +106,39 @@ const labelFor = {
|
||||
'exit-pages': 'Exit Pages',
|
||||
}
|
||||
|
||||
export default class Pages extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.tabKey = `pageTab__${props.site.domain}`
|
||||
const storedTab = storage.getItem(this.tabKey)
|
||||
this.state = {
|
||||
mode: storedTab || 'pages'
|
||||
}
|
||||
export default function Pages(props) {
|
||||
const {site, query} = props
|
||||
const tabKey = `pageTab__${site.domain}`
|
||||
const storedTab = storage.getItem(tabKey)
|
||||
const [mode, setMode] = useState(storedTab || 'pages')
|
||||
const [importedQueryUnsupported, setImportedQueryUnsupported] = useState(false)
|
||||
|
||||
function switchTab(mode) {
|
||||
storage.setItem(tabKey, mode)
|
||||
setMode(mode)
|
||||
}
|
||||
|
||||
setMode(mode) {
|
||||
return () => {
|
||||
storage.setItem(this.tabKey, mode)
|
||||
this.setState({ mode })
|
||||
}
|
||||
function afterFetchData(apiResponse) {
|
||||
const unsupportedQuery = apiResponse.skip_imported_reason === 'unsupported_query'
|
||||
const isRealtime = query.period === 'realtime'
|
||||
setImportedQueryUnsupported(unsupportedQuery && !isRealtime)
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
switch (this.state.mode) {
|
||||
function renderContent() {
|
||||
switch (mode) {
|
||||
case "entry-pages":
|
||||
return <EntryPages site={this.props.site} query={this.props.query} />
|
||||
return <EntryPages site={site} query={query} afterFetchData={afterFetchData} />
|
||||
case "exit-pages":
|
||||
return <ExitPages site={this.props.site} query={this.props.query} />
|
||||
return <ExitPages site={site} query={query} afterFetchData={afterFetchData} />
|
||||
case "pages":
|
||||
default:
|
||||
return <TopPages site={this.props.site} query={this.props.query} />
|
||||
return <TopPages site={site} query={query} afterFetchData={afterFetchData} />
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
renderPill(name, mode) {
|
||||
const isActive = this.state.mode === mode
|
||||
function renderPill(name, pill) {
|
||||
const isActive = mode === pill
|
||||
|
||||
if (isActive) {
|
||||
return (
|
||||
@ -148,30 +153,31 @@ export default class Pages extends React.Component {
|
||||
return (
|
||||
<button
|
||||
className="hover:text-indigo-600 cursor-pointer"
|
||||
onClick={this.setMode(mode)}
|
||||
onClick={() => switchTab(pill)}
|
||||
>
|
||||
{name}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{/* Header Container */}
|
||||
<div className="w-full flex justify-between">
|
||||
return (
|
||||
<div>
|
||||
{/* Header Container */}
|
||||
<div className="w-full flex justify-between">
|
||||
<div className="flex gap-x-1">
|
||||
<h3 className="font-bold dark:text-gray-100">
|
||||
{labelFor[this.state.mode] || 'Page Visits'}
|
||||
{labelFor[mode] || 'Page Visits'}
|
||||
</h3>
|
||||
<div className="flex font-medium text-xs text-gray-500 dark:text-gray-400 space-x-2">
|
||||
{this.renderPill('Top Pages', 'pages')}
|
||||
{this.renderPill('Entry Pages', 'entry-pages')}
|
||||
{this.renderPill('Exit Pages', 'exit-pages')}
|
||||
</div>
|
||||
<ImportedQueryUnsupportedWarning condition={importedQueryUnsupported}/>
|
||||
</div>
|
||||
<div className="flex font-medium text-xs text-gray-500 dark:text-gray-400 space-x-2">
|
||||
{renderPill('Top Pages', 'pages')}
|
||||
{renderPill('Entry Pages', 'entry-pages')}
|
||||
{renderPill('Exit Pages', 'exit-pages')}
|
||||
</div>
|
||||
{/* Main Contents */}
|
||||
{this.renderContent()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{/* Main Contents */}
|
||||
{renderContent()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -110,6 +110,12 @@ function ExternalLink({ item, externalLinkDest }) {
|
||||
|
||||
// * `color` - color of the comparison bars in light-mode
|
||||
|
||||
// * `afterFetchData` - a function to be called directly after `fetchData`. Receives the,
|
||||
// raw API response as an argument. The return value is ignored by ListReport. Allows
|
||||
// hooking into the request lifecycle and doing actions with returned metadata. For
|
||||
// example, the parent component might want to control what happens when imported data
|
||||
// is included or not.
|
||||
|
||||
export default function ListReport(props) {
|
||||
const [state, setState] = useState({ loading: true, list: null })
|
||||
const [visible, setVisible] = useState(false)
|
||||
@ -125,8 +131,11 @@ export default function ListReport(props) {
|
||||
}
|
||||
props.fetchData()
|
||||
.then((response) => {
|
||||
const results = response.results ? response.results : response
|
||||
setState({ loading: false, list: results })
|
||||
if (props.afterFetchData) {
|
||||
props.afterFetchData(response)
|
||||
}
|
||||
|
||||
setState({ loading: false, list: response.results })
|
||||
})
|
||||
}, [props.keyLabel, props.query])
|
||||
|
||||
|
@ -1,14 +1,23 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import * as api from '../../api'
|
||||
import * as url from '../../util/url'
|
||||
import { VISITORS_METRIC, maybeWithCR } from '../reports/metrics'
|
||||
import ListReport from '../reports/list'
|
||||
import ImportedQueryUnsupportedWarning from '../../stats/imported-query-unsupported-warning'
|
||||
|
||||
export default function Referrers({source, site, query}) {
|
||||
const [importedQueryUnsupported, setImportedQueryUnsupported] = useState(false)
|
||||
|
||||
function fetchReferrers() {
|
||||
return api.get(url.apiPath(site, `/referrers/${encodeURIComponent(source)}`), query, {limit: 9})
|
||||
}
|
||||
|
||||
function afterFetchReferrers(apiResponse) {
|
||||
const unsupportedQuery = apiResponse.skip_imported_reason === 'unsupported_query'
|
||||
const isRealtime = query.period === 'realtime'
|
||||
setImportedQueryUnsupported(unsupportedQuery && !isRealtime)
|
||||
}
|
||||
|
||||
function externalLinkDest(referrer) {
|
||||
if (referrer.name === 'Direct / None') { return null }
|
||||
return `https://${referrer.name}`
|
||||
@ -35,9 +44,13 @@ export default function Referrers({source, site, query}) {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-grow">
|
||||
<h3 className="font-bold dark:text-gray-100">Top Referrers</h3>
|
||||
<div className="flex gap-x-1">
|
||||
<h3 className="font-bold dark:text-gray-100">Top Referrers</h3>
|
||||
<ImportedQueryUnsupportedWarning condition={importedQueryUnsupported}/>
|
||||
</div>
|
||||
<ListReport
|
||||
fetchData={fetchReferrers}
|
||||
afterFetchData={afterFetchReferrers}
|
||||
getFilterFor={getFilterFor}
|
||||
keyLabel="Referrer"
|
||||
metrics={maybeWithCR([VISITORS_METRIC], query)}
|
||||
|
@ -8,6 +8,7 @@ import { VISITORS_METRIC, maybeWithCR } from '../reports/metrics';
|
||||
import { Menu, Transition } from '@headlessui/react'
|
||||
import { ChevronDownIcon } from '@heroicons/react/20/solid'
|
||||
import classNames from 'classnames'
|
||||
import ImportedQueryUnsupportedWarning from '../imported-query-unsupported-warning';
|
||||
|
||||
const UTM_TAGS = {
|
||||
utm_medium: { label: 'UTM Medium', shortLabel: 'UTM Medium', endpoint: '/utm_mediums' },
|
||||
@ -43,6 +44,7 @@ function AllSources(props) {
|
||||
return (
|
||||
<ListReport
|
||||
fetchData={fetchData}
|
||||
afterFetchData={props.afterFetchData}
|
||||
getFilterFor={getFilterFor}
|
||||
keyLabel="Source"
|
||||
metrics={maybeWithCR([VISITORS_METRIC], query)}
|
||||
@ -72,6 +74,7 @@ function UTMSources(props) {
|
||||
return (
|
||||
<ListReport
|
||||
fetchData={fetchData}
|
||||
afterFetchData={props.afterFetchData}
|
||||
getFilterFor={getFilterFor}
|
||||
keyLabel={utmTag.label}
|
||||
metrics={maybeWithCR([VISITORS_METRIC], query)}
|
||||
@ -87,6 +90,7 @@ export default function SourceList(props) {
|
||||
const tabKey = 'sourceTab__' + props.site.domain
|
||||
const storedTab = storage.getItem(tabKey)
|
||||
const [currentTab, setCurrentTab] = useState(storedTab || 'all')
|
||||
const [importedQueryUnsupported, setImportedQueryUnsupported] = useState(false)
|
||||
|
||||
function setTab(tab) {
|
||||
return () => {
|
||||
@ -152,19 +156,28 @@ export default function SourceList(props) {
|
||||
|
||||
function renderContent() {
|
||||
if (currentTab === 'all') {
|
||||
return <AllSources site={site} query={query} />
|
||||
return <AllSources site={site} query={query} afterFetchData={afterFetchData} />
|
||||
} else {
|
||||
return <UTMSources tab={currentTab} site={site} query={query} />
|
||||
return <UTMSources tab={currentTab} site={site} query={query} afterFetchData={afterFetchData} />
|
||||
}
|
||||
}
|
||||
|
||||
function afterFetchData(apiResponse) {
|
||||
const unsupportedQuery = apiResponse.skip_imported_reason === 'unsupported_query'
|
||||
const isRealtime = query.period === 'realtime'
|
||||
setImportedQueryUnsupported(unsupportedQuery && !isRealtime)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Header Container */}
|
||||
<div className="w-full flex justify-between">
|
||||
<h3 className="font-bold dark:text-gray-100">
|
||||
Top Sources
|
||||
</h3>
|
||||
<div className="flex gap-x-1">
|
||||
<h3 className="font-bold dark:text-gray-100">
|
||||
Top Sources
|
||||
</h3>
|
||||
<ImportedQueryUnsupportedWarning condition={importedQueryUnsupported}/>
|
||||
</div>
|
||||
{renderTabs()}
|
||||
</div>
|
||||
{/* Main Contents */}
|
||||
|
@ -54,6 +54,11 @@ defmodule Plausible.Imported do
|
||||
@max_complete_imports
|
||||
end
|
||||
|
||||
@spec imported_custom_props() :: [String.t()]
|
||||
def imported_custom_props do
|
||||
Plausible.Props.internal_keys()
|
||||
end
|
||||
|
||||
@spec goals_with_url() :: [String.t()]
|
||||
def goals_with_url() do
|
||||
@goals_with_url
|
||||
|
@ -308,8 +308,10 @@ defmodule Plausible.Stats.Base do
|
||||
|
||||
def add_percentage_metric(q, site, query, metrics) do
|
||||
if :percentage in metrics do
|
||||
total_query = Query.set_property(query, nil, skip_refresh_imported_opts: true)
|
||||
|
||||
q
|
||||
|> select_merge(^%{__total_visitors: total_visitors_subquery(site, query)})
|
||||
|> select_merge(^%{__total_visitors: total_visitors_subquery(site, total_query)})
|
||||
|> select_merge(%{
|
||||
percentage:
|
||||
fragment(
|
||||
@ -329,7 +331,10 @@ defmodule Plausible.Stats.Base do
|
||||
# filters.
|
||||
def maybe_add_conversion_rate(q, site, query, metrics) do
|
||||
if :conversion_rate in metrics do
|
||||
total_query = query |> Query.remove_filters(["event:goal", "event:props"])
|
||||
total_query =
|
||||
query
|
||||
|> Query.remove_filters(["event:goal", "event:props"], skip_refresh_imported_opts: true)
|
||||
|> Query.set_property(nil, skip_refresh_imported_opts: true)
|
||||
|
||||
# :TRICKY: Subquery is used due to event:goal breakdown above doing an UNION ALL
|
||||
subquery(q)
|
||||
|
@ -37,11 +37,10 @@ defmodule Plausible.Stats.Breakdown do
|
||||
{event_goals, pageview_goals} = Enum.split_with(site.goals, & &1.event_name)
|
||||
events = Enum.map(event_goals, & &1.event_name)
|
||||
|
||||
event_query = %Query{
|
||||
event_query =
|
||||
query
|
||||
| filters: query.filters ++ [[:member, "event:name", events]],
|
||||
property: "event:name"
|
||||
}
|
||||
|> Query.put_filter([:member, "event:name", events])
|
||||
|> Query.set_property("event:name")
|
||||
|
||||
if !Keyword.get(opts, :skip_tracing), do: Query.trace(query, metrics)
|
||||
|
||||
@ -74,12 +73,14 @@ defmodule Plausible.Stats.Breakdown do
|
||||
|
||||
page_q =
|
||||
if Enum.any?(pageview_goals) do
|
||||
page_query = Query.set_property(query, "event:page")
|
||||
|
||||
page_exprs = Enum.map(pageview_goals, & &1.page_path)
|
||||
page_regexes = Enum.map(page_exprs, &page_regex/1)
|
||||
|
||||
select_columns = metrics_to_select |> select_event_metrics |> mark_revenue_as_nil
|
||||
|
||||
from(e in base_event_query(site, query),
|
||||
from(e in base_event_query(site, page_query),
|
||||
order_by: [desc: fragment("uniq(?)", e.user_id)],
|
||||
where:
|
||||
fragment(
|
||||
@ -94,7 +95,7 @@ defmodule Plausible.Stats.Breakdown do
|
||||
}
|
||||
)
|
||||
|> select_merge(^select_columns)
|
||||
|> merge_imported_pageview_goals(site, query, page_exprs, metrics_to_select)
|
||||
|> merge_imported_pageview_goals(site, page_query, page_exprs, metrics_to_select)
|
||||
|> apply_pagination(pagination)
|
||||
else
|
||||
nil
|
||||
@ -180,8 +181,9 @@ defmodule Plausible.Stats.Breakdown do
|
||||
|
||||
pages ->
|
||||
query
|
||||
|> Query.remove_filters(["event:page"])
|
||||
|> Query.put_filter([:member, "visit:entry_page", Enum.map(pages, & &1[:page])])
|
||||
|> struct!(property: "visit:entry_page")
|
||||
|> Query.set_property("visit:entry_page")
|
||||
end
|
||||
|
||||
if Enum.any?(event_metrics) && Enum.empty?(event_result) do
|
||||
@ -244,24 +246,25 @@ defmodule Plausible.Stats.Breakdown do
|
||||
"visit:entry_page",
|
||||
"visit:referrer"
|
||||
] do
|
||||
update_hostname(query, "visit:entry_page_hostname")
|
||||
update_hostname_filter_prop(query, "visit:entry_page_hostname")
|
||||
end
|
||||
|
||||
defp maybe_update_breakdown_filters(%Query{property: "visit:exit_page"} = query) do
|
||||
update_hostname(query, "visit:exit_page_hostname")
|
||||
update_hostname_filter_prop(query, "visit:exit_page_hostname")
|
||||
end
|
||||
|
||||
defp maybe_update_breakdown_filters(query) do
|
||||
query
|
||||
end
|
||||
|
||||
defp update_hostname(query, visit_prop) do
|
||||
defp update_hostname_filter_prop(query, visit_prop) do
|
||||
case Query.get_filter(query, "event:hostname") do
|
||||
nil ->
|
||||
query
|
||||
|
||||
[op, "event:hostname", value] ->
|
||||
Plausible.Stats.Query.put_filter(query, [op, visit_prop, value])
|
||||
query
|
||||
|> Query.put_filter([op, visit_prop, value])
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -63,7 +63,7 @@ defmodule Plausible.Stats.Comparisons do
|
||||
|
||||
with :ok <- validate_mode(source_query, mode),
|
||||
{:ok, comparison_query} <- do_compare(source_query, mode, opts),
|
||||
comparison_query <- maybe_include_imported(comparison_query, source_query, site),
|
||||
comparison_query <- maybe_include_imported(comparison_query, source_query),
|
||||
do: {:ok, comparison_query}
|
||||
end
|
||||
|
||||
@ -162,12 +162,10 @@ defmodule Plausible.Stats.Comparisons do
|
||||
Date.add(date, -days_to_subtract)
|
||||
end
|
||||
|
||||
defp maybe_include_imported(query, %Query{imported_data_requested: false}, _site) do
|
||||
%Stats.Query{query | include_imported: false}
|
||||
end
|
||||
defp maybe_include_imported(query, source_query) do
|
||||
requested? = source_query.imported_data_requested
|
||||
|
||||
defp maybe_include_imported(query, _source_query, site) do
|
||||
case Query.ensure_include_imported(query, site) do
|
||||
case Query.ensure_include_imported(query, requested?) do
|
||||
:ok ->
|
||||
struct!(query,
|
||||
imported_data_requested: true,
|
||||
@ -176,7 +174,7 @@ defmodule Plausible.Stats.Comparisons do
|
||||
|
||||
{:error, reason} ->
|
||||
struct!(query,
|
||||
imported_data_requested: true,
|
||||
imported_data_requested: requested?,
|
||||
include_imported: false,
|
||||
skip_imported_reason: reason
|
||||
)
|
||||
|
@ -40,7 +40,7 @@ defmodule Plausible.Stats.EmailReport do
|
||||
end
|
||||
|
||||
defp put_top_5_pages(stats, site, query) do
|
||||
query = struct!(query, property: "event:page")
|
||||
query = Query.set_property(query, "event:page")
|
||||
pages = Stats.breakdown(site, query, [:visitors], {5, 1})
|
||||
Map.put(stats, :pages, pages)
|
||||
end
|
||||
@ -49,7 +49,7 @@ defmodule Plausible.Stats.EmailReport do
|
||||
query =
|
||||
query
|
||||
|> Query.put_filter([:is_not, "visit:source", "Direct / None"])
|
||||
|> struct!(property: "visit:source")
|
||||
|> Query.set_property("visit:source")
|
||||
|
||||
sources = Stats.breakdown(site, query, [:visitors], {5, 1})
|
||||
|
||||
|
@ -2,9 +2,12 @@ defmodule Plausible.Stats.FilterSuggestions do
|
||||
use Plausible.Repo
|
||||
use Plausible.ClickhouseRepo
|
||||
use Plausible.Stats.Fragments
|
||||
|
||||
import Plausible.Stats.Base
|
||||
import Ecto.Query
|
||||
|
||||
alias Plausible.Stats.Query
|
||||
alias Plausible.Stats.Imported
|
||||
|
||||
def filter_suggestions(site, query, "country", filter_search) do
|
||||
matches = Location.search_country(filter_search)
|
||||
@ -16,6 +19,7 @@ defmodule Plausible.Stats.FilterSuggestions do
|
||||
order_by: [desc: fragment("count(*)")],
|
||||
select: e.country_code
|
||||
)
|
||||
|> Imported.merge_imported_country_suggestions(site, query)
|
||||
|
||||
ClickhouseRepo.all(q)
|
||||
|> Enum.map(fn c -> Enum.find(matches, fn x -> x.alpha_2 == c end) end)
|
||||
@ -35,33 +39,60 @@ defmodule Plausible.Stats.FilterSuggestions do
|
||||
group_by: e.subdivision1_code,
|
||||
order_by: [desc: fragment("count(*)")],
|
||||
select: e.subdivision1_code,
|
||||
where: e.subdivision1_code != "",
|
||||
limit: 24
|
||||
where: e.subdivision1_code != ""
|
||||
)
|
||||
|> Imported.merge_imported_region_suggestions(site, query)
|
||||
|> limit(24)
|
||||
|> ClickhouseRepo.all()
|
||||
|> Enum.map(fn c ->
|
||||
subdiv = Location.get_subdivision(c)
|
||||
|
||||
%{
|
||||
value: c,
|
||||
label: subdiv.name
|
||||
}
|
||||
if subdiv do
|
||||
%{
|
||||
value: c,
|
||||
label: subdiv.name
|
||||
}
|
||||
else
|
||||
%{
|
||||
value: c,
|
||||
label: c
|
||||
}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
def filter_suggestions(site, query, "region", filter_search) do
|
||||
matches = Location.search_subdivision(filter_search)
|
||||
filter_search = String.downcase(filter_search)
|
||||
|
||||
q =
|
||||
from(
|
||||
e in query_sessions(site, query),
|
||||
group_by: e.subdivision1_code,
|
||||
order_by: [desc: fragment("count(*)")],
|
||||
select: e.subdivision1_code
|
||||
select: e.subdivision1_code,
|
||||
where: e.subdivision1_code != ""
|
||||
)
|
||||
|> Imported.merge_imported_region_suggestions(site, query)
|
||||
|
||||
ClickhouseRepo.all(q)
|
||||
|> Enum.map(fn c -> Enum.find(matches, fn x -> x.code == c end) end)
|
||||
|> Enum.map(fn c ->
|
||||
match = Enum.find(matches, fn x -> x.code == c end)
|
||||
|
||||
cond do
|
||||
match ->
|
||||
match
|
||||
|
||||
String.contains?(String.downcase(c), filter_search) ->
|
||||
%{
|
||||
code: c,
|
||||
name: c
|
||||
}
|
||||
|
||||
true ->
|
||||
nil
|
||||
end
|
||||
end)
|
||||
|> Enum.filter(& &1)
|
||||
|> Enum.slice(0..24)
|
||||
|> Enum.map(fn subdiv ->
|
||||
@ -78,9 +109,10 @@ defmodule Plausible.Stats.FilterSuggestions do
|
||||
group_by: e.city_geoname_id,
|
||||
order_by: [desc: fragment("count(*)")],
|
||||
select: e.city_geoname_id,
|
||||
where: e.city_geoname_id != 0,
|
||||
limit: 24
|
||||
where: e.city_geoname_id != 0
|
||||
)
|
||||
|> Imported.merge_imported_city_suggestions(site, query)
|
||||
|> limit(24)
|
||||
|> ClickhouseRepo.all()
|
||||
|> Enum.map(fn c ->
|
||||
city = Location.get_city(c)
|
||||
@ -101,9 +133,10 @@ defmodule Plausible.Stats.FilterSuggestions do
|
||||
group_by: e.city_geoname_id,
|
||||
order_by: [desc: fragment("count(*)")],
|
||||
select: e.city_geoname_id,
|
||||
where: e.city_geoname_id != 0,
|
||||
limit: 5000
|
||||
where: e.city_geoname_id != 0
|
||||
)
|
||||
|> Imported.merge_imported_city_suggestions(site, query)
|
||||
|> limit(5000)
|
||||
|
||||
ClickhouseRepo.all(q)
|
||||
|> Enum.map(fn c -> Location.get_city(c) end)
|
||||
@ -223,10 +256,16 @@ defmodule Plausible.Stats.FilterSuggestions do
|
||||
where: fragment("? ilike ?", field(e, ^filter_name), ^filter_query),
|
||||
select: field(e, ^filter_name),
|
||||
group_by: ^filter_name,
|
||||
order_by: [desc: fragment("count(*)")],
|
||||
limit: 25
|
||||
order_by: [desc: fragment("count(*)")]
|
||||
)
|
||||
|> apply_additional_filters(filter_name, site)
|
||||
|> Imported.merge_imported_filter_suggestions(
|
||||
site,
|
||||
query,
|
||||
filter_name,
|
||||
filter_query
|
||||
)
|
||||
|> limit(25)
|
||||
|> ClickhouseRepo.all()
|
||||
|> Enum.filter(fn suggestion -> suggestion != "" end)
|
||||
|> wrap_suggestions()
|
||||
|
191
lib/plausible/stats/imported/base.ex
Normal file
191
lib/plausible/stats/imported/base.ex
Normal file
@ -0,0 +1,191 @@
|
||||
defmodule Plausible.Stats.Imported.Base do
|
||||
@moduledoc """
|
||||
A module for building the base of an imported stats query
|
||||
"""
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
alias Plausible.Imported
|
||||
alias Plausible.Stats.Filters
|
||||
alias Plausible.Stats.Query
|
||||
|
||||
@property_to_table_mappings %{
|
||||
"visit:source" => "imported_sources",
|
||||
"visit:referrer" => "imported_sources",
|
||||
"visit:utm_source" => "imported_sources",
|
||||
"visit:utm_medium" => "imported_sources",
|
||||
"visit:utm_campaign" => "imported_sources",
|
||||
"visit:utm_term" => "imported_sources",
|
||||
"visit:utm_content" => "imported_sources",
|
||||
"visit:entry_page" => "imported_entry_pages",
|
||||
"visit:exit_page" => "imported_exit_pages",
|
||||
"visit:country" => "imported_locations",
|
||||
"visit:region" => "imported_locations",
|
||||
"visit:city" => "imported_locations",
|
||||
"visit:device" => "imported_devices",
|
||||
"visit:browser" => "imported_browsers",
|
||||
"visit:browser_version" => "imported_browsers",
|
||||
"visit:os" => "imported_operating_systems",
|
||||
"visit:os_version" => "imported_operating_systems",
|
||||
"event:page" => "imported_pages",
|
||||
"event:name" => "imported_custom_events",
|
||||
"event:props:url" => "imported_custom_events",
|
||||
"event:props:path" => "imported_custom_events",
|
||||
|
||||
# NOTE: these properties can be only filtered by
|
||||
"visit:screen" => "imported_devices",
|
||||
"event:hostname" => "imported_pages"
|
||||
}
|
||||
|
||||
@imported_custom_props Imported.imported_custom_props()
|
||||
|
||||
@db_field_mappings %{
|
||||
referrer_source: :source,
|
||||
screen_size: :device,
|
||||
screen: :device,
|
||||
os: :operating_system,
|
||||
os_version: :operating_system_version,
|
||||
country_code: :country,
|
||||
subdivision1_code: :region,
|
||||
city_geoname_id: :city,
|
||||
entry_page_hostname: :hostname,
|
||||
pathname: :page,
|
||||
url: :link_url
|
||||
}
|
||||
|
||||
def property_to_table_mappings(), do: @property_to_table_mappings
|
||||
|
||||
def query_imported(site, query) do
|
||||
query
|
||||
|> transform_filters()
|
||||
|> decide_table()
|
||||
|> query_imported(site, query)
|
||||
end
|
||||
|
||||
def query_imported(table, site, query) do
|
||||
query = transform_filters(query)
|
||||
import_ids = site.complete_import_ids
|
||||
%{first: date_from, last: date_to} = query.date_range
|
||||
|
||||
from(i in table,
|
||||
where: i.site_id == ^site.id,
|
||||
where: i.import_id in ^import_ids,
|
||||
where: i.date >= ^date_from,
|
||||
where: i.date <= ^date_to,
|
||||
select: %{}
|
||||
)
|
||||
|> apply_filter(query)
|
||||
end
|
||||
|
||||
def decide_table(query) do
|
||||
query
|
||||
|> transform_filters()
|
||||
|> do_decide_table()
|
||||
end
|
||||
|
||||
defp transform_filters(query) do
|
||||
new_filters =
|
||||
query.filters
|
||||
|> Enum.reject(fn
|
||||
[:is, "event:name", "pageview"] -> true
|
||||
_ -> false
|
||||
end)
|
||||
|> Enum.flat_map(fn filter ->
|
||||
case filter do
|
||||
[op, "event:goal", {:event, name}] ->
|
||||
[[op, "event:name", name]]
|
||||
|
||||
[op, "event:goal", {:page, page}] ->
|
||||
[[op, "event:page", page]]
|
||||
|
||||
[op, "event:goal", events] ->
|
||||
events
|
||||
|> Enum.group_by(&elem(&1, 0), &elem(&1, 1))
|
||||
|> Enum.map(fn
|
||||
{:event, names} -> [op, "event:name", names]
|
||||
{:page, pages} -> [op, "event:page", pages]
|
||||
end)
|
||||
|
||||
filter ->
|
||||
[filter]
|
||||
end
|
||||
end)
|
||||
|
||||
struct!(query, filters: new_filters)
|
||||
end
|
||||
|
||||
defp do_decide_table(%Query{filters: [], property: nil}), do: "imported_visitors"
|
||||
|
||||
defp do_decide_table(%Query{filters: filters, property: "event:props:" <> prop_key = property})
|
||||
when prop_key in @imported_custom_props do
|
||||
has_required_name_filter? =
|
||||
Enum.any?(filters, fn
|
||||
[:is, "event:name", name] -> name in special_goals_for(prop_key)
|
||||
_ -> false
|
||||
end)
|
||||
|
||||
has_unsupported_filters? =
|
||||
Enum.any?(filters, fn [_, filtered_prop | _] ->
|
||||
filtered_prop not in [property, "event:name"]
|
||||
end)
|
||||
|
||||
if has_required_name_filter? && not has_unsupported_filters? do
|
||||
"imported_custom_events"
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defp do_decide_table(%Query{filters: [], property: "event:goal"}) do
|
||||
"imported_custom_events"
|
||||
end
|
||||
|
||||
defp do_decide_table(%Query{filters: [], property: property}) do
|
||||
@property_to_table_mappings[property]
|
||||
end
|
||||
|
||||
defp do_decide_table(%Query{filters: filters, property: "event:goal"}) do
|
||||
filter_props = Enum.map(filters, &Enum.at(&1, 1))
|
||||
|
||||
any_event_name_filters? = "event:name" in filter_props
|
||||
any_page_filters? = "event:page" in filter_props
|
||||
any_other_filters? = Enum.any?(filter_props, &(&1 not in ["event:page", "event:name"]))
|
||||
|
||||
cond do
|
||||
any_other_filters? -> nil
|
||||
any_event_name_filters? and not any_page_filters? -> "imported_custom_events"
|
||||
any_page_filters? and not any_event_name_filters? -> "imported_pages"
|
||||
true -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp do_decide_table(%Query{filters: filters, property: property}) do
|
||||
table_candidates =
|
||||
filters
|
||||
|> Enum.map(fn [_, prop | _] -> prop end)
|
||||
|> Enum.concat(if property, do: [property], else: [])
|
||||
|> Enum.map(fn
|
||||
"visit:screen" -> "visit:device"
|
||||
prop -> prop
|
||||
end)
|
||||
|> Enum.map(&@property_to_table_mappings[&1])
|
||||
|
||||
case Enum.uniq(table_candidates) do
|
||||
[candidate] -> candidate
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp apply_filter(q, %Query{filters: filters}) do
|
||||
Enum.reduce(filters, q, fn [_, filtered_prop | _] = filter, q ->
|
||||
db_field = Plausible.Stats.Filters.without_prefix(filtered_prop)
|
||||
mapped_db_field = Map.get(@db_field_mappings, db_field, db_field)
|
||||
condition = Filters.WhereBuilder.build_condition(mapped_db_field, filter)
|
||||
|
||||
where(q, ^condition)
|
||||
end)
|
||||
end
|
||||
|
||||
def special_goals_for("url"), do: Imported.goals_with_url()
|
||||
def special_goals_for("path"), do: Imported.goals_with_path()
|
||||
end
|
@ -1,43 +1,29 @@
|
||||
defmodule Plausible.Stats.Imported do
|
||||
use Plausible.ClickhouseRepo
|
||||
alias Plausible.Stats.{Query, Base}
|
||||
|
||||
import Ecto.Query
|
||||
import Plausible.Stats.Fragments
|
||||
|
||||
alias Plausible.Stats.Base
|
||||
alias Plausible.Stats.Imported
|
||||
alias Plausible.Stats.Query
|
||||
|
||||
@no_ref "Direct / None"
|
||||
@not_set "(not set)"
|
||||
@none "(none)"
|
||||
|
||||
@property_to_table_mappings %{
|
||||
"visit:source" => "imported_sources",
|
||||
"visit:referrer" => "imported_sources",
|
||||
"visit:utm_source" => "imported_sources",
|
||||
"visit:utm_medium" => "imported_sources",
|
||||
"visit:utm_campaign" => "imported_sources",
|
||||
"visit:utm_term" => "imported_sources",
|
||||
"visit:utm_content" => "imported_sources",
|
||||
"visit:entry_page" => "imported_entry_pages",
|
||||
"visit:exit_page" => "imported_exit_pages",
|
||||
"visit:country" => "imported_locations",
|
||||
"visit:region" => "imported_locations",
|
||||
"visit:city" => "imported_locations",
|
||||
"visit:device" => "imported_devices",
|
||||
"visit:browser" => "imported_browsers",
|
||||
"visit:browser_version" => "imported_browsers",
|
||||
"visit:os" => "imported_operating_systems",
|
||||
"visit:os_version" => "imported_operating_systems",
|
||||
"event:page" => "imported_pages",
|
||||
"event:name" => "imported_custom_events",
|
||||
"event:props:url" => "imported_custom_events",
|
||||
"event:props:path" => "imported_custom_events"
|
||||
}
|
||||
@property_to_table_mappings Imported.Base.property_to_table_mappings()
|
||||
|
||||
@imported_properties Map.keys(@property_to_table_mappings)
|
||||
|
||||
@goals_with_url Plausible.Imported.goals_with_url()
|
||||
|
||||
def goals_with_url(), do: @goals_with_url
|
||||
|
||||
@goals_with_path Plausible.Imported.goals_with_path()
|
||||
|
||||
def goals_with_path(), do: @goals_with_path
|
||||
|
||||
@doc """
|
||||
Returns a boolean indicating whether the combination of filters and
|
||||
breakdown property is possible to query from the imported tables.
|
||||
@ -49,17 +35,193 @@ defmodule Plausible.Stats.Imported do
|
||||
(see `@goals_with_url` and `@goals_with_path`).
|
||||
"""
|
||||
def schema_supports_query?(query) do
|
||||
filter_count = length(query.filters)
|
||||
not is_nil(Imported.Base.decide_table(query))
|
||||
end
|
||||
|
||||
case {filter_count, query.property} do
|
||||
{0, "event:props:" <> _} -> false
|
||||
{0, _} -> true
|
||||
{1, "event:props:url"} -> has_special_goal_filter?(query, @goals_with_url)
|
||||
{1, "event:props:path"} -> has_special_goal_filter?(query, @goals_with_path)
|
||||
{_, _} -> false
|
||||
def merge_imported_country_suggestions(native_q, _site, %Plausible.Stats.Query{
|
||||
include_imported: false
|
||||
}) do
|
||||
native_q
|
||||
end
|
||||
|
||||
def merge_imported_country_suggestions(native_q, site, query) do
|
||||
supports_filter_set? =
|
||||
Enum.all?(query.filters, fn filter ->
|
||||
[_, filtered_prop | _] = filter
|
||||
@property_to_table_mappings[filtered_prop] == "imported_locations"
|
||||
end)
|
||||
|
||||
if supports_filter_set? do
|
||||
native_q =
|
||||
native_q
|
||||
|> exclude(:order_by)
|
||||
|> exclude(:select)
|
||||
|> select([e], %{country_code: e.country_code, count: fragment("count(*)")})
|
||||
|
||||
imported_q =
|
||||
from i in Imported.Base.query_imported("imported_locations", site, query),
|
||||
group_by: i.country,
|
||||
select_merge: %{country_code: i.country, count: fragment("sum(?)", i.pageviews)}
|
||||
|
||||
from(s in subquery(native_q),
|
||||
full_join: i in subquery(imported_q),
|
||||
on: s.country_code == i.country_code,
|
||||
select:
|
||||
fragment("if(not empty(?), ?, ?)", s.country_code, s.country_code, i.country_code),
|
||||
order_by: [desc: fragment("? + ?", s.count, i.count)]
|
||||
)
|
||||
else
|
||||
native_q
|
||||
end
|
||||
end
|
||||
|
||||
def merge_imported_region_suggestions(native_q, _site, %Plausible.Stats.Query{
|
||||
include_imported: false
|
||||
}) do
|
||||
native_q
|
||||
end
|
||||
|
||||
def merge_imported_region_suggestions(native_q, site, query) do
|
||||
supports_filter_set? =
|
||||
Enum.all?(query.filters, fn filter ->
|
||||
[_, filtered_prop | _] = filter
|
||||
@property_to_table_mappings[filtered_prop] == "imported_locations"
|
||||
end)
|
||||
|
||||
if supports_filter_set? do
|
||||
native_q =
|
||||
native_q
|
||||
|> exclude(:order_by)
|
||||
|> exclude(:select)
|
||||
|> select([e], %{region_code: e.subdivision1_code, count: fragment("count(*)")})
|
||||
|
||||
imported_q =
|
||||
from i in Imported.Base.query_imported("imported_locations", site, query),
|
||||
where: i.region != "",
|
||||
group_by: i.region,
|
||||
select_merge: %{region_code: i.region, count: fragment("sum(?)", i.pageviews)}
|
||||
|
||||
from(s in subquery(native_q),
|
||||
full_join: i in subquery(imported_q),
|
||||
on: s.region_code == i.region_code,
|
||||
select: fragment("if(not empty(?), ?, ?)", s.region_code, s.region_code, i.region_code),
|
||||
order_by: [desc: fragment("? + ?", s.count, i.count)]
|
||||
)
|
||||
else
|
||||
native_q
|
||||
end
|
||||
end
|
||||
|
||||
def merge_imported_city_suggestions(native_q, _site, %Plausible.Stats.Query{
|
||||
include_imported: false
|
||||
}) do
|
||||
native_q
|
||||
end
|
||||
|
||||
def merge_imported_city_suggestions(native_q, site, query) do
|
||||
supports_filter_set? =
|
||||
Enum.all?(query.filters, fn filter ->
|
||||
[_, filtered_prop | _] = filter
|
||||
@property_to_table_mappings[filtered_prop] == "imported_locations"
|
||||
end)
|
||||
|
||||
if supports_filter_set? do
|
||||
native_q =
|
||||
native_q
|
||||
|> exclude(:order_by)
|
||||
|> exclude(:select)
|
||||
|> select([e], %{city_id: e.city_geoname_id, count: fragment("count(*)")})
|
||||
|
||||
imported_q =
|
||||
from i in Imported.Base.query_imported("imported_locations", site, query),
|
||||
where: i.city != 0,
|
||||
group_by: i.city,
|
||||
select_merge: %{city_id: i.city, count: fragment("sum(?)", i.pageviews)}
|
||||
|
||||
from(s in subquery(native_q),
|
||||
full_join: i in subquery(imported_q),
|
||||
on: s.city_id == i.city_id,
|
||||
select: fragment("if(? > 0, ?, ?)", s.city_id, s.city_id, i.city_id),
|
||||
order_by: [desc: fragment("? + ?", s.count, i.count)]
|
||||
)
|
||||
else
|
||||
native_q
|
||||
end
|
||||
end
|
||||
|
||||
def merge_imported_filter_suggestions(
|
||||
native_q,
|
||||
_site,
|
||||
%Plausible.Stats.Query{include_imported: false},
|
||||
_filter_name,
|
||||
_filter_search
|
||||
) do
|
||||
native_q
|
||||
end
|
||||
|
||||
def merge_imported_filter_suggestions(
|
||||
native_q,
|
||||
site,
|
||||
query,
|
||||
filter_name,
|
||||
filter_query
|
||||
) do
|
||||
{table, db_field} = expand_suggestions_field(filter_name)
|
||||
|
||||
supports_filter_set? =
|
||||
Enum.all?(query.filters, fn filter ->
|
||||
[_, filtered_prop | _] = filter
|
||||
@property_to_table_mappings[filtered_prop] == table
|
||||
end)
|
||||
|
||||
if supports_filter_set? do
|
||||
native_q =
|
||||
native_q
|
||||
|> exclude(:order_by)
|
||||
|> exclude(:select)
|
||||
|> select([e], %{name: field(e, ^filter_name), count: fragment("count(*)")})
|
||||
|
||||
imported_q =
|
||||
from i in Imported.Base.query_imported(table, site, query),
|
||||
where: fragment("? ilike ?", field(i, ^db_field), ^filter_query),
|
||||
group_by: field(i, ^db_field),
|
||||
select_merge: %{name: field(i, ^db_field), count: fragment("sum(?)", i.pageviews)}
|
||||
|
||||
from(s in subquery(native_q),
|
||||
full_join: i in subquery(imported_q),
|
||||
on: s.name == i.name,
|
||||
select: fragment("if(not empty(?), ?, ?)", s.name, s.name, i.name),
|
||||
order_by: [desc: fragment("? + ?", s.count, i.count)],
|
||||
limit: 25
|
||||
)
|
||||
else
|
||||
native_q
|
||||
end
|
||||
end
|
||||
|
||||
@filter_suggestions_mapping %{
|
||||
referrer_source: :source,
|
||||
screen_size: :device,
|
||||
pathname: :page
|
||||
}
|
||||
|
||||
defp expand_suggestions_field(filter_name) do
|
||||
db_field = Map.get(@filter_suggestions_mapping, filter_name, filter_name)
|
||||
|
||||
property =
|
||||
case db_field do
|
||||
:operating_system -> :os
|
||||
:operating_system_version -> :os_version
|
||||
other -> other
|
||||
end
|
||||
|
||||
table_by_visit = Map.get(@property_to_table_mappings, "visit:#{property}")
|
||||
table_by_event = Map.get(@property_to_table_mappings, "event:#{property}")
|
||||
table = table_by_visit || table_by_event
|
||||
|
||||
{table, db_field}
|
||||
end
|
||||
|
||||
def merge_imported_timeseries(native_q, _, %Plausible.Stats.Query{include_imported: false}, _),
|
||||
do: native_q
|
||||
|
||||
@ -69,15 +231,9 @@ defmodule Plausible.Stats.Imported do
|
||||
query,
|
||||
metrics
|
||||
) do
|
||||
import_ids = site.complete_import_ids
|
||||
|
||||
imported_q =
|
||||
from(v in "imported_visitors",
|
||||
where: v.site_id == ^site.id,
|
||||
where: v.import_id in ^import_ids,
|
||||
where: v.date >= ^query.date_range.first and v.date <= ^query.date_range.last,
|
||||
select: %{}
|
||||
)
|
||||
site
|
||||
|> Imported.Base.query_imported(query)
|
||||
|> select_imported_metrics(metrics)
|
||||
|> apply_interval(query, site)
|
||||
|
||||
@ -111,19 +267,12 @@ defmodule Plausible.Stats.Imported do
|
||||
|
||||
def merge_imported(q, site, %Query{property: property} = query, metrics)
|
||||
when property in @imported_properties do
|
||||
table = Map.fetch!(@property_to_table_mappings, property)
|
||||
dim = Plausible.Stats.Filters.without_prefix(property)
|
||||
import_ids = site.complete_import_ids
|
||||
|
||||
imported_q =
|
||||
from(
|
||||
i in table,
|
||||
where: i.site_id == ^site.id,
|
||||
where: i.import_id in ^import_ids,
|
||||
where: i.date >= ^query.date_range.first and i.date <= ^query.date_range.last,
|
||||
where: i.visitors > 0,
|
||||
select: %{}
|
||||
)
|
||||
site
|
||||
|> Imported.Base.query_imported(query)
|
||||
|> where([i], i.visitors > 0)
|
||||
|> maybe_apply_filter(query, property, dim)
|
||||
|> group_imported_by(dim)
|
||||
|> select_imported_metrics(metrics)
|
||||
@ -155,7 +304,8 @@ defmodule Plausible.Stats.Imported do
|
||||
|
||||
def merge_imported(q, site, %Query{property: nil} = query, metrics) do
|
||||
imported_q =
|
||||
imported_visitors(site, query)
|
||||
site
|
||||
|> Imported.Base.query_imported(query)
|
||||
|> select_imported_metrics(metrics)
|
||||
|
||||
from(
|
||||
@ -171,71 +321,42 @@ defmodule Plausible.Stats.Imported do
|
||||
def merge_imported_pageview_goals(q, _, %Query{include_imported: false}, _, _), do: q
|
||||
|
||||
def merge_imported_pageview_goals(q, site, query, page_exprs, metrics) do
|
||||
page_regexes = Enum.map(page_exprs, &Base.page_regex/1)
|
||||
if Imported.Base.decide_table(query) == "imported_pages" do
|
||||
page_regexes = Enum.map(page_exprs, &Base.page_regex/1)
|
||||
|
||||
imported_q =
|
||||
from(
|
||||
i in "imported_pages",
|
||||
where: i.site_id == ^site.id,
|
||||
where: i.import_id in ^site.complete_import_ids,
|
||||
where: i.date >= ^query.date_range.first and i.date <= ^query.date_range.last,
|
||||
where: i.visitors > 0,
|
||||
where:
|
||||
fragment(
|
||||
"notEmpty(multiMatchAllIndices(?, ?) as indices)",
|
||||
i.page,
|
||||
^page_regexes
|
||||
),
|
||||
array_join: index in fragment("indices"),
|
||||
group_by: index,
|
||||
select: %{
|
||||
imported_q =
|
||||
"imported_pages"
|
||||
|> Imported.Base.query_imported(site, query)
|
||||
|> where([i], i.visitors > 0)
|
||||
|> where(
|
||||
[i],
|
||||
fragment("notEmpty(multiMatchAllIndices(?, ?) as indices)", i.page, ^page_regexes)
|
||||
)
|
||||
|> join(:array, index in fragment("indices"))
|
||||
|> group_by([_i, index], index)
|
||||
|> select_merge([_i, index], %{
|
||||
name: fragment("concat('Visit ', ?[?])", ^page_exprs, index)
|
||||
}
|
||||
)
|
||||
|> select_imported_metrics(metrics)
|
||||
})
|
||||
|> select_imported_metrics(metrics)
|
||||
|
||||
from(s in Ecto.Query.subquery(q),
|
||||
full_join: i in subquery(imported_q),
|
||||
on: s.name == i.name,
|
||||
select: %{}
|
||||
)
|
||||
|> select_joined_dimension(:name)
|
||||
|> select_joined_metrics(metrics)
|
||||
from(s in Ecto.Query.subquery(q),
|
||||
full_join: i in subquery(imported_q),
|
||||
on: s.name == i.name,
|
||||
select: %{}
|
||||
)
|
||||
|> select_joined_dimension(:name)
|
||||
|> select_joined_metrics(metrics)
|
||||
else
|
||||
q
|
||||
end
|
||||
end
|
||||
|
||||
def total_imported_visitors(site, query) do
|
||||
imported_visitors(site, query)
|
||||
site
|
||||
|> Imported.Base.query_imported(query)
|
||||
|> select_merge([i], %{total_visitors: fragment("sum(?)", i.visitors)})
|
||||
end
|
||||
|
||||
defp imported_visitors(site, query) do
|
||||
import_ids = site.complete_import_ids
|
||||
|
||||
from(
|
||||
i in "imported_visitors",
|
||||
where: i.site_id == ^site.id,
|
||||
where: i.import_id in ^import_ids,
|
||||
where: i.date >= ^query.date_range.first and i.date <= ^query.date_range.last,
|
||||
select: %{}
|
||||
)
|
||||
end
|
||||
|
||||
defp maybe_apply_filter(q, query, "event:props:url", _) do
|
||||
if name = find_special_goal_filter(query, @goals_with_url) do
|
||||
where(q, [i], i.name == ^name)
|
||||
else
|
||||
q
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_apply_filter(q, query, "event:props:path", _) do
|
||||
if name = find_special_goal_filter(query, @goals_with_path) do
|
||||
where(q, [i], i.name == ^name)
|
||||
else
|
||||
q
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_apply_filter(q, query, property, dim) do
|
||||
case Query.get_filter(query, property) do
|
||||
[:member, _, list] -> where(q, [i], field(i, ^dim) in ^list)
|
||||
@ -243,20 +364,6 @@ defmodule Plausible.Stats.Imported do
|
||||
end
|
||||
end
|
||||
|
||||
defp has_special_goal_filter?(query, event_names) do
|
||||
not is_nil(find_special_goal_filter(query, event_names))
|
||||
end
|
||||
|
||||
defp find_special_goal_filter(query, event_names) do
|
||||
case Query.get_filter(query, "event:goal") do
|
||||
[:is, "event:goal", {:event, name}] ->
|
||||
if name in event_names, do: name, else: nil
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defp select_imported_metrics(q, []), do: q
|
||||
|
||||
defp select_imported_metrics(q, [:visitors | rest]) do
|
||||
@ -320,6 +427,18 @@ defmodule Plausible.Stats.Imported do
|
||||
|> select_imported_metrics(rest)
|
||||
end
|
||||
|
||||
defp select_imported_metrics(
|
||||
%Ecto.Query{from: %Ecto.Query.FromExpr{source: {"imported_pages", _}}} = q,
|
||||
[:bounce_rate | rest]
|
||||
) do
|
||||
q
|
||||
|> select_merge([i], %{
|
||||
bounces: 0,
|
||||
__internal_visits: 0
|
||||
})
|
||||
|> select_imported_metrics(rest)
|
||||
end
|
||||
|
||||
defp select_imported_metrics(
|
||||
%Ecto.Query{from: %Ecto.Query.FromExpr{source: {"imported_entry_pages", _}}} = q,
|
||||
[:bounce_rate | rest]
|
||||
@ -353,6 +472,18 @@ defmodule Plausible.Stats.Imported do
|
||||
|> select_imported_metrics(rest)
|
||||
end
|
||||
|
||||
defp select_imported_metrics(
|
||||
%Ecto.Query{from: %Ecto.Query.FromExpr{source: {"imported_pages", _}}} = q,
|
||||
[:visit_duration | rest]
|
||||
) do
|
||||
q
|
||||
|> select_merge([i], %{
|
||||
visit_duration: 0,
|
||||
__internal_visits: 0
|
||||
})
|
||||
|> select_imported_metrics(rest)
|
||||
end
|
||||
|
||||
defp select_imported_metrics(
|
||||
%Ecto.Query{from: %Ecto.Query.FromExpr{source: {"imported_entry_pages", _}}} = q,
|
||||
[:visit_duration | rest]
|
||||
@ -386,6 +517,32 @@ defmodule Plausible.Stats.Imported do
|
||||
|> select_imported_metrics(rest)
|
||||
end
|
||||
|
||||
defp select_imported_metrics(
|
||||
%Ecto.Query{from: %Ecto.Query.FromExpr{source: {"imported_entry_pages", _}}} = q,
|
||||
[:views_per_visit | rest]
|
||||
) do
|
||||
q
|
||||
|> where([i], i.pageviews > 0)
|
||||
|> select_merge([i], %{
|
||||
pageviews: sum(i.pageviews),
|
||||
__internal_visits: sum(i.entrances)
|
||||
})
|
||||
|> select_imported_metrics(rest)
|
||||
end
|
||||
|
||||
defp select_imported_metrics(
|
||||
%Ecto.Query{from: %Ecto.Query.FromExpr{source: {"imported_exit_pages", _}}} = q,
|
||||
[:views_per_visit | rest]
|
||||
) do
|
||||
q
|
||||
|> where([i], i.pageviews > 0)
|
||||
|> select_merge([i], %{
|
||||
pageviews: sum(i.pageviews),
|
||||
__internal_visits: sum(i.exits)
|
||||
})
|
||||
|> select_imported_metrics(rest)
|
||||
end
|
||||
|
||||
defp select_imported_metrics(q, [:views_per_visit | rest]) do
|
||||
q
|
||||
|> where([i], i.pageviews > 0)
|
||||
@ -558,7 +715,7 @@ defmodule Plausible.Stats.Imported do
|
||||
end
|
||||
|
||||
defp select_joined_metrics(q, []), do: q
|
||||
# TODO: Reverse-engineering the native data bounces and total visit
|
||||
# NOTE: Reverse-engineering the native data bounces and total visit
|
||||
# durations to combine with imported data is inefficient. Instead both
|
||||
# queries should fetch bounces/total_visit_duration and visits and be
|
||||
# used as subqueries to a main query that then find the bounce rate/avg
|
@ -12,13 +12,15 @@ defmodule Plausible.Stats.Query do
|
||||
skip_imported_reason: nil,
|
||||
now: nil,
|
||||
experimental_session_count?: false,
|
||||
experimental_reduced_joins?: false
|
||||
experimental_reduced_joins?: false,
|
||||
latest_import_end_date: nil
|
||||
|
||||
require OpenTelemetry.Tracer, as: Tracer
|
||||
alias Plausible.Stats.{Filters, Interval, Imported}
|
||||
|
||||
@type t :: %__MODULE__{}
|
||||
|
||||
@spec from(Plausible.Site.t(), map()) :: t()
|
||||
def from(site, params) do
|
||||
now = NaiveDateTime.utc_now(:second)
|
||||
|
||||
@ -201,19 +203,43 @@ defmodule Plausible.Stats.Query do
|
||||
struct!(query, filters: Filters.parse(params["filters"]))
|
||||
end
|
||||
|
||||
def put_filter(query, filter) do
|
||||
struct!(query,
|
||||
filters: query.filters ++ [filter]
|
||||
)
|
||||
@spec set_property(t(), String.t() | nil, Keyword.t()) :: t()
|
||||
def set_property(query, property, opts \\ []) do
|
||||
query = struct!(query, property: property)
|
||||
|
||||
if Keyword.get(opts, :skip_refresh_imported_opts),
|
||||
do: query,
|
||||
else: refresh_imported_opts(query)
|
||||
end
|
||||
|
||||
def remove_filters(query, prefixes) do
|
||||
def put_filter(query, filter) do
|
||||
query
|
||||
|> struct!(filters: query.filters ++ [filter])
|
||||
|> refresh_imported_opts()
|
||||
end
|
||||
|
||||
def remove_filters(query, prefixes, opts \\ []) do
|
||||
new_filters =
|
||||
Enum.reject(query.filters, fn [_, filter_key | _rest] ->
|
||||
Enum.any?(prefixes, &String.starts_with?(filter_key, &1))
|
||||
end)
|
||||
|
||||
struct!(query, filters: new_filters)
|
||||
query = struct!(query, filters: new_filters)
|
||||
|
||||
if Keyword.get(opts, :skip_refresh_imported_opts),
|
||||
do: query,
|
||||
else: refresh_imported_opts(query)
|
||||
end
|
||||
|
||||
def exclude_imported(query) do
|
||||
struct!(query,
|
||||
include_imported: false,
|
||||
skip_imported_reason: :manual_exclusion
|
||||
)
|
||||
end
|
||||
|
||||
defp refresh_imported_opts(query) do
|
||||
put_imported_opts(query, nil, %{})
|
||||
end
|
||||
|
||||
def has_event_filters?(query) do
|
||||
@ -247,13 +273,22 @@ defmodule Plausible.Stats.Query do
|
||||
end
|
||||
|
||||
defp put_imported_opts(query, site, params) do
|
||||
requested? = params["with_imported"] == "true"
|
||||
requested? = params["with_imported"] == "true" || query.imported_data_requested
|
||||
|
||||
case ensure_include_imported(query, site) do
|
||||
latest_import_end_date =
|
||||
if site do
|
||||
site.latest_import_end_date
|
||||
else
|
||||
query.latest_import_end_date
|
||||
end
|
||||
|
||||
query = struct!(query, latest_import_end_date: latest_import_end_date)
|
||||
|
||||
case ensure_include_imported(query, requested?) do
|
||||
:ok ->
|
||||
struct!(query,
|
||||
imported_data_requested: requested?,
|
||||
include_imported: requested?
|
||||
imported_data_requested: true,
|
||||
include_imported: true
|
||||
)
|
||||
|
||||
{:error, reason} ->
|
||||
@ -265,12 +300,13 @@ defmodule Plausible.Stats.Query do
|
||||
end
|
||||
end
|
||||
|
||||
@spec ensure_include_imported(t(), Plausible.Site.t()) ::
|
||||
:ok | {:error, :no_imported_data | :out_of_range | :unsupported_query}
|
||||
def ensure_include_imported(query, site) do
|
||||
@spec ensure_include_imported(t(), boolean()) ::
|
||||
:ok | {:error, :not_requested | :no_imported_data | :out_of_range | :unsupported_query}
|
||||
def ensure_include_imported(query, requested?) do
|
||||
cond do
|
||||
is_nil(site.latest_import_end_date) -> {:error, :no_imported_data}
|
||||
Date.after?(query.date_range.first, site.latest_import_end_date) -> {:error, :out_of_range}
|
||||
not requested? -> {:error, :not_requested}
|
||||
is_nil(query.latest_import_end_date) -> {:error, :no_imported_data}
|
||||
Date.after?(query.date_range.first, query.latest_import_end_date) -> {:error, :out_of_range}
|
||||
not Imported.schema_supports_query?(query) -> {:error, :unsupported_query}
|
||||
query.period == "realtime" -> {:error, :unsupported_query}
|
||||
true -> :ok
|
||||
|
@ -1,7 +1,7 @@
|
||||
defmodule Plausible.Stats.Timeseries do
|
||||
use Plausible.ClickhouseRepo
|
||||
use Plausible
|
||||
alias Plausible.Stats.{Query, Util}
|
||||
alias Plausible.Stats.{Query, Util, Imported}
|
||||
import Plausible.Stats.{Base}
|
||||
import Ecto.Query
|
||||
use Plausible.Stats.Fragments
|
||||
@ -56,8 +56,8 @@ defmodule Plausible.Stats.Timeseries do
|
||||
|
||||
from(e in base_event_query(site, query), select: ^select_event_metrics(metrics))
|
||||
|> select_bucket(:events, site, query)
|
||||
|> Imported.merge_imported_timeseries(site, query, metrics)
|
||||
|> maybe_add_timeseries_conversion_rate(site, query, metrics)
|
||||
|> Plausible.Stats.Imported.merge_imported_timeseries(site, query, metrics)
|
||||
|> ClickhouseRepo.all()
|
||||
end
|
||||
|
||||
@ -67,7 +67,7 @@ defmodule Plausible.Stats.Timeseries do
|
||||
from(e in query_sessions(site, query), select: ^select_session_metrics(metrics, query))
|
||||
|> filter_converted_sessions(site, query)
|
||||
|> select_bucket(:sessions, site, query)
|
||||
|> Plausible.Stats.Imported.merge_imported_timeseries(site, query, metrics)
|
||||
|> Imported.merge_imported_timeseries(site, query, metrics)
|
||||
|> ClickhouseRepo.all()
|
||||
|> Util.keep_requested_metrics(metrics)
|
||||
end
|
||||
@ -314,13 +314,16 @@ defmodule Plausible.Stats.Timeseries do
|
||||
|
||||
defp maybe_add_timeseries_conversion_rate(q, site, query, metrics) do
|
||||
if :conversion_rate in metrics do
|
||||
totals_query = query |> Query.remove_filters(["event:goal", "event:props"])
|
||||
totals_query =
|
||||
query
|
||||
|> Query.remove_filters(["event:goal", "event:props"], skip_refresh_imported_opts: true)
|
||||
|
||||
totals_timeseries_q =
|
||||
from(e in base_event_query(site, totals_query),
|
||||
select: ^select_event_metrics([:visitors])
|
||||
)
|
||||
|> select_bucket(:events, site, query)
|
||||
|> select_bucket(:events, site, totals_query)
|
||||
|> Imported.merge_imported_timeseries(site, totals_query, [:visitors])
|
||||
|
||||
from(e in subquery(q),
|
||||
left_join: c in subquery(totals_timeseries_q),
|
||||
|
@ -380,7 +380,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController do
|
||||
end
|
||||
|
||||
defp maybe_add_warning(payload, %{skip_imported_reason: reason})
|
||||
when reason in [nil, :no_imported_data, :out_of_range] do
|
||||
when reason in [nil, :not_requested, :no_imported_data, :out_of_range, :manual_exclusion] do
|
||||
payload
|
||||
end
|
||||
|
||||
|
@ -65,7 +65,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
|
||||
* `interval` - the interval used for querying.
|
||||
|
||||
* `with_imported` - boolean indicating whether the Google Analytics data
|
||||
* `includes_imported` - boolean indicating whether imported data
|
||||
was queried or not.
|
||||
|
||||
* `imports_exist` - boolean indicating whether there are any completed
|
||||
@ -92,7 +92,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
"labels" => ["2021-09-01", "2021-10-01", "2021-11-01", "2021-12-01"],
|
||||
"plot" => [0, 0, 0, 0],
|
||||
"present_index" => nil,
|
||||
"with_imported" => false
|
||||
"includes_imported" => false
|
||||
}
|
||||
```
|
||||
|
||||
@ -137,7 +137,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
comparison_labels: comparison_result && label_timeseries(comparison_result, nil),
|
||||
present_index: present_index,
|
||||
interval: query.interval,
|
||||
with_imported: with_imported?(query, comparison_query),
|
||||
includes_imported: includes_imported?(query, comparison_query),
|
||||
imports_exist: site.complete_import_ids != [],
|
||||
full_intervals: full_intervals
|
||||
})
|
||||
@ -217,7 +217,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
top_stats: top_stats,
|
||||
interval: query.interval,
|
||||
sample_percent: sample_percent,
|
||||
with_imported: with_imported?(query, comparison_query),
|
||||
includes_imported: includes_imported?(query, comparison_query),
|
||||
imports_exist: site.complete_import_ids != [],
|
||||
comparing_from: comparison_query && comparison_query.date_range.first,
|
||||
comparing_to: comparison_query && comparison_query.date_range.last,
|
||||
@ -381,26 +381,15 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
end
|
||||
|
||||
defp fetch_other_top_stats(site, query, comparison_query) do
|
||||
page_filter? = Query.get_filter(query, "event:page")
|
||||
|
||||
metrics = [:visitors, :visits, :pageviews, :sample_percent]
|
||||
|
||||
metrics =
|
||||
if Query.get_filter(query, "event:page") do
|
||||
[
|
||||
:visitors,
|
||||
:visits,
|
||||
:pageviews,
|
||||
:bounce_rate,
|
||||
:time_on_page,
|
||||
:sample_percent
|
||||
]
|
||||
else
|
||||
[
|
||||
:visitors,
|
||||
:visits,
|
||||
:pageviews,
|
||||
:views_per_visit,
|
||||
:bounce_rate,
|
||||
:visit_duration,
|
||||
:sample_percent
|
||||
]
|
||||
cond do
|
||||
page_filter? && query.include_imported -> metrics
|
||||
page_filter? -> metrics ++ [:bounce_rate, :time_on_page]
|
||||
true -> metrics ++ [:views_per_visit, :bounce_rate, :visit_duration]
|
||||
end
|
||||
|
||||
current_results = Stats.aggregate(site, query, metrics)
|
||||
@ -492,7 +481,10 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
res |> to_csv([:name, :visitors, :bounce_rate, :visit_duration])
|
||||
end
|
||||
else
|
||||
json(conn, res)
|
||||
json(conn, %{
|
||||
results: res,
|
||||
skip_imported_reason: query.skip_imported_reason
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@ -570,7 +562,10 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
res |> to_csv([:name, :visitors, :bounce_rate, :visit_duration])
|
||||
end
|
||||
else
|
||||
json(conn, res)
|
||||
json(conn, %{
|
||||
results: res,
|
||||
skip_imported_reason: query.skip_imported_reason
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@ -594,7 +589,10 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
res |> to_csv([:name, :visitors, :bounce_rate, :visit_duration])
|
||||
end
|
||||
else
|
||||
json(conn, res)
|
||||
json(conn, %{
|
||||
results: res,
|
||||
skip_imported_reason: query.skip_imported_reason
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@ -618,7 +616,10 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
res |> to_csv([:name, :visitors, :bounce_rate, :visit_duration])
|
||||
end
|
||||
else
|
||||
json(conn, res)
|
||||
json(conn, %{
|
||||
results: res,
|
||||
skip_imported_reason: query.skip_imported_reason
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@ -642,7 +643,10 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
res |> to_csv([:name, :visitors, :bounce_rate, :visit_duration])
|
||||
end
|
||||
else
|
||||
json(conn, res)
|
||||
json(conn, %{
|
||||
results: res,
|
||||
skip_imported_reason: query.skip_imported_reason
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@ -666,7 +670,10 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
res |> to_csv([:name, :visitors, :bounce_rate, :visit_duration])
|
||||
end
|
||||
else
|
||||
json(conn, res)
|
||||
json(conn, %{
|
||||
results: res,
|
||||
skip_imported_reason: query.skip_imported_reason
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@ -690,7 +697,10 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
res |> to_csv([:name, :visitors, :bounce_rate, :visit_duration])
|
||||
end
|
||||
else
|
||||
json(conn, res)
|
||||
json(conn, %{
|
||||
results: res,
|
||||
skip_imported_reason: query.skip_imported_reason
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@ -743,7 +753,10 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
Stats.breakdown(site, query, metrics, pagination)
|
||||
|> transform_keys(%{referrer: :name})
|
||||
|
||||
json(conn, referrers)
|
||||
json(conn, %{
|
||||
results: referrers,
|
||||
skip_imported_reason: query.skip_imported_reason
|
||||
})
|
||||
end
|
||||
|
||||
def pages(conn, params) do
|
||||
@ -772,7 +785,10 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
pages |> to_csv([:name, :visitors, :pageviews, :bounce_rate, :time_on_page])
|
||||
end
|
||||
else
|
||||
json(conn, pages)
|
||||
json(conn, %{
|
||||
results: pages,
|
||||
skip_imported_reason: query.skip_imported_reason
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@ -803,7 +819,10 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
])
|
||||
end
|
||||
else
|
||||
json(conn, entry_pages)
|
||||
json(conn, %{
|
||||
results: entry_pages,
|
||||
skip_imported_reason: query.skip_imported_reason
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@ -835,7 +854,10 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
])
|
||||
end
|
||||
else
|
||||
json(conn, exit_pages)
|
||||
json(conn, %{
|
||||
results: exit_pages,
|
||||
skip_imported_reason: query.skip_imported_reason
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@ -845,14 +867,15 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
else
|
||||
pages = Enum.map(breakdown_results, & &1[:exit_page])
|
||||
|
||||
total_visits_query =
|
||||
total_pageviews_query =
|
||||
query
|
||||
|> Query.remove_filters(["visit:exit_page"])
|
||||
|> Query.put_filter([:member, "event:page", pages])
|
||||
|> Query.put_filter([:is, "event:name", "pageview"])
|
||||
|> struct!(property: "event:page")
|
||||
|> Query.set_property("event:page")
|
||||
|
||||
total_pageviews =
|
||||
Stats.breakdown(site, total_visits_query, [:pageviews], {limit, 1})
|
||||
Stats.breakdown(site, total_pageviews_query, [:pageviews], {limit, 1})
|
||||
|
||||
Enum.map(breakdown_results, fn result ->
|
||||
exit_rate =
|
||||
@ -917,7 +940,10 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
end
|
||||
end)
|
||||
|
||||
json(conn, countries)
|
||||
json(conn, %{
|
||||
results: countries,
|
||||
skip_imported_reason: query.skip_imported_reason
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@ -952,7 +978,10 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
regions |> to_csv([:name, :visitors])
|
||||
end
|
||||
else
|
||||
json(conn, regions)
|
||||
json(conn, %{
|
||||
results: regions,
|
||||
skip_imported_reason: query.skip_imported_reason
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@ -992,7 +1021,10 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
cities |> to_csv([:name, :visitors])
|
||||
end
|
||||
else
|
||||
json(conn, cities)
|
||||
json(conn, %{
|
||||
results: cities,
|
||||
skip_imported_reason: query.skip_imported_reason
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@ -1016,7 +1048,10 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
browsers |> to_csv([:name, :visitors])
|
||||
end
|
||||
else
|
||||
json(conn, browsers)
|
||||
json(conn, %{
|
||||
results: browsers,
|
||||
skip_imported_reason: query.skip_imported_reason
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@ -1046,7 +1081,10 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
|> to_csv([:name, :version, :visitors])
|
||||
end
|
||||
else
|
||||
json(conn, versions)
|
||||
json(conn, %{
|
||||
results: versions,
|
||||
skip_imported_reason: query.skip_imported_reason
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@ -1070,7 +1108,10 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
systems |> to_csv([:name, :visitors])
|
||||
end
|
||||
else
|
||||
json(conn, systems)
|
||||
json(conn, %{
|
||||
results: systems,
|
||||
skip_imported_reason: query.skip_imported_reason
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@ -1096,7 +1137,10 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
|> to_csv([:name, :version, :visitors])
|
||||
end
|
||||
else
|
||||
json(conn, versions)
|
||||
json(conn, %{
|
||||
results: versions,
|
||||
skip_imported_reason: query.skip_imported_reason
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@ -1120,7 +1164,10 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
sizes |> to_csv([:name, :visitors])
|
||||
end
|
||||
else
|
||||
json(conn, sizes)
|
||||
json(conn, %{
|
||||
results: sizes,
|
||||
skip_imported_reason: query.skip_imported_reason
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@ -1156,7 +1203,10 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
:total_conversions
|
||||
])
|
||||
else
|
||||
json(conn, conversions)
|
||||
json(conn, %{
|
||||
results: conversions,
|
||||
skip_imported_reason: query.skip_imported_reason
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@ -1166,8 +1216,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
|
||||
case Plausible.Props.ensure_prop_key_accessible(prop_key, site.owner) do
|
||||
:ok ->
|
||||
props = breakdown_custom_prop_values(site, params)
|
||||
json(conn, props)
|
||||
json(conn, breakdown_custom_prop_values(site, params))
|
||||
|
||||
{:error, :upgrade_required} ->
|
||||
H.payment_required(
|
||||
@ -1195,6 +1244,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
prop_names
|
||||
|> Enum.map(fn prop_key ->
|
||||
breakdown_custom_prop_values(site, Map.put(params, "prop_key", prop_key))
|
||||
|> Map.get(:results)
|
||||
|> Enum.map(&Map.put(&1, :property, prop_key))
|
||||
|> transform_keys(%{:name => :value})
|
||||
end)
|
||||
@ -1224,12 +1274,15 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
[:visitors, :events, :percentage] ++ @revenue_metrics
|
||||
end
|
||||
|
||||
Stats.breakdown(site, query, metrics, pagination)
|
||||
|> transform_keys(%{prop_key => :name})
|
||||
|> Enum.map(fn entry ->
|
||||
Enum.map(entry, &format_revenue_metric/1)
|
||||
|> Map.new()
|
||||
end)
|
||||
props =
|
||||
Stats.breakdown(site, query, metrics, pagination)
|
||||
|> transform_keys(%{prop_key => :name})
|
||||
|> Enum.map(fn entry ->
|
||||
Enum.map(entry, &format_revenue_metric/1)
|
||||
|> Map.new()
|
||||
end)
|
||||
|
||||
%{results: props, skip_imported_reason: query.skip_imported_reason}
|
||||
end
|
||||
|
||||
def current_visitors(conn, _) do
|
||||
@ -1401,7 +1454,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
]
|
||||
end
|
||||
|
||||
defp with_imported?(source_query, comparison_query) do
|
||||
defp includes_imported?(source_query, comparison_query) do
|
||||
cond do
|
||||
source_query.include_imported -> true
|
||||
comparison_query && comparison_query.include_imported -> true
|
||||
|
@ -561,14 +561,14 @@ defmodule PlausibleWeb.Api.ExternalStatsController.AggregateTest do
|
||||
}
|
||||
end
|
||||
|
||||
test "ignores imported data when filters are applied", %{
|
||||
test "includes imported data in comparison when filter applied", %{
|
||||
conn: conn,
|
||||
site: site,
|
||||
site_import: site_import
|
||||
} do
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:imported_visitors, date: ~D[2023-01-01]),
|
||||
build(:imported_sources, date: ~D[2023-01-01]),
|
||||
build(:imported_sources, source: "Google", date: ~D[2023-01-01], visitors: 3),
|
||||
build(:pageview,
|
||||
referrer_source: "Google",
|
||||
timestamp: ~N[2023-01-02 00:10:00]
|
||||
@ -587,7 +587,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController.AggregateTest do
|
||||
})
|
||||
|
||||
assert json_response(conn, 200)["results"] == %{
|
||||
"visitors" => %{"value" => 1, "change" => 100}
|
||||
"visitors" => %{"value" => 1, "change" => -67}
|
||||
}
|
||||
end
|
||||
|
||||
@ -681,6 +681,60 @@ defmodule PlausibleWeb.Api.ExternalStatsController.AggregateTest do
|
||||
|
||||
refute json_response(conn, 200)["warning"]
|
||||
end
|
||||
|
||||
test "excludes imported data from conversion rate computation when query filters by non-imported props",
|
||||
%{conn: conn, site: site, site_import: site_import} do
|
||||
insert(:goal, site: site, event_name: "Purchase")
|
||||
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:event,
|
||||
name: "Purchase",
|
||||
"meta.key": ["package"],
|
||||
"meta.value": ["large"]
|
||||
),
|
||||
build(:imported_visitors, visitors: 9)
|
||||
])
|
||||
|
||||
conn =
|
||||
get(conn, "/api/v1/stats/aggregate", %{
|
||||
"site_id" => site.domain,
|
||||
"period" => "day",
|
||||
"metrics" => "visitors,conversion_rate",
|
||||
"filters" => "event:goal==Purchase;event:props:package==large",
|
||||
"with_imported" => "true"
|
||||
})
|
||||
|
||||
assert json_response(conn, 200)["results"] == %{
|
||||
"visitors" => %{"value" => 1},
|
||||
"conversion_rate" => %{"value" => 100.0}
|
||||
}
|
||||
end
|
||||
|
||||
test "returns stats with page + pageview goal filter",
|
||||
%{conn: conn, site: site, site_import: site_import} do
|
||||
insert(:goal, site: site, page_path: "/blog/**")
|
||||
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:imported_pages, page: "/blog/1", visitors: 1, pageviews: 1),
|
||||
build(:imported_pages, page: "/blog/2", visitors: 2, pageviews: 2),
|
||||
build(:imported_pages, visitors: 3)
|
||||
])
|
||||
|
||||
conn =
|
||||
get(conn, "/api/v1/stats/aggregate", %{
|
||||
"site_id" => site.domain,
|
||||
"period" => "day",
|
||||
"metrics" => "visitors,events,conversion_rate",
|
||||
"filters" => "event:page==/blog/2;event:goal==Visit /blog/**",
|
||||
"with_imported" => "true"
|
||||
})
|
||||
|
||||
assert json_response(conn, 200)["results"] == %{
|
||||
"visitors" => %{"value" => 2},
|
||||
"events" => %{"value" => 2},
|
||||
"conversion_rate" => %{"value" => 100.0}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
describe "filters" do
|
||||
|
@ -3158,6 +3158,37 @@ defmodule PlausibleWeb.Api.ExternalStatsController.BreakdownTest do
|
||||
end
|
||||
|
||||
describe "imported data" do
|
||||
test "returns screen sizes breakdown when filtering by screen size", %{conn: conn, site: site} do
|
||||
site_import = insert(:site_import, site: site)
|
||||
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:pageview,
|
||||
timestamp: ~N[2021-01-01 00:00:01],
|
||||
screen_size: "Mobile"
|
||||
),
|
||||
build(:imported_devices,
|
||||
device: "Mobile",
|
||||
visitors: 3,
|
||||
pageviews: 5,
|
||||
date: ~D[2021-01-01]
|
||||
)
|
||||
])
|
||||
|
||||
conn =
|
||||
get(conn, "/api/v1/stats/breakdown", %{
|
||||
"site_id" => site.domain,
|
||||
"period" => "day",
|
||||
"date" => "2021-01-01",
|
||||
"property" => "visit:device",
|
||||
"filters" => "visit:device==Mobile",
|
||||
"metrics" => "visitors,pageviews",
|
||||
"with_imported" => "true"
|
||||
})
|
||||
|
||||
assert [%{"pageviews" => 6, "visitors" => 4, "device" => "Mobile"}] =
|
||||
json_response(conn, 200)["results"]
|
||||
end
|
||||
|
||||
test "returns custom event goals and pageview goals", %{conn: conn, site: site} do
|
||||
insert(:goal, site: site, event_name: "Purchase")
|
||||
insert(:goal, site: site, page_path: "/test")
|
||||
@ -3478,5 +3509,64 @@ defmodule PlausibleWeb.Api.ExternalStatsController.BreakdownTest do
|
||||
|
||||
refute json_response(conn, 200)["warning"]
|
||||
end
|
||||
|
||||
test "applies multiple filters if the properties belong to the same table", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
site_import = insert(:site_import, site: site)
|
||||
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:imported_sources, source: "Google", utm_medium: "organic", utm_term: "one"),
|
||||
build(:imported_sources, source: "Twitter", utm_medium: "organic", utm_term: "two"),
|
||||
build(:imported_sources,
|
||||
source: "Facebook",
|
||||
utm_medium: "something_else",
|
||||
utm_term: "one"
|
||||
)
|
||||
])
|
||||
|
||||
conn =
|
||||
get(conn, "/api/v1/stats/breakdown", %{
|
||||
"site_id" => site.domain,
|
||||
"period" => "day",
|
||||
"property" => "visit:source",
|
||||
"filters" => "visit:utm_medium==organic;visit:utm_term==one",
|
||||
"with_imported" => "true"
|
||||
})
|
||||
|
||||
assert json_response(conn, 200) == %{
|
||||
"results" => [%{"source" => "Google", "visitors" => 1}]
|
||||
}
|
||||
end
|
||||
|
||||
test "ignores imported data if filtered property belongs to a different table than the breakdown property",
|
||||
%{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
site_import = insert(:site_import, site: site)
|
||||
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:imported_sources, source: "Google"),
|
||||
build(:imported_devices, device: "Desktop")
|
||||
])
|
||||
|
||||
conn =
|
||||
get(conn, "/api/v1/stats/breakdown", %{
|
||||
"site_id" => site.domain,
|
||||
"period" => "day",
|
||||
"property" => "visit:source",
|
||||
"filters" => "visit:device==Desktop",
|
||||
"with_imported" => "true"
|
||||
})
|
||||
|
||||
assert %{
|
||||
"results" => [],
|
||||
"warning" => warning
|
||||
} = json_response(conn, 200)
|
||||
|
||||
assert warning =~ "Imported stats are not included in the results"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1647,5 +1647,240 @@ defmodule PlausibleWeb.Api.ExternalStatsController.TimeseriesTest do
|
||||
|
||||
refute json_response(conn, 200)["warning"]
|
||||
end
|
||||
|
||||
test "returns all metrics based on imported/native data when filtering by browser", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
site_import = insert(:site_import, site: site)
|
||||
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:pageview, browser: "Chrome", user_id: 1, timestamp: ~N[2021-01-01 00:00:00]),
|
||||
build(:pageview, browser: "Chrome", user_id: 1, timestamp: ~N[2021-01-01 00:03:00]),
|
||||
build(:pageview, browser: "Firefox", timestamp: ~N[2021-01-01 00:00:00]),
|
||||
build(:imported_browsers, browser: "Firefox", date: ~D[2021-01-02]),
|
||||
build(:imported_browsers,
|
||||
browser: "Chrome",
|
||||
visitors: 1,
|
||||
pageviews: 1,
|
||||
bounces: 1,
|
||||
visit_duration: 3,
|
||||
visits: 1,
|
||||
date: ~D[2021-01-03]
|
||||
),
|
||||
build(:pageview, browser: "Chrome", user_id: 2, timestamp: ~N[2021-01-04 00:00:00]),
|
||||
build(:event,
|
||||
name: "Signup",
|
||||
browser: "Chrome",
|
||||
user_id: 2,
|
||||
timestamp: ~N[2021-01-04 00:10:00]
|
||||
),
|
||||
build(:imported_browsers,
|
||||
browser: "Chrome",
|
||||
visitors: 4,
|
||||
pageviews: 6,
|
||||
bounces: 1,
|
||||
visit_duration: 300,
|
||||
visits: 5,
|
||||
date: ~D[2021-01-04]
|
||||
)
|
||||
])
|
||||
|
||||
results =
|
||||
conn
|
||||
|> get("/api/v1/stats/timeseries", %{
|
||||
"site_id" => site.domain,
|
||||
"period" => "custom",
|
||||
"date" => "2021-01-01,2021-01-04",
|
||||
"metrics" =>
|
||||
"visitors,pageviews,events,visits,views_per_visit,bounce_rate,visit_duration",
|
||||
"filters" => "visit:browser==Chrome",
|
||||
"with_imported" => "true"
|
||||
})
|
||||
|> json_response(200)
|
||||
|> Map.get("results")
|
||||
|
||||
assert results == [
|
||||
%{
|
||||
"bounce_rate" => 0.0,
|
||||
"date" => "2021-01-01",
|
||||
"events" => 2,
|
||||
"pageviews" => 2,
|
||||
"views_per_visit" => 2.0,
|
||||
"visit_duration" => 180.0,
|
||||
"visitors" => 1,
|
||||
"visits" => 1
|
||||
},
|
||||
%{
|
||||
"bounce_rate" => nil,
|
||||
"date" => "2021-01-02",
|
||||
"events" => 0,
|
||||
"pageviews" => 0,
|
||||
"views_per_visit" => 0.0,
|
||||
"visit_duration" => nil,
|
||||
"visitors" => 0,
|
||||
"visits" => 0
|
||||
},
|
||||
%{
|
||||
"bounce_rate" => 100,
|
||||
"date" => "2021-01-03",
|
||||
"events" => 1,
|
||||
"pageviews" => 1,
|
||||
"views_per_visit" => 1.0,
|
||||
"visit_duration" => 3,
|
||||
"visitors" => 1,
|
||||
"visits" => 1
|
||||
},
|
||||
%{
|
||||
"bounce_rate" => 17.0,
|
||||
"date" => "2021-01-04",
|
||||
"events" => 8,
|
||||
"pageviews" => 7,
|
||||
"views_per_visit" => 1.17,
|
||||
"visit_duration" => 150,
|
||||
"visitors" => 5,
|
||||
"visits" => 6
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
test "returns conversion rate timeseries with a goal filter", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
site_import = insert(:site_import, site: site)
|
||||
|
||||
insert(:goal, site: site, event_name: "Outbound Link: Click")
|
||||
|
||||
populate_stats(site, site_import.id, [
|
||||
# 2021-01-01
|
||||
build(:event, name: "Outbound Link: Click", timestamp: ~N[2021-01-01 00:00:00]),
|
||||
build(:imported_custom_events, name: "Outbound Link: Click", date: ~D[2021-01-01]),
|
||||
build(:imported_visitors, date: ~D[2021-01-01], visitors: 4),
|
||||
# 2021-01-02
|
||||
build(:event, name: "Outbound Link: Click", timestamp: ~N[2021-01-02 00:00:00]),
|
||||
build(:pageview, timestamp: ~N[2021-01-02 00:00:00]),
|
||||
# 2021-01-03
|
||||
build(:imported_custom_events, name: "Outbound Link: Click", date: ~D[2021-01-03]),
|
||||
build(:imported_visitors, date: ~D[2021-01-03]),
|
||||
# 2021-01-04
|
||||
build(:event, name: "Outbound Link: Click", timestamp: ~N[2021-01-04 00:00:00]),
|
||||
build(:imported_visitors, date: ~D[2021-01-04], visitors: 2)
|
||||
])
|
||||
|
||||
results =
|
||||
conn
|
||||
|> get("/api/v1/stats/timeseries", %{
|
||||
"site_id" => site.domain,
|
||||
"period" => "custom",
|
||||
"date" => "2021-01-01,2021-01-04",
|
||||
"metrics" => "conversion_rate",
|
||||
"filters" => "event:goal==Outbound Link: Click",
|
||||
"with_imported" => "true"
|
||||
})
|
||||
|> json_response(200)
|
||||
|> Map.get("results")
|
||||
|
||||
assert results == [
|
||||
%{
|
||||
"date" => "2021-01-01",
|
||||
"conversion_rate" => 40.0
|
||||
},
|
||||
%{
|
||||
"date" => "2021-01-02",
|
||||
"conversion_rate" => 50.0
|
||||
},
|
||||
%{
|
||||
"date" => "2021-01-03",
|
||||
"conversion_rate" => 100.0
|
||||
},
|
||||
%{
|
||||
"date" => "2021-01-04",
|
||||
"conversion_rate" => 33.3
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
test "returns conversion rate timeseries with a goal + custom prop filter", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
site_import = insert(:site_import, site: site)
|
||||
|
||||
insert(:goal, site: site, event_name: "Outbound Link: Click")
|
||||
|
||||
populate_stats(site, site_import.id, [
|
||||
# 2021-01-01
|
||||
build(:event,
|
||||
name: "Outbound Link: Click",
|
||||
"meta.key": ["url"],
|
||||
"meta.value": ["https://site.com"],
|
||||
timestamp: ~N[2021-01-01 00:00:00]
|
||||
),
|
||||
build(:imported_custom_events,
|
||||
name: "Outbound Link: Click",
|
||||
link_url: "https://site.com",
|
||||
date: ~D[2021-01-01]
|
||||
),
|
||||
build(:imported_custom_events,
|
||||
name: "File Download",
|
||||
link_url: "https://site.com",
|
||||
date: ~D[2021-01-01]
|
||||
),
|
||||
build(:imported_custom_events,
|
||||
name: "Outbound Link: Click",
|
||||
link_url: "https://notthis.com",
|
||||
date: ~D[2021-01-01]
|
||||
),
|
||||
build(:imported_visitors, date: ~D[2021-01-01], visitors: 4),
|
||||
# 2021-01-03
|
||||
build(:imported_custom_events,
|
||||
name: "Outbound Link: Click",
|
||||
link_url: "https://site.com",
|
||||
date: ~D[2021-01-03]
|
||||
),
|
||||
build(:imported_visitors, date: ~D[2021-01-03]),
|
||||
# 2021-01-04
|
||||
build(:event,
|
||||
name: "Outbound Link: Click",
|
||||
"meta.key": ["url"],
|
||||
"meta.value": ["https://site.com"],
|
||||
timestamp: ~N[2021-01-04 00:00:00]
|
||||
),
|
||||
build(:imported_visitors, date: ~D[2021-01-04], visitors: 2)
|
||||
])
|
||||
|
||||
results =
|
||||
conn
|
||||
|> get("/api/v1/stats/timeseries", %{
|
||||
"site_id" => site.domain,
|
||||
"period" => "custom",
|
||||
"date" => "2021-01-01,2021-01-04",
|
||||
"metrics" => "conversion_rate",
|
||||
"filters" => "event:goal==Outbound Link: Click;event:props:url==https://site.com",
|
||||
"with_imported" => "true"
|
||||
})
|
||||
|> json_response(200)
|
||||
|> Map.get("results")
|
||||
|
||||
assert results == [
|
||||
%{
|
||||
"date" => "2021-01-01",
|
||||
"conversion_rate" => 40.0
|
||||
},
|
||||
%{
|
||||
"date" => "2021-01-02",
|
||||
"conversion_rate" => 0.0
|
||||
},
|
||||
%{
|
||||
"date" => "2021-01-03",
|
||||
"conversion_rate" => 100.0
|
||||
},
|
||||
%{
|
||||
"date" => "2021-01-04",
|
||||
"conversion_rate" => 33.3
|
||||
}
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -13,7 +13,7 @@ defmodule PlausibleWeb.Api.StatsController.BrowsersTest do
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/browsers?period=day")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "Chrome", "visitors" => 2, "percentage" => 66.7},
|
||||
%{"name" => "Firefox", "visitors" => 1, "percentage" => 33.3}
|
||||
]
|
||||
@ -47,7 +47,7 @@ defmodule PlausibleWeb.Api.StatsController.BrowsersTest do
|
||||
filters = Jason.encode!(%{props: %{"author" => "John Doe"}})
|
||||
conn = get(conn, "/api/stats/#{site.domain}/browsers?period=day&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "Chrome", "visitors" => 1, "percentage" => 100}
|
||||
]
|
||||
end
|
||||
@ -82,7 +82,7 @@ defmodule PlausibleWeb.Api.StatsController.BrowsersTest do
|
||||
filters = Jason.encode!(%{props: %{"author" => "!John Doe"}})
|
||||
conn = get(conn, "/api/stats/#{site.domain}/browsers?period=day&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "Firefox", "visitors" => 1, "percentage" => 50},
|
||||
%{"name" => "Safari", "visitors" => 1, "percentage" => 50}
|
||||
]
|
||||
@ -99,7 +99,7 @@ defmodule PlausibleWeb.Api.StatsController.BrowsersTest do
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/browsers?period=day&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "Chrome",
|
||||
"total_visitors" => 2,
|
||||
@ -123,13 +123,13 @@ defmodule PlausibleWeb.Api.StatsController.BrowsersTest do
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/browsers?period=day")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "Chrome", "visitors" => 1, "percentage" => 100}
|
||||
]
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/browsers?period=day&with_imported=true")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "Chrome", "visitors" => 2, "percentage" => 66.7},
|
||||
%{"name" => "Firefox", "visitors" => 1, "percentage" => 33.3}
|
||||
]
|
||||
@ -154,7 +154,7 @@ defmodule PlausibleWeb.Api.StatsController.BrowsersTest do
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/browsers?period=day&with_imported=true")
|
||||
|
||||
assert json_response(conn, 200) == []
|
||||
assert json_response(conn, 200)["results"] == []
|
||||
end
|
||||
|
||||
test "returns (not set) when appropriate", %{conn: conn, site: site} do
|
||||
@ -167,7 +167,7 @@ defmodule PlausibleWeb.Api.StatsController.BrowsersTest do
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/browsers?period=day")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "(not set)", "visitors" => 1, "percentage" => 100.0}
|
||||
]
|
||||
end
|
||||
@ -185,7 +185,7 @@ defmodule PlausibleWeb.Api.StatsController.BrowsersTest do
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/browsers?period=day&with_imported=true")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "(not set)", "visitors" => 2, "percentage" => 100.0}
|
||||
]
|
||||
end
|
||||
@ -220,6 +220,7 @@ defmodule PlausibleWeb.Api.StatsController.BrowsersTest do
|
||||
"/api/stats/#{site.domain}/browser-versions?period=day&filters=#{filters}"
|
||||
)
|
||||
|> json_response(200)
|
||||
|> Map.get("results")
|
||||
|
||||
assert %{
|
||||
"browser" => "Chrome",
|
||||
@ -254,7 +255,7 @@ defmodule PlausibleWeb.Api.StatsController.BrowsersTest do
|
||||
"/api/stats/#{site.domain}/browser-versions?period=day&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
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"}
|
||||
]
|
||||
@ -273,7 +274,7 @@ defmodule PlausibleWeb.Api.StatsController.BrowsersTest do
|
||||
"/api/stats/#{site.domain}/browser-versions?period=day&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "(not set)",
|
||||
"visitors" => 1,
|
||||
@ -317,7 +318,7 @@ defmodule PlausibleWeb.Api.StatsController.BrowsersTest do
|
||||
"/api/stats/#{site.domain}/browser-versions?period=day&date=2021-01-01&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"browser" => "(not set)",
|
||||
"name" => "(not set)",
|
||||
|
@ -37,7 +37,7 @@ defmodule PlausibleWeb.Api.StatsController.CitiesTest do
|
||||
test "returns top cities by new visitors", %{conn: conn, site: site} do
|
||||
conn = get(conn, "/api/stats/#{site.domain}/cities?period=day")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
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}
|
||||
]
|
||||
@ -47,7 +47,7 @@ defmodule PlausibleWeb.Api.StatsController.CitiesTest do
|
||||
filters = Jason.encode!(%{city: "591632"})
|
||||
conn = get(conn, "/api/stats/#{site.domain}/cities?period=day&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"code" => 591_632, "country_flag" => "🇪🇪", "name" => "Kärdla", "visitors" => 2}
|
||||
]
|
||||
end
|
||||
@ -61,7 +61,7 @@ defmodule PlausibleWeb.Api.StatsController.CitiesTest do
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/cities?period=day&with_imported=true")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
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}
|
||||
]
|
||||
|
@ -32,7 +32,7 @@ defmodule PlausibleWeb.Api.StatsController.ConversionsTest do
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/conversions?period=day")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "Signup",
|
||||
"visitors" => 2,
|
||||
@ -79,7 +79,7 @@ defmodule PlausibleWeb.Api.StatsController.ConversionsTest do
|
||||
filters = Jason.encode!(%{props: %{"logged_in" => "true"}})
|
||||
conn = get(conn, "/api/stats/#{site.domain}/conversions?period=day&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "Payment",
|
||||
"visitors" => 1,
|
||||
@ -119,7 +119,7 @@ defmodule PlausibleWeb.Api.StatsController.ConversionsTest do
|
||||
filters = Jason.encode!(%{props: %{"logged_in" => "!true"}})
|
||||
conn = get(conn, "/api/stats/#{site.domain}/conversions?period=day&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "Payment",
|
||||
"visitors" => 2,
|
||||
@ -157,7 +157,7 @@ defmodule PlausibleWeb.Api.StatsController.ConversionsTest do
|
||||
filters = Jason.encode!(%{props: %{"logged_in" => "(none)"}})
|
||||
conn = get(conn, "/api/stats/#{site.domain}/conversions?period=day&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "Payment",
|
||||
"visitors" => 2,
|
||||
@ -197,7 +197,7 @@ defmodule PlausibleWeb.Api.StatsController.ConversionsTest do
|
||||
filters = Jason.encode!(%{props: %{"logged_in" => "!(none)"}})
|
||||
conn = get(conn, "/api/stats/#{site.domain}/conversions?period=day&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "Payment",
|
||||
"visitors" => 2,
|
||||
@ -215,6 +215,7 @@ defmodule PlausibleWeb.Api.StatsController.ConversionsTest do
|
||||
conn
|
||||
|> get("/api/stats/#{site.domain}/conversions?period=day&filters=#{filters}")
|
||||
|> json_response(200)
|
||||
|> Map.get("results")
|
||||
|
||||
assert resp == []
|
||||
end
|
||||
@ -249,7 +250,7 @@ defmodule PlausibleWeb.Api.StatsController.ConversionsTest do
|
||||
filters = Jason.encode!(%{browser: "Firefox"})
|
||||
conn = get(conn, "/api/stats/#{site.domain}/conversions?period=day&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "Payment",
|
||||
"visitors" => 1,
|
||||
@ -294,7 +295,7 @@ defmodule PlausibleWeb.Api.StatsController.ConversionsTest do
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/conversions?period=day")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "Payment",
|
||||
"visitors" => 5,
|
||||
@ -340,7 +341,7 @@ defmodule PlausibleWeb.Api.StatsController.ConversionsTest do
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/conversions?period=day")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "Payment",
|
||||
"visitors" => 5,
|
||||
@ -372,7 +373,7 @@ defmodule PlausibleWeb.Api.StatsController.ConversionsTest do
|
||||
insert(:goal, %{site: site, event_name: "Payment", currency: :EUR})
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/conversions?period=day")
|
||||
response = json_response(conn, 200)
|
||||
response = json_response(conn, 200)["results"]
|
||||
|
||||
assert [
|
||||
%{
|
||||
@ -414,7 +415,7 @@ defmodule PlausibleWeb.Api.StatsController.ConversionsTest do
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/conversions?period=day")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "Signup",
|
||||
"visitors" => 1,
|
||||
@ -447,6 +448,7 @@ defmodule PlausibleWeb.Api.StatsController.ConversionsTest do
|
||||
|
||||
get(conn, path <> query)
|
||||
|> json_response(200)
|
||||
|> Map.get("results")
|
||||
end
|
||||
|
||||
expected = [
|
||||
@ -488,6 +490,7 @@ defmodule PlausibleWeb.Api.StatsController.ConversionsTest do
|
||||
|
||||
get(conn, path <> query)
|
||||
|> json_response(200)
|
||||
|> Map.get("results")
|
||||
end
|
||||
|
||||
expected = [
|
||||
@ -539,7 +542,7 @@ defmodule PlausibleWeb.Api.StatsController.ConversionsTest do
|
||||
"/api/stats/#{site.domain}/conversions?period=day&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "Signup",
|
||||
"visitors" => 2,
|
||||
@ -573,7 +576,7 @@ defmodule PlausibleWeb.Api.StatsController.ConversionsTest do
|
||||
"/api/stats/#{site.domain}/conversions?period=day&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "Visit /blog/**",
|
||||
"visitors" => 2,
|
||||
@ -611,7 +614,7 @@ defmodule PlausibleWeb.Api.StatsController.ConversionsTest do
|
||||
"/api/stats/#{site.domain}/conversions?period=day&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "Visit /blog**",
|
||||
"visitors" => 2,
|
||||
@ -649,7 +652,7 @@ defmodule PlausibleWeb.Api.StatsController.ConversionsTest do
|
||||
"/api/stats/#{site.domain}/conversions?period=day&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "Signup",
|
||||
"visitors" => 1,
|
||||
@ -713,7 +716,7 @@ defmodule PlausibleWeb.Api.StatsController.ConversionsTest do
|
||||
"/api/stats/#{site.domain}/conversions?period=day&date=2019-07-01"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"conversion_rate" => 100.0,
|
||||
"visitors" => 8,
|
||||
@ -801,7 +804,291 @@ defmodule PlausibleWeb.Api.StatsController.ConversionsTest do
|
||||
"events" => 3,
|
||||
"conversion_rate" => 37.5
|
||||
}
|
||||
] = json_response(conn, 200)
|
||||
] = json_response(conn, 200)["results"]
|
||||
end
|
||||
|
||||
test "returns only custom event goals with a custom event goal filter", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
insert(:goal, site: site, event_name: "Purchase")
|
||||
insert(:goal, site: site, event_name: "Activation")
|
||||
insert(:goal, site: site, page_path: "/test")
|
||||
|
||||
site_import = insert(:site_import, site: site)
|
||||
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:pageview,
|
||||
timestamp: ~N[2021-01-01 00:00:01],
|
||||
pathname: "/test"
|
||||
),
|
||||
build(:event,
|
||||
name: "Purchase",
|
||||
timestamp: ~N[2021-01-01 00:00:03]
|
||||
),
|
||||
build(:event,
|
||||
name: "Purchase",
|
||||
timestamp: ~N[2021-01-01 00:00:03]
|
||||
),
|
||||
build(:imported_custom_events,
|
||||
name: "Purchase",
|
||||
visitors: 3,
|
||||
events: 5,
|
||||
date: ~D[2021-01-01]
|
||||
),
|
||||
build(:imported_pages,
|
||||
page: "/test",
|
||||
visitors: 2,
|
||||
pageviews: 2,
|
||||
date: ~D[2021-01-01]
|
||||
),
|
||||
build(:imported_visitors, visitors: 5, date: ~D[2021-01-01])
|
||||
])
|
||||
|
||||
filters = Jason.encode!(%{goal: "Purchase"})
|
||||
url_query_params = "?filters=#{filters}&period=day&date=2021-01-01&with_imported=true"
|
||||
conn = get(conn, "/api/stats/#{site.domain}/conversions#{url_query_params}")
|
||||
|
||||
assert [
|
||||
%{
|
||||
"name" => "Purchase",
|
||||
"visitors" => 5,
|
||||
"events" => 7,
|
||||
"conversion_rate" => 62.5
|
||||
}
|
||||
] = json_response(conn, 200)["results"]
|
||||
end
|
||||
|
||||
test "returns custom event goals with more than one option in goal filter", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
insert(:goal, site: site, event_name: "Purchase")
|
||||
insert(:goal, site: site, event_name: "Activation")
|
||||
insert(:goal, site: site, page_path: "/test")
|
||||
|
||||
site_import = insert(:site_import, site: site)
|
||||
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:pageview,
|
||||
timestamp: ~N[2021-01-01 00:00:01],
|
||||
pathname: "/test"
|
||||
),
|
||||
build(:event,
|
||||
name: "Purchase",
|
||||
timestamp: ~N[2021-01-01 00:00:03]
|
||||
),
|
||||
build(:event,
|
||||
name: "Purchase",
|
||||
timestamp: ~N[2021-01-01 00:00:03]
|
||||
),
|
||||
build(:event,
|
||||
name: "Activation",
|
||||
timestamp: ~N[2021-01-01 00:00:03]
|
||||
),
|
||||
build(:imported_custom_events,
|
||||
name: "Purchase",
|
||||
visitors: 3,
|
||||
events: 5,
|
||||
date: ~D[2021-01-01]
|
||||
),
|
||||
build(:imported_custom_events,
|
||||
name: "Activation",
|
||||
visitors: 2,
|
||||
events: 4,
|
||||
date: ~D[2021-01-01]
|
||||
),
|
||||
build(:imported_pages,
|
||||
page: "/test",
|
||||
visitors: 2,
|
||||
pageviews: 2,
|
||||
date: ~D[2021-01-01]
|
||||
),
|
||||
build(:imported_visitors, visitors: 5, date: ~D[2021-01-01])
|
||||
])
|
||||
|
||||
filters = Jason.encode!(%{goal: "Purchase|Activation"})
|
||||
url_query_params = "?filters=#{filters}&period=day&date=2021-01-01&with_imported=true"
|
||||
conn = get(conn, "/api/stats/#{site.domain}/conversions#{url_query_params}")
|
||||
|
||||
assert [
|
||||
%{
|
||||
"name" => "Purchase",
|
||||
"visitors" => 5,
|
||||
"events" => 7,
|
||||
"conversion_rate" => 55.6
|
||||
},
|
||||
%{
|
||||
"name" => "Activation",
|
||||
"visitors" => 3,
|
||||
"events" => 5,
|
||||
"conversion_rate" => 33.3
|
||||
}
|
||||
] = json_response(conn, 200)["results"]
|
||||
end
|
||||
|
||||
test "returns only pageview goals with a pageview goal filter", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
insert(:goal, site: site, event_name: "Purchase")
|
||||
insert(:goal, site: site, event_name: "Activation")
|
||||
insert(:goal, site: site, page_path: "/test")
|
||||
|
||||
site_import = insert(:site_import, site: site)
|
||||
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:pageview,
|
||||
timestamp: ~N[2021-01-01 00:00:01],
|
||||
pathname: "/test"
|
||||
),
|
||||
build(:event,
|
||||
name: "Purchase",
|
||||
timestamp: ~N[2021-01-01 00:00:03]
|
||||
),
|
||||
build(:event,
|
||||
name: "Purchase",
|
||||
timestamp: ~N[2021-01-01 00:00:03]
|
||||
),
|
||||
build(:imported_custom_events,
|
||||
name: "Purchase",
|
||||
visitors: 3,
|
||||
events: 5,
|
||||
date: ~D[2021-01-01]
|
||||
),
|
||||
build(:imported_pages,
|
||||
page: "/test",
|
||||
visitors: 2,
|
||||
pageviews: 2,
|
||||
date: ~D[2021-01-01]
|
||||
),
|
||||
build(:imported_visitors, visitors: 5, date: ~D[2021-01-01])
|
||||
])
|
||||
|
||||
filters = Jason.encode!(%{goal: "Visit /test"})
|
||||
url_query_params = "?filters=#{filters}&period=day&date=2021-01-01&with_imported=true"
|
||||
conn = get(conn, "/api/stats/#{site.domain}/conversions#{url_query_params}")
|
||||
|
||||
assert [
|
||||
%{
|
||||
"name" => "Visit /test",
|
||||
"visitors" => 3,
|
||||
"events" => 3,
|
||||
"conversion_rate" => 37.5
|
||||
}
|
||||
] = json_response(conn, 200)["results"]
|
||||
end
|
||||
|
||||
test "returns pageview goals with more than one option in pageview goal filter", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
insert(:goal, site: site, event_name: "Purchase")
|
||||
insert(:goal, site: site, event_name: "Activation")
|
||||
insert(:goal, site: site, page_path: "/test")
|
||||
insert(:goal, site: site, page_path: "/blog")
|
||||
|
||||
site_import = insert(:site_import, site: site)
|
||||
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:pageview,
|
||||
timestamp: ~N[2021-01-01 00:00:01],
|
||||
pathname: "/test"
|
||||
),
|
||||
build(:pageview,
|
||||
timestamp: ~N[2021-01-01 00:00:01],
|
||||
pathname: "/blog"
|
||||
),
|
||||
build(:event,
|
||||
name: "Purchase",
|
||||
timestamp: ~N[2021-01-01 00:00:03]
|
||||
),
|
||||
build(:event,
|
||||
name: "Purchase",
|
||||
timestamp: ~N[2021-01-01 00:00:03]
|
||||
),
|
||||
build(:imported_custom_events,
|
||||
name: "Purchase",
|
||||
visitors: 3,
|
||||
events: 5,
|
||||
date: ~D[2021-01-01]
|
||||
),
|
||||
build(:imported_pages,
|
||||
page: "/test",
|
||||
visitors: 2,
|
||||
pageviews: 2,
|
||||
date: ~D[2021-01-01]
|
||||
),
|
||||
build(:imported_pages,
|
||||
page: "/blog",
|
||||
visitors: 1,
|
||||
pageviews: 1,
|
||||
date: ~D[2021-01-01]
|
||||
),
|
||||
build(:imported_visitors, visitors: 5, date: ~D[2021-01-01])
|
||||
])
|
||||
|
||||
filters = Jason.encode!(%{goal: "Visit /test|Visit /blog"})
|
||||
url_query_params = "?filters=#{filters}&period=day&date=2021-01-01&with_imported=true"
|
||||
conn = get(conn, "/api/stats/#{site.domain}/conversions#{url_query_params}")
|
||||
|
||||
assert [
|
||||
%{
|
||||
"name" => "Visit /test",
|
||||
"visitors" => 3,
|
||||
"events" => 3,
|
||||
"conversion_rate" => 33.3
|
||||
},
|
||||
%{
|
||||
"name" => "Visit /blog",
|
||||
"visitors" => 2,
|
||||
"events" => 2,
|
||||
"conversion_rate" => 22.2
|
||||
}
|
||||
] = json_response(conn, 200)["results"]
|
||||
end
|
||||
|
||||
test "returns pageview goals with a page filter", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
insert(:goal, site: site, page_path: "/blog/two")
|
||||
insert(:goal, site: site, page_path: "/blog/thr**")
|
||||
insert(:goal, site: site, page_path: "/blog/*")
|
||||
|
||||
site_import = insert(:site_import, site: site)
|
||||
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:imported_pages, page: "/", visitors: 1, pageviews: 1, date: ~D[2021-01-01]),
|
||||
build(:imported_pages, page: "/blog/one", visitors: 2, pageviews: 2, date: ~D[2021-01-01]),
|
||||
build(:imported_pages, page: "/blog/two", visitors: 3, pageviews: 3, date: ~D[2021-01-01]),
|
||||
build(:imported_pages,
|
||||
page: "/blog/three",
|
||||
visitors: 4,
|
||||
pageviews: 4,
|
||||
date: ~D[2021-01-01]
|
||||
),
|
||||
build(:imported_visitors, visitors: 10, date: ~D[2021-01-01])
|
||||
])
|
||||
|
||||
filters = Jason.encode!(%{page: "/blog/one|/blog/two"})
|
||||
q = "?filters=#{filters}&period=day&date=2021-01-01&with_imported=true"
|
||||
conn = get(conn, "/api/stats/#{site.domain}/conversions#{q}")
|
||||
|
||||
assert [
|
||||
%{
|
||||
"name" => "Visit /blog/*",
|
||||
"visitors" => 5,
|
||||
"events" => 5,
|
||||
"conversion_rate" => 100.0
|
||||
},
|
||||
%{
|
||||
"name" => "Visit /blog/two",
|
||||
"visitors" => 3,
|
||||
"events" => 3,
|
||||
"conversion_rate" => 60.0
|
||||
}
|
||||
] = json_response(conn, 200)["results"]
|
||||
end
|
||||
|
||||
test "calculates conversion_rate for goals with glob pattern with imported data", %{
|
||||
@ -832,7 +1119,7 @@ defmodule PlausibleWeb.Api.StatsController.ConversionsTest do
|
||||
"/api/stats/#{site.domain}/conversions?period=day"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "Visit /blog**",
|
||||
"visitors" => 2,
|
||||
|
@ -16,7 +16,7 @@ defmodule PlausibleWeb.Api.StatsController.CountriesTest do
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/countries?period=day")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"code" => "EE",
|
||||
"alpha_3" => "EST",
|
||||
@ -37,7 +37,7 @@ defmodule PlausibleWeb.Api.StatsController.CountriesTest do
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/countries?period=day&with_imported=true")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"code" => "EE",
|
||||
"alpha_3" => "EST",
|
||||
@ -65,7 +65,7 @@ defmodule PlausibleWeb.Api.StatsController.CountriesTest do
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/countries?period=day&with_imported=true")
|
||||
|
||||
assert json_response(conn, 200) == []
|
||||
assert json_response(conn, 200)["results"] == []
|
||||
end
|
||||
|
||||
test "calculates conversion_rate when filtering for goal", %{conn: conn, site: site} do
|
||||
@ -90,7 +90,7 @@ defmodule PlausibleWeb.Api.StatsController.CountriesTest do
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/countries?period=day&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"code" => "EE",
|
||||
"alpha_3" => "EST",
|
||||
@ -140,7 +140,7 @@ defmodule PlausibleWeb.Api.StatsController.CountriesTest do
|
||||
filters = Jason.encode!(%{props: %{"author" => "John Doe"}})
|
||||
conn = get(conn, "/api/stats/#{site.domain}/countries?period=day&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"code" => "EE",
|
||||
"alpha_3" => "EST",
|
||||
@ -182,7 +182,7 @@ defmodule PlausibleWeb.Api.StatsController.CountriesTest do
|
||||
filters = Jason.encode!(%{props: %{"author" => "!John Doe"}})
|
||||
conn = get(conn, "/api/stats/#{site.domain}/countries?period=day&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"code" => "GB",
|
||||
"alpha_3" => "GBR",
|
||||
@ -217,7 +217,7 @@ defmodule PlausibleWeb.Api.StatsController.CountriesTest do
|
||||
filters = Jason.encode!(%{props: %{"author" => "(none)"}})
|
||||
conn = get(conn, "/api/stats/#{site.domain}/countries?period=day&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"code" => "GB",
|
||||
"alpha_3" => "GBR",
|
||||
@ -257,7 +257,7 @@ defmodule PlausibleWeb.Api.StatsController.CountriesTest do
|
||||
filters = Jason.encode!(%{props: %{"author" => "!(none)"}})
|
||||
conn = get(conn, "/api/stats/#{site.domain}/countries?period=day&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"code" => "EE",
|
||||
"alpha_3" => "EST",
|
||||
@ -279,7 +279,7 @@ defmodule PlausibleWeb.Api.StatsController.CountriesTest do
|
||||
filters = Jason.encode!(%{country: "GB"})
|
||||
conn = get(conn, "/api/stats/#{site.domain}/countries?period=day&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"code" => "GB",
|
||||
"alpha_3" => "GBR",
|
||||
|
@ -21,7 +21,7 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
|
||||
"/api/stats/#{site.domain}/custom-prop-values/#{prop_key}?period=day"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"visitors" => 2,
|
||||
"name" => "K2sna Kalle",
|
||||
@ -57,7 +57,7 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
|
||||
"/api/stats/#{site.domain}/custom-prop-values/#{prop_key}?period=day&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"visitors" => 1,
|
||||
"name" => "K2sna Kalle",
|
||||
@ -65,6 +65,8 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
|
||||
"percentage" => 100.0
|
||||
}
|
||||
]
|
||||
|
||||
refute json_response(conn, 200)["warning"]
|
||||
end
|
||||
|
||||
test "returns (none) values in the breakdown", %{conn: conn, site: site} do
|
||||
@ -82,7 +84,7 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
|
||||
"/api/stats/#{site.domain}/custom-prop-values/#{prop_key}?period=day"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"visitors" => 2,
|
||||
"name" => "K2sna Kalle",
|
||||
@ -122,7 +124,7 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
|
||||
"/api/stats/#{site.domain}/custom-prop-values/#{prop_key}?period=day&limit=2&page=2"
|
||||
)
|
||||
|
||||
assert json_response(conn1, 200) == [
|
||||
assert json_response(conn1, 200)["results"] == [
|
||||
%{
|
||||
"visitors" => 3,
|
||||
"name" => "Tiit",
|
||||
@ -137,7 +139,7 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
|
||||
}
|
||||
]
|
||||
|
||||
assert json_response(conn2, 200) == [
|
||||
assert json_response(conn2, 200)["results"] == [
|
||||
%{
|
||||
"visitors" => 1,
|
||||
"name" => "(none)",
|
||||
@ -171,7 +173,7 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
|
||||
"/api/stats/#{site.domain}/custom-prop-values/#{prop_key}?period=day&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"visitors" => 2,
|
||||
"name" => "B",
|
||||
@ -207,7 +209,7 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
|
||||
"/api/stats/#{site.domain}/custom-prop-values/#{prop_key}?period=day&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"visitors" => 2,
|
||||
"name" => "(none)",
|
||||
@ -250,7 +252,7 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
|
||||
"/api/stats/#{site.domain}/custom-prop-values/cost?period=day&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "0",
|
||||
"visitors" => 1,
|
||||
@ -287,7 +289,7 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
|
||||
"/api/stats/#{site.domain}/custom-prop-values/cost?period=day&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "(none)",
|
||||
"visitors" => 1,
|
||||
@ -334,7 +336,7 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
|
||||
"/api/stats/#{site.domain}/custom-prop-values/cost?period=day&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "20",
|
||||
"visitors" => 2,
|
||||
@ -377,7 +379,7 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
|
||||
"/api/stats/#{site.domain}/custom-prop-values/cost?period=day&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "0",
|
||||
"visitors" => 1,
|
||||
@ -424,7 +426,7 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
|
||||
"/api/stats/#{site.domain}/custom-prop-values/cost?period=day&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "1",
|
||||
"visitors" => 2,
|
||||
@ -475,7 +477,7 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
|
||||
"/api/stats/#{site.domain}/custom-prop-values/cost?period=day&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "1",
|
||||
"visitors" => 2,
|
||||
@ -533,7 +535,7 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
|
||||
"/api/stats/#{site.domain}/custom-prop-values/cost?period=day&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "20",
|
||||
"visitors" => 2,
|
||||
@ -584,7 +586,7 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
|
||||
"/api/stats/#{site.domain}/custom-prop-values/cost?period=day&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "20",
|
||||
"visitors" => 2,
|
||||
@ -611,7 +613,7 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
|
||||
"/api/stats/#{site.domain}/custom-prop-values/variant?period=day&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"visitors" => 2,
|
||||
"name" => "A",
|
||||
@ -645,7 +647,7 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
|
||||
"/api/stats/#{site.domain}/custom-prop-values/#{prop_key}?period=day&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"visitors" => 1,
|
||||
"name" => "B",
|
||||
@ -690,7 +692,7 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
|
||||
"/api/stats/#{site.domain}/custom-prop-values/#{prop_key}?period=day&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "A",
|
||||
"visitors" => 1,
|
||||
@ -735,7 +737,7 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
|
||||
"/api/stats/#{site.domain}/custom-prop-values/#{prop_key}?period=day&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "A",
|
||||
"visitors" => 1,
|
||||
@ -783,7 +785,7 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
|
||||
"/api/stats/#{site.domain}/custom-prop-values/#{prop_key}?period=day&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"visitors" => 2,
|
||||
"name" => "true",
|
||||
@ -844,7 +846,7 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
|
||||
"/api/stats/#{site.domain}/custom-prop-values/#{prop_key}?period=day&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"visitors" => 2,
|
||||
"name" => "true",
|
||||
@ -889,6 +891,7 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
|
||||
|
||||
returned_metrics =
|
||||
json_response(conn, 200)
|
||||
|> Map.get("results")
|
||||
|> List.first()
|
||||
|> Map.keys()
|
||||
|
||||
@ -916,7 +919,7 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
|
||||
"/api/stats/#{site.domain}/custom-prop-values/#{prop_key}?period=day&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"visitors" => 1,
|
||||
"name" => "Sipsik",
|
||||
@ -946,7 +949,7 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
|
||||
"/api/stats/#{site.domain}/custom-prop-values/#{prop_key}?period=day&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"visitors" => 1,
|
||||
"name" => "Sipsik",
|
||||
@ -973,7 +976,7 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
|
||||
"/api/stats/#{site.domain}/custom-prop-values/#{prop_key}?period=day&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"visitors" => 1,
|
||||
"name" => "Sipsik",
|
||||
@ -1004,7 +1007,7 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
|
||||
"/api/stats/#{site.domain}/custom-prop-values/#{prop_key}?period=day&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"visitors" => 2,
|
||||
"name" => "K2sna Kalle",
|
||||
@ -1040,7 +1043,7 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
|
||||
"/api/stats/#{site.domain}/custom-prop-values/key?period=day&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"visitors" => 2,
|
||||
"name" => "bar",
|
||||
@ -1078,7 +1081,7 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
|
||||
"/api/stats/#{site.domain}/custom-prop-values/key?period=day&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"visitors" => 1,
|
||||
"name" => "bar",
|
||||
@ -1116,11 +1119,13 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
|
||||
conn
|
||||
|> get("/api/stats/#{site.domain}/custom-prop-values/url?period=day")
|
||||
|> json_response(200)
|
||||
|> Map.get("results")
|
||||
|
||||
[%{"visitors" => 1, "name" => "two"}] =
|
||||
conn
|
||||
|> get("/api/stats/#{site.domain}/custom-prop-values/path?period=day")
|
||||
|> json_response(200)
|
||||
|> Map.get("results")
|
||||
end
|
||||
|
||||
test "returns 402 'upgrade required' for any other prop key", %{conn: conn, site: site} do
|
||||
@ -1136,7 +1141,7 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
|
||||
describe "with imported data" do
|
||||
setup [:create_user, :log_in, :create_new_site]
|
||||
|
||||
for goal_name <- ["Outbound Link: Click", "File Download"] do
|
||||
for goal_name <- ["Outbound Link: Click", "File Download", "Cloaked Link: Click"] do
|
||||
test "returns url breakdown for #{goal_name} goal", %{conn: conn, site: site} do
|
||||
insert(:goal, event_name: unquote(goal_name), site: site)
|
||||
site_import = insert(:site_import, site: site)
|
||||
@ -1175,7 +1180,7 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
|
||||
"/api/stats/#{site.domain}/custom-prop-values/url?period=day&with_imported=true&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"visitors" => 5,
|
||||
"name" => "https://two.com",
|
||||
@ -1191,5 +1196,58 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
for goal_name <- ["Outbound Link: Click", "File Download", "Cloaked Link: Click"] do
|
||||
test "returns url breakdown for #{goal_name} goal with a url filter", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
insert(:goal, event_name: unquote(goal_name), site: site)
|
||||
site_import = insert(:site_import, site: site)
|
||||
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:event,
|
||||
name: unquote(goal_name),
|
||||
"meta.key": ["url"],
|
||||
"meta.value": ["https://one.com"]
|
||||
),
|
||||
build(:imported_custom_events,
|
||||
name: unquote(goal_name),
|
||||
visitors: 2,
|
||||
events: 5,
|
||||
link_url: "https://one.com"
|
||||
),
|
||||
build(:imported_custom_events,
|
||||
name: unquote(goal_name),
|
||||
visitors: 5,
|
||||
events: 10,
|
||||
link_url: "https://two.com"
|
||||
),
|
||||
build(:imported_custom_events,
|
||||
name: "view_search_results",
|
||||
visitors: 100,
|
||||
events: 200
|
||||
),
|
||||
build(:imported_visitors, visitors: 9)
|
||||
])
|
||||
|
||||
filters = Jason.encode!(%{goal: unquote(goal_name), props: %{url: "https://two.com"}})
|
||||
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
"/api/stats/#{site.domain}/custom-prop-values/url?period=day&with_imported=true&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"visitors" => 5,
|
||||
"name" => "https://two.com",
|
||||
"events" => 10,
|
||||
"conversion_rate" => 50.0
|
||||
}
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -261,13 +261,16 @@ defmodule PlausibleWeb.Api.StatsController.ImportedTest do
|
||||
"imported_sources"
|
||||
)
|
||||
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
results =
|
||||
conn
|
||||
|> get(
|
||||
"/api/stats/#{site.domain}/sources?period=month&date=2021-01-01&with_imported=true"
|
||||
)
|
||||
|> json_response(200)
|
||||
|> Map.get("results")
|
||||
|> Enum.sort()
|
||||
|
||||
assert conn |> json_response(200) |> Enum.sort() == [
|
||||
assert results == [
|
||||
%{"name" => "A Nice Newsletter", "visitors" => 1},
|
||||
%{"name" => "Direct / None", "visitors" => 1},
|
||||
%{"name" => "DuckDuckGo", "visitors" => 2},
|
||||
@ -338,7 +341,7 @@ defmodule PlausibleWeb.Api.StatsController.ImportedTest do
|
||||
"/api/stats/#{site.domain}/utm_mediums?period=day&date=2021-01-01&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"bounce_rate" => 100.0,
|
||||
"name" => "social",
|
||||
@ -420,7 +423,7 @@ defmodule PlausibleWeb.Api.StatsController.ImportedTest do
|
||||
"/api/stats/#{site.domain}/utm_campaigns?period=day&date=2021-01-01&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "august",
|
||||
"visitors" => 2,
|
||||
@ -509,7 +512,7 @@ defmodule PlausibleWeb.Api.StatsController.ImportedTest do
|
||||
"/api/stats/#{site.domain}/utm_terms?period=day&date=2021-01-01&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "Sweden",
|
||||
"visitors" => 3,
|
||||
@ -597,7 +600,7 @@ defmodule PlausibleWeb.Api.StatsController.ImportedTest do
|
||||
"/api/stats/#{site.domain}/utm_contents?period=day&date=2021-01-01&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "blog",
|
||||
"visitors" => 2,
|
||||
@ -703,7 +706,7 @@ defmodule PlausibleWeb.Api.StatsController.ImportedTest do
|
||||
"/api/stats/#{site.domain}/pages?period=day&date=2021-01-01&detailed=true&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"bounce_rate" => nil,
|
||||
"time_on_page" => 60,
|
||||
@ -786,7 +789,7 @@ defmodule PlausibleWeb.Api.StatsController.ImportedTest do
|
||||
"/api/stats/#{site.domain}/exit-pages?period=day&date=2021-01-01&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "/page2",
|
||||
"visitors" => 3,
|
||||
@ -859,7 +862,7 @@ defmodule PlausibleWeb.Api.StatsController.ImportedTest do
|
||||
"/api/stats/#{site.domain}/cities?period=day&date=2021-01-01&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"code" => 588_335, "name" => "Tartu", "visitors" => 1, "country_flag" => "🇪🇪"},
|
||||
%{
|
||||
"code" => 2_650_225,
|
||||
@ -933,7 +936,7 @@ defmodule PlausibleWeb.Api.StatsController.ImportedTest do
|
||||
"/api/stats/#{site.domain}/countries?period=day&date=2021-01-01&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"code" => "EE",
|
||||
"alpha_3" => "EST",
|
||||
@ -997,7 +1000,7 @@ defmodule PlausibleWeb.Api.StatsController.ImportedTest do
|
||||
"/api/stats/#{site.domain}/screen-sizes?period=day&date=2021-01-01&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "Desktop", "visitors" => 2, "percentage" => 40},
|
||||
%{"name" => "Laptop", "visitors" => 2, "percentage" => 40},
|
||||
%{"name" => "Mobile", "visitors" => 1, "percentage" => 20}
|
||||
@ -1050,7 +1053,7 @@ defmodule PlausibleWeb.Api.StatsController.ImportedTest do
|
||||
"/api/stats/#{site.domain}/browsers?period=day&date=2021-01-01&with_imported=true"
|
||||
)
|
||||
|
||||
assert stats = json_response(conn, 200)
|
||||
assert stats = json_response(conn, 200)["results"]
|
||||
assert length(stats) == 3
|
||||
assert %{"name" => "Firefox", "visitors" => 2, "percentage" => 50.0} in stats
|
||||
assert %{"name" => "Mobile App", "visitors" => 1, "percentage" => 25.0} in stats
|
||||
@ -1104,7 +1107,7 @@ defmodule PlausibleWeb.Api.StatsController.ImportedTest do
|
||||
"/api/stats/#{site.domain}/operating-systems?period=day&date=2021-01-01&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "Mac", "visitors" => 3, "percentage" => 60},
|
||||
%{"name" => "GNU/Linux", "visitors" => 2, "percentage" => 40}
|
||||
]
|
||||
|
@ -53,7 +53,7 @@ defmodule PlausibleWeb.Api.StatsController.MainGraphTest do
|
||||
)
|
||||
|
||||
zeroes = List.duplicate(0, 30)
|
||||
assert %{"plot" => ^zeroes, "with_imported" => false} = json_response(conn, 200)
|
||||
assert %{"plot" => ^zeroes, "includes_imported" => false} = json_response(conn, 200)
|
||||
end
|
||||
|
||||
test "displays visitors for a day with imported data", %{conn: conn, site: site} do
|
||||
@ -70,7 +70,7 @@ defmodule PlausibleWeb.Api.StatsController.MainGraphTest do
|
||||
"/api/stats/#{site.domain}/main-graph?period=day&date=2021-01-01&with_imported=true"
|
||||
)
|
||||
|
||||
assert %{"plot" => plot, "imports_exist" => true, "with_imported" => true} =
|
||||
assert %{"plot" => plot, "imports_exist" => true, "includes_imported" => true} =
|
||||
json_response(conn, 200)
|
||||
|
||||
assert plot == [2] ++ List.duplicate(0, 23)
|
||||
@ -137,7 +137,7 @@ defmodule PlausibleWeb.Api.StatsController.MainGraphTest do
|
||||
"/api/stats/#{site.domain}/main-graph?period=month&date=2021-01-01&with_imported=true"
|
||||
)
|
||||
|
||||
assert %{"plot" => plot, "imports_exist" => true, "with_imported" => true} =
|
||||
assert %{"plot" => plot, "imports_exist" => true, "includes_imported" => true} =
|
||||
json_response(conn, 200)
|
||||
|
||||
assert Enum.count(plot) == 31
|
||||
@ -158,7 +158,7 @@ defmodule PlausibleWeb.Api.StatsController.MainGraphTest do
|
||||
"/api/stats/#{site.domain}/main-graph?period=month&date=2021-01-01&with_imported=true"
|
||||
)
|
||||
|
||||
assert %{"plot" => plot, "imports_exist" => true, "with_imported" => true} =
|
||||
assert %{"plot" => plot, "imports_exist" => true, "includes_imported" => true} =
|
||||
json_response(conn, 200)
|
||||
|
||||
assert Enum.count(plot) == 31
|
||||
@ -1157,7 +1157,7 @@ defmodule PlausibleWeb.Api.StatsController.MainGraphTest do
|
||||
"plot" => plot,
|
||||
"comparison_plot" => comparison_plot,
|
||||
"imports_exist" => true,
|
||||
"with_imported" => true
|
||||
"includes_imported" => true
|
||||
} = json_response(conn, 200)
|
||||
|
||||
assert 4 == Enum.sum(plot)
|
||||
@ -1203,7 +1203,7 @@ defmodule PlausibleWeb.Api.StatsController.MainGraphTest do
|
||||
"plot" => plot,
|
||||
"comparison_plot" => comparison_plot,
|
||||
"imports_exist" => true,
|
||||
"with_imported" => false
|
||||
"includes_imported" => false
|
||||
} = json_response(conn, 200)
|
||||
|
||||
assert 4 == Enum.sum(plot)
|
||||
@ -1233,7 +1233,7 @@ defmodule PlausibleWeb.Api.StatsController.MainGraphTest do
|
||||
"plot" => this_week_plot,
|
||||
"comparison_plot" => last_week_plot,
|
||||
"imports_exist" => true,
|
||||
"with_imported" => false
|
||||
"includes_imported" => false
|
||||
} = json_response(conn, 200)
|
||||
|
||||
assert this_week_plot == [50.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
|
||||
|
@ -13,7 +13,7 @@ defmodule PlausibleWeb.Api.StatsController.OperatingSystemsTest do
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/operating-systems?period=day")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "Mac", "visitors" => 2, "percentage" => 66.7},
|
||||
%{"name" => "Android", "visitors" => 1, "percentage" => 33.3}
|
||||
]
|
||||
@ -31,7 +31,7 @@ defmodule PlausibleWeb.Api.StatsController.OperatingSystemsTest do
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/operating-systems?period=day")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "(not set)", "visitors" => 1, "percentage" => 50},
|
||||
%{"name" => "Linux", "visitors" => 1, "percentage" => 50}
|
||||
]
|
||||
@ -41,7 +41,7 @@ defmodule PlausibleWeb.Api.StatsController.OperatingSystemsTest do
|
||||
conn =
|
||||
get(conn, "/api/stats/#{site.domain}/operating-systems?period=day&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "(not set)", "visitors" => 1, "percentage" => 100}
|
||||
]
|
||||
end
|
||||
@ -57,7 +57,7 @@ defmodule PlausibleWeb.Api.StatsController.OperatingSystemsTest do
|
||||
conn =
|
||||
get(conn, "/api/stats/#{site.domain}/operating-systems?period=day&with_imported=true")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "(not set)", "visitors" => 2, "percentage" => 100.0}
|
||||
]
|
||||
end
|
||||
@ -74,7 +74,7 @@ defmodule PlausibleWeb.Api.StatsController.OperatingSystemsTest do
|
||||
conn =
|
||||
get(conn, "/api/stats/#{site.domain}/operating-systems?period=day&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "Mac",
|
||||
"total_visitors" => 2,
|
||||
@ -114,7 +114,7 @@ defmodule PlausibleWeb.Api.StatsController.OperatingSystemsTest do
|
||||
conn =
|
||||
get(conn, "/api/stats/#{site.domain}/operating-systems?period=day&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "Mac", "visitors" => 1, "percentage" => 100}
|
||||
]
|
||||
end
|
||||
@ -151,7 +151,7 @@ defmodule PlausibleWeb.Api.StatsController.OperatingSystemsTest do
|
||||
conn =
|
||||
get(conn, "/api/stats/#{site.domain}/operating-systems?period=day&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "Android", "visitors" => 1, "percentage" => 50},
|
||||
%{"name" => "Mac", "visitors" => 1, "percentage" => 50}
|
||||
]
|
||||
@ -172,7 +172,7 @@ defmodule PlausibleWeb.Api.StatsController.OperatingSystemsTest do
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/operating-systems?period=day")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "Mac", "visitors" => 2, "percentage" => 66.7},
|
||||
%{"name" => "Android", "visitors" => 1, "percentage" => 33.3}
|
||||
]
|
||||
@ -180,7 +180,7 @@ defmodule PlausibleWeb.Api.StatsController.OperatingSystemsTest do
|
||||
conn =
|
||||
get(conn, "/api/stats/#{site.domain}/operating-systems?period=day&with_imported=true")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "Mac", "visitors" => 3, "percentage" => 60},
|
||||
%{"name" => "Android", "visitors" => 2, "percentage" => 40}
|
||||
]
|
||||
@ -199,7 +199,7 @@ defmodule PlausibleWeb.Api.StatsController.OperatingSystemsTest do
|
||||
conn =
|
||||
get(conn, "/api/stats/#{site.domain}/operating-systems?period=day&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "Mac",
|
||||
"total_visitors" => 2,
|
||||
@ -241,7 +241,7 @@ defmodule PlausibleWeb.Api.StatsController.OperatingSystemsTest do
|
||||
"/api/stats/#{site.domain}/operating-system-versions?period=day&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
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"}
|
||||
]
|
||||
@ -281,7 +281,7 @@ defmodule PlausibleWeb.Api.StatsController.OperatingSystemsTest do
|
||||
"/api/stats/#{site.domain}/operating-system-versions?period=day&date=2021-01-01&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"os" => "(not set)",
|
||||
"name" => "(not set)",
|
||||
|
@ -18,7 +18,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/pages?period=day")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"visitors" => 3, "name" => "/"},
|
||||
%{"visitors" => 2, "name" => "/register"},
|
||||
%{"visitors" => 1, "name" => "/contact"}
|
||||
@ -40,7 +40,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
filters = Jason.encode!(%{"hostname" => "*.example.com"})
|
||||
conn = get(conn1, "/api/stats/#{site.domain}/pages?period=day&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"visitors" => 3, "name" => "/"},
|
||||
%{"visitors" => 2, "name" => "/register"},
|
||||
%{"visitors" => 1, "name" => "/contact"},
|
||||
@ -50,7 +50,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
filters = Jason.encode!(%{"hostname" => "d.example.com"})
|
||||
conn = get(conn1, "/api/stats/#{site.domain}/pages?period=day&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"visitors" => 2, "name" => "/register"},
|
||||
%{"visitors" => 1, "name" => "/"}
|
||||
]
|
||||
@ -74,7 +74,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
filters = Jason.encode!(%{props: %{"author" => "John Doe"}})
|
||||
conn = get(conn, "/api/stats/#{site.domain}/pages?period=day&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"visitors" => 1, "name" => "/blog/john-1"}
|
||||
]
|
||||
end
|
||||
@ -100,7 +100,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
filters = Jason.encode!(%{props: %{"author" => "!John Doe"}})
|
||||
conn = get(conn, "/api/stats/#{site.domain}/pages?period=day&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"visitors" => 1, "name" => "/"},
|
||||
%{"visitors" => 1, "name" => "/blog/other-post"}
|
||||
]
|
||||
@ -137,7 +137,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
filters = Jason.encode!(%{props: %{"prop" => "~bar"}})
|
||||
conn = get(conn, "/api/stats/#{site.domain}/pages?period=day&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"visitors" => 1, "name" => "/1"},
|
||||
%{"visitors" => 1, "name" => "/2"}
|
||||
]
|
||||
@ -179,7 +179,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
filters = Jason.encode!(%{props: %{"prop" => "~bar|nea"}})
|
||||
conn = get(conn, "/api/stats/#{site.domain}/pages?period=day&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"visitors" => 1, "name" => "/1"},
|
||||
%{"visitors" => 1, "name" => "/2"},
|
||||
%{"visitors" => 1, "name" => "/6"}
|
||||
@ -217,7 +217,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
filters = Jason.encode!(%{props: %{"prop" => "bar", "number" => "1"}})
|
||||
conn = get(conn, "/api/stats/#{site.domain}/pages?period=day&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"visitors" => 1, "name" => "/1"}
|
||||
]
|
||||
end
|
||||
@ -266,7 +266,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
"/api/stats/#{site.domain}/pages?period=day&date=2021-01-01&filters=#{filters}&detailed=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "/blog/john-2",
|
||||
"visitors" => 2,
|
||||
@ -328,7 +328,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
"/api/stats/#{site.domain}/pages?period=day&date=2021-01-01&filters=#{filters}&detailed=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "/blog",
|
||||
"visitors" => 2,
|
||||
@ -380,7 +380,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
"/api/stats/#{site.domain}/pages?period=day&date=2021-01-01&filters=#{filters}&detailed=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "/blog",
|
||||
"visitors" => 2,
|
||||
@ -436,7 +436,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
"/api/stats/#{site.domain}/pages?period=day&date=2021-01-01&filters=#{filters}&detailed=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "/blog/other-post",
|
||||
"visitors" => 2,
|
||||
@ -494,7 +494,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
conn =
|
||||
get(conn, "/api/stats/#{site.domain}/pages?period=day&date=2021-01-01&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "/firefox",
|
||||
"visitors" => 2
|
||||
@ -534,7 +534,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
conn =
|
||||
get(conn, "/api/stats/#{site.domain}/pages?period=day&date=2021-01-01&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "/safari",
|
||||
"visitors" => 1
|
||||
@ -578,7 +578,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
"/api/stats/#{site.domain}/pages?period=day&date=2021-01-01&filters=#{filters}&detailed=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "/",
|
||||
"visitors" => 2,
|
||||
@ -625,7 +625,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
"/api/stats/#{site.domain}/pages?period=day&date=2021-01-01&filters=#{filters}&detailed=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "/",
|
||||
"visitors" => 2,
|
||||
@ -679,7 +679,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
"/api/stats/#{site.domain}/pages?period=day&date=2021-01-01&filters=#{filters}&detailed=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "/",
|
||||
"visitors" => 2,
|
||||
@ -725,7 +725,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
"/api/stats/#{site.domain}/pages?period=day&date=2021-01-01&filters=#{filters}&detailed=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "/articles/post-1",
|
||||
"visitors" => 2,
|
||||
@ -777,7 +777,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
"/api/stats/#{site.domain}/pages?period=day&date=2021-01-01&filters=#{filters}&detailed=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "/blog/(/post-1",
|
||||
"visitors" => 1,
|
||||
@ -830,7 +830,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
"/api/stats/#{site.domain}/pages?period=day&date=2021-01-01&filters=#{filters}&detailed=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "/",
|
||||
"visitors" => 2,
|
||||
@ -862,7 +862,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/pages?period=day")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"visitors" => 3, "name" => "/"},
|
||||
%{"visitors" => 2, "name" => "/register"},
|
||||
%{"visitors" => 1, "name" => "/contact"}
|
||||
@ -870,13 +870,43 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/pages?period=day&with_imported=true")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"visitors" => 4, "name" => "/"},
|
||||
%{"visitors" => 3, "name" => "/register"},
|
||||
%{"visitors" => 1, "name" => "/contact"}
|
||||
]
|
||||
end
|
||||
|
||||
test "returns imported pages with a pageview goal filter", %{conn: conn, site: site} do
|
||||
insert(:goal, site: site, page_path: "/blog**")
|
||||
|
||||
populate_stats(site, [
|
||||
build(:imported_pages, page: "/blog"),
|
||||
build(:imported_pages, page: "/not-this"),
|
||||
build(:imported_pages, page: "/blog/post-1", visitors: 2),
|
||||
build(:imported_visitors, visitors: 4)
|
||||
])
|
||||
|
||||
filters = Jason.encode!(%{goal: "Visit /blog**"})
|
||||
q = "?period=day&filters=#{filters}&with_imported=true"
|
||||
conn = get(conn, "/api/stats/#{site.domain}/pages#{q}")
|
||||
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"visitors" => 2,
|
||||
"name" => "/blog/post-1",
|
||||
"conversion_rate" => 100.0,
|
||||
"total_visitors" => 2
|
||||
},
|
||||
%{
|
||||
"visitors" => 1,
|
||||
"name" => "/blog",
|
||||
"conversion_rate" => 100.0,
|
||||
"total_visitors" => 1
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
test "calculates bounce rate and time on page for pages", %{conn: conn, site: site} do
|
||||
populate_stats(site, [
|
||||
build(:pageview,
|
||||
@ -901,7 +931,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
"/api/stats/#{site.domain}/pages?period=day&date=2021-01-01&detailed=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"bounce_rate" => 50.0,
|
||||
"time_on_page" => 900.0,
|
||||
@ -948,7 +978,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
"/api/stats/#{site.domain}/pages?period=day&date=2021-01-01&detailed=true&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"bounce_rate" => 50,
|
||||
"name" => "/about",
|
||||
@ -1027,7 +1057,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
"/api/stats/#{site.domain}/pages?period=day&date=2021-01-01&detailed=true&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"bounce_rate" => 50,
|
||||
"name" => "/about-blog",
|
||||
@ -1055,6 +1085,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
conn
|
||||
|> get("/api/stats/#{site.domain}/pages?period=day&date=2021-01-01&detailed=true")
|
||||
|> json_response(200)
|
||||
|> Map.get("results")
|
||||
end
|
||||
|
||||
test "ignores page refresh when calculating time on page", %{conn: conn, site: site} do
|
||||
@ -1072,6 +1103,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
conn
|
||||
|> get("/api/stats/#{site.domain}/pages?period=day&date=2021-01-01&detailed=true")
|
||||
|> json_response(200)
|
||||
|> Map.get("results")
|
||||
end
|
||||
|
||||
test "calculates time on page per unique transition within session", %{conn: conn, site: site} do
|
||||
@ -1105,6 +1137,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
conn
|
||||
|> get("/api/stats/#{site.domain}/pages?period=day&date=2021-01-01&detailed=true")
|
||||
|> json_response(200)
|
||||
|> Map.get("results")
|
||||
end
|
||||
|
||||
test "calculates bounce rate and time on page for pages with imported data", %{
|
||||
@ -1150,7 +1183,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
"/api/stats/#{site.domain}/pages?period=day&date=2021-01-01&detailed=true&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"bounce_rate" => 40.0,
|
||||
"time_on_page" => 800.0,
|
||||
@ -1177,7 +1210,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/pages?period=realtime")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"visitors" => 2, "name" => "/page1"},
|
||||
%{"visitors" => 1, "name" => "/page2"}
|
||||
]
|
||||
@ -1195,10 +1228,160 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/pages?period=day&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"total_visitors" => 3, "visitors" => 1, "name" => "/", "conversion_rate" => 33.3}
|
||||
]
|
||||
end
|
||||
|
||||
test "filter by :is page with imported data", %{conn: conn, site: site} do
|
||||
site_import = insert(:site_import, site: site)
|
||||
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:pageview, user_id: 1, pathname: "/", timestamp: ~N[2021-01-01 12:00:00]),
|
||||
build(:pageview, user_id: 1, pathname: "/ignored", timestamp: ~N[2021-01-01 12:01:00]),
|
||||
build(:imported_entry_pages,
|
||||
entry_page: "/",
|
||||
visitors: 1,
|
||||
bounces: 1,
|
||||
date: ~D[2021-01-01]
|
||||
),
|
||||
build(:imported_pages,
|
||||
page: "/",
|
||||
visitors: 3,
|
||||
pageviews: 3,
|
||||
time_on_page: 300,
|
||||
date: ~D[2021-01-01]
|
||||
),
|
||||
build(:imported_pages, page: "/ignored", visitors: 10, date: ~D[2021-01-01])
|
||||
])
|
||||
|
||||
filters = Jason.encode!(%{"page" => "/"})
|
||||
q = "?period=day&date=2021-01-01&filters=#{filters}&detailed=true&with_imported=true"
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/pages#{q}")
|
||||
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"bounce_rate" => 50,
|
||||
"name" => "/",
|
||||
"pageviews" => 4,
|
||||
"time_on_page" => 90.0,
|
||||
"visitors" => 4
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
test "filter by :member page with imported data", %{conn: conn, site: site} do
|
||||
site_import = insert(:site_import, site: site)
|
||||
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:pageview, user_id: 1, pathname: "/", timestamp: ~N[2021-01-01 12:00:00]),
|
||||
build(:pageview, user_id: 1, pathname: "/ignored", timestamp: ~N[2021-01-01 12:01:00]),
|
||||
build(:imported_entry_pages,
|
||||
entry_page: "/",
|
||||
visitors: 1,
|
||||
bounces: 1,
|
||||
date: ~D[2021-01-01]
|
||||
),
|
||||
build(:imported_entry_pages,
|
||||
entry_page: "/a",
|
||||
visitors: 1,
|
||||
bounces: 1,
|
||||
date: ~D[2021-01-01]
|
||||
),
|
||||
build(:imported_pages,
|
||||
page: "/",
|
||||
visitors: 3,
|
||||
pageviews: 3,
|
||||
time_on_page: 300,
|
||||
date: ~D[2021-01-01]
|
||||
),
|
||||
build(:imported_pages,
|
||||
page: "/a",
|
||||
visitors: 1,
|
||||
date: ~D[2021-01-01]
|
||||
),
|
||||
build(:imported_pages, page: "/ignored", visitors: 10, date: ~D[2021-01-01])
|
||||
])
|
||||
|
||||
filters = Jason.encode!(%{"page" => "/|/a"})
|
||||
q = "?period=day&date=2021-01-01&filters=#{filters}&detailed=true&with_imported=true"
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/pages#{q}")
|
||||
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"bounce_rate" => 50,
|
||||
"name" => "/",
|
||||
"pageviews" => 4,
|
||||
"time_on_page" => 90.0,
|
||||
"visitors" => 4
|
||||
},
|
||||
%{
|
||||
"bounce_rate" => 100,
|
||||
"name" => "/a",
|
||||
"pageviews" => 1,
|
||||
"time_on_page" => 10.0,
|
||||
"visitors" => 1
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
test "filter by :matches page with imported data", %{conn: conn, site: site} do
|
||||
site_import = insert(:site_import, site: site)
|
||||
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:pageview, user_id: 1, pathname: "/aaa", timestamp: ~N[2021-01-01 12:00:00]),
|
||||
build(:pageview, user_id: 1, pathname: "/ignored", timestamp: ~N[2021-01-01 12:01:00]),
|
||||
build(:imported_entry_pages,
|
||||
entry_page: "/aaa",
|
||||
visitors: 1,
|
||||
bounces: 1,
|
||||
date: ~D[2021-01-01]
|
||||
),
|
||||
build(:imported_entry_pages,
|
||||
entry_page: "/a",
|
||||
visitors: 1,
|
||||
bounces: 1,
|
||||
date: ~D[2021-01-01]
|
||||
),
|
||||
build(:imported_pages,
|
||||
page: "/aaa",
|
||||
visitors: 3,
|
||||
pageviews: 3,
|
||||
time_on_page: 300,
|
||||
date: ~D[2021-01-01]
|
||||
),
|
||||
build(:imported_pages,
|
||||
page: "/a",
|
||||
visitors: 1,
|
||||
date: ~D[2021-01-01]
|
||||
),
|
||||
build(:imported_pages, page: "/ignored", visitors: 10, date: ~D[2021-01-01])
|
||||
])
|
||||
|
||||
filters = Jason.encode!(%{"page" => "/a**"})
|
||||
q = "?period=day&date=2021-01-01&filters=#{filters}&detailed=true&with_imported=true"
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/pages#{q}")
|
||||
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"bounce_rate" => 50,
|
||||
"name" => "/aaa",
|
||||
"pageviews" => 4,
|
||||
"time_on_page" => 90.0,
|
||||
"visitors" => 4
|
||||
},
|
||||
%{
|
||||
"bounce_rate" => 100,
|
||||
"name" => "/a",
|
||||
"pageviews" => 1,
|
||||
"time_on_page" => 10.0,
|
||||
"visitors" => 1
|
||||
}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /api/stats/:domain/entry-pages" do
|
||||
@ -1236,7 +1419,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/entry-pages?period=day&date=2021-01-01")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"visitors" => 2,
|
||||
"visits" => 2,
|
||||
@ -1292,7 +1475,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
"/api/stats/#{site.domain}/entry-pages?period=day&date=2021-01-01&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"visitors" => 1,
|
||||
"visits" => 1,
|
||||
@ -1347,7 +1530,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/entry-pages?period=day&date=2021-01-01")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"visitors" => 2,
|
||||
"visits" => 2,
|
||||
@ -1368,7 +1551,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
"/api/stats/#{site.domain}/entry-pages?period=day&date=2021-01-01&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"visitors" => 3,
|
||||
"visits" => 5,
|
||||
@ -1431,7 +1614,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
)
|
||||
|
||||
# We're going to only join sessions where the exit hostname matches the filter
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "/page1", "visit_duration" => 0, "visitors" => 1, "visits" => 1},
|
||||
%{"name" => "/page2", "visit_duration" => 0, "visitors" => 1, "visits" => 1}
|
||||
]
|
||||
@ -1460,6 +1643,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
"/api/stats/#{site.domain}/pages?date=2021-01-01&period=day&filters=#{filters}&limit=#{limit}&page=#{page}"
|
||||
)
|
||||
|> json_response(200)
|
||||
|> Map.get("results")
|
||||
|> Enum.map(fn %{"name" => "/signup/" <> seq} ->
|
||||
seq
|
||||
end)
|
||||
@ -1519,7 +1703,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
"/api/stats/#{site.domain}/entry-pages?period=day&date=2021-01-01&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"total_visitors" => 2,
|
||||
"visitors" => 1,
|
||||
@ -1550,7 +1734,57 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
"/api/stats/#{site.domain}/entry-pages?period=day&date=2021-01-01"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == []
|
||||
assert json_response(conn, 200)["results"] == []
|
||||
end
|
||||
|
||||
test "filter by :matches_member entry_page with imported data", %{conn: conn, site: site} do
|
||||
site_import = insert(:site_import, site: site)
|
||||
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:pageview, pathname: "/aaa", timestamp: ~N[2021-01-01 12:00:00]),
|
||||
build(:pageview, pathname: "/a", timestamp: ~N[2021-01-01 12:00:00]),
|
||||
build(:pageview, pathname: "/ignored", timestamp: ~N[2021-01-01 12:01:00]),
|
||||
build(:imported_entry_pages,
|
||||
entry_page: "/a",
|
||||
visitors: 5,
|
||||
entrances: 9,
|
||||
visit_duration: 1000,
|
||||
date: ~D[2021-01-01]
|
||||
),
|
||||
build(:imported_entry_pages,
|
||||
entry_page: "/bbb",
|
||||
visitors: 2,
|
||||
entrances: 2,
|
||||
visit_duration: 100,
|
||||
date: ~D[2021-01-01]
|
||||
)
|
||||
])
|
||||
|
||||
filters = Jason.encode!(%{"entry_page" => "/a**|/b**"})
|
||||
q = "?period=day&date=2021-01-01&filters=#{filters}&detailed=true&with_imported=true"
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/entry-pages#{q}")
|
||||
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"visit_duration" => 100.0,
|
||||
"name" => "/a",
|
||||
"visits" => 10,
|
||||
"visitors" => 6
|
||||
},
|
||||
%{
|
||||
"visit_duration" => 50.0,
|
||||
"name" => "/bbb",
|
||||
"visits" => 2,
|
||||
"visitors" => 2
|
||||
},
|
||||
%{
|
||||
"visit_duration" => 0,
|
||||
"name" => "/aaa",
|
||||
"visits" => 1,
|
||||
"visitors" => 1
|
||||
}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
@ -1581,7 +1815,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/exit-pages?period=day&date=2021-01-01")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "/page1", "visitors" => 2, "visits" => 2, "exit_rate" => 66},
|
||||
%{"name" => "/page2", "visitors" => 1, "visits" => 1, "exit_rate" => 100}
|
||||
]
|
||||
@ -1629,7 +1863,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
)
|
||||
|
||||
# We're going to only join sessions where the entry hostname matches the filter
|
||||
assert json_response(conn, 200) ==
|
||||
assert json_response(conn, 200)["results"] ==
|
||||
[%{"name" => "/page1", "visitors" => 1, "visits" => 1}]
|
||||
end
|
||||
|
||||
@ -1667,7 +1901,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
"/api/stats/#{site.domain}/exit-pages?period=day&date=2021-01-01&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "/", "visitors" => 1, "visits" => 1}
|
||||
]
|
||||
end
|
||||
@ -1711,7 +1945,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/exit-pages?period=day&date=2021-01-01")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "/page1", "visitors" => 2, "visits" => 2, "exit_rate" => 66},
|
||||
%{"name" => "/page2", "visitors" => 1, "visits" => 1, "exit_rate" => 100}
|
||||
]
|
||||
@ -1722,7 +1956,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
"/api/stats/#{site.domain}/exit-pages?period=day&date=2021-01-01&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "/page2",
|
||||
"visitors" => 3,
|
||||
@ -1773,7 +2007,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
"/api/stats/#{site.domain}/exit-pages?period=day&date=2021-01-01&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "/exit1",
|
||||
"visitors" => 1,
|
||||
@ -1826,7 +2060,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
"/api/stats/#{site.domain}/exit-pages?period=day&date=2021-01-01&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "/exit1", "visitors" => 1, "visits" => 1},
|
||||
%{"name" => "/exit2", "visitors" => 1, "visits" => 1}
|
||||
]
|
||||
@ -1847,7 +2081,59 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
"/api/stats/#{site.domain}/exit-pages?period=day&date=2021-01-01"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == []
|
||||
assert json_response(conn, 200)["results"] == []
|
||||
end
|
||||
|
||||
test "filter by :is_not exit_page with imported data", %{conn: conn, site: site} do
|
||||
site_import = insert(:site_import, site: site)
|
||||
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:pageview, pathname: "/aaa", timestamp: ~N[2021-01-01 12:00:00]),
|
||||
build(:pageview, pathname: "/a", timestamp: ~N[2021-01-01 12:00:00]),
|
||||
build(:pageview, pathname: "/ignored", timestamp: ~N[2021-01-01 12:01:00]),
|
||||
build(:imported_exit_pages,
|
||||
exit_page: "/a",
|
||||
visitors: 5,
|
||||
exits: 9,
|
||||
visit_duration: 1000,
|
||||
date: ~D[2021-01-01]
|
||||
),
|
||||
build(:imported_exit_pages,
|
||||
exit_page: "/bbb",
|
||||
visitors: 2,
|
||||
exits: 2,
|
||||
visit_duration: 100,
|
||||
date: ~D[2021-01-01]
|
||||
),
|
||||
build(:imported_pages, page: "/a", pageviews: 19, date: ~D[2021-01-01]),
|
||||
build(:imported_pages, page: "/bbb", pageviews: 2, date: ~D[2021-01-01])
|
||||
])
|
||||
|
||||
filters = Jason.encode!(%{"exit_page" => "!/ignored"})
|
||||
q = "?period=day&date=2021-01-01&filters=#{filters}&detailed=true&with_imported=true"
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/exit-pages#{q}")
|
||||
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"exit_rate" => 50.0,
|
||||
"name" => "/a",
|
||||
"visits" => 10,
|
||||
"visitors" => 6
|
||||
},
|
||||
%{
|
||||
"exit_rate" => 100.0,
|
||||
"name" => "/bbb",
|
||||
"visits" => 2,
|
||||
"visitors" => 2
|
||||
},
|
||||
%{
|
||||
"exit_rate" => 100.0,
|
||||
"name" => "/aaa",
|
||||
"visits" => 1,
|
||||
"visitors" => 1
|
||||
}
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -37,7 +37,7 @@ defmodule PlausibleWeb.Api.StatsController.RegionsTest do
|
||||
test "returns top cities by new visitors", %{conn: conn, site: site} do
|
||||
conn = get(conn, "/api/stats/#{site.domain}/regions?period=day")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"code" => "EE-37", "country_flag" => "🇪🇪", "name" => "Harjumaa", "visitors" => 3},
|
||||
%{"code" => "EE-39", "country_flag" => "🇪🇪", "name" => "Hiiumaa", "visitors" => 2}
|
||||
]
|
||||
@ -47,7 +47,7 @@ defmodule PlausibleWeb.Api.StatsController.RegionsTest do
|
||||
filters = Jason.encode!(%{region: "EE-39"})
|
||||
conn = get(conn, "/api/stats/#{site.domain}/regions?period=day&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"code" => "EE-39", "country_flag" => "🇪🇪", "name" => "Hiiumaa", "visitors" => 2}
|
||||
]
|
||||
end
|
||||
|
@ -13,7 +13,7 @@ defmodule PlausibleWeb.Api.StatsController.ScreenSizesTest do
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/screen-sizes?period=day")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "Desktop", "visitors" => 2, "percentage" => 66.7},
|
||||
%{"name" => "Laptop", "visitors" => 1, "percentage" => 33.3}
|
||||
]
|
||||
@ -39,7 +39,7 @@ defmodule PlausibleWeb.Api.StatsController.ScreenSizesTest do
|
||||
"date" => "2021-01-01"
|
||||
})
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "Desktop", "visitors" => 1, "percentage" => 100},
|
||||
%{"name" => "Laptop", "visitors" => 1, "percentage" => 100}
|
||||
]
|
||||
@ -57,7 +57,7 @@ defmodule PlausibleWeb.Api.StatsController.ScreenSizesTest do
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/screen-sizes?period=day")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "(not set)", "visitors" => 1, "percentage" => 50},
|
||||
%{"name" => "Desktop", "visitors" => 1, "percentage" => 50}
|
||||
]
|
||||
@ -67,7 +67,7 @@ defmodule PlausibleWeb.Api.StatsController.ScreenSizesTest do
|
||||
filters = Jason.encode!(%{screen: "(not set)"})
|
||||
conn = get(conn, "/api/stats/#{site.domain}/screen-sizes?period=day&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "(not set)", "visitors" => 1, "percentage" => 100}
|
||||
]
|
||||
end
|
||||
@ -84,7 +84,7 @@ defmodule PlausibleWeb.Api.StatsController.ScreenSizesTest do
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/screen-sizes?period=day&with_imported=true")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "(not set)", "visitors" => 2, "percentage" => 100.0}
|
||||
]
|
||||
end
|
||||
@ -117,7 +117,7 @@ defmodule PlausibleWeb.Api.StatsController.ScreenSizesTest do
|
||||
filters = Jason.encode!(%{props: %{"author" => "John Doe"}})
|
||||
conn = get(conn, "/api/stats/#{site.domain}/screen-sizes?period=day&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "Desktop", "visitors" => 1, "percentage" => 100}
|
||||
]
|
||||
end
|
||||
@ -152,7 +152,7 @@ defmodule PlausibleWeb.Api.StatsController.ScreenSizesTest do
|
||||
filters = Jason.encode!(%{props: %{"author" => "!John Doe"}})
|
||||
conn = get(conn, "/api/stats/#{site.domain}/screen-sizes?period=day&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "Mobile", "visitors" => 1, "percentage" => 50},
|
||||
%{"name" => "Tablet", "visitors" => 1, "percentage" => 50}
|
||||
]
|
||||
@ -173,20 +173,41 @@ defmodule PlausibleWeb.Api.StatsController.ScreenSizesTest do
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/screen-sizes?period=day")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "Desktop", "visitors" => 2, "percentage" => 66.7},
|
||||
%{"name" => "Laptop", "visitors" => 1, "percentage" => 33.3}
|
||||
]
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/screen-sizes?period=day&with_imported=true")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "Desktop", "visitors" => 2, "percentage" => 40},
|
||||
%{"name" => "Laptop", "visitors" => 2, "percentage" => 40},
|
||||
%{"name" => "Mobile", "visitors" => 1, "percentage" => 20}
|
||||
]
|
||||
end
|
||||
|
||||
test "returns screen sizes when filtering by imported screen size", %{conn: conn, site: site} do
|
||||
populate_stats(site, [
|
||||
build(:pageview, screen_size: "Desktop"),
|
||||
build(:imported_devices, device: "Desktop"),
|
||||
build(:imported_devices, device: "Laptop"),
|
||||
build(:imported_visitors, visitors: 2)
|
||||
])
|
||||
|
||||
filters = Jason.encode!(%{screen: "Desktop"})
|
||||
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
"/api/stats/#{site.domain}/screen-sizes?filters=#{filters}&period=day&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "Desktop", "visitors" => 2, "percentage" => 100.0}
|
||||
]
|
||||
end
|
||||
|
||||
test "returns screen sizes for user making multiple sessions by no of visitors with imported data",
|
||||
%{conn: conn, site: site} do
|
||||
populate_stats(site, [
|
||||
@ -215,7 +236,7 @@ defmodule PlausibleWeb.Api.StatsController.ScreenSizesTest do
|
||||
"with_imported" => "true"
|
||||
})
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "Desktop", "visitors" => 2, "percentage" => 100},
|
||||
%{"name" => "Laptop", "visitors" => 2, "percentage" => 100}
|
||||
]
|
||||
@ -232,7 +253,7 @@ defmodule PlausibleWeb.Api.StatsController.ScreenSizesTest do
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/screen-sizes?period=day&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "Desktop",
|
||||
"total_visitors" => 2,
|
||||
@ -258,7 +279,7 @@ defmodule PlausibleWeb.Api.StatsController.ScreenSizesTest do
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/screen-sizes?period=day&filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "Desktop", "visitors" => 2, "percentage" => 66.7},
|
||||
%{"name" => "Mobile", "visitors" => 1, "percentage" => 33.3}
|
||||
]
|
||||
|
@ -33,7 +33,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/sources")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "Google", "visitors" => 3},
|
||||
%{"name" => "DuckDuckGo", "visitors" => 2},
|
||||
%{"name" => "Direct / None", "visitors" => 1}
|
||||
@ -83,7 +83,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"/api/stats/#{site.domain}/sources?period=day&date=2021-01-01&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "Google", "visitors" => 2},
|
||||
%{"name" => "DuckDuckGo", "visitors" => 1}
|
||||
]
|
||||
@ -137,7 +137,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"/api/stats/#{site.domain}/sources?period=day&date=2021-01-01&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "Google", "visitors" => 2},
|
||||
%{"name" => "DuckDuckGo", "visitors" => 1}
|
||||
]
|
||||
@ -187,7 +187,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"/api/stats/#{site.domain}/sources?period=day&date=2021-01-01&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "Facebook", "visitors" => 2},
|
||||
%{"name" => "DuckDuckGo", "visitors" => 1}
|
||||
]
|
||||
@ -241,7 +241,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"/api/stats/#{site.domain}/sources?period=day&date=2021-01-01&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "Google", "visitors" => 2},
|
||||
%{"name" => "DuckDuckGo", "visitors" => 1}
|
||||
]
|
||||
@ -270,14 +270,14 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/sources")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "Google", "visitors" => 2},
|
||||
%{"name" => "DuckDuckGo", "visitors" => 1}
|
||||
]
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/sources?with_imported=true")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "Google", "visitors" => 4},
|
||||
%{"name" => "DuckDuckGo", "visitors" => 2}
|
||||
]
|
||||
@ -310,7 +310,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"/api/stats/#{site.domain}/sources?period=day&date=2021-01-01&detailed=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "DuckDuckGo",
|
||||
"visitors" => 1,
|
||||
@ -375,7 +375,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"/api/stats/#{site.domain}/sources?period=day&date=2021-01-01&detailed=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "DuckDuckGo",
|
||||
"visitors" => 1,
|
||||
@ -396,7 +396,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"/api/stats/#{site.domain}/sources?period=day&date=2021-01-01&detailed=true&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "Google",
|
||||
"visitors" => 3,
|
||||
@ -433,7 +433,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/sources?period=realtime")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "Google", "visitors" => 2},
|
||||
%{"name" => "DuckDuckGo", "visitors" => 1}
|
||||
]
|
||||
@ -460,13 +460,13 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/sources?limit=1&page=2")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "DuckDuckGo", "visitors" => 1}
|
||||
]
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/sources?limit=1&page=2&with_imported=true")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "DuckDuckGo", "visitors" => 2}
|
||||
]
|
||||
end
|
||||
@ -490,7 +490,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
filters = Jason.encode!(%{"page" => "/page1"})
|
||||
conn = get(conn, "/api/stats/#{site.domain}/sources?filters=#{filters}")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "Google", "visitors" => 2},
|
||||
%{"name" => "DuckDuckGo", "visitors" => 1}
|
||||
]
|
||||
@ -538,7 +538,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
)
|
||||
|
||||
# nobody landed on one.example.com from utm_param=ad
|
||||
assert json_response(conn, 200) == []
|
||||
assert json_response(conn, 200)["results"] == []
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -589,7 +589,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"/api/stats/#{site.domain}/utm_mediums?period=day&date=2021-01-01"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "social",
|
||||
"visitors" => 1,
|
||||
@ -610,7 +610,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"/api/stats/#{site.domain}/utm_mediums?period=day&date=2021-01-01&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "social",
|
||||
"visitors" => 2,
|
||||
@ -669,7 +669,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"/api/stats/#{site.domain}/utm_mediums?period=day&date=2021-01-01"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "social",
|
||||
"visitors" => 1,
|
||||
@ -684,7 +684,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"/api/stats/#{site.domain}/utm_mediums?period=day&date=2021-01-01&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "social",
|
||||
"visitors" => 2,
|
||||
@ -745,7 +745,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"/api/stats/#{site.domain}/utm_campaigns?period=day&date=2021-01-01"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "august",
|
||||
"visitors" => 2,
|
||||
@ -766,7 +766,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"/api/stats/#{site.domain}/utm_campaigns?period=day&date=2021-01-01&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "august",
|
||||
"visitors" => 3,
|
||||
@ -829,7 +829,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"/api/stats/#{site.domain}/utm_campaigns?period=day&date=2021-01-01"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "profile",
|
||||
"visitors" => 1,
|
||||
@ -844,7 +844,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"/api/stats/#{site.domain}/utm_campaigns?period=day&date=2021-01-01&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "profile",
|
||||
"visitors" => 2,
|
||||
@ -886,7 +886,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"/api/stats/#{site.domain}/utm_sources?period=day&date=2021-01-01"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "newsletter",
|
||||
"visitors" => 2,
|
||||
@ -953,7 +953,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"/api/stats/#{site.domain}/utm_terms?period=day&date=2021-01-01"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "Sweden",
|
||||
"visitors" => 2,
|
||||
@ -974,7 +974,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"/api/stats/#{site.domain}/utm_terms?period=day&date=2021-01-01&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "Sweden",
|
||||
"visitors" => 3,
|
||||
@ -1037,7 +1037,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"/api/stats/#{site.domain}/utm_terms?period=day&date=2021-01-01"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "oat milk",
|
||||
"visitors" => 1,
|
||||
@ -1052,7 +1052,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"/api/stats/#{site.domain}/utm_terms?period=day&date=2021-01-01&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "oat milk",
|
||||
"visitors" => 2,
|
||||
@ -1113,7 +1113,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"/api/stats/#{site.domain}/utm_contents?period=day&date=2021-01-01"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "blog",
|
||||
"visitors" => 2,
|
||||
@ -1134,7 +1134,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"/api/stats/#{site.domain}/utm_contents?period=day&date=2021-01-01&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "blog",
|
||||
"visitors" => 3,
|
||||
@ -1197,7 +1197,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"/api/stats/#{site.domain}/utm_contents?period=day&date=2021-01-01"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "ad",
|
||||
"visitors" => 1,
|
||||
@ -1212,7 +1212,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"/api/stats/#{site.domain}/utm_contents?period=day&date=2021-01-01&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "ad",
|
||||
"visitors" => 2,
|
||||
@ -1257,7 +1257,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"/api/stats/#{site.domain}/sources?period=day&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "Twitter",
|
||||
"total_visitors" => 2,
|
||||
@ -1299,7 +1299,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"/api/stats/#{site.domain}/sources?period=day&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == []
|
||||
assert json_response(conn, 200)["results"] == []
|
||||
end
|
||||
|
||||
test "returns top referrers for a custom goal and filtered by hostname (2)",
|
||||
@ -1330,7 +1330,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"/api/stats/#{site.domain}/sources?period=day&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"conversion_rate" => 100.0,
|
||||
"name" => "Facebook",
|
||||
@ -1380,7 +1380,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"/api/stats/#{site.domain}/sources?period=day&date=2021-01-01&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "DuckDuckGo",
|
||||
"visitors" => 1,
|
||||
@ -1431,7 +1431,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"/api/stats/#{site.domain}/sources?period=day&date=2021-01-01&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "DuckDuckGo",
|
||||
"visitors" => 1,
|
||||
@ -1473,7 +1473,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"/api/stats/#{site.domain}/sources?period=day&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "Twitter",
|
||||
"total_visitors" => 2,
|
||||
@ -1513,7 +1513,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"/api/stats/#{site.domain}/referrers/10words?period=day"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "10words.com", "visitors" => 2},
|
||||
%{"name" => "10words.com/page1", "visitors" => 1}
|
||||
]
|
||||
@ -1559,7 +1559,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"/api/stats/#{site.domain}/referrers/example?period=day&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "example.com/page1", "visitors" => 1}
|
||||
]
|
||||
end
|
||||
@ -1596,7 +1596,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"/api/stats/#{site.domain}/referrers/10words?period=day&date=2021-01-01&detailed=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "10words.com",
|
||||
"visitors" => 2,
|
||||
@ -1649,13 +1649,13 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/referrers/!Google?period=day")
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"name" => "duckduckgo.com", "visitors" => 1}
|
||||
]
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/referrers/Google|DuckDuckGo?period=day")
|
||||
|
||||
assert [entry1, entry2] = json_response(conn, 200)
|
||||
assert [entry1, entry2] = json_response(conn, 200)["results"]
|
||||
assert %{"name" => "google.com", "visitors" => 2} in [entry1, entry2]
|
||||
assert %{"name" => "duckduckgo.com", "visitors" => 1} in [entry1, entry2]
|
||||
end
|
||||
@ -1688,7 +1688,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"/api/stats/#{site.domain}/referrers/10words?period=day&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "10words.com",
|
||||
"total_visitors" => 2,
|
||||
@ -1726,7 +1726,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"/api/stats/#{site.domain}/referrers/10words?period=day&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{
|
||||
"name" => "10words.com",
|
||||
"total_visitors" => 2,
|
||||
|
@ -634,4 +634,611 @@ defmodule PlausibleWeb.Api.StatsController.SuggestionsTest do
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
describe "imported data" do
|
||||
setup [:create_user, :log_in, :create_site, :create_site_import]
|
||||
|
||||
test "merges country suggestions from native and imported data", %{
|
||||
conn: conn,
|
||||
site: site,
|
||||
site_import: site_import
|
||||
} do
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:pageview, timestamp: ~N[2019-01-01 23:00:01], country_code: "US"),
|
||||
build(:pageview, timestamp: ~N[2019-01-01 23:30:01], country_code: "US"),
|
||||
build(:pageview, timestamp: ~N[2019-01-01 23:40:01], country_code: "US"),
|
||||
build(:pageview, timestamp: ~N[2019-01-01 23:00:01], country_code: "GB"),
|
||||
build(:imported_locations, date: ~D[2019-01-01], country: "GB", pageviews: 3)
|
||||
])
|
||||
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
"/api/stats/#{site.domain}/suggestions/country?period=month&date=2019-01-01&q=Unit&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
%{"value" => "GB", "label" => "United Kingdom"},
|
||||
%{"value" => "US", "label" => "United States"}
|
||||
]
|
||||
end
|
||||
|
||||
test "ignores imported data in country suggestions when a different property is filtered by",
|
||||
%{
|
||||
conn: conn,
|
||||
site: site,
|
||||
site_import: site_import
|
||||
} do
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:pageview, country_code: "EE", referrer_source: "Bing"),
|
||||
build(:imported_locations, country: "GB")
|
||||
])
|
||||
|
||||
filters = Jason.encode!(%{source: "Bing"})
|
||||
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
"/api/stats/#{site.domain}/suggestions/country?filters=#{filters}&q=&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [%{"value" => "EE", "label" => "Estonia"}]
|
||||
end
|
||||
|
||||
test "queries imported countries when filtering by country", %{
|
||||
conn: conn,
|
||||
site: site,
|
||||
site_import: site_import
|
||||
} do
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:imported_locations, date: ~D[2019-01-01], country: "EE")
|
||||
])
|
||||
|
||||
filters = Jason.encode!(%{country: "EE"})
|
||||
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
"/api/stats/#{site.domain}/suggestions/country?period=month&date=2019-01-01&filters=#{filters}&q=&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [%{"value" => "EE", "label" => "Estonia"}]
|
||||
end
|
||||
|
||||
test "ignores imported country data when not requested", %{
|
||||
conn: conn,
|
||||
site: site,
|
||||
site_import: site_import
|
||||
} do
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:imported_locations, date: ~D[2019-01-01], country: "GB", pageviews: 3)
|
||||
])
|
||||
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
"/api/stats/#{site.domain}/suggestions/country?period=month&date=2019-01-01&q="
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == []
|
||||
end
|
||||
|
||||
for {q, label} <- [{"", "without filter"}, {"H", "with filter"}] do
|
||||
test "merges region suggestions from native and imported data #{label}", %{
|
||||
conn: conn,
|
||||
site: site,
|
||||
site_import: site_import
|
||||
} do
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:pageview, country_code: "EE", subdivision1_code: "EE-37"),
|
||||
build(:pageview, country_code: "EE", subdivision1_code: "EE-39"),
|
||||
build(:pageview, country_code: "EE", subdivision1_code: "EE-39"),
|
||||
build(:imported_locations, country: "EE", region: "EE-37", pageviews: 2)
|
||||
])
|
||||
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
"/api/stats/#{site.domain}/suggestions/region?q=#{unquote(q)}&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
%{"value" => "EE-37", "label" => "Harjumaa"},
|
||||
%{"value" => "EE-39", "label" => "Hiiumaa"}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
test "handles invalid region codes in imported data gracefully (GA4)", %{
|
||||
conn: conn,
|
||||
site: site,
|
||||
site_import: site_import
|
||||
} do
|
||||
# NOTE: Currently, the regions imported from GA4 do not conform to region code standard
|
||||
# we are using. Instead, literal region names are persisted. Those names often do not
|
||||
# match the names from our region databases either. Regardless of that, we still consider
|
||||
# them when filtering suggestions.
|
||||
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:imported_locations, country: "EE", region: "EE-37", pageviews: 2),
|
||||
build(:imported_locations, country: "EE", region: "Hiiumaa", pageviews: 1)
|
||||
])
|
||||
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
"/api/stats/#{site.domain}/suggestions/region?q=&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
%{"value" => "EE-37", "label" => "Harjumaa"},
|
||||
%{"value" => "Hiiumaa", "label" => "Hiiumaa"}
|
||||
]
|
||||
|
||||
conn2 =
|
||||
get(
|
||||
conn,
|
||||
"/api/stats/#{site.domain}/suggestions/region?q=H&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn2, 200) == [
|
||||
%{"value" => "EE-37", "label" => "Harjumaa"},
|
||||
%{"value" => "Hiiumaa", "label" => "Hiiumaa"}
|
||||
]
|
||||
end
|
||||
|
||||
test "ignores imported data in region suggestions when a different property is filtered by",
|
||||
%{
|
||||
conn: conn,
|
||||
site: site,
|
||||
site_import: site_import
|
||||
} do
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:pageview,
|
||||
country_code: "EE",
|
||||
subdivision1_code: "EE-39",
|
||||
referrer_source: "Bing"
|
||||
),
|
||||
build(:imported_locations, country: "EE", region: "EE-37")
|
||||
])
|
||||
|
||||
filters = Jason.encode!(%{source: "Bing"})
|
||||
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
"/api/stats/#{site.domain}/suggestions/region?filters=#{filters}&q=&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [%{"value" => "EE-39", "label" => "Hiiumaa"}]
|
||||
end
|
||||
|
||||
test "queries imported regions when filtering by region", %{
|
||||
conn: conn,
|
||||
site: site,
|
||||
site_import: site_import
|
||||
} do
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:imported_locations, date: ~D[2019-01-01], region: "EE-39")
|
||||
])
|
||||
|
||||
filters = Jason.encode!(%{region: "EE-39"})
|
||||
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
"/api/stats/#{site.domain}/suggestions/region?period=month&date=2019-01-01&filters=#{filters}&q=&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [%{"value" => "EE-39", "label" => "Hiiumaa"}]
|
||||
end
|
||||
|
||||
test "ignores imported region data when not requested", %{
|
||||
conn: conn,
|
||||
site: site,
|
||||
site_import: site_import
|
||||
} do
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:imported_locations, country: "EE", region: "EE-37", pageviews: 2)
|
||||
])
|
||||
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
"/api/stats/#{site.domain}/suggestions/region?q="
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == []
|
||||
end
|
||||
|
||||
for {q, label} <- [{"", "without filter"}, {"l", "with filter"}] do
|
||||
test "merges city suggestions from native and imported data #{label}", %{
|
||||
conn: conn,
|
||||
site: site,
|
||||
site_import: site_import
|
||||
} do
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:pageview,
|
||||
country_code: "EE",
|
||||
subdivision1_code: "EE-37",
|
||||
city_geoname_id: 588_409
|
||||
),
|
||||
build(:pageview,
|
||||
country_code: "EE",
|
||||
subdivision1_code: "EE-39",
|
||||
city_geoname_id: 591_632
|
||||
),
|
||||
build(:pageview,
|
||||
country_code: "EE",
|
||||
subdivision1_code: "EE-39",
|
||||
city_geoname_id: 591_632
|
||||
),
|
||||
build(:imported_locations, country: "EE", region: "EE-37", city: 588_409, pageviews: 2)
|
||||
])
|
||||
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
"/api/stats/#{site.domain}/suggestions/city?q=#{unquote(q)}&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
%{"value" => "588409", "label" => "Tallinn"},
|
||||
%{"value" => "591632", "label" => "Kärdla"}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
test "ignores imported data in city suggestions when a different property is filtered by", %{
|
||||
conn: conn,
|
||||
site: site,
|
||||
site_import: site_import
|
||||
} do
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:pageview,
|
||||
country_code: "EE",
|
||||
subdivision1_code: "EE-39",
|
||||
city_geoname_id: 591_632,
|
||||
referrer_source: "Bing"
|
||||
),
|
||||
build(:imported_locations, country: "EE", region: "EE-37", city: 588_409)
|
||||
])
|
||||
|
||||
filters = Jason.encode!(%{source: "Bing"})
|
||||
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
"/api/stats/#{site.domain}/suggestions/city?filters=#{filters}&q=&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [%{"value" => "591632", "label" => "Kärdla"}]
|
||||
end
|
||||
|
||||
test "queries imported cities when filtering by city", %{
|
||||
conn: conn,
|
||||
site: site,
|
||||
site_import: site_import
|
||||
} do
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:imported_locations, date: ~D[2019-01-01], city: 591_632)
|
||||
])
|
||||
|
||||
filters = Jason.encode!(%{city: "591632"})
|
||||
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
"/api/stats/#{site.domain}/suggestions/city?period=month&date=2019-01-01&filters=#{filters}&q=&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [%{"value" => "591632", "label" => "Kärdla"}]
|
||||
end
|
||||
|
||||
test "ignores imported city data when not requested", %{
|
||||
conn: conn,
|
||||
site: site,
|
||||
site_import: site_import
|
||||
} do
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:imported_locations, country: "EE", region: "EE-37", city: 588_409, pageviews: 2)
|
||||
])
|
||||
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
"/api/stats/#{site.domain}/suggestions/city?q="
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == []
|
||||
end
|
||||
|
||||
test "ignores imported data when asking for prop key and value suggestions", %{
|
||||
conn: conn,
|
||||
site: site,
|
||||
site_import: site_import
|
||||
} do
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:pageview,
|
||||
"meta.key": ["url"],
|
||||
"meta.value": ["http://example1.com"],
|
||||
timestamp: ~N[2022-01-01 00:00:00]
|
||||
),
|
||||
build(:imported_custom_events,
|
||||
date: ~D[2022-01-01],
|
||||
name: "Outbound Link: Click",
|
||||
link_url: "http://example2.com"
|
||||
),
|
||||
build(:imported_custom_events,
|
||||
date: ~D[2022-01-01],
|
||||
name: "404",
|
||||
path: "/dev/null"
|
||||
)
|
||||
])
|
||||
|
||||
key_conn =
|
||||
get(
|
||||
conn,
|
||||
"/api/stats/#{site.domain}/suggestions/prop_key?period=day&date=2022-01-01&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(key_conn, 200) == [%{"label" => "url", "value" => "url"}]
|
||||
|
||||
filters = Jason.encode!(%{props: %{url: "!(none)"}})
|
||||
|
||||
value_conn =
|
||||
get(
|
||||
conn,
|
||||
"/api/stats/#{site.domain}/suggestions/prop_value?period=day&date=2022-01-01&with_imported=true&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(value_conn, 200) == [
|
||||
%{"label" => "http://example1.com", "value" => "http://example1.com"}
|
||||
]
|
||||
end
|
||||
|
||||
for {q, label} <- [{"", "without filter"}, {"g", "with filter"}] do
|
||||
test "merges source suggestions from native and imported data #{label}", %{
|
||||
conn: conn,
|
||||
site: site,
|
||||
site_import: site_import
|
||||
} do
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:pageview, timestamp: ~N[2019-01-01 23:00:01], referrer_source: "Bing"),
|
||||
build(:pageview, timestamp: ~N[2019-01-01 23:30:01], referrer_source: "Bing"),
|
||||
build(:pageview, timestamp: ~N[2019-01-01 23:40:01], referrer_source: "Bing"),
|
||||
build(:pageview, timestamp: ~N[2019-01-01 23:00:01], referrer_source: "Google"),
|
||||
build(:imported_sources, date: ~D[2019-01-01], source: "Google", pageviews: 3)
|
||||
])
|
||||
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
"/api/stats/#{site.domain}/suggestions/source?period=month&date=2019-01-01&q=#{unquote(q)}&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
%{"value" => "Google", "label" => "Google"},
|
||||
%{"value" => "Bing", "label" => "Bing"}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
for {q, label} <- [{"", "without filter"}, {"o", "with filter"}] do
|
||||
test "merges screen suggestions from native and imported data #{label}", %{
|
||||
conn: conn,
|
||||
site: site,
|
||||
site_import: site_import
|
||||
} do
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:pageview, timestamp: ~N[2019-01-01 23:00:01], screen_size: "Mobile"),
|
||||
build(:pageview, timestamp: ~N[2019-01-01 23:30:01], screen_size: "Mobile"),
|
||||
build(:pageview, timestamp: ~N[2019-01-01 23:40:01], screen_size: "Mobile"),
|
||||
build(:pageview, timestamp: ~N[2019-01-01 23:00:01], screen_size: "Desktop"),
|
||||
build(:imported_devices, date: ~D[2019-01-01], device: "Desktop", pageviews: 3)
|
||||
])
|
||||
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
"/api/stats/#{site.domain}/suggestions/screen?period=month&date=2019-01-01&q=#{unquote(q)}&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
%{"value" => "Desktop", "label" => "Desktop"},
|
||||
%{"value" => "Mobile", "label" => "Mobile"}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
for {q, label} <- [{"", "without filter"}, {"o", "with filter"}] do
|
||||
test "merges page suggestions from native and imported data #{label}", %{
|
||||
conn: conn,
|
||||
site: site,
|
||||
site_import: site_import
|
||||
} do
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:pageview, timestamp: ~N[2019-01-01 23:00:01], pathname: "/welcome"),
|
||||
build(:pageview, timestamp: ~N[2019-01-01 23:30:01], pathname: "/welcome"),
|
||||
build(:pageview, timestamp: ~N[2019-01-01 23:40:01], pathname: "/welcome"),
|
||||
build(:pageview, timestamp: ~N[2019-01-01 23:00:01], pathname: "/blog"),
|
||||
build(:imported_pages, date: ~D[2019-01-01], page: "/blog", pageviews: 3)
|
||||
])
|
||||
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
"/api/stats/#{site.domain}/suggestions/page?period=month&date=2019-01-01&q=#{unquote(q)}&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
%{"value" => "/blog", "label" => "/blog"},
|
||||
%{"value" => "/welcome", "label" => "/welcome"}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
for {q, label} <- [{"", "without filter"}, {"o", "with filter"}] do
|
||||
test "merges entry page suggestions from native and imported data #{label}", %{
|
||||
conn: conn,
|
||||
site: site,
|
||||
site_import: site_import
|
||||
} do
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:pageview, timestamp: ~N[2019-01-01 23:00:01], pathname: "/welcome"),
|
||||
build(:pageview, timestamp: ~N[2019-01-01 23:30:01], pathname: "/welcome"),
|
||||
build(:pageview, timestamp: ~N[2019-01-01 23:40:01], pathname: "/welcome"),
|
||||
build(:pageview, timestamp: ~N[2019-01-01 23:00:01], pathname: "/blog"),
|
||||
build(:imported_entry_pages, date: ~D[2019-01-01], entry_page: "/blog", pageviews: 3)
|
||||
])
|
||||
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
"/api/stats/#{site.domain}/suggestions/entry_page?period=month&date=2019-01-01&q=#{unquote(q)}&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
%{"value" => "/blog", "label" => "/blog"},
|
||||
%{"value" => "/welcome", "label" => "/welcome"}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
for {q, label} <- [{"", "without filter"}, {"o", "with filter"}] do
|
||||
test "merges exit page suggestions from native and imported data #{label}", %{
|
||||
conn: conn,
|
||||
site: site,
|
||||
site_import: site_import
|
||||
} do
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:pageview, timestamp: ~N[2019-01-01 23:00:01], pathname: "/welcome"),
|
||||
build(:pageview, timestamp: ~N[2019-01-01 23:30:01], pathname: "/welcome"),
|
||||
build(:pageview, timestamp: ~N[2019-01-01 23:40:01], pathname: "/welcome"),
|
||||
build(:pageview, timestamp: ~N[2019-01-01 23:00:01], pathname: "/blog"),
|
||||
build(:imported_exit_pages, date: ~D[2019-01-01], exit_page: "/blog", pageviews: 3)
|
||||
])
|
||||
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
"/api/stats/#{site.domain}/suggestions/exit_page?period=month&date=2019-01-01&q=#{unquote(q)}&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
%{"value" => "/blog", "label" => "/blog"},
|
||||
%{"value" => "/welcome", "label" => "/welcome"}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
for {q, label} <- [{"", "without filter"}, {"o", "with filter"}] do
|
||||
test "merges browser suggestions from native and imported data #{label}", %{
|
||||
conn: conn,
|
||||
site: site,
|
||||
site_import: site_import
|
||||
} do
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:pageview, timestamp: ~N[2019-01-01 23:00:01], browser: "Chrome"),
|
||||
build(:pageview, timestamp: ~N[2019-01-01 23:30:01], browser: "Chrome"),
|
||||
build(:pageview, timestamp: ~N[2019-01-01 23:40:01], browser: "Chrome"),
|
||||
build(:pageview, timestamp: ~N[2019-01-01 23:00:01], browser: "Firefox"),
|
||||
build(:imported_browsers, date: ~D[2019-01-01], browser: "Firefox", pageviews: 3)
|
||||
])
|
||||
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
"/api/stats/#{site.domain}/suggestions/browser?period=month&date=2019-01-01&q=#{unquote(q)}&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
%{"value" => "Firefox", "label" => "Firefox"},
|
||||
%{"value" => "Chrome", "label" => "Chrome"}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
for {q, label} <- [{"", "without filter"}, {"i", "with filter"}] do
|
||||
test "merges operating system suggestions from native and imported data #{label}", %{
|
||||
conn: conn,
|
||||
site: site,
|
||||
site_import: site_import
|
||||
} do
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:pageview, timestamp: ~N[2019-01-01 23:00:01], operating_system: "Linux"),
|
||||
build(:pageview, timestamp: ~N[2019-01-01 23:30:01], operating_system: "Linux"),
|
||||
build(:pageview, timestamp: ~N[2019-01-01 23:40:01], operating_system: "Linux"),
|
||||
build(:pageview, timestamp: ~N[2019-01-01 23:00:01], operating_system: "Windows"),
|
||||
build(:imported_operating_systems,
|
||||
date: ~D[2019-01-01],
|
||||
operating_system: "Windows",
|
||||
pageviews: 3
|
||||
)
|
||||
])
|
||||
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
"/api/stats/#{site.domain}/suggestions/operating_system?period=month&date=2019-01-01&q=#{unquote(q)}&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
%{"value" => "Windows", "label" => "Windows"},
|
||||
%{"value" => "Linux", "label" => "Linux"}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
test "does not query imported data when a different property is filtered by", %{
|
||||
conn: conn,
|
||||
site: site,
|
||||
site_import: site_import
|
||||
} do
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:pageview,
|
||||
timestamp: ~N[2019-01-01 23:00:01],
|
||||
pathname: "/blog",
|
||||
operating_system: "Linux"
|
||||
),
|
||||
build(:imported_operating_systems, date: ~D[2019-01-01], operating_system: "Windows")
|
||||
])
|
||||
|
||||
filters = Jason.encode!(%{page: "/blog"})
|
||||
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
"/api/stats/#{site.domain}/suggestions/operating_system?period=month&date=2019-01-01&filters=#{filters}&q=&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [%{"value" => "Linux", "label" => "Linux"}]
|
||||
end
|
||||
|
||||
test "queries imported data when filtering by the same property", %{
|
||||
conn: conn,
|
||||
site: site,
|
||||
site_import: site_import
|
||||
} do
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:pageview,
|
||||
timestamp: ~N[2019-01-01 23:00:01],
|
||||
pathname: "/blog",
|
||||
operating_system: "Linux"
|
||||
),
|
||||
build(:imported_operating_systems, date: ~D[2019-01-01], operating_system: "Windows"),
|
||||
build(:imported_operating_systems, date: ~D[2019-01-01], operating_system: "Linux")
|
||||
])
|
||||
|
||||
filters = Jason.encode!(%{os: "!Linux"})
|
||||
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
"/api/stats/#{site.domain}/suggestions/operating_system?period=month&date=2019-01-01&filters=#{filters}&q=&with_imported=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [%{"value" => "Windows", "label" => "Windows"}]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -530,6 +530,102 @@ defmodule PlausibleWeb.Api.StatsController.TopStatsTest do
|
||||
%{"name" => "Visit duration", "value" => 303, "graph_metric" => "visit_duration"}
|
||||
]
|
||||
end
|
||||
|
||||
test ":member filter on country", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
populate_stats(site, [
|
||||
build(:pageview,
|
||||
country_code: "EE",
|
||||
user_id: @user_id,
|
||||
timestamp: ~N[2021-01-01 00:00:00]
|
||||
),
|
||||
build(:pageview,
|
||||
country_code: "EE",
|
||||
user_id: @user_id,
|
||||
timestamp: ~N[2021-01-01 00:01:00]
|
||||
),
|
||||
build(:imported_locations,
|
||||
country: "EE",
|
||||
date: ~D[2021-01-01],
|
||||
visitors: 1,
|
||||
visits: 3,
|
||||
pageviews: 34,
|
||||
bounces: 0,
|
||||
visit_duration: 420
|
||||
),
|
||||
build(:imported_locations,
|
||||
country: "FR",
|
||||
date: ~D[2021-01-01],
|
||||
visitors: 3,
|
||||
visits: 7,
|
||||
pageviews: 65,
|
||||
bounces: 1,
|
||||
visit_duration: 300
|
||||
),
|
||||
build(:imported_locations, country: "US", date: ~D[2021-01-01], visitors: 999)
|
||||
])
|
||||
|
||||
filters = Jason.encode!(%{country: "EE|FR"})
|
||||
q = "?period=day&date=2021-01-01&with_imported=true&filters=#{filters}"
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/top-stats#{q}")
|
||||
|
||||
res = json_response(conn, 200)
|
||||
|
||||
assert res["top_stats"] == [
|
||||
%{"name" => "Unique visitors", "value" => 5, "graph_metric" => "visitors"},
|
||||
%{"name" => "Total visits", "value" => 11, "graph_metric" => "visits"},
|
||||
%{"name" => "Total pageviews", "value" => 101, "graph_metric" => "pageviews"},
|
||||
%{
|
||||
"name" => "Views per visit",
|
||||
"value" => 9.18,
|
||||
"graph_metric" => "views_per_visit"
|
||||
},
|
||||
%{"name" => "Bounce rate", "value" => 9, "graph_metric" => "bounce_rate"},
|
||||
%{"name" => "Visit duration", "value" => 71, "graph_metric" => "visit_duration"}
|
||||
]
|
||||
end
|
||||
|
||||
test ":is filter on page returns only visitors, visits and pageviews", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
populate_stats(site, [
|
||||
build(:pageview,
|
||||
pathname: "/",
|
||||
user_id: @user_id,
|
||||
timestamp: ~N[2021-01-01 00:00:00]
|
||||
),
|
||||
build(:pageview,
|
||||
pathname: "/",
|
||||
user_id: @user_id,
|
||||
timestamp: ~N[2021-01-01 00:01:00]
|
||||
),
|
||||
build(:imported_pages,
|
||||
page: "/",
|
||||
date: ~D[2021-01-01],
|
||||
visitors: 1,
|
||||
visits: 3,
|
||||
pageviews: 34
|
||||
),
|
||||
build(:imported_pages, page: "/ignored", date: ~D[2021-01-01], visitors: 999)
|
||||
])
|
||||
|
||||
filters = Jason.encode!(%{page: "/"})
|
||||
q = "?period=day&date=2021-01-01&with_imported=true&filters=#{filters}"
|
||||
|
||||
conn = get(conn, "/api/stats/#{site.domain}/top-stats#{q}")
|
||||
|
||||
res = json_response(conn, 200)
|
||||
|
||||
assert res["top_stats"] == [
|
||||
%{"name" => "Unique visitors", "value" => 2, "graph_metric" => "visitors"},
|
||||
%{"name" => "Total visits", "value" => 4, "graph_metric" => "visits"},
|
||||
%{"name" => "Total pageviews", "value" => 36, "graph_metric" => "pageviews"}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /api/stats/top-stats - realtime" do
|
||||
@ -1358,7 +1454,7 @@ defmodule PlausibleWeb.Api.StatsController.TopStatsTest do
|
||||
"/api/stats/#{site.domain}/top-stats?period=month&date=2021-01-01&with_imported=true&comparison=year_over_year"
|
||||
)
|
||||
|
||||
assert %{"top_stats" => top_stats, "with_imported" => true} = json_response(conn, 200)
|
||||
assert %{"top_stats" => top_stats, "includes_imported" => true} = json_response(conn, 200)
|
||||
|
||||
assert %{
|
||||
"change" => 100,
|
||||
@ -1388,7 +1484,7 @@ defmodule PlausibleWeb.Api.StatsController.TopStatsTest do
|
||||
"/api/stats/#{site.domain}/top-stats?period=month&date=2021-01-01&with_imported=false&comparison=year_over_year"
|
||||
)
|
||||
|
||||
assert %{"top_stats" => top_stats, "with_imported" => false} = json_response(conn, 200)
|
||||
assert %{"top_stats" => top_stats, "includes_imported" => false} = json_response(conn, 200)
|
||||
|
||||
assert %{
|
||||
"change" => 100,
|
||||
|
Loading…
Reference in New Issue
Block a user