analytics/lib/plausible/stats/imported/base.ex

210 lines
6.6 KiB
Elixir

defmodule Plausible.Stats.Imported.Base do
@moduledoc """
A module for building the base of an imported stats query
"""
import Ecto.Query
alias Plausible.Imported
alias Plausible.Stats.Query
import Plausible.Stats.Filters, only: [dimensions_used_in_filters: 1]
@property_to_table_mappings %{
"visit:source" => "imported_sources",
"visit:channel" => "imported_sources",
"visit:referrer" => "imported_sources",
"visit:utm_source" => "imported_sources",
"visit:utm_medium" => "imported_sources",
"visit:utm_campaign" => "imported_sources",
"visit:utm_term" => "imported_sources",
"visit:utm_content" => "imported_sources",
"visit:entry_page" => "imported_entry_pages",
"visit:exit_page" => "imported_exit_pages",
"visit:country" => "imported_locations",
"visit:region" => "imported_locations",
"visit:city" => "imported_locations",
"visit:country_name" => "imported_locations",
"visit:region_name" => "imported_locations",
"visit:city_name" => "imported_locations",
"visit:device" => "imported_devices",
"visit:browser" => "imported_browsers",
"visit:browser_version" => "imported_browsers",
"visit:os" => "imported_operating_systems",
"visit:os_version" => "imported_operating_systems",
"event:page" => "imported_pages",
"event:name" => "imported_custom_events",
# NOTE: these dimensions can be only filtered by
"visit:screen" => "imported_devices",
"event:hostname" => "imported_pages",
# NOTE: These dimensions are only used in group by
"time:month" => "imported_visitors",
"time:week" => "imported_visitors",
"time:day" => "imported_visitors",
"time:hour" => "imported_visitors"
}
@queriable_time_dimensions ["time:month", "time:week", "time:day", "time:hour"]
@imported_custom_props Imported.imported_custom_props()
def property_to_table_mappings(), do: @property_to_table_mappings
def query_imported(site, query) do
[table] = decide_tables(query)
query_imported(table, site, query)
end
def query_imported(table, site, query) do
import_ids = site.complete_import_ids
# Assumption: dates in imported table are in user-local timezone.
%{first: date_from, last: date_to} = Query.date_range(query)
from(i in table,
where: i.site_id == ^site.id,
where: i.import_id in ^import_ids,
where: i.date >= ^date_from,
where: i.date <= ^date_to,
where: ^Plausible.Stats.Imported.SQL.WhereBuilder.build(query),
select: %{}
)
end
def decide_tables(query) do
if custom_prop_query?(query) do
do_decide_custom_prop_table(query)
else
do_decide_tables(query)
end
end
defp custom_prop_query?(query) do
dimensions_used_in_filters(query.filters)
|> Enum.concat(query.dimensions)
|> Enum.any?(&(&1 in @imported_custom_props))
end
defp do_decide_custom_prop_table(%{dimensions: [dimension]} = query)
when dimension in @imported_custom_props do
do_decide_custom_prop_table(query, dimension)
end
@queriable_custom_prop_dimensions ["event:goal", "event:name"] ++ @queriable_time_dimensions
defp do_decide_custom_prop_table(%{dimensions: dimensions} = query) do
if dimensions == [] or
(length(dimensions) == 1 and hd(dimensions) in @queriable_custom_prop_dimensions) do
custom_prop_filters =
dimensions_used_in_filters(query.filters)
|> Enum.filter(&(&1 in @imported_custom_props))
|> Enum.uniq()
case custom_prop_filters do
[custom_prop_filter] ->
do_decide_custom_prop_table(query, custom_prop_filter)
_ ->
[]
end
else
[]
end
end
defp do_decide_custom_prop_table(query, property) do
has_required_name_filter? =
query.filters
|> Enum.flat_map(fn
[:is, "event:name", names] -> names
[:is, "event:goal", names] -> names
_ -> []
end)
|> Enum.any?(&(&1 in special_goals_for(property)))
has_unsupported_filters? =
Enum.any?(query.filters, fn [_, filter_key | _] ->
filter_key not in [property, "event:name", "event:goal"]
end)
if has_required_name_filter? and not has_unsupported_filters? do
["imported_custom_events"]
else
[]
end
end
defp do_decide_tables(%Query{filters: [], dimensions: []}), do: ["imported_visitors"]
defp do_decide_tables(%Query{filters: [], dimensions: ["event:goal"]}) do
["imported_pages", "imported_custom_events"]
end
defp do_decide_tables(%Query{dimensions: ["event:goal"]} = query) do
filter_dimensions = dimensions_used_in_filters(query.filters)
filter_goals = get_filter_goals(query)
any_event_goals? = Enum.any?(filter_goals, fn goal -> Plausible.Goal.type(goal) == :event end)
any_pageview_goals? =
Enum.any?(filter_goals, fn goal -> Plausible.Goal.type(goal) == :page end)
any_event_name_filters? = "event:name" in filter_dimensions or any_event_goals?
any_page_filters? = "event:page" in filter_dimensions or any_pageview_goals?
any_other_filters? =
Enum.any?(filter_dimensions, &(&1 not in ["event:page", "event:name", "event:goal"]))
cond do
any_other_filters? -> []
any_event_name_filters? and not any_page_filters? -> ["imported_custom_events"]
any_page_filters? and not any_event_name_filters? -> ["imported_pages"]
true -> []
end
end
defp do_decide_tables(query) do
table_candidates =
dimensions_used_in_filters(query.filters)
|> Enum.concat(query.dimensions)
|> Enum.reject(&(&1 in @queriable_time_dimensions or &1 == "event:goal"))
|> Enum.flat_map(fn
"visit:screen" -> ["visit:device"]
dimension -> [dimension]
end)
|> Enum.map(&@property_to_table_mappings[&1])
filter_goal_table_candidates =
query
|> get_filter_goals()
|> Enum.map(&Plausible.Goal.type/1)
|> Enum.map(fn
:event -> "imported_custom_events"
:page -> "imported_pages"
end)
case Enum.uniq(table_candidates ++ filter_goal_table_candidates) do
[] -> ["imported_visitors"]
[nil] -> []
[candidate] -> [candidate]
_ -> []
end
end
defp get_filter_goals(query) do
query.filters
|> Enum.filter(fn [_, dimension | _rest] -> dimension == "event:goal" end)
|> Enum.flat_map(fn [operation, _dimension, clauses] ->
Enum.flat_map(clauses, fn clause ->
query.preloaded_goals
|> Plausible.Goals.Filters.filter_preloaded(operation, clause)
end)
end)
end
def special_goals_for("event:props:url"), do: Imported.goals_with_url()
def special_goals_for("event:props:path"), do: Imported.goals_with_path()
end