From c540c70e87e46f85c8ec0da1d99b91dd23fd9cab Mon Sep 17 00:00:00 2001 From: Uku Taht Date: Fri, 22 Nov 2019 16:37:44 +0800 Subject: [PATCH 1/6] Show conversion rate when filtering for goal --- assets/js/dashboard/stats/visitor-graph.js | 18 ++++++++++++++-- lib/plausible/stats/stats.ex | 10 +++++++++ .../controllers/api/stats_controller.ex | 4 +++- .../api/stats_controller/main_graph_test.exs | 21 ++++++++++++++++--- 4 files changed, 47 insertions(+), 6 deletions(-) diff --git a/assets/js/dashboard/stats/visitor-graph.js b/assets/js/dashboard/stats/visitor-graph.js index da230988f..065a96dcd 100644 --- a/assets/js/dashboard/stats/visitor-graph.js +++ b/assets/js/dashboard/stats/visitor-graph.js @@ -197,6 +197,19 @@ class LineGraph extends React.Component { } } + renderConversionRate() { + if (typeof(this.props.graphData.conversion_rate) === "number") { + return ( +
+
CONVERSION RATE
+
+ {this.props.graphData.conversion_rate}% +
+
+ ) + } + } + render() { const {graphData} = this.props const extraClass = graphData.interval === 'hour' ? '' : 'cursor-pointer' @@ -204,20 +217,21 @@ class LineGraph extends React.Component { return (
-
+
UNIQUE VISITORS
{numberFormatter(graphData.unique_visitors)}
{this.renderComparison(graphData.change_visitors)}
-
+
TOTAL {eventName(this.props.query)}
{numberFormatter(graphData.pageviews)}
{this.renderComparison(graphData.change_pageviews)}
+ { this.renderConversionRate() }
diff --git a/lib/plausible/stats/stats.ex b/lib/plausible/stats/stats.ex index 83b4ba513..6b9e30497 100644 --- a/lib/plausible/stats/stats.ex +++ b/lib/plausible/stats/stats.ex @@ -95,6 +95,16 @@ defmodule Plausible.Stats do )) end + def conversion_rate(site, query) do + {_, total_visitors} = pageviews_and_visitors(site, %{ query | filters: %{} }) + {_, converted_visitors} = pageviews_and_visitors(site, query) + if total_visitors > 0 do + Float.round(converted_visitors / total_visitors * 100, 1) + else + 0.0 + end + end + def top_referrers(site, query, limit \\ 5) do Repo.all(from e in base_query(site, query), select: %{name: e.referrer_source, count: count(e.user_id, :distinct)}, diff --git a/lib/plausible_web/controllers/api/stats_controller.ex b/lib/plausible_web/controllers/api/stats_controller.ex index 95312470e..e98bca76f 100644 --- a/lib/plausible_web/controllers/api/stats_controller.ex +++ b/lib/plausible_web/controllers/api/stats_controller.ex @@ -11,6 +11,7 @@ defmodule PlausibleWeb.Api.StatsController do plot_task = Task.async(fn -> Stats.calculate_plot(site, query) end) {pageviews, visitors} = Stats.pageviews_and_visitors(site, query) {change_pageviews, change_visitors} = Stats.compare_pageviews_and_visitors(site, query, {pageviews, visitors}) + conversion_rate = if query.filters["goal"], do: Stats.conversion_rate(site, query) {plot, labels, present_index} = Task.await(plot_task) json(conn, %{ @@ -21,7 +22,8 @@ defmodule PlausibleWeb.Api.StatsController do unique_visitors: visitors, change_pageviews: change_pageviews, change_visitors: change_visitors, - interval: query.step_type + conversion_rate: conversion_rate, + interval: query.step_type, }) 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 0a7693262..a517cc4c7 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 @@ -6,7 +6,7 @@ defmodule PlausibleWeb.Api.StatsController.MainGraphTest do describe "GET /api/stats/main-graph - plot" do setup [:create_user, :log_in, :create_site] - test "displays pageviews for a day", %{conn: conn, site: site} do + test "displays visitors for a day", %{conn: conn, site: site} do insert(:pageview, hostname: site.domain, timestamp: ~N[2019-01-01 00:00:00]) insert(:pageview, hostname: site.domain, timestamp: ~N[2019-01-01 23:59:00]) @@ -33,7 +33,7 @@ defmodule PlausibleWeb.Api.StatsController.MainGraphTest do assert plot == [0, 1] ++ zeroes # Expecting pageview to show at 1am CET end - test "displays pageviews for a month", %{conn: conn, site: site} do + test "displays visitors for a month", %{conn: conn, site: site} do insert(:pageview, hostname: site.domain, timestamp: ~N[2019-01-01 12:00:00]) insert(:pageview, hostname: site.domain, timestamp: ~N[2019-01-31 12:00:00]) @@ -46,7 +46,7 @@ defmodule PlausibleWeb.Api.StatsController.MainGraphTest do assert List.last(plot) == 1 end - test "displays pageviews for 3 months", %{conn: conn, site: site} do + test "displays visitors for 3 months", %{conn: conn, site: site} do insert(:pageview, hostname: site.domain) insert(:pageview, hostname: site.domain, timestamp: months_ago(2)) @@ -155,6 +155,21 @@ defmodule PlausibleWeb.Api.StatsController.MainGraphTest do end end + describe "GET /api/stats/main-graph - conversion rate" do + setup [:create_user, :log_in, :create_site] + + test "returns conversion rate when filtering for a goal", %{conn: conn, site: site} do + insert(:pageview, hostname: site.domain, timestamp: ~N[2019-01-01 02:00:00]) + insert(:pageview, hostname: site.domain, user_id: @user_id, timestamp: ~N[2019-01-01 01:00:00]) + insert(:event, name: "Signup", hostname: site.domain, user_id: @user_id, timestamp: ~N[2019-01-01 02:00:00]) + + filters = Jason.encode!(%{goal: "Signup"}) + conn = get(conn, "/api/stats/#{site.domain}/main-graph?period=day&date=2019-01-01&filters=#{filters}") + + assert %{"conversion_rate" => 50} = json_response(conn, 200) + end + end + defp months_ago(months) do Timex.now() |> Timex.shift(months: -months) end From 3b533fae216ef04d128c7551beb907b3eed919e9 Mon Sep 17 00:00:00 2001 From: Uku Taht Date: Mon, 25 Nov 2019 11:37:50 +0800 Subject: [PATCH 2/6] Compare graph when filtering for goal --- assets/js/dashboard/stats/visitor-graph.js | 99 +++++++++++++++---- lib/plausible/stats/stats.ex | 32 +++++- .../controllers/api/stats_controller.ex | 3 +- 3 files changed, 108 insertions(+), 26 deletions(-) diff --git a/assets/js/dashboard/stats/visitor-graph.js b/assets/js/dashboard/stats/visitor-graph.js index 065a96dcd..fd020cfa1 100644 --- a/assets/js/dashboard/stats/visitor-graph.js +++ b/assets/js/dashboard/stats/visitor-graph.js @@ -6,21 +6,21 @@ import numberFormatter from '../number-formatter' import { isToday, shiftMonths, formatMonth } from '../date' import * as api from '../api' -function dataSets(graphData, ctx) { +function mainSet(plot, present_index, ctx) { var gradient = ctx.createLinearGradient(0, 0, 0, 300); gradient.addColorStop(0, 'rgba(101,116,205, 0.2)'); gradient.addColorStop(1, 'rgba(101,116,205, 0)'); - if (graphData.present_index) { - var dashedPart = graphData.plot.slice(graphData.present_index - 1); - var dashedPlot = (new Array(graphData.plot.length - dashedPart.length)).concat(dashedPart) - for(var i = graphData.present_index; i < graphData.plot.length; i++) { - graphData.plot[i] = undefined + if (present_index) { + var dashedPart = plot.slice(present_index - 1); + var dashedPlot = (new Array(plot.length - dashedPart.length)).concat(dashedPart) + for(var i = present_index; i < plot.length; i++) { + plot[i] = undefined } return [{ label: 'Visitors', - data: graphData.plot, + data: plot, borderWidth: 3, borderColor: 'rgba(101,116,205)', pointBackgroundColor: 'rgba(101,116,205)', @@ -38,7 +38,7 @@ function dataSets(graphData, ctx) { } else { return [{ label: 'Visitors', - data: graphData.plot, + data: plot, borderWidth: 3, borderColor: 'rgba(101,116,205)', pointBackgroundColor: 'rgba(101,116,205)', @@ -47,6 +47,57 @@ function dataSets(graphData, ctx) { } } +function compareSet(plot, present_index, ctx) { + var gradient = ctx.createLinearGradient(0, 0, 0, 300); + gradient.addColorStop(0, 'rgba(255, 68, 87, .2)'); + gradient.addColorStop(1, 'rgba(255, 68, 87, 0)'); + + if (present_index) { + var dashedPart = plot.slice(present_index - 1); + var dashedPlot = (new Array(plot.length - dashedPart.length)).concat(dashedPart) + for(var i = present_index; i < plot.length; i++) { + plot[i] = undefined + } + + return [{ + label: 'Conversions', + data: plot, + borderWidth: 3, + borderColor: 'rgba(255, 68, 87, 1)', + pointBackgroundColor: 'rgba(255, 68, 87, 1)', + backgroundColor: gradient, + }, + { + label: 'Conversions', + data: dashedPlot, + borderWidth: 3, + borderDash: [5, 10], + borderColor: 'rgba(255, 68, 87, 1)', + pointBackgroundColor: 'rgba(255, 68, 87, 1)', + backgroundColor: gradient, + }] + } else { + return [{ + label: 'Conversions', + data: plot, + borderWidth: 3, + borderColor: 'rgba(101,116,205)', + pointBackgroundColor: 'rgba(101,116,205)', + backgroundColor: gradient, + }] + } +} + +function dataSets(graphData, ctx) { + const dataSets = mainSet(graphData.plot, graphData.present_index, ctx) + + if (graphData.compare_plot) { + return dataSets.concat(compareSet(graphData.compare_plot, graphData.present_index, ctx)) + } else { + return dataSets + } +} + const MONTHS = [ "January", "February", "March", "April", "May", "June", "July", @@ -87,31 +138,39 @@ class LineGraph extends React.Component { animation: false, legend: {display: false}, responsive: true, - elements: {line: {tension: 0.1}, point: {radius: 0}}, + elements: {line: {tension: 0}, point: {radius: 0}}, onClick: this.onClick.bind(this), tooltips: { mode: 'index', intersect: false, xPadding: 10, yPadding: 10, - titleFontSize: 16, + titleFontSize: 18, footerFontSize: 14, - footerFontColor: '#e6e8ff', + bodyFontSize: 14, backgroundColor: 'rgba(25, 30, 56)', + titleMarginBottom: 8, + bodySpacing: 6, + footerMarginTop: 8, + xPadding: 16, + yPadding: 12, + multiKeyBackground: 'none', callbacks: { title: function(dataPoints) { const data = dataPoints[0] - const formatDate = dateFormatter(graphData) - if (graphData.interval === 'month') { - return data.yLabel.toLocaleString() + ' visitors in ' + formatDate(data.xLabel) - } else if (graphData.interval === 'date') { - return data.yLabel.toLocaleString() + ' visitors on ' + formatDate(data.xLabel) - } else if (graphData.interval === 'hour') { - return data.yLabel.toLocaleString() + ' visitors at ' + formatDate(data.xLabel) + return dateFormatter(graphData)(data.xLabel) + }, + beforeBody: function() { + this.drawnLabels = {} + }, + label: function(item) { + const dataset = this._data.datasets[item.datasetIndex] + if (!this.drawnLabels[dataset.label]) { + this.drawnLabels[dataset.label] = true + return ` ${item.yLabel} ${dataset.label}` } }, - label: function() {}, - afterBody: function(dataPoints) { + footer: function(dataPoints) { if (graphData.interval === 'month') { return 'Click to view month' } else if (graphData.interval === 'date') { diff --git a/lib/plausible/stats/stats.ex b/lib/plausible/stats/stats.ex index 6b9e30497..fe7da7588 100644 --- a/lib/plausible/stats/stats.ex +++ b/lib/plausible/stats/stats.ex @@ -29,37 +29,59 @@ defmodule Plausible.Stats do end) groups = Repo.all( - from e in base_query(site, query), + from e in base_query(site, %{query | filters: %{}}), group_by: 1, order_by: 1, select: {fragment("date_trunc('month', ? at time zone 'utc' at time zone ?)", e.timestamp, ^site.timezone), count(e.user_id, :distinct)} ) |> Enum.into(%{}) |> transform_keys(fn dt -> NaiveDateTime.to_date(dt) end) + compare_groups = if query.filters["goal"] do + Repo.all( + from e in base_query(site, query), + group_by: 1, + order_by: 1, + select: {fragment("date_trunc('month', ? at time zone 'utc' at time zone ?)", e.timestamp, ^site.timezone), count(e.user_id, :distinct)} + ) |> Enum.into(%{}) + |> transform_keys(fn dt -> NaiveDateTime.to_date(dt) end) + end + present_index = Enum.find_index(steps, fn step -> step == Timex.now(site.timezone) |> Timex.to_date |> Timex.beginning_of_month end) plot = Enum.map(steps, fn step -> groups[step] || 0 end) + compare_plot = compare_groups && Enum.map(steps, fn step -> compare_groups[step] || 0 end) labels = Enum.map(steps, fn step -> Timex.format!(step, "{ISOdate}") end) - {plot, labels, present_index} + {plot, compare_plot, labels, present_index} end def calculate_plot(site, %Query{step_type: "date"} = query) do steps = Enum.into(query.date_range, []) groups = Repo.all( - from e in base_query(site, query), + from e in base_query(site, %{ query | filters: %{} }), group_by: 1, order_by: 1, select: {fragment("date_trunc('day', ? at time zone 'utc' at time zone ?)", e.timestamp, ^site.timezone), count(e.user_id, :distinct)} ) |> Enum.into(%{}) |> transform_keys(fn dt -> NaiveDateTime.to_date(dt) end) + compare_groups = if query.filters["goal"] do + Repo.all( + from e in base_query(site, query), + group_by: 1, + order_by: 1, + select: {fragment("date_trunc('day', ? at time zone 'utc' at time zone ?)", e.timestamp, ^site.timezone), count(e.user_id, :distinct)} + ) |> Enum.into(%{}) + |> transform_keys(fn dt -> NaiveDateTime.to_date(dt) end) + end + present_index = Enum.find_index(steps, fn step -> step == Timex.now(site.timezone) |> Timex.to_date end) steps_to_show = if present_index, do: present_index + 1, else: Enum.count(steps) plot = Enum.map(steps, fn step -> groups[step] || 0 end) |> Enum.take(steps_to_show) + compare_plot = compare_groups && Enum.map(steps, fn step -> compare_groups[step] || 0 end) labels = Enum.map(steps, fn step -> Timex.format!(step, "{ISOdate}") end) - {plot, labels, present_index} + {plot, compare_plot, labels, present_index} end def calculate_plot(site, %Query{step_type: "hour"} = query) do @@ -85,7 +107,7 @@ defmodule Plausible.Stats do steps_to_show = if present_index, do: present_index + 1, else: Enum.count(steps) plot = Enum.map(steps, fn step -> groups[step] || 0 end) |> Enum.take(steps_to_show) labels = Enum.map(steps, fn step -> NaiveDateTime.to_iso8601(step) end) - {plot, labels, present_index} + {plot, [], labels, present_index} end def pageviews_and_visitors(site, query) do diff --git a/lib/plausible_web/controllers/api/stats_controller.ex b/lib/plausible_web/controllers/api/stats_controller.ex index e98bca76f..7bc407fc9 100644 --- a/lib/plausible_web/controllers/api/stats_controller.ex +++ b/lib/plausible_web/controllers/api/stats_controller.ex @@ -12,10 +12,11 @@ defmodule PlausibleWeb.Api.StatsController do {pageviews, visitors} = Stats.pageviews_and_visitors(site, query) {change_pageviews, change_visitors} = Stats.compare_pageviews_and_visitors(site, query, {pageviews, visitors}) conversion_rate = if query.filters["goal"], do: Stats.conversion_rate(site, query) - {plot, labels, present_index} = Task.await(plot_task) + {plot, compare_plot, labels, present_index} = Task.await(plot_task) json(conn, %{ plot: plot, + compare_plot: compare_plot, labels: labels, present_index: present_index, pageviews: pageviews, From 5ee2d908f254b65b7dd7eda1259b8ea88ec27d82 Mon Sep 17 00:00:00 2001 From: Uku Taht Date: Mon, 25 Nov 2019 17:17:18 +0800 Subject: [PATCH 3/6] Show conversion rate in the top stats when filtered for goal --- assets/js/dashboard/stats/visitor-graph.js | 39 ++++------ lib/plausible/stats/stats.ex | 7 ++ .../controllers/api/stats_controller.ex | 48 ++++++++++-- .../stats_controller/authorization_test.exs | 6 +- .../api/stats_controller/main_graph_test.exs | 77 ++++++++++--------- 5 files changed, 103 insertions(+), 74 deletions(-) diff --git a/assets/js/dashboard/stats/visitor-graph.js b/assets/js/dashboard/stats/visitor-graph.js index fd020cfa1..e6e2f8488 100644 --- a/assets/js/dashboard/stats/visitor-graph.js +++ b/assets/js/dashboard/stats/visitor-graph.js @@ -238,9 +238,9 @@ class LineGraph extends React.Component { } else if (query.period === '30d') { return 'last month' } else if (query.period === '3mo') { - return 'previous 3 months' + return 'prev 3 months' } else if (query.period === '6mo') { - return 'previous 6 months' + return 'prev 6 months' } } @@ -256,41 +256,30 @@ class LineGraph extends React.Component { } } - renderConversionRate() { - if (typeof(this.props.graphData.conversion_rate) === "number") { + renderTopStats() { + const {graphData} = this.props + return this.props.graphData.top_stats.map((stat, index) => { + const border = index > 0 ? 'border-l border-grey-light' : '' + return ( -
-
CONVERSION RATE
+
+
{stat.name}
- {this.props.graphData.conversion_rate}% + { typeof(stat.count) == 'number' ? numberFormatter(stat.count) : stat.percentage + '%' }
+ {this.renderComparison(stat.change)}
) - } + }) } render() { - const {graphData} = this.props - const extraClass = graphData.interval === 'hour' ? '' : 'cursor-pointer' + const extraClass = this.props.graphData.interval === 'hour' ? '' : 'cursor-pointer' return (
-
-
UNIQUE VISITORS
-
- {numberFormatter(graphData.unique_visitors)} -
- {this.renderComparison(graphData.change_visitors)} -
-
-
TOTAL {eventName(this.props.query)}
-
- {numberFormatter(graphData.pageviews)} -
- {this.renderComparison(graphData.change_pageviews)} -
- { this.renderConversionRate() } + { this.renderTopStats() }
diff --git a/lib/plausible/stats/stats.ex b/lib/plausible/stats/stats.ex index fe7da7588..942b3b1a5 100644 --- a/lib/plausible/stats/stats.ex +++ b/lib/plausible/stats/stats.ex @@ -117,6 +117,13 @@ defmodule Plausible.Stats do )) end + def unique_visitors(site, query) do + Repo.one(from( + e in base_query(site, query), + select: count(e.user_id, :distinct) + )) + end + def conversion_rate(site, query) do {_, total_visitors} = pageviews_and_visitors(site, %{ query | filters: %{} }) {_, converted_visitors} = pageviews_and_visitors(site, query) diff --git a/lib/plausible_web/controllers/api/stats_controller.ex b/lib/plausible_web/controllers/api/stats_controller.ex index 7bc407fc9..bb0fba646 100644 --- a/lib/plausible_web/controllers/api/stats_controller.ex +++ b/lib/plausible_web/controllers/api/stats_controller.ex @@ -2,6 +2,7 @@ defmodule PlausibleWeb.Api.StatsController do use PlausibleWeb, :controller use Plausible.Repo alias Plausible.Stats + alias Plausible.Stats.Query plug :authorize def main_graph(conn, params) do @@ -9,9 +10,7 @@ defmodule PlausibleWeb.Api.StatsController do query = Stats.Query.from(site.timezone, params) plot_task = Task.async(fn -> Stats.calculate_plot(site, query) end) - {pageviews, visitors} = Stats.pageviews_and_visitors(site, query) - {change_pageviews, change_visitors} = Stats.compare_pageviews_and_visitors(site, query, {pageviews, visitors}) - conversion_rate = if query.filters["goal"], do: Stats.conversion_rate(site, query) + top_stats = fetch_top_stats(site, query) {plot, compare_plot, labels, present_index} = Task.await(plot_task) json(conn, %{ @@ -19,15 +18,48 @@ defmodule PlausibleWeb.Api.StatsController do compare_plot: compare_plot, labels: labels, present_index: present_index, - pageviews: pageviews, - unique_visitors: visitors, - change_pageviews: change_pageviews, - change_visitors: change_visitors, - conversion_rate: conversion_rate, + top_stats: top_stats, interval: query.step_type, }) end + defp fetch_top_stats(site, %Query{filters: %{"goal" => goal}} = query) when is_binary(goal) do + prev_query = Query.shift_back(query) + total_visitors = Stats.unique_visitors(site, %{query | filters: %{}}) + prev_total_visitors = Stats.unique_visitors(site, %{prev_query | filters: %{}}) + converted_visitors = Stats.unique_visitors(site, query) + prev_converted_visitors = Stats.unique_visitors(site, prev_query) + conversion_rate = if total_visitors > 0, do: Float.round(converted_visitors / total_visitors * 100, 1), else: 0.0 + prev_conversion_rate = if prev_total_visitors > 0, do: Float.round(prev_converted_visitors / prev_total_visitors * 100, 1), else: 0.0 + + [ + %{name: "Total visitors", count: total_visitors, change: percent_change(prev_total_visitors, total_visitors)}, + %{name: "Converted visitors", count: converted_visitors, change: percent_change(prev_converted_visitors, converted_visitors)}, + %{name: "Conversion rate", percentage: conversion_rate, change: percent_change(prev_conversion_rate, conversion_rate)}, + ] + end + + defp fetch_top_stats(site, query) do + {pageviews, visitors} = Stats.pageviews_and_visitors(site, query) + {prev_pageviews, prev_visitors} = Stats.pageviews_and_visitors(site, Query.shift_back(query)) + + [ + %{name: "Unique visitors", count: visitors, change: percent_change(prev_visitors, visitors)}, + %{name: "Total pageviews", count: pageviews, change: percent_change(prev_pageviews, pageviews)}, + ] + end + + defp percent_change(old_count, new_count) do + cond do + old_count == 0 and new_count > 0 -> + 100 + old_count == 0 and new_count == 0 -> + 0 + true -> + round((new_count - old_count) / old_count * 100) + end + end + def referrers(conn, params) do site = conn.assigns[:site] query = Stats.Query.from(site.timezone, params) diff --git a/test/plausible_web/controllers/api/stats_controller/authorization_test.exs b/test/plausible_web/controllers/api/stats_controller/authorization_test.exs index 815014e2c..c80593b07 100644 --- a/test/plausible_web/controllers/api/stats_controller/authorization_test.exs +++ b/test/plausible_web/controllers/api/stats_controller/authorization_test.exs @@ -23,7 +23,7 @@ defmodule PlausibleWeb.Api.StatsController.AuthorizationTest do site = insert(:site, public: true) conn = get(conn, "/api/stats/#{site.domain}/main-graph") - assert %{"unique_visitors" => _any} = json_response(conn, 200) + assert %{"plot" => _any} = json_response(conn, 200) end end @@ -48,14 +48,14 @@ defmodule PlausibleWeb.Api.StatsController.AuthorizationTest do site = insert(:site, public: true) conn = get(conn, "/api/stats/#{site.domain}/main-graph") - assert %{"unique_visitors" => _any} = json_response(conn, 200) + assert %{"plot" => _any} = json_response(conn, 200) end test "returns stats for a private site that the user owns", %{conn: conn, user: user} do site = insert(:site, public: false, members: [user]) conn = get(conn, "/api/stats/#{site.domain}/main-graph") - assert %{"unique_visitors" => _any} = json_response(conn, 200) + assert %{"plot" => _any} = json_response(conn, 200) end end 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 a517cc4c7..c22870335 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 @@ -84,7 +84,7 @@ defmodule PlausibleWeb.Api.StatsController.MainGraphTest do end end - describe "GET /api/stats/main-graph - unique visitors" 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 @@ -93,21 +93,19 @@ defmodule PlausibleWeb.Api.StatsController.MainGraphTest do conn = get(conn, "/api/stats/#{site.domain}/main-graph?period=day&date=2019-01-01") - assert %{"unique_visitors" => 1} = json_response(conn, 200) + res = json_response(conn, 200) + assert %{"name" => "Unique visitors", "count" => 1, "change" => 100} in res["top_stats"] end - test "does not count custom events", %{conn: conn, site: site} do + test "does not count custom events in custom user ids", %{conn: conn, site: site} do insert(:pageview, hostname: site.domain, timestamp: ~N[2019-01-01 00:00:00]) insert(:event, name: "Custom", hostname: site.domain, timestamp: ~N[2019-01-01 00:00:00]) conn = get(conn, "/api/stats/#{site.domain}/main-graph?period=day&date=2019-01-01") - assert %{"unique_visitors" => 1} = json_response(conn, 200) + res = json_response(conn, 200) + assert %{"name" => "Unique visitors", "count" => 1, "change" => 100} in res["top_stats"] end - end - - describe "GET /api/stats/main-graph - pageviews" do - setup [:create_user, :log_in, :create_site] test "counts total pageviews even from same user ids", %{conn: conn, site: site} do insert(:pageview, hostname: site.domain, user_id: @user_id, timestamp: ~N[2019-01-01 00:00:00]) @@ -115,32 +113,8 @@ defmodule PlausibleWeb.Api.StatsController.MainGraphTest do conn = get(conn, "/api/stats/#{site.domain}/main-graph?period=day&date=2019-01-01") - assert %{"pageviews" => 2} = json_response(conn, 200) - end - - test "does not count custom events", %{conn: conn, site: site} do - insert(:pageview, hostname: site.domain, timestamp: ~N[2019-01-01 00:00:00]) - insert(:event, name: "Custom", hostname: site.domain, timestamp: ~N[2019-01-01 00:00:00]) - - conn = get(conn, "/api/stats/#{site.domain}/main-graph?period=day&date=2019-01-01") - - assert %{"unique_visitors" => 1} = json_response(conn, 200) - end - end - - describe "GET /api/stats/main-graph - comparisons" do - setup [:create_user, :log_in, :create_site] - - test "compares unique users with previous time period", %{conn: conn, site: site} do - insert(:pageview, hostname: site.domain, user_id: @user_id, timestamp: ~N[2019-01-01 01:00:00]) - insert(:pageview, hostname: site.domain, user_id: @user_id, timestamp: ~N[2019-01-01 02:00:00]) - - insert(:pageview, hostname: site.domain, timestamp: ~N[2019-01-02 01:00:00]) - insert(:pageview, hostname: site.domain, timestamp: ~N[2019-01-02 02:00:00]) - - conn = get(conn, "/api/stats/#{site.domain}/main-graph?period=day&date=2019-01-02") - - assert %{"change_visitors" => 100} = json_response(conn, 200) + res = json_response(conn, 200) + assert %{"name" => "Total pageviews", "count" => 2, "change" => 100} in res["top_stats"] end test "compares pageviews with previous time period", %{conn: conn, site: site} do @@ -151,14 +125,16 @@ defmodule PlausibleWeb.Api.StatsController.MainGraphTest do conn = get(conn, "/api/stats/#{site.domain}/main-graph?period=day&date=2019-01-02") - assert %{"change_pageviews" => -50} = json_response(conn, 200) + res = json_response(conn, 200) + assert %{"name" => "Total pageviews", "count" => 1, "change" => -50} in res["top_stats"] end end - describe "GET /api/stats/main-graph - conversion rate" do + + describe "GET /api/stats/main-graph - filtered for goal" do setup [:create_user, :log_in, :create_site] - test "returns conversion rate when filtering for a goal", %{conn: conn, site: site} do + test "returns total unique visitors", %{conn: conn, site: site} do insert(:pageview, hostname: site.domain, timestamp: ~N[2019-01-01 02:00:00]) insert(:pageview, hostname: site.domain, user_id: @user_id, timestamp: ~N[2019-01-01 01:00:00]) insert(:event, name: "Signup", hostname: site.domain, user_id: @user_id, timestamp: ~N[2019-01-01 02:00:00]) @@ -166,7 +142,32 @@ defmodule PlausibleWeb.Api.StatsController.MainGraphTest do filters = Jason.encode!(%{goal: "Signup"}) conn = get(conn, "/api/stats/#{site.domain}/main-graph?period=day&date=2019-01-01&filters=#{filters}") - assert %{"conversion_rate" => 50} = json_response(conn, 200) + res = json_response(conn, 200) + assert %{"name" => "Total visitors", "count" => 2, "change" => 100} in res["top_stats"] + end + + test "returns converted visitors", %{conn: conn, site: site} do + insert(:pageview, hostname: site.domain, timestamp: ~N[2019-01-01 02:00:00]) + insert(:pageview, hostname: site.domain, user_id: @user_id, timestamp: ~N[2019-01-01 01:00:00]) + insert(:event, name: "Signup", hostname: site.domain, user_id: @user_id, timestamp: ~N[2019-01-01 02:00:00]) + + filters = Jason.encode!(%{goal: "Signup"}) + conn = get(conn, "/api/stats/#{site.domain}/main-graph?period=day&date=2019-01-01&filters=#{filters}") + + res = json_response(conn, 200) + assert %{"name" => "Converted visitors", "count" => 1, "change" => 100} in res["top_stats"] + end + + test "returns conversion rate", %{conn: conn, site: site} do + insert(:pageview, hostname: site.domain, timestamp: ~N[2019-01-01 02:00:00]) + insert(:pageview, hostname: site.domain, user_id: @user_id, timestamp: ~N[2019-01-01 01:00:00]) + insert(:event, name: "Signup", hostname: site.domain, user_id: @user_id, timestamp: ~N[2019-01-01 02:00:00]) + + filters = Jason.encode!(%{goal: "Signup"}) + conn = get(conn, "/api/stats/#{site.domain}/main-graph?period=day&date=2019-01-01&filters=#{filters}") + + res = json_response(conn, 200) + assert %{"name" => "Conversion rate", "percentage" => 50.0, "change" => 100} in res["top_stats"] end end From e17632e363fb50715526ace94a0749c30bd28f18 Mon Sep 17 00:00:00 2001 From: Uku Taht Date: Mon, 25 Nov 2019 17:27:14 +0800 Subject: [PATCH 4/6] Remove unused function --- lib/plausible/stats/stats.ex | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/lib/plausible/stats/stats.ex b/lib/plausible/stats/stats.ex index 942b3b1a5..e67aa0a30 100644 --- a/lib/plausible/stats/stats.ex +++ b/lib/plausible/stats/stats.ex @@ -124,16 +124,6 @@ defmodule Plausible.Stats do )) end - def conversion_rate(site, query) do - {_, total_visitors} = pageviews_and_visitors(site, %{ query | filters: %{} }) - {_, converted_visitors} = pageviews_and_visitors(site, query) - if total_visitors > 0 do - Float.round(converted_visitors / total_visitors * 100, 1) - else - 0.0 - end - end - def top_referrers(site, query, limit \\ 5) do Repo.all(from e in base_query(site, query), select: %{name: e.referrer_source, count: count(e.user_id, :distinct)}, From 29404816cbda40fd3d7364b1e78bb34d78719001 Mon Sep 17 00:00:00 2001 From: Uku Taht Date: Tue, 26 Nov 2019 11:48:27 +0800 Subject: [PATCH 5/6] Fix comparison graph for hourly stats --- assets/js/dashboard/stats/visitor-graph.js | 4 ++-- lib/plausible/stats/stats.ex | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/assets/js/dashboard/stats/visitor-graph.js b/assets/js/dashboard/stats/visitor-graph.js index e6e2f8488..f9c50dd82 100644 --- a/assets/js/dashboard/stats/visitor-graph.js +++ b/assets/js/dashboard/stats/visitor-graph.js @@ -81,8 +81,8 @@ function compareSet(plot, present_index, ctx) { label: 'Conversions', data: plot, borderWidth: 3, - borderColor: 'rgba(101,116,205)', - pointBackgroundColor: 'rgba(101,116,205)', + borderColor: 'rgba(255, 68, 87, 1)', + pointBackgroundColor: 'rgba(255, 68, 87, 1)', backgroundColor: gradient, }] } diff --git a/lib/plausible/stats/stats.ex b/lib/plausible/stats/stats.ex index e67aa0a30..2981d6567 100644 --- a/lib/plausible/stats/stats.ex +++ b/lib/plausible/stats/stats.ex @@ -95,7 +95,7 @@ defmodule Plausible.Stats do end) groups = Repo.all( - from e in base_query(site, query), + from e in base_query(site, %{query | filters: %{}}), group_by: 1, order_by: 1, select: {fragment("date_trunc('hour', ? at time zone 'utc' at time zone ?)", e.timestamp, ^site.timezone), count(e.user_id, :distinct)} @@ -103,11 +103,23 @@ defmodule Plausible.Stats do |> Enum.into(%{}) |> transform_keys(fn dt -> NaiveDateTime.truncate(dt, :second) end) + compare_groups = if query.filters["goal"] do + Repo.all( + from e in base_query(site, query), + group_by: 1, + order_by: 1, + select: {fragment("date_trunc('hour', ? at time zone 'utc' at time zone ?)", e.timestamp, ^site.timezone), count(e.user_id, :distinct)} + ) + |> Enum.into(%{}) + |> transform_keys(fn dt -> NaiveDateTime.truncate(dt, :second) end) + end + present_index = Enum.find_index(steps, fn step -> step == Timex.now(site.timezone) |> truncate_to_hour |> NaiveDateTime.truncate(:second) end) steps_to_show = if present_index, do: present_index + 1, else: Enum.count(steps) plot = Enum.map(steps, fn step -> groups[step] || 0 end) |> Enum.take(steps_to_show) + compare_plot = compare_groups && Enum.map(steps, fn step -> compare_groups[step] || 0 end) labels = Enum.map(steps, fn step -> NaiveDateTime.to_iso8601(step) end) - {plot, [], labels, present_index} + {plot, compare_plot, labels, present_index} end def pageviews_and_visitors(site, query) do From e0737849ba74245231f837043d32b30a963deb57 Mon Sep 17 00:00:00 2001 From: Uku Taht Date: Tue, 26 Nov 2019 11:55:54 +0800 Subject: [PATCH 6/6] Add test for conversion in month --- .../controllers/api/stats_controller/main_graph_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c22870335..de7467f33 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 @@ -152,7 +152,7 @@ defmodule PlausibleWeb.Api.StatsController.MainGraphTest do insert(:event, name: "Signup", hostname: site.domain, user_id: @user_id, timestamp: ~N[2019-01-01 02:00:00]) filters = Jason.encode!(%{goal: "Signup"}) - conn = get(conn, "/api/stats/#{site.domain}/main-graph?period=day&date=2019-01-01&filters=#{filters}") + conn = get(conn, "/api/stats/#{site.domain}/main-graph?period=month&date=2019-01-01&filters=#{filters}") res = json_response(conn, 200) assert %{"name" => "Converted visitors", "count" => 1, "change" => 100} in res["top_stats"]