analytics / lib /plausible /stats /dashboard /csv_export.ex
Leon4gr45's picture
Upload folder using huggingface_hub (part 2)
8da2481 verified
Raw
History Blame Contribute Delete
10.3 kB
defmodule Plausible.Stats.Dashboard.CsvExport do
@moduledoc false
alias Plausible.Stats.Dashboard.QueryParser
alias Plausible.Stats.{Time, ParsedQueryParams, Query, QueryBuilder, QueryRunner, QueryResult}
@csv_filenames [
"visitors.csv",
"pages.csv",
"entry_pages.csv",
"exit_pages.csv",
"browsers.csv",
"browser_versions.csv",
"operating_systems.csv",
"operating_system_versions.csv",
"devices.csv",
"channels.csv",
"sources.csv",
"referrers.csv",
"utm_mediums.csv",
"utm_sources.csv",
"utm_campaigns.csv",
"utm_contents.csv",
"utm_terms.csv",
"countries.csv",
"regions.csv",
"cities.csv",
"custom_props.csv",
"conversions.csv"
]
def get_csvs(site, params, debug_metadata) do
with {:ok, tasks_by_filename} <- create_tasks_by_filename(site, params, debug_metadata) do
csv_values =
tasks_by_filename
|> Enum.map(fn {_filename, task} -> task end)
|> Plausible.ClickhouseRepo.parallel_tasks()
csvs =
tasks_by_filename
|> Enum.map(fn {filename, _task} -> filename end)
|> Enum.zip(csv_values)
|> Enum.reject(fn {_filename, csv_values} -> is_nil(csv_values) end)
|> Enum.map(fn {k, v} -> {String.to_charlist(k), IO.iodata_to_binary(v)} end)
{:ok, csvs}
end
end
defp create_tasks_by_filename(site, params, debug_metadata) do
requested = Map.keys(params["reports"] || %{}) |> Enum.filter(&(&1 in @csv_filenames))
Enum.reduce_while(requested, {:ok, []}, fn filename, acc ->
case create_task(site, filename, params, debug_metadata) do
{:ok, task} -> {:cont, {:ok, elem(acc, 1) ++ [{filename, task}]}}
error_tuple -> {:halt, error_tuple}
end
end)
end
defp create_task(site, "custom_props.csv", params, debug_metadata) do
with {:ok, prop_keys_query} <- get_custom_prop_key_query(site, params, debug_metadata),
prop_keys = get_custom_prop_keys(site, prop_keys_query),
{:ok, prop_value_queries_by_prop_key} <-
get_custom_prop_queries(site, params, debug_metadata, prop_keys) do
{:ok, fn -> get_custom_props_csv(site, prop_value_queries_by_prop_key) end}
end
end
defp create_task(site, filename, params, debug_metadata) do
with {:ok, query} <- parse_csv_query(site, filename, params, debug_metadata) do
{:ok, fn -> run_query_into_csv(site, filename, query) end}
end
end
defp parse_csv_query(site, filename, params, debug_metadata) do
params = construct_raw_query_params(filename, params)
with {:ok, %ParsedQueryParams{} = params} <- QueryParser.parse(params) do
QueryBuilder.build(site, params, debug_metadata)
end
end
defp construct_raw_query_params(filename, params) do
report_params = params["reports"][filename]
filters =
case report_params["always_on_filters"] do
always_on when is_list(always_on) -> params["filters"] ++ always_on
_ -> params["filters"]
end
%{
"dimensions" => report_params["dimensions"],
"metrics" => report_params["metrics"],
"date_range" => params["date_range"],
"relative_date" => params["relative_date"],
"filters" => filters,
"include" => params["include"]
}
end
defp run_query_into_csv(site, "visitors.csv", query) do
query =
query
|> Query.set(order_by: [{Time.time_dimension(query), :asc}])
|> Query.set_include(:time_labels, true)
|> Query.set_include(:empty_metrics, true)
QueryRunner.run(site, query) |> timeseries_query_result_to_csv()
end
defp run_query_into_csv(site, filename, query) do
# The export is limited to 300 entries for other reports and 100 entries for
# pages because bigger result sets start causing failures. Since we request
# data like time on page or bounce_rate for pages in a separate query using
# the IN filter, it causes the requests to balloon in payload size.
limit = if filename in ["pages.csv", "exit_pages.csv"], do: 100, else: 300
query =
query
|> Query.set(order_by: get_order_by(query.dimensions))
|> Query.set(pagination: %{limit: limit, offset: 0})
QueryRunner.run(site, query) |> query_result_to_csv()
end
defp query_result_to_csv(%QueryResult{results: results, query: query}) do
results
|> Enum.reduce([csv_first_row(query)], fn row, acc ->
acc ++ [csv_dimension_values(query[:dimensions], row) ++ row.metrics]
end)
|> NimbleCSV.RFC4180.dump_to_iodata()
end
defp timeseries_query_result_to_csv(%QueryResult{results: results, query: query, meta: meta}) do
meta[:time_labels]
|> Enum.reduce([csv_first_row(query)], fn timelabel, acc ->
if result_item = find_result_item_by_timelabel(results, timelabel) do
acc ++ [[timelabel | result_item.metrics]]
else
acc ++ [[timelabel | meta[:empty_metrics]]]
end
end)
|> NimbleCSV.RFC4180.dump_to_iodata()
end
defp find_result_item_by_timelabel(results, timelabel) do
results
|> Map.new(fn entry -> {Enum.at(entry.dimensions, 0), entry} end)
|> Map.get(timelabel)
end
defp get_order_by(["event:goal"]), do: [{:visitors, :desc}]
defp get_order_by(dimensions) do
[{:visitors, :desc} | Enum.map(dimensions, &{&1, :asc})]
end
defp csv_first_row(query) do
csv_dimension_labels(query[:dimensions]) ++ csv_metric_labels(query)
end
defp csv_dimension_labels(["time:" <> _]) do
[:date]
end
defp csv_dimension_labels(["visit:browser_version", "visit:browser"]) do
[:name, :version]
end
defp csv_dimension_labels(["visit:os_version", "visit:os"]) do
[:name, :version]
end
defp csv_dimension_labels([_dimension]) do
[:name]
end
defp get_goal_filtered_metric_label_overrides(query) do
case query[:dimensions] do
["time:" <> _] ->
%{
visitors: :unique_conversions,
events: :total_conversions,
group_conversion_rate: :conversion_rate
}
["event:goal"] ->
%{
visitors: :unique_conversions,
events: :total_conversions
}
_ ->
%{
visitors: :conversions,
group_conversion_rate: :conversion_rate
}
end
end
@dimension_specific_metric_label_overrides %{
["visit:entry_page"] => %{
visitors: :unique_entrances,
visits: :total_entrances
},
["visit:exit_page"] => %{
visitors: :unique_exits,
visits: :total_exits
},
["event:goal"] => %{
visitors: :unique_conversions,
events: :total_conversions
}
}
defp csv_metric_labels(query) do
goal_filter? =
Enum.any?(query[:filters], fn [_operator, key | _rest] ->
key == "event:goal"
end)
metric_label_overrides =
if goal_filter? do
get_goal_filtered_metric_label_overrides(query)
else
Map.get(@dimension_specific_metric_label_overrides, query[:dimensions], %{})
end
Enum.map(query[:metrics], &(metric_label_overrides[&1] || &1))
end
defp csv_dimension_values(["visit:browser_version", "visit:browser"], row) do
row[:dimensions] |> Enum.reverse()
end
defp csv_dimension_values(["visit:os_version", "visit:os"], row) do
row[:dimensions] |> Enum.reverse()
end
defp csv_dimension_values([_dimension], row) do
row[:dimensions]
end
defp get_custom_prop_key_query(site, params, debug_metadata) do
raw_params =
construct_raw_query_params("custom_props.csv", params)
|> Map.merge(%{
"dimensions" => ["event:prop_key"],
"metrics" => ["visitors"],
"pagination" => %{"limit" => 25}
})
with {:ok, parsed} <- QueryParser.parse(raw_params),
{:ok, query} <- QueryBuilder.build(site, parsed, debug_metadata) do
{:ok, Query.set(query, order_by: [{:visitors, :desc}, {"event:prop_key", :asc}])}
end
end
defp get_custom_prop_keys(site, query) do
case Plausible.Stats.Filters.get_toplevel_filter(query, "event:props:") do
[_op, "event:props:" <> key | _rest] ->
[key]
_ ->
%QueryResult{results: results} = QueryRunner.run(site, query)
Enum.map(results, fn r -> hd(r.dimensions) end)
|> maybe_allowed_props_only(site)
end
end
defp get_custom_prop_queries(site, params, debug_metadata, prop_keys) do
Enum.reduce_while(prop_keys, {:ok, []}, fn prop_key, acc ->
dimension = "event:props:#{prop_key}"
raw_params =
construct_raw_query_params("custom_props.csv", params)
|> Map.put("dimensions", [dimension])
with {:ok, parsed} <- QueryParser.parse(raw_params),
{:ok, query} <- QueryBuilder.build(site, parsed, debug_metadata) do
query =
query
|> Query.set(order_by: [{:visitors, :desc}, {dimension, :asc}])
|> Query.set(pagination: %{limit: 300, offset: 0})
{:cont, {:ok, elem(acc, 1) ++ [{prop_key, query}]}}
else
error_tuple -> {:halt, error_tuple}
end
end)
end
defp get_custom_props_csv(_site, []), do: nil
defp get_custom_props_csv(site, prop_value_queries_by_prop_key) do
[{_prop_key, %Query{metrics: metrics}} | _] = prop_value_queries_by_prop_key
header_row = format_custom_props_header_row(metrics)
data_rows = custom_prop_queries_into_data_rows(site, prop_value_queries_by_prop_key)
NimbleCSV.RFC4180.dump_to_iodata([header_row] ++ data_rows)
end
defp custom_prop_queries_into_data_rows(site, prop_value_queries_by_prop_key) do
prop_value_queries_by_prop_key
|> Enum.map(fn {prop_key, query} ->
fn ->
%QueryResult{results: results} = QueryRunner.run(site, query)
Enum.map(results, &format_custom_props_data_row(prop_key, &1))
end
end)
|> Plausible.ClickhouseRepo.parallel_tasks()
|> Enum.concat()
end
defp format_custom_props_header_row(metrics), do: [:property, :value] ++ metrics
defp format_custom_props_data_row(prop_key, row),
do: [prop_key | row.dimensions] ++ row.metrics
defp maybe_allowed_props_only(prop_keys, site) do
case Plausible.Props.allowed_for(site) do
:all -> prop_keys
allowed -> Enum.filter(prop_keys, &(&1 in allowed))
end
end
end