New private API to return custom prop values (#3111)

* copy prop_breakdown tests into a separate file

* add a new endpoint for custom props

Duplicate existing goal prop breakdown as a building base for the
new endpoint

* stick to original metric names + CR definitions

We currently use CR with two different definitions, which is inconsistent
and should be changed. This commit just documents the difference for the
time being.

* basic prop breakdown without goal filter

* increase % metric precision to one decimal place

* add some tests without goal filter

* silence credo for TODO comment

* use events metric instead of pageviews

* review feedback

* inline add_cr_a instead
This commit is contained in:
RobertJoonas 2023-07-17 18:00:52 +03:00 committed by GitHub
parent e6996ccfda
commit 34fbc3d5bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 1506 additions and 733 deletions

View File

@ -496,7 +496,7 @@ defmodule PlausibleWeb.Api.StatsController do
res =
Stats.breakdown(site, query, "visit:source", metrics, pagination)
|> maybe_add_cr(site, query, pagination, :source, "visit:source")
|> add_cr(site, query, pagination, :source, "visit:source")
|> transform_keys(%{source: :name})
if params["csv"] do
@ -571,7 +571,7 @@ defmodule PlausibleWeb.Api.StatsController do
res =
Stats.breakdown(site, query, "visit:utm_medium", metrics, pagination)
|> maybe_add_cr(site, query, pagination, :utm_medium, "visit:utm_medium")
|> add_cr(site, query, pagination, :utm_medium, "visit:utm_medium")
|> transform_keys(%{utm_medium: :name})
if params["csv"] do
@ -600,7 +600,7 @@ defmodule PlausibleWeb.Api.StatsController do
res =
Stats.breakdown(site, query, "visit:utm_campaign", metrics, pagination)
|> maybe_add_cr(site, query, pagination, :utm_campaign, "visit:utm_campaign")
|> add_cr(site, query, pagination, :utm_campaign, "visit:utm_campaign")
|> transform_keys(%{utm_campaign: :name})
if params["csv"] do
@ -628,7 +628,7 @@ defmodule PlausibleWeb.Api.StatsController do
res =
Stats.breakdown(site, query, "visit:utm_content", metrics, pagination)
|> maybe_add_cr(site, query, pagination, :utm_content, "visit:utm_content")
|> add_cr(site, query, pagination, :utm_content, "visit:utm_content")
|> transform_keys(%{utm_content: :name})
if params["csv"] do
@ -656,7 +656,7 @@ defmodule PlausibleWeb.Api.StatsController do
res =
Stats.breakdown(site, query, "visit:utm_term", metrics, pagination)
|> maybe_add_cr(site, query, pagination, :utm_term, "visit:utm_term")
|> add_cr(site, query, pagination, :utm_term, "visit:utm_term")
|> transform_keys(%{utm_term: :name})
if params["csv"] do
@ -685,7 +685,7 @@ defmodule PlausibleWeb.Api.StatsController do
res =
Stats.breakdown(site, query, "visit:utm_source", metrics, pagination)
|> maybe_add_cr(site, query, pagination, :utm_source, "visit:utm_source")
|> add_cr(site, query, pagination, :utm_source, "visit:utm_source")
|> transform_keys(%{utm_source: :name})
if params["csv"] do
@ -752,7 +752,7 @@ defmodule PlausibleWeb.Api.StatsController do
referrers =
Stats.breakdown(site, query, "visit:referrer", metrics, pagination)
|> maybe_add_cr(site, query, pagination, :referrer, "visit:referrer")
|> add_cr(site, query, pagination, :referrer, "visit:referrer")
|> transform_keys(%{referrer: :name})
|> Enum.map(&Map.drop(&1, [:visits]))
@ -773,7 +773,7 @@ defmodule PlausibleWeb.Api.StatsController do
pages =
Stats.breakdown(site, query, "event:page", metrics, pagination)
|> maybe_add_cr(site, query, pagination, :page, "event:page")
|> add_cr(site, query, pagination, :page, "event:page")
|> transform_keys(%{page: :name})
if params["csv"] do
@ -797,7 +797,7 @@ defmodule PlausibleWeb.Api.StatsController do
entry_pages =
Stats.breakdown(site, query, "visit:entry_page", metrics, pagination)
|> maybe_add_cr(site, query, pagination, :entry_page, "visit:entry_page")
|> add_cr(site, query, pagination, :entry_page, "visit:entry_page")
|> transform_keys(%{
entry_page: :name,
visitors: :unique_entrances,
@ -825,7 +825,7 @@ defmodule PlausibleWeb.Api.StatsController do
exit_pages =
Stats.breakdown(site, query, "visit:exit_page", metrics, {limit, page})
|> maybe_add_cr(site, query, {limit, page}, :exit_page, "visit:exit_page")
|> add_cr(site, query, {limit, page}, :exit_page, "visit:exit_page")
|> transform_keys(%{
exit_page: :name,
visitors: :unique_exits,
@ -879,9 +879,9 @@ defmodule PlausibleWeb.Api.StatsController do
countries =
Stats.breakdown(site, query, "visit:country", [:visitors], pagination)
|> maybe_add_cr(site, query, {300, 1}, :country, "visit:country")
|> add_cr(site, query, {300, 1}, :country, "visit:country")
|> transform_keys(%{country: :code})
|> maybe_add_percentages(query)
|> add_percentages(query)
if params["csv"] do
countries =
@ -1002,9 +1002,9 @@ defmodule PlausibleWeb.Api.StatsController do
browsers =
Stats.breakdown(site, query, "visit:browser", [:visitors], pagination)
|> maybe_add_cr(site, query, pagination, :browser, "visit:browser")
|> add_cr(site, query, pagination, :browser, "visit:browser")
|> transform_keys(%{browser: :name})
|> maybe_add_percentages(query)
|> add_percentages(query)
if params["csv"] do
if Map.has_key?(query.filters, "event:goal") do
@ -1026,9 +1026,9 @@ defmodule PlausibleWeb.Api.StatsController do
versions =
Stats.breakdown(site, query, "visit:browser_version", [:visitors], pagination)
|> maybe_add_cr(site, query, pagination, :browser_version, "visit:browser_version")
|> add_cr(site, query, pagination, :browser_version, "visit:browser_version")
|> transform_keys(%{browser_version: :name})
|> maybe_add_percentages(query)
|> add_percentages(query)
json(conn, versions)
end
@ -1040,9 +1040,9 @@ defmodule PlausibleWeb.Api.StatsController do
systems =
Stats.breakdown(site, query, "visit:os", [:visitors], pagination)
|> maybe_add_cr(site, query, pagination, :os, "visit:os")
|> add_cr(site, query, pagination, :os, "visit:os")
|> transform_keys(%{os: :name})
|> maybe_add_percentages(query)
|> add_percentages(query)
if params["csv"] do
if Map.has_key?(query.filters, "event:goal") do
@ -1064,9 +1064,9 @@ defmodule PlausibleWeb.Api.StatsController do
versions =
Stats.breakdown(site, query, "visit:os_version", [:visitors], pagination)
|> maybe_add_cr(site, query, pagination, :os_version, "visit:os_version")
|> add_cr(site, query, pagination, :os_version, "visit:os_version")
|> transform_keys(%{os_version: :name})
|> maybe_add_percentages(query)
|> add_percentages(query)
json(conn, versions)
end
@ -1078,9 +1078,9 @@ defmodule PlausibleWeb.Api.StatsController do
sizes =
Stats.breakdown(site, query, "visit:device", [:visitors], pagination)
|> maybe_add_cr(site, query, pagination, :device, "visit:device")
|> add_cr(site, query, pagination, :device, "visit:device")
|> transform_keys(%{device: :name})
|> maybe_add_percentages(query)
|> add_percentages(query)
if params["csv"] do
if Map.has_key?(query.filters, "event:goal") do
@ -1166,6 +1166,34 @@ defmodule PlausibleWeb.Api.StatsController do
end
end
def custom_prop_values(conn, params) do
site = conn.assigns[:site]
query = Query.from(site, params) |> Filters.add_prefix()
pagination = parse_pagination(params)
total_q = Query.remove_event_filters(query, [:goal, :props])
%{:visitors => %{value: total_unique_visitors}} = Stats.aggregate(site, total_q, [:visitors])
prefixed_prop = "event:props:" <> params["prop_key"]
props =
Stats.breakdown(site, query, prefixed_prop, [:visitors, :events], pagination)
|> transform_keys(%{params["prop_key"] => :name})
|> add_percentages(query)
props =
if Map.has_key?(query.filters, "event:goal") do
Enum.map(props, fn prop ->
Map.put(prop, :conversion_rate, calculate_cr(total_unique_visitors, prop.visitors))
end)
else
props
end
json(conn, props)
end
def prop_breakdown(conn, params) do
site = conn.assigns[:site]
query = Query.from(site, params) |> Filters.add_prefix()
@ -1271,49 +1299,46 @@ defmodule PlausibleWeb.Api.StatsController do
defp to_int(_, default), do: default
defp maybe_add_percentages(stat_list, query) do
if Map.has_key?(query.filters, "event:goal") do
stat_list
else
total = Enum.reduce(stat_list, 0, fn %{visitors: count}, total -> total + count end)
Enum.map(stat_list, fn stat ->
Map.put(stat, :percentage, round(stat[:visitors] / total * 100))
end)
end
defp add_percentages([_ | _] = breakdown_result, query)
when not is_map_key(query.filters, "event:goal") do
total = Enum.reduce(breakdown_result, 0, fn %{visitors: count}, total -> total + count end)
do_add_percentages(breakdown_result, total)
end
defp add_cr(list, list_without_goals, key_name) do
Enum.map(list, fn item ->
without_goal = Enum.find(list_without_goals, fn s -> s[key_name] === item[key_name] end)
defp add_percentages(breakdown_result, _), do: breakdown_result
item
|> Map.put(:total_visitors, without_goal[:visitors])
|> Map.put(:conversion_rate, calculate_cr(without_goal[:visitors], item[:visitors]))
defp do_add_percentages(stat_list, total) do
Enum.map(stat_list, fn stat ->
Map.put(stat, :percentage, Float.round(stat.visitors / total * 100, 1))
end)
end
defp maybe_add_cr([], _site, _query, _pagination, _key_name, _filter_name), do: []
defp add_cr([_ | _] = breakdown_results, site, query, pagination, key_name, filter_name)
when is_map_key(query.filters, "event:goal") do
items = Enum.map(breakdown_results, fn item -> Map.fetch!(item, key_name) end)
defp maybe_add_cr(list, site, query, pagination, key_name, filter_name) do
if Map.has_key?(query.filters, "event:goal") do
items = Enum.map(list, fn item -> item[key_name] end)
query_without_goal =
query
|> Query.put_filter(filter_name, {:member, items})
|> Query.remove_event_filters([:goal, :props])
query_without_goal =
query
|> Query.put_filter(filter_name, {:member, items})
|> Query.remove_event_filters([:goal, :props])
res_without_goal =
Stats.breakdown(site, query_without_goal, filter_name, [:visitors], pagination)
res_without_goal =
Stats.breakdown(site, query_without_goal, filter_name, [:visitors], pagination)
Enum.map(breakdown_results, fn item ->
without_goal =
Enum.find(res_without_goal, fn s ->
Map.fetch!(s, key_name) == Map.fetch!(item, key_name)
end)
list
|> add_cr(res_without_goal, key_name)
else
list
end
item
|> Map.put(:total_visitors, without_goal.visitors)
|> Map.put(:conversion_rate, calculate_cr(without_goal.visitors, item.visitors))
end)
end
defp add_cr(breakdown_results, _, _, _, _, _), do: breakdown_results
defp to_csv(list, headers) do
list
|> Enum.map(fn row -> Enum.map(headers, &row[&1]) end)

View File

@ -88,6 +88,7 @@ defmodule PlausibleWeb.Router do
get "/:domain/operating-system-versions", StatsController, :operating_system_versions
get "/:domain/screen-sizes", StatsController, :screen_sizes
get "/:domain/conversions", StatsController, :conversions
get "/:domain/custom-prop-values/:prop_key", StatsController, :custom_prop_values
get "/:domain/property/:prop_name", StatsController, :prop_breakdown
get "/:domain/suggestions/:filter_name", StatsController, :filter_suggestions
end

View File

@ -988,9 +988,9 @@ defmodule Plausible.ImportedTest do
assert stats = json_response(conn, 200)
assert length(stats) == 3
assert %{"name" => "Firefox", "visitors" => 2, "percentage" => 50} in stats
assert %{"name" => "Mobile App", "visitors" => 1, "percentage" => 25} in stats
assert %{"name" => "Chrome", "visitors" => 1, "percentage" => 25} in stats
assert %{"name" => "Firefox", "visitors" => 2, "percentage" => 50.0} in stats
assert %{"name" => "Mobile App", "visitors" => 1, "percentage" => 25.0} in stats
assert %{"name" => "Chrome", "visitors" => 1, "percentage" => 25.0} in stats
end
test "OS data imported from Google Analytics", %{conn: conn, site: site} do

View File

@ -14,8 +14,8 @@ defmodule PlausibleWeb.Api.StatsController.BrowsersTest do
conn = get(conn, "/api/stats/#{site.domain}/browsers?period=day")
assert json_response(conn, 200) == [
%{"name" => "Chrome", "visitors" => 2, "percentage" => 67},
%{"name" => "Firefox", "visitors" => 1, "percentage" => 33}
%{"name" => "Chrome", "visitors" => 2, "percentage" => 66.7},
%{"name" => "Firefox", "visitors" => 1, "percentage" => 33.3}
]
end
@ -125,8 +125,8 @@ defmodule PlausibleWeb.Api.StatsController.BrowsersTest do
conn = get(conn, "/api/stats/#{site.domain}/browsers?period=day&with_imported=true")
assert json_response(conn, 200) == [
%{"name" => "Chrome", "visitors" => 2, "percentage" => 67},
%{"name" => "Firefox", "visitors" => 1, "percentage" => 33}
%{"name" => "Chrome", "visitors" => 2, "percentage" => 66.7},
%{"name" => "Firefox", "visitors" => 1, "percentage" => 33.3}
]
end
@ -162,7 +162,7 @@ defmodule PlausibleWeb.Api.StatsController.BrowsersTest do
conn = get(conn, "/api/stats/#{site.domain}/browsers?period=day")
assert json_response(conn, 200) == [
%{"name" => "(not set)", "visitors" => 1, "percentage" => 100}
%{"name" => "(not set)", "visitors" => 1, "percentage" => 100.0}
]
end
end
@ -187,8 +187,8 @@ defmodule PlausibleWeb.Api.StatsController.BrowsersTest do
)
assert json_response(conn, 200) == [
%{"name" => "78.0", "visitors" => 2, "percentage" => 67},
%{"name" => "77.0", "visitors" => 1, "percentage" => 33}
%{"name" => "78.0", "visitors" => 2, "percentage" => 66.7},
%{"name" => "77.0", "visitors" => 1, "percentage" => 33.3}
]
end

View File

@ -813,662 +813,6 @@ defmodule PlausibleWeb.Api.StatsController.ConversionsTest do
end
end
describe "GET /api/stats/:domain/property/:key" do
setup [:create_user, :log_in, :create_new_site]
test "returns property breakdown for goal", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview, pathname: "/"),
build(:pageview, pathname: "/"),
build(:pageview, pathname: "/register"),
build(:event, name: "Signup", "meta.key": ["variant"], "meta.value": ["A"]),
build(:event, name: "Signup", "meta.key": ["variant"], "meta.value": ["B"]),
build(:event, name: "Signup", "meta.key": ["variant"], "meta.value": ["B"])
])
insert(:goal, %{site: site, event_name: "Signup"})
filters = Jason.encode!(%{goal: "Signup"})
prop_key = "variant"
conn =
get(
conn,
"/api/stats/#{site.domain}/property/#{prop_key}?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"unique_conversions" => 2,
"name" => "B",
"total_conversions" => 2,
"conversion_rate" => 33.3
},
%{
"unique_conversions" => 1,
"name" => "A",
"total_conversions" => 1,
"conversion_rate" => 16.7
}
]
end
test "returns property breakdown for revenue goal", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview, pathname: "/"),
build(:pageview, pathname: "/"),
build(:pageview, pathname: "/register"),
build(:event,
name: "Purchase",
revenue_reporting_amount: Decimal.new("132.21"),
revenue_reporting_currency: "EUR",
"meta.key": ["method"],
"meta.value": ["card"]
),
build(:event,
name: "Purchase",
revenue_reporting_amount: Decimal.new("412.30"),
revenue_reporting_currency: "EUR",
"meta.key": ["method"],
"meta.value": ["cash"]
),
build(:event,
name: "Purchase",
revenue_reporting_amount: Decimal.new("30.23"),
revenue_reporting_currency: "EUR",
"meta.key": ["method"],
"meta.value": ["cash"]
)
])
insert(:goal, site: site, event_name: "Purchase", currency: "EUR")
filters = Jason.encode!(%{goal: "Purchase"})
prop_key = "method"
conn =
get(
conn,
"/api/stats/#{site.domain}/property/#{prop_key}?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"average_revenue" => %{"long" => "€221.26", "short" => "€221.3"},
"conversion_rate" => 33.3,
"name" => "cash",
"total_conversions" => 2,
"total_revenue" => %{"long" => "€442.53", "short" => "€442.5"},
"unique_conversions" => 2
},
%{
"average_revenue" => %{"long" => "€132.21", "short" => "€132.2"},
"conversion_rate" => 16.7,
"name" => "card",
"total_conversions" => 1,
"total_revenue" => %{"long" => "€132.21", "short" => "€132.2"},
"unique_conversions" => 1
}
]
end
test "returns (none) values in property breakdown for goal", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview, pathname: "/"),
build(:pageview, pathname: "/"),
build(:pageview, pathname: "/register"),
build(:event, name: "Signup"),
build(:event, name: "Signup"),
build(:event, name: "Signup", "meta.key": ["variant"], "meta.value": ["A"])
])
insert(:goal, %{site: site, event_name: "Signup"})
filters = Jason.encode!(%{goal: "Signup"})
prop_key = "variant"
conn =
get(
conn,
"/api/stats/#{site.domain}/property/#{prop_key}?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"unique_conversions" => 2,
"name" => "(none)",
"total_conversions" => 2,
"conversion_rate" => 33.3
},
%{
"unique_conversions" => 1,
"name" => "A",
"total_conversions" => 1,
"conversion_rate" => 16.7
}
]
end
test "does not return (none) value in property breakdown with is filter on prop_value", %{
conn: conn,
site: site
} do
populate_stats(site, [
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["0"]
),
build(:event, name: "Purchase")
])
insert(:goal, %{site: site, event_name: "Purchase"})
filters =
Jason.encode!(%{
goal: "Purchase",
props: %{cost: "0"}
})
conn =
get(
conn,
"/api/stats/#{site.domain}/property/cost?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"name" => "0",
"unique_conversions" => 1,
"total_conversions" => 1,
"conversion_rate" => 50.0
}
]
end
test "returns only (none) value in property breakdown with is (none) filter", %{
conn: conn,
site: site
} do
populate_stats(site, [
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["0"]
),
build(:event, name: "Purchase")
])
insert(:goal, %{site: site, event_name: "Purchase"})
filters =
Jason.encode!(%{
goal: "Purchase",
props: %{cost: "(none)"}
})
conn =
get(
conn,
"/api/stats/#{site.domain}/property/cost?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"name" => "(none)",
"unique_conversions" => 1,
"total_conversions" => 1,
"conversion_rate" => 50.0
}
]
end
test "returns (none) value in property breakdown with is_not filter on prop_value", %{
conn: conn,
site: site
} do
populate_stats(site, [
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["0"]
),
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["20"]
),
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["20"]
),
build(:event, name: "Purchase")
])
insert(:goal, %{site: site, event_name: "Purchase"})
filters =
Jason.encode!(%{
goal: "Purchase",
props: %{cost: "!0"}
})
conn =
get(
conn,
"/api/stats/#{site.domain}/property/cost?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"name" => "20",
"unique_conversions" => 2,
"total_conversions" => 2,
"conversion_rate" => 50.0
},
%{
"name" => "(none)",
"unique_conversions" => 1,
"total_conversions" => 1,
"conversion_rate" => 25.0
}
]
end
test "does not return (none) value in property breakdown with is_not (none) filter", %{
conn: conn,
site: site
} do
populate_stats(site, [
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["0"]
),
build(:event, name: "Purchase")
])
insert(:goal, %{site: site, event_name: "Purchase"})
filters =
Jason.encode!(%{
goal: "Purchase",
props: %{cost: "!(none)"}
})
conn =
get(
conn,
"/api/stats/#{site.domain}/property/cost?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"name" => "0",
"unique_conversions" => 1,
"total_conversions" => 1,
"conversion_rate" => 50.0
}
]
end
test "does not return (none) value in property breakdown with member filter on prop_value", %{
conn: conn,
site: site
} do
populate_stats(site, [
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["0"]
),
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["1"]
),
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["1"]
),
build(:event, name: "Purchase")
])
insert(:goal, %{site: site, event_name: "Purchase"})
filters =
Jason.encode!(%{
goal: "Purchase",
props: %{cost: "0|1"}
})
conn =
get(
conn,
"/api/stats/#{site.domain}/property/cost?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"name" => "1",
"unique_conversions" => 2,
"total_conversions" => 2,
"conversion_rate" => 50.0
},
%{
"name" => "0",
"unique_conversions" => 1,
"total_conversions" => 1,
"conversion_rate" => 25.0
}
]
end
test "returns (none) value in property breakdown with member filter including a (none) value",
%{conn: conn, site: site} do
populate_stats(site, [
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["0"]
),
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["1"]
),
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["1"]
),
build(:event, name: "Purchase")
])
insert(:goal, %{site: site, event_name: "Purchase"})
filters =
Jason.encode!(%{
goal: "Purchase",
props: %{cost: "1|(none)"}
})
conn =
get(
conn,
"/api/stats/#{site.domain}/property/cost?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"name" => "1",
"unique_conversions" => 2,
"total_conversions" => 2,
"conversion_rate" => 50.0
},
%{
"name" => "(none)",
"unique_conversions" => 1,
"total_conversions" => 1,
"conversion_rate" => 25.0
}
]
end
test "returns (none) value in property breakdown with not_member filter on prop_value", %{
conn: conn,
site: site
} do
populate_stats(site, [
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["0"]
),
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["0.01"]
),
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["20"]
),
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["20"]
),
build(:event, name: "Purchase")
])
insert(:goal, %{site: site, event_name: "Purchase"})
filters =
Jason.encode!(%{
goal: "Purchase",
props: %{cost: "!0|0.01"}
})
conn =
get(
conn,
"/api/stats/#{site.domain}/property/cost?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"name" => "20",
"unique_conversions" => 2,
"total_conversions" => 2,
"conversion_rate" => 40.0
},
%{
"name" => "(none)",
"unique_conversions" => 1,
"total_conversions" => 1,
"conversion_rate" => 20.0
}
]
end
test "does not return (none) value in property breakdown with not_member filter including a (none) value",
%{conn: conn, site: site} do
populate_stats(site, [
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["0"]
),
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["20"]
),
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["20"]
),
build(:event, name: "Purchase")
])
insert(:goal, %{site: site, event_name: "Purchase"})
filters =
Jason.encode!(%{
goal: "Purchase",
props: %{cost: "!0|(none)"}
})
conn =
get(
conn,
"/api/stats/#{site.domain}/property/cost?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"name" => "20",
"unique_conversions" => 2,
"total_conversions" => 2,
"conversion_rate" => 50.0
}
]
end
test "returns property breakdown with a pageview goal filter", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview, pathname: "/"),
build(:pageview, pathname: "/register"),
build(:pageview, pathname: "/register", "meta.key": ["variant"], "meta.value": ["A"]),
build(:pageview, pathname: "/register", "meta.key": ["variant"], "meta.value": ["A"])
])
insert(:goal, %{site: site, page_path: "/register"})
filters = Jason.encode!(%{goal: "Visit /register"})
conn =
get(
conn,
"/api/stats/#{site.domain}/property/variant?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"unique_conversions" => 2,
"name" => "A",
"total_conversions" => 2,
"conversion_rate" => 50.0
},
%{
"unique_conversions" => 1,
"name" => "(none)",
"total_conversions" => 1,
"conversion_rate" => 25.0
}
]
end
test "property breakdown with prop filter", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview, user_id: 1),
build(:event, user_id: 1, name: "Signup", "meta.key": ["variant"], "meta.value": ["A"]),
build(:pageview, user_id: 2),
build(:event, user_id: 2, name: "Signup", "meta.key": ["variant"], "meta.value": ["B"])
])
insert(:goal, %{site: site, event_name: "Signup"})
filters = Jason.encode!(%{goal: "Signup", props: %{"variant" => "B"}})
prop_key = "variant"
conn =
get(
conn,
"/api/stats/#{site.domain}/property/#{prop_key}?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"unique_conversions" => 1,
"name" => "B",
"total_conversions" => 1,
"conversion_rate" => 50.0
}
]
end
test "Property breakdown with prop and goal filter", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview, user_id: 1, utm_campaign: "campaignA"),
build(:event,
user_id: 1,
name: "ButtonClick",
"meta.key": ["variant"],
"meta.value": ["A"]
),
build(:pageview, user_id: 2, utm_campaign: "campaignA"),
build(:event,
user_id: 2,
name: "ButtonClick",
"meta.key": ["variant"],
"meta.value": ["B"]
)
])
insert(:goal, %{site: site, event_name: "ButtonClick"})
filters =
Jason.encode!(%{
goal: "ButtonClick",
props: %{variant: "A"},
utm_campaign: "campaignA"
})
prop_key = "variant"
conn =
get(
conn,
"/api/stats/#{site.domain}/property/#{prop_key}?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"name" => "A",
"unique_conversions" => 1,
"total_conversions" => 1,
"conversion_rate" => 50.0
}
]
end
test "Property breakdown with goal and source filter", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview, user_id: 1, referrer_source: "Google"),
build(:event,
user_id: 1,
name: "ButtonClick",
"meta.key": ["variant"],
"meta.value": ["A"]
),
build(:pageview, user_id: 2, referrer_source: "Google"),
build(:pageview, user_id: 3, referrer_source: "ignore"),
build(:event,
user_id: 3,
name: "ButtonClick",
"meta.key": ["variant"],
"meta.value": ["B"]
)
])
insert(:goal, %{site: site, event_name: "ButtonClick"})
filters =
Jason.encode!(%{
goal: "ButtonClick",
source: "Google"
})
prop_key = "variant"
conn =
get(
conn,
"/api/stats/#{site.domain}/property/#{prop_key}?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"name" => "A",
"unique_conversions" => 1,
"total_conversions" => 1,
"conversion_rate" => 50.0
}
]
end
end
describe "GET /api/stats/:domain/conversions - with glob goals" do
setup [:create_user, :log_in, :create_site]

