* Reapply "Goals with custom props (Stats API queries, funnels) (#5936)" (#5943) This reverts commit45116bda7b. * Gracefully handle `nil` for existing goal.custom_props * Revert "Gracefully handle `nil` for existing goal.custom_props" This reverts commit8e38748775. * Migration: make `goals.custom_props` non-null * Adjust test
This commit is contained in:
parent
c78ddf6ba4
commit
38381195f8
|
|
@ -19,18 +19,32 @@ defmodule Plausible.Goal do
|
|||
field :funnels, {:array, :map}, virtual: true, default: []
|
||||
end
|
||||
|
||||
field :custom_props, :map, default: %{}
|
||||
|
||||
belongs_to :site, Plausible.Site
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@fields [:id, :site_id, :event_name, :page_path, :scroll_threshold, :display_name] ++
|
||||
@fields [
|
||||
:id,
|
||||
:site_id,
|
||||
:event_name,
|
||||
:page_path,
|
||||
:scroll_threshold,
|
||||
:display_name,
|
||||
:custom_props
|
||||
] ++
|
||||
on_ee(do: [:currency], else: [])
|
||||
|
||||
@max_event_name_length 120
|
||||
|
||||
def max_event_name_length(), do: @max_event_name_length
|
||||
|
||||
@max_custom_props_per_goal 3
|
||||
|
||||
def max_custom_props_per_goal(), do: @max_custom_props_per_goal
|
||||
|
||||
def changeset(goal, attrs \\ %{}) do
|
||||
goal
|
||||
|> cast(attrs, @fields)
|
||||
|
|
@ -40,16 +54,18 @@ defmodule Plausible.Goal do
|
|||
|> validate_event_name_and_page_path()
|
||||
|> validate_page_path_for_scroll_goal()
|
||||
|> maybe_put_display_name()
|
||||
|> unique_constraint(:event_name, name: :goals_event_name_unique)
|
||||
|> validate_change(:custom_props, fn :custom_props, custom_props ->
|
||||
if map_size(custom_props) > @max_custom_props_per_goal do
|
||||
[custom_props: "use at most #{@max_custom_props_per_goal} properties per goal"]
|
||||
else
|
||||
[]
|
||||
end
|
||||
end)
|
||||
|> unique_constraint(:display_name, name: :goals_display_name_unique)
|
||||
|> unique_constraint(:event_name, name: :goals_event_config_unique)
|
||||
|> unique_constraint([:page_path, :scroll_threshold],
|
||||
name: :goals_pageview_config_unique
|
||||
)
|
||||
|> unique_constraint([:page_path, :scroll_threshold],
|
||||
name: :goals_page_path_and_scroll_threshold_unique
|
||||
)
|
||||
|> unique_constraint(:display_name, name: :goals_display_name_unique)
|
||||
|> unique_constraint(:event_name, name: :goals_event_config_unique)
|
||||
|> unique_constraint(:display_name, name: :goals_site_id_display_name_index)
|
||||
|> validate_length(:event_name, max: @max_event_name_length)
|
||||
|> validate_number(:scroll_threshold,
|
||||
greater_than_or_equal_to: -1,
|
||||
|
|
@ -17,6 +17,8 @@ defmodule Plausible.Goals do
|
|||
if override do
|
||||
override
|
||||
else
|
||||
# see: config/test.exs - you can steer this limit for tests
|
||||
# by providing `max_goals_per_site` option to e.g. create/3
|
||||
Application.get_env(:plausible, :max_goals_per_site, @max_goals_per_site)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -67,7 +67,9 @@ defmodule Plausible.Stats.Goals do
|
|||
event_names_imports: [String.t()],
|
||||
event_names_by_type: [String.t()],
|
||||
page_regexes: [String.t()],
|
||||
scroll_thresholds: [non_neg_integer()]
|
||||
scroll_thresholds: [non_neg_integer()],
|
||||
custom_props_keys: [[String.t()]],
|
||||
custom_props_values: [[String.t()]]
|
||||
}
|
||||
|
||||
@doc """
|
||||
|
|
@ -77,33 +79,50 @@ defmodule Plausible.Stats.Goals do
|
|||
def goal_join_data(query) do
|
||||
goals = query.preloaded_goals.matching_toplevel_filters
|
||||
|
||||
%{
|
||||
indices: Enum.with_index(goals, 1) |> Enum.map(fn {_goal, idx} -> idx end),
|
||||
types: Enum.map(goals, &to_string(Plausible.Goal.type(&1))),
|
||||
# :TRICKY: This will contain "" for non-event goals
|
||||
event_names_imports: Enum.map(goals, &to_string(&1.event_name)),
|
||||
event_names_by_type:
|
||||
Enum.map(goals, fn goal ->
|
||||
case Plausible.Goal.type(goal) do
|
||||
:event -> goal.event_name
|
||||
:page -> "pageview"
|
||||
:scroll -> "engagement"
|
||||
end
|
||||
end),
|
||||
# :TRICKY: event goals are considered to match everything for the sake of efficient queries in query_builder.ex
|
||||
# See also Plausible.Stats.SQL.Expression#event_goal_join
|
||||
page_regexes:
|
||||
Enum.map(goals, fn goal ->
|
||||
case Plausible.Goal.type(goal) do
|
||||
:event -> ".?"
|
||||
:page -> Filters.Utils.page_regex(goal.page_path)
|
||||
:scroll -> Filters.Utils.page_regex(goal.page_path)
|
||||
end
|
||||
end),
|
||||
scroll_thresholds: Enum.map(goals, & &1.scroll_threshold)
|
||||
}
|
||||
goals
|
||||
|> Enum.with_index(1)
|
||||
|> Enum.reduce(
|
||||
%{
|
||||
indices: [],
|
||||
types: [],
|
||||
event_names_imports: [],
|
||||
event_names_by_type: [],
|
||||
page_regexes: [],
|
||||
scroll_thresholds: [],
|
||||
custom_props_keys: [],
|
||||
custom_props_values: []
|
||||
},
|
||||
fn {goal, idx}, acc ->
|
||||
goal_type = Plausible.Goal.type(goal)
|
||||
{prop_keys, prop_values} = Enum.unzip(goal.custom_props)
|
||||
|
||||
%{
|
||||
indices: [idx | acc.indices],
|
||||
types: [to_string(goal_type) | acc.types],
|
||||
# This will contain "" for non-event goals
|
||||
event_names_imports: [to_string(goal.event_name) | acc.event_names_imports],
|
||||
event_names_by_type: [event_name_by_type(goal_type, goal) | acc.event_names_by_type],
|
||||
# Event goals are considered to match everything for the sake of efficient queries in query_builder.ex
|
||||
# See also Plausible.Stats.SQL.Expression.event_goal_join/1
|
||||
page_regexes: [page_regex_for_goal(goal_type, goal) | acc.page_regexes],
|
||||
scroll_thresholds: [goal.scroll_threshold | acc.scroll_thresholds],
|
||||
custom_props_keys: [prop_keys | acc.custom_props_keys],
|
||||
custom_props_values: [prop_values | acc.custom_props_values]
|
||||
}
|
||||
end
|
||||
)
|
||||
|> Enum.map(fn {key, list} -> {key, Enum.reverse(list)} end)
|
||||
|> Map.new()
|
||||
end
|
||||
|
||||
defp event_name_by_type(:event, goal), do: goal.event_name
|
||||
defp event_name_by_type(:page, _goal), do: "pageview"
|
||||
defp event_name_by_type(:scroll, _goal), do: "engagement"
|
||||
|
||||
defp page_regex_for_goal(:event, _goal), do: ".?"
|
||||
defp page_regex_for_goal(:page, goal), do: Filters.Utils.page_regex(goal.page_path)
|
||||
defp page_regex_for_goal(:scroll, goal), do: Filters.Utils.page_regex(goal.page_path)
|
||||
|
||||
def toplevel_scroll_goal_filters?(query) do
|
||||
goal_filters? =
|
||||
Enum.any?(query.filters, fn
|
||||
|
|
@ -179,7 +198,14 @@ defmodule Plausible.Stats.Goals do
|
|||
end
|
||||
|
||||
defp goal_condition(:event, goal, _) do
|
||||
dynamic([e], e.name == ^goal.event_name)
|
||||
name_condition = dynamic([e], e.name == ^goal.event_name)
|
||||
|
||||
if map_size(goal.custom_props) > 0 do
|
||||
custom_props_condition = build_custom_props_condition(goal.custom_props)
|
||||
dynamic([e], ^name_condition and ^custom_props_condition)
|
||||
else
|
||||
name_condition
|
||||
end
|
||||
end
|
||||
|
||||
defp goal_condition(:scroll, goal, false = _imported?) do
|
||||
|
|
@ -213,6 +239,24 @@ defmodule Plausible.Stats.Goals do
|
|||
end
|
||||
end
|
||||
|
||||
defp build_custom_props_condition(custom_props) do
|
||||
Enum.reduce(custom_props, true, fn {prop_key, prop_value}, acc ->
|
||||
condition =
|
||||
dynamic(
|
||||
[e],
|
||||
fragment(
|
||||
"?[indexOf(?, ?)] = ?",
|
||||
field(e, :"meta.value"),
|
||||
field(e, :"meta.key"),
|
||||
^prop_key,
|
||||
^prop_value
|
||||
)
|
||||
)
|
||||
|
||||
dynamic([e], ^acc and ^condition)
|
||||
end)
|
||||
end
|
||||
|
||||
def page_path_db_field(true = _imported?), do: :page
|
||||
def page_path_db_field(false = _imported?), do: :pathname
|
||||
end
|
||||
|
|
|
|||
|
|
@ -204,7 +204,7 @@ defmodule Plausible.Stats.SQL.Expression do
|
|||
|
||||
def select_dimension_internal(q, "visit:exit_page") do
|
||||
# As exit page changes with every pageview event over the lifetime
|
||||
# of a session, only the most recent value must be considered.
|
||||
# of a session, only the most recent value must be considered.
|
||||
select_merge_as(q, [t], %{
|
||||
exit_page: fragment("argMax(?, ?)", field(t, :exit_page), field(t, :events))
|
||||
})
|
||||
|
|
@ -443,6 +443,15 @@ defmodule Plausible.Stats.SQL.Expression do
|
|||
def session_metric(:conversion_rate, _query), do: %{}
|
||||
def session_metric(:group_conversion_rate, _query), do: %{}
|
||||
|
||||
@doc """
|
||||
The fragment matches events to goals by:
|
||||
1. Checking if the pathname matches the goal's page regex pattern
|
||||
2. Verifying the event name matches the expected name for the goal type
|
||||
3. Validating scroll depth is within threshold (for scroll goals)
|
||||
4. Ensuring all custom properties match (if any are defined on the goal)
|
||||
|
||||
Returns an array of goal indices that the event matches.
|
||||
"""
|
||||
defmacro event_goal_join(goal_join_data) do
|
||||
quote do
|
||||
fragment(
|
||||
|
|
@ -450,7 +459,13 @@ defmodule Plausible.Stats.SQL.Expression do
|
|||
arrayIntersect(
|
||||
multiMatchAllIndices(?, ?),
|
||||
arrayMap(
|
||||
(expected_name, threshold, index) -> if(expected_name = ? and ? between threshold and 100, index, -1),
|
||||
(expected_name, threshold, index, custom_props_keys, custom_props_values) -> if(
|
||||
expected_name = ? and ? between threshold and 100 and
|
||||
(empty(custom_props_keys) OR arrayAll((k, v) -> ?[indexOf(?, k)] = v, custom_props_keys, custom_props_values)),
|
||||
index, -1
|
||||
),
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?
|
||||
|
|
@ -461,9 +476,13 @@ defmodule Plausible.Stats.SQL.Expression do
|
|||
type(^unquote(goal_join_data).page_regexes, {:array, :string}),
|
||||
e.name,
|
||||
e.scroll_depth,
|
||||
field(e, :"meta.value"),
|
||||
field(e, :"meta.key"),
|
||||
type(^unquote(goal_join_data).event_names_by_type, {:array, :string}),
|
||||
type(^unquote(goal_join_data).scroll_thresholds, {:array, :integer}),
|
||||
type(^unquote(goal_join_data).indices, {:array, :integer})
|
||||
type(^unquote(goal_join_data).indices, {:array, :integer}),
|
||||
type(^unquote(goal_join_data).custom_props_keys, {:array, {:array, :string}}),
|
||||
type(^unquote(goal_join_data).custom_props_values, {:array, {:array, :string}})
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -83,8 +83,8 @@ defmodule Plausible.Stats.SQL.QueryBuilder do
|
|||
group_by: s.session_id
|
||||
)
|
||||
|
||||
# The session-only dimension columns are explicitly selected in joined
|
||||
# sessions table. This enables combining session-only dimensions (entry
|
||||
# The session-only dimension columns are explicitly selected in joined
|
||||
# sessions table. This enables combining session-only dimensions (entry
|
||||
# and exit pages) with event-only metrics, like revenue.
|
||||
sessions_q =
|
||||
Enum.reduce(dimensions, sessions_q, fn dimension, acc ->
|
||||
|
|
|
|||
|
|
@ -0,0 +1,259 @@
|
|||
defmodule Plausible.FunnelsCustomPropsGoals do
|
||||
use Plausible.DataCase
|
||||
@moduletag :ee_only
|
||||
|
||||
on_ee do
|
||||
alias Plausible.Goals
|
||||
alias Plausible.Funnels
|
||||
alias Plausible.Stats
|
||||
|
||||
describe "Plausible.Stats.Funnel - with custom property goals" do
|
||||
setup do
|
||||
site = new_site()
|
||||
{:ok, site: site}
|
||||
end
|
||||
|
||||
test "funnels with custom property filters on event goals", %{site: site} do
|
||||
{:ok, g1} = Goals.create(site, %{"page_path" => "/start"})
|
||||
|
||||
{:ok, g2} =
|
||||
Goals.create(site, %{
|
||||
"event_name" => "Purchase",
|
||||
"custom_props" => %{"plan" => "premium"}
|
||||
})
|
||||
|
||||
{:ok, g3} = Goals.create(site, %{"page_path" => "/thank-you"})
|
||||
|
||||
{:ok, funnel} =
|
||||
Funnels.create(
|
||||
site,
|
||||
"Premium purchase funnel",
|
||||
[
|
||||
%{"goal_id" => g1.id},
|
||||
%{"goal_id" => g2.id},
|
||||
%{"goal_id" => g3.id}
|
||||
]
|
||||
)
|
||||
|
||||
populate_stats(site, [
|
||||
build(:pageview, pathname: "/start", user_id: 100),
|
||||
build(:event,
|
||||
name: "Purchase",
|
||||
"meta.key": ["plan"],
|
||||
"meta.value": ["premium"],
|
||||
user_id: 100
|
||||
),
|
||||
build(:pageview, pathname: "/thank-you", user_id: 100),
|
||||
build(:pageview, pathname: "/start", user_id: 200),
|
||||
build(:event,
|
||||
name: "Purchase",
|
||||
"meta.key": ["plan"],
|
||||
"meta.value": ["free"],
|
||||
user_id: 200
|
||||
),
|
||||
build(:pageview, pathname: "/thank-you", user_id: 200),
|
||||
build(:pageview, pathname: "/start", user_id: 300),
|
||||
build(:event, name: "Purchase", user_id: 300)
|
||||
])
|
||||
|
||||
query = Plausible.Stats.Query.from(site, %{"period" => "all"})
|
||||
|
||||
{:ok, funnel_data} = Stats.funnel(site, query, funnel.id)
|
||||
|
||||
assert funnel_data[:all_visitors] == 3
|
||||
assert funnel_data[:entering_visitors] == 3
|
||||
|
||||
assert [step1, step2, step3] = funnel_data[:steps]
|
||||
assert step1.visitors == 3
|
||||
assert step2.visitors == 1
|
||||
assert step3.visitors == 1
|
||||
end
|
||||
|
||||
test "funnels with multiple custom property filters", %{site: site} do
|
||||
{:ok, g1} = Goals.create(site, %{"event_name" => "Start"})
|
||||
|
||||
{:ok, g2} =
|
||||
Goals.create(site, %{
|
||||
"event_name" => "Purchase",
|
||||
"custom_props" => %{"plan" => "premium", "variant" => "A"}
|
||||
})
|
||||
|
||||
{:ok, funnel} =
|
||||
Funnels.create(
|
||||
site,
|
||||
"Premium variant A funnel",
|
||||
[
|
||||
%{"goal_id" => g1.id},
|
||||
%{"goal_id" => g2.id}
|
||||
]
|
||||
)
|
||||
|
||||
populate_stats(site, [
|
||||
build(:event, name: "Start", user_id: 100),
|
||||
build(:event,
|
||||
name: "Purchase",
|
||||
"meta.key": ["plan", "variant"],
|
||||
"meta.value": ["premium", "A"],
|
||||
user_id: 100
|
||||
),
|
||||
build(:event, name: "Start", user_id: 200),
|
||||
build(:event,
|
||||
name: "Purchase",
|
||||
"meta.key": ["plan", "variant"],
|
||||
"meta.value": ["premium", "B"],
|
||||
user_id: 200
|
||||
),
|
||||
build(:event, name: "Start", user_id: 300),
|
||||
build(:event,
|
||||
name: "Purchase",
|
||||
"meta.key": ["plan", "variant"],
|
||||
"meta.value": ["free", "A"],
|
||||
user_id: 300
|
||||
),
|
||||
build(:event, name: "Start", user_id: 400),
|
||||
build(:event,
|
||||
name: "Purchase",
|
||||
"meta.key": ["plan"],
|
||||
"meta.value": ["premium"],
|
||||
user_id: 400
|
||||
)
|
||||
])
|
||||
|
||||
query = Plausible.Stats.Query.from(site, %{"period" => "all"})
|
||||
|
||||
{:ok, funnel_data} = Stats.funnel(site, query, funnel.id)
|
||||
|
||||
assert funnel_data[:all_visitors] == 4
|
||||
assert funnel_data[:entering_visitors] == 4
|
||||
|
||||
assert [step1, step2] = funnel_data[:steps]
|
||||
assert step1.visitors == 4
|
||||
assert step2.visitors == 1
|
||||
end
|
||||
|
||||
test "funnels with mixed goals (custom props and regular)", %{site: site} do
|
||||
{:ok, g1} =
|
||||
Goals.create(site, %{
|
||||
"event_name" => "Signup",
|
||||
"custom_props" => %{"method" => "email"}
|
||||
})
|
||||
|
||||
{:ok, g2} = Goals.create(site, %{"event_name" => "Onboarding Complete"})
|
||||
|
||||
{:ok, g3} =
|
||||
Goals.create(site, %{
|
||||
"event_name" => "Purchase",
|
||||
"custom_props" => %{"plan" => "premium"}
|
||||
})
|
||||
|
||||
{:ok, funnel} =
|
||||
Funnels.create(
|
||||
site,
|
||||
"Email signup to premium purchase",
|
||||
[
|
||||
%{"goal_id" => g1.id},
|
||||
%{"goal_id" => g2.id},
|
||||
%{"goal_id" => g3.id}
|
||||
]
|
||||
)
|
||||
|
||||
populate_stats(site, [
|
||||
build(:event,
|
||||
name: "Signup",
|
||||
"meta.key": ["method"],
|
||||
"meta.value": ["email"],
|
||||
user_id: 100
|
||||
),
|
||||
build(:event, name: "Onboarding Complete", user_id: 100),
|
||||
build(:event,
|
||||
name: "Purchase",
|
||||
"meta.key": ["plan"],
|
||||
"meta.value": ["premium"],
|
||||
user_id: 100
|
||||
),
|
||||
build(:event,
|
||||
name: "Signup",
|
||||
"meta.key": ["method"],
|
||||
"meta.value": ["google"],
|
||||
user_id: 200
|
||||
),
|
||||
build(:event, name: "Onboarding Complete", user_id: 200),
|
||||
build(:event,
|
||||
name: "Purchase",
|
||||
"meta.key": ["plan"],
|
||||
"meta.value": ["premium"],
|
||||
user_id: 200
|
||||
),
|
||||
build(:event,
|
||||
name: "Signup",
|
||||
"meta.key": ["method"],
|
||||
"meta.value": ["email"],
|
||||
user_id: 300
|
||||
),
|
||||
build(:event, name: "Onboarding Complete", user_id: 300),
|
||||
build(:event,
|
||||
name: "Purchase",
|
||||
"meta.key": ["plan"],
|
||||
"meta.value": ["free"],
|
||||
user_id: 300
|
||||
)
|
||||
])
|
||||
|
||||
query = Plausible.Stats.Query.from(site, %{"period" => "all"})
|
||||
|
||||
{:ok, funnel_data} = Stats.funnel(site, query, funnel.id)
|
||||
|
||||
assert funnel_data[:all_visitors] == 3
|
||||
assert funnel_data[:entering_visitors] == 2
|
||||
|
||||
assert [step1, step2, step3] = funnel_data[:steps]
|
||||
assert step1.visitors == 2
|
||||
assert step2.visitors == 2
|
||||
assert step3.visitors == 1
|
||||
end
|
||||
|
||||
test "funnel with empty custom_props does not filter", %{site: site} do
|
||||
{:ok, g1} =
|
||||
Goals.create(site, %{
|
||||
"event_name" => "Click",
|
||||
"custom_props" => %{}
|
||||
})
|
||||
|
||||
{:ok, g2} = Goals.create(site, %{"event_name" => "Convert"})
|
||||
|
||||
{:ok, funnel} =
|
||||
Funnels.create(
|
||||
site,
|
||||
"Click to convert funnel",
|
||||
[
|
||||
%{"goal_id" => g1.id},
|
||||
%{"goal_id" => g2.id}
|
||||
]
|
||||
)
|
||||
|
||||
populate_stats(site, [
|
||||
build(:event, name: "Click", user_id: 100),
|
||||
build(:event, name: "Convert", user_id: 100),
|
||||
build(:event,
|
||||
name: "Click",
|
||||
"meta.key": ["button"],
|
||||
"meta.value": ["cta"],
|
||||
user_id: 200
|
||||
),
|
||||
build(:event, name: "Convert", user_id: 200)
|
||||
])
|
||||
|
||||
query = Plausible.Stats.Query.from(site, %{"period" => "all"})
|
||||
|
||||
{:ok, funnel_data} = Stats.funnel(site, query, funnel.id)
|
||||
|
||||
assert funnel_data[:all_visitors] == 2
|
||||
assert funnel_data[:entering_visitors] == 2
|
||||
|
||||
assert [step1, step2] = funnel_data[:steps]
|
||||
assert step1.visitors == 2
|
||||
assert step2.visitors == 2
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -64,7 +64,7 @@ defmodule Plausible.GoalsTest do
|
|||
changeset.errors[:scroll_threshold]
|
||||
end
|
||||
|
||||
test "create/2 validates uniqueness across page_path and scroll_threshold" do
|
||||
test "create/2 fails when same pageview+scroll threshold config exists with different display name" do
|
||||
site = new_site()
|
||||
|
||||
{:ok, _} =
|
||||
|
|
@ -105,18 +105,169 @@ defmodule Plausible.GoalsTest do
|
|||
test "create/2 fails to create the same custom event goal twice" do
|
||||
site = new_site()
|
||||
{:ok, _} = Goals.create(site, %{"event_name" => "foo bar"})
|
||||
assert {:error, _changeset} = Goals.create(site, %{"event_name" => "foo bar"})
|
||||
# assert {"has already been taken", _} = changeset.errors[:display_name]
|
||||
assert {:error, changeset} = Goals.create(site, %{"event_name" => "foo bar"})
|
||||
assert {"has already been taken", _} = changeset.errors[:display_name]
|
||||
end
|
||||
|
||||
test "create/2 succeeds to create the same custom event goal thrice with different custom props and different display names each" do
|
||||
site = new_site()
|
||||
|
||||
{:ok, _} =
|
||||
Goals.create(site, %{
|
||||
"event_name" => "Purchase",
|
||||
"display_name" => "Tablet Purchase",
|
||||
"custom_props" => %{"product" => "tablet"}
|
||||
})
|
||||
|
||||
{:ok, _} =
|
||||
Goals.create(site, %{
|
||||
"event_name" => "Purchase",
|
||||
"display_name" => "Speaker Purchase",
|
||||
"custom_props" => %{"product" => "speaker"}
|
||||
})
|
||||
|
||||
{:ok, _} =
|
||||
Goals.create(site, %{
|
||||
"event_name" => "Purchase",
|
||||
"display_name" => "General Purchase"
|
||||
})
|
||||
end
|
||||
|
||||
test "create/2 fails to create the same custom event goal twice with different display names but no props each" do
|
||||
site = new_site()
|
||||
|
||||
{:ok, _} =
|
||||
Goals.create(site, %{
|
||||
"event_name" => "Purchase",
|
||||
"display_name" => "General Purchase"
|
||||
})
|
||||
|
||||
{:error, changeset} =
|
||||
Goals.create(site, %{
|
||||
"event_name" => "Purchase",
|
||||
"display_name" => "General Purchase 2"
|
||||
})
|
||||
|
||||
assert {"has already been taken", _} = changeset.errors[:event_name]
|
||||
end
|
||||
|
||||
test "create/3 fails to create a goal with more than #{Plausible.Goal.max_custom_props_per_goal()} custom props" do
|
||||
site = new_site()
|
||||
|
||||
{:error, changeset} =
|
||||
Goals.create(site, %{
|
||||
"event_name" => "Purchase",
|
||||
"display_name" => "General Purchase",
|
||||
"custom_props" => %{
|
||||
"variant" => "A",
|
||||
"promo" => "true",
|
||||
"product" => "tablet",
|
||||
"limit" => "hit"
|
||||
}
|
||||
})
|
||||
|
||||
assert {"use at most 3 properties per goal", _} = changeset.errors[:custom_props]
|
||||
end
|
||||
|
||||
test "create/2 succeeds to create the same custom event twice with different props and different display names each" do
|
||||
site = new_site()
|
||||
|
||||
{:ok, _} =
|
||||
Goals.create(site, %{
|
||||
"event_name" => "Purchase",
|
||||
"display_name" => "General Purchase",
|
||||
"custom_props" => %{"variant" => "A"}
|
||||
})
|
||||
|
||||
{:ok, _} =
|
||||
Goals.create(site, %{
|
||||
"event_name" => "Purchase",
|
||||
"display_name" => "General Purchase 2",
|
||||
"custom_props" => %{"variant" => "A", "foo" => "bar"}
|
||||
})
|
||||
end
|
||||
|
||||
test "create/2 fails to create the same currency goal twice" do
|
||||
site = new_site()
|
||||
{:ok, _} = Goals.create(site, %{"event_name" => "foo bar", "currency" => "EUR"})
|
||||
|
||||
assert {:error, _changeset} =
|
||||
Goals.create(site, %{"event_name" => "foo bar", "currency" => "EUR"})
|
||||
assert {:error, changeset} =
|
||||
Goals.create(site, %{
|
||||
"event_name" => "foo bar",
|
||||
"currency" => "EUR",
|
||||
"display_name" => "Purchase copy"
|
||||
})
|
||||
|
||||
# assert {"has already been taken", _} = changeset.errors[:display_name]
|
||||
assert {"has already been taken", _} = changeset.errors[:event_name]
|
||||
end
|
||||
|
||||
test "create/2 fails to create two pageview goals with same display name" do
|
||||
site = new_site()
|
||||
{:ok, _} = Goals.create(site, %{"page_path" => "/index", "display_name" => "Index"})
|
||||
|
||||
assert {:error, changeset} =
|
||||
Goals.create(site, %{"page_path" => "/index-2", "display_name" => "Index"})
|
||||
|
||||
assert {"has already been taken", _} =
|
||||
changeset.errors[:display_name]
|
||||
end
|
||||
|
||||
test "create/2 fails when same custom props config exists with different display name" do
|
||||
site = new_site()
|
||||
|
||||
{:ok, _} =
|
||||
Goals.create(site, %{
|
||||
"event_name" => "Purchase",
|
||||
"display_name" => "Purchase Event",
|
||||
"custom_props" => %{"product" => "tablet"}
|
||||
})
|
||||
|
||||
{:error, changeset} =
|
||||
Goals.create(site, %{
|
||||
"event_name" => "Purchase",
|
||||
"display_name" => "Different Display Name",
|
||||
"custom_props" => %{"product" => "tablet"}
|
||||
})
|
||||
|
||||
assert {"has already been taken", _} = changeset.errors[:event_name]
|
||||
end
|
||||
|
||||
test "create/2 fails when same display name exists despite different custom props config" do
|
||||
site = new_site()
|
||||
|
||||
{:ok, _} =
|
||||
Goals.create(site, %{
|
||||
"event_name" => "Purchase",
|
||||
"display_name" => "My Goal",
|
||||
"custom_props" => %{"product" => "tablet"}
|
||||
})
|
||||
|
||||
{:error, changeset} =
|
||||
Goals.create(site, %{
|
||||
"event_name" => "Purchase",
|
||||
"display_name" => "My Goal",
|
||||
"custom_props" => %{"product" => "speaker"}
|
||||
})
|
||||
|
||||
assert {"has already been taken", _} = changeset.errors[:display_name]
|
||||
end
|
||||
|
||||
test "create/2 fails when same display name exists between event and pageview goals" do
|
||||
site = new_site()
|
||||
|
||||
{:ok, _} =
|
||||
Goals.create(site, %{
|
||||
"event_name" => "Signup",
|
||||
"display_name" => "User Action"
|
||||
})
|
||||
|
||||
{:error, changeset} =
|
||||
Goals.create(site, %{
|
||||
"page_path" => "/signup",
|
||||
"display_name" => "User Action"
|
||||
})
|
||||
|
||||
assert {"has already been taken", _} = changeset.errors[:display_name]
|
||||
end
|
||||
|
||||
test "create/2 fails to create a goal with 'engagement' as event_name (reserved)" do
|
||||
|
|
@ -127,41 +278,38 @@ defmodule Plausible.GoalsTest do
|
|||
changeset.errors[:event_name]
|
||||
end
|
||||
|
||||
@tag :ee_only
|
||||
test "create/2 sets site.updated_at for revenue goal" do
|
||||
site_1 = new_site(updated_at: DateTime.add(DateTime.utc_now(), -3600))
|
||||
|
||||
{:ok, _goal_1} = Goals.create(site_1, %{"event_name" => "Checkout", "currency" => "BRL"})
|
||||
|
||||
assert NaiveDateTime.compare(site_1.updated_at, Plausible.Repo.reload!(site_1).updated_at) ==
|
||||
:lt
|
||||
|
||||
site_2 = new_site(updated_at: DateTime.add(DateTime.utc_now(), -3600))
|
||||
{:ok, _goal_2} = Goals.create(site_2, %{"event_name" => "Read Article", "currency" => nil})
|
||||
|
||||
assert NaiveDateTime.compare(site_2.updated_at, Plausible.Repo.reload!(site_2).updated_at) ==
|
||||
:eq
|
||||
end
|
||||
|
||||
@tag :ee_only
|
||||
test "create/2 creates revenue goal" do
|
||||
site = new_site()
|
||||
{:ok, goal} = Goals.create(site, %{"event_name" => "Purchase", "currency" => "EUR"})
|
||||
assert goal.event_name == "Purchase"
|
||||
assert goal.page_path == nil
|
||||
assert goal.currency == :EUR
|
||||
end
|
||||
|
||||
@tag :ee_only
|
||||
test "create/2 returns error when site does not have access to revenue goals" do
|
||||
user = new_user() |> subscribe_to_growth_plan()
|
||||
site = new_site(owner: user)
|
||||
|
||||
{:error, :upgrade_required} =
|
||||
Goals.create(site, %{"event_name" => "Purchase", "currency" => "EUR"})
|
||||
end
|
||||
|
||||
on_ee do
|
||||
test "create/2 sets site.updated_at for revenue goal" do
|
||||
site_1 = new_site(updated_at: DateTime.add(DateTime.utc_now(), -3600))
|
||||
|
||||
{:ok, _goal_1} = Goals.create(site_1, %{"event_name" => "Checkout", "currency" => "BRL"})
|
||||
|
||||
assert NaiveDateTime.compare(site_1.updated_at, Plausible.Repo.reload!(site_1).updated_at) ==
|
||||
:lt
|
||||
|
||||
site_2 = new_site(updated_at: DateTime.add(DateTime.utc_now(), -3600))
|
||||
{:ok, _goal_2} = Goals.create(site_2, %{"event_name" => "Read Article", "currency" => nil})
|
||||
|
||||
assert NaiveDateTime.compare(site_2.updated_at, Plausible.Repo.reload!(site_2).updated_at) ==
|
||||
:eq
|
||||
end
|
||||
|
||||
test "create/2 creates revenue goal" do
|
||||
site = new_site()
|
||||
{:ok, goal} = Goals.create(site, %{"event_name" => "Purchase", "currency" => "EUR"})
|
||||
assert goal.event_name == "Purchase"
|
||||
assert goal.page_path == nil
|
||||
assert goal.currency == :EUR
|
||||
end
|
||||
|
||||
test "create/2 returns error when site does not have access to revenue goals" do
|
||||
user = new_user() |> subscribe_to_growth_plan()
|
||||
site = new_site(owner: user)
|
||||
|
||||
{:error, :upgrade_required} =
|
||||
Goals.create(site, %{"event_name" => "Purchase", "currency" => "EUR"})
|
||||
end
|
||||
|
||||
test "create/2 returns error when creating a revenue goal for consolidated view" do
|
||||
user = new_user()
|
||||
new_site(owner: user)
|
||||
|
|
@ -172,16 +320,15 @@ defmodule Plausible.GoalsTest do
|
|||
{:error, :revenue_goals_unavailable} =
|
||||
Goals.create(site, %{"event_name" => "Purchase", "currency" => "EUR"})
|
||||
end
|
||||
end
|
||||
|
||||
@tag :ee_only
|
||||
test "create/2 fails for unknown currency code" do
|
||||
site = new_site()
|
||||
test "create/2 fails for unknown currency code" do
|
||||
site = new_site()
|
||||
|
||||
assert {:error, changeset} =
|
||||
Goals.create(site, %{"event_name" => "Purchase", "currency" => "Euro"})
|
||||
assert {:error, changeset} =
|
||||
Goals.create(site, %{"event_name" => "Purchase", "currency" => "Euro"})
|
||||
|
||||
assert [currency: {"is invalid", _}] = changeset.errors
|
||||
assert [currency: {"is invalid", _}] = changeset.errors
|
||||
end
|
||||
end
|
||||
|
||||
test "update/2 updates a goal" do
|
||||
|
|
@ -240,22 +387,23 @@ defmodule Plausible.GoalsTest do
|
|||
}
|
||||
end
|
||||
|
||||
@tag :ee_only
|
||||
test "list_revenue_goals/1 lists event_names and currencies for each revenue goal" do
|
||||
site = new_site()
|
||||
on_ee do
|
||||
test "list_revenue_goals/1 lists event_names and currencies for each revenue goal" do
|
||||
site = new_site()
|
||||
|
||||
Goals.create(site, %{"event_name" => "One", "currency" => "EUR"})
|
||||
Goals.create(site, %{"event_name" => "Two", "currency" => "EUR"})
|
||||
Goals.create(site, %{"event_name" => "Three", "currency" => "USD"})
|
||||
Goals.create(site, %{"event_name" => "Four"})
|
||||
Goals.create(site, %{"page_path" => "/some-page"})
|
||||
Goals.create(site, %{"event_name" => "One", "currency" => "EUR"})
|
||||
Goals.create(site, %{"event_name" => "Two", "currency" => "EUR"})
|
||||
Goals.create(site, %{"event_name" => "Three", "currency" => "USD"})
|
||||
Goals.create(site, %{"event_name" => "Four"})
|
||||
Goals.create(site, %{"page_path" => "/some-page"})
|
||||
|
||||
revenue_goals = Goals.list_revenue_goals(site)
|
||||
revenue_goals = Goals.list_revenue_goals(site)
|
||||
|
||||
assert length(revenue_goals) == 3
|
||||
assert %{display_name: "One", currency: :EUR} in revenue_goals
|
||||
assert %{display_name: "Two", currency: :EUR} in revenue_goals
|
||||
assert %{display_name: "Three", currency: :USD} in revenue_goals
|
||||
assert length(revenue_goals) == 3
|
||||
assert %{display_name: "One", currency: :EUR} in revenue_goals
|
||||
assert %{display_name: "Two", currency: :EUR} in revenue_goals
|
||||
assert %{display_name: "Three", currency: :USD} in revenue_goals
|
||||
end
|
||||
end
|
||||
|
||||
test "create/2 clears currency for pageview goals" do
|
||||
|
|
|
|||
|
|
@ -0,0 +1,372 @@
|
|||
defmodule PlausibleWeb.Api.ExternalStatsController.QueryGoalCustomPropsTest do
|
||||
use PlausibleWeb.ConnCase
|
||||
|
||||
@user_id Enum.random(1000..9999)
|
||||
|
||||
setup [:create_user, :create_site, :create_api_key, :use_api_key]
|
||||
|
||||
alias Plausible.Goals
|
||||
|
||||
describe "goals with custom property filters" do
|
||||
test "filters custom event goals by custom properties", %{conn: conn, site: site} do
|
||||
{:ok, _goal} =
|
||||
Goals.create(site, %{
|
||||
"event_name" => "Purchase",
|
||||
"custom_props" => %{"variant" => "A"}
|
||||
})
|
||||
|
||||
populate_stats(site, [
|
||||
build(:event,
|
||||
name: "Purchase",
|
||||
"meta.key": ["variant"],
|
||||
"meta.value": ["A"],
|
||||
timestamp: ~N[2021-01-01 00:00:01]
|
||||
),
|
||||
build(:event,
|
||||
name: "Purchase",
|
||||
"meta.key": ["variant"],
|
||||
"meta.value": ["A"],
|
||||
timestamp: ~N[2021-01-01 00:00:02]
|
||||
),
|
||||
build(:event,
|
||||
name: "Purchase",
|
||||
"meta.key": ["variant"],
|
||||
"meta.value": ["B"],
|
||||
timestamp: ~N[2021-01-01 00:00:03]
|
||||
),
|
||||
build(:event,
|
||||
name: "Purchase",
|
||||
timestamp: ~N[2021-01-01 00:00:04]
|
||||
)
|
||||
])
|
||||
|
||||
conn =
|
||||
post(conn, "/api/v2/query", %{
|
||||
"site_id" => site.domain,
|
||||
"date_range" => "all",
|
||||
"metrics" => ["visitors", "events"],
|
||||
"filters" => [["is", "event:goal", ["Purchase"]]]
|
||||
})
|
||||
|
||||
resp = json_response(conn, 200)
|
||||
|
||||
assert resp["results"] == [
|
||||
%{"dimensions" => [], "metrics" => [2, 2]}
|
||||
]
|
||||
end
|
||||
|
||||
test "filters with multiple custom properties (AND logic)", %{conn: conn, site: site} do
|
||||
{:ok, _goal} =
|
||||
Goals.create(site, %{
|
||||
"event_name" => "Purchase",
|
||||
"custom_props" => %{"variant" => "A", "plan" => "premium"}
|
||||
})
|
||||
|
||||
populate_stats(site, [
|
||||
build(:event,
|
||||
name: "Purchase",
|
||||
"meta.key": ["variant", "plan"],
|
||||
"meta.value": ["A", "premium"]
|
||||
),
|
||||
build(:event,
|
||||
name: "Purchase",
|
||||
"meta.key": ["variant", "plan"],
|
||||
"meta.value": ["A", "free"],
|
||||
user_id: @user_id
|
||||
),
|
||||
build(:event,
|
||||
name: "Purchase",
|
||||
"meta.key": ["variant", "plan"],
|
||||
"meta.value": ["B", "premium"],
|
||||
user_id: @user_id + 1
|
||||
),
|
||||
build(:event,
|
||||
name: "Purchase",
|
||||
"meta.key": ["variant"],
|
||||
"meta.value": ["A"],
|
||||
user_id: @user_id + 2
|
||||
)
|
||||
])
|
||||
|
||||
conn =
|
||||
post(conn, "/api/v2/query", %{
|
||||
"site_id" => site.domain,
|
||||
"date_range" => "all",
|
||||
"metrics" => ["visitors", "events"],
|
||||
"filters" => [["is", "event:goal", ["Purchase"]]]
|
||||
})
|
||||
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"dimensions" => [], "metrics" => [1, 1]}
|
||||
]
|
||||
end
|
||||
|
||||
test "goals without custom_props filter match all events", %{conn: conn, site: site} do
|
||||
{:ok, _goal} = Goals.create(site, %{"event_name" => "Signup"})
|
||||
|
||||
populate_stats(site, [
|
||||
build(:event,
|
||||
name: "Signup",
|
||||
"meta.key": ["source"],
|
||||
"meta.value": ["google"]
|
||||
),
|
||||
build(:event,
|
||||
name: "Signup",
|
||||
"meta.key": ["source"],
|
||||
"meta.value": ["twitter"]
|
||||
),
|
||||
build(:event, name: "Signup")
|
||||
])
|
||||
|
||||
conn =
|
||||
post(conn, "/api/v2/query", %{
|
||||
"site_id" => site.domain,
|
||||
"date_range" => "all",
|
||||
"metrics" => ["visitors", "events"],
|
||||
"filters" => [["is", "event:goal", ["Signup"]]]
|
||||
})
|
||||
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"dimensions" => [], "metrics" => [3, 3]}
|
||||
]
|
||||
end
|
||||
|
||||
test "breakdown by event:goal with custom property filters", %{conn: conn, site: site} do
|
||||
{:ok, _g1} =
|
||||
Goals.create(site, %{
|
||||
"event_name" => "Purchase A",
|
||||
"custom_props" => %{"variant" => "A"}
|
||||
})
|
||||
|
||||
{:ok, _g2} =
|
||||
Goals.create(site, %{
|
||||
"event_name" => "Purchase B",
|
||||
"custom_props" => %{"variant" => "B"}
|
||||
})
|
||||
|
||||
populate_stats(site, [
|
||||
build(:event,
|
||||
name: "Purchase A",
|
||||
"meta.key": ["variant"],
|
||||
"meta.value": ["A"],
|
||||
user_id: @user_id
|
||||
),
|
||||
build(:event,
|
||||
name: "Purchase A",
|
||||
"meta.key": ["variant"],
|
||||
"meta.value": ["A"],
|
||||
user_id: @user_id
|
||||
),
|
||||
build(:event,
|
||||
name: "Purchase B",
|
||||
"meta.key": ["variant"],
|
||||
"meta.value": ["B"],
|
||||
user_id: @user_id
|
||||
)
|
||||
])
|
||||
|
||||
conn =
|
||||
post(conn, "/api/v2/query", %{
|
||||
"site_id" => site.domain,
|
||||
"date_range" => "all",
|
||||
"metrics" => ["visitors", "events"],
|
||||
"dimensions" => ["event:goal"]
|
||||
})
|
||||
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"dimensions" => ["Purchase A"], "metrics" => [1, 2]},
|
||||
%{"dimensions" => ["Purchase B"], "metrics" => [1, 1]}
|
||||
]
|
||||
end
|
||||
|
||||
test "custom property filters work with conversion_rate metric", %{conn: conn, site: site} do
|
||||
{:ok, _goal} =
|
||||
Goals.create(site, %{
|
||||
"event_name" => "Signup",
|
||||
"custom_props" => %{"method" => "email"}
|
||||
})
|
||||
|
||||
populate_stats(site, [
|
||||
build(:pageview, user_id: @user_id),
|
||||
build(:event,
|
||||
name: "Signup",
|
||||
"meta.key": ["method"],
|
||||
"meta.value": ["email"],
|
||||
user_id: @user_id
|
||||
),
|
||||
build(:pageview, user_id: @user_id + 1),
|
||||
build(:event,
|
||||
name: "Signup",
|
||||
"meta.key": ["method"],
|
||||
"meta.value": ["google"],
|
||||
user_id: @user_id + 1
|
||||
),
|
||||
build(:pageview, user_id: @user_id + 2)
|
||||
])
|
||||
|
||||
conn =
|
||||
post(conn, "/api/v2/query", %{
|
||||
"site_id" => site.domain,
|
||||
"date_range" => "all",
|
||||
"metrics" => ["visitors", "events", "conversion_rate"],
|
||||
"filters" => [["is", "event:goal", ["Signup"]]]
|
||||
})
|
||||
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"dimensions" => [], "metrics" => [1, 1, 33.33]}
|
||||
]
|
||||
end
|
||||
|
||||
test "custom property filters work with multi-dimensional breakdown", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
{:ok, _goal} =
|
||||
Goals.create(site, %{
|
||||
"event_name" => "Purchase",
|
||||
"custom_props" => %{"variant" => "A"}
|
||||
})
|
||||
|
||||
populate_stats(site, [
|
||||
build(:event,
|
||||
name: "Purchase",
|
||||
"meta.key": ["variant", "plan"],
|
||||
"meta.value": ["A", "premium"]
|
||||
),
|
||||
build(:event,
|
||||
name: "Purchase",
|
||||
"meta.key": ["variant", "plan"],
|
||||
"meta.value": ["A", "free"],
|
||||
user_id: @user_id + 1
|
||||
),
|
||||
build(:event,
|
||||
name: "Purchase",
|
||||
"meta.key": ["variant", "plan"],
|
||||
"meta.value": ["B", "premium"],
|
||||
user_id: @user_id + 2
|
||||
)
|
||||
])
|
||||
|
||||
conn =
|
||||
post(conn, "/api/v2/query", %{
|
||||
"site_id" => site.domain,
|
||||
"date_range" => "all",
|
||||
"metrics" => ["visitors", "events"],
|
||||
"dimensions" => ["event:goal", "event:props:plan"],
|
||||
"filters" => [["is", "event:goal", ["Purchase"]]]
|
||||
})
|
||||
|
||||
results = json_response(conn, 200)["results"]
|
||||
assert length(results) == 2
|
||||
assert Enum.member?(results, %{"dimensions" => ["Purchase", "free"], "metrics" => [1, 1]})
|
||||
|
||||
assert Enum.member?(results, %{"dimensions" => ["Purchase", "premium"], "metrics" => [1, 1]})
|
||||
end
|
||||
|
||||
test "custom property filters work with time series queries", %{conn: conn, site: site} do
|
||||
{:ok, _goal} =
|
||||
Goals.create(site, %{
|
||||
"event_name" => "Click",
|
||||
"custom_props" => %{"button" => "cta"}
|
||||
})
|
||||
|
||||
populate_stats(site, [
|
||||
build(:event,
|
||||
name: "Click",
|
||||
"meta.key": ["button"],
|
||||
"meta.value": ["cta"],
|
||||
timestamp: ~N[2021-01-01 00:00:00]
|
||||
),
|
||||
build(:event,
|
||||
name: "Click",
|
||||
"meta.key": ["button"],
|
||||
"meta.value": ["nav"],
|
||||
timestamp: ~N[2021-01-01 00:00:00]
|
||||
),
|
||||
build(:event,
|
||||
name: "Click",
|
||||
"meta.key": ["button"],
|
||||
"meta.value": ["cta"],
|
||||
timestamp: ~N[2021-01-02 00:00:00]
|
||||
)
|
||||
])
|
||||
|
||||
conn =
|
||||
post(conn, "/api/v2/query", %{
|
||||
"site_id" => site.domain,
|
||||
"date_range" => ["2021-01-01", "2021-01-02"],
|
||||
"metrics" => ["visitors"],
|
||||
"dimensions" => ["time:day"],
|
||||
"filters" => [["is", "event:goal", ["Click"]]]
|
||||
})
|
||||
|
||||
assert json_response(conn, 200)["results"] == [
|
||||
%{"dimensions" => ["2021-01-01"], "metrics" => [1]},
|
||||
%{"dimensions" => ["2021-01-02"], "metrics" => [1]}
|
||||
]
|
||||
end
|
||||
|
||||
test "different goals with same event name but different custom props are distinguished", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
{:ok, _g1} =
|
||||
Goals.create(site, %{
|
||||
"event_name" => "Button Click",
|
||||
"display_name" => "Red Button",
|
||||
"custom_props" => %{"color" => "red"}
|
||||
})
|
||||
|
||||
{:ok, _g2} =
|
||||
Goals.create(site, %{
|
||||
"event_name" => "Button Click",
|
||||
"display_name" => "Blue Button",
|
||||
"custom_props" => %{"color" => "blue"}
|
||||
})
|
||||
|
||||
populate_stats(site, [
|
||||
build(:event,
|
||||
name: "Button Click",
|
||||
"meta.key": ["color"],
|
||||
"meta.value": ["red"],
|
||||
user_id: @user_id
|
||||
),
|
||||
build(:event,
|
||||
name: "Button Click",
|
||||
"meta.key": ["color"],
|
||||
"meta.value": ["red"],
|
||||
user_id: @user_id
|
||||
),
|
||||
build(:event,
|
||||
name: "Button Click",
|
||||
"meta.key": ["color"],
|
||||
"meta.value": ["blue"],
|
||||
user_id: @user_id + 1
|
||||
),
|
||||
build(:event, name: "Button Click", user_id: @user_id + 2)
|
||||
])
|
||||
|
||||
conn =
|
||||
post(conn, "/api/v2/query", %{
|
||||
"site_id" => site.domain,
|
||||
"date_range" => "all",
|
||||
"metrics" => ["visitors", "events"],
|
||||
"dimensions" => ["event:goal"]
|
||||
})
|
||||
|
||||
results = json_response(conn, 200)["results"]
|
||||
|
||||
assert Enum.find(results, &(&1["dimensions"] == ["Red Button"])) == %{
|
||||
"dimensions" => ["Red Button"],
|
||||
"metrics" => [1, 2]
|
||||
}
|
||||
|
||||
assert Enum.find(results, &(&1["dimensions"] == ["Blue Button"])) == %{
|
||||
"dimensions" => ["Blue Button"],
|
||||
"metrics" => [1, 1]
|
||||
}
|
||||
|
||||
refute Enum.find(results, &(&1["dimensions"] == ["Button Click"]))
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in New Issue