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

303 lines
9.1 KiB
Elixir

defmodule Plausible.Stats.Imported do
use Plausible.ClickhouseRepo
use Plausible.Stats.SQL.Fragments
import Ecto.Query
import Plausible.Stats.Imported.SQL.Expression
alias Plausible.Stats.Filters
alias Plausible.Stats.Imported
alias Plausible.Stats.Query
alias Plausible.Stats.SQL.QueryBuilder
@property_to_table_mappings Imported.Base.property_to_table_mappings()
@goals_with_url Plausible.Imported.goals_with_url()
def goals_with_url(), do: @goals_with_url
@goals_with_path Plausible.Imported.goals_with_path()
def goals_with_path(), do: @goals_with_path
@doc """
Returns a boolean indicating whether the combination of filters and
breakdown property is possible to query from the imported tables.
Usually, when no filters are used, the imported schema supports the
query. There is one exception though - breakdown by a custom property.
We are currently importing only two custom properties - `url` and `path.
Both these properties can only be used with their special goal filter
(see `@goals_with_url` and `@goals_with_path`).
"""
def schema_supports_query?(query) do
length(Imported.Base.decide_tables(query)) > 0
end
def merge_imported_country_suggestions(native_q, _site, %Plausible.Stats.Query{
include_imported: false
}) do
native_q
end
def merge_imported_country_suggestions(native_q, site, query) do
supports_filter_set? =
Enum.all?(query.filters, fn filter ->
[_, filtered_prop | _] = filter
@property_to_table_mappings[filtered_prop] == "imported_locations"
end)
if supports_filter_set? do
native_q =
native_q
|> exclude(:order_by)
|> exclude(:select)
|> select([e], %{country_code: e.country_code, count: fragment("count(*)")})
imported_q =
from i in Imported.Base.query_imported("imported_locations", site, query),
group_by: i.country,
select_merge: %{country_code: i.country, count: fragment("sum(?)", i.pageviews)}
from(s in subquery(native_q),
full_join: i in subquery(imported_q),
on: s.country_code == i.country_code,
select:
fragment("if(not empty(?), ?, ?)", s.country_code, s.country_code, i.country_code),
order_by: [desc: fragment("? + ?", s.count, i.count)]
)
else
native_q
end
end
def merge_imported_region_suggestions(native_q, _site, %Plausible.Stats.Query{
include_imported: false
}) do
native_q
end
def merge_imported_region_suggestions(native_q, site, query) do
supports_filter_set? =
Enum.all?(query.filters, fn filter ->
[_, filtered_prop | _] = filter
@property_to_table_mappings[filtered_prop] == "imported_locations"
end)
if supports_filter_set? do
native_q =
native_q
|> exclude(:order_by)
|> exclude(:select)
|> select([e], %{region_code: e.subdivision1_code, count: fragment("count(*)")})
imported_q =
from i in Imported.Base.query_imported("imported_locations", site, query),
where: i.region != "",
group_by: i.region,
select_merge: %{region_code: i.region, count: fragment("sum(?)", i.pageviews)}
from(s in subquery(native_q),
full_join: i in subquery(imported_q),
on: s.region_code == i.region_code,
select: fragment("if(not empty(?), ?, ?)", s.region_code, s.region_code, i.region_code),
order_by: [desc: fragment("? + ?", s.count, i.count)]
)
else
native_q
end
end
def merge_imported_city_suggestions(native_q, _site, %Plausible.Stats.Query{
include_imported: false
}) do
native_q
end
def merge_imported_city_suggestions(native_q, site, query) do
supports_filter_set? =
Enum.all?(query.filters, fn filter ->
[_, filtered_prop | _] = filter
@property_to_table_mappings[filtered_prop] == "imported_locations"
end)
if supports_filter_set? do
native_q =
native_q
|> exclude(:order_by)
|> exclude(:select)
|> select([e], %{city_id: e.city_geoname_id, count: fragment("count(*)")})
imported_q =
from i in Imported.Base.query_imported("imported_locations", site, query),
where: i.city != 0,
group_by: i.city,
select_merge: %{city_id: i.city, count: fragment("sum(?)", i.pageviews)}
from(s in subquery(native_q),
full_join: i in subquery(imported_q),
on: s.city_id == i.city_id,
select: fragment("if(? > 0, ?, ?)", s.city_id, s.city_id, i.city_id),
order_by: [desc: fragment("? + ?", s.count, i.count)]
)
else
native_q
end
end
def merge_imported_filter_suggestions(
native_q,
_site,
%Plausible.Stats.Query{include_imported: false},
_filter_name,
_filter_search
) do
native_q
end
def merge_imported_filter_suggestions(
native_q,
site,
query,
filter_name,
filter_query
) do
{table, db_field} = expand_suggestions_field(filter_name)
supports_filter_set? =
Enum.all?(query.filters, fn filter ->
[_, filtered_prop | _] = filter
@property_to_table_mappings[filtered_prop] == table
end)
if supports_filter_set? do
native_q =
native_q
|> exclude(:order_by)
|> exclude(:select)
|> select([e], %{name: field(e, ^filter_name), count: fragment("count(*)")})
imported_q =
from i in Imported.Base.query_imported(table, site, query),
where: fragment("? ilike ?", field(i, ^db_field), ^filter_query),
group_by: field(i, ^db_field),
select_merge: %{name: field(i, ^db_field), count: fragment("sum(?)", i.pageviews)}
from(s in subquery(native_q),
full_join: i in subquery(imported_q),
on: s.name == i.name,
select: fragment("if(not empty(?), ?, ?)", s.name, s.name, i.name),
order_by: [desc: fragment("? + ?", s.count, i.count)],
limit: 25
)
else
native_q
end
end
@filter_suggestions_mapping %{
referrer_source: :source,
acquisition_channel: :channel,
screen_size: :device,
pathname: :page
}
defp expand_suggestions_field(filter_name) do
db_field = Map.get(@filter_suggestions_mapping, filter_name, filter_name)
property =
case db_field do
:operating_system -> :os
:operating_system_version -> :os_version
other -> other
end
table_by_visit = Map.get(@property_to_table_mappings, "visit:#{property}")
table_by_event = Map.get(@property_to_table_mappings, "event:#{property}")
table = table_by_visit || table_by_event
{table, db_field}
end
def merge_imported(q, _, %Query{include_imported: false}, _), do: q
def merge_imported(q, site, %Query{dimensions: []} = query, metrics) do
imported_q =
site
|> Imported.Base.query_imported(query)
|> select_imported_metrics(metrics)
from(
s in subquery(q),
cross_join: i in subquery(imported_q),
select: %{}
)
|> select_joined_metrics(metrics)
end
def merge_imported(q, site, %Query{dimensions: ["event:goal"]} = query, metrics) do
{events, page_regexes} = Filters.Utils.split_goals_query_expressions(query.preloaded_goals)
Imported.Base.decide_tables(query)
|> Enum.map(fn
"imported_custom_events" ->
Imported.Base.query_imported("imported_custom_events", site, query)
|> where([i], i.visitors > 0)
|> select_merge_as([i], %{
dim0: fragment("-indexOf(?, ?)", type(^events, {:array, :string}), i.name)
})
|> select_imported_metrics(metrics)
|> group_by([], selected_as(:dim0))
|> where([], selected_as(:dim0) != 0)
"imported_pages" ->
Imported.Base.query_imported("imported_pages", site, query)
|> where([i], i.visitors > 0)
|> where(
[i],
fragment(
"notEmpty(multiMatchAllIndices(?, ?) as indices)",
i.page,
type(^page_regexes, {:array, :string})
)
)
|> join(:inner, [_i], index in fragment("indices"), hints: "ARRAY", on: true)
|> group_by([_i, index], index)
|> select_merge_as([_i, index], %{
dim0: type(fragment("?", index), :integer)
})
|> select_imported_metrics(metrics)
end)
|> Enum.reduce(q, fn imports_q, q ->
naive_dimension_join(q, imports_q, metrics)
end)
end
def merge_imported(q, site, query, metrics) do
if schema_supports_query?(query) do
imported_q =
site
|> Imported.Base.query_imported(query)
|> where([i], i.visitors > 0)
|> group_imported_by(query)
|> select_imported_metrics(metrics)
from(s in subquery(q),
full_join: i in subquery(imported_q),
on: ^QueryBuilder.build_group_by_join(query),
select: %{}
)
|> select_joined_dimensions(query)
|> select_joined_metrics(metrics)
else
q
end
end
def total_imported_visitors(site, query) do
site
|> Imported.Base.query_imported(query)
|> select_merge([i], %{total_visitors: fragment("sum(?)", i.visitors)})
end
end