Put total conversions on the graph + goal-filtered CSV export improvements (#3929)

* Add validation for the events metric in main_graph

* Test the already existing events metric support in main-graph

* Put total conversions on the graph

* extract main_graph_csv function (refactor only)

* add total_conversions and conversion_rate to goal-filtered visitors.csv

* update changelog
This commit is contained in:
RobertJoonas 2024-03-22 09:35:23 +00:00 committed by GitHub
parent 561dcd821e
commit d6e1e8bebd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 148 additions and 60 deletions

View File

@ -2,6 +2,8 @@
All notable changes to this project will be documented in this file.
### Added
- Add `total_conversions` and `conversion_rate` to `visitors.csv` in a goal-filtered CSV export
- Ability to display total conversions (with a goal filter) on the main graph
- Add `conversion_rate` to Stats API Timeseries and on the main graph
- Add `time_on_page` metric into the Stats API
- County Block List in Site Settings

View File

@ -10,6 +10,7 @@ export const METRIC_MAPPING = {
'Total visits': 'visits',
'Bounce rate': 'bounce_rate',
'Unique conversions': 'conversions',
'Total conversions': 'events',
'Conversion rate': 'conversion_rate',
'Average revenue': 'average_revenue',
'Total revenue': 'total_revenue',
@ -18,6 +19,7 @@ export const METRIC_MAPPING = {
export const METRIC_LABELS = {
'visitors': 'Visitors',
'pageviews': 'Pageviews',
'events': 'Total conversions',
'views_per_visit': 'Views per Visit',
'visits': 'Visits',
'bounce_rate': 'Bounce Rate',
@ -31,6 +33,7 @@ export const METRIC_LABELS = {
export const METRIC_FORMATTER = {
'visitors': numberFormatter,
'pageviews': numberFormatter,
'events': numberFormatter,
'visits': numberFormatter,
'views_per_visit': (number) => (number),
'bounce_rate': (number) => (`${number}%`),

View File

@ -436,7 +436,7 @@ export default class VisitorGraph extends React.Component {
const selectableMetrics = topStatData && topStatData.top_stats.map(({ name }) => METRIC_MAPPING[name]).filter(name => name)
const canSelectSavedMetric = selectableMetrics && selectableMetrics.includes(savedMetric)
if (query.filters.goal && savedMetric !== 'conversion_rate') {
if (query.filters.goal && !['conversion_rate', 'events'].includes(savedMetric)) {
this.setState({ metric: 'conversions' })
} else if (canSelectSavedMetric) {
this.setState({ metric: savedMetric })

View File

@ -8,6 +8,7 @@ defmodule Plausible.Stats.Timeseries do
@typep metric ::
:pageviews
| :events
| :visitors
| :visits
| :bounce_rate

View File

@ -1313,8 +1313,10 @@ defmodule PlausibleWeb.Api.StatsController do
m -> Plausible.Stats.Metrics.from_string!(m)
end
if metric == :conversion_rate and !query.filters["event:goal"] do
{:error, "Metric `:conversion_rate` can only be queried with a goal filter"}
requires_goal_filter? = metric in [:conversion_rate, :events]
if requires_goal_filter? and !query.filters["event:goal"] do
{:error, "Metric `#{metric}` can only be queried with a goal filter"}
else
{:ok, metric}
end

View File

@ -112,29 +112,6 @@ defmodule PlausibleWeb.StatsController do
site = Plausible.Repo.preload(conn.assigns.site, :owner)
query = Query.from(site, params)
metrics =
if query.filters["event:goal"] do
[:visitors]
else
[:visitors, :pageviews, :visits, :views_per_visit, :bounce_rate, :visit_duration]
end
graph = Plausible.Stats.timeseries(site, query, metrics)
columns = [:date | metrics]
column_headers =
if query.filters["event:goal"] do
[:date, :unique_conversions]
else
columns
end
visitors =
Enum.map(graph, fn row -> Enum.map(columns, &row[&1]) end)
|> (fn data -> [column_headers | data] end).()
|> CSV.encode()
|> Enum.join()
filename =
~c"Plausible export #{params["domain"]} #{Timex.format!(query.date_range.first, "{ISOdate} ")} to #{Timex.format!(query.date_range.last, "{ISOdate} ")}.zip"
@ -142,6 +119,7 @@ defmodule PlausibleWeb.StatsController do
limited_params = Map.merge(params, %{"limit" => "100"})
csvs = %{
~c"visitors.csv" => fn -> main_graph_csv(site, query) end,
~c"sources.csv" => fn -> Api.StatsController.sources(conn, params) end,
~c"utm_mediums.csv" => fn -> Api.StatsController.utm_mediums(conn, params) end,
~c"utm_sources.csv" => fn -> Api.StatsController.utm_sources(conn, params) end,
@ -175,8 +153,6 @@ defmodule PlausibleWeb.StatsController do
|> Enum.zip(csv_values)
|> Enum.reject(fn {_k, v} -> is_nil(v) end)
csvs = [{~c"visitors.csv", visitors} | csvs]
{:ok, {_, zip_content}} = :zip.create(filename, csvs, [:memory])
conn
@ -191,6 +167,33 @@ defmodule PlausibleWeb.StatsController do
end
end
defp main_graph_csv(site, query) do
{metrics, column_headers} = csv_graph_metrics(query)
map_bucket_to_row = fn bucket -> Enum.map([:date | metrics], &bucket[&1]) end
prepend_column_headers = fn data -> [column_headers | data] end
Plausible.Stats.timeseries(site, query, metrics)
|> Enum.map(map_bucket_to_row)
|> prepend_column_headers.()
|> CSV.encode()
|> Enum.join()
end
defp csv_graph_metrics(%Query{filters: %{"event:goal" => _}}) do
metrics = [:visitors, :events, :conversion_rate]
column_headers = [:date, :unique_conversions, :total_conversions, :conversion_rate]
{metrics, column_headers}
end
defp csv_graph_metrics(_) do
metrics = [:visitors, :pageviews, :visits, :views_per_visit, :bounce_rate, :visit_duration]
column_headers = [:date | metrics]
{metrics, column_headers}
end
@doc """
Authorizes and renders a shared link:
1. Shared link with no password protection: needs to just make sure the shared link entry is still

View File

@ -1,32 +1,32 @@
date,unique_conversions
2021-09-20,0
2021-09-21,0
2021-09-22,0
2021-09-23,0
2021-09-24,0
2021-09-25,0
2021-09-26,0
2021-09-27,0
2021-09-28,0
2021-09-29,0
2021-09-30,0
2021-10-01,0
2021-10-02,0
2021-10-03,0
2021-10-04,0
2021-10-05,0
2021-10-06,0
2021-10-07,0
2021-10-08,0
2021-10-09,0
2021-10-10,0
2021-10-11,0
2021-10-12,0
2021-10-13,0
2021-10-14,0
2021-10-15,0
2021-10-16,0
2021-10-17,0
2021-10-18,0
2021-10-19,1
2021-10-20,0
date,unique_conversions,total_conversions,conversion_rate
2021-09-20,0,0,0.0
2021-09-21,0,0,0.0
2021-09-22,0,0,0.0
2021-09-23,0,0,0.0
2021-09-24,0,0,0.0
2021-09-25,0,0,0.0
2021-09-26,0,0,0.0
2021-09-27,0,0,0.0
2021-09-28,0,0,0.0
2021-09-29,0,0,0.0
2021-09-30,0,0,0.0
2021-10-01,0,0,0.0
2021-10-02,0,0,0.0
2021-10-03,0,0,0.0
2021-10-04,0,0,0.0
2021-10-05,0,0,0.0
2021-10-06,0,0,0.0
2021-10-07,0,0,0.0
2021-10-08,0,0,0.0
2021-10-09,0,0,0.0
2021-10-10,0,0,0.0
2021-10-11,0,0,0.0
2021-10-12,0,0,0.0
2021-10-13,0,0,0.0
2021-10-14,0,0,0.0
2021-10-15,0,0,0.0
2021-10-16,0,0,0.0
2021-10-17,0,0,0.0
2021-10-18,0,0,0.0
2021-10-19,1,1,50.0
2021-10-20,0,0,0.0

1 date unique_conversions total_conversions conversion_rate
2 2021-09-20 0 0 0.0
3 2021-09-21 0 0 0.0
4 2021-09-22 0 0 0.0
5 2021-09-23 0 0 0.0
6 2021-09-24 0 0 0.0
7 2021-09-25 0 0 0.0
8 2021-09-26 0 0 0.0
9 2021-09-27 0 0 0.0
10 2021-09-28 0 0 0.0
11 2021-09-29 0 0 0.0
12 2021-09-30 0 0 0.0
13 2021-10-01 0 0 0.0
14 2021-10-02 0 0 0.0
15 2021-10-03 0 0 0.0
16 2021-10-04 0 0 0.0
17 2021-10-05 0 0 0.0
18 2021-10-06 0 0 0.0
19 2021-10-07 0 0 0.0
20 2021-10-08 0 0 0.0
21 2021-10-09 0 0 0.0
22 2021-10-10 0 0 0.0
23 2021-10-11 0 0 0.0
24 2021-10-12 0 0 0.0
25 2021-10-13 0 0 0.0
26 2021-10-14 0 0 0.0
27 2021-10-15 0 0 0.0
28 2021-10-16 0 0 0.0
29 2021-10-17 0 0 0.0
30 2021-10-18 0 0 0.0
31 2021-10-19 1 1 50.0
32 2021-10-20 0 0 0.0

View File

@ -478,6 +478,83 @@ defmodule PlausibleWeb.Api.StatsController.MainGraphTest do
end
end
describe "GET /api/stats/main-graph - events (total conversions) plot" do
setup [:create_user, :log_in, :create_new_site]
test "returns 400 when the `events` metric is queried without a goal filter", %{
conn: conn,
site: site
} do
conn =
get(
conn,
"/api/stats/#{site.domain}/main-graph?period=month&date=2021-01-01&metric=events"
)
assert %{"error" => error} = json_response(conn, 400)
assert error =~ "`events` can only be queried with a goal filter"
end
test "displays total conversions for a goal", %{conn: conn, site: site} do
insert(:goal, site: site, event_name: "Signup")
populate_stats(site, [
build(:event, name: "Different", timestamp: ~N[2021-01-01 00:00:00]),
build(:event, name: "Signup", timestamp: ~N[2021-01-01 00:00:00]),
build(:event, name: "Signup", timestamp: ~N[2021-01-01 00:00:00]),
build(:pageview, timestamp: ~N[2021-01-31 00:00:00]),
build(:event, name: "Signup", user_id: 123, timestamp: ~N[2021-01-31 00:00:00]),
build(:event, name: "Signup", user_id: 123, timestamp: ~N[2021-01-31 00:00:00]),
build(:event, name: "Signup", user_id: 123, timestamp: ~N[2021-01-31 00:00:00])
])
filters = Jason.encode!(%{goal: "Signup"})
conn =
get(
conn,
"/api/stats/#{site.domain}/main-graph?period=month&date=2021-01-01&metric=events&filters=#{filters}"
)
assert %{"plot" => plot} = json_response(conn, 200)
assert Enum.count(plot) == 31
assert List.first(plot) == 2
assert Enum.at(plot, 10) == 0.0
assert List.last(plot) == 3
end
test "displays total conversions per hour with previous day comparison plot", %{
conn: conn,
site: site
} do
insert(:goal, site: site, event_name: "Signup")
populate_stats(site, [
build(:event, name: "Different", timestamp: ~N[2021-01-10 05:00:00]),
build(:event, name: "Signup", timestamp: ~N[2021-01-10 05:00:00]),
build(:event, name: "Signup", timestamp: ~N[2021-01-10 05:00:00]),
build(:event, name: "Signup", timestamp: ~N[2021-01-10 19:00:00]),
build(:pageview, timestamp: ~N[2021-01-10 19:00:00]),
build(:event, name: "Signup", timestamp: ~N[2021-01-11 04:00:00]),
build(:event, name: "Signup", timestamp: ~N[2021-01-11 05:00:00]),
build(:event, name: "Signup", timestamp: ~N[2021-01-11 18:00:00])
])
filters = Jason.encode!(%{goal: "Signup"})
conn =
get(
conn,
"/api/stats/#{site.domain}/main-graph?period=day&date=2021-01-11&metric=events&filters=#{filters}&comparison=previous_period"
)
assert %{"plot" => curr, "comparison_plot" => prev} = json_response(conn, 200)
assert [0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0] = prev
assert [0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0] = curr
end
end
describe "GET /api/stats/main-graph - bounce_rate plot" do
setup [:create_user, :log_in, :create_new_site, :add_imported_data]