Download lib/plausible/stats/dashboard/csv_export.ex from Leon4gr45/analytics: direct link, hf CLI and curl.
- Browser
- Download file 10.3 kB
-
https://huggingface.co/spaces/Leon4gr45/analytics/resolve/main/lib/plausible/stats/dashboard/csv_export.ex
- Command line
-
hf download hf://spaces/Leon4gr45/analytics/lib/plausible/stats/dashboard/csv_export.ex
-
curl -L -o csv_export.ex https://huggingface.co/spaces/Leon4gr45/analytics/resolve/main/lib/plausible/stats/dashboard/csv_export.ex
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 | |