View File

@ -32,7 +32,7 @@ defmodule PlausibleWeb.Api.StatsController.CountriesTest do
"name" => "Estonia",
"flag" => "🇪🇪",
"visitors" => 2,
"percentage" => 67
"percentage" => 66.7
},
%{
"code" => "GB",
@ -40,7 +40,7 @@ defmodule PlausibleWeb.Api.StatsController.CountriesTest do
"name" => "United Kingdom",
"flag" => "🇬🇧",
"visitors" => 1,
"percentage" => 33
"percentage" => 33.3
}
]

View File

@ -0,0 +1,795 @@
defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do
use PlausibleWeb.ConnCase
describe "GET /api/stats/:domain/custom-prop-values/:prop_key" do
setup [:create_user, :log_in, :create_new_site]
test "returns breakdown by a custom property", %{conn: conn, site: site} do
prop_key = "parim_s6ber"
populate_stats(site, [
build(:pageview, "meta.key": [prop_key], "meta.value": ["K2sna Kalle"]),
build(:pageview, "meta.key": [prop_key], "meta.value": ["K2sna Kalle"]),
build(:pageview, user_id: 123, "meta.key": [prop_key], "meta.value": ["Lotte"]),
build(:pageview, user_id: 123, "meta.key": [prop_key], "meta.value": ["Lotte"]),
build(:pageview, "meta.key": [prop_key], "meta.value": ["Sipsik"])
])
conn =
get(
conn,
"/api/stats/#{site.domain}/custom-prop-values/#{prop_key}?period=day"
)
assert json_response(conn, 200) == [
%{
"visitors" => 2,
"name" => "K2sna Kalle",
"events" => 2,
"percentage" => 50.0
},
%{
"visitors" => 1,
"name" => "Lotte",
"events" => 2,
"percentage" => 25.0
},
%{
"visitors" => 1,
"name" => "Sipsik",
"events" => 1,
"percentage" => 25.0
}
]
end
test "returns (none) values in the breakdown", %{conn: conn, site: site} do
prop_key = "parim_s6ber"
populate_stats(site, [
build(:pageview, "meta.key": [prop_key], "meta.value": ["K2sna Kalle"]),
build(:pageview, "meta.key": [prop_key], "meta.value": ["K2sna Kalle"]),
build(:pageview)
])
conn =
get(
conn,
"/api/stats/#{site.domain}/custom-prop-values/#{prop_key}?period=day"
)
assert json_response(conn, 200) == [
%{
"visitors" => 2,
"name" => "K2sna Kalle",
"events" => 2,
"percentage" => 66.7
},
%{
"visitors" => 1,
"name" => "(none)",
"events" => 1,
"percentage" => 33.3
}
]
end
end
describe "GET /api/stats/:domain/custom-prop-values/:prop_key - with goal filter" do
setup [:create_user, :log_in, :create_new_site]
test "returns property breakdown for goal", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview, pathname: "/"),
build(:pageview, pathname: "/"),
build(:pageview, pathname: "/register"),
build(:event, name: "Signup", "meta.key": ["variant"], "meta.value": ["A"]),
build(:event, name: "Signup", "meta.key": ["variant"], "meta.value": ["B"]),
build(:event, name: "Signup", "meta.key": ["variant"], "meta.value": ["B"])
])
insert(:goal, %{site: site, event_name: "Signup"})
filters = Jason.encode!(%{goal: "Signup"})
prop_key = "variant"
conn =
get(
conn,
"/api/stats/#{site.domain}/custom-prop-values/#{prop_key}?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"visitors" => 2,
"name" => "B",
"events" => 2,
"conversion_rate" => 33.3
},
%{
"visitors" => 1,
"name" => "A",
"events" => 1,
"conversion_rate" => 16.7
}
]
end
test "returns (none) values in property breakdown for goal", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview, pathname: "/"),
build(:pageview, pathname: "/"),
build(:pageview, pathname: "/register"),
build(:event, name: "Signup"),
build(:event, name: "Signup"),
build(:event, name: "Signup", "meta.key": ["variant"], "meta.value": ["A"])
])
insert(:goal, %{site: site, event_name: "Signup"})
filters = Jason.encode!(%{goal: "Signup"})
prop_key = "variant"
conn =
get(
conn,
"/api/stats/#{site.domain}/custom-prop-values/#{prop_key}?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"visitors" => 2,
"name" => "(none)",
"events" => 2,
"conversion_rate" => 33.3
},
%{
"visitors" => 1,
"name" => "A",
"events" => 1,
"conversion_rate" => 16.7
}
]
end
test "does not return (none) value in property breakdown with is filter on prop_value", %{
conn: conn,
site: site
} do
populate_stats(site, [
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["0"]
),
build(:event, name: "Purchase")
])
insert(:goal, %{site: site, event_name: "Purchase"})
filters =
Jason.encode!(%{
goal: "Purchase",
props: %{cost: "0"}
})
conn =
get(
conn,
"/api/stats/#{site.domain}/custom-prop-values/cost?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"name" => "0",
"visitors" => 1,
"events" => 1,
"conversion_rate" => 50.0
}
]
end
test "returns only (none) value in property breakdown with is (none) filter", %{
conn: conn,
site: site
} do
populate_stats(site, [
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["0"]
),
build(:event, name: "Purchase")
])
insert(:goal, %{site: site, event_name: "Purchase"})
filters =
Jason.encode!(%{
goal: "Purchase",
props: %{cost: "(none)"}
})
conn =
get(
conn,
"/api/stats/#{site.domain}/custom-prop-values/cost?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"name" => "(none)",
"visitors" => 1,
"events" => 1,
"conversion_rate" => 50.0
}
]
end
test "returns (none) value in property breakdown with is_not filter on prop_value", %{
conn: conn,
site: site
} do
populate_stats(site, [
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["0"]
),
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["20"]
),
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["20"]
),
build(:event, name: "Purchase")
])
insert(:goal, %{site: site, event_name: "Purchase"})
filters =
Jason.encode!(%{
goal: "Purchase",
props: %{cost: "!0"}
})
conn =
get(
conn,
"/api/stats/#{site.domain}/custom-prop-values/cost?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"name" => "20",
"visitors" => 2,
"events" => 2,
"conversion_rate" => 50.0
},
%{
"name" => "(none)",
"visitors" => 1,
"events" => 1,
"conversion_rate" => 25.0
}
]
end
test "does not return (none) value in property breakdown with is_not (none) filter", %{
conn: conn,
site: site
} do
populate_stats(site, [
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["0"]
),
build(:event, name: "Purchase")
])
insert(:goal, %{site: site, event_name: "Purchase"})
filters =
Jason.encode!(%{
goal: "Purchase",
props: %{cost: "!(none)"}
})
conn =
get(
conn,
"/api/stats/#{site.domain}/custom-prop-values/cost?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"name" => "0",
"visitors" => 1,
"events" => 1,
"conversion_rate" => 50.0
}
]
end
test "does not return (none) value in property breakdown with member filter on prop_value", %{
conn: conn,
site: site
} do
populate_stats(site, [
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["0"]
),
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["1"]
),
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["1"]
),
build(:event, name: "Purchase")
])
insert(:goal, %{site: site, event_name: "Purchase"})
filters =
Jason.encode!(%{
goal: "Purchase",
props: %{cost: "0|1"}
})
conn =
get(
conn,
"/api/stats/#{site.domain}/custom-prop-values/cost?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"name" => "1",
"visitors" => 2,
"events" => 2,
"conversion_rate" => 50.0
},
%{
"name" => "0",
"visitors" => 1,
"events" => 1,
"conversion_rate" => 25.0
}
]
end
test "returns (none) value in property breakdown with member filter including a (none) value",
%{conn: conn, site: site} do
populate_stats(site, [
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["0"]
),
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["1"]
),
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["1"]
),
build(:event, name: "Purchase")
])
insert(:goal, %{site: site, event_name: "Purchase"})
filters =
Jason.encode!(%{
goal: "Purchase",
props: %{cost: "1|(none)"}
})
conn =
get(
conn,
"/api/stats/#{site.domain}/custom-prop-values/cost?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"name" => "1",
"visitors" => 2,
"events" => 2,
"conversion_rate" => 50.0
},
%{
"name" => "(none)",
"visitors" => 1,
"events" => 1,
"conversion_rate" => 25.0
}
]
end
test "returns (none) value in property breakdown with not_member filter on prop_value", %{
conn: conn,
site: site
} do
populate_stats(site, [
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["0"]
),
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["0.01"]
),
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["20"]
),
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["20"]
),
build(:event, name: "Purchase")
])
insert(:goal, %{site: site, event_name: "Purchase"})
filters =
Jason.encode!(%{
goal: "Purchase",
props: %{cost: "!0|0.01"}
})
conn =
get(
conn,
"/api/stats/#{site.domain}/custom-prop-values/cost?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"name" => "20",
"visitors" => 2,
"events" => 2,
"conversion_rate" => 40.0
},
%{
"name" => "(none)",
"visitors" => 1,
"events" => 1,
"conversion_rate" => 20.0
}
]
end
test "does not return (none) value in property breakdown with not_member filter including a (none) value",
%{conn: conn, site: site} do
populate_stats(site, [
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["0"]
),
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["20"]
),
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["20"]
),
build(:event, name: "Purchase")
])
insert(:goal, %{site: site, event_name: "Purchase"})
filters =
Jason.encode!(%{
goal: "Purchase",
props: %{cost: "!0|(none)"}
})
conn =
get(
conn,
"/api/stats/#{site.domain}/custom-prop-values/cost?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"name" => "20",
"visitors" => 2,
"events" => 2,
"conversion_rate" => 50.0
}
]
end
test "returns property breakdown with a pageview goal filter", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview, pathname: "/"),
build(:pageview, pathname: "/register"),
build(:pageview, pathname: "/register", "meta.key": ["variant"], "meta.value": ["A"]),
build(:pageview, pathname: "/register", "meta.key": ["variant"], "meta.value": ["A"])
])
insert(:goal, %{site: site, page_path: "/register"})
filters = Jason.encode!(%{goal: "Visit /register"})
conn =
get(
conn,
"/api/stats/#{site.domain}/custom-prop-values/variant?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"visitors" => 2,
"name" => "A",
"events" => 2,
"conversion_rate" => 50.0
},
%{
"visitors" => 1,
"name" => "(none)",
"events" => 1,
"conversion_rate" => 25.0
}
]
end
test "property breakdown with prop filter", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview, user_id: 1),
build(:event, user_id: 1, name: "Signup", "meta.key": ["variant"], "meta.value": ["A"]),
build(:pageview, user_id: 2),
build(:event, user_id: 2, name: "Signup", "meta.key": ["variant"], "meta.value": ["B"])
])
insert(:goal, %{site: site, event_name: "Signup"})
filters = Jason.encode!(%{goal: "Signup", props: %{"variant" => "B"}})
prop_key = "variant"
conn =
get(
conn,
"/api/stats/#{site.domain}/custom-prop-values/#{prop_key}?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"visitors" => 1,
"name" => "B",
"events" => 1,
"conversion_rate" => 50.0
}
]
end
test "Property breakdown with prop and goal filter", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview, user_id: 1, utm_campaign: "campaignA"),
build(:event,
user_id: 1,
name: "ButtonClick",
"meta.key": ["variant"],
"meta.value": ["A"]
),
build(:pageview, user_id: 2, utm_campaign: "campaignA"),
build(:event,
user_id: 2,
name: "ButtonClick",
"meta.key": ["variant"],
"meta.value": ["B"]
)
])
insert(:goal, %{site: site, event_name: "ButtonClick"})
filters =
Jason.encode!(%{
goal: "ButtonClick",
props: %{variant: "A"},
utm_campaign: "campaignA"
})
prop_key = "variant"
conn =
get(
conn,
"/api/stats/#{site.domain}/custom-prop-values/#{prop_key}?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"name" => "A",
"visitors" => 1,
"events" => 1,
"conversion_rate" => 50.0
}
]
end
test "Property breakdown with goal and source filter", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview, user_id: 1, referrer_source: "Google"),
build(:event,
user_id: 1,
name: "ButtonClick",
"meta.key": ["variant"],
"meta.value": ["A"]
),
build(:pageview, user_id: 2, referrer_source: "Google"),
build(:pageview, user_id: 3, referrer_source: "ignore"),
build(:event,
user_id: 3,
name: "ButtonClick",
"meta.key": ["variant"],
"meta.value": ["B"]
)
])
insert(:goal, %{site: site, event_name: "ButtonClick"})
filters =
Jason.encode!(%{
goal: "ButtonClick",
source: "Google"
})
prop_key = "variant"
conn =
get(
conn,
"/api/stats/#{site.domain}/custom-prop-values/#{prop_key}?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"name" => "A",
"visitors" => 1,
"events" => 1,
"conversion_rate" => 50.0
}
]
end
end
describe "GET /api/stats/:domain/custom-prop-values/:prop_key - other filters" do
setup [:create_user, :log_in, :create_new_site]
test "returns prop-breakdown with a page filter", %{conn: conn, site: site} do
prop_key = "parim_s6ber"
populate_stats(site, [
build(:pageview, "meta.key": [prop_key], "meta.value": ["K2sna Kalle"]),
build(:pageview, pathname: "/sipsik", "meta.key": [prop_key], "meta.value": ["Sipsik"])
])
filters = Jason.encode!(%{page: "/sipsik"})
conn =
get(
conn,
"/api/stats/#{site.domain}/custom-prop-values/#{prop_key}?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"visitors" => 1,
"name" => "Sipsik",
"events" => 1,
"percentage" => 100.0
}
]
end
test "returns prop-breakdown with a session-level filter", %{conn: conn, site: site} do
prop_key = "parim_s6ber"
populate_stats(site, [
build(:pageview, "meta.key": [prop_key], "meta.value": ["K2sna Kalle"]),
build(:pageview, browser: "Chrome", "meta.key": [prop_key], "meta.value": ["Sipsik"])
])
filters = Jason.encode!(%{browser: "Chrome"})
conn =
get(
conn,
"/api/stats/#{site.domain}/custom-prop-values/#{prop_key}?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"visitors" => 1,
"name" => "Sipsik",
"events" => 1,
"percentage" => 100.0
}
]
end
test "returns prop-breakdown with a prop_value filter", %{conn: conn, site: site} do
prop_key = "parim_s6ber"
populate_stats(site, [
build(:pageview, "meta.key": [prop_key], "meta.value": ["K2sna Kalle"]),
build(:pageview, "meta.key": [prop_key], "meta.value": ["K2sna Kalle"]),
build(:pageview, "meta.key": [prop_key], "meta.value": ["Sipsik"])
])
filters = Jason.encode!(%{props: %{parim_s6ber: "Sipsik"}})
conn =
get(
conn,
"/api/stats/#{site.domain}/custom-prop-values/#{prop_key}?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"visitors" => 1,
"name" => "Sipsik",
"events" => 1,
"percentage" => 100.0
}
]
end
test "returns prop-breakdown with a prop_value is_not (none) filter", %{
conn: conn,
site: site
} do
prop_key = "parim_s6ber"
populate_stats(site, [
build(:pageview, "meta.key": [prop_key], "meta.value": ["K2sna Kalle"]),
build(:pageview, "meta.key": [prop_key], "meta.value": ["K2sna Kalle"]),
build(:pageview, "meta.key": [prop_key], "meta.value": ["Sipsik"]),
build(:pageview)
])
filters = Jason.encode!(%{props: %{parim_s6ber: "!(none)"}})
conn =
get(
conn,
"/api/stats/#{site.domain}/custom-prop-values/#{prop_key}?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"visitors" => 2,
"name" => "K2sna Kalle",
"events" => 2,
"percentage" => 66.7
},
%{
"visitors" => 1,
"name" => "Sipsik",
"events" => 1,
"percentage" => 33.3
}
]
end
end
end

