mirror of
https://github.com/plausible/analytics.git
synced 2024-12-23 17:44:43 +03:00
Pull 1506 - formatted (#1527)
* adding api route PUT /api/v1/sites/goals with form fields "goal_type" and "goal_value" with supported types "event" and "page" * arm64 docker images * adding api route DELETE /api/v1/sites/goals/:goal_id with form param "site_id" * revert makfile + package.json * return statement hotfix in case site could not be found * adding api route PUT /api/v1/sites/goals/:goal_id with form params "site_id", "goal_type", and "goal_value" * update the goal api routes to accept event_name or page_path instead of goal_value * cleaning goals model * mix format Co-authored-by: Ahmed Abbas <a.abbas@ixdc.net>
This commit is contained in:
parent
442e401ede
commit
b49fd19934
@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
- API route `PUT /api/v1/sites/goals` with form params `site_id`, `event_name` and/or `page_path`, and `goal_type` with supported types `event` and `page`
|
||||
- API route `DELETE /api/v1/sites/goals/:goal_id` with form params `site_id`
|
||||
|
||||
### Added
|
||||
- Data exported via the download button will contain CSV data for all visible graps in a zip file.
|
||||
- Region and city-level geolocation plausible/analytics#1449
|
||||
|
@ -2,6 +2,7 @@ defmodule Plausible.Goal do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
@derive {Jason.Encoder, only: [:domain, :event_name, :page_path]}
|
||||
schema "goals" do
|
||||
field :domain, :string
|
||||
field :event_name, :string
|
||||
|
@ -3,6 +3,7 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
|
||||
use Plausible.Repo
|
||||
use Plug.ErrorHandler
|
||||
alias Plausible.Sites
|
||||
alias Plausible.Goals
|
||||
alias PlausibleWeb.Api.Helpers, as: H
|
||||
|
||||
def create_site(conn, params) do
|
||||
@ -80,6 +81,127 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
|
||||
end
|
||||
end
|
||||
|
||||
def find_or_create_goal(conn, params) do
|
||||
with {:ok, site_id} <- expect_param_key(params, "site_id"),
|
||||
{:ok, goal_type} <- expect_param_key(params, "goal_type"),
|
||||
site when not is_nil(site) <-
|
||||
Sites.get_for_user(conn.assigns[:current_user].id, site_id, [:owner, :admin]) do
|
||||
case goal_type do
|
||||
"event" ->
|
||||
with {:ok, event_name} <- expect_param_key(params, "event_name") do
|
||||
goal = Repo.get_by(Plausible.Goal, domain: site_id, event_name: event_name)
|
||||
|
||||
goal =
|
||||
case goal do
|
||||
nil -> Goals.create(site, %{"event_name" => event_name})
|
||||
goal -> {:ok, goal}
|
||||
end
|
||||
|
||||
case goal do
|
||||
{:ok, goal} ->
|
||||
json(conn, %{
|
||||
domain: site_id,
|
||||
goal_id: goal.id,
|
||||
goal_type: goal_type,
|
||||
event_name: goal.event_name
|
||||
})
|
||||
|
||||
nil ->
|
||||
H.not_found(conn, "Goal could not be found")
|
||||
end
|
||||
else
|
||||
nil ->
|
||||
H.not_found(conn, "Goal could not be found")
|
||||
|
||||
{:missing, "event_name"} ->
|
||||
H.bad_request(conn, "Parameter `event_name` is required to create a goal")
|
||||
|
||||
e ->
|
||||
H.bad_request(conn, "Something went wrong: #{inspect(e)}")
|
||||
end
|
||||
|
||||
"page" ->
|
||||
with {:ok, page_path} <- expect_param_key(params, "page_path") do
|
||||
goal = Repo.get_by(Plausible.Goal, domain: site_id, page_path: page_path)
|
||||
|
||||
goal =
|
||||
case goal do
|
||||
nil -> Goals.create(site, %{"page_path" => page_path})
|
||||
goal -> {:ok, goal}
|
||||
end
|
||||
|
||||
case goal do
|
||||
{:ok, goal} ->
|
||||
json(conn, %{
|
||||
domain: site_id,
|
||||
goal_id: goal.id,
|
||||
goal_type: goal_type,
|
||||
page_path: goal.page_path
|
||||
})
|
||||
|
||||
nil ->
|
||||
H.not_found(conn, "Goal could not be found")
|
||||
end
|
||||
else
|
||||
nil ->
|
||||
H.not_found(conn, "Goal could not be found")
|
||||
|
||||
{:missing, "page_path"} ->
|
||||
H.bad_request(conn, "Parameter `page_path` is required to create a goal")
|
||||
|
||||
e ->
|
||||
H.bad_request(conn, "Something went wrong: #{inspect(e)}")
|
||||
end
|
||||
|
||||
_ ->
|
||||
H.not_found(conn, "Invalid goal type")
|
||||
end
|
||||
else
|
||||
nil ->
|
||||
H.not_found(conn, "Site could not be found")
|
||||
|
||||
{:missing, "site_id"} ->
|
||||
H.bad_request(conn, "Parameter `site_id` is required to create a goal")
|
||||
|
||||
{:missing, "goal_type"} ->
|
||||
H.bad_request(conn, "Parameter `goal_type` is required to create a goal")
|
||||
|
||||
{:missing, "goal_value"} ->
|
||||
H.bad_request(conn, "Parameter `goal_value` is required to create a goal")
|
||||
|
||||
e ->
|
||||
H.bad_request(conn, "Something went wrong: #{inspect(e)}")
|
||||
end
|
||||
end
|
||||
|
||||
def delete_goal(conn, params) do
|
||||
with {:ok, site_id} <- expect_param_key(params, "site_id"),
|
||||
{:ok, goal_id} <- expect_param_key(params, "goal_id"),
|
||||
site when not is_nil(site) <-
|
||||
Sites.get_for_user(conn.assigns[:current_user].id, site_id, [:owner]) do
|
||||
goal = Repo.get_by(Plausible.Goal, id: goal_id)
|
||||
|
||||
if goal do
|
||||
Goals.delete(goal_id)
|
||||
json(conn, %{"deleted" => true})
|
||||
else
|
||||
H.not_found(conn, "Goal could not be found")
|
||||
end
|
||||
else
|
||||
nil ->
|
||||
H.not_found(conn, "Site could not be found")
|
||||
|
||||
{:missing, "site_id"} ->
|
||||
H.bad_request(conn, "Parameter `site_id` is required to delete a goal")
|
||||
|
||||
{:missing, "goal_id"} ->
|
||||
H.bad_request(conn, "Parameter `goal_id` is required to delete a goal")
|
||||
|
||||
e ->
|
||||
H.bad_request(conn, "Something went wrong: #{inspect(e)}")
|
||||
end
|
||||
end
|
||||
|
||||
defp serialize_errors(changeset) do
|
||||
{field, {msg, _opts}} = List.first(changeset.errors)
|
||||
error_msg = Atom.to_string(field) <> " " <> msg
|
||||
|
@ -88,6 +88,8 @@ defmodule PlausibleWeb.Router do
|
||||
post "/", ExternalSitesController, :create_site
|
||||
delete "/:site_id", ExternalSitesController, :delete_site
|
||||
put "/shared-links", ExternalSitesController, :find_or_create_shared_link
|
||||
put "/goals", ExternalSitesController, :find_or_create_goal
|
||||
delete "/goals/:goal_id", ExternalSitesController, :delete_goal
|
||||
end
|
||||
|
||||
scope "/api", PlausibleWeb do
|
||||
|
@ -191,4 +191,195 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
|
||||
assert res["error"] == "Site could not be found"
|
||||
end
|
||||
end
|
||||
|
||||
describe "PUT /api/v1/sites/goals" do
|
||||
setup :create_site
|
||||
|
||||
test "can add a goal as event to a site", %{conn: conn, site: site} do
|
||||
conn =
|
||||
put(conn, "/api/v1/sites/goals", %{
|
||||
site_id: site.domain,
|
||||
goal_type: "event",
|
||||
event_name: "Signup"
|
||||
})
|
||||
|
||||
res = json_response(conn, 200)
|
||||
assert res["event_name"] == "Signup"
|
||||
end
|
||||
|
||||
test "can add a goal as page to a site", %{conn: conn, site: site} do
|
||||
conn =
|
||||
put(conn, "/api/v1/sites/goals", %{
|
||||
site_id: site.domain,
|
||||
goal_type: "page",
|
||||
page_path: "/signup"
|
||||
})
|
||||
|
||||
res = json_response(conn, 200)
|
||||
assert res["page_path"] == "/signup"
|
||||
end
|
||||
|
||||
test "is idempotent find or create op", %{conn: conn, site: site} do
|
||||
conn =
|
||||
put(conn, "/api/v1/sites/goals", %{
|
||||
site_id: site.domain,
|
||||
goal_type: "event",
|
||||
event_name: "Signup"
|
||||
})
|
||||
|
||||
%{"goal_id" => goal_id} = json_response(conn, 200)
|
||||
|
||||
conn =
|
||||
put(conn, "/api/v1/sites/goals", %{
|
||||
site_id: site.domain,
|
||||
goal_type: "event",
|
||||
event_name: "Signup"
|
||||
})
|
||||
|
||||
assert %{"goal_id" => ^goal_id} = json_response(conn, 200)
|
||||
end
|
||||
|
||||
test "returns 400 when site id missing", %{conn: conn} do
|
||||
conn =
|
||||
put(conn, "/api/v1/sites/goals", %{
|
||||
goal_type: "event",
|
||||
event_name: "Signup"
|
||||
})
|
||||
|
||||
res = json_response(conn, 400)
|
||||
assert res["error"] == "Parameter `site_id` is required to create a goal"
|
||||
end
|
||||
|
||||
test "returns 404 when site id is non existent", %{conn: conn} do
|
||||
conn =
|
||||
put(conn, "/api/v1/sites/goals", %{
|
||||
goal_type: "event",
|
||||
event_name: "Signup",
|
||||
site_id: "bad"
|
||||
})
|
||||
|
||||
res = json_response(conn, 404)
|
||||
assert res["error"] == "Site could not be found"
|
||||
end
|
||||
|
||||
test "returns 404 when api key owner does not have permissions to create a goal", %{
|
||||
conn: conn,
|
||||
site: site,
|
||||
user: user
|
||||
} do
|
||||
Repo.update_all(
|
||||
from(sm in Plausible.Site.Membership,
|
||||
where: sm.site_id == ^site.id and sm.user_id == ^user.id
|
||||
),
|
||||
set: [role: :viewer]
|
||||
)
|
||||
|
||||
conn =
|
||||
put(conn, "/api/v1/sites/goals", %{
|
||||
site_id: site.domain,
|
||||
goal_type: "event",
|
||||
event_name: "Signup"
|
||||
})
|
||||
|
||||
res = json_response(conn, 404)
|
||||
assert res["error"] == "Site could not be found"
|
||||
end
|
||||
|
||||
test "returns 400 when goal type missing", %{conn: conn, site: site} do
|
||||
conn =
|
||||
put(conn, "/api/v1/sites/goals", %{
|
||||
site_id: site.domain,
|
||||
event_name: "Signup"
|
||||
})
|
||||
|
||||
res = json_response(conn, 400)
|
||||
assert res["error"] == "Parameter `goal_type` is required to create a goal"
|
||||
end
|
||||
|
||||
test "returns 400 when goal event name missing", %{conn: conn, site: site} do
|
||||
conn =
|
||||
put(conn, "/api/v1/sites/goals", %{
|
||||
site_id: site.domain,
|
||||
goal_type: "event"
|
||||
})
|
||||
|
||||
res = json_response(conn, 400)
|
||||
assert res["error"] == "Parameter `event_name` is required to create a goal"
|
||||
end
|
||||
|
||||
test "returns 400 when goal page path missing", %{conn: conn, site: site} do
|
||||
conn =
|
||||
put(conn, "/api/v1/sites/goals", %{
|
||||
site_id: site.domain,
|
||||
goal_type: "page"
|
||||
})
|
||||
|
||||
res = json_response(conn, 400)
|
||||
assert res["error"] == "Parameter `page_path` is required to create a goal"
|
||||
end
|
||||
end
|
||||
|
||||
describe "DELETE /api/v1/sites/goals/:goal_id" do
|
||||
setup :create_new_site
|
||||
|
||||
test "delete a goal by it's id", %{conn: conn, site: site} do
|
||||
conn =
|
||||
put(conn, "/api/v1/sites/goals", %{
|
||||
site_id: site.domain,
|
||||
goal_type: "event",
|
||||
event_name: "Signup"
|
||||
})
|
||||
|
||||
%{"goal_id" => goal_id} = json_response(conn, 200)
|
||||
|
||||
conn =
|
||||
delete(conn, "/api/v1/sites/goals/#{goal_id}", %{
|
||||
site_id: site.domain
|
||||
})
|
||||
|
||||
assert json_response(conn, 200) == %{"deleted" => true}
|
||||
end
|
||||
|
||||
test "is 404 when goal cannot be found", %{conn: conn, site: site} do
|
||||
conn =
|
||||
delete(conn, "/api/v1/sites/goals/0", %{
|
||||
site_id: site.domain
|
||||
})
|
||||
|
||||
assert json_response(conn, 404) == %{"error" => "Goal could not be found"}
|
||||
end
|
||||
|
||||
test "cannot delete a goal belongs to a site that the user does not own", %{
|
||||
conn: conn,
|
||||
user: user
|
||||
} do
|
||||
site = insert(:site, members: [])
|
||||
insert(:site_membership, user: user, site: site, role: :admin)
|
||||
|
||||
conn =
|
||||
delete(conn, "/api/v1/sites/goals/1", %{
|
||||
site_id: site.domain
|
||||
})
|
||||
|
||||
assert json_response(conn, 404) == %{"error" => "Site could not be found"}
|
||||
end
|
||||
|
||||
test "cannot access with a bad API key scope", %{conn: conn, site: site, user: user} do
|
||||
api_key = insert(:api_key, user: user, scopes: ["stats:read:*"])
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> Plug.Conn.put_req_header("authorization", "Bearer #{api_key.key}")
|
||||
|
||||
conn =
|
||||
delete(conn, "/api/v1/sites/goals/1", %{
|
||||
site_id: site.domain
|
||||
})
|
||||
|
||||
assert json_response(conn, 401) == %{
|
||||
"error" =>
|
||||
"Invalid API key. Please make sure you're using a valid API key with access to the resource you've requested."
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
Loading…
Reference in New Issue
Block a user