mirror of
https://github.com/plausible/analytics.git
synced 2024-12-24 01:54:34 +03:00
Improve event metadata (#385)
* Change from dropdown to tabs for metadata breakdown * Add meta filter to base_query * Refactor: use base_query_w_sessions in top_referrers_for_goal * Do not allow individual metadata filter * Fix conversions report when combining 3 filters: goal, metadata, country * Remember selected metadata key * Compress conversions component
This commit is contained in:
parent
40900c7653
commit
ff515c641d
@ -48,7 +48,9 @@ function filterText(key, value, query) {
|
|||||||
|
|
||||||
function renderFilter(history, [key, value], query) {
|
function renderFilter(history, [key, value], query) {
|
||||||
function removeFilter() {
|
function removeFilter() {
|
||||||
history.push({search: removeQueryParam(location.search, key)})
|
let newQuery = removeQueryParam(location.search, key)
|
||||||
|
if (key === 'goal') { newQuery = removeQueryParam(newQuery, 'meta') }
|
||||||
|
history.push({search: newQuery})
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -50,13 +50,13 @@ export default class Conversions extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<div className="my-2 text-sm" key={goal.name}>
|
<div className="my-2 text-sm" key={goal.name}>
|
||||||
<div className="flex items-center justify-between my-2">
|
<div className="flex items-center justify-between my-2">
|
||||||
<div className="w-full h-8 relative" style={{maxWidth: 'calc(100% - 14rem)'}}>
|
<div className="w-full h-8 relative" style={{maxWidth: 'calc(100% - 10rem)'}}>
|
||||||
<Bar count={goal.count} all={this.state.goals} bg="bg-red-50" />
|
<Bar count={goal.count} all={this.state.goals} bg="bg-red-50" />
|
||||||
{this.renderGoalText(goal.name)}
|
{this.renderGoalText(goal.name)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium inline-block w-20 text-right">{numberFormatter(goal.count)}</span>
|
<span className="font-medium inline-block w-20 text-right">{numberFormatter(goal.count)}</span>
|
||||||
<span className="font-medium inline-block w-36 text-right">{numberFormatter(goal.total_count)}</span>
|
<span className="font-medium inline-block w-20 text-right">{numberFormatter(goal.total_count)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{ renderMeta && <MetaBreakdown site={this.props.site} query={this.props.query} goal={goal} /> }
|
{ renderMeta && <MetaBreakdown site={this.props.site} query={this.props.query} goal={goal} /> }
|
||||||
@ -79,7 +79,7 @@ export default class Conversions extends React.Component {
|
|||||||
<span>Goal</span>
|
<span>Goal</span>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<span className="inline-block w-20">Uniques</span>
|
<span className="inline-block w-20">Uniques</span>
|
||||||
<span className="inline-block w-36">Total conversions</span>
|
<span className="inline-block w-20">Total</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
import Transition from "../../../transition.js";
|
|
||||||
import Bar from '../bar'
|
import Bar from '../bar'
|
||||||
import numberFormatter from '../../number-formatter'
|
import numberFormatter from '../../number-formatter'
|
||||||
import * as api from '../../api'
|
import * as api from '../../api'
|
||||||
@ -9,31 +8,24 @@ import * as api from '../../api'
|
|||||||
export default class MetaBreakdown extends React.Component {
|
export default class MetaBreakdown extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
this.handleClick = this.handleClick.bind(this)
|
let metaKey = props.goal.meta_keys[0]
|
||||||
const metaFilter = props.query.filters['meta']
|
this.storageKey = 'goalMetaTab__' + props.site.domain + props.goal.name
|
||||||
console.log(metaFilter)
|
const storedKey = window.localStorage[this.storageKey]
|
||||||
const metaKey = metaFilter ? Object.keys(metaFilter)[0] : props.goal.meta_keys[0]
|
if (props.goal.meta_keys.includes(storedKey)) {
|
||||||
|
metaKey = storedKey
|
||||||
|
}
|
||||||
|
if (props.query.filters['meta']) {
|
||||||
|
metaKey = Object.keys(props.query.filters['meta'])[0]
|
||||||
|
}
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
loading: true,
|
loading: true,
|
||||||
dropdownOpen: false,
|
|
||||||
metaKey: metaKey
|
metaKey: metaKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.fetchMetaBreakdown()
|
this.fetchMetaBreakdown()
|
||||||
document.addEventListener('mousedown', this.handleClick, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
document.removeEventListener('mousedown', this.handleClick, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleClick(e) {
|
|
||||||
if (this.dropDownNode && this.dropDownNode.contains(e.target)) return;
|
|
||||||
if (!this.state.dropdownOpen) return;
|
|
||||||
|
|
||||||
this.setState({dropdownOpen: false})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchMetaBreakdown() {
|
fetchMetaBreakdown() {
|
||||||
@ -49,7 +41,7 @@ export default class MetaBreakdown extends React.Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between my-2" key={value.name}>
|
<div className="flex items-center justify-between my-2" key={value.name}>
|
||||||
<div className="w-full h-8 relative" style={{maxWidth: 'calc(100% - 14rem)'}}>
|
<div className="w-full h-8 relative" style={{maxWidth: 'calc(100% - 10rem)'}}>
|
||||||
<Bar count={value.count} all={this.state.breakdown} bg="bg-red-50" />
|
<Bar count={value.count} all={this.state.breakdown} bg="bg-red-50" />
|
||||||
<Link to={{search: query.toString()}} style={{marginTop: '-26px'}} className="hover:underline block px-2">
|
<Link to={{search: query.toString()}} style={{marginTop: '-26px'}} className="hover:underline block px-2">
|
||||||
{ value.name }
|
{ value.name }
|
||||||
@ -57,36 +49,15 @@ export default class MetaBreakdown extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium inline-block w-20 text-right">{numberFormatter(value.count)}</span>
|
<span className="font-medium inline-block w-20 text-right">{numberFormatter(value.count)}</span>
|
||||||
<span className="font-medium inline-block w-36 text-right">{numberFormatter(value.total_count)}</span>
|
<span className="font-medium inline-block w-20 text-right">{numberFormatter(value.total_count)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
changeMetaKey(newKey) {
|
changeMetaKey(newKey) {
|
||||||
this.setState({metaKey: newKey, loading: true, dropdownOpen: false}, this.fetchMetaBreakdown)
|
window.localStorage[this.storageKey] = newKey
|
||||||
}
|
this.setState({metaKey: newKey, loading: true}, this.fetchMetaBreakdown)
|
||||||
|
|
||||||
renderMetaKeyOption(key) {
|
|
||||||
const extraClass = key === this.state.metaKey ? 'font-medium text-gray-900' : 'hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900'
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span onClick={this.changeMetaKey.bind(this, key)} key={key} className={`cursor-pointer block truncate px-4 py-2 text-sm leading-5 text-gray-700 ${extraClass}`}>
|
|
||||||
{key}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
renderDropdown() {
|
|
||||||
return (
|
|
||||||
<div className="py-1">
|
|
||||||
{ this.props.goal.meta_keys.map(this.renderMetaKeyOption.bind(this)) }
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleDropdown() {
|
|
||||||
this.setState({dropdownOpen: !this.state.dropdownOpen})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderBody() {
|
renderBody() {
|
||||||
@ -97,32 +68,24 @@ export default class MetaBreakdown extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderPill(key) {
|
||||||
|
const isActive = this.state.metaKey === key
|
||||||
|
|
||||||
|
if (isActive) {
|
||||||
|
return <li key={key} className="inline-block h-5 text-indigo-700 font-bold border-b-2 border-indigo-700">{key}</li>
|
||||||
|
} else {
|
||||||
|
return <li key={key} className="hover:text-indigo-700 cursor-pointer" onClick={this.changeMetaKey.bind(this, key)}>{key}</li>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="w-full pl-6 mt-4">
|
<div className="w-full pl-6 mt-4">
|
||||||
<div className="relative">
|
<div className="flex items-center pb-1">
|
||||||
Breakdown by
|
<span className="text-xs font-bold text-gray-600">Breakdown by:</span>
|
||||||
<button onClick={this.toggleDropdown.bind(this)} className="ml-1 inline-flex items-center rounded-md leading-5 font-bold text-gray-700 focus:outline-none transition ease-in-out duration-150 hover:text-gray-500 focus:border-blue-300 focus:shadow-outline-blue">
|
<ul className="flex font-medium text-xs text-gray-500 space-x-2 leading-5 pl-1">
|
||||||
{ this.state.metaKey }
|
{ this.props.goal.meta_keys.map(this.renderPill.bind(this)) }
|
||||||
<svg className="mt-px h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
</ul>
|
||||||
<path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<Transition
|
|
||||||
show={this.state.dropdownOpen}
|
|
||||||
enter="transition ease-out duration-100 transform"
|
|
||||||
enterFrom="opacity-0 scale-95"
|
|
||||||
enterTo="opacity-100 scale-100"
|
|
||||||
leave="transition ease-in duration-75 transform"
|
|
||||||
leaveFrom="opacity-100 scale-100"
|
|
||||||
leaveTo="opacity-0 scale-95"
|
|
||||||
>
|
|
||||||
<div className="z-10 origin-top-left absolute left-0 mt-2 w-64 rounded-md shadow-lg" ref={node => this.dropDownNode = node} >
|
|
||||||
<div className="rounded-md bg-white shadow-xs">
|
|
||||||
{ this.renderDropdown() }
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
</div>
|
||||||
{ this.renderBody() }
|
{ this.renderBody() }
|
||||||
</div>
|
</div>
|
||||||
|
@ -291,10 +291,9 @@ export default class Devices extends React.Component {
|
|||||||
|
|
||||||
renderPill(name, mode) {
|
renderPill(name, mode) {
|
||||||
const isActive = this.state.mode === mode
|
const isActive = this.state.mode === mode
|
||||||
const extraClass = name === 'OS' ? '' : ' border-r border-gray-300'
|
|
||||||
|
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
return <li className="inline-block h-5 text-indigo-700 font-bold border-b-2 border-indigo-700" onClick={this.setMode(mode)}>{name}</li>
|
return <li className="inline-block h-5 text-indigo-700 font-bold border-b-2 border-indigo-700">{name}</li>
|
||||||
} else {
|
} else {
|
||||||
return <li className="hover:text-indigo-700 cursor-pointer" onClick={this.setMode(mode)}>{name}</li>
|
return <li className="hover:text-indigo-700 cursor-pointer" onClick={this.setMode(mode)}>{name}</li>
|
||||||
}
|
}
|
||||||
|
@ -171,14 +171,8 @@ defmodule Plausible.Stats.Clickhouse do
|
|||||||
def top_referrers_for_goal(site, query, limit, page) do
|
def top_referrers_for_goal(site, query, limit, page) do
|
||||||
offset = (page - 1) * limit
|
offset = (page - 1) * limit
|
||||||
|
|
||||||
converted_sessions =
|
|
||||||
from(e in base_query(site, query),
|
|
||||||
select: %{session_id: e.session_id})
|
|
||||||
|
|
||||||
ClickhouseRepo.all(
|
ClickhouseRepo.all(
|
||||||
from s in Plausible.ClickhouseSession,
|
from s in base_query_w_sessions(site, query),
|
||||||
join: cs in subquery(converted_sessions),
|
|
||||||
on: s.session_id == cs.session_id,
|
|
||||||
where: s.referrer_source != "",
|
where: s.referrer_source != "",
|
||||||
group_by: s.referrer_source,
|
group_by: s.referrer_source,
|
||||||
order_by: [desc: fragment("count")],
|
order_by: [desc: fragment("count")],
|
||||||
@ -611,7 +605,7 @@ defmodule Plausible.Stats.Clickhouse do
|
|||||||
%{goal => [key]}
|
%{goal => [key]}
|
||||||
else
|
else
|
||||||
ClickhouseRepo.all(
|
ClickhouseRepo.all(
|
||||||
from [e, meta] in base_query_w_sessions_bare(site, query),
|
from [e, meta: meta] in base_query_w_sessions_bare(site, query),
|
||||||
select: {e.name, meta.key},
|
select: {e.name, meta.key},
|
||||||
distinct: true
|
distinct: true
|
||||||
) |> Enum.reduce(%{}, fn {goal_name, meta_key}, acc ->
|
) |> Enum.reduce(%{}, fn {goal_name, meta_key}, acc ->
|
||||||
@ -647,7 +641,7 @@ defmodule Plausible.Stats.Clickhouse do
|
|||||||
)
|
)
|
||||||
else
|
else
|
||||||
ClickhouseRepo.all(
|
ClickhouseRepo.all(
|
||||||
from [e, meta] in base_query_w_sessions(site, query),
|
from [e, meta: meta] in base_query_w_sessions(site, query),
|
||||||
group_by: meta.value,
|
group_by: meta.value,
|
||||||
order_by: [desc: fragment("count")],
|
order_by: [desc: fragment("count")],
|
||||||
select: %{
|
select: %{
|
||||||
@ -857,7 +851,7 @@ defmodule Plausible.Stats.Clickhouse do
|
|||||||
q
|
q
|
||||||
end
|
end
|
||||||
|
|
||||||
if query.filters["page"] do
|
q = if query.filters["page"] do
|
||||||
page = query.filters["page"]
|
page = query.filters["page"]
|
||||||
from(e in q, where: e.pathname == ^page)
|
from(e in q, where: e.pathname == ^page)
|
||||||
else
|
else
|
||||||
@ -876,6 +870,7 @@ defmodule Plausible.Stats.Clickhouse do
|
|||||||
from(
|
from(
|
||||||
e in q,
|
e in q,
|
||||||
inner_lateral_join: meta in fragment("meta as m"),
|
inner_lateral_join: meta in fragment("meta as m"),
|
||||||
|
as: :meta,
|
||||||
where: meta.key == ^key and meta.value == ^val,
|
where: meta.key == ^key and meta.value == ^val,
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@ -1075,6 +1070,26 @@ defmodule Plausible.Stats.Clickhouse do
|
|||||||
q
|
q
|
||||||
end
|
end
|
||||||
|
|
||||||
|
q = if query.filters["meta"] do
|
||||||
|
[{key, val}] = query.filters["meta"] |> Enum.into([])
|
||||||
|
|
||||||
|
if val == "(none)" do
|
||||||
|
from(
|
||||||
|
e in q,
|
||||||
|
where: fragment("not has(meta.key, ?)", ^key)
|
||||||
|
)
|
||||||
|
else
|
||||||
|
from(
|
||||||
|
e in q,
|
||||||
|
inner_lateral_join: meta in fragment("meta as m"),
|
||||||
|
where: meta.key == ^key and meta.value == ^val,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
q
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
q =
|
q =
|
||||||
if path do
|
if path do
|
||||||
from(e in q, where: e.pathname == ^path)
|
from(e in q, where: e.pathname == ^path)
|
||||||
|
Loading…
Reference in New Issue
Block a user