| 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, dimensions_used_in_filters: 2] |
|
|
| @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", |
|
|
| |
| "visit:screen" => "imported_devices", |
| "event:hostname" => "imported_pages", |
|
|
| |
| "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 = Imported.complete_import_ids(site) |
| |
| %{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 |
| behavioral_filters = dimensions_used_in_filters(query.filters, behavioral_filters: :only) |
|
|
| cond do |
| |
| length(behavioral_filters) > 0 -> |
| [] |
|
|
| custom_prop_query?(query) -> |
| do_decide_custom_prop_table(query) |
|
|
| true -> |
| 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 |
| "event:props:" <> prop_key = property |
|
|
| has_required_event_or_goal_name_filter? = |
| query.filters |
| |> Enum.flat_map(fn |
| [:is, "event:name", event_names | _rest] -> event_names |
| [:is, "event:goal", goal_names | _rest] -> goal_names |
| _ -> [] |
| end) |
| |> Enum.any?(fn event_or_goal_name -> |
| event_or_goal_name in Plausible.Event.SystemEvents.special_events_for_prop_key(prop_key) |
| end) |
|
|
| has_unsupported_filters? = |
| query.filters |
| |> dimensions_used_in_filters() |
| |> Enum.any?(&(&1 not in [property, "event:name", "event:goal"])) |
|
|
| if has_required_event_or_goal_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 = query.preloaded_goals.matching_toplevel_filters |
|
|
| 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.preloaded_goals.matching_toplevel_filters |
| |> Enum.map(&Plausible.Goal.type/1) |
| |> Enum.map(fn |
| :event -> "imported_custom_events" |
| :page -> "imported_pages" |
| :scroll -> nil |
| end) |
|
|
| case Enum.uniq(table_candidates ++ filter_goal_table_candidates) do |
| [] -> ["imported_visitors"] |
| [nil] -> [] |
| [candidate] -> [candidate] |
| _ -> [] |
| end |
| end |
| end |
|
|