Skip to content

Commit 99fd335

Browse files
aerosolapata
andauthored
Expose goals w/ custom props through Sites/Plugins API (#5952)
* Improve goal custom props validation * Implement goals with custom props in Plugins API * Implement goals with custom props in Sites API * DRY: Extract CustomProps schemas * rename test * Check for Props feature availability on goals with custom props creation * Guard goals w/ custom props with billing feature checks * ce * ce * Tidy up test * credo * Fix error wording for Sites API * Update test/support/teams/test.ex Co-authored-by: Artur Pata <[email protected]> * Use strict map assertion per @apata's suggestion * Stronger match * Improve custom props validation * Keep the hybrid feature encapsulated * Format * Revert "Keep the hybrid feature encapsulated" This reverts commit 3d5d7cf. * Fixup --------- Co-authored-by: Artur Pata <[email protected]>
1 parent 9aa6a8a commit 99fd335

File tree

16 files changed

+547
-47
lines changed

16 files changed

+547
-47
lines changed

extra/lib/plausible_web/controllers/api/external_sites_controller.ex

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,8 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
9898
display_name: goal.display_name,
9999
goal_type: Goal.type(goal),
100100
event_name: goal.event_name,
101-
page_path: goal.page_path
101+
page_path: goal.page_path,
102+
custom_props: goal.custom_props
102103
}
103104
end),
104105
meta: pagination_meta(page.metadata)
@@ -413,6 +414,12 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
413414

414415
H.bad_request(conn, message)
415416

417+
{:error, :upgrade_required} ->
418+
H.payment_required(
419+
conn,
420+
"Your current subscription plan does not include Custom Properties"
421+
)
422+
416423
e ->
417424
H.bad_request(conn, "Something went wrong: #{inspect(e)}")
418425
end

