mirror of
https://github.com/plausible/analytics.git
synced 2025-01-09 03:26:52 +03:00
Add support for imported custom events (#4033)
* Add Ecto schema for imported custom events * Start importing custom events from GA4 * query imported goals * make it possible to query events metric from imported * make it possible to query pageviews in goal breakdown * make it possible to query conversion rate * fix rate limiting test * add CR tests for dashboard API * implement imported link_url breakdown * override special custom event names coming from GA4 * allow specific goal filters in imported_q * update GA4 import tests to use Stats API * Improve tests slightly * Update CHANGELOG.md --------- Co-authored-by: Robert Joonas <robertjoonas16@gmail.com>
This commit is contained in:
parent
d8435f2e01
commit
4e7e932a75
@ -40,6 +40,8 @@ All notable changes to this project will be documented in this file.
|
|||||||
- Add `EXTRA_CONFIG_PATH` env var to specify extra Elixir config plausible/analytics#3906
|
- Add `EXTRA_CONFIG_PATH` env var to specify extra Elixir config plausible/analytics#3906
|
||||||
- Add restrictive `robots.txt` for self-hosted plausible/analytics#3905
|
- Add restrictive `robots.txt` for self-hosted plausible/analytics#3905
|
||||||
- Add Yesterday as an time range option in the dashboard
|
- Add Yesterday as an time range option in the dashboard
|
||||||
|
- Add support for importing Google Analytics 4 data
|
||||||
|
- Import custom events from Google Analytics 4
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
- Removed the nested custom event property breakdown UI when filtering by a goal in Goal Conversions
|
- Removed the nested custom event property breakdown UI when filtering by a goal in Goal Conversions
|
||||||
|
1212
fixture/ga4_report_imported_custom_events.json
Normal file
1212
fixture/ga4_report_imported_custom_events.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -31,6 +31,7 @@ defmodule Plausible.Google.GA4.HTTP do
|
|||||||
desc: true
|
desc: true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
dimensionFilter: report_request.dimension_filter,
|
||||||
limit: report_request.limit,
|
limit: report_request.limit,
|
||||||
offset: report_request.offset
|
offset: report_request.offset
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,13 @@ defmodule Plausible.Google.GA4.ReportRequest do
|
|||||||
Report request struct for Google Analytics 4 API
|
Report request struct for Google Analytics 4 API
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@excluded_event_names [
|
||||||
|
"page_view",
|
||||||
|
"session_start",
|
||||||
|
"first_visit",
|
||||||
|
"user_engagement"
|
||||||
|
]
|
||||||
|
|
||||||
defstruct [
|
defstruct [
|
||||||
:dataset,
|
:dataset,
|
||||||
:dimensions,
|
:dimensions,
|
||||||
@ -11,6 +18,7 @@ defmodule Plausible.Google.GA4.ReportRequest do
|
|||||||
:property,
|
:property,
|
||||||
:access_token,
|
:access_token,
|
||||||
:offset,
|
:offset,
|
||||||
|
:dimension_filter,
|
||||||
:limit
|
:limit
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -91,6 +99,25 @@ defmodule Plausible.Google.GA4.ReportRequest do
|
|||||||
# "bounces = sessions - engagedSessions"
|
# "bounces = sessions - engagedSessions"
|
||||||
# ]
|
# ]
|
||||||
# },
|
# },
|
||||||
|
%__MODULE__{
|
||||||
|
dataset: "imported_custom_events",
|
||||||
|
dimensions: ["date", "eventName", "linkUrl"],
|
||||||
|
metrics: [
|
||||||
|
"totalUsers",
|
||||||
|
"eventCount"
|
||||||
|
],
|
||||||
|
dimension_filter: %{
|
||||||
|
"notExpression" => %{
|
||||||
|
"filter" => %{
|
||||||
|
"fieldName" => "eventName",
|
||||||
|
"inListFilter" => %{
|
||||||
|
"values" => @excluded_event_names,
|
||||||
|
"caseSensitive" => true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
%__MODULE__{
|
%__MODULE__{
|
||||||
dataset: "imported_locations",
|
dataset: "imported_locations",
|
||||||
dimensions: ["date", "countryId", "region", "city"],
|
dimensions: ["date", "countryId", "region", "city"],
|
||||||
|
@ -27,6 +27,7 @@ defmodule Plausible.Imported do
|
|||||||
Imported.Page,
|
Imported.Page,
|
||||||
Imported.EntryPage,
|
Imported.EntryPage,
|
||||||
Imported.ExitPage,
|
Imported.ExitPage,
|
||||||
|
Imported.CustomEvent,
|
||||||
Imported.Location,
|
Imported.Location,
|
||||||
Imported.Device,
|
Imported.Device,
|
||||||
Imported.Browser,
|
Imported.Browser,
|
||||||
|
@ -107,6 +107,7 @@ defmodule Plausible.Imported.Buffer do
|
|||||||
defp table_schema("imported_pages"), do: Plausible.Imported.Page
|
defp table_schema("imported_pages"), do: Plausible.Imported.Page
|
||||||
defp table_schema("imported_entry_pages"), do: Plausible.Imported.EntryPage
|
defp table_schema("imported_entry_pages"), do: Plausible.Imported.EntryPage
|
||||||
defp table_schema("imported_exit_pages"), do: Plausible.Imported.ExitPage
|
defp table_schema("imported_exit_pages"), do: Plausible.Imported.ExitPage
|
||||||
|
defp table_schema("imported_custom_events"), do: Plausible.Imported.CustomEvent
|
||||||
defp table_schema("imported_locations"), do: Plausible.Imported.Location
|
defp table_schema("imported_locations"), do: Plausible.Imported.Location
|
||||||
defp table_schema("imported_devices"), do: Plausible.Imported.Device
|
defp table_schema("imported_devices"), do: Plausible.Imported.Device
|
||||||
defp table_schema("imported_browsers"), do: Plausible.Imported.Browser
|
defp table_schema("imported_browsers"), do: Plausible.Imported.Browser
|
||||||
|
15
lib/plausible/imported/custom_event.ex
Normal file
15
lib/plausible/imported/custom_event.ex
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
defmodule Plausible.Imported.CustomEvent do
|
||||||
|
@moduledoc false
|
||||||
|
use Ecto.Schema
|
||||||
|
|
||||||
|
@primary_key false
|
||||||
|
schema "imported_custom_events" do
|
||||||
|
field :site_id, Ch, type: "UInt64"
|
||||||
|
field :import_id, Ch, type: "UInt64"
|
||||||
|
field :date, :date
|
||||||
|
field :name, :string
|
||||||
|
field :link_url, :string
|
||||||
|
field :visitors, Ch, type: "UInt64"
|
||||||
|
field :events, Ch, type: "UInt64"
|
||||||
|
end
|
||||||
|
end
|
@ -149,6 +149,10 @@ defmodule Plausible.Imported.GoogleAnalytics4 do
|
|||||||
round(float)
|
round(float)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp maybe_override_event_name("file_download"), do: "File Download"
|
||||||
|
defp maybe_override_event_name("click"), do: "Outbound Link: Click"
|
||||||
|
defp maybe_override_event_name(name), do: name
|
||||||
|
|
||||||
defp new_from_report(site_id, import_id, "imported_visitors", row) do
|
defp new_from_report(site_id, import_id, "imported_visitors", row) do
|
||||||
%{
|
%{
|
||||||
site_id: site_id,
|
site_id: site_id,
|
||||||
@ -229,6 +233,18 @@ defmodule Plausible.Imported.GoogleAnalytics4 do
|
|||||||
# }
|
# }
|
||||||
# end
|
# end
|
||||||
|
|
||||||
|
defp new_from_report(site_id, import_id, "imported_custom_events", row) do
|
||||||
|
%{
|
||||||
|
site_id: site_id,
|
||||||
|
import_id: import_id,
|
||||||
|
date: get_date(row),
|
||||||
|
name: row.dimensions |> Map.fetch!("eventName") |> maybe_override_event_name(),
|
||||||
|
link_url: row.dimensions |> Map.fetch!("linkUrl"),
|
||||||
|
visitors: row.metrics |> Map.fetch!("totalUsers") |> parse_number(),
|
||||||
|
events: row.metrics |> Map.fetch!("eventCount") |> parse_number()
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
defp new_from_report(site_id, import_id, "imported_locations", row) do
|
defp new_from_report(site_id, import_id, "imported_locations", row) do
|
||||||
country_code = row.dimensions |> Map.fetch!("countryId") |> default_if_missing("")
|
country_code = row.dimensions |> Map.fetch!("countryId") |> default_if_missing("")
|
||||||
city_name = row.dimensions |> Map.fetch!("city") |> default_if_missing("")
|
city_name = row.dimensions |> Map.fetch!("city") |> default_if_missing("")
|
||||||
|
@ -45,7 +45,7 @@ defmodule Plausible.Stats.Aggregate do
|
|||||||
defp aggregate_events(site, query, metrics) do
|
defp aggregate_events(site, query, metrics) do
|
||||||
from(e in base_event_query(site, query), select: ^select_event_metrics(metrics))
|
from(e in base_event_query(site, query), select: ^select_event_metrics(metrics))
|
||||||
|> merge_imported(site, query, metrics)
|
|> merge_imported(site, query, metrics)
|
||||||
|> maybe_add_conversion_rate(site, query, metrics, include_imported: query.include_imported)
|
|> maybe_add_conversion_rate(site, query, metrics)
|
||||||
|> ClickhouseRepo.one()
|
|> ClickhouseRepo.one()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -577,7 +577,7 @@ defmodule Plausible.Stats.Base do
|
|||||||
|> select([e], total_visitors: fragment(@uniq_users_expression, e.user_id))
|
|> select([e], total_visitors: fragment(@uniq_users_expression, e.user_id))
|
||||||
end
|
end
|
||||||
|
|
||||||
defp total_visitors_subquery(site, query, true) do
|
defp total_visitors_subquery(site, %Query{include_imported: true} = query) do
|
||||||
dynamic(
|
dynamic(
|
||||||
[e],
|
[e],
|
||||||
selected_as(
|
selected_as(
|
||||||
@ -588,16 +588,14 @@ defmodule Plausible.Stats.Base do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp total_visitors_subquery(site, query, false) do
|
defp total_visitors_subquery(site, query) do
|
||||||
dynamic([e], selected_as(subquery(total_visitors(site, query)), :__total_visitors))
|
dynamic([e], selected_as(subquery(total_visitors(site, query)), :__total_visitors))
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_percentage_metric(q, site, query, metrics) do
|
def add_percentage_metric(q, site, query, metrics) do
|
||||||
if :percentage in metrics do
|
if :percentage in metrics do
|
||||||
q
|
q
|
||||||
|> select_merge(
|
|> select_merge(^%{__total_visitors: total_visitors_subquery(site, query)})
|
||||||
^%{__total_visitors: total_visitors_subquery(site, query, query.include_imported)}
|
|
||||||
)
|
|
||||||
|> select_merge(%{
|
|> select_merge(%{
|
||||||
percentage:
|
percentage:
|
||||||
fragment(
|
fragment(
|
||||||
@ -615,17 +613,13 @@ defmodule Plausible.Stats.Base do
|
|||||||
# Adds conversion_rate metric to query, calculated as
|
# Adds conversion_rate metric to query, calculated as
|
||||||
# X / Y where Y is the same breakdown value without goal or props
|
# X / Y where Y is the same breakdown value without goal or props
|
||||||
# filters.
|
# filters.
|
||||||
def maybe_add_conversion_rate(q, site, query, metrics, opts) do
|
def maybe_add_conversion_rate(q, site, query, metrics) do
|
||||||
if :conversion_rate in metrics do
|
if :conversion_rate in metrics do
|
||||||
include_imported = Keyword.fetch!(opts, :include_imported)
|
|
||||||
|
|
||||||
total_query = query |> Query.remove_event_filters([:goal, :props])
|
total_query = query |> Query.remove_event_filters([:goal, :props])
|
||||||
|
|
||||||
# :TRICKY: Subquery is used due to event:goal breakdown above doing an UNION ALL
|
# :TRICKY: Subquery is used due to event:goal breakdown above doing an UNION ALL
|
||||||
subquery(q)
|
subquery(q)
|
||||||
|> select_merge(
|
|> select_merge(^%{total_visitors: total_visitors_subquery(site, total_query)})
|
||||||
^%{total_visitors: total_visitors_subquery(site, total_query, include_imported)}
|
|
||||||
)
|
|
||||||
|> select_merge([e], %{
|
|> select_merge([e], %{
|
||||||
conversion_rate:
|
conversion_rate:
|
||||||
fragment(
|
fragment(
|
||||||
|
@ -94,6 +94,7 @@ defmodule Plausible.Stats.Breakdown do
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|> select_merge(^select_columns)
|
|> select_merge(^select_columns)
|
||||||
|
|> merge_imported_pageview_goals(site, query, page_exprs, metrics_to_select)
|
||||||
|> apply_pagination(pagination)
|
|> apply_pagination(pagination)
|
||||||
else
|
else
|
||||||
nil
|
nil
|
||||||
@ -120,7 +121,7 @@ defmodule Plausible.Stats.Breakdown do
|
|||||||
|
|
||||||
if full_q do
|
if full_q do
|
||||||
full_q
|
full_q
|
||||||
|> maybe_add_conversion_rate(site, query, metrics, include_imported: false)
|
|> maybe_add_conversion_rate(site, query, metrics)
|
||||||
|> ClickhouseRepo.all()
|
|> ClickhouseRepo.all()
|
||||||
|> transform_keys(%{name: :goal})
|
|> transform_keys(%{name: :goal})
|
||||||
|> cast_revenue_metrics_to_money(revenue_goals)
|
|> cast_revenue_metrics_to_money(revenue_goals)
|
||||||
@ -149,7 +150,7 @@ defmodule Plausible.Stats.Breakdown do
|
|||||||
if !Keyword.get(opts, :skip_tracing), do: Query.trace(query, metrics)
|
if !Keyword.get(opts, :skip_tracing), do: Query.trace(query, metrics)
|
||||||
|
|
||||||
breakdown_events(site, query, metrics_to_select)
|
breakdown_events(site, query, metrics_to_select)
|
||||||
|> maybe_add_conversion_rate(site, query, metrics, include_imported: false)
|
|> maybe_add_conversion_rate(site, query, metrics)
|
||||||
|> paginate_and_execute(metrics, pagination)
|
|> paginate_and_execute(metrics, pagination)
|
||||||
|> transform_keys(%{breakdown_prop_value: custom_prop})
|
|> transform_keys(%{breakdown_prop_value: custom_prop})
|
||||||
|> Enum.map(&cast_revenue_metrics_to_money(&1, currency))
|
|> Enum.map(&cast_revenue_metrics_to_money(&1, currency))
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
defmodule Plausible.Stats.Imported do
|
defmodule Plausible.Stats.Imported do
|
||||||
use Plausible.ClickhouseRepo
|
use Plausible.ClickhouseRepo
|
||||||
alias Plausible.Stats.Query
|
alias Plausible.Stats.{Query, Base}
|
||||||
|
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
import Plausible.Stats.Fragments
|
import Plausible.Stats.Fragments
|
||||||
@ -26,11 +26,34 @@ defmodule Plausible.Stats.Imported do
|
|||||||
"visit:browser_version" => "imported_browsers",
|
"visit:browser_version" => "imported_browsers",
|
||||||
"visit:os" => "imported_operating_systems",
|
"visit:os" => "imported_operating_systems",
|
||||||
"visit:os_version" => "imported_operating_systems",
|
"visit:os_version" => "imported_operating_systems",
|
||||||
"event:page" => "imported_pages"
|
"event:page" => "imported_pages",
|
||||||
|
"event:name" => "imported_custom_events",
|
||||||
|
"event:props:url" => "imported_custom_events"
|
||||||
}
|
}
|
||||||
|
|
||||||
@imported_properties Map.keys(@property_to_table_mappings)
|
@imported_properties Map.keys(@property_to_table_mappings)
|
||||||
|
|
||||||
|
def schema_supports_query?(query) do
|
||||||
|
filter_count = length(Map.keys(query.filters))
|
||||||
|
|
||||||
|
case {filter_count, query.property} do
|
||||||
|
{0, "event:props:" <> _} -> false
|
||||||
|
{0, _} -> true
|
||||||
|
{1, _} -> supports_single_filter?(query)
|
||||||
|
{_, _} -> false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp supports_single_filter?(%Query{
|
||||||
|
filters: %{"event:goal" => {:is, {:event, event}}},
|
||||||
|
property: "event:props:url"
|
||||||
|
})
|
||||||
|
when event in ["Outbound Link: Click", "File Download"] do
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
defp supports_single_filter?(_query), do: false
|
||||||
|
|
||||||
def merge_imported_timeseries(native_q, _, %Plausible.Stats.Query{include_imported: false}, _),
|
def merge_imported_timeseries(native_q, _, %Plausible.Stats.Query{include_imported: false}, _),
|
||||||
do: native_q
|
do: native_q
|
||||||
|
|
||||||
@ -79,7 +102,6 @@ defmodule Plausible.Stats.Imported do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def merge_imported(q, _, %Query{include_imported: false}, _), do: q
|
def merge_imported(q, _, %Query{include_imported: false}, _), do: q
|
||||||
def merge_imported(q, _, _, [:events | _]), do: q
|
|
||||||
|
|
||||||
def merge_imported(q, site, %Query{property: property} = query, metrics)
|
def merge_imported(q, site, %Query{property: property} = query, metrics)
|
||||||
when property in @imported_properties do
|
when property in @imported_properties do
|
||||||
@ -96,12 +118,15 @@ defmodule Plausible.Stats.Imported do
|
|||||||
where: i.visitors > 0,
|
where: i.visitors > 0,
|
||||||
select: %{}
|
select: %{}
|
||||||
)
|
)
|
||||||
|> maybe_filter_by_breakdown_property(query.filters[property], dim)
|
|> maybe_apply_filter(query.filters, property, dim)
|
||||||
|> group_imported_by(dim)
|
|> group_imported_by(dim)
|
||||||
|> select_imported_metrics(metrics)
|
|> select_imported_metrics(metrics)
|
||||||
|
|
||||||
join_on =
|
join_on =
|
||||||
case dim do
|
case dim do
|
||||||
|
:url ->
|
||||||
|
dynamic([s, i], s.breakdown_prop_value == i.breakdown_prop_value)
|
||||||
|
|
||||||
:os_version ->
|
:os_version ->
|
||||||
dynamic([s, i], s.os == i.os and s.os_version == i.os_version)
|
dynamic([s, i], s.os == i.os and s.os_version == i.os_version)
|
||||||
|
|
||||||
@ -137,6 +162,41 @@ defmodule Plausible.Stats.Imported do
|
|||||||
|
|
||||||
def merge_imported(q, _, _, _), do: q
|
def merge_imported(q, _, _, _), do: q
|
||||||
|
|
||||||
|
def merge_imported_pageview_goals(q, _, %Query{include_imported: false}, _, _), do: q
|
||||||
|
|
||||||
|
def merge_imported_pageview_goals(q, site, query, page_exprs, metrics) do
|
||||||
|
page_regexes = Enum.map(page_exprs, &Base.page_regex/1)
|
||||||
|
|
||||||
|
imported_q =
|
||||||
|
from(
|
||||||
|
i in "imported_pages",
|
||||||
|
where: i.site_id == ^site.id,
|
||||||
|
where: i.import_id in ^site.complete_import_ids,
|
||||||
|
where: i.date >= ^query.date_range.first and i.date <= ^query.date_range.last,
|
||||||
|
where: i.visitors > 0,
|
||||||
|
where:
|
||||||
|
fragment(
|
||||||
|
"notEmpty(multiMatchAllIndices(?, ?) as indices)",
|
||||||
|
i.page,
|
||||||
|
^page_regexes
|
||||||
|
),
|
||||||
|
array_join: index in fragment("indices"),
|
||||||
|
group_by: index,
|
||||||
|
select: %{
|
||||||
|
name: fragment("concat('Visit ', ?[?])", ^page_exprs, index)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|> select_imported_metrics(metrics)
|
||||||
|
|
||||||
|
from(s in Ecto.Query.subquery(q),
|
||||||
|
full_join: i in subquery(imported_q),
|
||||||
|
on: s.name == i.name,
|
||||||
|
select: %{}
|
||||||
|
)
|
||||||
|
|> select_joined_dimension(:name)
|
||||||
|
|> select_joined_metrics(metrics)
|
||||||
|
end
|
||||||
|
|
||||||
def total_imported_visitors(site, query) do
|
def total_imported_visitors(site, query) do
|
||||||
imported_visitors(site, query)
|
imported_visitors(site, query)
|
||||||
|> select_merge([i], %{total_visitors: fragment("sum(?)", i.visitors)})
|
|> select_merge([i], %{total_visitors: fragment("sum(?)", i.visitors)})
|
||||||
@ -154,11 +214,22 @@ defmodule Plausible.Stats.Imported do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_filter_by_breakdown_property(q, {:member, list}, dim) do
|
defp maybe_apply_filter(
|
||||||
where(q, [i], field(i, ^dim) in ^list)
|
q,
|
||||||
|
%{"event:goal" => {:is, {:event, event_name}}},
|
||||||
|
"event:props:url",
|
||||||
|
_dim
|
||||||
|
)
|
||||||
|
when event_name in ["Outbound Link: Click", "File Download"] do
|
||||||
|
where(q, [i], i.name == ^event_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_filter_by_breakdown_property(q, _, _), do: q
|
defp maybe_apply_filter(q, filters, property, dim) do
|
||||||
|
case filters[property] do
|
||||||
|
{:member, list} -> where(q, [i], field(i, ^dim) in ^list)
|
||||||
|
_ -> q
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp select_imported_metrics(q, []), do: q
|
defp select_imported_metrics(q, []), do: q
|
||||||
|
|
||||||
@ -168,6 +239,21 @@ defmodule Plausible.Stats.Imported do
|
|||||||
|> select_imported_metrics(rest)
|
|> select_imported_metrics(rest)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp select_imported_metrics(
|
||||||
|
%Ecto.Query{from: %Ecto.Query.FromExpr{source: {"imported_custom_events", _}}} = q,
|
||||||
|
[:events | rest]
|
||||||
|
) do
|
||||||
|
q
|
||||||
|
|> select_merge([i], %{events: sum(i.events)})
|
||||||
|
|> select_imported_metrics(rest)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp select_imported_metrics(q, [:events | rest]) do
|
||||||
|
q
|
||||||
|
|> select_merge([i], %{events: sum(i.pageviews)})
|
||||||
|
|> select_imported_metrics(rest)
|
||||||
|
end
|
||||||
|
|
||||||
defp select_imported_metrics(
|
defp select_imported_metrics(
|
||||||
%Ecto.Query{from: %Ecto.Query.FromExpr{source: {"imported_exit_pages", _}}} = q,
|
%Ecto.Query{from: %Ecto.Query.FromExpr{source: {"imported_exit_pages", _}}} = q,
|
||||||
[:visits | rest]
|
[:visits | rest]
|
||||||
@ -192,6 +278,15 @@ defmodule Plausible.Stats.Imported do
|
|||||||
|> select_imported_metrics(rest)
|
|> select_imported_metrics(rest)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp select_imported_metrics(
|
||||||
|
%Ecto.Query{from: %Ecto.Query.FromExpr{source: {"imported_custom_events", _}}} = q,
|
||||||
|
[:pageviews | rest]
|
||||||
|
) do
|
||||||
|
q
|
||||||
|
|> select_merge([i], %{pageviews: 0})
|
||||||
|
|> select_imported_metrics(rest)
|
||||||
|
end
|
||||||
|
|
||||||
defp select_imported_metrics(q, [:pageviews | rest]) do
|
defp select_imported_metrics(q, [:pageviews | rest]) do
|
||||||
q
|
q
|
||||||
|> where([i], i.pageviews > 0)
|
|> where([i], i.pageviews > 0)
|
||||||
@ -351,6 +446,18 @@ defmodule Plausible.Stats.Imported do
|
|||||||
|> select_merge([i], %{^dim => field(i, ^dim)})
|
|> select_merge([i], %{^dim => field(i, ^dim)})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp group_imported_by(q, :name) do
|
||||||
|
q
|
||||||
|
|> group_by([i], i.name)
|
||||||
|
|> select_merge([i], %{name: i.name})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp group_imported_by(q, :url) do
|
||||||
|
q
|
||||||
|
|> group_by([i], i.link_url)
|
||||||
|
|> select_merge([i], %{breakdown_prop_value: i.link_url})
|
||||||
|
end
|
||||||
|
|
||||||
defp select_joined_dimension(q, :city) do
|
defp select_joined_dimension(q, :city) do
|
||||||
select_merge(q, [s, i], %{
|
select_merge(q, [s, i], %{
|
||||||
city: fragment("greatest(?,?)", i.city, s.city)
|
city: fragment("greatest(?,?)", i.city, s.city)
|
||||||
@ -372,6 +479,18 @@ defmodule Plausible.Stats.Imported do
|
|||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp select_joined_dimension(q, :url) do
|
||||||
|
select_merge(q, [s, i], %{
|
||||||
|
breakdown_prop_value:
|
||||||
|
fragment(
|
||||||
|
"if(empty(?), ?, ?)",
|
||||||
|
s.breakdown_prop_value,
|
||||||
|
i.breakdown_prop_value,
|
||||||
|
s.breakdown_prop_value
|
||||||
|
)
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
defp select_joined_dimension(q, dim) do
|
defp select_joined_dimension(q, dim) do
|
||||||
select_merge(q, [s, i], %{
|
select_merge(q, [s, i], %{
|
||||||
^dim => fragment("if(empty(?), ?, ?)", field(s, ^dim), field(i, ^dim), field(s, ^dim))
|
^dim => fragment("if(empty(?), ?, ?)", field(s, ^dim), field(i, ^dim), field(s, ^dim))
|
||||||
@ -397,6 +516,12 @@ defmodule Plausible.Stats.Imported do
|
|||||||
|> select_joined_metrics(rest)
|
|> select_joined_metrics(rest)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp select_joined_metrics(q, [:events | rest]) do
|
||||||
|
q
|
||||||
|
|> select_merge([s, i], %{events: s.events + i.events})
|
||||||
|
|> select_joined_metrics(rest)
|
||||||
|
end
|
||||||
|
|
||||||
defp select_joined_metrics(q, [:pageviews | rest]) do
|
defp select_joined_metrics(q, [:pageviews | rest]) do
|
||||||
q
|
q
|
||||||
|> select_merge([s, i], %{pageviews: s.pageviews + i.pageviews})
|
|> select_merge([s, i], %{pageviews: s.pageviews + i.pageviews})
|
||||||
|
@ -14,7 +14,7 @@ defmodule Plausible.Stats.Query do
|
|||||||
experimental_reduced_joins?: false
|
experimental_reduced_joins?: false
|
||||||
|
|
||||||
require OpenTelemetry.Tracer, as: Tracer
|
require OpenTelemetry.Tracer, as: Tracer
|
||||||
alias Plausible.Stats.{Filters, Interval}
|
alias Plausible.Stats.{Filters, Interval, Imported}
|
||||||
|
|
||||||
@type t :: %__MODULE__{}
|
@type t :: %__MODULE__{}
|
||||||
|
|
||||||
@ -273,7 +273,7 @@ defmodule Plausible.Stats.Query do
|
|||||||
cond do
|
cond do
|
||||||
is_nil(site.latest_import_end_date) -> false
|
is_nil(site.latest_import_end_date) -> false
|
||||||
Date.after?(query.date_range.first, site.latest_import_end_date) -> false
|
Date.after?(query.date_range.first, site.latest_import_end_date) -> false
|
||||||
Enum.any?(query.filters) -> false
|
not Imported.schema_supports_query?(query) -> false
|
||||||
query.period == "realtime" -> false
|
query.period == "realtime" -> false
|
||||||
true -> requested?
|
true -> requested?
|
||||||
end
|
end
|
||||||
|
@ -192,15 +192,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp validate_metric("events" = metric, query) do
|
defp validate_metric(metric, _) when metric in ["visitors", "pageviews", "events"] do
|
||||||
if query.include_imported do
|
|
||||||
{:error, "Metric `#{metric}` cannot be queried with imported data"}
|
|
||||||
else
|
|
||||||
{:ok, metric}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp validate_metric(metric, _) when metric in ["visitors", "pageviews"] do
|
|
||||||
{:ok, metric}
|
{:ok, metric}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1204,9 +1204,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
|
|
||||||
params = Map.put(params, "property", prefixed_prop)
|
params = Map.put(params, "property", prefixed_prop)
|
||||||
|
|
||||||
query =
|
query = Query.from(site, params)
|
||||||
Query.from(site, params)
|
|
||||||
|> Map.put(:include_imported, false)
|
|
||||||
|
|
||||||
metrics =
|
metrics =
|
||||||
if query.filters["event:goal"] do
|
if query.filters["event:goal"] do
|
||||||
|
@ -26,6 +26,7 @@ defmodule Plausible.Workers.ClickhouseCleanSites do
|
|||||||
"imported_locations",
|
"imported_locations",
|
||||||
"imported_operating_systems",
|
"imported_operating_systems",
|
||||||
"imported_pages",
|
"imported_pages",
|
||||||
|
"imported_custom_events",
|
||||||
"imported_sources",
|
"imported_sources",
|
||||||
"imported_visitors"
|
"imported_visitors"
|
||||||
]
|
]
|
||||||
|
@ -16,6 +16,7 @@ defmodule Plausible.Imported.GoogleAnalytics4Test do
|
|||||||
"fixture/ga4_report_imported_sources.json",
|
"fixture/ga4_report_imported_sources.json",
|
||||||
"fixture/ga4_report_imported_pages.json",
|
"fixture/ga4_report_imported_pages.json",
|
||||||
"fixture/ga4_report_imported_entry_pages.json",
|
"fixture/ga4_report_imported_entry_pages.json",
|
||||||
|
"fixture/ga4_report_imported_custom_events.json",
|
||||||
"fixture/ga4_report_imported_locations.json",
|
"fixture/ga4_report_imported_locations.json",
|
||||||
"fixture/ga4_report_imported_devices.json",
|
"fixture/ga4_report_imported_devices.json",
|
||||||
"fixture/ga4_report_imported_browsers.json",
|
"fixture/ga4_report_imported_browsers.json",
|
||||||
@ -89,6 +90,7 @@ defmodule Plausible.Imported.GoogleAnalytics4Test do
|
|||||||
"imported_pages" -> 3340
|
"imported_pages" -> 3340
|
||||||
"imported_entry_pages" -> 2934
|
"imported_entry_pages" -> 2934
|
||||||
"imported_exit_pages" -> 0
|
"imported_exit_pages" -> 0
|
||||||
|
"imported_custom_events" -> 56
|
||||||
"imported_locations" -> 2291
|
"imported_locations" -> 2291
|
||||||
"imported_devices" -> 93
|
"imported_devices" -> 93
|
||||||
"imported_browsers" -> 233
|
"imported_browsers" -> 233
|
||||||
@ -122,9 +124,19 @@ defmodule Plausible.Imported.GoogleAnalytics4Test do
|
|||||||
|
|
||||||
conn = put_req_header(conn, "authorization", "Bearer #{api_key}")
|
conn = put_req_header(conn, "authorization", "Bearer #{api_key}")
|
||||||
|
|
||||||
assert_timeseries(conn, common_params)
|
insert(:goal, event_name: "Outbound Link: Click", site: site)
|
||||||
assert_pages(conn, common_params)
|
insert(:goal, event_name: "view_search_results", site: site)
|
||||||
|
insert(:goal, event_name: "scroll", site: site)
|
||||||
|
|
||||||
|
# Timeseries
|
||||||
|
assert_timeseries(conn, common_params)
|
||||||
|
|
||||||
|
# Breakdown (event:*)
|
||||||
|
assert_pages(conn, common_params)
|
||||||
|
assert_custom_events(conn, common_params)
|
||||||
|
assert_outbound_link_urls(conn, common_params)
|
||||||
|
|
||||||
|
# Breakdown (visit:*)
|
||||||
assert_sources(conn, breakdown_params)
|
assert_sources(conn, breakdown_params)
|
||||||
assert_utm_mediums(conn, breakdown_params)
|
assert_utm_mediums(conn, breakdown_params)
|
||||||
assert_entry_pages(conn, breakdown_params)
|
assert_entry_pages(conn, breakdown_params)
|
||||||
@ -133,6 +145,8 @@ defmodule Plausible.Imported.GoogleAnalytics4Test do
|
|||||||
assert_browsers(conn, breakdown_params)
|
assert_browsers(conn, breakdown_params)
|
||||||
assert_os(conn, breakdown_params)
|
assert_os(conn, breakdown_params)
|
||||||
assert_os_versions(conn, breakdown_params)
|
assert_os_versions(conn, breakdown_params)
|
||||||
|
|
||||||
|
# Misc
|
||||||
assert_active_visitors(site_import)
|
assert_active_visitors(site_import)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -202,6 +216,7 @@ defmodule Plausible.Imported.GoogleAnalytics4Test do
|
|||||||
"imported_devices" -> 0
|
"imported_devices" -> 0
|
||||||
"imported_browsers" -> 0
|
"imported_browsers" -> 0
|
||||||
"imported_operating_systems" -> 0
|
"imported_operating_systems" -> 0
|
||||||
|
"imported_custom_events" -> 0
|
||||||
end
|
end
|
||||||
|
|
||||||
query = from(imported in table, where: imported.site_id == ^site.id)
|
query = from(imported in table, where: imported.site_id == ^site.id)
|
||||||
@ -229,6 +244,7 @@ defmodule Plausible.Imported.GoogleAnalytics4Test do
|
|||||||
"imported_devices" -> 93
|
"imported_devices" -> 93
|
||||||
"imported_browsers" -> 233
|
"imported_browsers" -> 233
|
||||||
"imported_operating_systems" -> 1068
|
"imported_operating_systems" -> 1068
|
||||||
|
"imported_custom_events" -> 56
|
||||||
end
|
end
|
||||||
|
|
||||||
query = from(imported in table, where: imported.site_id == ^site.id)
|
query = from(imported in table, where: imported.site_id == ^site.id)
|
||||||
@ -265,6 +281,67 @@ defmodule Plausible.Imported.GoogleAnalytics4Test do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp assert_custom_events(conn, params) do
|
||||||
|
params =
|
||||||
|
params
|
||||||
|
|> Map.put("metrics", "visitors,events,conversion_rate")
|
||||||
|
|> Map.put("property", "event:goal")
|
||||||
|
|
||||||
|
%{"results" => results} =
|
||||||
|
get(conn, "/api/v1/stats/breakdown", params) |> json_response(200)
|
||||||
|
|
||||||
|
assert results == [
|
||||||
|
%{
|
||||||
|
"goal" => "scroll",
|
||||||
|
"visitors" => 1513,
|
||||||
|
"events" => 2130,
|
||||||
|
"conversion_rate" => 24.7
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
"goal" => "Outbound Link: Click",
|
||||||
|
"visitors" => 17,
|
||||||
|
"events" => 17,
|
||||||
|
"conversion_rate" => 0.3
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
"goal" => "view_search_results",
|
||||||
|
"visitors" => 11,
|
||||||
|
"events" => 30,
|
||||||
|
"conversion_rate" => 0.2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp assert_outbound_link_urls(conn, params) do
|
||||||
|
params =
|
||||||
|
Map.merge(params, %{
|
||||||
|
"metrics" => "visitors,events,conversion_rate",
|
||||||
|
"property" => "event:props:url",
|
||||||
|
"filters" => "event:goal==Outbound Link: Click"
|
||||||
|
})
|
||||||
|
|
||||||
|
%{"results" => results} =
|
||||||
|
get(conn, "/api/v1/stats/breakdown", params) |> json_response(200)
|
||||||
|
|
||||||
|
assert length(results) == 10
|
||||||
|
|
||||||
|
assert List.first(results) ==
|
||||||
|
%{
|
||||||
|
"url" => "https://www.facebook.com/kuhinjskeprice",
|
||||||
|
"visitors" => 6,
|
||||||
|
"conversion_rate" => 0.1,
|
||||||
|
"events" => 6
|
||||||
|
}
|
||||||
|
|
||||||
|
assert %{
|
||||||
|
"url" =>
|
||||||
|
"http://www.jamieoliver.com/recipes/pasta-recipes/spinach-ricotta-cannelloni/",
|
||||||
|
"visitors" => 1,
|
||||||
|
"conversion_rate" => 0.0,
|
||||||
|
"events" => 1
|
||||||
|
} in results
|
||||||
|
end
|
||||||
|
|
||||||
defp assert_timeseries(conn, params) do
|
defp assert_timeseries(conn, params) do
|
||||||
params =
|
params =
|
||||||
Map.put(
|
Map.put(
|
||||||
|
@ -30,6 +30,7 @@ defmodule Plausible.PurgeTest do
|
|||||||
build(:imported_pages),
|
build(:imported_pages),
|
||||||
build(:imported_entry_pages),
|
build(:imported_entry_pages),
|
||||||
build(:imported_exit_pages),
|
build(:imported_exit_pages),
|
||||||
|
build(:imported_custom_events),
|
||||||
build(:imported_locations),
|
build(:imported_locations),
|
||||||
build(:imported_devices),
|
build(:imported_devices),
|
||||||
build(:imported_browsers),
|
build(:imported_browsers),
|
||||||
@ -43,6 +44,7 @@ defmodule Plausible.PurgeTest do
|
|||||||
build(:imported_pages),
|
build(:imported_pages),
|
||||||
build(:imported_entry_pages),
|
build(:imported_entry_pages),
|
||||||
build(:imported_exit_pages),
|
build(:imported_exit_pages),
|
||||||
|
build(:imported_custom_events),
|
||||||
build(:imported_locations),
|
build(:imported_locations),
|
||||||
build(:imported_devices),
|
build(:imported_devices),
|
||||||
build(:imported_browsers),
|
build(:imported_browsers),
|
||||||
@ -101,6 +103,7 @@ defmodule Plausible.PurgeTest do
|
|||||||
build(:imported_pages),
|
build(:imported_pages),
|
||||||
build(:imported_entry_pages),
|
build(:imported_entry_pages),
|
||||||
build(:imported_exit_pages),
|
build(:imported_exit_pages),
|
||||||
|
build(:imported_custom_events),
|
||||||
build(:imported_locations),
|
build(:imported_locations),
|
||||||
build(:imported_devices),
|
build(:imported_devices),
|
||||||
build(:imported_browsers),
|
build(:imported_browsers),
|
||||||
|
@ -204,4 +204,79 @@ defmodule Plausible.Stats.QueryTest do
|
|||||||
assert q.filters["visit:source"] == {:is, "Twitter"}
|
assert q.filters["visit:source"] == {:is, "Twitter"}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "include_imported" do
|
||||||
|
setup [:create_site]
|
||||||
|
|
||||||
|
test "is true when requested via params and imported data exists", %{site: site} do
|
||||||
|
insert(:site_import, site: site)
|
||||||
|
site = Plausible.Imported.load_import_data(site)
|
||||||
|
|
||||||
|
assert %{include_imported: true} =
|
||||||
|
Query.from(site, %{"period" => "day", "with_imported" => "true"})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "is false when imported data does not exist", %{site: site} do
|
||||||
|
assert %{include_imported: false} =
|
||||||
|
Query.from(site, %{"period" => "day", "with_imported" => "true"})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "is false when imported data exists but is out of the date range", %{site: site} do
|
||||||
|
insert(:site_import, site: site, start_date: ~D[2021-01-01], end_date: ~D[2022-01-01])
|
||||||
|
site = Plausible.Imported.load_import_data(site)
|
||||||
|
|
||||||
|
assert %{include_imported: false} =
|
||||||
|
Query.from(site, %{"period" => "day", "with_imported" => "true"})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "is false in realtime even when imported data from today exists", %{site: site} do
|
||||||
|
insert(:site_import, site: site)
|
||||||
|
site = Plausible.Imported.load_import_data(site)
|
||||||
|
|
||||||
|
assert %{include_imported: false} =
|
||||||
|
Query.from(site, %{"period" => "realtime", "with_imported" => "true"})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "is false when an arbitrary custom property filter is used", %{site: site} do
|
||||||
|
insert(:site_import, site: site)
|
||||||
|
site = Plausible.Imported.load_import_data(site)
|
||||||
|
|
||||||
|
assert %{include_imported: false} =
|
||||||
|
Query.from(site, %{
|
||||||
|
"period" => "day",
|
||||||
|
"with_imported" => "true",
|
||||||
|
"property" => "event:props:url",
|
||||||
|
"filters" => Jason.encode!(%{"props" => %{"author" => "!John Doe"}})
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "is true when breaking down by url and filtering by outbound link or file download goal",
|
||||||
|
%{site: site} do
|
||||||
|
insert(:site_import, site: site)
|
||||||
|
site = Plausible.Imported.load_import_data(site)
|
||||||
|
|
||||||
|
Enum.each(["Outbound Link: Click", "File Download"], fn goal_name ->
|
||||||
|
assert %{include_imported: true} =
|
||||||
|
Query.from(site, %{
|
||||||
|
"period" => "day",
|
||||||
|
"with_imported" => "true",
|
||||||
|
"property" => "event:props:url",
|
||||||
|
"filters" => Jason.encode!(%{"goal" => goal_name})
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "is false when breaking down by url but without a special goal filter",
|
||||||
|
%{site: site} do
|
||||||
|
insert(:site_import, site: site)
|
||||||
|
site = Plausible.Imported.load_import_data(site)
|
||||||
|
|
||||||
|
assert %{include_imported: false} =
|
||||||
|
Query.from(site, %{
|
||||||
|
"period" => "day",
|
||||||
|
"with_imported" => "true",
|
||||||
|
"property" => "event:props:url"
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -588,28 +588,6 @@ defmodule PlausibleWeb.Api.ExternalStatsController.AggregateTest do
|
|||||||
"visitors" => %{"value" => 1, "change" => 100}
|
"visitors" => %{"value" => 1, "change" => 100}
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "events metric with imported data is disallowed", %{
|
|
||||||
conn: conn,
|
|
||||||
site: site,
|
|
||||||
site_import: site_import
|
|
||||||
} do
|
|
||||||
populate_stats(site, site_import.id, [
|
|
||||||
build(:imported_visitors, date: ~D[2023-01-01])
|
|
||||||
])
|
|
||||||
|
|
||||||
conn =
|
|
||||||
get(conn, "/api/v1/stats/aggregate", %{
|
|
||||||
"site_id" => site.domain,
|
|
||||||
"period" => "day",
|
|
||||||
"date" => "2023-01-02",
|
|
||||||
"metrics" => "events",
|
|
||||||
"with_imported" => "true"
|
|
||||||
})
|
|
||||||
|
|
||||||
assert %{"error" => msg} = json_response(conn, 400)
|
|
||||||
assert msg == "Metric `events` cannot be queried with imported data"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "filters" do
|
describe "filters" do
|
||||||
|
@ -3053,4 +3053,173 @@ defmodule PlausibleWeb.Api.ExternalStatsController.BreakdownTest do
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "imported data" do
|
||||||
|
test "returns custom event goals and pageview goals", %{conn: conn, site: site} do
|
||||||
|
insert(:goal, site: site, event_name: "Purchase")
|
||||||
|
insert(:goal, site: site, page_path: "/test")
|
||||||
|
|
||||||
|
site_import = insert(:site_import, site: site)
|
||||||
|
|
||||||
|
populate_stats(site, site_import.id, [
|
||||||
|
build(:pageview,
|
||||||
|
timestamp: ~N[2021-01-01 00:00:01],
|
||||||
|
pathname: "/test"
|
||||||
|
),
|
||||||
|
build(:event,
|
||||||
|
name: "Purchase",
|
||||||
|
timestamp: ~N[2021-01-01 00:00:03]
|
||||||
|
),
|
||||||
|
build(:event,
|
||||||
|
name: "Purchase",
|
||||||
|
timestamp: ~N[2021-01-01 00:00:03]
|
||||||
|
),
|
||||||
|
build(:imported_custom_events,
|
||||||
|
name: "Purchase",
|
||||||
|
visitors: 3,
|
||||||
|
events: 5,
|
||||||
|
date: ~D[2021-01-01]
|
||||||
|
),
|
||||||
|
build(:imported_pages,
|
||||||
|
page: "/test",
|
||||||
|
visitors: 2,
|
||||||
|
pageviews: 2,
|
||||||
|
date: ~D[2021-01-01]
|
||||||
|
),
|
||||||
|
build(:imported_visitors, visitors: 5, date: ~D[2021-01-01])
|
||||||
|
])
|
||||||
|
|
||||||
|
conn =
|
||||||
|
get(conn, "/api/v1/stats/breakdown", %{
|
||||||
|
"site_id" => site.domain,
|
||||||
|
"period" => "day",
|
||||||
|
"date" => "2021-01-01",
|
||||||
|
"property" => "event:goal",
|
||||||
|
"metrics" => "visitors,events,pageviews,conversion_rate",
|
||||||
|
"with_imported" => "true"
|
||||||
|
})
|
||||||
|
|
||||||
|
assert [
|
||||||
|
%{
|
||||||
|
"goal" => "Purchase",
|
||||||
|
"visitors" => 5,
|
||||||
|
"events" => 7,
|
||||||
|
"pageviews" => 0,
|
||||||
|
"conversion_rate" => 62.5
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
"goal" => "Visit /test",
|
||||||
|
"visitors" => 3,
|
||||||
|
"events" => 3,
|
||||||
|
"pageviews" => 3,
|
||||||
|
"conversion_rate" => 37.5
|
||||||
|
}
|
||||||
|
] = json_response(conn, 200)["results"]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "pageviews are returned as events for breakdown reports other than custom events", %{
|
||||||
|
conn: conn,
|
||||||
|
site: site
|
||||||
|
} do
|
||||||
|
site_import = insert(:site_import, site: site)
|
||||||
|
|
||||||
|
populate_stats(site, site_import.id, [
|
||||||
|
build(:imported_browsers, browser: "Chrome", pageviews: 1, date: ~D[2021-01-01]),
|
||||||
|
build(:imported_devices, device: "Desktop", pageviews: 1, date: ~D[2021-01-01]),
|
||||||
|
build(:imported_entry_pages, entry_page: "/test", pageviews: 1, date: ~D[2021-01-01]),
|
||||||
|
build(:imported_exit_pages, exit_page: "/test", pageviews: 1, date: ~D[2021-01-01]),
|
||||||
|
build(:imported_locations, country: "EE", pageviews: 1, date: ~D[2021-01-01]),
|
||||||
|
build(:imported_operating_systems,
|
||||||
|
operating_system: "Mac",
|
||||||
|
pageviews: 1,
|
||||||
|
date: ~D[2021-01-01]
|
||||||
|
),
|
||||||
|
build(:imported_pages, page: "/test", pageviews: 1, date: ~D[2021-01-01]),
|
||||||
|
build(:imported_sources, source: "Google", pageviews: 1, date: ~D[2021-01-01])
|
||||||
|
])
|
||||||
|
|
||||||
|
params = %{
|
||||||
|
"site_id" => site.domain,
|
||||||
|
"period" => "day",
|
||||||
|
"date" => "2021-01-01",
|
||||||
|
"metrics" => "events",
|
||||||
|
"with_imported" => "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
breakdown_and_first = fn property ->
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/stats/breakdown", Map.put(params, "property", property))
|
||||||
|
|> json_response(200)
|
||||||
|
|> Map.get("results")
|
||||||
|
|> List.first()
|
||||||
|
end
|
||||||
|
|
||||||
|
assert %{"browser" => "Chrome", "events" => 1} = breakdown_and_first.("visit:browser")
|
||||||
|
assert %{"device" => "Desktop", "events" => 1} = breakdown_and_first.("visit:device")
|
||||||
|
assert %{"entry_page" => "/test", "events" => 1} = breakdown_and_first.("visit:entry_page")
|
||||||
|
assert %{"exit_page" => "/test", "events" => 1} = breakdown_and_first.("visit:exit_page")
|
||||||
|
assert %{"country" => "EE", "events" => 1} = breakdown_and_first.("visit:country")
|
||||||
|
assert %{"os" => "Mac", "events" => 1} = breakdown_and_first.("visit:os")
|
||||||
|
assert %{"page" => "/test", "events" => 1} = breakdown_and_first.("event:page")
|
||||||
|
assert %{"source" => "Google", "events" => 1} = breakdown_and_first.("visit:source")
|
||||||
|
end
|
||||||
|
|
||||||
|
for goal_name <- ["Outbound Link: Click", "File Download"] do
|
||||||
|
test "returns url breakdown for #{goal_name} goal", %{conn: conn, site: site} do
|
||||||
|
insert(:goal, event_name: unquote(goal_name), site: site)
|
||||||
|
site_import = insert(:site_import, site: site)
|
||||||
|
|
||||||
|
populate_stats(site, site_import.id, [
|
||||||
|
build(:event,
|
||||||
|
name: unquote(goal_name),
|
||||||
|
"meta.key": ["url"],
|
||||||
|
"meta.value": ["https://one.com"]
|
||||||
|
),
|
||||||
|
build(:imported_custom_events,
|
||||||
|
name: unquote(goal_name),
|
||||||
|
visitors: 2,
|
||||||
|
events: 5,
|
||||||
|
link_url: "https://one.com"
|
||||||
|
),
|
||||||
|
build(:imported_custom_events,
|
||||||
|
name: unquote(goal_name),
|
||||||
|
visitors: 5,
|
||||||
|
events: 10,
|
||||||
|
link_url: "https://two.com"
|
||||||
|
),
|
||||||
|
build(:imported_custom_events,
|
||||||
|
name: "some goal",
|
||||||
|
visitors: 5,
|
||||||
|
events: 10
|
||||||
|
),
|
||||||
|
build(:imported_visitors, visitors: 9)
|
||||||
|
])
|
||||||
|
|
||||||
|
conn =
|
||||||
|
get(conn, "/api/v1/stats/breakdown", %{
|
||||||
|
"site_id" => site.domain,
|
||||||
|
"period" => "day",
|
||||||
|
"property" => "event:props:url",
|
||||||
|
"filters" => "event:goal==#{unquote(goal_name)}",
|
||||||
|
"metrics" => "visitors,events,conversion_rate",
|
||||||
|
"with_imported" => "true"
|
||||||
|
})
|
||||||
|
|
||||||
|
assert json_response(conn, 200)["results"] == [
|
||||||
|
%{
|
||||||
|
"visitors" => 5,
|
||||||
|
"url" => "https://two.com",
|
||||||
|
"events" => 10,
|
||||||
|
"conversion_rate" => 50.0
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
"visitors" => 3,
|
||||||
|
"url" => "https://one.com",
|
||||||
|
"events" => 6,
|
||||||
|
"conversion_rate" => 30.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -1525,4 +1525,32 @@ defmodule PlausibleWeb.Api.ExternalStatsController.TimeseriesTest do
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "imported data" do
|
||||||
|
test "returns pageviews as the value of events metric", %{
|
||||||
|
conn: conn,
|
||||||
|
site: site
|
||||||
|
} do
|
||||||
|
site_import = insert(:site_import, site: site)
|
||||||
|
|
||||||
|
populate_stats(site, site_import.id, [
|
||||||
|
build(:imported_visitors, pageviews: 1, date: ~D[2021-01-01])
|
||||||
|
])
|
||||||
|
|
||||||
|
first_result =
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/stats/timeseries", %{
|
||||||
|
"site_id" => site.domain,
|
||||||
|
"period" => "7d",
|
||||||
|
"metrics" => "events",
|
||||||
|
"date" => "2021-01-07",
|
||||||
|
"with_imported" => "true"
|
||||||
|
})
|
||||||
|
|> json_response(200)
|
||||||
|
|> Map.get("results")
|
||||||
|
|> List.first()
|
||||||
|
|
||||||
|
assert first_result == %{"date" => "2021-01-01", "events" => 1}
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -626,44 +626,6 @@ defmodule PlausibleWeb.Api.StatsController.ConversionsTest do
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "conversion_rate for goals should not be calculated with imported data", %{
|
|
||||||
conn: conn,
|
|
||||||
site: site
|
|
||||||
} do
|
|
||||||
site_import =
|
|
||||||
insert(:site_import,
|
|
||||||
start_date: ~D[2005-01-01],
|
|
||||||
end_date: Timex.today(),
|
|
||||||
source: :universal_analytics
|
|
||||||
)
|
|
||||||
|
|
||||||
populate_stats(site, site_import.id, [
|
|
||||||
build(:pageview, pathname: "/"),
|
|
||||||
build(:pageview, pathname: "/another"),
|
|
||||||
build(:pageview, pathname: "/blog/post-1"),
|
|
||||||
build(:pageview, pathname: "/blog/post-2"),
|
|
||||||
build(:imported_pages, page: "/blog/post-1"),
|
|
||||||
build(:imported_visitors)
|
|
||||||
])
|
|
||||||
|
|
||||||
insert(:goal, %{site: site, page_path: "/blog**"})
|
|
||||||
|
|
||||||
conn =
|
|
||||||
get(
|
|
||||||
conn,
|
|
||||||
"/api/stats/#{site.domain}/conversions?period=day"
|
|
||||||
)
|
|
||||||
|
|
||||||
assert json_response(conn, 200) == [
|
|
||||||
%{
|
|
||||||
"name" => "Visit /blog**",
|
|
||||||
"visitors" => 2,
|
|
||||||
"events" => 2,
|
|
||||||
"conversion_rate" => 50
|
|
||||||
}
|
|
||||||
]
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "GET /api/stats/:domain/conversions - with goal and prop=(none) filter" do
|
describe "GET /api/stats/:domain/conversions - with goal and prop=(none) filter" do
|
||||||
@ -785,4 +747,99 @@ defmodule PlausibleWeb.Api.StatsController.ConversionsTest do
|
|||||||
]
|
]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "GET /api/stats/:domain/conversions - with imported data" do
|
||||||
|
setup [:create_user, :log_in, :create_site]
|
||||||
|
|
||||||
|
test "returns custom event goals and pageview goals", %{conn: conn, site: site} do
|
||||||
|
insert(:goal, site: site, event_name: "Purchase")
|
||||||
|
insert(:goal, site: site, page_path: "/test")
|
||||||
|
|
||||||
|
site_import = insert(:site_import, site: site)
|
||||||
|
|
||||||
|
populate_stats(site, site_import.id, [
|
||||||
|
build(:pageview,
|
||||||
|
timestamp: ~N[2021-01-01 00:00:01],
|
||||||
|
pathname: "/test"
|
||||||
|
),
|
||||||
|
build(:event,
|
||||||
|
name: "Purchase",
|
||||||
|
timestamp: ~N[2021-01-01 00:00:03]
|
||||||
|
),
|
||||||
|
build(:event,
|
||||||
|
name: "Purchase",
|
||||||
|
timestamp: ~N[2021-01-01 00:00:03]
|
||||||
|
),
|
||||||
|
build(:imported_custom_events,
|
||||||
|
name: "Purchase",
|
||||||
|
visitors: 3,
|
||||||
|
events: 5,
|
||||||
|
date: ~D[2021-01-01]
|
||||||
|
),
|
||||||
|
build(:imported_pages,
|
||||||
|
page: "/test",
|
||||||
|
visitors: 2,
|
||||||
|
pageviews: 2,
|
||||||
|
date: ~D[2021-01-01]
|
||||||
|
),
|
||||||
|
build(:imported_visitors, visitors: 5, date: ~D[2021-01-01])
|
||||||
|
])
|
||||||
|
|
||||||
|
url_query_params = "?period=day&date=2021-01-01&with_imported=true"
|
||||||
|
conn = get(conn, "/api/stats/#{site.domain}/conversions#{url_query_params}")
|
||||||
|
|
||||||
|
assert [
|
||||||
|
%{
|
||||||
|
"name" => "Purchase",
|
||||||
|
"visitors" => 5,
|
||||||
|
"events" => 7,
|
||||||
|
"conversion_rate" => 62.5
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
"name" => "Visit /test",
|
||||||
|
"visitors" => 3,
|
||||||
|
"events" => 3,
|
||||||
|
"conversion_rate" => 37.5
|
||||||
|
}
|
||||||
|
] = json_response(conn, 200)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "calculates conversion_rate for goals with glob pattern with imported data", %{
|
||||||
|
conn: conn,
|
||||||
|
site: site
|
||||||
|
} do
|
||||||
|
site_import =
|
||||||
|
insert(:site_import,
|
||||||
|
start_date: ~D[2005-01-01],
|
||||||
|
end_date: Timex.today(),
|
||||||
|
source: :universal_analytics
|
||||||
|
)
|
||||||
|
|
||||||
|
populate_stats(site, site_import.id, [
|
||||||
|
build(:pageview, pathname: "/"),
|
||||||
|
build(:pageview, pathname: "/another"),
|
||||||
|
build(:pageview, pathname: "/blog/post-1"),
|
||||||
|
build(:pageview, pathname: "/blog/post-2"),
|
||||||
|
build(:imported_pages, page: "/blog/post-1"),
|
||||||
|
build(:imported_visitors)
|
||||||
|
])
|
||||||
|
|
||||||
|
insert(:goal, %{site: site, page_path: "/blog**"})
|
||||||
|
|
||||||
|
conn =
|
||||||
|
get(
|
||||||
|
conn,
|
||||||
|
"/api/stats/#{site.domain}/conversions?period=day"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert json_response(conn, 200) == [
|
||||||
|
%{
|
||||||
|
"name" => "Visit /blog**",
|
||||||
|
"visitors" => 2,
|
||||||
|
"events" => 2,
|
||||||
|
"conversion_rate" => 50
|
||||||
|
}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -1132,4 +1132,64 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "with imported data" do
|
||||||
|
setup [:create_user, :log_in, :create_new_site]
|
||||||
|
|
||||||
|
for goal_name <- ["Outbound Link: Click", "File Download"] do
|
||||||
|
test "returns url breakdown for #{goal_name} goal", %{conn: conn, site: site} do
|
||||||
|
insert(:goal, event_name: unquote(goal_name), site: site)
|
||||||
|
site_import = insert(:site_import, site: site)
|
||||||
|
|
||||||
|
populate_stats(site, site_import.id, [
|
||||||
|
build(:event,
|
||||||
|
name: unquote(goal_name),
|
||||||
|
"meta.key": ["url"],
|
||||||
|
"meta.value": ["https://one.com"]
|
||||||
|
),
|
||||||
|
build(:imported_custom_events,
|
||||||
|
name: unquote(goal_name),
|
||||||
|
visitors: 2,
|
||||||
|
events: 5,
|
||||||
|
link_url: "https://one.com"
|
||||||
|
),
|
||||||
|
build(:imported_custom_events,
|
||||||
|
name: unquote(goal_name),
|
||||||
|
visitors: 5,
|
||||||
|
events: 10,
|
||||||
|
link_url: "https://two.com"
|
||||||
|
),
|
||||||
|
build(:imported_custom_events,
|
||||||
|
name: "view_search_results",
|
||||||
|
visitors: 100,
|
||||||
|
events: 200
|
||||||
|
),
|
||||||
|
build(:imported_visitors, visitors: 9)
|
||||||
|
])
|
||||||
|
|
||||||
|
filters = Jason.encode!(%{goal: unquote(goal_name)})
|
||||||
|
|
||||||
|
conn =
|
||||||
|
get(
|
||||||
|
conn,
|
||||||
|
"/api/stats/#{site.domain}/custom-prop-values/url?period=day&with_imported=true&filters=#{filters}"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert json_response(conn, 200) == [
|
||||||
|
%{
|
||||||
|
"visitors" => 5,
|
||||||
|
"name" => "https://two.com",
|
||||||
|
"events" => 10,
|
||||||
|
"conversion_rate" => 50.0
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
"visitors" => 3,
|
||||||
|
"name" => "https://one.com",
|
||||||
|
"events" => 6,
|
||||||
|
"conversion_rate" => 30.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -237,6 +237,17 @@ defmodule Plausible.Factory do
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def imported_custom_events_factory do
|
||||||
|
%{
|
||||||
|
table: "imported_custom_events",
|
||||||
|
date: Timex.today(),
|
||||||
|
name: "",
|
||||||
|
link_url: "",
|
||||||
|
visitors: 1,
|
||||||
|
events: 1
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def imported_locations_factory do
|
def imported_locations_factory do
|
||||||
%{
|
%{
|
||||||
table: "imported_locations",
|
table: "imported_locations",
|
||||||
|
Loading…
Reference in New Issue
Block a user