From 3e7e49e4b6c777c4186a15ea7c6355b3855ce93a Mon Sep 17 00:00:00 2001 From: Artur Pata Date: Fri, 13 Mar 2026 12:28:58 +0200 Subject: [PATCH 1/3] Add failing tests --- .../query_controller_test.exs | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 test/plausible_web/controllers/api/stats_controller/query_controller_test.exs diff --git a/test/plausible_web/controllers/api/stats_controller/query_controller_test.exs b/test/plausible_web/controllers/api/stats_controller/query_controller_test.exs new file mode 100644 index 000000000000..8c86d4cced6a --- /dev/null +++ b/test/plausible_web/controllers/api/stats_controller/query_controller_test.exs @@ -0,0 +1,136 @@ +defmodule PlausibleWeb.Api.InternalController.QueryTest do + use PlausibleWeb.ConnCase, async: false + use Plausible.Repo + + describe "POST /api/:domain/query shared/public" do + test "returns aggregated metrics for public site", %{conn: conn} do + site = new_site(public: true) + + populate_stats(site, [ + build(:pageview, user_id: 5, pathname: "/foo", timestamp: ~N[2021-01-01 00:00:00]) + ]) + + conn = + post(conn, "/api/stats/#{URI.encode(site.domain)}/query", %{ + "metrics" => ["pageviews"], + "date_range" => "all", + "filters" => [["is", "event:page", ["/foo"]]] + }) + + assert json_response(conn, 200)["results"] == [ + %{"metrics" => [1], "dimensions" => []} + ] + end + + test "returns aggregated metrics with shared link auth", %{conn: conn} do + site = new_site() + + populate_stats(site, [ + build(:pageview, user_id: 5, pathname: "/foo", timestamp: ~N[2021-01-01 00:00:00]), + build(:pageview, user_id: 5, timestamp: ~N[2021-01-01 00:25:00]), + build(:pageview, timestamp: ~N[2021-01-01 00:00:00]) + ]) + + link = insert(:shared_link, site: site) + + conn = + post(conn, "/api/stats/#{URI.encode(site.domain)}/query?auth=#{link.slug}", %{ + "metrics" => ["pageviews"], + "date_range" => "all", + "filters" => [["is", "event:page", ["/foo"]]] + }) + + assert json_response(conn, 200)["results"] == [%{"metrics" => [1], "dimensions" => []}] + end + + test "returns metrics with shared link auth that is limited to segment", %{conn: conn} do + site = new_site() + + populate_stats(site, [ + build(:pageview, user_id: 5, pathname: "/foo", timestamp: ~N[2021-01-01 00:00:00]), + build(:pageview, user_id: 5, timestamp: ~N[2021-01-01 00:25:00]), + build(:pageview, timestamp: ~N[2021-01-01 00:00:00]) + ]) + + segment = + insert(:segment, + site: site, + name: "Scandinavia", + type: :site, + segment_data: %{"filters" => [["is", "event:page", ["/foo"]]]} + ) + + link = insert(:shared_link, segment_id: segment.id, site: site) + + conn = + post(conn, "/api/stats/#{URI.encode(site.domain)}/query?auth=#{link.slug}", %{ + "metrics" => ["pageviews"], + "date_range" => "all", + "filters" => [["is", "segment", [segment.id]]] + }) + + assert json_response(conn, 200)["results"] == [%{"metrics" => [1], "dimensions" => []}] + end + + test "errors when expected segment filter not present for shared link auth that is limited to segment", + %{conn: conn} do + site = new_site() + + segment = + insert(:segment, + site: site, + name: "Scandinavia", + type: :site, + segment_data: %{"filters" => [["is", "event:page", ["/foo"]]]} + ) + + link = insert(:shared_link, segment_id: segment.id, site: site) + + conn = + post(conn, "/api/stats/#{URI.encode(site.domain)}/query?auth=#{link.slug}", %{ + "metrics" => ["pageviews"], + "date_range" => "all", + "filters" => [] + }) + + assert json_response(conn, 400) == %{ + "error" => "The first filter must be for the segment with id #{segment.id}" + } + end + end + + describe "POST /api/:domain/query" do + setup [:create_user, :create_site, :log_in] + + test "rejects when accessing any other site", %{conn: conn, site: site} do + conn = + post(conn, "/api/stats/any.other.site/query", %{ + "site_id" => site.domain, # ignored + "metrics" => ["pageviews"], + "date_range" => "all", + "filters" => [] + }) + + assert json_response(conn, 404) == %{ + "error" => "Site does not exist or user does not have sufficient access." + } + end + + test "returns aggregated metrics", %{conn: conn, site: site} do + populate_stats(site, [ + build(:pageview, user_id: 5, timestamp: ~N[2021-01-01 00:00:00]), + build(:pageview, user_id: 5, timestamp: ~N[2021-01-01 00:25:00]), + build(:pageview, timestamp: ~N[2021-01-01 00:00:00]) + ]) + + conn = + post(conn, "/api/stats/#{URI.encode(site.domain)}/query?site_id=ignored", %{ + "metrics" => ["pageviews"], + "date_range" => "all", + "filters" => [] + }) + + assert json_response(conn, 200)["results"] == [%{"metrics" => [3], "dimensions" => []}] + end + end +end From d188b218acfab4f59d71617089f5bdaf14706592 Mon Sep 17 00:00:00 2001 From: Artur Pata Date: Fri, 13 Mar 2026 12:41:02 +0200 Subject: [PATCH 2/3] Fix issue with top stats not loading for shared links limited to segment --- .../controllers/api/stats_controller.ex | 21 ++++++++++++++----- .../query_controller_test.exs | 3 ++- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/lib/plausible_web/controllers/api/stats_controller.ex b/lib/plausible_web/controllers/api/stats_controller.ex index 9d471c664b82..07461450fe48 100644 --- a/lib/plausible_web/controllers/api/stats_controller.ex +++ b/lib/plausible_web/controllers/api/stats_controller.ex @@ -1647,7 +1647,9 @@ defmodule PlausibleWeb.Api.StatsController do _opts ) when is_integer(segment_id) do - case ensure_expected_segment_filter_present(conn.params, segment_id) do + case conn.params + |> get_filters_param() + |> ensure_expected_segment_filter_present(segment_id) do :ok -> conn @@ -1661,11 +1663,20 @@ defmodule PlausibleWeb.Api.StatsController do defp validate_required_filters_plug(conn, _opts), do: conn + defp get_filters_param(%{"filters" => filters} = _params) when is_binary(filters) do + JSON.decode!(filters) + end + + defp get_filters_param(%{"filters" => filters} = _params) when is_list(filters) do + filters + end + defp ensure_expected_segment_filter_present( - %{"filters" => filters} = _params, + filters, expected_segment_id - ) do - case JSON.decode!(filters) do + ) + when is_list(filters) do + case filters do [["is", "segment", [segment_id]] | _other_filters] when segment_id == expected_segment_id -> :ok @@ -1674,7 +1685,7 @@ defmodule PlausibleWeb.Api.StatsController do end end - defp ensure_expected_segment_filter_present(_params, _expected_segment_id) do + defp ensure_expected_segment_filter_present(_filters, _expected_segment_id) do :error end diff --git a/test/plausible_web/controllers/api/stats_controller/query_controller_test.exs b/test/plausible_web/controllers/api/stats_controller/query_controller_test.exs index 8c86d4cced6a..d7bab5c17d9b 100644 --- a/test/plausible_web/controllers/api/stats_controller/query_controller_test.exs +++ b/test/plausible_web/controllers/api/stats_controller/query_controller_test.exs @@ -105,7 +105,8 @@ defmodule PlausibleWeb.Api.InternalController.QueryTest do test "rejects when accessing any other site", %{conn: conn, site: site} do conn = post(conn, "/api/stats/any.other.site/query", %{ - "site_id" => site.domain, # ignored + # ignored + "site_id" => site.domain, "metrics" => ["pageviews"], "date_range" => "all", "filters" => [] From 1ea3ce22167059def4c733fb5228840ce555a614 Mon Sep 17 00:00:00 2001 From: Artur Pata Date: Fri, 13 Mar 2026 12:48:31 +0200 Subject: [PATCH 3/3] Fix issue with missing case for get_filters_param --- lib/plausible_web/controllers/api/stats_controller.ex | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/plausible_web/controllers/api/stats_controller.ex b/lib/plausible_web/controllers/api/stats_controller.ex index 07461450fe48..ab3f74209745 100644 --- a/lib/plausible_web/controllers/api/stats_controller.ex +++ b/lib/plausible_web/controllers/api/stats_controller.ex @@ -1671,6 +1671,10 @@ defmodule PlausibleWeb.Api.StatsController do filters end + defp get_filters_param(_params) do + nil + end + defp ensure_expected_segment_filter_present( filters, expected_segment_id