From 3a371fdf4dc6cf607477ceb570e560c1a3b7d035 Mon Sep 17 00:00:00 2001 From: RobertJoonas <56999674+RobertJoonas@users.noreply.github.com> Date: Thu, 18 Apr 2024 11:12:24 +0100 Subject: [PATCH] Test new imported metrics (GA4) (#4014) * test fixture imported data with stats requests * take visits metric from the events table in event:page breakdown * Remove assert_referrers after all pageReferrer is an event scoped property in GA4, which when queried along with session-level dimensions will return unexpected data. Adding the pageReferrer dimension to the GA4 Data API request, it will cause the selected metric totals to increase significantly, even though they shouldn't. * Adjust sources and utm_mediums assertions * adjust assert_pages * Make formatter happy --------- Co-authored-by: Adrian Gruntkowski --- lib/plausible/stats/breakdown.ex | 4 +- .../imported/google_analytics4_test.exs | 335 +++++++++++++++++- 2 files changed, 334 insertions(+), 5 deletions(-) diff --git a/lib/plausible/stats/breakdown.ex b/lib/plausible/stats/breakdown.ex index 795c400de..9eb64bab2 100644 --- a/lib/plausible/stats/breakdown.ex +++ b/lib/plausible/stats/breakdown.ex @@ -11,11 +11,11 @@ defmodule Plausible.Stats.Breakdown do @no_ref "Direct / None" @not_set "(not set)" - @session_metrics [:visits, :bounce_rate, :visit_duration] + @session_metrics [:bounce_rate, :visit_duration] @revenue_metrics on_full_build(do: Plausible.Stats.Goal.Revenue.revenue_metrics(), else: []) - @event_metrics [:visitors, :pageviews, :events, :percentage] ++ @revenue_metrics + @event_metrics [:visits, :visitors, :pageviews, :events, :percentage] ++ @revenue_metrics # These metrics can be asked from the `breakdown/5` function, # but they are different from regular metrics such as `visitors`, diff --git a/test/plausible/imported/google_analytics4_test.exs b/test/plausible/imported/google_analytics4_test.exs index 687444183..54f187b22 100644 --- a/test/plausible/imported/google_analytics4_test.exs +++ b/test/plausible/imported/google_analytics4_test.exs @@ -1,10 +1,10 @@ defmodule Plausible.Imported.GoogleAnalytics4Test do - use Plausible.DataCase, async: true + use PlausibleWeb.ConnCase, async: true import Mox - import Ecto.Query, only: [from: 2] + alias Plausible.Repo alias Plausible.Imported.GoogleAnalytics4 @refresh_token_body Jason.decode!(File.read!("fixture/ga_refresh_token.json")) @@ -27,7 +27,7 @@ defmodule Plausible.Imported.GoogleAnalytics4Test do describe "parse_args/1 and import_data/2" do setup [:create_user, :create_new_site] - test "imports data returned from GA4 Data API", %{user: user, site: site} do + test "imports data returned from GA4 Data API", %{conn: conn, user: user, site: site} do past = DateTime.add(DateTime.utc_now(), -3600, :second) {:ok, job} = @@ -96,6 +96,335 @@ defmodule Plausible.Imported.GoogleAnalytics4Test do query = from(imported in table, where: imported.site_id == ^site.id) assert await_clickhouse_count(query, count) end) + + # NOTE: Consider using GoogleAnalytics.run_import instead of import_data + # to avoid having to set SiteImport to completed manually + site_import + |> Plausible.Imported.SiteImport.complete_changeset() + |> Repo.update!() + + # Assert the actual data via Stats API requests + common_params = %{ + "site_id" => site.domain, + "period" => "custom", + "date" => "2024-01-01,2024-01-31", + "with_imported" => "true" + } + + breakdown_params = + common_params + |> Map.put("metrics", "visitors,visits,pageviews,visit_duration,bounce_rate") + |> Map.put("limit", 1000) + + %{key: api_key} = insert(:api_key, user: user) + + conn = put_req_header(conn, "authorization", "Bearer #{api_key}") + + assert_timeseries(conn, common_params) + assert_pages(conn, common_params) + + assert_sources(conn, breakdown_params) + assert_utm_mediums(conn, breakdown_params) + assert_entry_pages(conn, breakdown_params) + assert_cities(conn, breakdown_params) + assert_devices(conn, breakdown_params) + assert_browsers(conn, breakdown_params) + assert_os(conn, breakdown_params) + assert_os_versions(conn, breakdown_params) end end + + defp assert_timeseries(conn, params) do + params = + Map.put( + params, + "metrics", + "visitors,visits,pageviews,views_per_visit,visit_duration,bounce_rate" + ) + + %{"results" => results} = + get(conn, "/api/v1/stats/timeseries", params) |> json_response(200) + + assert length(results) == 31 + + assert List.first(results) == %{ + "bounce_rate" => 36.0, + "date" => "2024-01-01", + "pageviews" => 224, + "views_per_visit" => 1.14, + "visit_duration" => 41.0, + "visitors" => 191, + "visits" => 197 + } + + assert List.last(results) == %{ + "bounce_rate" => 38.0, + "date" => "2024-01-31", + "pageviews" => 195, + "views_per_visit" => 1.34, + "visit_duration" => 33.0, + "visitors" => 141, + "visits" => 146 + } + end + + defp assert_sources(conn, params) do + params = Map.put(params, "property", "visit:source") + + %{"results" => results} = + get(conn, "/api/v1/stats/breakdown", params) |> json_response(200) + + assert length(results) == 26 + + assert List.first(results) == %{ + "bounce_rate" => 35.0, + "pageviews" => 6229, + "visit_duration" => 40.0, + "visitors" => 4671, + "visits" => 4917, + "source" => "Google" + } + + assert List.last(results) == %{ + "bounce_rate" => 100.0, + "pageviews" => 1, + "visit_duration" => 0.0, + "visitors" => 1, + "visits" => 1, + "source" => "petalsearch.com" + } + end + + defp assert_utm_mediums(conn, params) do + params = Map.put(params, "property", "visit:utm_medium") + + %{"results" => results} = + get(conn, "/api/v1/stats/breakdown", params) |> json_response(200) + + assert [ + %{ + "bounce_rate" => 35.0, + "pageviews" => 6399, + "utm_medium" => "organic", + "visit_duration" => 40.0, + "visitors" => 4787, + "visits" => 5042 + }, + %{ + "bounce_rate" => 58.0, + "pageviews" => 491, + "utm_medium" => "referral", + "visit_duration" => 27.5, + "visitors" => 294, + "visits" => 298 + } + ] = results + end + + defp assert_entry_pages(conn, params) do + params = Map.put(params, "property", "visit:entry_page") + + %{"results" => results} = + get(conn, "/api/v1/stats/breakdown", params) |> json_response(200) + + assert length(results) == 629 + + assert List.first(results) == %{ + "bounce_rate" => 35.0, + "pageviews" => 838, + "visit_duration" => 43.1, + "visitors" => 675, + "visits" => 712, + "entry_page" => "/brza-kukuruza" + } + + assert List.last(results) == %{ + "bounce_rate" => 0.0, + "pageviews" => 1, + "visit_duration" => 27.0, + "visitors" => 1, + "visits" => 1, + "entry_page" => "/kad-lisce-pada" + } + end + + defp assert_cities(conn, params) do + params = Map.put(params, "property", "visit:city") + + %{"results" => results} = + get(conn, "/api/v1/stats/breakdown", params) |> json_response(200) + + assert length(results) == 488 + + assert List.first(results) == %{ + "bounce_rate" => 35.0, + "city" => 792_680, + "pageviews" => 1650, + "visit_duration" => 38.9, + "visitors" => 1233, + "visits" => 1273 + } + + assert List.last(results) == %{ + "bounce_rate" => 0.0, + "city" => 4_399_605, + "pageviews" => 7, + "visit_duration" => 128.0, + "visitors" => 1, + "visits" => 1 + } + end + + defp assert_devices(conn, params) do + params = Map.put(params, "property", "visit:device") + + %{"results" => results} = + get(conn, "/api/v1/stats/breakdown", params) |> json_response(200) + + assert length(results) == 3 + + assert List.first(results) == %{ + "bounce_rate" => 38.0, + "pageviews" => 7041, + "visit_duration" => 36.6, + "visitors" => 5277, + "visits" => 5532, + "device" => "Mobile" + } + + assert List.last(results) == %{ + "bounce_rate" => 37.0, + "pageviews" => 143, + "visit_duration" => 59.8, + "visitors" => 97, + "visits" => 100, + "device" => "Tablet" + } + end + + defp assert_browsers(conn, params) do + params = Map.put(params, "property", "visit:browser") + + %{"results" => results} = + get(conn, "/api/v1/stats/breakdown", params) |> json_response(200) + + assert length(results) == 11 + + assert List.first(results) == %{ + "bounce_rate" => 33.0, + "pageviews" => 8143, + "visit_duration" => 50.2, + "visitors" => 4625, + "visits" => 4655, + "browser" => "Chrome" + } + + assert List.last(results) == %{ + "bounce_rate" => 0.0, + "pageviews" => 6, + "visit_duration" => 0.0, + "visitors" => 1, + "visits" => 1, + "browser" => "Opera Mini" + } + end + + defp assert_os(conn, params) do + params = Map.put(params, "property", "visit:os") + + %{"results" => results} = + get(conn, "/api/v1/stats/breakdown", params) |> json_response(200) + + assert length(results) == 7 + + assert List.first(results) == %{ + "bounce_rate" => 34.0, + "pageviews" => 5827, + "visit_duration" => 40.6, + "visitors" => 4319, + "visits" => 4495, + "os" => "Android" + } + + assert List.last(results) == %{ + "bounce_rate" => 0.0, + "pageviews" => 6, + "visit_duration" => 0.0, + "visitors" => 1, + "visits" => 1, + "os" => "(not set)" + } + end + + defp assert_os_versions(conn, params) do + params = Map.put(params, "property", "visit:os_version") + + %{"results" => results} = + get(conn, "/api/v1/stats/breakdown", params) |> json_response(200) + + assert length(results) == 107 + + assert List.first(results) == %{ + "bounce_rate" => 32.0, + "os" => "Android", + "os_version" => "13.0.0", + "pageviews" => 1673, + "visit_duration" => 42.4, + "visitors" => 1247, + "visits" => 1295 + } + + assert List.last(results) == %{ + "bounce_rate" => 0.0, + "os" => "iOS", + "os_version" => "15.1", + "pageviews" => 1, + "visit_duration" => 54.0, + "visitors" => 1, + "visits" => 1 + } + end + + defp assert_pages(conn, params) do + metrics = "visitors,visits,pageviews,time_on_page,visit_duration,bounce_rate" + + params = + params + |> Map.put("metrics", metrics) + |> Map.put("limit", 1000) + |> Map.put("property", "event:page") + + %{"results" => results} = + get(conn, "/api/v1/stats/breakdown", params) |> json_response(200) + + assert length(results) == 729 + + # The `event:page` breakdown is currently using the `entry_page` + # property to allow querying session metrics. + # + # We assert on the 3rd element of the results, because that page + # was also an entry page somewhere along the queried period. So + # it will allow us to assert on the session metrics as well. + assert Enum.at(results, 2) == %{ + "page" => "/", + "pageviews" => 5537, + "time_on_page" => 17.677262055264585, + "visitors" => 371, + "visits" => 212, + "bounce_rate" => 54.0, + "visit_duration" => 45.0 + } + + # This page was never an entry_page in the imported data, and + # therefore the session metrics are returned as `nil`. + assert List.last(results) == %{ + "page" => "/5-dobrih-razloga-zasto-zapoceti-dan-zobenom-kasom/", + "pageviews" => 2, + "time_on_page" => 10.0, + "visitors" => 1, + "visits" => 1, + "bounce_rate" => nil, + "visit_duration" => nil + } + end end