lib/plausible/goal.ex

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,7 @@ defmodule Plausible.Goal do
5454
|> validate_event_name_and_page_path()
5555
|> validate_page_path_for_scroll_goal()
5656
|> maybe_put_display_name()
57-
|> validate_change(:custom_props, fn :custom_props, custom_props ->
58-
if map_size(custom_props) > @max_custom_props_per_goal do
59-
[custom_props: "use at most #{@max_custom_props_per_goal} properties per goal"]
60-
else
61-
[]
62-
end
63-
end)
57+
|> validate_change(:custom_props, &validate_custom_props/2)
6458
|> unique_constraint(:display_name, name: :goals_display_name_unique)
6559
|> unique_constraint(:event_name, name: :goals_event_config_unique)
6660
|> unique_constraint([:page_path, :scroll_threshold],
@@ -183,6 +177,35 @@ defmodule Plausible.Goal do
183177
|> update_change(:display_name, &String.trim/1)
184178
|> validate_required(:display_name)
185179
end
180+
181+
defp validate_custom_props(:custom_props, custom_props) when is_map(custom_props) do
182+
cond do
183+
map_size(custom_props) > @max_custom_props_per_goal ->
184+
[custom_props: "use at most #{@max_custom_props_per_goal} properties per goal"]
185+
186+
not Enum.all?(custom_props, fn {k, v} ->
187+
is_binary(k) and is_binary(v)
188+
end) ->
189+
[custom_props: "must be a map with string keys and string values"]
190+
191+
Enum.any?(custom_props, fn {k, _v} ->
192+
String.length(k) not in 1..Plausible.Props.max_prop_key_length()
193+
end) ->
194+
[
195+
custom_props: "key length is 1..#{Plausible.Props.max_prop_key_length()} characters"
196+
]
197+
198+
Enum.any?(custom_props, fn {_k, v} ->
199+
String.length(v) not in 1..Plausible.Props.max_prop_value_length()
200+
end) ->
201+
[
202+
custom_props: "value length is 1..#{Plausible.Props.max_prop_value_length()} characters"
203+
]
204+
205+
true ->
206+
[]
207+
end
208+
end
186209
end
187210

188211
defimpl Jason.Encoder, for: Plausible.Goal do
@@ -191,7 +214,7 @@ defimpl Jason.Encoder, for: Plausible.Goal do
191214

192215
value
193216
|> Map.put(:goal_type, Plausible.Goal.type(value))
194-
|> Map.take([:id, :goal_type, :event_name, :page_path])
217+
|> Map.take([:id, :goal_type, :event_name, :page_path, :custom_props])
195218
|> Map.put(:domain, domain)
196219
|> Map.put(:display_name, value.display_name)
197220
|> Jason.Encode.map(opts)

lib/plausible/goals/goals.ex

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,12 @@ defmodule Plausible.Goals do
387387
end
388388

389389
defp maybe_check_feature_access(site, changeset) do
390+
with :ok <- revenue_goals_access_check(site, changeset) do
391+
custom_props_goals_access_check(site, changeset)
392+
end
393+
end
394+
395+
defp revenue_goals_access_check(site, changeset) do
390396
if Changeset.get_field(changeset, :currency) do
391397
site = Plausible.Repo.preload(site, :team)
392398
Plausible.Billing.Feature.RevenueGoals.check_availability(site.team)
@@ -395,6 +401,15 @@ defmodule Plausible.Goals do
395401
end
396402
end
397403

404+
defp custom_props_goals_access_check(site, changeset) do
405+
if map_size(Changeset.get_field(changeset, :custom_props)) > 0 do
406+
site = Plausible.Repo.preload(site, :team)
407+
Plausible.Billing.Feature.Props.check_availability(site.team)
408+
else
409+
:ok
410+
end
411+
end
412+
398413
defp check_goals_limit(site, changeset, opts) do
399414
if upsert?(opts) and goal_exists_for_upsert?(site, changeset) do
400415
:ok

lib/plausible/plugins/api/goals.ex

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,42 @@ defmodule Plausible.Plugins.API.Goals do
5151
:ok
5252
end
5353

54+
defp convert_to_create_params(%CreateRequest.CustomEvent{
55+
goal: %{event_name: event_name, custom_props: custom_props}
56+
})
57+
when is_map(custom_props) do
58+
%{"goal_type" => "event", "event_name" => event_name, "custom_props" => custom_props}
59+
end
60+
5461
defp convert_to_create_params(%CreateRequest.CustomEvent{goal: %{event_name: event_name}}) do
5562
%{"goal_type" => "event", "event_name" => event_name}
5663
end
5764

65+
defp convert_to_create_params(%CreateRequest.Revenue{
66+
goal: %{event_name: event_name, currency: currency, custom_props: custom_props}
67+
})
68+
when is_map(custom_props) do
69+
%{
70+
"goal_type" => "event",
71+
"event_name" => event_name,
72+
"currency" => currency,
73+
"custom_props" => custom_props
74+
}
75+
end
76+
5877
defp convert_to_create_params(%CreateRequest.Revenue{
5978
goal: %{event_name: event_name, currency: currency}
6079
}) do
6180
%{"goal_type" => "event", "event_name" => event_name, "currency" => currency}
6281
end
6382

83+
defp convert_to_create_params(%CreateRequest.Pageview{
84+
goal: %{path: page_path, custom_props: custom_props}
85+
})
86+
when is_map(custom_props) do
87+
%{"goal_type" => "page", "page_path" => page_path, "custom_props" => custom_props}
88+
end
89+
6490
defp convert_to_create_params(%CreateRequest.Pageview{goal: %{path: page_path}}) do
6591
%{"goal_type" => "page", "page_path" => page_path}
6692
end
@@ -73,6 +99,9 @@ defmodule Plausible.Plugins.API.Goals do
7399
{:ok, goal} ->
74100
goal
75101

102+
{:error, :upgrade_required} ->
103+
Repo.rollback(:upgrade_required)
104+
76105
{:error, changeset} ->
77106
Repo.rollback(changeset)
78107
end

lib/plausible_web/plugins/api/schemas/goal/create_request/custom_event.ex

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ defmodule PlausibleWeb.Plugins.API.Schemas.Goal.CreateRequest.CustomEvent do
55

66
use PlausibleWeb, :open_api_schema
77

8+
alias Schemas.Goal.CustomProps
9+
810
OpenApiSpex.schema(%{
911
title: "Goal.CreateRequest.CustomEvent",
1012
description: "Custom Event Goal creation params",
@@ -20,7 +22,8 @@ defmodule PlausibleWeb.Plugins.API.Schemas.Goal.CreateRequest.CustomEvent do
2022
type: :object,
2123
required: [:event_name],
2224
properties: %{
23-
event_name: %Schema{type: :string}
25+
event_name: %Schema{type: :string},
26+
custom_props: CustomProps.request_schema()
2427
}
2528
}
2629
},

lib/plausible_web/plugins/api/schemas/goal/create_request/pageview.ex

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ defmodule PlausibleWeb.Plugins.API.Schemas.Goal.CreateRequest.Pageview do
55

66
use PlausibleWeb, :open_api_schema
77

8+
alias Schemas.Goal.CustomProps
9+
810
OpenApiSpex.schema(%{
911
title: "Goal.CreateRequest.Pageview",
1012
description: "Pageview Goal creation params",
@@ -20,7 +22,8 @@ defmodule PlausibleWeb.Plugins.API.Schemas.Goal.CreateRequest.Pageview do
2022
type: :object,
2123
required: [:path],
2224
properties: %{
23-
path: %Schema{type: :string}
25+
path: %Schema{type: :string},
26+
custom_props: CustomProps.request_schema()
2427
}
2528
}
2629
},

lib/plausible_web/plugins/api/schemas/goal/create_request/revenue.ex

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ defmodule PlausibleWeb.Plugins.API.Schemas.Goal.CreateRequest.Revenue do
55

66
use PlausibleWeb, :open_api_schema
77

8+
alias Schemas.Goal.CustomProps
9+
810
OpenApiSpex.schema(%{
911
title: "Goal.CreateRequest.Revenue",
1012
description: "Revenue Goal creation params",
@@ -21,7 +23,8 @@ defmodule PlausibleWeb.Plugins.API.Schemas.Goal.CreateRequest.Revenue do
2123
required: [:event_name, :currency],
2224
properties: %{
2325
event_name: %Schema{type: :string},
24-
currency: %Schema{type: :string}
26+
currency: %Schema{type: :string},
27+
custom_props: CustomProps.request_schema()
2528
}
2629
}
2730
},

lib/plausible_web/plugins/api/schemas/goal/custom_event.ex

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ defmodule PlausibleWeb.Plugins.API.Schemas.Goal.CustomEvent do
44
"""
55
use PlausibleWeb, :open_api_schema
66

7+
alias Schemas.Goal.CustomProps
8+
79
OpenApiSpex.schema(%{
810
description: "Custom Event Goal object",
911
title: "Goal.CustomEvent",
@@ -20,7 +22,8 @@ defmodule PlausibleWeb.Plugins.API.Schemas.Goal.CustomEvent do
2022
properties: %{
2123
id: %Schema{type: :integer, description: "Goal ID", readOnly: true},
2224
display_name: %Schema{type: :string, description: "Display name", readOnly: true},
23-
event_name: %Schema{type: :string, description: "Event Name"}
25+
event_name: %Schema{type: :string, description: "Event Name"},
26+
custom_props: CustomProps.response_schema()
2427
}
2528
}
2629
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
defmodule PlausibleWeb.Plugins.API.Schemas.Goal.CustomProps do
2+
@moduledoc """
3+
Reusable OpenAPI schema definitions for Custom Properties in Goals
4+
"""
5+
6+
alias OpenApiSpex.Schema
7+
8+
def response_schema do
9+
%Schema{
10+
type: :object,
11+
description: "Custom properties (string keys and values)",
12+
additionalProperties: %Schema{type: :string},
13+
readOnly: true
14+
}
15+
end
16+
17+
def request_schema do
18+
%Schema{
19+
type: :object,
20+
description: "Custom properties (max 3, string keys and values)",
21+
additionalProperties: %Schema{type: :string},
22+
maxProperties: 3
23+
}
24+
end
25+
end

lib/plausible_web/plugins/api/schemas/goal/pageview.ex

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ defmodule PlausibleWeb.Plugins.API.Schemas.Goal.Pageview do
44
"""
55
use PlausibleWeb, :open_api_schema
66

7+
alias Schemas.Goal.CustomProps
8+
79
OpenApiSpex.schema(%{
810
description: "Pageview Goal object",
911
title: "Goal.Pageview",
@@ -20,7 +22,8 @@ defmodule PlausibleWeb.Plugins.API.Schemas.Goal.Pageview do
2022
properties: %{
2123
id: %Schema{type: :integer, description: "Goal ID", readOnly: true},
2224
display_name: %Schema{type: :string, description: "Display name", readOnly: true},
23-
path: %Schema{type: :string, description: "Page Path"}
25+
path: %Schema{type: :string, description: "Page Path"},
26+
custom_props: CustomProps.response_schema()
2427
}
2528
}
2629
}

0 commit comments

Comments
 (0)