0
Fork 0
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:
Hannah Wolfe 2024-09-24 15:26:08 +01:00 committed by GitHub
parent 5ebdbe4e25
commit 7e27b1cb36
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 528 additions and 118 deletions

View file

@ -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>

View file

@ -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: [

View file

@ -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>

View file

@ -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>
<span style={{backgroundColor: item.color, display: 'inline-block', width: '10px', height: '10px', marginRight: '5px', borderRadius: '2px'}}></span>
{item.name}
<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>

View file

@ -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 &rarr;</span>
</button>
</div>

View file

@ -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]}

View file

@ -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 &rarr;</span>
</button>
</div>

View file

@ -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]}

View file

@ -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 &rarr;</span>
</button>
</div>

View file

@ -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]}

View file

@ -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>

View file

@ -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}`, {

View file

@ -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]}

View file

@ -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>

View file

@ -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);
}

View file

@ -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;
}

View file

@ -1,49 +1,124 @@
<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
@excludedAudiences={{this.excludedAudiences}}
@onChange={{this.onAudienceChange}}
/>
<div class="view-actions">
<Stats::Parts::AudienceFilter
@excludedAudiences={{this.excludedAudiences}}
@onChange={{this.onAudienceChange}}
/>
<PowerSelect
@selected={{this.selectedRangeOption}}
@options={{this.rangeOptions}}
@searchEnabled={{false}}
@onChange={{this.onRangeChange}}
@triggerComponent={{component "gh-power-select/trigger"}}
@triggerClass="gh-btn"
@dropdownClass="gh-contentfilter-menu-dropdown is-narrow"
@matchTriggerWidth={{false}}
@horizontalPosition="right"
as |option|
>
{{#if option.name}}{{option.name}}{{else}}<span class="red">Unknown option</span>{{/if}}
</PowerSelect>
</div>
<PowerSelect
@selected={{this.selectedRangeOption}}
@options={{this.rangeOptions}}
@searchEnabled={{false}}
@onChange={{this.onRangeChange}}
@triggerComponent={{component "gh-power-select/trigger"}}
@triggerClass="gh-btn"
@dropdownClass="gh-contentfilter-menu-dropdown is-narrow"
@matchTriggerWidth={{false}}
@horizontalPosition="right"
as |option|
>
{{#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>

View file

@ -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;
}

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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) }}

View file

@ -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) }}

View file

@ -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) }}

View file

@ -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) }}

View file

@ -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) }}