| defmodule Plausible.Imported.GoogleAnalytics4Test do |
| use PlausibleWeb.ConnCase, async: true |
| use Oban.Testing, repo: Plausible.Repo |
|
|
| import Mox |
| import Ecto.Query, only: [from: 2] |
| import ExUnit.CaptureLog |
|
|
| alias Plausible.Repo |
| alias Plausible.Imported.GoogleAnalytics4 |
|
|
| @refresh_token_body Jason.decode!(File.read!("fixture/ga_refresh_token.json")) |
|
|
| @full_report_mock [ |
| "fixture/ga4_report_imported_visitors.json", |
| "fixture/ga4_report_imported_sources.json", |
| "fixture/ga4_report_imported_pages.json", |
| "fixture/ga4_report_imported_entry_pages.json", |
| "fixture/ga4_report_imported_custom_events.json", |
| "fixture/ga4_report_imported_locations.json", |
| "fixture/ga4_report_imported_devices.json", |
| "fixture/ga4_report_imported_browsers.json", |
| "fixture/ga4_report_imported_operating_systems.json" |
| ] |
| |> Enum.map(&File.read!/1) |
| |> Enum.map(&Jason.decode!/1) |
|
|
| on_ce do |
| @moduletag :capture_log |
| end |
|
|
| setup :verify_on_exit! |
|
|
| describe "parse_args/1 and import_data/2" do |
| setup [:create_user, :create_site] |
|
|
| 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} = |
| Plausible.Imported.GoogleAnalytics4.new_import( |
| site, |
| user, |
| label: "properties/123456", |
| property: "properties/123456", |
| start_date: ~D[2024-01-01], |
| end_date: ~D[2024-01-31], |
| access_token: "redacted_access_token", |
| refresh_token: "redacted_refresh_token", |
| token_expires_at: DateTime.to_iso8601(past) |
| ) |
|
|
| site_import = Plausible.Imported.get_import(site, job.args.import_id) |
|
|
| assert site_import.label == "properties/123456" |
|
|
| opts = job |> Repo.reload!() |> Map.get(:args) |> GoogleAnalytics4.parse_args() |
|
|
| opts = Keyword.put(opts, :flush_interval_ms, 10) |
|
|
| expect(Plausible.HTTPClient.Mock, :post, fn "https://www.googleapis.com/oauth2/v4/token", |
| headers, |
| body -> |
| assert [{"content-type", "application/x-www-form-urlencoded"}] == headers |
|
|
| assert %{ |
| grant_type: :refresh_token, |
| redirect_uri: "http://localhost:8000/auth/google/callback", |
| refresh_token: "redacted_refresh_token" |
| } = body |
|
|
| {:ok, %Finch.Response{status: 200, body: @refresh_token_body}} |
| end) |
|
|
| for report <- @full_report_mock do |
| expect(Plausible.HTTPClient.Mock, :post, fn _url, headers, _body, _opts -> |
| assert [{"Authorization", "Bearer 1/fFAGRNJru1FTz70BzhT3Zg"}] == headers |
| {:ok, %Finch.Response{status: 200, body: report}} |
| end) |
| end |
|
|
| Enum.each(Plausible.Imported.tables(), fn table -> |
| query = from(imported in table, where: imported.site_id == ^site.id) |
| assert await_clickhouse_count(query, 0) |
| end) |
|
|
| assert :ok = GoogleAnalytics4.import_data(site_import, opts) |
|
|
| Enum.each(Plausible.Imported.tables(), fn table -> |
| count = |
| case table do |
| "imported_sources" -> 210 |
| "imported_visitors" -> 31 |
| "imported_pages" -> 3340 |
| "imported_entry_pages" -> 2934 |
| "imported_exit_pages" -> 0 |
| "imported_custom_events" -> 56 |
| "imported_locations" -> 2291 |
| "imported_devices" -> 93 |
| "imported_browsers" -> 233 |
| "imported_operating_systems" -> 1068 |
| end |
|
|
| query = from(imported in table, where: imported.site_id == ^site.id) |
| assert await_clickhouse_count(query, count) |
| end) |
|
|
| |
| |
| site_import |
| |> Plausible.Imported.SiteImport.complete_changeset() |
| |> Repo.update!() |
|
|
| |
| 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,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}") |
|
|
| insert(:goal, event_name: "Outbound Link: Click", site: site) |
| insert(:goal, event_name: "view_search_results", site: site) |
| insert(:goal, event_name: "scroll", site: site) |
|
|
| |
| assert_timeseries(conn, common_params) |
|
|
| |
| assert_pages(conn, common_params) |
| assert_custom_events(conn, common_params) |
| assert_outbound_link_urls(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 |
|
|
| test "handles empty response payload gracefully", %{user: user, site: site} do |
| future = DateTime.add(DateTime.utc_now(), 3600, :second) |
|
|
| empty_custom_events = %{ |
| "reports" => [ |
| %{ |
| "dimensionHeaders" => [ |
| %{"name" => "date"}, |
| %{"name" => "eventName"}, |
| %{"name" => "linkUrl"} |
| ], |
| "kind" => "analyticsData#runReport", |
| "metadata" => %{"currencyCode" => "USD", "timeZone" => "Etc/GMT"}, |
| "metricHeaders" => [ |
| %{"name" => "totalUsers", "type" => "TYPE_INTEGER"}, |
| %{"name" => "eventCount", "type" => "TYPE_INTEGER"} |
| ] |
| } |
| ] |
| } |
|
|
| {:ok, job} = |
| Plausible.Imported.GoogleAnalytics4.new_import( |
| site, |
| user, |
| label: "properties/123456", |
| property: "properties/123456", |
| start_date: ~D[2024-01-01], |
| end_date: ~D[2024-01-31], |
| access_token: "redacted_access_token", |
| refresh_token: "redacted_refresh_token", |
| token_expires_at: DateTime.to_iso8601(future) |
| ) |
|
|
| site_import = Plausible.Imported.get_import(site, job.args.import_id) |
|
|
| opts = job |> Repo.reload!() |> Map.get(:args) |> GoogleAnalytics4.parse_args() |
|
|
| opts = Keyword.put(opts, :flush_interval_ms, 10) |
|
|
| for report <- Enum.take(@full_report_mock, 4) do |
| expect(Plausible.HTTPClient.Mock, :post, fn _url, headers, _body, _opts -> |
| assert [{"Authorization", "Bearer redacted_access_token"}] == headers |
| {:ok, %Finch.Response{status: 200, body: report}} |
| end) |
| end |
|
|
| expect(Plausible.HTTPClient.Mock, :post, fn _url, headers, _body, _opts -> |
| assert [{"Authorization", "Bearer redacted_access_token"}] == headers |
| {:ok, %Finch.Response{status: 200, body: empty_custom_events}} |
| end) |
|
|
| for report <- Enum.drop(@full_report_mock, 5) do |
| expect(Plausible.HTTPClient.Mock, :post, fn _url, headers, _body, _opts -> |
| assert [{"Authorization", "Bearer redacted_access_token"}] == headers |
| {:ok, %Finch.Response{status: 200, body: report}} |
| end) |
| end |
|
|
| assert :ok = GoogleAnalytics4.import_data(site_import, opts) |
|
|
| Enum.each(Plausible.Imported.tables(), fn table -> |
| count = |
| case table do |
| "imported_sources" -> 210 |
| "imported_visitors" -> 31 |
| "imported_pages" -> 3340 |
| "imported_entry_pages" -> 2934 |
| "imported_exit_pages" -> 0 |
| "imported_locations" -> 2291 |
| "imported_devices" -> 93 |
| "imported_browsers" -> 233 |
| "imported_operating_systems" -> 1068 |
| "imported_custom_events" -> 0 |
| end |
|
|
| query = from(imported in table, where: imported.site_id == ^site.id) |
| assert await_clickhouse_count(query, count) |
| end) |
| end |
|
|
| test "handles rate limiting gracefully", %{user: user, site: site} do |
| future = DateTime.add(DateTime.utc_now(), 3600, :second) |
|
|
| {:ok, job} = |
| Plausible.Imported.GoogleAnalytics4.new_import( |
| site, |
| user, |
| label: "properties/123456", |
| property: "properties/123456", |
| start_date: ~D[2024-01-01], |
| end_date: ~D[2024-01-31], |
| access_token: "redacted_access_token", |
| refresh_token: "redacted_refresh_token", |
| token_expires_at: DateTime.to_iso8601(future) |
| ) |
|
|
| site_import = Plausible.Imported.get_import(site, job.args.import_id) |
|
|
| opts = job |> Repo.reload!() |> Map.get(:args) |> GoogleAnalytics4.parse_args() |
|
|
| opts = Keyword.put(opts, :flush_interval_ms, 10) |
|
|
| expect(Plausible.HTTPClient.Mock, :post, fn _url, headers, _body, _opts -> |
| assert [{"Authorization", "Bearer redacted_access_token"}] == headers |
| {:ok, %Finch.Response{status: 200, body: List.first(@full_report_mock)}} |
| end) |
|
|
| expect(Plausible.HTTPClient.Mock, :post, fn _url, headers, _body, _opts -> |
| assert [{"Authorization", "Bearer redacted_access_token"}] == headers |
|
|
| {:error, |
| Plausible.HTTPClient.Non200Error.new(%Finch.Response{ |
| status: 429, |
| body: "Rate limit exceeded" |
| })} |
| end) |
|
|
| assert {:error, :rate_limit_exceeded, skip_purge?: true, skip_mark_failed?: true} = |
| GoogleAnalytics4.import_data(site_import, opts) |
|
|
| in_65_minutes = DateTime.add(DateTime.utc_now(), 3900, :second) |
|
|
| assert_enqueued( |
| worker: Plausible.Workers.ImportAnalytics, |
| args: %{resume_from_import_id: site_import.id}, |
| scheduled_at: {in_65_minutes, delta: 10} |
| ) |
|
|
| [%{args: resume_args}, _] = all_enqueued() |
|
|
| resume_opts = GoogleAnalytics4.parse_args(resume_args) |
| resume_opts = Keyword.put(resume_opts, :flush_interval_ms, 10) |
| site_import = Repo.reload!(site_import) |
|
|
| Enum.each(Plausible.Imported.tables(), fn table -> |
| count = |
| case table do |
| "imported_visitors" -> 31 |
| "imported_sources" -> 0 |
| "imported_pages" -> 0 |
| "imported_entry_pages" -> 0 |
| "imported_exit_pages" -> 0 |
| "imported_locations" -> 0 |
| "imported_devices" -> 0 |
| "imported_browsers" -> 0 |
| "imported_operating_systems" -> 0 |
| "imported_custom_events" -> 0 |
| end |
|
|
| query = from(imported in table, where: imported.site_id == ^site.id) |
| assert await_clickhouse_count(query, count) |
| end) |
|
|
| for report <- tl(@full_report_mock) do |
| expect(Plausible.HTTPClient.Mock, :post, fn _url, headers, _body, _opts -> |
| assert [{"Authorization", "Bearer redacted_access_token"}] == headers |
| {:ok, %Finch.Response{status: 200, body: report}} |
| end) |
| end |
|
|
| assert :ok = GoogleAnalytics4.import_data(site_import, resume_opts) |
|
|
| Enum.each(Plausible.Imported.tables(), fn table -> |
| count = |
| case table do |
| "imported_sources" -> 210 |
| "imported_visitors" -> 31 |
| "imported_pages" -> 3340 |
| "imported_entry_pages" -> 2934 |
| "imported_exit_pages" -> 0 |
| "imported_locations" -> 2291 |
| "imported_devices" -> 93 |
| "imported_browsers" -> 233 |
| "imported_operating_systems" -> 1068 |
| "imported_custom_events" -> 56 |
| end |
|
|
| query = from(imported in table, where: imported.site_id == ^site.id) |
| assert await_clickhouse_count(query, count) |
| end) |
| end |
|
|
| @recoverable_errors [ |
| { |
| Macro.escape( |
| Plausible.HTTPClient.Non200Error.new(%Finch.Response{ |
| status: 500, |
| body: "Internal server error" |
| }) |
| ), |
| :server_failed, |
| ~s|Request failed for imported_sources with code 500: "Internal server error"| |
| }, |
| { |
| :timeout, |
| :socket_failed, |
| ~s|Request failed for imported_sources: :timeout| |
| } |
| ] |
|
|
| for {error_mock, error_returned, log_message} <- @recoverable_errors do |
| test "handles #{error_returned} gracefully", %{user: user, site: site} do |
| future = DateTime.add(DateTime.utc_now(), 3600, :second) |
|
|
| {:ok, job} = |
| Plausible.Imported.GoogleAnalytics4.new_import( |
| site, |
| user, |
| label: "properties/123456", |
| property: "properties/123456", |
| start_date: ~D[2024-01-01], |
| end_date: ~D[2024-01-31], |
| access_token: "redacted_access_token", |
| refresh_token: "redacted_refresh_token", |
| token_expires_at: DateTime.to_iso8601(future) |
| ) |
|
|
| site_import = Plausible.Imported.get_import(site, job.args.import_id) |
|
|
| opts = job |> Repo.reload!() |> Map.get(:args) |> GoogleAnalytics4.parse_args() |
|
|
| opts = |
| opts |
| |> Keyword.put(:flush_interval_ms, 10) |
| |> Keyword.put(:fetch_opts, max_attempts: 2, sleep_time: 50) |
|
|
| expect(Plausible.HTTPClient.Mock, :post, fn _url, headers, _body, _opts -> |
| assert [{"Authorization", "Bearer redacted_access_token"}] == headers |
| {:ok, %Finch.Response{status: 200, body: List.first(@full_report_mock)}} |
| end) |
|
|
| expect(Plausible.HTTPClient.Mock, :post, 2, fn _url, headers, _body, _opts -> |
| assert [{"Authorization", "Bearer redacted_access_token"}] == headers |
|
|
| {:error, unquote(error_mock)} |
| end) |
|
|
| assert capture_log(fn -> |
| assert {:error, unquote(error_returned), |
| skip_purge?: true, skip_mark_failed?: true} = |
| GoogleAnalytics4.import_data(site_import, opts) |
| end) =~ unquote(log_message) |
|
|
| in_65_minutes = DateTime.add(DateTime.utc_now(), 3900, :second) |
|
|
| assert_enqueued( |
| worker: Plausible.Workers.ImportAnalytics, |
| args: %{resume_from_import_id: site_import.id}, |
| scheduled_at: {in_65_minutes, delta: 10} |
| ) |
|
|
| [%{args: resume_args}, _] = all_enqueued() |
|
|
| resume_opts = GoogleAnalytics4.parse_args(resume_args) |
| resume_opts = Keyword.put(resume_opts, :flush_interval_ms, 10) |
| site_import = Repo.reload!(site_import) |
|
|
| Enum.each(Plausible.Imported.tables(), fn table -> |
| count = |
| case table do |
| "imported_visitors" -> 31 |
| "imported_sources" -> 0 |
| "imported_pages" -> 0 |
| "imported_entry_pages" -> 0 |
| "imported_exit_pages" -> 0 |
| "imported_locations" -> 0 |
| "imported_devices" -> 0 |
| "imported_browsers" -> 0 |
| "imported_operating_systems" -> 0 |
| "imported_custom_events" -> 0 |
| end |
|
|
| query = from(imported in table, where: imported.site_id == ^site.id) |
| assert await_clickhouse_count(query, count) |
| end) |
|
|
| for report <- tl(@full_report_mock) do |
| expect(Plausible.HTTPClient.Mock, :post, fn _url, headers, _body, _opts -> |
| assert [{"Authorization", "Bearer redacted_access_token"}] == headers |
| {:ok, %Finch.Response{status: 200, body: report}} |
| end) |
| end |
|
|
| assert :ok = GoogleAnalytics4.import_data(site_import, resume_opts) |
|
|
| Enum.each(Plausible.Imported.tables(), fn table -> |
| count = |
| case table do |
| "imported_sources" -> 210 |
| "imported_visitors" -> 31 |
| "imported_pages" -> 3340 |
| "imported_entry_pages" -> 2934 |
| "imported_exit_pages" -> 0 |
| "imported_locations" -> 2291 |
| "imported_devices" -> 93 |
| "imported_browsers" -> 233 |
| "imported_operating_systems" -> 1068 |
| "imported_custom_events" -> 56 |
| end |
|
|
| query = from(imported in table, where: imported.site_id == ^site.id) |
| assert await_clickhouse_count(query, count) |
| end) |
| end |
| end |
| end |
|
|
| defp assert_custom_events(conn, params) do |
| params = |
| params |
| |> Map.put("metrics", "visitors,events,conversion_rate") |
| |> Map.put("property", "event:goal") |
|
|
| %{"results" => results} = |
| get(conn, "/api/v1/stats/breakdown", params) |> json_response(200) |
|
|
| assert results == [ |
| %{ |
| "goal" => "scroll", |
| "visitors" => 1513, |
| "events" => 2130, |
| "conversion_rate" => 24.69 |
| }, |
| %{ |
| "goal" => "Outbound Link: Click", |
| "visitors" => 17, |
| "events" => 17, |
| "conversion_rate" => 0.28 |
| }, |
| %{ |
| "goal" => "view_search_results", |
| "visitors" => 11, |
| "events" => 30, |
| "conversion_rate" => 0.18 |
| } |
| ] |
| end |
|
|
| defp assert_outbound_link_urls(conn, params) do |
| params = |
| Map.merge(params, %{ |
| "metrics" => "visitors,events,conversion_rate", |
| "property" => "event:props:url", |
| "filters" => "event:goal==Outbound Link: Click" |
| }) |
|
|
| %{"results" => results} = |
| get(conn, "/api/v1/stats/breakdown", params) |> json_response(200) |
|
|
| assert length(results) == 10 |
|
|
| assert List.first(results) == |
| %{ |
| "url" => "https://www.facebook.com/kuhinjskeprice", |
| "visitors" => 6, |
| "conversion_rate" => 0.1, |
| "events" => 6 |
| } |
|
|
| results |
| |> Enum.find( |
| &(&1["url"] == |
| "http://www.jamieoliver.com/recipes/pasta-recipes/spinach-ricotta-cannelloni/") |
| ) |
| |> then(fn page -> |
| assert %{"visitors" => 1, "conversion_rate" => 0.02, "events" => 1} = page |
| 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, |
| "visit_duration" => 40.0, |
| "visitors" => 4671, |
| "visits" => 4917, |
| "source" => "Google" |
| } |
|
|
| assert List.last(results) == %{ |
| "bounce_rate" => 0.0, |
| "source" => "yahoo", |
| "visit_duration" => 41.0, |
| "visitors" => 1, |
| "visits" => 1 |
| } |
| 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, |
| "utm_medium" => "organic", |
| "visit_duration" => 40.0, |
| "visitors" => 4787, |
| "visits" => 5042 |
| }, |
| %{ |
| "bounce_rate" => 58.0, |
| "utm_medium" => "referral", |
| "visit_duration" => 27.0, |
| "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, |
| "visit_duration" => 43.0, |
| "visitors" => 675, |
| "visits" => 712, |
| "entry_page" => "/brza-kukuruza" |
| } |
|
|
| assert List.last(results) == %{ |
| "bounce_rate" => 0.0, |
| "entry_page" => "/znamenitosti-rima-koje-treba-vidjeti", |
| "visit_duration" => 40.0, |
| "visitors" => 1, |
| "visits" => 1 |
| } |
| 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) == 494 |
|
|
| assert List.first(results) == %{ |
| "bounce_rate" => 35.0, |
| "city" => 792_680, |
| "visit_duration" => 39.0, |
| "visitors" => 1233, |
| "visits" => 1273 |
| } |
|
|
| assert List.last(results) == %{ |
| "bounce_rate" => 0.0, |
| "city" => 11_951_298, |
| "visit_duration" => 271.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, |
| "visit_duration" => 37.0, |
| "visitors" => 5277, |
| "visits" => 5532, |
| "device" => "Mobile" |
| } |
|
|
| assert List.last(results) == %{ |
| "bounce_rate" => 37.0, |
| "visit_duration" => 60.0, |
| "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, |
| "visit_duration" => 50.0, |
| "visitors" => 4625, |
| "visits" => 4655, |
| "browser" => "Chrome" |
| } |
|
|
| assert List.last(results) == %{ |
| "bounce_rate" => 0.0, |
| "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, |
| "visit_duration" => 41.0, |
| "visitors" => 4319, |
| "visits" => 4495, |
| "os" => "Android" |
| } |
|
|
| assert List.last(results) == %{ |
| "bounce_rate" => 0.0, |
| "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", |
| "visit_duration" => 42.0, |
| "visitors" => 1247, |
| "visits" => 1295 |
| } |
|
|
| assert List.last(results) == %{ |
| "bounce_rate" => 0.0, |
| "os" => "Chrome OS", |
| "os_version" => "x86_64 15662.76.0", |
| "visit_duration" => 16.0, |
| "visitors" => 1, |
| "visits" => 1 |
| } |
| end |
|
|
| defp assert_pages(conn, params) do |
| metrics = "visitors,visits,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) == 730 |
|
|
| |
| |
| |
| |
| |
| |
| assert Enum.at(results, 2) == %{ |
| "page" => "/", |
| "time_on_page" => 462, |
| "visitors" => 371, |
| "visits" => 212, |
| "bounce_rate" => 54.0, |
| "visit_duration" => 45.0 |
| } |
|
|
| |
| |
| assert List.last(results) == %{ |
| "bounce_rate" => 0.0, |
| "page" => "/znamenitosti-rima-koje-treba-vidjeti/", |
| "time_on_page" => 40, |
| "visit_duration" => 0.0, |
| "visitors" => 1, |
| "visits" => 1 |
| } |
| end |
| end |
|
|