mirror of
https://github.com/plausible/analytics.git
synced 2024-11-23 20:13:31 +03:00
Merge pull request #22 from plausible-insights/present-session
Show bounce rate and session length
This commit is contained in:
commit
82ca5b7edc
@ -64,12 +64,12 @@ blockquote {
|
||||
@tailwind utilities;
|
||||
|
||||
.main-graph {
|
||||
height: 310px;
|
||||
height: 440px;
|
||||
}
|
||||
|
||||
@screen md {
|
||||
.main-graph {
|
||||
height: 360px;
|
||||
height: 480px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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">↑</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}>↑</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">↓</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}>↓</span> {formattedComparison}% from {this.comparisonTimeframe()}</span>
|
||||
} else if (comparison === 0) {
|
||||
return <span className="py-1 text-xs text-grey-darker">〰 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">
|
||||
|
@ -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 ->
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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(),
|
||||
|
Loading…
Reference in New Issue
Block a user