View File

@ -14,8 +14,8 @@ defmodule PlausibleWeb.Api.StatsController.OperatingSystemsTest do
conn = get(conn, "/api/stats/#{site.domain}/operating-systems?period=day")
assert json_response(conn, 200) == [
%{"name" => "Mac", "visitors" => 2, "percentage" => 67},
%{"name" => "Android", "visitors" => 1, "percentage" => 33}
%{"name" => "Mac", "visitors" => 2, "percentage" => 66.7},
%{"name" => "Android", "visitors" => 1, "percentage" => 33.3}
]
end
@ -156,8 +156,8 @@ defmodule PlausibleWeb.Api.StatsController.OperatingSystemsTest do
conn = get(conn, "/api/stats/#{site.domain}/operating-systems?period=day")
assert json_response(conn, 200) == [
%{"name" => "Mac", "visitors" => 2, "percentage" => 67},
%{"name" => "Android", "visitors" => 1, "percentage" => 33}
%{"name" => "Mac", "visitors" => 2, "percentage" => 66.7},
%{"name" => "Android", "visitors" => 1, "percentage" => 33.3}
]
conn =
@ -213,8 +213,8 @@ defmodule PlausibleWeb.Api.StatsController.OperatingSystemsTest do
)
assert json_response(conn, 200) == [
%{"name" => "10.16", "visitors" => 2, "percentage" => 67},
%{"name" => "10.15", "visitors" => 1, "percentage" => 33}
%{"name" => "10.16", "visitors" => 2, "percentage" => 66.7},
%{"name" => "10.15", "visitors" => 1, "percentage" => 33.3}
]
end
end

