Skip to content

Commit ae00c3c

Browse files
authored
Get rid of old /top-stats endpoint (#6176)
* transform top_stats_test.exs to target /query endpoint * change test assertion according to new behaviour With the switch to the /query endpoint, the aggregate query with a page filter started returning `time_on_page` as `nil` without any engagement data, instead of 0 as before. When this happens, Time on Page is simply hidden in Top Stats. * remove old /top-stats endpoint * tag test ee only * review feebdack * format * fix function signature * add filters key
1 parent 7b18ffc commit ae00c3c

5 files changed

Lines changed: 656 additions & 1273 deletions

File tree

lib/plausible_web/controllers/api/stats_controller.ex

Lines changed: 0 additions & 242 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ defmodule PlausibleWeb.Api.StatsController do
1212
Filters,
1313
Time,
1414
TableDecider,
15-
TimeOnPage,
1615
Dashboard,
1716
ParsedQueryParams,
1817
QueryBuilder,
@@ -207,56 +206,6 @@ defmodule PlausibleWeb.Api.StatsController do
207206
end
208207
end
209208

210-
def top_stats(conn, params) do
211-
site = conn.assigns[:site]
212-
213-
params = realtime_period_to_30m(params)
214-
215-
query =
216-
site
217-
|> Query.from(params, debug_metadata: debug_metadata(conn))
218-
|> Query.set_include(:imports_meta, true)
219-
220-
%{
221-
top_stats: top_stats,
222-
meta: meta,
223-
sample_percent: sample_percent,
224-
graphable_metrics: graphable_metrics
225-
} = fetch_top_stats(site, query)
226-
227-
comparison_query = comparison_query(query)
228-
229-
json(conn, %{
230-
top_stats: top_stats,
231-
meta: meta,
232-
graphable_metrics: graphable_metrics,
233-
interval: query.interval,
234-
sample_percent: sample_percent,
235-
with_imported_switch: with_imported_switch_info(meta),
236-
includes_imported: meta[:imports_included] == true,
237-
comparing_from: query.include.compare && Query.date_range(comparison_query).first,
238-
comparing_to: query.include.compare && Query.date_range(comparison_query).last,
239-
from: Query.date_range(query).first,
240-
to: Query.date_range(query).last
241-
})
242-
end
243-
244-
defp with_imported_switch_info(%Jason.OrderedObject{} = meta) do
245-
case {meta[:imports_included], meta[:imports_skip_reason]} do
246-
{true, nil} ->
247-
%{visible: true, togglable: true, tooltip_msg: "Click to exclude imported data"}
248-
249-
{false, nil} ->
250-
%{visible: true, togglable: true, tooltip_msg: "Click to include imported data"}
251-
252-
{false, :unsupported_query} ->
253-
%{visible: true, togglable: false, tooltip_msg: "Imported data cannot be included"}
254-
255-
{false, reason} when reason in [:no_imported_data, :out_of_range] ->
256-
%{visible: false, togglable: false, tooltip_msg: nil}
257-
end
258-
end
259-
260209
defp present_index_for(site, query, dates) do
261210
case query.interval do
262211
"hour" ->
@@ -303,197 +252,6 @@ defmodule PlausibleWeb.Api.StatsController do
303252
end
304253
end
305254

306-
defp fetch_top_stats(site, query) do
307-
goal_filter? =
308-
toplevel_goal_filter?(query)
309-
310-
cond do
311-
query.input_date_range == :realtime_30m && goal_filter? ->
312-
fetch_goal_realtime_top_stats(site, query)
313-
314-
query.input_date_range == :realtime_30m ->
315-
fetch_realtime_top_stats(site, query)
316-
317-
goal_filter? ->
318-
fetch_goal_top_stats(site, query)
319-
320-
true ->
321-
fetch_other_top_stats(site, query)
322-
end
323-
end
324-
325-
defp fetch_goal_realtime_top_stats(site, query) do
326-
query = Query.set_include(query, :compare, nil)
327-
328-
%{
329-
results: %{
330-
visitors: %{value: unique_conversions},
331-
events: %{value: total_conversions}
332-
},
333-
meta: meta
334-
} = Stats.aggregate(site, query, [:visitors, :events])
335-
336-
top_stats = [
337-
%{
338-
name: "Current visitors",
339-
graph_metric: :current_visitors,
340-
value: Stats.current_visitors(site)
341-
},
342-
%{
343-
name: "Unique conversions (last 30 min)",
344-
graph_metric: :visitors,
345-
value: unique_conversions
346-
},
347-
%{
348-
name: "Total conversions (last 30 min)",
349-
graph_metric: :events,
350-
value: total_conversions
351-
}
352-
]
353-
354-
%{
355-
top_stats: top_stats,
356-
meta: meta,
357-
graphable_metrics: [:visitors, :events],
358-
sample_percent: 100
359-
}
360-
end
361-
362-
defp fetch_realtime_top_stats(site, query) do
363-
query = Query.set_include(query, :compare, nil)
364-
365-
%{
366-
results: %{
367-
visitors: %{value: visitors},
368-
pageviews: %{value: pageviews}
369-
},
370-
meta: meta
371-
} = Stats.aggregate(site, query, [:visitors, :pageviews])
372-
373-
top_stats = [
374-
%{
375-
name: "Current visitors",
376-
graph_metric: :current_visitors,
377-
value: Stats.current_visitors(site)
378-
},
379-
%{
380-
name: "Unique visitors (last 30 min)",
381-
graph_metric: :visitors,
382-
value: visitors
383-
},
384-
%{
385-
name: "Pageviews (last 30 min)",
386-
graph_metric: :pageviews,
387-
value: pageviews
388-
}
389-
]
390-
391-
%{
392-
top_stats: top_stats,
393-
meta: meta,
394-
sample_percent: 100,
395-
graphable_metrics: [:visitors, :pageviews]
396-
}
397-
end
398-
399-
defp fetch_goal_top_stats(site, query) do
400-
metrics =
401-
[:visitors, :events, :conversion_rate] ++ @revenue_metrics
402-
403-
%{results: results, meta: meta} = Stats.aggregate(site, query, metrics)
404-
405-
top_stats =
406-
[
407-
top_stats_entry(results, "Unique conversions", :visitors),
408-
top_stats_entry(results, "Total conversions", :events),
409-
on_ee do
410-
top_stats_entry(results, "Average revenue", :average_revenue)
411-
end,
412-
on_ee do
413-
top_stats_entry(results, "Total revenue", :total_revenue)
414-
end,
415-
top_stats_entry(results, "Conversion rate", :conversion_rate)
416-
]
417-
|> Enum.reject(&is_nil/1)
418-
419-
%{top_stats: top_stats, meta: meta, graphable_metrics: metrics, sample_percent: 100}
420-
end
421-
422-
defp fetch_other_top_stats(site, query) do
423-
page_filter? =
424-
Filters.filtering_on_dimension?(query, "event:page", behavioral_filters: :ignore)
425-
426-
metrics = [:visitors, :visits, :pageviews, :sample_percent]
427-
428-
metrics =
429-
cond do
430-
page_filter? and query.include_imported ->
431-
metrics ++ [:bounce_rate, :scroll_depth]
432-
433-
page_filter? ->
434-
metrics ++ [:bounce_rate, :scroll_depth, :time_on_page]
435-
436-
true ->
437-
metrics ++ [:views_per_visit, :bounce_rate, :visit_duration]
438-
end
439-
440-
%{results: results, meta: meta} = Stats.aggregate(site, query, metrics)
441-
442-
top_stats =
443-
[
444-
top_stats_entry(results, "Unique visitors", :visitors),
445-
top_stats_entry(results, "Total visits", :visits),
446-
top_stats_entry(results, "Total pageviews", :pageviews),
447-
top_stats_entry(results, "Views per visit", :views_per_visit),
448-
top_stats_entry(results, "Bounce rate", :bounce_rate),
449-
top_stats_entry(results, "Visit duration", :visit_duration),
450-
top_stats_entry(results, "Time on page", :time_on_page,
451-
formatter: fn
452-
nil -> 0
453-
value -> value
454-
end
455-
),
456-
top_stats_entry(results, "Scroll depth", :scroll_depth)
457-
]
458-
|> Enum.filter(& &1)
459-
460-
sample_percent = results[:sample_percent][:value]
461-
462-
%{
463-
top_stats: top_stats,
464-
meta: meta,
465-
graphable_metrics:
466-
if(TimeOnPage.new_time_on_page_visible?(site),
467-
do: metrics,
468-
else: metrics -- [:time_on_page]
469-
),
470-
sample_percent: sample_percent
471-
}
472-
end
473-
474-
defp top_stats_entry(current_results, name, key, opts \\ []) do
475-
if current_results[key] do
476-
formatter = Keyword.get(opts, :formatter, & &1)
477-
value = get_in(current_results, [key, :value])
478-
479-
%{name: name, value: formatter.(value), graph_metric: key}
480-
|> maybe_put_comparison(current_results, key, formatter)
481-
end
482-
end
483-
484-
defp maybe_put_comparison(entry, results, key, formatter) do
485-
prev_value = get_in(results, [key, :comparison_value])
486-
change = get_in(results, [key, :change])
487-
488-
if prev_value do
489-
entry
490-
|> Map.put(:comparison_value, formatter.(prev_value))
491-
|> Map.put(:change, change)
492-
else
493-
entry
494-
end
495-
end
496-
497255
def sources(conn, params) do
498256
site = conn.assigns[:site]
499257
params = Map.put(params, "property", "visit:source")

lib/plausible_web/router.ex

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,6 @@ defmodule PlausibleWeb.Router do
286286
post "/:domain/query", StatsController, :query
287287
get "/:domain/current-visitors", StatsController, :current_visitors
288288
get "/:domain/main-graph", StatsController, :main_graph
289-
get "/:domain/top-stats", StatsController, :top_stats
290289
get "/:domain/sources", StatsController, :sources
291290
get "/:domain/channels", StatsController, :channels
292291
get "/:domain/utm_mediums", StatsController, :utm_mediums

test/plausible_web/controllers/api/stats_controller/authorization_test.exs

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
defmodule PlausibleWeb.Api.StatsController.AuthorizationTest do
22
use PlausibleWeb.ConnCase, async: true
33

4+
defp query_visitors(conn, site, params) do
5+
params =
6+
[date_range: "day", metrics: ["visitors"], filters: []]
7+
|> Keyword.merge(params)
8+
9+
post(conn, Routes.stats_path(conn, :query, site.domain), params)
10+
end
11+
412
describe "API authorization - as anonymous user" do
513
test "returns 404 for a site that doesn't exist", %{conn: conn} do
614
conn = init_session(conn)
@@ -34,7 +42,7 @@ defmodule PlausibleWeb.Api.StatsController.AuthorizationTest do
3442
test "returns 404 for non-existent shared link", %{conn: conn} do
3543
site = new_site()
3644

37-
conn = get(conn, "/api/stats/#{site.domain}/top-stats?auth=does-not-exist")
45+
conn = query_visitors(conn, site, auth: "does-not-exist")
3846

3947
assert json_response(conn, 404) == %{
4048
"error" => "Site does not exist or user does not have sufficient access."
@@ -45,9 +53,9 @@ defmodule PlausibleWeb.Api.StatsController.AuthorizationTest do
4553
site = new_site()
4654
link = insert(:shared_link, site: site)
4755

48-
conn = get(conn, "/api/stats/#{site.domain}/top-stats?auth=#{link.slug}")
56+
conn = query_visitors(conn, site, auth: link.slug)
4957

50-
assert %{"top_stats" => _any} = json_response(conn, 200)
58+
assert %{"results" => _any} = json_response(conn, 200)
5159
end
5260

5361
test "returns 200 for password-protected link with valid cookie", %{conn: conn} do
@@ -62,9 +70,9 @@ defmodule PlausibleWeb.Api.StatsController.AuthorizationTest do
6270
conn =
6371
conn
6472
|> put_req_cookie(cookie_name, token)
65-
|> get("/api/stats/#{site.domain}/top-stats?auth=#{link.slug}")
73+
|> query_visitors(site, auth: link.slug)
6674

67-
assert %{"top_stats" => _any} = json_response(conn, 200)
75+
assert %{"results" => _any} = json_response(conn, 200)
6876
end
6977

7078
test "returns 404 for password-protected link with invalid cookie value", %{conn: conn} do
@@ -86,7 +94,7 @@ defmodule PlausibleWeb.Api.StatsController.AuthorizationTest do
8694
conn =
8795
conn
8896
|> put_req_cookie(cookie_name, other_link_token)
89-
|> get("/api/stats/#{site.domain}/top-stats?auth=#{link.slug}")
97+
|> query_visitors(site, auth: link.slug)
9098

9199
assert json_response(conn, 404) == %{
92100
"error" => "Site does not exist or user does not have sufficient access."
@@ -99,7 +107,7 @@ defmodule PlausibleWeb.Api.StatsController.AuthorizationTest do
99107
link =
100108
insert(:shared_link, site: site, password_hash: Plausible.Auth.Password.hash("password"))
101109

102-
conn = get(conn, "/api/stats/#{site.domain}/top-stats?auth=#{link.slug}")
110+
conn = query_visitors(conn, site, auth: link.slug)
103111

104112
assert json_response(conn, 404) == %{
105113
"error" => "Site does not exist or user does not have sufficient access."
@@ -116,7 +124,7 @@ defmodule PlausibleWeb.Api.StatsController.AuthorizationTest do
116124
link =
117125
insert(:shared_link, site: site, segment: segment)
118126

119-
conn = get(conn, "/api/stats/#{site.domain}/top-stats?auth=#{link.slug}")
127+
conn = query_visitors(conn, site, auth: link.slug)
120128

121129
assert json_response(conn, 400) == %{
122130
"error" => "The first filter must be for the segment with id #{segment.id}"
@@ -133,9 +141,9 @@ defmodule PlausibleWeb.Api.StatsController.AuthorizationTest do
133141
insert(:shared_link, site: site, segment: segment)
134142

135143
conn =
136-
get(
137-
conn,
138-
"/api/stats/#{site.domain}/top-stats?auth=#{link.slug}&filters=#{JSON.encode!([["is", "segment", [segment.id + 1]]])}"
144+
query_visitors(conn, site,
145+
auth: link.slug,
146+
filters: [["is", "segment", [segment.id + 1]]]
139147
)
140148

141149
assert json_response(conn, 400) == %{
@@ -153,9 +161,12 @@ defmodule PlausibleWeb.Api.StatsController.AuthorizationTest do
153161
insert(:shared_link, site: site, segment: segment)
154162

155163
conn =
156-
get(
157-
conn,
158-
"/api/stats/#{site.domain}/top-stats?auth=#{link.slug}&filters=#{JSON.encode!([["is", "segment", [segment.id]], ["is", "event:page", ["/docs"]]])}"
164+
query_visitors(conn, site,
165+
auth: link.slug,
166+
filters: [
167+
["is", "segment", [segment.id]],
168+
["is", "event:page", ["/docs"]]
169+
]
159170
)
160171

161172
assert json_response(conn, 200)

0 commit comments

Comments
 (0)