Merge pull request #22 from plausible-insights/present-session

Show bounce rate and session length
This commit is contained in:
Uku Taht 2020-01-06 15:00:38 +02:00 committed by GitHub
commit 82ca5b7edc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 132 additions and 20 deletions

View File

@ -64,12 +64,12 @@ blockquote {
@tailwind utilities;
.main-graph {
height: 310px;
height: 440px;
}
@screen md {
.main-graph {
height: 360px;
height: 480px;
}
}

View File

@ -123,6 +123,16 @@ function dateFormatter(graphData) {
}
}
function formatStat(stat) {
if (typeof(stat.count) === 'number') {
return numberFormatter(stat.count)
} else if (typeof(stat.duration) === 'number') {
return new Date(stat.duration * 1000).toISOString().substr(14, 5)
} else if (typeof(stat.percentage) === 'number') {
return stat.percentage + '%'
}
}
class LineGraph extends React.Component {
componentDidMount() {
const {graphData} = this.props
@ -244,13 +254,15 @@ class LineGraph extends React.Component {
}
}
renderComparison(comparison) {
renderComparison(name, comparison) {
const formattedComparison = numberFormatter(Math.abs(comparison))
if (comparison > 0) {
return <span className="py-1 text-xs text-grey-darker"><span className="text-green-dark">&uarr;</span> {formattedComparison}% from {this.comparisonTimeframe()}</span>
const color = name === 'Bounce rate' ? 'text-red-light' : 'text-green-dark'
return <span className="py-1 text-xs text-grey-darker"><span className={color}>&uarr;</span> {formattedComparison}% from {this.comparisonTimeframe()}</span>
} else if (comparison < 0) {
return <span className="py-1 text-xs text-grey-darker"><span className="text-red-light">&darr;</span> {formattedComparison}% from {this.comparisonTimeframe()}</span>
const color = name === 'Bounce rate' ? 'text-green-dark' : 'text-red-light'
return <span className="py-1 text-xs text-grey-darker"><span className={color}>&darr;</span> {formattedComparison}% from {this.comparisonTimeframe()}</span>
} else if (comparison === 0) {
return <span className="py-1 text-xs text-grey-darker">&#12336; same as {this.comparisonTimeframe()}</span>
}
@ -259,15 +271,16 @@ class LineGraph extends React.Component {
renderTopStats() {
const {graphData} = this.props
return this.props.graphData.top_stats.map((stat, index) => {
const border = index > 0 ? 'border-l border-grey-light' : ''
let border = index > 0 ? 'lg:border-l border-grey-light' : ''
border = index % 2 === 0 ? border + ' border-r lg:border-r-0' : border
return (
<div className={`pl-8 w-52 ${border}`} key={stat.name}>
<div className={`pl-8 w-1/2 my-4 lg:w-52 ${border}`} key={stat.name}>
<div className="text-grey-dark text-xs font-bold tracking-wide uppercase">{stat.name}</div>
<div className="my-1 flex items-end justify-between">
<b className="text-2xl">{ typeof(stat.count) == 'number' ? numberFormatter(stat.count) : stat.percentage + '%' }</b>
<b className="text-2xl">{formatStat(stat)}</b>
</div>
{this.renderComparison(stat.change)}
{this.renderComparison(stat.name, stat.change)}
</div>
)
})
@ -278,7 +291,7 @@ class LineGraph extends React.Component {
return (
<React.Fragment>
<div className="border-b border-grey-light flex p-4">
<div className="border-b border-grey-light flex flex-wrap">
{ this.renderTopStats() }
</div>
<div className="p-4">

View File

@ -122,6 +122,33 @@ defmodule Plausible.Stats do
{plot, compare_plot, labels, present_index}
end
def bounce_rate(site, query) do
{first_datetime, last_datetime} = date_range_utc_boundaries(query.date_range, site.timezone)
sessions_query = from(s in Plausible.Session,
where: s.hostname == ^site.domain,
where: s.new_visitor,
where: s.start >= ^first_datetime and s.start < ^last_datetime
)
total_sessions = Repo.one( from s in sessions_query, select: count(s))
bounced_sessions = Repo.one(from s in sessions_query, where: s.is_bounce, select: count(s))
case total_sessions do
0 -> 0
total -> round(bounced_sessions / total * 100)
end
end
def session_length(site, query) do
{first_datetime, last_datetime} = date_range_utc_boundaries(query.date_range, site.timezone)
Repo.one(from s in Plausible.Session,
where: s.hostname == ^site.domain,
where: s.start >= ^first_datetime and s.start < ^last_datetime,
select: coalesce(avg(s.length), 0)
) |> Decimal.round |> Decimal.to_integer
end
def pageviews_and_visitors(site, query) do
Repo.one(from(
e in base_query(site, query),
@ -296,14 +323,7 @@ defmodule Plausible.Stats do
end
defp base_query(site, query, events \\ ["pageview"]) do
{:ok, first} = NaiveDateTime.new(query.date_range.first, ~T[00:00:00])
first_datetime = Timex.to_datetime(first, site.timezone)
|> Timex.Timezone.convert("UTC")
{:ok, last} = NaiveDateTime.new(query.date_range.last |> Timex.shift(days: 1), ~T[00:00:00])
last_datetime = Timex.to_datetime(last, site.timezone)
|> Timex.Timezone.convert("UTC")
{first_datetime, last_datetime} = date_range_utc_boundaries(query.date_range, site.timezone)
{goal_event, path} = event_name_for_goal(query)
q = from(e in Plausible.Event,
@ -324,6 +344,18 @@ defmodule Plausible.Stats do
end
end
defp date_range_utc_boundaries(date_range, timezone) do
{:ok, first} = NaiveDateTime.new(date_range.first, ~T[00:00:00])
first_datetime = Timex.to_datetime(first, timezone)
|> Timex.Timezone.convert("UTC")
{:ok, last} = NaiveDateTime.new(date_range.last |> Timex.shift(days: 1), ~T[00:00:00])
last_datetime = Timex.to_datetime(last, timezone)
|> Timex.Timezone.convert("UTC")
{first_datetime, last_datetime}
end
defp event_name_for_goal(query) do
case query.filters["goal"] do
"Visit " <> page ->

View File

@ -40,12 +40,21 @@ defmodule PlausibleWeb.Api.StatsController do
end
defp fetch_top_stats(site, query) do
prev_query = Query.shift_back(query)
{pageviews, visitors} = Stats.pageviews_and_visitors(site, query)
{prev_pageviews, prev_visitors} = Stats.pageviews_and_visitors(site, Query.shift_back(query))
{prev_pageviews, prev_visitors} = Stats.pageviews_and_visitors(site, prev_query)
bounce_rate = Stats.bounce_rate(site, query)
prev_bounce_rate = Stats.bounce_rate(site, prev_query)
change_bounce_rate = if prev_bounce_rate > 0, do: bounce_rate - prev_bounce_rate
session_length = Stats.session_length(site, query)
prev_session_length = Stats.session_length(site, prev_query)
change_session_length = if prev_session_length > 0, do: session_length - prev_session_length
[
%{name: "Unique visitors", count: visitors, change: percent_change(prev_visitors, visitors)},
%{name: "Total pageviews", count: pageviews, change: percent_change(prev_pageviews, pageviews)},
%{name: "Bounce rate", percentage: bounce_rate, change: change_bounce_rate},
%{name: "Session length", duration: session_length, change: change_session_length},
]
end

View File

@ -87,7 +87,7 @@ defmodule PlausibleWeb.Api.StatsController.MainGraphTest do
describe "GET /api/stats/main-graph - top stats" do
setup [:create_user, :log_in, :create_site]
test "counts distinct user ids", %{conn: conn, site: site} do
test "unique users counts distinct user ids", %{conn: conn, site: site} do
insert(:pageview, hostname: site.domain, user_id: @user_id, timestamp: ~N[2019-01-01 00:00:00])
insert(:pageview, hostname: site.domain, user_id: @user_id, timestamp: ~N[2019-01-01 23:59:00])
@ -128,6 +128,52 @@ defmodule PlausibleWeb.Api.StatsController.MainGraphTest do
res = json_response(conn, 200)
assert %{"name" => "Total pageviews", "count" => 1, "change" => -50} in res["top_stats"]
end
test "calculates bounce rate", %{conn: conn, site: site} do
insert(:session, hostname: site.domain, is_bounce: true, start: ~N[2019-01-01 01:00:00])
insert(:session, hostname: site.domain, is_bounce: false, start: ~N[2019-01-01 02:00:00])
conn = get(conn, "/api/stats/#{site.domain}/main-graph?period=day&date=2019-01-01")
res = json_response(conn, 200)
assert %{"name" => "Bounce rate", "percentage" => 50, "change" => nil} in res["top_stats"]
end
test "calculates change in bounce rate", %{conn: conn, site: site} do
insert(:session, hostname: site.domain, is_bounce: true, start: ~N[2019-01-01 01:00:00])
insert(:session, hostname: site.domain, is_bounce: false, start: ~N[2019-01-01 02:00:00])
insert(:session, hostname: site.domain, is_bounce: true, start: ~N[2019-01-02 01:00:00])
insert(:session, hostname: site.domain, is_bounce: true, start: ~N[2019-01-02 01:00:00])
insert(:session, hostname: site.domain, is_bounce: false, start: ~N[2019-01-02 02:00:00])
conn = get(conn, "/api/stats/#{site.domain}/main-graph?period=day&date=2019-01-02")
res = json_response(conn, 200)
assert %{"name" => "Bounce rate", "percentage" => 67, "change" => 17} in res["top_stats"]
end
test "calculates avg session length", %{conn: conn, site: site} do
insert(:session, hostname: site.domain, length: 10, start: ~N[2019-01-01 01:00:00])
insert(:session, hostname: site.domain, length: 20, start: ~N[2019-01-01 02:00:00])
conn = get(conn, "/api/stats/#{site.domain}/main-graph?period=day&date=2019-01-01")
res = json_response(conn, 200)
assert %{"name" => "Session length", "duration" => 15, "change" => nil} in res["top_stats"]
end
test "calculates change in session length", %{conn: conn, site: site} do
insert(:session, hostname: site.domain, length: 10, start: ~N[2019-01-01 01:00:00])
insert(:session, hostname: site.domain, length: 20, start: ~N[2019-01-01 02:00:00])
insert(:session, hostname: site.domain, length: 20, start: ~N[2019-01-02 02:00:00])
conn = get(conn, "/api/stats/#{site.domain}/main-graph?period=day&date=2019-01-02")
res = json_response(conn, 200)
assert %{"name" => "Session length", "duration" => 20, "change" => 5} in res["top_stats"]
end
end

View File

@ -22,6 +22,18 @@ defmodule Plausible.Factory do
}
end
def session_factory do
hostname = sequence(:domain, &"example-#{&1}.com")
%Plausible.Session{
hostname: hostname,
new_visitor: true,
user_id: UUID.uuid4(),
start: Timex.now(),
is_bounce: false
}
end
def pageview_factory do
struct!(
event_factory(),