Skip to content

Commit

Permalink
Additional metrics for timeseries API call and CSV export (#952)
Browse files Browse the repository at this point in the history
* Add `metrics` option to timeseries call

* Add more metrics to csv export

* Add changelog entry
  • Loading branch information
ukutaht authored Apr 23, 2021
1 parent 4b321bd commit b107717
Show file tree
Hide file tree
Showing 7 changed files with 349 additions and 80 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file.

## Unreleased

### Added
- New parameter `metrics` for the `/api/v1/stats/timeseries` endpoint plausible/analytics#952
- CSV export now includes pageviews, bounce rate and visit duration in addition to visitors plausible/analytics#952

### Fixed
- Fix weekly report time range plausible/analytics#951

Expand Down
2 changes: 1 addition & 1 deletion lib/plausible/stats/mod.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
defmodule Plausible.Stats do
defdelegate breakdown(site, query, prop, metrics, pagination), to: Plausible.Stats.Breakdown
defdelegate aggregate(site, query, metrics), to: Plausible.Stats.Aggregate
defdelegate timeseries(site, query), to: Plausible.Stats.Timeseries
defdelegate timeseries(site, query, metrics), to: Plausible.Stats.Timeseries
end
117 changes: 92 additions & 25 deletions lib/plausible/stats/timeseries.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,49 @@ defmodule Plausible.Stats.Timeseries do
use Plausible.ClickhouseRepo
alias Plausible.Stats.Query
import Plausible.Stats.Base
use Plausible.Stats.Fragments

def timeseries(site, query) do
@event_metrics ["visitors", "pageviews"]
@session_metrics ["bounce_rate", "visit_duration"]
def timeseries(site, query, metrics) do
steps = buckets(query)

groups =
from(e in base_event_query(site, query),
group_by: fragment("bucket"),
order_by: fragment("bucket")
)
|> select_bucket(site, query)
|> ClickhouseRepo.all()
|> Enum.into(%{})
event_metrics = Enum.filter(metrics, &(&1 in @event_metrics))
session_metrics = Enum.filter(metrics, &(&1 in @session_metrics))

plot = Enum.map(steps, fn step -> groups[step] || 0 end)
[event_result, session_result] =
Task.await_many([
Task.async(fn -> events_timeseries(site, query, event_metrics) end),
Task.async(fn -> sessions_timeseries(site, query, session_metrics) end)
])

labels =
Enum.map(steps, fn
step when is_binary(step) -> step
step -> Timex.format!(step, "{ISOdate}")
end)
Enum.map(steps, fn step ->
empty_row(step, metrics)
|> Map.merge(Enum.find(event_result, fn row -> row["date"] == step end) || %{})
|> Map.merge(Enum.find(session_result, fn row -> row["date"] == step end) || %{})
end)
end

defp events_timeseries(site, query, metrics) do
from(e in base_event_query(site, query),
group_by: fragment("date"),
order_by: fragment("date"),
select: %{}
)
|> select_bucket(site, query)
|> select_event_metrics(metrics)
|> ClickhouseRepo.all()
end

{plot, labels}
defp sessions_timeseries(site, query, metrics) do
from(e in query_sessions(site, query),
group_by: fragment("date"),
order_by: fragment("date"),
select: %{}
)
|> select_bucket(site, query)
|> select_session_metrics(metrics)
|> ClickhouseRepo.all()
end

defp buckets(%Query{interval: "month"} = query) do
Expand Down Expand Up @@ -51,27 +72,73 @@ defmodule Plausible.Stats.Timeseries do
defp select_bucket(q, site, %Query{interval: "month"}) do
from(
e in q,
select:
{fragment("toStartOfMonth(toTimeZone(?, ?)) as bucket", e.timestamp, ^site.timezone),
fragment("uniq(?)", e.user_id)}
select_merge: %{
"date" =>
fragment("toStartOfMonth(toTimeZone(?, ?)) as date", e.timestamp, ^site.timezone)
}
)
end

defp select_bucket(q, site, %Query{interval: "date"}) do
from(
e in q,
select:
{fragment("toDate(toTimeZone(?, ?)) as bucket", e.timestamp, ^site.timezone),
fragment("uniq(?)", e.user_id)}
select_merge: %{
"date" => fragment("toDate(toTimeZone(?, ?)) as date", e.timestamp, ^site.timezone)
}
)
end

defp select_bucket(q, site, %Query{interval: "hour"}) do
from(
e in q,
select:
{fragment("toStartOfHour(toTimeZone(?, ?)) as bucket", e.timestamp, ^site.timezone),
fragment("uniq(?)", e.user_id)}
select_merge: %{
"date" => fragment("toStartOfHour(toTimeZone(?, ?)) as date", e.timestamp, ^site.timezone)
}
)
end

defp select_event_metrics(q, []), do: q

defp select_event_metrics(q, ["pageviews" | rest]) do
from(e in q,
select_merge: %{"pageviews" => fragment("countIf(? = 'pageview')", e.name)}
)
|> select_event_metrics(rest)
end

defp select_event_metrics(q, ["visitors" | rest]) do
from(e in q,
select_merge: %{"visitors" => fragment("uniq(?) as count", e.user_id)}
)
|> select_event_metrics(rest)
end

defp select_session_metrics(q, []), do: q

defp select_session_metrics(q, ["bounce_rate" | rest]) do
from(s in q,
select_merge: %{
"bounce_rate" => bounce_rate()
}
)
|> select_session_metrics(rest)
end

defp select_session_metrics(q, ["visit_duration" | rest]) do
from(s in q,
select_merge: %{"visit_duration" => visit_duration()}
)
|> select_session_metrics(rest)
end

defp empty_row(date, metrics) do
Enum.reduce(metrics, %{"date" => date}, fn metric, row ->
case metric do
"pageviews" -> Map.merge(row, %{"pageviews" => 0})
"visitors" -> Map.merge(row, %{"visitors" => 0})
"bounce_rate" -> Map.merge(row, %{"bounce_rate" => nil})
"visit_duration" -> Map.merge(row, %{"visit_duration" => nil})
end
end)
end
end
11 changes: 3 additions & 8 deletions lib/plausible_web/controllers/api/external_stats_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -120,14 +120,9 @@ defmodule PlausibleWeb.Api.ExternalStatsController do
with :ok <- validate_period(params),
:ok <- validate_date(params),
:ok <- validate_interval(params),
query <- Query.from(site.timezone, params) do
{plot, labels} = Plausible.Stats.timeseries(site, query)

graph =
Enum.zip(labels, plot)
|> Enum.map(fn {label, val} -> %{date: label, visitors: val} end)
|> Enum.into([])

query <- Query.from(site.timezone, params),
{:ok, metrics} <- parse_metrics(params, nil, query) do
graph = Plausible.Stats.timeseries(site, query, metrics)
json(conn, %{"results" => graph})
else
{:error, msg} ->
Expand Down
20 changes: 14 additions & 6 deletions lib/plausible_web/controllers/stats_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,28 @@ defmodule PlausibleWeb.StatsController do

def csv_export(conn, %{"domain" => domain}) do
site = conn.assigns[:site]

query = Query.from(site.timezone, conn.params)
{plot, labels, _} = Stats.calculate_plot(site, query)

metrics =
if query.filters["event:name"] do
["visitors", "pageviews"]
else
["visitors", "pageviews", "bounce_rate", "visit_duration"]
end

graph = Plausible.Stats.timeseries(site, query, metrics)

headers = ["date" | metrics]

csv_content =
Enum.zip(labels, plot)
|> Enum.map(fn {k, v} -> [k, v] end)
|> (fn data -> [["Date", "Visitors"] | data] end).()
Enum.map(graph, fn row -> Enum.map(headers, &row[&1]) end)
|> (fn data -> [headers | data] end).()
|> CSV.encode()
|> Enum.into([])
|> Enum.join()

filename =
"Visitors #{domain} #{Timex.format!(query.date_range.first, "{ISOdate} ")} to #{
"Plausible export #{domain} #{Timex.format!(query.date_range.first, "{ISOdate} ")} to #{
Timex.format!(query.date_range.last, "{ISOdate} ")
}.csv"

Expand Down
Loading

0 comments on commit b107717

Please sign in to comment.