Skip to content

Commit

Permalink
Show bounce rate and session length
Browse files Browse the repository at this point in the history
  • Loading branch information
ukutaht committed Jan 6, 2020
1 parent 438a1c3 commit 9802750
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 20 deletions.
4 changes: 2 additions & 2 deletions assets/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,12 @@ blockquote {
@tailwind utilities;

.main-graph {
height: 310px;
height: 440px;
}

@screen md {
.main-graph {
height: 360px;
height: 480px;
}
}

Expand Down
29 changes: 21 additions & 8 deletions assets/js/dashboard/stats/visitor-graph.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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>
}
Expand All @@ -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>
)
})
Expand All @@ -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">
Expand Down
48 changes: 40 additions & 8 deletions lib/plausible/stats/stats.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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,
Expand All @@ -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 ->
Expand Down
11 changes: 10 additions & 1 deletion lib/plausible_web/controllers/api/stats_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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])

Expand Down Expand Up @@ -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


Expand Down
12 changes: 12 additions & 0 deletions test/support/factory.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down

0 comments on commit 9802750

Please sign in to comment.