analytics/test/plausible/google/api_test.exs

435 lines
14 KiB
Elixir

defmodule Plausible.Google.APITest do
use Plausible.DataCase, async: true
use Plausible.Test.Support.HTTPMocker
alias Plausible.Google
alias Plausible.Imported.UniversalAnalytics
import ExUnit.CaptureLog
import Mox
setup :verify_on_exit!
setup [:create_user, :create_new_site]
@refresh_token_body Jason.decode!(File.read!("fixture/ga_refresh_token.json"))
@full_report_mock [
"fixture/ga_report_imported_visitors.json",
"fixture/ga_report_imported_sources.json",
"fixture/ga_report_imported_pages.json",
"fixture/ga_report_imported_entry_pages.json",
"fixture/ga_report_imported_exit_pages.json",
"fixture/ga_report_imported_locations.json",
"fixture/ga_report_imported_devices.json",
"fixture/ga_report_imported_browsers.json",
"fixture/ga_report_imported_operating_systems.json"
]
|> Enum.map(&File.read!/1)
|> Enum.map(&Jason.decode!/1)
@tag :slow
test "imports page views from Google Analytics", %{site: site} do
mock_http_with("google_analytics_import#1.json")
view_id = "54297898"
date_range = Date.range(~D[2011-01-01], ~D[2022-07-19])
future = DateTime.utc_now() |> DateTime.add(3600, :second) |> DateTime.to_iso8601()
auth = {"***", "refresh_token", future}
{:ok, buffer} = Plausible.Imported.Buffer.start_link()
persist_fn = fn table, rows ->
records = UniversalAnalytics.from_report(rows, site.id, _import_id = 123, table)
Plausible.Imported.Buffer.insert_many(buffer, table, records)
end
assert :ok == Google.UA.API.import_analytics(date_range, view_id, auth, persist_fn)
Plausible.Imported.Buffer.flush(buffer)
Plausible.Imported.Buffer.stop(buffer)
assert 1_495_150 == Plausible.Stats.Clickhouse.imported_pageview_count(site)
end
@tag :slow
test "import_analytics/4 refreshes OAuth token when needed", %{site: site} do
past = DateTime.add(DateTime.utc_now(), -3600, :second)
auth = {"redacted_access_token", "redacted_refresh_token", DateTime.to_iso8601(past)}
range = Date.range(~D[2020-01-01], ~D[2020-02-02])
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
{:ok, buffer} = Plausible.Imported.Buffer.start_link()
persist_fn = fn table, rows ->
records = UniversalAnalytics.from_report(rows, site.id, _import_id = 123, table)
Plausible.Imported.Buffer.insert_many(buffer, table, records)
end
assert :ok == Google.UA.API.import_analytics(range, "123551", auth, persist_fn)
Plausible.Imported.Buffer.flush(buffer)
Plausible.Imported.Buffer.stop(buffer)
end
describe "fetch_and_persist/4" do
@ok_response Jason.decode!(File.read!("fixture/ga_batch_report.json"))
@no_report_response Jason.decode!(File.read!("fixture/ga_report_empty_rows.json"))
@tag :slow
test "will fetch and persist import data from Google Analytics" do
request = %Plausible.Google.UA.ReportRequest{
dataset: "imported_exit_pages",
view_id: "123",
date_range: Date.range(~D[2022-01-01], ~D[2022-02-01]),
dimensions: ["ga:date", "ga:exitPagePath"],
metrics: ["ga:users", "ga:exits"],
access_token: "fake-token",
page_token: nil,
page_size: 10_000
}
expect(
Plausible.HTTPClient.Mock,
:post,
fn
"https://analyticsreporting.googleapis.com/v4/reports:batchGet",
[{"Authorization", "Bearer fake-token"}],
%{
reportRequests: [
%{
dateRanges: [%{endDate: ~D[2022-02-01], startDate: ~D[2022-01-01]}],
dimensions: [
%{histogramBuckets: [], name: "ga:date"},
%{histogramBuckets: [], name: "ga:exitPagePath"}
],
hideTotals: true,
hideValueRanges: true,
metrics: [%{expression: "ga:users"}, %{expression: "ga:exits"}],
orderBys: [%{fieldName: "ga:date", sortOrder: "DESCENDING"}],
pageSize: 10_000,
pageToken: nil,
viewId: "123"
}
]
},
[receive_timeout: 60_000] ->
{:ok, %Finch.Response{status: 200, body: @ok_response}}
end
)
assert :ok =
Google.UA.API.fetch_and_persist(request,
sleep_time: 0,
persist_fn: fn dataset, row ->
assert dataset == "imported_exit_pages"
assert length(row) == 1479
:ok
end
)
end
test "retries HTTP request up to 5 times before raising the last error" do
expect(
Plausible.HTTPClient.Mock,
:post,
5,
fn
"https://analyticsreporting.googleapis.com/v4/reports:batchGet",
_,
_,
[receive_timeout: 60_000] ->
Enum.random([
{:error, %Mint.TransportError{reason: :nxdomain}},
{:error, %{reason: %Finch.Response{status: 500}}}
])
end
)
request = %Plausible.Google.UA.ReportRequest{
view_id: "123",
date_range: Date.range(~D[2022-01-01], ~D[2022-02-01]),
dimensions: ["ga:date"],
metrics: ["ga:users"],
access_token: "fake-token",
page_token: nil,
page_size: 10_000
}
assert {:error, :request_failed} =
Google.UA.API.fetch_and_persist(request,
sleep_time: 0,
persist_fn: fn _dataset, _rows -> :ok end
)
end
test "does not fail when report does not have rows key" do
expect(
Plausible.HTTPClient.Mock,
:post,
fn
"https://analyticsreporting.googleapis.com/v4/reports:batchGet",
_,
_,
[receive_timeout: 60_000] ->
{:ok, %Finch.Response{status: 200, body: @no_report_response}}
end
)
request = %Plausible.Google.UA.ReportRequest{
dataset: "imported_exit_pages",
view_id: "123",
date_range: Date.range(~D[2022-01-01], ~D[2022-02-01]),
dimensions: ["ga:date", "ga:exitPagePath"],
metrics: ["ga:users", "ga:exits"],
access_token: "fake-token",
page_token: nil,
page_size: 10_000
}
assert :ok ==
Google.UA.API.fetch_and_persist(request,
sleep_time: 0,
persist_fn: fn dataset, rows ->
assert dataset == "imported_exit_pages"
assert rows == []
:ok
end
)
end
end
describe "fetch_stats/3 errors" do
setup %{user: user, site: site} do
insert(:google_auth,
user: user,
site: site,
property: "sc-domain:dummy.test",
expires: NaiveDateTime.add(NaiveDateTime.utc_now(), 3600)
)
:ok
end
test "returns generic google_auth_error on 401/403", %{site: site} do
expect(
Plausible.HTTPClient.Mock,
:post,
fn
"https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Adummy.test/searchAnalytics/query",
[{"Authorization", "Bearer 123"}],
%{
dimensionFilterGroups: %{},
dimensions: ["query"],
endDate: "2022-01-05",
rowLimit: 5,
startDate: "2022-01-01"
} ->
{:error, %{reason: %Finch.Response{status: Enum.random([401, 403])}}}
end
)
query = %Plausible.Stats.Query{date_range: Date.range(~D[2022-01-01], ~D[2022-01-05])}
assert {:error, "google_auth_error"} = Google.API.fetch_stats(site, query, 5)
end
test "returns whatever error code google returns on API client error", %{site: site} do
expect(
Plausible.HTTPClient.Mock,
:post,
fn
"https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Adummy.test/searchAnalytics/query",
_,
_ ->
{:error, %{reason: %Finch.Response{status: 400, body: %{"error" => "some_error"}}}}
end
)
query = %Plausible.Stats.Query{date_range: Date.range(~D[2022-01-01], ~D[2022-01-05])}
assert {:error, "some_error"} = Google.API.fetch_stats(site, query, 5)
end
test "returns generic HTTP error and logs it", %{site: site} do
expect(
Plausible.HTTPClient.Mock,
:post,
fn
"https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Adummy.test/searchAnalytics/query",
_,
_ ->
{:error, Finch.Error.exception(:some_reason)}
end
)
query = %Plausible.Stats.Query{date_range: Date.range(~D[2022-01-01], ~D[2022-01-05])}
log =
capture_log(fn ->
assert {:error, "failed_to_list_stats"} =
Google.API.fetch_stats(site, query, 5)
end)
assert log =~ "Google Analytics: failed to list stats: %Finch.Error{reason: :some_reason}"
end
end
describe "fetch_stats/3 with VCR cassetes" do
test "returns name and visitor count", %{user: user, site: site} do
mock_http_with("google_analytics_stats.json")
insert(:google_auth,
user: user,
site: site,
property: "sc-domain:dummy.test",
expires: NaiveDateTime.add(NaiveDateTime.utc_now(), 3600)
)
query = %Plausible.Stats.Query{date_range: Date.range(~D[2022-01-01], ~D[2022-01-05])}
assert {:ok,
[
%{name: ["keyword1", "keyword2"], visitors: 25},
%{name: ["keyword3", "keyword4"], visitors: 15}
]} = Google.API.fetch_stats(site, query, 5)
end
test "returns next page when page argument is set", %{user: user, site: site} do
mock_http_with("google_analytics_stats#with_page.json")
insert(:google_auth,
user: user,
site: site,
property: "sc-domain:dummy.test",
expires: NaiveDateTime.add(NaiveDateTime.utc_now(), 3600)
)
query = %Plausible.Stats.Query{
filters: %{"page" => 5},
date_range: Date.range(~D[2022-01-01], ~D[2022-01-05])
}
assert {:ok,
[
%{name: ["keyword1", "keyword2"], visitors: 25},
%{name: ["keyword3", "keyword4"], visitors: 15}
]} = Google.API.fetch_stats(site, query, 5)
end
test "defaults first page when page argument is not set", %{user: user, site: site} do
mock_http_with("google_analytics_stats#without_page.json")
insert(:google_auth,
user: user,
site: site,
property: "sc-domain:dummy.test",
expires: NaiveDateTime.add(NaiveDateTime.utc_now(), 3600)
)
query = %Plausible.Stats.Query{date_range: Date.range(~D[2022-01-01], ~D[2022-01-05])}
assert {:ok,
[
%{name: ["keyword1", "keyword2"], visitors: 25},
%{name: ["keyword3", "keyword4"], visitors: 15}
]} = Google.API.fetch_stats(site, query, 5)
end
test "returns error when token refresh fails", %{user: user, site: site} do
mock_http_with("google_analytics_auth#invalid_grant.json")
insert(:google_auth,
user: user,
site: site,
property: "sc-domain:dummy.test",
access_token: "*****",
refresh_token: "*****",
expires: NaiveDateTime.add(NaiveDateTime.utc_now(), -3600)
)
query = %Plausible.Stats.Query{date_range: Date.range(~D[2022-01-01], ~D[2022-01-05])}
assert {:error, "invalid_grant"} = Google.API.fetch_stats(site, query, 5)
end
end
test "list_views/1 returns view IDs grouped by hostname" do
expect(
Plausible.HTTPClient.Mock,
:get,
fn url, _headers ->
assert url ==
"https://www.googleapis.com/analytics/v3/management/accounts/~all/webproperties/~all/profiles"
response = "fixture/ga_list_views.json" |> File.read!() |> Jason.decode!()
{:ok, %Finch.Response{status: 200, body: response}}
end
)
assert {:ok,
[
{"one.test", [{"57238190 - one.test", "57238190"}]},
{"two.test", [{"54460083 - two.test", "54460083"}]}
]} == Google.UA.API.list_views("access_token")
end
test "list_views/1 returns authentication_failed when request fails with HTTP 403" do
expect(
Plausible.HTTPClient.Mock,
:get,
fn _url, _headers ->
{:error, %Plausible.HTTPClient.Non200Error{reason: %{status: 403, body: %{}}}}
end
)
assert {:error, :authentication_failed} == Google.UA.API.list_views("access_token")
end
test "list_views/1 returns authentication_failed when request fails with HTTP 401" do
expect(
Plausible.HTTPClient.Mock,
:get,
fn _url, _headers ->
{:error, %Plausible.HTTPClient.Non200Error{reason: %{status: 401, body: %{}}}}
end
)
assert {:error, :authentication_failed} == Google.UA.API.list_views("access_token")
end
test "list_views/1 returns error when request fails with HTTP 500" do
expect(
Plausible.HTTPClient.Mock,
:get,
fn _url, _headers ->
{:error, %Plausible.HTTPClient.Non200Error{reason: %{status: 500, body: "server error"}}}
end
)
assert {:error, :unknown} == Google.UA.API.list_views("access_token")
end
end