View File

@ -0,0 +1,608 @@
defmodule PlausibleWeb.Api.StatsController.PropBreakdownTest do
use PlausibleWeb.ConnCase
# credo:disable-for-next-line
# TODO: Remove this file once the new Properties feature is live.
# All tests in this file were copied into `custom_prop_breakdown_test.exs`
# testing the exact same features for the new
# `/api/stats/:domain/custom-prop-values/:key` route.
describe "GET /api/stats/:domain/property/:key" do
setup [:create_user, :log_in, :create_new_site]
test "returns property breakdown for goal", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview, pathname: "/"),
build(:pageview, pathname: "/"),
build(:pageview, pathname: "/register"),
build(:event, name: "Signup", "meta.key": ["variant"], "meta.value": ["A"]),
build(:event, name: "Signup", "meta.key": ["variant"], "meta.value": ["B"]),
build(:event, name: "Signup", "meta.key": ["variant"], "meta.value": ["B"])
])
insert(:goal, %{site: site, event_name: "Signup"})
filters = Jason.encode!(%{goal: "Signup"})
prop_key = "variant"
conn =
get(
conn,
"/api/stats/#{site.domain}/property/#{prop_key}?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"unique_conversions" => 2,
"name" => "B",
"total_conversions" => 2,
"conversion_rate" => 33.3
},
%{
"unique_conversions" => 1,
"name" => "A",
"total_conversions" => 1,
"conversion_rate" => 16.7
}
]
end
test "returns (none) values in property breakdown for goal", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview, pathname: "/"),
build(:pageview, pathname: "/"),
build(:pageview, pathname: "/register"),
build(:event, name: "Signup"),
build(:event, name: "Signup"),
build(:event, name: "Signup", "meta.key": ["variant"], "meta.value": ["A"])
])
insert(:goal, %{site: site, event_name: "Signup"})
filters = Jason.encode!(%{goal: "Signup"})
prop_key = "variant"
conn =
get(
conn,
"/api/stats/#{site.domain}/property/#{prop_key}?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"unique_conversions" => 2,
"name" => "(none)",
"total_conversions" => 2,
"conversion_rate" => 33.3
},
%{
"unique_conversions" => 1,
"name" => "A",
"total_conversions" => 1,
"conversion_rate" => 16.7
}
]
end
test "does not return (none) value in property breakdown with is filter on prop_value", %{
conn: conn,
site: site
} do
populate_stats(site, [
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["0"]
),
build(:event, name: "Purchase")
])
insert(:goal, %{site: site, event_name: "Purchase"})
filters =
Jason.encode!(%{
goal: "Purchase",
props: %{cost: "0"}
})
conn =
get(
conn,
"/api/stats/#{site.domain}/property/cost?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"name" => "0",
"unique_conversions" => 1,
"total_conversions" => 1,
"conversion_rate" => 50.0
}
]
end
test "returns only (none) value in property breakdown with is (none) filter", %{
conn: conn,
site: site
} do
populate_stats(site, [
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["0"]
),
build(:event, name: "Purchase")
])
insert(:goal, %{site: site, event_name: "Purchase"})
filters =
Jason.encode!(%{
goal: "Purchase",
props: %{cost: "(none)"}
})
conn =
get(
conn,
"/api/stats/#{site.domain}/property/cost?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"name" => "(none)",
"unique_conversions" => 1,
"total_conversions" => 1,
"conversion_rate" => 50.0
}
]
end
test "returns (none) value in property breakdown with is_not filter on prop_value", %{
conn: conn,
site: site
} do
populate_stats(site, [
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["0"]
),
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["20"]
),
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["20"]
),
build(:event, name: "Purchase")
])
insert(:goal, %{site: site, event_name: "Purchase"})
filters =
Jason.encode!(%{
goal: "Purchase",
props: %{cost: "!0"}
})
conn =
get(
conn,
"/api/stats/#{site.domain}/property/cost?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"name" => "20",
"unique_conversions" => 2,
"total_conversions" => 2,
"conversion_rate" => 50.0
},
%{
"name" => "(none)",
"unique_conversions" => 1,
"total_conversions" => 1,
"conversion_rate" => 25.0
}
]
end
test "does not return (none) value in property breakdown with is_not (none) filter", %{
conn: conn,
site: site
} do
populate_stats(site, [
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["0"]
),
build(:event, name: "Purchase")
])
insert(:goal, %{site: site, event_name: "Purchase"})
filters =
Jason.encode!(%{
goal: "Purchase",
props: %{cost: "!(none)"}
})
conn =
get(
conn,
"/api/stats/#{site.domain}/property/cost?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"name" => "0",
"unique_conversions" => 1,
"total_conversions" => 1,
"conversion_rate" => 50.0
}
]
end
test "does not return (none) value in property breakdown with member filter on prop_value", %{
conn: conn,
site: site
} do
populate_stats(site, [
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["0"]
),
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["1"]
),
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["1"]
),
build(:event, name: "Purchase")
])
insert(:goal, %{site: site, event_name: "Purchase"})
filters =
Jason.encode!(%{
goal: "Purchase",
props: %{cost: "0|1"}
})
conn =
get(
conn,
"/api/stats/#{site.domain}/property/cost?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"name" => "1",
"unique_conversions" => 2,
"total_conversions" => 2,
"conversion_rate" => 50.0
},
%{
"name" => "0",
"unique_conversions" => 1,
"total_conversions" => 1,
"conversion_rate" => 25.0
}
]
end
test "returns (none) value in property breakdown with member filter including a (none) value",
%{conn: conn, site: site} do
populate_stats(site, [
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["0"]
),
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["1"]
),
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["1"]
),
build(:event, name: "Purchase")
])
insert(:goal, %{site: site, event_name: "Purchase"})
filters =
Jason.encode!(%{
goal: "Purchase",
props: %{cost: "1|(none)"}
})
conn =
get(
conn,
"/api/stats/#{site.domain}/property/cost?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"name" => "1",
"unique_conversions" => 2,
"total_conversions" => 2,
"conversion_rate" => 50.0
},
%{
"name" => "(none)",
"unique_conversions" => 1,
"total_conversions" => 1,
"conversion_rate" => 25.0
}
]
end
test "returns (none) value in property breakdown with not_member filter on prop_value", %{
conn: conn,
site: site
} do
populate_stats(site, [
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["0"]
),
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["0.01"]
),
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["20"]
),
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["20"]
),
build(:event, name: "Purchase")
])
insert(:goal, %{site: site, event_name: "Purchase"})
filters =
Jason.encode!(%{
goal: "Purchase",
props: %{cost: "!0|0.01"}
})
conn =
get(
conn,
"/api/stats/#{site.domain}/property/cost?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"name" => "20",
"unique_conversions" => 2,
"total_conversions" => 2,
"conversion_rate" => 40.0
},
%{
"name" => "(none)",
"unique_conversions" => 1,
"total_conversions" => 1,
"conversion_rate" => 20.0
}
]
end
test "does not return (none) value in property breakdown with not_member filter including a (none) value",
%{conn: conn, site: site} do
populate_stats(site, [
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["0"]
),
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["20"]
),
build(:event,
name: "Purchase",
"meta.key": ["cost"],
"meta.value": ["20"]
),
build(:event, name: "Purchase")
])
insert(:goal, %{site: site, event_name: "Purchase"})
filters =
Jason.encode!(%{
goal: "Purchase",
props: %{cost: "!0|(none)"}
})
conn =
get(
conn,
"/api/stats/#{site.domain}/property/cost?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"name" => "20",
"unique_conversions" => 2,
"total_conversions" => 2,
"conversion_rate" => 50.0
}
]
end
test "returns property breakdown with a pageview goal filter", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview, pathname: "/"),
build(:pageview, pathname: "/register"),
build(:pageview, pathname: "/register", "meta.key": ["variant"], "meta.value": ["A"]),
build(:pageview, pathname: "/register", "meta.key": ["variant"], "meta.value": ["A"])
])
insert(:goal, %{site: site, page_path: "/register"})
filters = Jason.encode!(%{goal: "Visit /register"})
conn =
get(
conn,
"/api/stats/#{site.domain}/property/variant?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"unique_conversions" => 2,
"name" => "A",
"total_conversions" => 2,
"conversion_rate" => 50.0
},
%{
"unique_conversions" => 1,
"name" => "(none)",
"total_conversions" => 1,
"conversion_rate" => 25.0
}
]
end
test "property breakdown with prop filter", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview, user_id: 1),
build(:event, user_id: 1, name: "Signup", "meta.key": ["variant"], "meta.value": ["A"]),
build(:pageview, user_id: 2),
build(:event, user_id: 2, name: "Signup", "meta.key": ["variant"], "meta.value": ["B"])
])
insert(:goal, %{site: site, event_name: "Signup"})
filters = Jason.encode!(%{goal: "Signup", props: %{"variant" => "B"}})
prop_key = "variant"
conn =
get(
conn,
"/api/stats/#{site.domain}/property/#{prop_key}?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"unique_conversions" => 1,
"name" => "B",
"total_conversions" => 1,
"conversion_rate" => 50.0
}
]
end
test "Property breakdown with prop and goal filter", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview, user_id: 1, utm_campaign: "campaignA"),
build(:event,
user_id: 1,
name: "ButtonClick",
"meta.key": ["variant"],
"meta.value": ["A"]
),
build(:pageview, user_id: 2, utm_campaign: "campaignA"),
build(:event,
user_id: 2,
name: "ButtonClick",
"meta.key": ["variant"],
"meta.value": ["B"]
)
])
insert(:goal, %{site: site, event_name: "ButtonClick"})
filters =
Jason.encode!(%{
goal: "ButtonClick",
props: %{variant: "A"},
utm_campaign: "campaignA"
})
prop_key = "variant"
conn =
get(
conn,
"/api/stats/#{site.domain}/property/#{prop_key}?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"name" => "A",
"unique_conversions" => 1,
"total_conversions" => 1,
"conversion_rate" => 50.0
}
]
end
test "Property breakdown with goal and source filter", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview, user_id: 1, referrer_source: "Google"),
build(:event,
user_id: 1,
name: "ButtonClick",
"meta.key": ["variant"],
"meta.value": ["A"]
),
build(:pageview, user_id: 2, referrer_source: "Google"),
build(:pageview, user_id: 3, referrer_source: "ignore"),
build(:event,
user_id: 3,
name: "ButtonClick",
"meta.key": ["variant"],
"meta.value": ["B"]
)
])
insert(:goal, %{site: site, event_name: "ButtonClick"})
filters =
Jason.encode!(%{
goal: "ButtonClick",
source: "Google"
})
prop_key = "variant"
conn =
get(
conn,
"/api/stats/#{site.domain}/property/#{prop_key}?period=day&filters=#{filters}"
)
assert json_response(conn, 200) == [
%{
"name" => "A",
"unique_conversions" => 1,
"total_conversions" => 1,
"conversion_rate" => 50.0
}
]
end
end
end

