mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-03 23:00:14 -05:00
Clickthrough filtering for stats page (#21095)
closes https://linear.app/tryghost/issue/ANAL-58/click-through-filtering-for-content closes https://linear.app/tryghost/issue/ANAL-60/click-through-filtering-for-sources closes https://linear.app/tryghost/issue/ANAL-61/click-through-filtering-for-locations - This implements filtering and click-throughs for device, browser, source, location and pathname. - It requires significant updates to our tinybird setup, to pass through all the right data and have them as parameters on the API endpoints - We update the UI to add query parameters when clicking around and then pass those through to every chart/request. - We've added a interface to display the filters and remove them --------- Co-authored-by: Peter Zimon <peter.zimon@gmail.com>
This commit is contained in:
parent
5ebdbe4e25
commit
7e27b1cb36
31 changed files with 528 additions and 118 deletions
|
@ -1 +1 @@
|
|||
<div {{react-render this.ReactComponent props=(hash chartRange=@chartRange audience=@audience selected=@selected)}}></div>
|
||||
<div {{react-render this.ReactComponent props=(hash chartRange=@chartRange audience=@audience device=@device browser=@browser location=@location source=@source pathname=@pathname selected=@selected)}}></div>
|
|
@ -13,12 +13,11 @@ export default class KpisComponent extends Component {
|
|||
@inject config;
|
||||
|
||||
ReactComponent = (props) => {
|
||||
const {chartRange, audience, selected} = props;
|
||||
const {chartRange, selected} = props;
|
||||
|
||||
const params = getStatsParams(
|
||||
this.config,
|
||||
chartRange,
|
||||
audience
|
||||
props
|
||||
);
|
||||
|
||||
const {data, meta, error, loading} = useQuery({
|
||||
|
@ -113,7 +112,7 @@ export default class KpisComponent extends Component {
|
|||
type: 'line',
|
||||
z: 1
|
||||
},
|
||||
extraCssText: 'box-shadow: 0px 100px 80px 0px rgba(0, 0, 0, 0.07), 0px 41.778px 33.422px 0px rgba(0, 0, 0, 0.05), 0px 22.336px 17.869px 0px rgba(0, 0, 0, 0.04), 0px 12.522px 10.017px 0px rgba(0, 0, 0, 0.04), 0px 6.65px 5.32px 0px rgba(0, 0, 0, 0.03), 0px 2.767px 2.214px 0px rgba(0, 0, 0, 0.02);',
|
||||
extraCssText: 'box-shadow: 0 0 0 1px rgba(0,0,0,0.03), 0px 100px 80px 0px rgba(0, 0, 0, 0.07), 0px 41.778px 33.422px 0px rgba(0, 0, 0, 0.05), 0px 22.336px 17.869px 0px rgba(0, 0, 0, 0.04), 0px 12.522px 10.017px 0px rgba(0, 0, 0, 0.04), 0px 6.65px 5.32px 0px rgba(0, 0, 0, 0.03), 0px 2.767px 2.214px 0px rgba(0, 0, 0, 0.02); padding: 6px 8px;',
|
||||
formatter: function (fparams) {
|
||||
let displayValue;
|
||||
let tooltipTitle;
|
||||
|
@ -134,7 +133,7 @@ export default class KpisComponent extends Component {
|
|||
if (!displayValue) {
|
||||
displayValue = 'N/A';
|
||||
}
|
||||
return `<div><div>${moment(fparams[0].value[0]).format('D MMM, YYYY')}</div><div><span style="display: inline-block; margin-right: 16px; font-weight: 600;">${tooltipTitle}</span> ${displayValue}</div></div>`;
|
||||
return `<div><div class="gh-stats-tooltip-header">${moment(fparams[0].value[0]).format('D MMM YYYY')}</div><div class="gh-stats-tooltip-data"><span class="gh-stats-tooltip-marker" style="background: ${LINE_COLOR}"></span><span class="gh-stats-tooltip-label">${tooltipTitle}</span> <span class="gh-stats-tooltip-value">${displayValue}</span></div></div>`;
|
||||
}
|
||||
},
|
||||
series: [
|
||||
|
|
|
@ -1 +1 @@
|
|||
<div {{react-render this.ReactComponent props=(hash chartRange=@chartRange audience=@audience selected=@selected)}}></div>
|
||||
<div {{react-render this.ReactComponent props=(hash chartRange=@chartRange audience=@audience device=@device browser=@browser location=@location source=@source pathname=@pathname selected=@selected)}}></div>
|
||||
|
|
|
@ -3,21 +3,37 @@
|
|||
import Component from '@glimmer/component';
|
||||
import React from 'react';
|
||||
import {DonutChart, useQuery} from '@tinybirdco/charts';
|
||||
import {action} from '@ember/object';
|
||||
import {formatNumber} from '../../../helpers/format-number';
|
||||
import {getStatsParams, statsStaticColors} from 'ghost-admin/utils/stats';
|
||||
import {inject} from 'ghost-admin/decorators/inject';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default class TechnicalComponent extends Component {
|
||||
@service router;
|
||||
@inject config;
|
||||
|
||||
@action
|
||||
navigateToFilter(type, value) {
|
||||
this.updateQueryParams({[type]: value});
|
||||
}
|
||||
|
||||
@action
|
||||
updateQueryParams(params) {
|
||||
const currentRoute = this.router.currentRoute;
|
||||
const newQueryParams = {...currentRoute.queryParams, ...params};
|
||||
|
||||
this.router.transitionTo({queryParams: newQueryParams});
|
||||
}
|
||||
|
||||
ReactComponent = (props) => {
|
||||
const {chartRange, audience, selected} = props;
|
||||
const {selected} = props;
|
||||
|
||||
const colorPalette = statsStaticColors.slice(1, 5);
|
||||
|
||||
const params = getStatsParams(
|
||||
this.config,
|
||||
chartRange,
|
||||
audience,
|
||||
props,
|
||||
{limit: 5}
|
||||
);
|
||||
|
||||
|
@ -53,18 +69,27 @@ export default class TechnicalComponent extends Component {
|
|||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th><span className="gh-stats-detail-header">{tableHead}</span></th>
|
||||
<th><span className="gh-stats-detail-header">Visits</span></th>
|
||||
<th><span className="gh-stats-data-header">{tableHead}</span></th>
|
||||
<th><span className="gh-stats-data-header">Visits</span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{transformedData.map((item, index) => (
|
||||
<tr key={index}>
|
||||
<td>
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
this.navigateToFilter(indexBy, item.name.toLowerCase());
|
||||
}}
|
||||
className="gh-stats-data-label"
|
||||
>
|
||||
<span style={{backgroundColor: item.color, display: 'inline-block', width: '10px', height: '10px', marginRight: '5px', borderRadius: '2px'}}></span>
|
||||
{item.name}
|
||||
</a>
|
||||
</td>
|
||||
<td>{formatNumber(item.value)}</td>
|
||||
<td><span className="gh-stats-data-value">{formatNumber(item.value)}</span></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<div>
|
||||
<div class="gh-stats-metric-header"><h5 class="gh-stats-metric-label">Locations</h5></div>
|
||||
<div {{react-render this.ReactComponent props=(hash chartRange=@chartRange audience=@audience)}}></div>
|
||||
<div {{react-render this.ReactComponent props=(hash chartRange=@chartRange audience=@audience device=@device browser=@browser location=@location source=@source pathname=@pathname)}}></div>
|
||||
</div>
|
||||
|
||||
<div class="gh-stats-see-all-container">
|
||||
<button type="button" class="gh-btn gh-btn-link gh-stats-see-all-btn" {{on "click" (fn this.openSeeAll @chartRange @audience)}}>
|
||||
<button type="button" class="gh-btn gh-btn-link gh-stats-see-all-btn" {{on "click" (fn this.openSeeAll @chartRange @audience @device @browser @location @source @pathname)}}>
|
||||
<span>See all →</span>
|
||||
</button>
|
||||
</div>
|
|
@ -13,6 +13,7 @@ import {inject as service} from '@ember/service';
|
|||
export default class TopLocations extends Component {
|
||||
@inject config;
|
||||
@service modals;
|
||||
@service router;
|
||||
|
||||
@action
|
||||
openSeeAll() {
|
||||
|
@ -23,13 +24,22 @@ export default class TopLocations extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
ReactComponent = (props) => {
|
||||
const {chartRange, audience} = props;
|
||||
@action
|
||||
navigateToFilter(location) {
|
||||
this.updateQueryParams({location});
|
||||
}
|
||||
|
||||
updateQueryParams(params) {
|
||||
const currentRoute = this.router.currentRoute;
|
||||
const newQueryParams = {...currentRoute.queryParams, ...params};
|
||||
|
||||
this.router.transitionTo({queryParams: newQueryParams});
|
||||
}
|
||||
|
||||
ReactComponent = (props) => {
|
||||
const params = getStatsParams(
|
||||
this.config,
|
||||
chartRange,
|
||||
audience,
|
||||
props,
|
||||
{limit: 7}
|
||||
);
|
||||
|
||||
|
@ -47,16 +57,27 @@ export default class TopLocations extends Component {
|
|||
loading={loading}
|
||||
index="location"
|
||||
indexConfig={{
|
||||
label: <span className="gh-stats-detail-header">Country</span>,
|
||||
label: <span className="gh-stats-data-header">Country</span>,
|
||||
renderBarContent: ({label}) => (
|
||||
<span className="gh-stats-detail-label">{getCountryFlag(label)} {label || 'Unknown'}</span>
|
||||
<span className="gh-stats-data-label">
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
this.navigateToFilter(label || 'Unknown');
|
||||
}}
|
||||
className="gh-stats-domain"
|
||||
>
|
||||
{getCountryFlag(label)} {label || 'Unknown'}
|
||||
</a>
|
||||
</span>
|
||||
)
|
||||
}}
|
||||
categories={['hits']}
|
||||
categoryConfig={{
|
||||
hits: {
|
||||
label: <span className="gh-stats-detail-header">Visits</span>,
|
||||
renderValue: ({value}) => <span className="gh-stats-detail-value">{formatNumber(value)}</span>
|
||||
label: <span className="gh-stats-data-header">Visits</span>,
|
||||
renderValue: ({value}) => <span className="gh-stats-data-value">{formatNumber(value)}</span>
|
||||
}
|
||||
}}
|
||||
colorPalette={[barListColor]}
|
||||
|
|
|
@ -18,11 +18,11 @@
|
|||
</PowerSelect>
|
||||
</div> --}}
|
||||
</div>
|
||||
<div {{react-render this.ReactComponent props=(hash chartRange=@chartRange audience=@audience)}}></div>
|
||||
<div {{react-render this.ReactComponent props=(hash chartRange=@chartRange audience=@audience device=@device browser=@browser location=@location source=@source pathname=@pathname)}}></div>
|
||||
</div>
|
||||
|
||||
<div class="gh-stats-see-all-container">
|
||||
<button type="button" class="gh-btn gh-btn-link gh-stats-see-all-btn" {{on "click" (fn this.openSeeAll @chartRange @audience)}}>
|
||||
<button type="button" class="gh-btn gh-btn-link gh-stats-see-all-btn" {{on "click" (fn this.openSeeAll @chartRange @audience @device @browser @location @source @pathname)}}>
|
||||
<span>See all →</span>
|
||||
</button>
|
||||
</div>
|
|
@ -13,12 +13,12 @@ import {tracked} from '@glimmer/tracking';
|
|||
|
||||
export default class TopPages extends Component {
|
||||
@inject config;
|
||||
@service modals;
|
||||
@service router;
|
||||
|
||||
@tracked contentOption = CONTENT_OPTIONS[0];
|
||||
@tracked contentOptions = CONTENT_OPTIONS;
|
||||
|
||||
@service modals;
|
||||
|
||||
@action
|
||||
openSeeAll(chartRange, audience) {
|
||||
this.modals.open(AllStatsModal, {
|
||||
|
@ -33,13 +33,22 @@ export default class TopPages extends Component {
|
|||
this.contentOption = selected;
|
||||
}
|
||||
|
||||
ReactComponent = (props) => {
|
||||
const {chartRange, audience} = props;
|
||||
@action
|
||||
navigateToFilter(pathname) {
|
||||
this.updateQueryParams({pathname});
|
||||
}
|
||||
|
||||
updateQueryParams(params) {
|
||||
const currentRoute = this.router.currentRoute;
|
||||
const newQueryParams = {...currentRoute.queryParams, ...params};
|
||||
|
||||
this.router.transitionTo({queryParams: newQueryParams});
|
||||
}
|
||||
|
||||
ReactComponent = (props) => {
|
||||
const params = getStatsParams(
|
||||
this.config,
|
||||
chartRange,
|
||||
audience,
|
||||
props,
|
||||
{limit: 7}
|
||||
);
|
||||
|
||||
|
@ -57,16 +66,27 @@ export default class TopPages extends Component {
|
|||
loading={loading}
|
||||
index="pathname"
|
||||
indexConfig={{
|
||||
label: <span className="gh-stats-detail-header">Post or page</span>,
|
||||
label: <span className="gh-stats-data-header">Post or page</span>,
|
||||
renderBarContent: ({label}) => (
|
||||
<span className="gh-stats-detail-label">{label}</span>
|
||||
<span className="gh-stats-data-label">
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
this.navigateToFilter(label);
|
||||
}}
|
||||
className="gh-stats-domain"
|
||||
>
|
||||
{label}
|
||||
</a>
|
||||
</span>
|
||||
)
|
||||
}}
|
||||
categories={['hits']}
|
||||
categoryConfig={{
|
||||
hits: {
|
||||
label: <span className="gh-stats-detail-header">Visits</span>,
|
||||
renderValue: ({value}) => <span className="gh-stats-detail-value">{formatNumber(value)}</span>
|
||||
label: <span className="gh-stats-data-header">Visits</span>,
|
||||
renderValue: ({value}) => <span className="gh-stats-data-value">{formatNumber(value)}</span>
|
||||
}
|
||||
}}
|
||||
colorPalette={[barListColor]}
|
||||
|
|
|
@ -19,11 +19,11 @@
|
|||
</div> --}}
|
||||
</div>
|
||||
|
||||
<div {{react-render this.ReactComponent props=(hash chartRange=@chartRange audience=@audience)}}></div>
|
||||
<div {{react-render this.ReactComponent props=(hash chartRange=@chartRange audience=@audience device=@device browser=@browser location=@location source=@source pathname=@pathname)}}></div>
|
||||
</div>
|
||||
|
||||
<div class="gh-stats-see-all-container">
|
||||
<button type="button" class="gh-btn gh-btn-link gh-stats-see-all-btn" {{on "click" (fn this.openSeeAll @chartRange @audience)}}>
|
||||
<button type="button" class="gh-btn gh-btn-link gh-stats-see-all-btn" {{on "click" (fn this.openSeeAll @chartRange @audience @device @browser @location @source @pathname)}}>
|
||||
<span>See all →</span>
|
||||
</button>
|
||||
</div>
|
|
@ -33,16 +33,25 @@ export default class TopSources extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
ReactComponent = (props) => {
|
||||
const {chartRange, audience} = props;
|
||||
@action
|
||||
navigateToFilter(source) {
|
||||
this.updateQueryParams({source});
|
||||
}
|
||||
|
||||
updateQueryParams(params) {
|
||||
const currentRoute = this.router.currentRoute;
|
||||
const newQueryParams = {...currentRoute.queryParams, ...params};
|
||||
|
||||
this.router.transitionTo({queryParams: newQueryParams});
|
||||
}
|
||||
|
||||
ReactComponent = (props) => {
|
||||
const {data, meta, error, loading} = useQuery({
|
||||
endpoint: `${this.config.stats.endpoint}/v0/pipes/top_sources.json`,
|
||||
token: this.config.stats.token,
|
||||
params: getStatsParams(
|
||||
this.config,
|
||||
chartRange,
|
||||
audience,
|
||||
props,
|
||||
{limit: 7}
|
||||
)
|
||||
});
|
||||
|
@ -55,9 +64,9 @@ export default class TopSources extends Component {
|
|||
loading={loading}
|
||||
index="source"
|
||||
indexConfig={{
|
||||
label: <span className="gh-stats-detail-header">Source</span>,
|
||||
label: <span className="gh-stats-data-header">Source</span>,
|
||||
renderBarContent: ({label}) => (
|
||||
<span className="gh-stats-detail-label">
|
||||
<span className="gh-stats-data-label">
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
|
@ -75,8 +84,8 @@ export default class TopSources extends Component {
|
|||
categories={['hits']}
|
||||
categoryConfig={{
|
||||
hits: {
|
||||
label: <span className="gh-stats-detail-header">Visits</span>,
|
||||
renderValue: ({value}) => <span className="gh-stats-detail-value">{formatNumber(value)}</span>
|
||||
label: <span className="gh-stats-data-header">Visits</span>,
|
||||
renderValue: ({value}) => <span className="gh-stats-data-value">{formatNumber(value)}</span>
|
||||
}
|
||||
}}
|
||||
colorPalette={[barListColor]}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
<div class="gh-stats-tabs-header" {{did-update this.fetchDataIfNeeded @chartRange @audience}}>
|
||||
<div class="gh-stats-tabs-header" {{did-update this.fetchDataIfNeeded @chartRange @audience @device @browser @location @source @pathname}}>
|
||||
<div class="gh-stats-tabs">
|
||||
<button type="button" class="gh-stats-tab min-width {{if this.uniqueVisitsTabSelected 'is-selected'}}" {{on "click" this.changeTabToUniqueVisits}}>
|
||||
<Stats::Parts::Metric
|
||||
|
@ -45,5 +45,14 @@
|
|||
</div> --}}
|
||||
</div>
|
||||
<div class="gh-stats-kpis-chart-container">
|
||||
<Stats::Charts::Kpis @chartRange={{@chartRange}} @audience={{@audience}} @selected={{this.selected}} />
|
||||
<Stats::Charts::Kpis
|
||||
@chartRange={{@chartRange}}
|
||||
@audience={{@audience}}
|
||||
@device={{@device}}
|
||||
@browser={{@browser}}
|
||||
@location={{@location}}
|
||||
@source={{@source}}
|
||||
@pathname={{@pathname}}
|
||||
@selected={{this.selected}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -48,16 +48,15 @@ export default class KpisOverview extends Component {
|
|||
|
||||
@action
|
||||
fetchDataIfNeeded() {
|
||||
this.fetchData.perform(this.args.chartRange, this.args.audience);
|
||||
this.fetchData.perform(this.args);
|
||||
}
|
||||
|
||||
@task
|
||||
*fetchData(chartRange, audience) {
|
||||
*fetchData(args) {
|
||||
try {
|
||||
const params = new URLSearchParams(getStatsParams(
|
||||
this.config,
|
||||
chartRange,
|
||||
audience
|
||||
args
|
||||
));
|
||||
|
||||
const response = yield fetch(`${this.config.stats.endpoint}/v0/pipes/kpis.json?${params}`, {
|
||||
|
|
|
@ -3,12 +3,15 @@
|
|||
import Component from '@glimmer/component';
|
||||
import React from 'react';
|
||||
import {BarList, useQuery} from '@tinybirdco/charts';
|
||||
import {action} from '@ember/object';
|
||||
import {barListColor, getCountryFlag, getStatsParams} from 'ghost-admin/utils/stats';
|
||||
import {formatNumber} from 'ghost-admin/helpers/format-number';
|
||||
import {inject} from 'ghost-admin/decorators/inject';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default class AllStatsModal extends Component {
|
||||
@inject config;
|
||||
@service router;
|
||||
|
||||
get type() {
|
||||
return this.args.data.type;
|
||||
|
@ -33,13 +36,33 @@ export default class AllStatsModal extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
@action
|
||||
navigateToFilter(label) {
|
||||
const params = {};
|
||||
if (this.type === 'top-sources') {
|
||||
params.source = label || 'direct';
|
||||
} else if (this.type === 'top-locations') {
|
||||
params.location = label || 'unknown';
|
||||
} else if (this.type === 'top-pages') {
|
||||
params.pathname = label;
|
||||
}
|
||||
|
||||
this.updateQueryParams(params);
|
||||
}
|
||||
|
||||
updateQueryParams(params) {
|
||||
const currentRoute = this.router.currentRoute;
|
||||
const newQueryParams = {...currentRoute.queryParams, ...params};
|
||||
|
||||
this.router.transitionTo({queryParams: newQueryParams});
|
||||
}
|
||||
|
||||
ReactComponent = (props) => {
|
||||
const {chartRange, audience, type} = props;
|
||||
const {type} = props;
|
||||
|
||||
const params = getStatsParams(
|
||||
this.config,
|
||||
chartRange,
|
||||
audience
|
||||
props
|
||||
);
|
||||
|
||||
let endpoint;
|
||||
|
@ -80,16 +103,34 @@ export default class AllStatsModal extends Component {
|
|||
loading={loading}
|
||||
index={indexBy}
|
||||
indexConfig={{
|
||||
label: <span className="gh-stats-detail-header">{labelText}</span>,
|
||||
label: <span className="gh-stats-data-header">{labelText}</span>,
|
||||
renderBarContent: ({label}) => (
|
||||
<span className={`gh-stats-detail-label ${type === 'top-sources' && 'gh-stats-domain'}`}>{(type === 'top-locations') && getCountryFlag(label)} {type === 'top-sources' && (<img src={`https://www.google.com/s2/favicons?domain=${label || 'direct'}&sz=32`} className="gh-stats-favicon" />)} {label || unknownOption}</span>
|
||||
<span className={`gh-stats-data-label ${type === 'top-sources' && 'gh-stats-domain'}`}>
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
this.navigateToFilter(label);
|
||||
}}
|
||||
className="gh-stats-domain"
|
||||
>
|
||||
{(type === 'top-locations') && getCountryFlag(label)}
|
||||
{(type === 'top-sources') && (
|
||||
<img
|
||||
src={`https://www.google.com/s2/favicons?domain=${label || 'direct'}&sz=32`}
|
||||
className="gh-stats-favicon"
|
||||
/>
|
||||
)}
|
||||
{label || unknownOption}
|
||||
</a>
|
||||
</span>
|
||||
)
|
||||
}}
|
||||
categories={['hits']}
|
||||
categoryConfig={{
|
||||
hits: {
|
||||
label: <span className="gh-stats-detail-header">Visits</span>,
|
||||
renderValue: ({value}) => <span className="gh-stats-detail-value">{formatNumber(value)}</span>
|
||||
label: <span className="gh-stats-data-header">Visits</span>,
|
||||
renderValue: ({value}) => <span className="gh-stats-data-value">{formatNumber(value)}</span>
|
||||
}
|
||||
}}
|
||||
colorPalette={[barListColor]}
|
||||
|
|
|
@ -18,5 +18,14 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<Stats::Charts::Technical @chartRange={{@chartRange}} @audience={{@audience}} @selected={{this.selected}} />
|
||||
<Stats::Charts::Technical
|
||||
@chartRange={{@chartRange}}
|
||||
@audience={{@audience}}
|
||||
@device={{@device}}
|
||||
@browser={{@browser}}
|
||||
@location={{@location}}
|
||||
@source={{@source}}
|
||||
@pathname={{@pathname}}
|
||||
@selected={{this.selected}}
|
||||
/>
|
||||
</div>
|
|
@ -4,6 +4,14 @@ import {action} from '@ember/object';
|
|||
import {tracked} from '@glimmer/tracking';
|
||||
|
||||
export default class StatsController extends Controller {
|
||||
queryParams = ['device', 'browser', 'location', 'source', 'pathname'];
|
||||
|
||||
@tracked device = null;
|
||||
@tracked browser = null;
|
||||
@tracked location = null;
|
||||
@tracked source = null;
|
||||
@tracked pathname = null;
|
||||
|
||||
rangeOptions = RANGE_OPTIONS;
|
||||
audienceOptions = AUDIENCE_TYPES;
|
||||
/**
|
||||
|
@ -36,6 +44,15 @@ export default class StatsController extends Controller {
|
|||
}
|
||||
}
|
||||
|
||||
@action
|
||||
clearFilters() {
|
||||
this.device = null;
|
||||
this.browser = null;
|
||||
this.location = null;
|
||||
this.source = null;
|
||||
this.pathname = null;
|
||||
}
|
||||
|
||||
get selectedRangeOption() {
|
||||
return this.rangeOptions.find(d => d.value === this.chartRange);
|
||||
}
|
||||
|
|
|
@ -1,3 +1,13 @@
|
|||
.gh-stats-header header {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.gh-stats-header header .view-actions {
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.gh-stats .view-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -198,7 +208,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.gh-stats-detail-header {
|
||||
.gh-stats-data-header {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-transform: none;
|
||||
|
@ -216,10 +226,22 @@
|
|||
color: var(--green);
|
||||
}
|
||||
|
||||
.gh-stats-detail-label,
|
||||
.gh-stats-detail-value {
|
||||
font-size: 13.5px;
|
||||
.gh-stats-data-label,
|
||||
.gh-stats-data-value {
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.gh-stats-data-label {
|
||||
color: var(--black);
|
||||
}
|
||||
|
||||
a.gh-stats-data-label:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.gh-stats-data-value {
|
||||
color: var(--middarkgrey);
|
||||
}
|
||||
|
||||
.gh-stats-see-all-container {
|
||||
|
@ -227,6 +249,7 @@
|
|||
}
|
||||
|
||||
.gh-stats-see-all-container::before {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
display: block;
|
||||
content: '';
|
||||
|
@ -246,9 +269,98 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--black);
|
||||
}
|
||||
|
||||
.gh-stats-domain:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.gh-stats-favicon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.gh-stats-tooltip-header {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.gh-stats-tooltip-data {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.gh-stats-tooltip-label {
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
color: var(--middarkgrey)
|
||||
}
|
||||
|
||||
.gh-stats-tooltip-value {
|
||||
font-family: var(--font-family-mono);
|
||||
font-size: 12.5px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.gh-stats-tooltip-marker {
|
||||
display: block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 3px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
/* Filters */
|
||||
.gh-stats-filters {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: -24px;
|
||||
}
|
||||
|
||||
.gh-stats-filters {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.gh-stats-filter-pill {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
border: 1px solid var(--whitegrey);
|
||||
border-radius: 999px;
|
||||
padding: 4px 12px;
|
||||
}
|
||||
|
||||
.gh-stats-filter-pill .value {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.gh-btn-clear-filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border-radius: 999px;
|
||||
color: var(--black);
|
||||
padding: 4px 12px;
|
||||
border: 1px solid var(--whitegrey);
|
||||
transition: all ease-in-out 0.3s;
|
||||
}
|
||||
|
||||
.gh-btn-clear-filters:hover {
|
||||
background-color: var(--whitegrey-l2);
|
||||
}
|
||||
|
||||
.gh-btn-clear-filters svg {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
stroke: var(--red);
|
||||
}
|
||||
|
||||
.gh-btn-clear-filters svg path {
|
||||
stroke-width: 2px;
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
<section class="gh-stats gh-canvas gh-canvas-sticky">
|
||||
<GhCanvasHeader class="gh-canvas-header sticky break tablet post-header">
|
||||
<GhCanvasHeader class="gh-canvas-header gh-stats-header sticky break tablet post-header">
|
||||
<GhCustomViewTitle @title="Stats" />
|
||||
<div class="view-actions">
|
||||
<Stats::Parts::AudienceFilter
|
||||
|
@ -22,28 +22,103 @@
|
|||
{{#if option.name}}{{option.name}}{{else}}<span class="red">Unknown option</span>{{/if}}
|
||||
</PowerSelect>
|
||||
</div>
|
||||
|
||||
{{#if (or this.device this.browser this.location this.source this.pathname)}}
|
||||
<div class="gh-stats-filters">
|
||||
{{#if this.pathname}}
|
||||
<div class="gh-stats-filter-pill">
|
||||
<div>Page is <span class="value">{{this.pathname}}</span></div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.source}}
|
||||
<div class="gh-stats-filter-pill">
|
||||
<div>Source is <span class="value">{{this.source}}</span></div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.location}}
|
||||
<div class="gh-stats-filter-pill">
|
||||
<div>Location is <span class="value">{{this.location}}</span></div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.device}}
|
||||
<div class="gh-stats-filter-pill">
|
||||
<div>Device is <span class="value">{{this.device}}</span></div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.browser}}
|
||||
<div class="gh-stats-filter-pill">
|
||||
<div>Browser is <span class="value">{{this.browser}}</span></div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<a href="#/stats" class="gh-btn-clear-filters" {{on "click" this.clearFilters}}>{{svg-jar "close"}} <span>Clear</span></a>
|
||||
</div>
|
||||
{{/if}}
|
||||
</GhCanvasHeader>
|
||||
|
||||
<section class="view-container">
|
||||
<section class="gh-stats-container no-gap">
|
||||
<Stats::KpisOverview @chartRange={{this.chartRange}} @audience={{this.audience}} />
|
||||
<Stats::KpisOverview
|
||||
@chartRange={{this.chartRange}}
|
||||
@audience={{this.audience}}
|
||||
@device={{this.device}}
|
||||
@browser={{this.browser}}
|
||||
@location={{this.location}}
|
||||
@source={{this.source}}
|
||||
@pathname={{this.pathname}}/>
|
||||
</section>
|
||||
|
||||
<section class="gh-stats-grid cols-2">
|
||||
<div class="gh-stats-container">
|
||||
<Stats::Charts::TopPages @chartRange={{this.chartRange}} @audience={{this.audience}} />
|
||||
<Stats::Charts::TopPages
|
||||
@chartRange={{this.chartRange}}
|
||||
@audience={{this.audience}}
|
||||
@device={{this.device}}
|
||||
@browser={{this.browser}}
|
||||
@location={{this.location}}
|
||||
@source={{this.source}}
|
||||
@pathname={{this.pathname}}
|
||||
/>
|
||||
</div>
|
||||
<div class="gh-stats-container">
|
||||
<Stats::Charts::TopSources @chartRange={{this.chartRange}} @audience={{this.audience}} />
|
||||
<Stats::Charts::TopSources
|
||||
@chartRange={{this.chartRange}}
|
||||
@audience={{this.audience}}
|
||||
@device={{this.device}}
|
||||
@browser={{this.browser}}
|
||||
@location={{this.location}}
|
||||
@source={{this.source}}
|
||||
@pathname={{this.pathname}}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="gh-stats-grid cols-2">
|
||||
<div class="gh-stats-container">
|
||||
<Stats::Charts::TopLocations @chartRange={{this.chartRange}} @audience={{this.audience}} />
|
||||
<Stats::Charts::TopLocations
|
||||
@chartRange={{this.chartRange}}
|
||||
@audience={{this.audience}}
|
||||
@device={{this.device}}
|
||||
@browser={{this.browser}}
|
||||
@location={{this.location}}
|
||||
@source={{this.source}}
|
||||
@pathname={{this.pathname}}
|
||||
/>
|
||||
</div>
|
||||
<div class="gh-stats-container">
|
||||
<Stats::TechnicalOverview @chartRange={{this.chartRange}} @audience={{this.audience}} />
|
||||
<Stats::TechnicalOverview
|
||||
@chartRange={{this.chartRange}}
|
||||
@audience={{this.audience}}
|
||||
@device={{this.device}}
|
||||
@browser={{this.browser}}
|
||||
@location={{this.location}}
|
||||
@source={{this.source}}
|
||||
@pathname={{this.pathname}}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
|
@ -128,7 +128,8 @@ export function getDateRange(chartRange) {
|
|||
return {startDate, endDate};
|
||||
}
|
||||
|
||||
export function getStatsParams(config, chartRange, audience, additionalParams = {}) {
|
||||
export function getStatsParams(config, props, additionalParams = {}) {
|
||||
const {chartRange, audience, device, browser, location, source, pathname} = props;
|
||||
const {startDate, endDate} = getDateRange(chartRange);
|
||||
|
||||
const params = {
|
||||
|
@ -142,5 +143,25 @@ export function getStatsParams(config, chartRange, audience, additionalParams =
|
|||
params.member_status = audience.join(',');
|
||||
}
|
||||
|
||||
if (device) {
|
||||
params.device = device;
|
||||
}
|
||||
|
||||
if (browser) {
|
||||
params.browser = browser;
|
||||
}
|
||||
|
||||
if (location) {
|
||||
params.location = location;
|
||||
}
|
||||
|
||||
if (source) {
|
||||
params.source = source === 'direct' ? '' : source;
|
||||
}
|
||||
|
||||
if (pathname) {
|
||||
params.pathname = pathname;
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
<svg version="1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<title>close</title>
|
||||
<path d="M12.707 12 23.854.854a.5.5 0 0 0-.707-.707L12 11.293.854.146a.5.5 0 0 0-.707.707L11.293 12 .146 23.146a.5.5 0 0 0 .708.708L12 12.707l11.146 11.146a.5.5 0 1 0 .708-.706L12.707 12z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 272 B |
|
@ -5,6 +5,7 @@ SCHEMA >
|
|||
`device` String,
|
||||
`browser` String,
|
||||
`location` String,
|
||||
`source` String,
|
||||
`pathname` String,
|
||||
`member_status` SimpleAggregateFunction(any, String),
|
||||
`visits` AggregateFunction(uniq, String),
|
||||
|
@ -12,4 +13,4 @@ SCHEMA >
|
|||
|
||||
ENGINE AggregatingMergeTree
|
||||
ENGINE_PARTITION_KEY toYYYYMM(date)
|
||||
ENGINE_SORTING_KEY date, device, browser, location, pathname, post_uuid, site_uuid
|
||||
ENGINE_SORTING_KEY date, device, browser, location, source, pathname, post_uuid, site_uuid
|
||||
|
|
|
@ -7,6 +7,8 @@ SCHEMA >
|
|||
`device` SimpleAggregateFunction(any, String),
|
||||
`browser` SimpleAggregateFunction(any, String),
|
||||
`location` SimpleAggregateFunction(any, String),
|
||||
`source` SimpleAggregateFunction(any, String),
|
||||
`pathname` SimpleAggregateFunction(any, String),
|
||||
`first_hit` SimpleAggregateFunction(min, DateTime),
|
||||
`latest_hit` SimpleAggregateFunction(max, DateTime),
|
||||
`hits` AggregateFunction(count)
|
||||
|
|
|
@ -5,10 +5,11 @@ SCHEMA >
|
|||
`browser` String,
|
||||
`location` String,
|
||||
`source` String,
|
||||
`pathname` String,
|
||||
`member_status` SimpleAggregateFunction(any, String),
|
||||
`visits` AggregateFunction(uniq, String),
|
||||
`hits` AggregateFunction(count)
|
||||
|
||||
ENGINE AggregatingMergeTree
|
||||
ENGINE_PARTITION_KEY toYYYYMM(date)
|
||||
ENGINE_SORTING_KEY date, device, browser, location, source, site_uuid
|
||||
ENGINE_SORTING_KEY date, device, browser, location, source, pathname, site_uuid
|
||||
|
|
|
@ -10,6 +10,7 @@ SQL >
|
|||
device,
|
||||
browser,
|
||||
location,
|
||||
source,
|
||||
pathname,
|
||||
maxIf(
|
||||
member_status,
|
||||
|
@ -18,7 +19,7 @@ SQL >
|
|||
uniqState(session_id) AS visits,
|
||||
countState() AS hits
|
||||
FROM analytics_hits
|
||||
GROUP BY date, device, browser, location, pathname, post_uuid,site_uuid
|
||||
GROUP BY date, device, browser, location, source, pathname, post_uuid,site_uuid
|
||||
|
||||
TYPE MATERIALIZED
|
||||
DATASOURCE analytics_pages_mv
|
||||
|
|
|
@ -15,6 +15,8 @@ SQL >
|
|||
anySimpleState(device) AS device,
|
||||
anySimpleState(browser) AS browser,
|
||||
anySimpleState(location) AS location,
|
||||
anySimpleState(source) AS source,
|
||||
anySimpleState(pathname) AS pathname,
|
||||
minSimpleState(timestamp) AS first_hit,
|
||||
maxSimpleState(timestamp) AS latest_hit,
|
||||
countState() AS hits
|
||||
|
|
|
@ -11,6 +11,7 @@ SQL >
|
|||
browser,
|
||||
location,
|
||||
source,
|
||||
pathname,
|
||||
maxIf(
|
||||
member_status,
|
||||
member_status IN ('paid', 'free', 'undefined')
|
||||
|
@ -19,7 +20,7 @@ SQL >
|
|||
countState() AS hits
|
||||
FROM analytics_hits
|
||||
WHERE source != current_domain
|
||||
GROUP BY date, device, browser, location, source, site_uuid
|
||||
GROUP BY date, device, browser, location, source, pathname, site_uuid
|
||||
|
||||
TYPE MATERIALIZED
|
||||
DATASOURCE analytics_sources_mv
|
||||
|
|
|
@ -76,6 +76,11 @@ SQL >
|
|||
toStartOfHour(timestamp) as date,
|
||||
session_id,
|
||||
member_status,
|
||||
device,
|
||||
browser,
|
||||
location,
|
||||
source,
|
||||
pathname,
|
||||
uniq(session_id) as visits,
|
||||
count() as pageviews,
|
||||
case when min(timestamp) = max(timestamp) then 1 else 0 end as is_bounce,
|
||||
|
@ -83,12 +88,17 @@ SQL >
|
|||
min(timestamp) as first_hit_aux
|
||||
from analytics_hits
|
||||
where toDate(timestamp) = {{ Date(date_from) }}
|
||||
group by toStartOfHour(timestamp), session_id, site_uuid, member_status
|
||||
group by toStartOfHour(timestamp), session_id, site_uuid, member_status, device, browser, location, source, pathname
|
||||
{% else %}
|
||||
select
|
||||
site_uuid,
|
||||
date,
|
||||
member_status,
|
||||
device,
|
||||
browser,
|
||||
location,
|
||||
source,
|
||||
pathname,
|
||||
session_id,
|
||||
uniq(session_id) as visits,
|
||||
countMerge(hits) as pageviews,
|
||||
|
@ -103,7 +113,7 @@ SQL >
|
|||
{% if defined(date_to) %} and date <= {{ Date(date_to) }}
|
||||
{% else %} and date <= today()
|
||||
{% end %}
|
||||
group by date, session_id, site_uuid, member_status
|
||||
group by date, session_id, site_uuid, member_status, device, browser, location, source, pathname
|
||||
{% end %}
|
||||
|
||||
NODE data
|
||||
|
@ -115,12 +125,17 @@ SQL >
|
|||
site_uuid,
|
||||
date,
|
||||
member_status,
|
||||
device,
|
||||
browser,
|
||||
location,
|
||||
source,
|
||||
pathname,
|
||||
uniq(session_id) as visits,
|
||||
sum(pageviews) as pageviews,
|
||||
sum(case when latest_hit_aux = first_hit_aux then 1 else 0 end) / visits as bounce_rate,
|
||||
avg(latest_hit_aux - first_hit_aux) as avg_session_sec
|
||||
from hits
|
||||
group by date, site_uuid, member_status
|
||||
group by date, site_uuid, member_status, device, browser, location, source, pathname
|
||||
|
||||
NODE endpoint
|
||||
DESCRIPTION >
|
||||
|
@ -138,8 +153,12 @@ SQL >
|
|||
left join data b using date
|
||||
where
|
||||
site_uuid = {{String(site_uuid, 'mock_site_uuid', description="Tenant ID", required=True)}}
|
||||
{% if defined(member_status) %}
|
||||
and member_status IN {{ Array(member_status) }}
|
||||
{% end %}
|
||||
|
||||
{% if defined(member_status) %} and member_status IN {{ Array(member_status, "'undefined', 'free', 'paid'", description="Member status to filter on", required=False) }} {% end %}
|
||||
{% if defined(device) %} and device = {{ String(device, description="Device to filter on", required=False) }} {% end %}
|
||||
{% if defined(browser) %} and browser = {{ String(browser, description="Browser to filter on", required=False) }} {% end %}
|
||||
{% if defined(source) %} and source = {{ String(source, description="Source to filter on", required=False) }} {% end %}
|
||||
{% if defined(location) %} and location = {{ String(location, description="Location to filter on", required=False) }} {% end %}
|
||||
{% if defined(pathname) %} and pathname = {{ String(pathname, description="Pathname to filter on", required=False) }} {% end %}
|
||||
group by date
|
||||
order by date WITH FILL STEP 1
|
||||
|
|
|
@ -16,9 +16,7 @@ SQL >
|
|||
from analytics_sources_mv
|
||||
where
|
||||
site_uuid = {{String(site_uuid, 'mock_site_uuid', description="Tenant ID", required=True)}}
|
||||
{% if defined(member_status) %}
|
||||
and member_status IN {{ Array(member_status,'String') }}
|
||||
{% end %}
|
||||
|
||||
{% if defined(date_from) %}
|
||||
and date
|
||||
>=
|
||||
|
@ -32,6 +30,13 @@ SQL >
|
|||
{{ Date(date_to, description="Finishing day for filtering a date range", required=False) }}
|
||||
{% else %} and date <= today()
|
||||
{% end %}
|
||||
|
||||
{% if defined(member_status) %} and member_status IN {{ Array(member_status, "'undefined', 'free', 'paid'", description="Member status to filter on", required=False) }} {% end %}
|
||||
{% if defined(device) %} and device = {{ String(device, description="Device to filter on", required=False) }} {% end %}
|
||||
{% if defined(browser) %} and browser = {{ String(browser, description="Browser to filter on", required=False) }} {% end %}
|
||||
{% if defined(source) %} and source = {{ String(source, description="Source to filter on", required=False) }} {% end %}
|
||||
{% if defined(location) %} and location = {{ String(location, description="Location to filter on", required=False) }} {% end %}
|
||||
{% if defined(pathname) %} and pathname = {{ String(pathname, description="Pathname to filter on", required=False) }} {% end %}
|
||||
group by browser
|
||||
order by visits desc
|
||||
limit {{ Int32(skip, 0) }},{{ Int32(limit, 50) }}
|
||||
|
|
|
@ -17,9 +17,7 @@ SQL >
|
|||
from analytics_sources_mv
|
||||
where
|
||||
site_uuid = {{String(site_uuid, 'mock_site_uuid', description="Tenant ID", required=True)}}
|
||||
{% if defined(member_status) %}
|
||||
and member_status IN {{ Array(member_status,'String') }}
|
||||
{% end %}
|
||||
|
||||
{% if defined(date_from) %}
|
||||
and date
|
||||
>=
|
||||
|
@ -33,6 +31,14 @@ SQL >
|
|||
{{ Date(date_to, description="Finishing day for filtering a date range", required=False) }}
|
||||
{% else %} and date <= today()
|
||||
{% end %}
|
||||
|
||||
{% if defined(member_status) %} and member_status IN {{ Array(member_status, "'undefined', 'free', 'paid'", description="Member status to filter on", required=False) }} {% end %}
|
||||
{% if defined(device) %} and device = {{ String(device, description="Device to filter on", required=False) }} {% end %}
|
||||
{% if defined(browser) %} and browser = {{ String(browser, description="Browser to filter on", required=False) }} {% end %}
|
||||
{% if defined(source) %} and source = {{ String(source, description="Source to filter on", required=False) }} {% end %}
|
||||
{% if defined(location) %} and location = {{ String(location, description="Location to filter on", required=False) }} {% end %}
|
||||
{% if defined(pathname) %} and pathname = {{ String(pathname, description="Pathname to filter on", required=False) }} {% end %}
|
||||
|
||||
group by device
|
||||
order by visits desc
|
||||
limit {{ Int32(skip, 0) }},{{ Int32(limit, 50) }}
|
||||
|
|
|
@ -16,9 +16,7 @@ SQL >
|
|||
from analytics_pages_mv
|
||||
where
|
||||
site_uuid = {{String(site_uuid, 'mock_site_uuid', description="Tenant ID", required=True)}}
|
||||
{% if defined(member_status) %}
|
||||
and member_status IN {{ Array(member_status,'String') }}
|
||||
{% end %}
|
||||
|
||||
{% if defined(date_from) %}
|
||||
and date
|
||||
>=
|
||||
|
@ -32,6 +30,14 @@ SQL >
|
|||
{{ Date(date_to, description="Finishing day for filtering a date range", required=False) }}
|
||||
{% else %} and date <= today()
|
||||
{% end %}
|
||||
|
||||
{% if defined(member_status) %} and member_status IN {{ Array(member_status, "'undefined', 'free', 'paid'", description="Member status to filter on", required=False) }} {% end %}
|
||||
{% if defined(device) %} and device = {{ String(device, description="Device to filter on", required=False) }} {% end %}
|
||||
{% if defined(browser) %} and browser = {{ String(browser, description="Browser to filter on", required=False) }} {% end %}
|
||||
{% if defined(source) %} and source = {{ String(source, description="Source to filter on", required=False) }} {% end %}
|
||||
{% if defined(location) %} and location = {{ String(location, description="Location to filter on", required=False) }} {% end %}
|
||||
{% if defined(pathname) %} and pathname = {{ String(pathname, description="Pathname to filter on", required=False) }} {% end %}
|
||||
|
||||
group by location
|
||||
order by visits desc
|
||||
limit {{ Int32(skip, 0) }},{{ Int32(limit, 50) }}
|
||||
|
|
|
@ -19,9 +19,7 @@ SQL >
|
|||
from analytics_pages_mv
|
||||
where
|
||||
site_uuid = {{String(site_uuid, 'mock_site_uuid', description="Tenant ID", required=True)}}
|
||||
{% if defined(member_status) %}
|
||||
and member_status IN {{ Array(member_status,'String') }}
|
||||
{% end %}
|
||||
|
||||
{% if defined(date_from) %}
|
||||
and date
|
||||
>=
|
||||
|
@ -36,6 +34,13 @@ SQL >
|
|||
{% else %} and date <= today()
|
||||
{% end %}
|
||||
|
||||
{% if defined(member_status) %} and member_status IN {{ Array(member_status, "'undefined', 'free', 'paid'", description="Member status to filter on", required=False) }} {% end %}
|
||||
{% if defined(device) %} and device = {{ String(device, description="Device to filter on", required=False) }} {% end %}
|
||||
{% if defined(browser) %} and browser = {{ String(browser, description="Browser to filter on", required=False) }} {% end %}
|
||||
{% if defined(source) %} and source = {{ String(source, description="Source to filter on", required=False) }} {% end %}
|
||||
{% if defined(location) %} and location = {{ String(location, description="Location to filter on", required=False) }} {% end %}
|
||||
{% if defined(pathname) %} and pathname = {{ String(pathname, description="Pathname to filter on", required=False) }} {% end %}
|
||||
|
||||
group by pathname
|
||||
order by visits desc
|
||||
limit {{ Int32(skip, 0) }},{{ Int32(limit, 50) }}
|
||||
|
|
|
@ -17,9 +17,7 @@ SQL >
|
|||
from analytics_sources_mv
|
||||
where
|
||||
site_uuid = {{String(site_uuid, 'mock_site_uuid', description="Tenant ID", required=True)}}
|
||||
{% if defined(member_status) %}
|
||||
and member_status IN {{ Array(member_status,'String') }}
|
||||
{% end %}
|
||||
|
||||
{% if defined(date_from) %}
|
||||
and date
|
||||
>=
|
||||
|
@ -33,6 +31,13 @@ SQL >
|
|||
{{ Date(date_to, description="Finishing day for filtering a date range", required=False) }}
|
||||
{% else %} and date <= today()
|
||||
{% end %}
|
||||
|
||||
{% if defined(member_status) %} and member_status IN {{ Array(member_status, "'undefined', 'free', 'paid'", description="Member status to filter on", required=False) }} {% end %}
|
||||
{% if defined(device) %} and device = {{ String(device, description="Device to filter on", required=False) }} {% end %}
|
||||
{% if defined(browser) %} and browser = {{ String(browser, description="Browser to filter on", required=False) }} {% end %}
|
||||
{% if defined(source) %} and source = {{ String(source, description="Source to filter on", required=False) }} {% end %}
|
||||
{% if defined(location) %} and location = {{ String(location, description="Location to filter on", required=False) }} {% end %}
|
||||
{% if defined(pathname) %} and pathname = {{ String(pathname, description="Pathname to filter on", required=False) }} {% end %}
|
||||
group by source
|
||||
order by visits desc
|
||||
limit {{ Int32(skip, 0) }},{{ Int32(limit, 50) }}
|
||||
|
|
Loading…
Add table
Reference in a new issue