+
{ this.renderTopStats() }
diff --git a/lib/plausible/stats/stats.ex b/lib/plausible/stats/stats.ex
index 2981d6567..897e7d3a7 100644
--- a/lib/plausible/stats/stats.ex
+++ b/lib/plausible/stats/stats.ex
@@ -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 ->
diff --git a/lib/plausible_web/controllers/api/stats_controller.ex b/lib/plausible_web/controllers/api/stats_controller.ex
index bb0fba646..e984da9f2 100644
--- a/lib/plausible_web/controllers/api/stats_controller.ex
+++ b/lib/plausible_web/controllers/api/stats_controller.ex
@@ -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
diff --git a/test/plausible_web/controllers/api/stats_controller/main_graph_test.exs b/test/plausible_web/controllers/api/stats_controller/main_graph_test.exs
index de7467f33..546b1ba9a 100644
--- a/test/plausible_web/controllers/api/stats_controller/main_graph_test.exs
+++ b/test/plausible_web/controllers/api/stats_controller/main_graph_test.exs
@@ -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
diff --git a/test/support/factory.ex b/test/support/factory.ex
index b994345b9..2326e3b48 100644
--- a/test/support/factory.ex
+++ b/test/support/factory.ex
@@ -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(),