View File

@ -14,8 +14,8 @@ defmodule PlausibleWeb.Api.StatsController.ScreenSizesTest do
conn = get(conn, "/api/stats/#{site.domain}/screen-sizes?period=day")
assert json_response(conn, 200) == [
%{"name" => "Desktop", "visitors" => 2, "percentage" => 67},
%{"name" => "Laptop", "visitors" => 1, "percentage" => 33}
%{"name" => "Desktop", "visitors" => 2, "percentage" => 66.7},
%{"name" => "Laptop", "visitors" => 1, "percentage" => 33.3}
]
end
@ -130,8 +130,8 @@ defmodule PlausibleWeb.Api.StatsController.ScreenSizesTest do
conn = get(conn, "/api/stats/#{site.domain}/screen-sizes?period=day")
assert json_response(conn, 200) == [
%{"name" => "Desktop", "visitors" => 2, "percentage" => 67},
%{"name" => "Laptop", "visitors" => 1, "percentage" => 33}
%{"name" => "Desktop", "visitors" => 2, "percentage" => 66.7},
%{"name" => "Laptop", "visitors" => 1, "percentage" => 33.3}
]
conn = get(conn, "/api/stats/#{site.domain}/screen-sizes?period=day&with_imported=true")
@ -178,8 +178,8 @@ defmodule PlausibleWeb.Api.StatsController.ScreenSizesTest do
conn = get(conn, "/api/stats/#{site.domain}/screen-sizes?period=day&filters=#{filters}")
assert json_response(conn, 200) == [
%{"name" => "Desktop", "visitors" => 2, "percentage" => 67},
%{"name" => "Mobile", "visitors" => 1, "percentage" => 33}
%{"name" => "Desktop", "visitors" => 2, "percentage" => 66.7},
%{"name" => "Mobile", "visitors" => 1, "percentage" => 33.3}
]
end
end