mirror of
https://github.com/plausible/analytics.git
synced 2024-08-16 21:30:29 +03:00
Remove Universal Analytics import logic (#4312)
This commit is contained in:
parent
2f2602e316
commit
35596e8692
File diff suppressed because it is too large
Load Diff
@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"reports": [
|
|
||||||
{
|
|
||||||
"columnHeader": {
|
|
||||||
"dimensions": [
|
|
||||||
"ga:date"
|
|
||||||
],
|
|
||||||
"metricHeader": {
|
|
||||||
"metricHeaderEntries": [
|
|
||||||
{
|
|
||||||
"name": "ga:pageviews",
|
|
||||||
"type": "INTEGER"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"isDataGolden": true,
|
|
||||||
"rowCount": 849,
|
|
||||||
"rows": [
|
|
||||||
{
|
|
||||||
"dimensions": [
|
|
||||||
"20200421"
|
|
||||||
],
|
|
||||||
"metrics": [
|
|
||||||
{
|
|
||||||
"values": [
|
|
||||||
"37"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"nextPageToken": "1"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,67 +0,0 @@
|
|||||||
{
|
|
||||||
"kind": "analytics#profiles",
|
|
||||||
"username": "email@provider.test",
|
|
||||||
"totalResults": 2,
|
|
||||||
"startIndex": 1,
|
|
||||||
"itemsPerPage": 1000,
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"id": "57238190",
|
|
||||||
"kind": "analytics#profile",
|
|
||||||
"selfLink": "https://www.googleapis.com/analytics/v3/management/accounts/61782930/webproperties/UA-1625528-17/profiles/57238190",
|
|
||||||
"accountId": "61782930",
|
|
||||||
"webPropertyId": "UA-61782930-17",
|
|
||||||
"internalWebPropertyId": "53451925",
|
|
||||||
"name": "one.test",
|
|
||||||
"currency": "USD",
|
|
||||||
"timezone": "Europe/London",
|
|
||||||
"websiteUrl": "http://one.test/",
|
|
||||||
"type": "WEB",
|
|
||||||
"permissions": {
|
|
||||||
"effective": [
|
|
||||||
"READ_AND_ANALYZE"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"created": "2011-12-25T15:16:53.069Z",
|
|
||||||
"updated": "2012-07-26T09:45:03.811Z",
|
|
||||||
"eCommerceTracking": false,
|
|
||||||
"parentLink": {
|
|
||||||
"type": "analytics#webproperty",
|
|
||||||
"href": "https://www.googleapis.com/analytics/v3/management/accounts/61782930/webproperties/UA-1625528-17"
|
|
||||||
},
|
|
||||||
"childLink": {
|
|
||||||
"type": "analytics#goals",
|
|
||||||
"href": "https://www.googleapis.com/analytics/v3/management/accounts/61782930/webproperties/UA-1625528-17/profiles/57238190/goals"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "54460083",
|
|
||||||
"kind": "analytics#profile",
|
|
||||||
"selfLink": "https://www.googleapis.com/analytics/v3/management/accounts/61782930/webproperties/UA-1625528-18/profiles/54460083",
|
|
||||||
"accountId": "61782930",
|
|
||||||
"webPropertyId": "UA-61782930-18",
|
|
||||||
"internalWebPropertyId": "53597744",
|
|
||||||
"name": "two.test",
|
|
||||||
"currency": "USD",
|
|
||||||
"timezone": "Europe/London",
|
|
||||||
"websiteUrl": "http://two.test/",
|
|
||||||
"type": "WEB",
|
|
||||||
"permissions": {
|
|
||||||
"effective": [
|
|
||||||
"READ_AND_ANALYZE"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"created": "2011-12-31T19:14:11.434Z",
|
|
||||||
"updated": "2012-04-03T18:34:11.475Z",
|
|
||||||
"eCommerceTracking": false,
|
|
||||||
"parentLink": {
|
|
||||||
"type": "analytics#webproperty",
|
|
||||||
"href": "https://www.googleapis.com/analytics/v3/management/accounts/61782930/webproperties/UA-1625528-18"
|
|
||||||
},
|
|
||||||
"childLink": {
|
|
||||||
"type": "analytics#goals",
|
|
||||||
"href": "https://www.googleapis.com/analytics/v3/management/accounts/61782930/webproperties/UA-1625528-18/profiles/54460083/goals"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"reports": [
|
|
||||||
{
|
|
||||||
"columnHeader": {
|
|
||||||
"dimensions": [
|
|
||||||
"ga:date",
|
|
||||||
"ga:source",
|
|
||||||
"ga:medium",
|
|
||||||
"ga:campaign",
|
|
||||||
"ga:adContent",
|
|
||||||
"ga:keyword"
|
|
||||||
],
|
|
||||||
"metricHeader": {
|
|
||||||
"metricHeaderEntries": [
|
|
||||||
{ "name": "ga:users", "type": "INTEGER" },
|
|
||||||
{ "name": "ga:sessions", "type": "INTEGER" },
|
|
||||||
{ "name": "ga:bounces", "type": "INTEGER" },
|
|
||||||
{ "name": "ga:sessionDuration", "type": "TIME" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"data": { "isDataGolden": true }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"reports": [
|
|
||||||
{
|
|
||||||
"columnHeader": {
|
|
||||||
"dimensions": [
|
|
||||||
"ga:date"
|
|
||||||
],
|
|
||||||
"metricHeader": {
|
|
||||||
"metricHeaderEntries": [
|
|
||||||
{
|
|
||||||
"name": "ga:pageviews",
|
|
||||||
"type": "INTEGER"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"isDataGolden": true,
|
|
||||||
"rowCount": 849,
|
|
||||||
"rows": [
|
|
||||||
{
|
|
||||||
"dimensions": [
|
|
||||||
"20120118"
|
|
||||||
],
|
|
||||||
"metrics": [
|
|
||||||
{
|
|
||||||
"values": [
|
|
||||||
"37"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"nextPageToken": "1"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"reports": [
|
|
||||||
{
|
|
||||||
"columnHeader": {
|
|
||||||
"dimensions": [
|
|
||||||
"ga:date"
|
|
||||||
],
|
|
||||||
"metricHeader": {
|
|
||||||
"metricHeaderEntries": [
|
|
||||||
{
|
|
||||||
"name": "ga:pageviews",
|
|
||||||
"type": "INTEGER"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"isDataGolden": true,
|
|
||||||
"rowCount": 849,
|
|
||||||
"rows": [
|
|
||||||
{
|
|
||||||
"dimensions": [
|
|
||||||
"20170118"
|
|
||||||
],
|
|
||||||
"metrics": [
|
|
||||||
{
|
|
||||||
"values": [
|
|
||||||
"37"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"nextPageToken": "1"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@ -31,32 +31,20 @@ defmodule Plausible.Google.API do
|
|||||||
HTTP.fetch_access_token!(code)
|
HTTP.fetch_access_token!(code)
|
||||||
end
|
end
|
||||||
|
|
||||||
def list_properties_and_views(access_token) do
|
def list_properties(access_token) do
|
||||||
Plausible.Google.GA4.API.list_properties(access_token)
|
Plausible.Google.GA4.API.list_properties(access_token)
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_property_or_view(access_token, property_or_view) do
|
def get_property(access_token, property) do
|
||||||
if property?(property_or_view) do
|
Plausible.Google.GA4.API.get_property(access_token, property)
|
||||||
Plausible.Google.GA4.API.get_property(access_token, property_or_view)
|
|
||||||
else
|
|
||||||
Plausible.Google.UA.API.get_view(access_token, property_or_view)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_analytics_start_date(access_token, property_or_view) do
|
def get_analytics_start_date(access_token, property) do
|
||||||
if property?(property_or_view) do
|
Plausible.Google.GA4.API.get_analytics_start_date(access_token, property)
|
||||||
Plausible.Google.GA4.API.get_analytics_start_date(access_token, property_or_view)
|
|
||||||
else
|
|
||||||
Plausible.Google.UA.API.get_analytics_start_date(access_token, property_or_view)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_analytics_end_date(access_token, property_or_view) do
|
def get_analytics_end_date(access_token, property) do
|
||||||
if property?(property_or_view) do
|
Plausible.Google.GA4.API.get_analytics_end_date(access_token, property)
|
||||||
Plausible.Google.GA4.API.get_analytics_end_date(access_token, property_or_view)
|
|
||||||
else
|
|
||||||
Plausible.Google.UA.API.get_analytics_end_date(access_token, property_or_view)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_verified_properties(auth) do
|
def fetch_verified_properties(auth) do
|
||||||
|
@ -1,155 +0,0 @@
|
|||||||
defmodule Plausible.Google.UA.API do
|
|
||||||
@moduledoc """
|
|
||||||
API for Universal Analytics
|
|
||||||
"""
|
|
||||||
|
|
||||||
alias Plausible.Google
|
|
||||||
alias Plausible.Google.UA
|
|
||||||
|
|
||||||
@type google_analytics_view() :: {view_name :: String.t(), view_id :: String.t()}
|
|
||||||
|
|
||||||
@type import_auth :: {
|
|
||||||
access_token :: String.t(),
|
|
||||||
refresh_token :: String.t(),
|
|
||||||
expires_at :: String.t()
|
|
||||||
}
|
|
||||||
|
|
||||||
@per_page 100_000
|
|
||||||
@backoff_factor :timer.seconds(10)
|
|
||||||
@max_attempts 5
|
|
||||||
|
|
||||||
@spec list_views(access_token :: String.t()) ::
|
|
||||||
{:ok, %{(hostname :: String.t()) => [google_analytics_view()]}} | {:error, term()}
|
|
||||||
@doc """
|
|
||||||
Lists Google Analytics views grouped by hostname.
|
|
||||||
"""
|
|
||||||
def list_views(access_token) do
|
|
||||||
case UA.HTTP.list_views_for_user(access_token) do
|
|
||||||
{:ok, %{"items" => views}} ->
|
|
||||||
views =
|
|
||||||
views
|
|
||||||
|> Enum.group_by(&view_hostname/1, &view_names/1)
|
|
||||||
|> Enum.sort_by(fn {key, _} -> key end)
|
|
||||||
|
|
||||||
{:ok, views}
|
|
||||||
|
|
||||||
error ->
|
|
||||||
error
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec get_view(access_token :: String.t(), lookup_id :: String.t()) ::
|
|
||||||
{:ok, google_analytics_view()} | {:ok, nil} | {:error, term()}
|
|
||||||
@doc """
|
|
||||||
Returns a single Google Analytics view if the user has access to it.
|
|
||||||
"""
|
|
||||||
def get_view(access_token, lookup_id) do
|
|
||||||
with {:ok, views} <- list_views(access_token) do
|
|
||||||
views =
|
|
||||||
views
|
|
||||||
|> Enum.map(&elem(&1, 1))
|
|
||||||
|> List.flatten()
|
|
||||||
|
|
||||||
case Enum.find(views, fn {_name, id} -> id == lookup_id end) do
|
|
||||||
{view_name, view_id} ->
|
|
||||||
{:ok, %{id: view_id, name: "#{view_name}"}}
|
|
||||||
|
|
||||||
nil ->
|
|
||||||
{:error, :not_found}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_analytics_start_date(access_token, view_id) do
|
|
||||||
UA.HTTP.get_analytics_start_date(access_token, view_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_analytics_end_date(access_token, view_id) do
|
|
||||||
UA.HTTP.get_analytics_end_date(access_token, view_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec import_analytics(Date.Range.t(), String.t(), import_auth(), (String.t(), [map()] -> :ok)) ::
|
|
||||||
:ok | {:error, term()}
|
|
||||||
@doc """
|
|
||||||
Imports stats from a Google Analytics UA view to a Plausible site.
|
|
||||||
|
|
||||||
This function fetches Google Analytics reports in batches of #{@per_page} per
|
|
||||||
request. The batches are then passed to persist callback.
|
|
||||||
|
|
||||||
Requests to Google Analytics can fail, and are retried at most
|
|
||||||
#{@max_attempts} times with an exponential backoff. Returns `:ok` when
|
|
||||||
importing has finished or `{:error, term()}` when a request to GA failed too
|
|
||||||
many times.
|
|
||||||
|
|
||||||
Useful links:
|
|
||||||
|
|
||||||
- [Feature documentation](https://plausible.io/docs/google-analytics-import)
|
|
||||||
- [GA API reference](https://developers.google.com/analytics/devguides/reporting/core/v4/rest/v4/reports/batchGet#ReportRequest)
|
|
||||||
- [GA Dimensions reference](https://ga-dev-tools.web.app/dimensions-metrics-explorer)
|
|
||||||
|
|
||||||
"""
|
|
||||||
def import_analytics(date_range, view_id, auth, persist_fn) do
|
|
||||||
with {:ok, access_token} <- Google.API.maybe_refresh_token(auth) do
|
|
||||||
do_import_analytics(date_range, view_id, access_token, persist_fn)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp do_import_analytics(date_range, view_id, access_token, persist_fn) do
|
|
||||||
Enum.reduce_while(UA.ReportRequest.full_report(), :ok, fn report_request, :ok ->
|
|
||||||
report_request = %UA.ReportRequest{
|
|
||||||
report_request
|
|
||||||
| date_range: date_range,
|
|
||||||
view_id: view_id,
|
|
||||||
access_token: access_token,
|
|
||||||
page_token: nil,
|
|
||||||
page_size: @per_page
|
|
||||||
}
|
|
||||||
|
|
||||||
case fetch_and_persist(report_request, persist_fn: persist_fn) do
|
|
||||||
:ok -> {:cont, :ok}
|
|
||||||
{:error, _} = error -> {:halt, error}
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec fetch_and_persist(UA.ReportRequest.t(), Keyword.t()) ::
|
|
||||||
:ok | {:error, term()}
|
|
||||||
def fetch_and_persist(%UA.ReportRequest{} = report_request, opts \\ []) do
|
|
||||||
persist_fn = Keyword.fetch!(opts, :persist_fn)
|
|
||||||
attempt = Keyword.get(opts, :attempt, 1)
|
|
||||||
sleep_time = Keyword.get(opts, :sleep_time, @backoff_factor)
|
|
||||||
|
|
||||||
case UA.HTTP.get_report(report_request) do
|
|
||||||
{:ok, {rows, next_page_token}} ->
|
|
||||||
:ok = persist_fn.(report_request.dataset, rows)
|
|
||||||
|
|
||||||
if next_page_token do
|
|
||||||
fetch_and_persist(
|
|
||||||
%UA.ReportRequest{report_request | page_token: next_page_token},
|
|
||||||
opts
|
|
||||||
)
|
|
||||||
else
|
|
||||||
:ok
|
|
||||||
end
|
|
||||||
|
|
||||||
{:error, cause} ->
|
|
||||||
if attempt >= @max_attempts do
|
|
||||||
{:error, cause}
|
|
||||||
else
|
|
||||||
Process.sleep(attempt * sleep_time)
|
|
||||||
fetch_and_persist(report_request, Keyword.merge(opts, attempt: attempt + 1))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp view_hostname(view) do
|
|
||||||
case view do
|
|
||||||
%{"websiteUrl" => url} when is_binary(url) -> url |> URI.parse() |> Map.get(:host)
|
|
||||||
_any -> "Others"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp view_names(%{"name" => name, "id" => id}) do
|
|
||||||
{"#{id} - #{name}", id}
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,199 +0,0 @@
|
|||||||
defmodule Plausible.Google.UA.HTTP do
|
|
||||||
@moduledoc """
|
|
||||||
HTTP client implementation for Universal Analytics API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
require Logger
|
|
||||||
alias Plausible.HTTPClient
|
|
||||||
|
|
||||||
@spec get_report(Plausible.Google.UA.ReportRequest.t()) ::
|
|
||||||
{:ok, {[map()], String.t() | nil}} | {:error, any()}
|
|
||||||
def get_report(%Plausible.Google.UA.ReportRequest{} = report_request) do
|
|
||||||
params = %{
|
|
||||||
reportRequests: [
|
|
||||||
%{
|
|
||||||
viewId: report_request.view_id,
|
|
||||||
dateRanges: [
|
|
||||||
%{
|
|
||||||
startDate: report_request.date_range.first,
|
|
||||||
endDate: report_request.date_range.last
|
|
||||||
}
|
|
||||||
],
|
|
||||||
dimensions: Enum.map(report_request.dimensions, &%{name: &1, histogramBuckets: []}),
|
|
||||||
metrics: Enum.map(report_request.metrics, &%{expression: &1}),
|
|
||||||
hideTotals: true,
|
|
||||||
hideValueRanges: true,
|
|
||||||
orderBys: [%{fieldName: "ga:date", sortOrder: "DESCENDING"}],
|
|
||||||
pageSize: report_request.page_size,
|
|
||||||
pageToken: report_request.page_token
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
response =
|
|
||||||
HTTPClient.impl().post(
|
|
||||||
"#{reporting_api_url()}/v4/reports:batchGet",
|
|
||||||
[{"Authorization", "Bearer #{report_request.access_token}"}],
|
|
||||||
params,
|
|
||||||
receive_timeout: 60_000
|
|
||||||
)
|
|
||||||
|
|
||||||
with {:ok, %{body: body}} <- response,
|
|
||||||
{:ok, report} <- parse_report_from_response(body),
|
|
||||||
token <- Map.get(report, "nextPageToken"),
|
|
||||||
{:ok, report} <- convert_to_maps(report) do
|
|
||||||
{:ok, {report, token}}
|
|
||||||
else
|
|
||||||
{:error, %{reason: %{status: status, body: body}}} ->
|
|
||||||
Logger.debug(
|
|
||||||
"[#{inspect(__MODULE__)}:#{report_request.view_id}] Request failed for #{report_request.dataset} with code #{status}: #{inspect(body)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
Sentry.Context.set_extra_context(%{ga_response: %{body: body, status: status}})
|
|
||||||
{:error, :request_failed}
|
|
||||||
|
|
||||||
{:error, reason} ->
|
|
||||||
Logger.debug(
|
|
||||||
"[#{inspect(__MODULE__)}:#{report_request.view_id}] Request failed for #{report_request.dataset}: #{inspect(reason)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
{:error, :request_failed}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp parse_report_from_response(%{"reports" => [report | _]}) do
|
|
||||||
{:ok, report}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp parse_report_from_response(body) do
|
|
||||||
Sentry.Context.set_extra_context(%{universal_analytics_response: body})
|
|
||||||
|
|
||||||
Logger.error(
|
|
||||||
"Universal Analytics: Failed to find report in response. Reason: #{inspect(body)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
{:error, {:invalid_response, body}}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp convert_to_maps(%{
|
|
||||||
"data" => %{} = data,
|
|
||||||
"columnHeader" => %{
|
|
||||||
"dimensions" => dimension_headers,
|
|
||||||
"metricHeader" => %{"metricHeaderEntries" => metric_headers}
|
|
||||||
}
|
|
||||||
}) do
|
|
||||||
metric_headers = Enum.map(metric_headers, & &1["name"])
|
|
||||||
rows = Map.get(data, "rows", [])
|
|
||||||
|
|
||||||
report =
|
|
||||||
Enum.map(rows, fn %{"dimensions" => dimensions, "metrics" => [%{"values" => metrics}]} ->
|
|
||||||
metrics = Enum.zip(metric_headers, metrics)
|
|
||||||
dimensions = Enum.zip(dimension_headers, dimensions)
|
|
||||||
%{metrics: Map.new(metrics), dimensions: Map.new(dimensions)}
|
|
||||||
end)
|
|
||||||
|
|
||||||
{:ok, report}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp convert_to_maps(response) do
|
|
||||||
Logger.error(
|
|
||||||
"Universal Analytics: Failed to read report in response. Reason: #{inspect(response)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
Sentry.Context.set_extra_context(%{universal_analytics_response: response})
|
|
||||||
{:error, {:invalid_response, response}}
|
|
||||||
end
|
|
||||||
|
|
||||||
def list_views_for_user(access_token) do
|
|
||||||
url = "#{api_url()}/analytics/v3/management/accounts/~all/webproperties/~all/profiles"
|
|
||||||
|
|
||||||
headers = [{"Authorization", "Bearer #{access_token}"}]
|
|
||||||
|
|
||||||
case HTTPClient.impl().get(url, headers) do
|
|
||||||
{:ok, %Finch.Response{body: body, status: 200}} ->
|
|
||||||
{:ok, body}
|
|
||||||
|
|
||||||
{:error, %HTTPClient.Non200Error{} = error} when error.reason.status in [401, 403] ->
|
|
||||||
{:error, :authentication_failed}
|
|
||||||
|
|
||||||
{:error, %{reason: :timeout}} ->
|
|
||||||
{:error, :timeout}
|
|
||||||
|
|
||||||
{:error, error} ->
|
|
||||||
Sentry.capture_message("Error listing UA views for user", extra: %{error: error})
|
|
||||||
{:error, :unknown}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@earliest_valid_date "2005-01-01"
|
|
||||||
|
|
||||||
def get_analytics_start_date(access_token, view_id) do
|
|
||||||
get_analytics_boundary_date(access_token, view_id, :start)
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_analytics_end_date(access_token, view_id) do
|
|
||||||
get_analytics_boundary_date(access_token, view_id, :end)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp get_analytics_boundary_date(access_token, view_id, edge) do
|
|
||||||
sort_order =
|
|
||||||
if edge == :start do
|
|
||||||
"ASCENDING"
|
|
||||||
else
|
|
||||||
"DESCENDING"
|
|
||||||
end
|
|
||||||
|
|
||||||
params = %{
|
|
||||||
reportRequests: [
|
|
||||||
%{
|
|
||||||
viewId: view_id,
|
|
||||||
dateRanges: [
|
|
||||||
%{startDate: @earliest_valid_date, endDate: Date.to_iso8601(Timex.today())}
|
|
||||||
],
|
|
||||||
dimensions: [%{name: "ga:date", histogramBuckets: []}],
|
|
||||||
metrics: [%{expression: "ga:pageviews"}],
|
|
||||||
hideTotals: true,
|
|
||||||
hideValueRanges: true,
|
|
||||||
orderBys: [%{fieldName: "ga:date", sortOrder: sort_order}],
|
|
||||||
pageSize: 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
url = "#{reporting_api_url()}/v4/reports:batchGet"
|
|
||||||
headers = [{"Authorization", "Bearer #{access_token}"}]
|
|
||||||
|
|
||||||
case HTTPClient.impl().post(url, headers, params) do
|
|
||||||
{:ok, %Finch.Response{body: body, status: 200}} ->
|
|
||||||
report = List.first(body["reports"])
|
|
||||||
|
|
||||||
date =
|
|
||||||
case report["data"]["rows"] do
|
|
||||||
[%{"dimensions" => [date_str]}] ->
|
|
||||||
Timex.parse!(date_str, "%Y%m%d", :strftime) |> NaiveDateTime.to_date()
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
|
|
||||||
{:ok, date}
|
|
||||||
|
|
||||||
{:error, %HTTPClient.Non200Error{} = error} when error.reason.status in [401, 403] ->
|
|
||||||
{:error, :authentication_failed}
|
|
||||||
|
|
||||||
{:error, %{reason: :timeout}} ->
|
|
||||||
{:error, :timeout}
|
|
||||||
|
|
||||||
{:error, error} ->
|
|
||||||
Sentry.capture_message("Error retrieving UA #{edge} date",
|
|
||||||
extra: %{error: error}
|
|
||||||
)
|
|
||||||
|
|
||||||
{:error, :unknown}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp config, do: Application.get_env(:plausible, :google)
|
|
||||||
defp reporting_api_url, do: Keyword.fetch!(config(), :reporting_api_url)
|
|
||||||
defp api_url, do: Keyword.fetch!(config(), :api_url)
|
|
||||||
end
|
|
@ -1,84 +0,0 @@
|
|||||||
defmodule Plausible.Google.UA.ReportRequest do
|
|
||||||
@moduledoc """
|
|
||||||
Report request struct for Universal Analytics API
|
|
||||||
"""
|
|
||||||
|
|
||||||
defstruct [
|
|
||||||
:dataset,
|
|
||||||
:dimensions,
|
|
||||||
:metrics,
|
|
||||||
:date_range,
|
|
||||||
:view_id,
|
|
||||||
:access_token,
|
|
||||||
:page_token,
|
|
||||||
:page_size
|
|
||||||
]
|
|
||||||
|
|
||||||
@type t() :: %__MODULE__{
|
|
||||||
dataset: String.t(),
|
|
||||||
dimensions: [String.t()],
|
|
||||||
metrics: [String.t()],
|
|
||||||
date_range: Date.Range.t(),
|
|
||||||
view_id: term(),
|
|
||||||
access_token: String.t(),
|
|
||||||
page_token: String.t() | nil,
|
|
||||||
page_size: non_neg_integer()
|
|
||||||
}
|
|
||||||
|
|
||||||
def full_report do
|
|
||||||
[
|
|
||||||
%__MODULE__{
|
|
||||||
dataset: "imported_visitors",
|
|
||||||
dimensions: ["ga:date"],
|
|
||||||
metrics: ["ga:users", "ga:pageviews", "ga:bounces", "ga:sessions", "ga:sessionDuration"]
|
|
||||||
},
|
|
||||||
%__MODULE__{
|
|
||||||
dataset: "imported_sources",
|
|
||||||
dimensions: [
|
|
||||||
"ga:date",
|
|
||||||
"ga:source",
|
|
||||||
"ga:medium",
|
|
||||||
"ga:campaign",
|
|
||||||
"ga:adContent",
|
|
||||||
"ga:keyword"
|
|
||||||
],
|
|
||||||
metrics: ["ga:users", "ga:sessions", "ga:bounces", "ga:sessionDuration"]
|
|
||||||
},
|
|
||||||
%__MODULE__{
|
|
||||||
dataset: "imported_pages",
|
|
||||||
dimensions: ["ga:date", "ga:hostname", "ga:pagePath"],
|
|
||||||
metrics: ["ga:users", "ga:pageviews", "ga:exits", "ga:timeOnPage"]
|
|
||||||
},
|
|
||||||
%__MODULE__{
|
|
||||||
dataset: "imported_entry_pages",
|
|
||||||
dimensions: ["ga:date", "ga:landingPagePath"],
|
|
||||||
metrics: ["ga:users", "ga:entrances", "ga:sessionDuration", "ga:bounces"]
|
|
||||||
},
|
|
||||||
%__MODULE__{
|
|
||||||
dataset: "imported_exit_pages",
|
|
||||||
dimensions: ["ga:date", "ga:exitPagePath"],
|
|
||||||
metrics: ["ga:users", "ga:exits"]
|
|
||||||
},
|
|
||||||
%__MODULE__{
|
|
||||||
dataset: "imported_locations",
|
|
||||||
dimensions: ["ga:date", "ga:countryIsoCode", "ga:regionIsoCode", "ga:city"],
|
|
||||||
metrics: ["ga:users", "ga:sessions", "ga:bounces", "ga:sessionDuration"]
|
|
||||||
},
|
|
||||||
%__MODULE__{
|
|
||||||
dataset: "imported_devices",
|
|
||||||
dimensions: ["ga:date", "ga:deviceCategory"],
|
|
||||||
metrics: ["ga:users", "ga:sessions", "ga:bounces", "ga:sessionDuration"]
|
|
||||||
},
|
|
||||||
%__MODULE__{
|
|
||||||
dataset: "imported_browsers",
|
|
||||||
dimensions: ["ga:date", "ga:browser"],
|
|
||||||
metrics: ["ga:users", "ga:sessions", "ga:bounces", "ga:sessionDuration"]
|
|
||||||
},
|
|
||||||
%__MODULE__{
|
|
||||||
dataset: "imported_operating_systems",
|
|
||||||
dimensions: ["ga:date", "ga:operatingSystem"],
|
|
||||||
metrics: ["ga:users", "ga:sessions", "ga:bounces", "ga:sessionDuration"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
end
|
|
||||||
end
|
|
@ -2,12 +2,7 @@ defmodule Plausible.Imported do
|
|||||||
@moduledoc """
|
@moduledoc """
|
||||||
Context for managing site statistics imports.
|
Context for managing site statistics imports.
|
||||||
|
|
||||||
Currently following importers are implemented:
|
For list of currently supported import sources see `Plausible.Imported.ImportSources`.
|
||||||
|
|
||||||
* `Plausible.Imported.UniversalAnalytics` - existing mechanism, for legacy Google
|
|
||||||
analytics formerly known as "Google Analytics"
|
|
||||||
* `Plausible.Imported.NoopImporter` - importer stub, used mainly for testing purposes
|
|
||||||
* `Plausible.Imported.CSVImporter` - CSV importer from S3
|
|
||||||
|
|
||||||
For more information on implementing importers, see `Plausible.Imported.Importer`.
|
For more information on implementing importers, see `Plausible.Imported.Importer`.
|
||||||
"""
|
"""
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
defmodule Plausible.Imported.UniversalAnalytics do
|
defmodule Plausible.Imported.UniversalAnalytics do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Import implementation for Universal Analytics.
|
Import implementation for Universal Analytics.
|
||||||
|
|
||||||
|
NOTE: As importing from UA is no longer supported, this module
|
||||||
|
is only used to support rendering existing imports.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
use Plausible.Imported.Importer
|
use Plausible.Imported.Importer
|
||||||
|
|
||||||
@missing_values ["(none)", "(not set)", "(not provided)", "(other)"]
|
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def name(), do: :universal_analytics
|
def name(), do: :universal_analytics
|
||||||
|
|
||||||
@ -17,234 +18,12 @@ defmodule Plausible.Imported.UniversalAnalytics do
|
|||||||
def email_template(), do: "google_analytics_import.html"
|
def email_template(), do: "google_analytics_import.html"
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def parse_args(
|
def parse_args(_args) do
|
||||||
%{"view_id" => view_id, "start_date" => start_date, "end_date" => end_date} = args
|
raise "Importing data not supported"
|
||||||
) do
|
|
||||||
start_date = Date.from_iso8601!(start_date)
|
|
||||||
end_date = Date.from_iso8601!(end_date)
|
|
||||||
date_range = Date.range(start_date, end_date)
|
|
||||||
|
|
||||||
auth = {
|
|
||||||
Map.fetch!(args, "access_token"),
|
|
||||||
Map.fetch!(args, "refresh_token"),
|
|
||||||
Map.fetch!(args, "token_expires_at")
|
|
||||||
}
|
|
||||||
|
|
||||||
[
|
|
||||||
view_id: view_id,
|
|
||||||
date_range: date_range,
|
|
||||||
auth: auth
|
|
||||||
]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
|
||||||
Imports stats from a Google Analytics UA view to a Plausible site.
|
|
||||||
|
|
||||||
This function fetches Google Analytics reports which are then passed in batches
|
|
||||||
to Clickhouse by the `Plausible.Imported.Buffer` process.
|
|
||||||
"""
|
|
||||||
@impl true
|
@impl true
|
||||||
def import_data(site_import, opts) do
|
def import_data(_site_import, _opts) do
|
||||||
date_range = Keyword.fetch!(opts, :date_range)
|
raise "Importing data not supported"
|
||||||
view_id = Keyword.fetch!(opts, :view_id)
|
|
||||||
auth = Keyword.fetch!(opts, :auth)
|
|
||||||
|
|
||||||
{:ok, buffer} = Plausible.Imported.Buffer.start_link()
|
|
||||||
|
|
||||||
persist_fn = fn table, rows ->
|
|
||||||
records = from_report(rows, site_import.site_id, site_import.id, table)
|
|
||||||
Plausible.Imported.Buffer.insert_many(buffer, table, records)
|
|
||||||
end
|
|
||||||
|
|
||||||
try do
|
|
||||||
Plausible.Google.UA.API.import_analytics(date_range, view_id, auth, persist_fn)
|
|
||||||
after
|
|
||||||
Plausible.Imported.Buffer.flush(buffer)
|
|
||||||
Plausible.Imported.Buffer.stop(buffer)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def from_report(nil, _site_id, _import_id, _metric), do: nil
|
|
||||||
|
|
||||||
def from_report(data, site_id, import_id, table) do
|
|
||||||
Enum.reduce(data, [], fn row, acc ->
|
|
||||||
if Map.get(row.dimensions, "ga:date") in @missing_values do
|
|
||||||
acc
|
|
||||||
else
|
|
||||||
[new_from_report(site_id, import_id, table, row) | acc]
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp parse_number(nr) do
|
|
||||||
{float, ""} = Float.parse(nr)
|
|
||||||
round(float)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp new_from_report(site_id, import_id, "imported_visitors", row) do
|
|
||||||
%{
|
|
||||||
site_id: site_id,
|
|
||||||
import_id: import_id,
|
|
||||||
date: get_date(row),
|
|
||||||
visitors: row.metrics |> Map.fetch!("ga:users") |> parse_number(),
|
|
||||||
pageviews: row.metrics |> Map.fetch!("ga:pageviews") |> parse_number(),
|
|
||||||
bounces: row.metrics |> Map.fetch!("ga:bounces") |> parse_number(),
|
|
||||||
visits: row.metrics |> Map.fetch!("ga:sessions") |> parse_number(),
|
|
||||||
visit_duration: row.metrics |> Map.fetch!("ga:sessionDuration") |> parse_number()
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp new_from_report(site_id, import_id, "imported_sources", row) do
|
|
||||||
%{
|
|
||||||
site_id: site_id,
|
|
||||||
import_id: import_id,
|
|
||||||
date: get_date(row),
|
|
||||||
source: row.dimensions |> Map.fetch!("ga:source") |> parse_referrer(),
|
|
||||||
utm_medium: row.dimensions |> Map.fetch!("ga:medium") |> default_if_missing(),
|
|
||||||
utm_campaign: row.dimensions |> Map.fetch!("ga:campaign") |> default_if_missing(),
|
|
||||||
utm_content: row.dimensions |> Map.fetch!("ga:adContent") |> default_if_missing(),
|
|
||||||
utm_term: row.dimensions |> Map.fetch!("ga:keyword") |> default_if_missing(),
|
|
||||||
visitors: row.metrics |> Map.fetch!("ga:users") |> parse_number(),
|
|
||||||
visits: row.metrics |> Map.fetch!("ga:sessions") |> parse_number(),
|
|
||||||
bounces: row.metrics |> Map.fetch!("ga:bounces") |> parse_number(),
|
|
||||||
visit_duration: row.metrics |> Map.fetch!("ga:sessionDuration") |> parse_number()
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp new_from_report(site_id, import_id, "imported_pages", row) do
|
|
||||||
%{
|
|
||||||
site_id: site_id,
|
|
||||||
import_id: import_id,
|
|
||||||
date: get_date(row),
|
|
||||||
hostname: row.dimensions |> Map.fetch!("ga:hostname") |> String.replace_prefix("www.", ""),
|
|
||||||
page: row.dimensions |> Map.fetch!("ga:pagePath") |> URI.parse() |> Map.get(:path),
|
|
||||||
visitors: row.metrics |> Map.fetch!("ga:users") |> parse_number(),
|
|
||||||
pageviews: row.metrics |> Map.fetch!("ga:pageviews") |> parse_number(),
|
|
||||||
exits: row.metrics |> Map.fetch!("ga:exits") |> parse_number(),
|
|
||||||
time_on_page: row.metrics |> Map.fetch!("ga:timeOnPage") |> parse_number()
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp new_from_report(site_id, import_id, "imported_entry_pages", row) do
|
|
||||||
%{
|
|
||||||
site_id: site_id,
|
|
||||||
import_id: import_id,
|
|
||||||
date: get_date(row),
|
|
||||||
entry_page: row.dimensions |> Map.fetch!("ga:landingPagePath"),
|
|
||||||
visitors: row.metrics |> Map.fetch!("ga:users") |> parse_number(),
|
|
||||||
entrances: row.metrics |> Map.fetch!("ga:entrances") |> parse_number(),
|
|
||||||
visit_duration: row.metrics |> Map.fetch!("ga:sessionDuration") |> parse_number(),
|
|
||||||
bounces: row.metrics |> Map.fetch!("ga:bounces") |> parse_number()
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp new_from_report(site_id, import_id, "imported_exit_pages", row) do
|
|
||||||
%{
|
|
||||||
site_id: site_id,
|
|
||||||
import_id: import_id,
|
|
||||||
date: get_date(row),
|
|
||||||
exit_page: Map.fetch!(row.dimensions, "ga:exitPagePath"),
|
|
||||||
visitors: row.metrics |> Map.fetch!("ga:users") |> parse_number(),
|
|
||||||
exits: row.metrics |> Map.fetch!("ga:exits") |> parse_number()
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp new_from_report(site_id, import_id, "imported_locations", row) do
|
|
||||||
country_code = row.dimensions |> Map.fetch!("ga:countryIsoCode") |> default_if_missing("")
|
|
||||||
city_name = row.dimensions |> Map.fetch!("ga:city") |> default_if_missing("")
|
|
||||||
city_data = Location.get_city(city_name, country_code)
|
|
||||||
|
|
||||||
%{
|
|
||||||
site_id: site_id,
|
|
||||||
import_id: import_id,
|
|
||||||
date: get_date(row),
|
|
||||||
country: country_code,
|
|
||||||
region: row.dimensions |> Map.fetch!("ga:regionIsoCode") |> default_if_missing(""),
|
|
||||||
city: city_data && city_data.id,
|
|
||||||
visitors: row.metrics |> Map.fetch!("ga:users") |> parse_number(),
|
|
||||||
visits: row.metrics |> Map.fetch!("ga:sessions") |> parse_number(),
|
|
||||||
bounces: row.metrics |> Map.fetch!("ga:bounces") |> parse_number(),
|
|
||||||
visit_duration: row.metrics |> Map.fetch!("ga:sessionDuration") |> parse_number()
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp new_from_report(site_id, import_id, "imported_devices", row) do
|
|
||||||
%{
|
|
||||||
site_id: site_id,
|
|
||||||
import_id: import_id,
|
|
||||||
date: get_date(row),
|
|
||||||
device: row.dimensions |> Map.fetch!("ga:deviceCategory") |> String.capitalize(),
|
|
||||||
visitors: row.metrics |> Map.fetch!("ga:users") |> parse_number(),
|
|
||||||
visits: row.metrics |> Map.fetch!("ga:sessions") |> parse_number(),
|
|
||||||
bounces: row.metrics |> Map.fetch!("ga:bounces") |> parse_number(),
|
|
||||||
visit_duration: row.metrics |> Map.fetch!("ga:sessionDuration") |> parse_number()
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
@browser_google_to_plausible %{
|
|
||||||
"User-Agent:Opera" => "Opera",
|
|
||||||
"Mozilla Compatible Agent" => "Mobile App",
|
|
||||||
"Android Webview" => "Mobile App",
|
|
||||||
"Android Browser" => "Mobile App",
|
|
||||||
"Safari (in-app)" => "Mobile App",
|
|
||||||
"User-Agent: Mozilla" => "Firefox",
|
|
||||||
"(not set)" => ""
|
|
||||||
}
|
|
||||||
|
|
||||||
defp new_from_report(site_id, import_id, "imported_browsers", row) do
|
|
||||||
browser = Map.fetch!(row.dimensions, "ga:browser")
|
|
||||||
|
|
||||||
%{
|
|
||||||
site_id: site_id,
|
|
||||||
import_id: import_id,
|
|
||||||
date: get_date(row),
|
|
||||||
browser: Map.get(@browser_google_to_plausible, browser, browser),
|
|
||||||
visitors: row.metrics |> Map.fetch!("ga:users") |> parse_number(),
|
|
||||||
visits: row.metrics |> Map.fetch!("ga:sessions") |> parse_number(),
|
|
||||||
bounces: row.metrics |> Map.fetch!("ga:bounces") |> parse_number(),
|
|
||||||
visit_duration: row.metrics |> Map.fetch!("ga:sessionDuration") |> parse_number()
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
@os_google_to_plausible %{
|
|
||||||
"Macintosh" => "Mac",
|
|
||||||
"Linux" => "GNU/Linux",
|
|
||||||
"(not set)" => ""
|
|
||||||
}
|
|
||||||
|
|
||||||
defp new_from_report(site_id, import_id, "imported_operating_systems", row) do
|
|
||||||
os = Map.fetch!(row.dimensions, "ga:operatingSystem")
|
|
||||||
|
|
||||||
%{
|
|
||||||
site_id: site_id,
|
|
||||||
import_id: import_id,
|
|
||||||
date: get_date(row),
|
|
||||||
operating_system: Map.get(@os_google_to_plausible, os, os),
|
|
||||||
visitors: row.metrics |> Map.fetch!("ga:users") |> parse_number(),
|
|
||||||
visits: row.metrics |> Map.fetch!("ga:sessions") |> parse_number(),
|
|
||||||
bounces: row.metrics |> Map.fetch!("ga:bounces") |> parse_number(),
|
|
||||||
visit_duration: row.metrics |> Map.fetch!("ga:sessionDuration") |> parse_number()
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp get_date(%{dimensions: %{"ga:date" => date}}) do
|
|
||||||
date
|
|
||||||
|> Timex.parse!("%Y%m%d", :strftime)
|
|
||||||
|> NaiveDateTime.to_date()
|
|
||||||
end
|
|
||||||
|
|
||||||
defp default_if_missing(value, default \\ nil)
|
|
||||||
defp default_if_missing(value, default) when value in @missing_values, do: default
|
|
||||||
defp default_if_missing(value, _default), do: value
|
|
||||||
|
|
||||||
defp parse_referrer(nil), do: nil
|
|
||||||
defp parse_referrer("(direct)"), do: nil
|
|
||||||
defp parse_referrer("google"), do: "Google"
|
|
||||||
defp parse_referrer("bing"), do: "Bing"
|
|
||||||
defp parse_referrer("duckduckgo"), do: "DuckDuckGo"
|
|
||||||
|
|
||||||
defp parse_referrer(ref) do
|
|
||||||
RefInspector.parse("https://" <> ref)
|
|
||||||
|> PlausibleWeb.RefInspector.parse()
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -749,7 +749,7 @@ defmodule PlausibleWeb.AuthController do
|
|||||||
"import" ->
|
"import" ->
|
||||||
redirect(conn,
|
redirect(conn,
|
||||||
external:
|
external:
|
||||||
Routes.google_analytics_path(conn, :property_or_view_form, site.domain,
|
Routes.google_analytics_path(conn, :property_form, site.domain,
|
||||||
access_token: res["access_token"],
|
access_token: res["access_token"],
|
||||||
refresh_token: res["refresh_token"],
|
refresh_token: res["refresh_token"],
|
||||||
expires_at: NaiveDateTime.to_iso8601(expires_at)
|
expires_at: NaiveDateTime.to_iso8601(expires_at)
|
||||||
|
@ -10,31 +10,7 @@ defmodule PlausibleWeb.GoogleAnalyticsController do
|
|||||||
|
|
||||||
plug(PlausibleWeb.AuthorizeSiteAccess, [:owner, :admin, :super_admin])
|
plug(PlausibleWeb.AuthorizeSiteAccess, [:owner, :admin, :super_admin])
|
||||||
|
|
||||||
def user_metric_notice(conn, %{
|
def property_form(
|
||||||
"property_or_view" => property_or_view,
|
|
||||||
"access_token" => access_token,
|
|
||||||
"refresh_token" => refresh_token,
|
|
||||||
"expires_at" => expires_at,
|
|
||||||
"start_date" => start_date,
|
|
||||||
"end_date" => end_date
|
|
||||||
}) do
|
|
||||||
site = conn.assigns.site
|
|
||||||
|
|
||||||
conn
|
|
||||||
|> assign(:skip_plausible_tracking, true)
|
|
||||||
|> render("user_metric_form.html",
|
|
||||||
site: site,
|
|
||||||
property_or_view: property_or_view,
|
|
||||||
access_token: access_token,
|
|
||||||
refresh_token: refresh_token,
|
|
||||||
expires_at: expires_at,
|
|
||||||
start_date: start_date,
|
|
||||||
end_date: end_date,
|
|
||||||
layout: {PlausibleWeb.LayoutView, "focus.html"}
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def property_or_view_form(
|
|
||||||
conn,
|
conn,
|
||||||
%{
|
%{
|
||||||
"access_token" => access_token,
|
"access_token" => access_token,
|
||||||
@ -46,7 +22,7 @@ defmodule PlausibleWeb.GoogleAnalyticsController do
|
|||||||
|
|
||||||
redirect_route = Routes.site_path(conn, :settings_imports_exports, site.domain)
|
redirect_route = Routes.site_path(conn, :settings_imports_exports, site.domain)
|
||||||
|
|
||||||
result = Google.API.list_properties_and_views(access_token)
|
result = Google.API.list_properties(access_token)
|
||||||
|
|
||||||
error =
|
error =
|
||||||
case params["error"] do
|
case params["error"] do
|
||||||
@ -61,16 +37,16 @@ defmodule PlausibleWeb.GoogleAnalyticsController do
|
|||||||
end
|
end
|
||||||
|
|
||||||
case result do
|
case result do
|
||||||
{:ok, properties_and_views} ->
|
{:ok, properties} ->
|
||||||
conn
|
conn
|
||||||
|> assign(:skip_plausible_tracking, true)
|
|> assign(:skip_plausible_tracking, true)
|
||||||
|> render("property_or_view_form.html",
|
|> render("property_form.html",
|
||||||
access_token: access_token,
|
access_token: access_token,
|
||||||
refresh_token: refresh_token,
|
refresh_token: refresh_token,
|
||||||
expires_at: expires_at,
|
expires_at: expires_at,
|
||||||
site: conn.assigns.site,
|
site: conn.assigns.site,
|
||||||
properties_and_views: properties_and_views,
|
properties: properties,
|
||||||
selected_property_or_view_error: error,
|
selected_property_error: error,
|
||||||
layout: {PlausibleWeb.LayoutView, "focus.html"}
|
layout: {PlausibleWeb.LayoutView, "focus.html"}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -102,19 +78,16 @@ defmodule PlausibleWeb.GoogleAnalyticsController do
|
|||||||
conn
|
conn
|
||||||
|> put_flash(
|
|> put_flash(
|
||||||
:error,
|
:error,
|
||||||
"We were unable to list your Google Analytics properties and views. If the problem persists, please contact support for assistance."
|
"We were unable to list your Google Analytics properties. If the problem persists, please contact support for assistance."
|
||||||
)
|
)
|
||||||
|> redirect(external: redirect_route)
|
|> redirect(external: redirect_route)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# see https://stackoverflow.com/a/57416769
|
def property(
|
||||||
@universal_analytics_new_user_metric_date ~D[2016-08-24]
|
|
||||||
|
|
||||||
def property_or_view(
|
|
||||||
conn,
|
conn,
|
||||||
%{
|
%{
|
||||||
"property_or_view" => property_or_view,
|
"property" => property,
|
||||||
"access_token" => access_token,
|
"access_token" => access_token,
|
||||||
"refresh_token" => refresh_token,
|
"refresh_token" => refresh_token,
|
||||||
"expires_at" => expires_at
|
"expires_at" => expires_at
|
||||||
@ -124,22 +97,14 @@ defmodule PlausibleWeb.GoogleAnalyticsController do
|
|||||||
|
|
||||||
redirect_route = Routes.site_path(conn, :settings_imports_exports, site.domain)
|
redirect_route = Routes.site_path(conn, :settings_imports_exports, site.domain)
|
||||||
|
|
||||||
with {:ok, api_start_date} <-
|
with {:ok, api_start_date} <- Google.API.get_analytics_start_date(access_token, property),
|
||||||
Google.API.get_analytics_start_date(access_token, property_or_view),
|
{:ok, api_end_date} <- Google.API.get_analytics_end_date(access_token, property),
|
||||||
{:ok, api_end_date} <- Google.API.get_analytics_end_date(access_token, property_or_view),
|
|
||||||
:ok <- ensure_dates(api_start_date, api_end_date),
|
:ok <- ensure_dates(api_start_date, api_end_date),
|
||||||
{:ok, start_date, end_date} <- Imported.clamp_dates(site, api_start_date, api_end_date) do
|
{:ok, start_date, end_date} <- Imported.clamp_dates(site, api_start_date, api_end_date) do
|
||||||
action =
|
|
||||||
if Timex.before?(api_start_date, @universal_analytics_new_user_metric_date) do
|
|
||||||
:user_metric_notice
|
|
||||||
else
|
|
||||||
:confirm
|
|
||||||
end
|
|
||||||
|
|
||||||
redirect(conn,
|
redirect(conn,
|
||||||
external:
|
external:
|
||||||
Routes.google_analytics_path(conn, action, site.domain,
|
Routes.google_analytics_path(conn, :confirm, site.domain,
|
||||||
property_or_view: property_or_view,
|
property: property,
|
||||||
access_token: access_token,
|
access_token: access_token,
|
||||||
refresh_token: refresh_token,
|
refresh_token: refresh_token,
|
||||||
expires_at: expires_at,
|
expires_at: expires_at,
|
||||||
@ -154,7 +119,7 @@ defmodule PlausibleWeb.GoogleAnalyticsController do
|
|||||||
|> Map.take(["access_token", "refresh_token", "expires_at"])
|
|> Map.take(["access_token", "refresh_token", "expires_at"])
|
||||||
|> Map.put("error", Atom.to_string(error))
|
|> Map.put("error", Atom.to_string(error))
|
||||||
|
|
||||||
property_or_view_form(conn, params)
|
property_form(conn, params)
|
||||||
|
|
||||||
{:error, :rate_limit_exceeded} ->
|
{:error, :rate_limit_exceeded} ->
|
||||||
conn
|
conn
|
||||||
@ -191,7 +156,7 @@ defmodule PlausibleWeb.GoogleAnalyticsController do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def confirm(conn, %{
|
def confirm(conn, %{
|
||||||
"property_or_view" => property_or_view,
|
"property" => property,
|
||||||
"access_token" => access_token,
|
"access_token" => access_token,
|
||||||
"refresh_token" => refresh_token,
|
"refresh_token" => refresh_token,
|
||||||
"expires_at" => expires_at,
|
"expires_at" => expires_at,
|
||||||
@ -205,8 +170,8 @@ defmodule PlausibleWeb.GoogleAnalyticsController do
|
|||||||
|
|
||||||
redirect_route = Routes.site_path(conn, :settings_imports_exports, site.domain)
|
redirect_route = Routes.site_path(conn, :settings_imports_exports, site.domain)
|
||||||
|
|
||||||
case Google.API.get_property_or_view(access_token, property_or_view) do
|
case Google.API.get_property(access_token, property) do
|
||||||
{:ok, %{name: property_or_view_name, id: property_or_view}} ->
|
{:ok, %{name: property_name, id: property}} ->
|
||||||
conn
|
conn
|
||||||
|> assign(:skip_plausible_tracking, true)
|
|> assign(:skip_plausible_tracking, true)
|
||||||
|> render("confirm.html",
|
|> render("confirm.html",
|
||||||
@ -214,11 +179,10 @@ defmodule PlausibleWeb.GoogleAnalyticsController do
|
|||||||
refresh_token: refresh_token,
|
refresh_token: refresh_token,
|
||||||
expires_at: expires_at,
|
expires_at: expires_at,
|
||||||
site: site,
|
site: site,
|
||||||
selected_property_or_view: property_or_view,
|
selected_property: property,
|
||||||
selected_property_or_view_name: property_or_view_name,
|
selected_property_name: property_name,
|
||||||
start_date: start_date,
|
start_date: start_date,
|
||||||
end_date: end_date,
|
end_date: end_date,
|
||||||
property?: Google.API.property?(property_or_view),
|
|
||||||
layout: {PlausibleWeb.LayoutView, "focus.html"}
|
layout: {PlausibleWeb.LayoutView, "focus.html"}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -265,7 +229,7 @@ defmodule PlausibleWeb.GoogleAnalyticsController do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def import(conn, %{
|
def import(conn, %{
|
||||||
"property_or_view" => property_or_view,
|
"property" => property,
|
||||||
"start_date" => start_date,
|
"start_date" => start_date,
|
||||||
"end_date" => end_date,
|
"end_date" => end_date,
|
||||||
"access_token" => access_token,
|
"access_token" => access_token,
|
||||||
@ -281,7 +245,8 @@ defmodule PlausibleWeb.GoogleAnalyticsController do
|
|||||||
redirect_route = Routes.site_path(conn, :settings_imports_exports, site.domain)
|
redirect_route = Routes.site_path(conn, :settings_imports_exports, site.domain)
|
||||||
|
|
||||||
import_opts = [
|
import_opts = [
|
||||||
label: property_or_view,
|
label: property,
|
||||||
|
property: property,
|
||||||
start_date: start_date,
|
start_date: start_date,
|
||||||
end_date: end_date,
|
end_date: end_date,
|
||||||
access_token: access_token,
|
access_token: access_token,
|
||||||
@ -291,7 +256,7 @@ defmodule PlausibleWeb.GoogleAnalyticsController do
|
|||||||
|
|
||||||
with {:ok, start_date, end_date} <- Imported.clamp_dates(site, start_date, end_date),
|
with {:ok, start_date, end_date} <- Imported.clamp_dates(site, start_date, end_date),
|
||||||
import_opts = [{:start_date, start_date}, {:end_date, end_date} | import_opts],
|
import_opts = [{:start_date, start_date}, {:end_date, end_date} | import_opts],
|
||||||
{:ok, _} <- schedule_job(site, current_user, property_or_view, import_opts) do
|
{:ok, _} <- Imported.GoogleAnalytics4.new_import(site, current_user, import_opts) do
|
||||||
conn
|
conn
|
||||||
|> put_flash(:success, "Import scheduled. An email will be sent when it completes.")
|
|> put_flash(:success, "Import scheduled. An email will be sent when it completes.")
|
||||||
|> redirect(external: redirect_route)
|
|> redirect(external: redirect_route)
|
||||||
@ -316,14 +281,4 @@ defmodule PlausibleWeb.GoogleAnalyticsController do
|
|||||||
|
|
||||||
defp ensure_dates(%Date{}, %Date{}), do: :ok
|
defp ensure_dates(%Date{}, %Date{}), do: :ok
|
||||||
defp ensure_dates(_, _), do: {:error, :no_data}
|
defp ensure_dates(_, _), do: {:error, :no_data}
|
||||||
|
|
||||||
defp schedule_job(site, current_user, property_or_view, opts) do
|
|
||||||
if Google.API.property?(property_or_view) do
|
|
||||||
opts = Keyword.put(opts, :property, property_or_view)
|
|
||||||
Imported.GoogleAnalytics4.new_import(site, current_user, opts)
|
|
||||||
else
|
|
||||||
opts = Keyword.put(opts, :view_id, property_or_view)
|
|
||||||
Imported.UniversalAnalytics.new_import(site, current_user, opts)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
@ -396,17 +396,13 @@ defmodule PlausibleWeb.Router do
|
|||||||
delete "/:website", SiteController, :delete_site
|
delete "/:website", SiteController, :delete_site
|
||||||
delete "/:website/stats", SiteController, :reset_stats
|
delete "/:website/stats", SiteController, :reset_stats
|
||||||
|
|
||||||
get "/:website/import/google-analytics/property-or-view",
|
get "/:website/import/google-analytics/property",
|
||||||
GoogleAnalyticsController,
|
GoogleAnalyticsController,
|
||||||
:property_or_view_form
|
:property_form
|
||||||
|
|
||||||
post "/:website/import/google-analytics/property-or-view",
|
post "/:website/import/google-analytics/property",
|
||||||
GoogleAnalyticsController,
|
GoogleAnalyticsController,
|
||||||
:property_or_view
|
:property
|
||||||
|
|
||||||
get "/:website/import/google-analytics/user-metric",
|
|
||||||
GoogleAnalyticsController,
|
|
||||||
:user_metric_notice
|
|
||||||
|
|
||||||
get "/:website/import/google-analytics/confirm", GoogleAnalyticsController, :confirm
|
get "/:website/import/google-analytics/confirm", GoogleAnalyticsController, :confirm
|
||||||
post "/:website/settings/google-import", GoogleAnalyticsController, :import
|
post "/:website/settings/google-import", GoogleAnalyticsController, :import
|
||||||
|
@ -6,27 +6,22 @@
|
|||||||
<%= hidden_input(f, :expires_at, value: @expires_at) %>
|
<%= hidden_input(f, :expires_at, value: @expires_at) %>
|
||||||
|
|
||||||
<div class="mt-6 text-sm text-gray-500 dark:text-gray-200">
|
<div class="mt-6 text-sm text-gray-500 dark:text-gray-200">
|
||||||
Stats from this
|
Stats from this property and time period will be imported from
|
||||||
<%= if @property? do %>
|
your Google Analytics account to your Plausible dashboard
|
||||||
property
|
|
||||||
<% else %>
|
|
||||||
view
|
|
||||||
<% end %>
|
|
||||||
and time period will be imported from your Google Analytics account to your Plausible dashboard
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6">
|
<div class="mt-6">
|
||||||
<%= styled_label(
|
<%= styled_label(
|
||||||
f,
|
f,
|
||||||
:property_or_view,
|
:property,
|
||||||
"Google Analytics #{if @property?, do: "property", else: "view"}"
|
"Google Analytics property"
|
||||||
) %>
|
) %>
|
||||||
<span class="block w-full text-base dark:text-gray-100 sm:text-sm dark:bg-gray-800">
|
<span class="block w-full text-base dark:text-gray-100 sm:text-sm dark:bg-gray-800">
|
||||||
<%= @selected_property_or_view_name %>
|
<%= @selected_property_name %>
|
||||||
</span>
|
</span>
|
||||||
<%= hidden_input(f, :property_or_view,
|
<%= hidden_input(f, :property,
|
||||||
readonly: "true",
|
readonly: "true",
|
||||||
value: @selected_property_or_view
|
value: @selected_property
|
||||||
) %>
|
) %>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between mt-3">
|
<div class="flex justify-between mt-3">
|
||||||
@ -51,8 +46,8 @@
|
|||||||
<p class="text-sm mt-4 sm:mt-0 dark:text-gray-100">
|
<p class="text-sm mt-4 sm:mt-0 dark:text-gray-100">
|
||||||
<a
|
<a
|
||||||
href={
|
href={
|
||||||
Routes.google_analytics_path(@conn, :property_or_view, @site.domain,
|
Routes.google_analytics_path(@conn, :property, @site.domain,
|
||||||
property_or_view: @selected_property_or_view,
|
property: @selected_property,
|
||||||
access_token: @access_token,
|
access_token: @access_token,
|
||||||
refresh_token: @refresh_token,
|
refresh_token: @refresh_token,
|
||||||
expires_at: @expires_at
|
expires_at: @expires_at
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<%= form_for @conn, Routes.google_analytics_path(@conn, :property_or_view, @site.domain), [onsubmit: "continueButton.disabled = true; return true;", class: "max-w-md w-full mx-auto bg-white dark:bg-gray-800 shadow-md rounded px-8 pt-6 pb-8 mb-4 mt-8"], fn f -> %>
|
<%= form_for @conn, Routes.google_analytics_path(@conn, :property, @site.domain), [onsubmit: "continueButton.disabled = true; return true;", class: "max-w-md w-full mx-auto bg-white dark:bg-gray-800 shadow-md rounded px-8 pt-6 pb-8 mb-4 mt-8"], fn f -> %>
|
||||||
<h2 class="text-xl font-black dark:text-gray-100">Import from Google Analytics</h2>
|
<h2 class="text-xl font-black dark:text-gray-100">Import from Google Analytics</h2>
|
||||||
|
|
||||||
<%= hidden_input(f, :access_token, value: @access_token) %>
|
<%= hidden_input(f, :access_token, value: @access_token) %>
|
||||||
@ -6,16 +6,16 @@
|
|||||||
<%= hidden_input(f, :expires_at, value: @expires_at) %>
|
<%= hidden_input(f, :expires_at, value: @expires_at) %>
|
||||||
|
|
||||||
<div class="mt-6 text-sm text-gray-500 dark:text-gray-200">
|
<div class="mt-6 text-sm text-gray-500 dark:text-gray-200">
|
||||||
Choose the property or view in your Google Analytics account that will be imported to the <%= @site.domain %> dashboard.
|
Choose the property in your Google Analytics account that will be imported to the <%= @site.domain %> dashboard.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<%= styled_label(f, :property_or_view, "Google Analytics property or view") %>
|
<%= styled_label(f, :property, "Google Analytics property") %>
|
||||||
<%= styled_select(f, :property_or_view, @properties_and_views,
|
<%= styled_select(f, :property, @properties,
|
||||||
prompt: "(Choose property or view)",
|
prompt: "(Choose property)",
|
||||||
required: "true"
|
required: "true"
|
||||||
) %>
|
) %>
|
||||||
<%= styled_error(@conn.assigns[:selected_property_or_view_error]) %>
|
<%= styled_error(@conn.assigns[:selected_property_error]) %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 flex flex-col-reverse sm:flex-row justify-between items-center">
|
<div class="mt-6 flex flex-col-reverse sm:flex-row justify-between items-center">
|
@ -1,65 +0,0 @@
|
|||||||
<div class="max-w-md w-full mx-auto bg-white dark:bg-gray-800 shadow-md rounded px-8 pt-6 pb-8 mb-4 mt-8">
|
|
||||||
<h2 class="text-xl font-black dark:text-gray-100">Import from Google Analytics</h2>
|
|
||||||
|
|
||||||
<div class="mt-6 text-sm text-gray-500 dark:text-gray-200">
|
|
||||||
<p>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="h-4 w-4 inline text-orange-600"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Important: Since your GA property includes data from before 23rd August 2016, you have to take an extra step to make sure we can import data smoothly.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<ol class="mt-4">
|
|
||||||
<li>1. Navigate to the GA property you want to import from</li>
|
|
||||||
<li>2. Go to Admin > Property Settings > User Analysis</li>
|
|
||||||
<li>3. Make sure <i>Enable Users Metric in Reporting</i> is <b>OFF</b></li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<p class="mt-4">
|
|
||||||
The setting may take a few minutes to take effect. If your imported data is showing 0 visitors in unexpected places, it's probably caused by this and you
|
|
||||||
can try importing again later.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-6 flex flex-col-reverse sm:flex-row justify-between items-center">
|
|
||||||
<p class="text-sm mt-4 sm:mt-0 dark:text-gray-100">
|
|
||||||
<a
|
|
||||||
href={
|
|
||||||
Routes.google_analytics_path(@conn, :property_or_view, @site.domain,
|
|
||||||
property_or_view: @property_or_view,
|
|
||||||
access_token: @access_token,
|
|
||||||
refresh_token: @refresh_token,
|
|
||||||
expires_at: @expires_at
|
|
||||||
)
|
|
||||||
}
|
|
||||||
class="underline text-indigo-600"
|
|
||||||
>
|
|
||||||
Go back
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<%= link("Continue ->",
|
|
||||||
to:
|
|
||||||
Routes.google_analytics_path(@conn, :confirm, @site.domain,
|
|
||||||
property_or_view: @property_or_view,
|
|
||||||
access_token: @access_token,
|
|
||||||
refresh_token: @refresh_token,
|
|
||||||
expires_at: @expires_at,
|
|
||||||
start_date: @start_date,
|
|
||||||
end_date: @end_date
|
|
||||||
),
|
|
||||||
class: "button sm:w-auto w-full"
|
|
||||||
) %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -3,7 +3,6 @@ defmodule Plausible.Google.APITest do
|
|||||||
use Plausible.Test.Support.HTTPMocker
|
use Plausible.Test.Support.HTTPMocker
|
||||||
|
|
||||||
alias Plausible.Google
|
alias Plausible.Google
|
||||||
alias Plausible.Imported.UniversalAnalytics
|
|
||||||
|
|
||||||
import ExUnit.CaptureLog
|
import ExUnit.CaptureLog
|
||||||
import Mox
|
import Mox
|
||||||
@ -11,220 +10,6 @@ defmodule Plausible.Google.APITest do
|
|||||||
|
|
||||||
setup [:create_user, :create_new_site]
|
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()
|
|
||||||
|
|
||||||
site_import = insert(:site_import)
|
|
||||||
|
|
||||||
persist_fn = fn table, rows ->
|
|
||||||
records = UniversalAnalytics.from_report(rows, site.id, site_import.id, 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()
|
|
||||||
|
|
||||||
site_import = insert(:site_import)
|
|
||||||
|
|
||||||
persist_fn = fn table, rows ->
|
|
||||||
records = UniversalAnalytics.from_report(rows, site.id, site_import.id, 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
|
describe "fetch_stats/3 errors" do
|
||||||
setup %{user: user, site: site} do
|
setup %{user: user, site: site} do
|
||||||
insert(:google_auth,
|
insert(:google_auth,
|
||||||
@ -393,60 +178,4 @@ defmodule Plausible.Google.APITest do
|
|||||||
assert {:error, :unsupported_filters} = Google.API.fetch_stats(site, query, 5)
|
assert {:error, :unsupported_filters} = Google.API.fetch_stats(site, query, 5)
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
@ -1,120 +0,0 @@
|
|||||||
defmodule Plausible.Imported.UniversalAnalyticsTest do
|
|
||||||
use Plausible.DataCase, async: true
|
|
||||||
use Plausible.Test.Support.HTTPMocker
|
|
||||||
|
|
||||||
alias Plausible.Imported.SiteImport
|
|
||||||
alias Plausible.Imported.UniversalAnalytics
|
|
||||||
|
|
||||||
require Plausible.Imported.SiteImport
|
|
||||||
|
|
||||||
setup [:create_user, :create_new_site]
|
|
||||||
|
|
||||||
describe "new_import/3 and parse_args/1" do
|
|
||||||
test "parses job args properly", %{user: user, site: site} do
|
|
||||||
expires_at = NaiveDateTime.to_iso8601(NaiveDateTime.utc_now())
|
|
||||||
|
|
||||||
assert {:ok, job} =
|
|
||||||
UniversalAnalytics.new_import(site, user,
|
|
||||||
view_id: "123",
|
|
||||||
label: "123",
|
|
||||||
start_date: "2023-10-01",
|
|
||||||
end_date: "2024-01-02",
|
|
||||||
access_token: "access123",
|
|
||||||
refresh_token: "refresh123",
|
|
||||||
token_expires_at: expires_at,
|
|
||||||
legacy: true
|
|
||||||
)
|
|
||||||
|
|
||||||
assert %Oban.Job{
|
|
||||||
args:
|
|
||||||
%{
|
|
||||||
"import_id" => import_id,
|
|
||||||
"view_id" => "123",
|
|
||||||
"start_date" => "2023-10-01",
|
|
||||||
"end_date" => "2024-01-02",
|
|
||||||
"access_token" => "access123",
|
|
||||||
"refresh_token" => "refresh123",
|
|
||||||
"token_expires_at" => ^expires_at
|
|
||||||
} = args
|
|
||||||
} = Repo.reload!(job)
|
|
||||||
|
|
||||||
assert [
|
|
||||||
%{
|
|
||||||
id: ^import_id,
|
|
||||||
label: "123",
|
|
||||||
source: :universal_analytics,
|
|
||||||
start_date: ~D[2023-10-01],
|
|
||||||
end_date: ~D[2024-01-02],
|
|
||||||
status: SiteImport.pending(),
|
|
||||||
legacy: true
|
|
||||||
}
|
|
||||||
] = Plausible.Imported.list_all_imports(site)
|
|
||||||
|
|
||||||
assert opts = [_ | _] = UniversalAnalytics.parse_args(args)
|
|
||||||
|
|
||||||
assert opts[:view_id] == "123"
|
|
||||||
assert opts[:date_range] == Date.range(~D[2023-10-01], ~D[2024-01-02])
|
|
||||||
assert opts[:auth] == {"access123", "refresh123", expires_at}
|
|
||||||
end
|
|
||||||
|
|
||||||
test "creates SiteImport with legacy flag set to false when instructed", %{
|
|
||||||
user: user,
|
|
||||||
site: site
|
|
||||||
} do
|
|
||||||
expires_at = NaiveDateTime.to_iso8601(NaiveDateTime.utc_now())
|
|
||||||
|
|
||||||
assert {:ok, job} =
|
|
||||||
UniversalAnalytics.new_import(site, user,
|
|
||||||
view_id: 123,
|
|
||||||
start_date: "2023-10-01",
|
|
||||||
end_date: "2024-01-02",
|
|
||||||
access_token: "access123",
|
|
||||||
refresh_token: "refresh123",
|
|
||||||
token_expires_at: expires_at,
|
|
||||||
legacy: false
|
|
||||||
)
|
|
||||||
|
|
||||||
assert %Oban.Job{args: %{"import_id" => import_id}} = Repo.reload!(job)
|
|
||||||
|
|
||||||
assert [
|
|
||||||
%{
|
|
||||||
id: ^import_id,
|
|
||||||
legacy: false
|
|
||||||
}
|
|
||||||
] = Plausible.Imported.list_all_imports(site)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "import_data/2" do
|
|
||||||
@tag :slow
|
|
||||||
test "imports page views from Google Analytics", %{user: user, site: site} do
|
|
||||||
mock_http_with("google_analytics_import#1.json")
|
|
||||||
|
|
||||||
view_id = "54297898"
|
|
||||||
start_date = ~D[2011-01-01]
|
|
||||||
end_date = ~D[2022-07-19]
|
|
||||||
|
|
||||||
future = DateTime.utc_now() |> DateTime.add(3600, :second) |> DateTime.to_iso8601()
|
|
||||||
access_token = "***"
|
|
||||||
refresh_token = "refresh_token"
|
|
||||||
|
|
||||||
{:ok, job} =
|
|
||||||
UniversalAnalytics.new_import(
|
|
||||||
site,
|
|
||||||
user,
|
|
||||||
view_id: view_id,
|
|
||||||
start_date: start_date,
|
|
||||||
end_date: end_date,
|
|
||||||
access_token: access_token,
|
|
||||||
refresh_token: refresh_token,
|
|
||||||
token_expires_at: future
|
|
||||||
)
|
|
||||||
|
|
||||||
job = Repo.reload!(job)
|
|
||||||
|
|
||||||
Plausible.Workers.ImportAnalytics.perform(job)
|
|
||||||
|
|
||||||
assert 1_495_150 == Plausible.Stats.Clickhouse.imported_pageview_count(site)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
File diff suppressed because it is too large
Load Diff
@ -12,39 +12,10 @@ defmodule PlausibleWeb.GoogleAnalyticsControllerTest do
|
|||||||
|
|
||||||
setup :verify_on_exit!
|
setup :verify_on_exit!
|
||||||
|
|
||||||
describe "GET /:website/import/google-analytics/user-metric" do
|
describe "GET /:website/import/google-analytics/property" do
|
||||||
setup [:create_user, :log_in, :create_new_site]
|
setup [:create_user, :log_in, :create_new_site]
|
||||||
|
|
||||||
test "renders with link to confirmation page", %{conn: conn, site: site} do
|
test "lists Google Analytics properties", %{conn: conn, site: site} do
|
||||||
response =
|
|
||||||
conn
|
|
||||||
|> get("/#{site.domain}/import/google-analytics/user-metric", %{
|
|
||||||
"property_or_view" => "123456",
|
|
||||||
"access_token" => "token",
|
|
||||||
"refresh_token" => "foo",
|
|
||||||
"expires_at" => "2022-09-22T20:01:37.112777",
|
|
||||||
"start_date" => "2020-02-22",
|
|
||||||
"end_date" => "2022-09-22"
|
|
||||||
})
|
|
||||||
|> html_response(200)
|
|
||||||
|
|
||||||
assert response =~
|
|
||||||
PlausibleWeb.Router.Helpers.google_analytics_path(conn, :confirm, site.domain,
|
|
||||||
property_or_view: "123456",
|
|
||||||
access_token: "token",
|
|
||||||
refresh_token: "foo",
|
|
||||||
expires_at: "2022-09-22T20:01:37.112777",
|
|
||||||
start_date: "2020-02-22",
|
|
||||||
end_date: "2022-09-22"
|
|
||||||
)
|
|
||||||
|> String.replace("&", "&")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "GET /:website/import/google-analytics/property-or-view" do
|
|
||||||
setup [:create_user, :log_in, :create_new_site]
|
|
||||||
|
|
||||||
test "lists Google Analytics properties WITHOUT UA views", %{conn: conn, site: site} do
|
|
||||||
expect(
|
expect(
|
||||||
Plausible.HTTPClient.Mock,
|
Plausible.HTTPClient.Mock,
|
||||||
:get,
|
:get,
|
||||||
@ -56,15 +27,13 @@ defmodule PlausibleWeb.GoogleAnalyticsControllerTest do
|
|||||||
|
|
||||||
response =
|
response =
|
||||||
conn
|
conn
|
||||||
|> get("/#{site.domain}/import/google-analytics/property-or-view", %{
|
|> get("/#{site.domain}/import/google-analytics/property", %{
|
||||||
"access_token" => "token",
|
"access_token" => "token",
|
||||||
"refresh_token" => "foo",
|
"refresh_token" => "foo",
|
||||||
"expires_at" => "2022-09-22T20:01:37.112777"
|
"expires_at" => "2022-09-22T20:01:37.112777"
|
||||||
})
|
})
|
||||||
|> html_response(200)
|
|> html_response(200)
|
||||||
|
|
||||||
refute response =~ "57238190 - one.test"
|
|
||||||
refute response =~ "54460083 - two.test"
|
|
||||||
assert response =~ "account.one - GA4 (properties/428685906)"
|
assert response =~ "account.one - GA4 (properties/428685906)"
|
||||||
assert response =~ "GA4 - Flood-It! (properties/153293282)"
|
assert response =~ "GA4 - Flood-It! (properties/153293282)"
|
||||||
assert response =~ "GA4 - Google Merch Shop (properties/213025502)"
|
assert response =~ "GA4 - Google Merch Shop (properties/213025502)"
|
||||||
@ -84,7 +53,7 @@ defmodule PlausibleWeb.GoogleAnalyticsControllerTest do
|
|||||||
|
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> get("/#{site.domain}/import/google-analytics/property-or-view", %{
|
|> get("/#{site.domain}/import/google-analytics/property", %{
|
||||||
"access_token" => "token",
|
"access_token" => "token",
|
||||||
"refresh_token" => "foo",
|
"refresh_token" => "foo",
|
||||||
"expires_at" => "2022-09-22T20:01:37.112777"
|
"expires_at" => "2022-09-22T20:01:37.112777"
|
||||||
@ -115,7 +84,7 @@ defmodule PlausibleWeb.GoogleAnalyticsControllerTest do
|
|||||||
|
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> get("/#{site.domain}/import/google-analytics/property-or-view", %{
|
|> get("/#{site.domain}/import/google-analytics/property", %{
|
||||||
"access_token" => "token",
|
"access_token" => "token",
|
||||||
"refresh_token" => "foo",
|
"refresh_token" => "foo",
|
||||||
"expires_at" => "2022-09-22T20:01:37.112777"
|
"expires_at" => "2022-09-22T20:01:37.112777"
|
||||||
@ -146,7 +115,7 @@ defmodule PlausibleWeb.GoogleAnalyticsControllerTest do
|
|||||||
|
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> get("/#{site.domain}/import/google-analytics/property-or-view", %{
|
|> get("/#{site.domain}/import/google-analytics/property", %{
|
||||||
"access_token" => "token",
|
"access_token" => "token",
|
||||||
"refresh_token" => "foo",
|
"refresh_token" => "foo",
|
||||||
"expires_at" => "2022-09-22T20:01:37.112777"
|
"expires_at" => "2022-09-22T20:01:37.112777"
|
||||||
@ -178,7 +147,7 @@ defmodule PlausibleWeb.GoogleAnalyticsControllerTest do
|
|||||||
|
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> get("/#{site.domain}/import/google-analytics/property-or-view", %{
|
|> get("/#{site.domain}/import/google-analytics/property", %{
|
||||||
"access_token" => "token",
|
"access_token" => "token",
|
||||||
"refresh_token" => "foo",
|
"refresh_token" => "foo",
|
||||||
"expires_at" => "2022-09-22T20:01:37.112777"
|
"expires_at" => "2022-09-22T20:01:37.112777"
|
||||||
@ -192,78 +161,14 @@ defmodule PlausibleWeb.GoogleAnalyticsControllerTest do
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert Phoenix.Flash.get(conn.assigns.flash, :error) =~
|
assert Phoenix.Flash.get(conn.assigns.flash, :error) =~
|
||||||
"We were unable to list your Google Analytics properties and views"
|
"We were unable to list your Google Analytics properties"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "POST /:website/import/google-analytics/property-or-view" do
|
describe "POST /:website/import/google-analytics/property" do
|
||||||
setup [:create_user, :log_in, :create_new_site]
|
setup [:create_user, :log_in, :create_new_site]
|
||||||
|
|
||||||
test "redirects to user metrics notice", %{conn: conn, site: site} do
|
|
||||||
expect(
|
|
||||||
Plausible.HTTPClient.Mock,
|
|
||||||
:post,
|
|
||||||
fn _url, _opts, _params ->
|
|
||||||
body = "fixture/ga_start_date.json" |> File.read!() |> Jason.decode!()
|
|
||||||
{:ok, %Finch.Response{body: body, status: 200}}
|
|
||||||
end
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(
|
|
||||||
Plausible.HTTPClient.Mock,
|
|
||||||
:post,
|
|
||||||
fn _url, _opts, _params ->
|
|
||||||
body = "fixture/ga_end_date.json" |> File.read!() |> Jason.decode!()
|
|
||||||
{:ok, %Finch.Response{body: body, status: 200}}
|
|
||||||
end
|
|
||||||
)
|
|
||||||
|
|
||||||
conn =
|
|
||||||
conn
|
|
||||||
|> post("/#{site.domain}/import/google-analytics/property-or-view", %{
|
|
||||||
"property_or_view" => "57238190",
|
|
||||||
"access_token" => "token",
|
|
||||||
"refresh_token" => "foo",
|
|
||||||
"expires_at" => "2022-09-22T20:01:37.112777"
|
|
||||||
})
|
|
||||||
|
|
||||||
assert redirected_to(conn, 302) =~
|
|
||||||
"/#{URI.encode_www_form(site.domain)}/import/google-analytics/user-metric"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "redirects to confirmation", %{conn: conn, site: site} do
|
test "redirects to confirmation", %{conn: conn, site: site} do
|
||||||
expect(
|
|
||||||
Plausible.HTTPClient.Mock,
|
|
||||||
:post,
|
|
||||||
fn _url, _opts, _params ->
|
|
||||||
body = "fixture/ga_start_date_later.json" |> File.read!() |> Jason.decode!()
|
|
||||||
{:ok, %Finch.Response{body: body, status: 200}}
|
|
||||||
end
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(
|
|
||||||
Plausible.HTTPClient.Mock,
|
|
||||||
:post,
|
|
||||||
fn _url, _opts, _params ->
|
|
||||||
body = "fixture/ga_end_date.json" |> File.read!() |> Jason.decode!()
|
|
||||||
{:ok, %Finch.Response{body: body, status: 200}}
|
|
||||||
end
|
|
||||||
)
|
|
||||||
|
|
||||||
conn =
|
|
||||||
conn
|
|
||||||
|> post("/#{site.domain}/import/google-analytics/property-or-view", %{
|
|
||||||
"property_or_view" => "57238190",
|
|
||||||
"access_token" => "token",
|
|
||||||
"refresh_token" => "foo",
|
|
||||||
"expires_at" => "2022-09-22T20:01:37.112777"
|
|
||||||
})
|
|
||||||
|
|
||||||
assert redirected_to(conn, 302) =~
|
|
||||||
"/#{URI.encode_www_form(site.domain)}/import/google-analytics/confirm"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "redirects to confirmation (GA4)", %{conn: conn, site: site} do
|
|
||||||
expect(
|
expect(
|
||||||
Plausible.HTTPClient.Mock,
|
Plausible.HTTPClient.Mock,
|
||||||
:post,
|
:post,
|
||||||
@ -284,8 +189,8 @@ defmodule PlausibleWeb.GoogleAnalyticsControllerTest do
|
|||||||
|
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> post("/#{site.domain}/import/google-analytics/property-or-view", %{
|
|> post("/#{site.domain}/import/google-analytics/property", %{
|
||||||
"property_or_view" => "properties/428685906",
|
"property" => "properties/428685906",
|
||||||
"access_token" => "token",
|
"access_token" => "token",
|
||||||
"refresh_token" => "foo",
|
"refresh_token" => "foo",
|
||||||
"expires_at" => "2022-09-22T20:01:37.112777"
|
"expires_at" => "2022-09-22T20:01:37.112777"
|
||||||
@ -336,8 +241,8 @@ defmodule PlausibleWeb.GoogleAnalyticsControllerTest do
|
|||||||
|
|
||||||
response =
|
response =
|
||||||
conn
|
conn
|
||||||
|> post("/#{site.domain}/import/google-analytics/property-or-view", %{
|
|> post("/#{site.domain}/import/google-analytics/property", %{
|
||||||
"property_or_view" => "properties/428685906",
|
"property" => "properties/428685906",
|
||||||
"access_token" => "token",
|
"access_token" => "token",
|
||||||
"refresh_token" => "foo",
|
"refresh_token" => "foo",
|
||||||
"expires_at" => "2022-09-22T20:01:37.112777"
|
"expires_at" => "2022-09-22T20:01:37.112777"
|
||||||
@ -376,8 +281,8 @@ defmodule PlausibleWeb.GoogleAnalyticsControllerTest do
|
|||||||
|
|
||||||
response =
|
response =
|
||||||
conn
|
conn
|
||||||
|> post("/#{site.domain}/import/google-analytics/property-or-view", %{
|
|> post("/#{site.domain}/import/google-analytics/property", %{
|
||||||
"property_or_view" => "properties/428685906",
|
"property" => "properties/428685906",
|
||||||
"access_token" => "token",
|
"access_token" => "token",
|
||||||
"refresh_token" => "foo",
|
"refresh_token" => "foo",
|
||||||
"expires_at" => "2022-09-22T20:01:37.112777"
|
"expires_at" => "2022-09-22T20:01:37.112777"
|
||||||
@ -387,7 +292,7 @@ defmodule PlausibleWeb.GoogleAnalyticsControllerTest do
|
|||||||
assert response =~ "No data found. Nothing to import."
|
assert response =~ "No data found. Nothing to import."
|
||||||
end
|
end
|
||||||
|
|
||||||
test "redirects to imports and exports on failed property/view choice with flash error",
|
test "redirects to imports and exports on failed property choice with flash error",
|
||||||
%{
|
%{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
site: site
|
site: site
|
||||||
@ -402,8 +307,8 @@ defmodule PlausibleWeb.GoogleAnalyticsControllerTest do
|
|||||||
|
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> post("/#{site.domain}/import/google-analytics/property-or-view", %{
|
|> post("/#{site.domain}/import/google-analytics/property", %{
|
||||||
"property_or_view" => "properties/428685906",
|
"property" => "properties/428685906",
|
||||||
"access_token" => "token",
|
"access_token" => "token",
|
||||||
"refresh_token" => "foo",
|
"refresh_token" => "foo",
|
||||||
"expires_at" => "2022-09-22T20:01:37.112777"
|
"expires_at" => "2022-09-22T20:01:37.112777"
|
||||||
@ -435,8 +340,8 @@ defmodule PlausibleWeb.GoogleAnalyticsControllerTest do
|
|||||||
|
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> post("/#{site.domain}/import/google-analytics/property-or-view", %{
|
|> post("/#{site.domain}/import/google-analytics/property", %{
|
||||||
"property_or_view" => "properties/428685906",
|
"property" => "properties/428685906",
|
||||||
"access_token" => "token",
|
"access_token" => "token",
|
||||||
"refresh_token" => "foo",
|
"refresh_token" => "foo",
|
||||||
"expires_at" => "2022-09-22T20:01:37.112777"
|
"expires_at" => "2022-09-22T20:01:37.112777"
|
||||||
@ -468,8 +373,8 @@ defmodule PlausibleWeb.GoogleAnalyticsControllerTest do
|
|||||||
|
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> post("/#{site.domain}/import/google-analytics/property-or-view", %{
|
|> post("/#{site.domain}/import/google-analytics/property", %{
|
||||||
"property_or_view" => "properties/428685906",
|
"property" => "properties/428685906",
|
||||||
"access_token" => "token",
|
"access_token" => "token",
|
||||||
"refresh_token" => "foo",
|
"refresh_token" => "foo",
|
||||||
"expires_at" => "2022-09-22T20:01:37.112777"
|
"expires_at" => "2022-09-22T20:01:37.112777"
|
||||||
@ -501,8 +406,8 @@ defmodule PlausibleWeb.GoogleAnalyticsControllerTest do
|
|||||||
|
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> post("/#{site.domain}/import/google-analytics/property-or-view", %{
|
|> post("/#{site.domain}/import/google-analytics/property", %{
|
||||||
"property_or_view" => "properties/428685906",
|
"property" => "properties/428685906",
|
||||||
"access_token" => "token",
|
"access_token" => "token",
|
||||||
"refresh_token" => "foo",
|
"refresh_token" => "foo",
|
||||||
"expires_at" => "2022-09-22T20:01:37.112777"
|
"expires_at" => "2022-09-22T20:01:37.112777"
|
||||||
@ -523,45 +428,6 @@ defmodule PlausibleWeb.GoogleAnalyticsControllerTest do
|
|||||||
describe "GET /:website/import/google-analytics/confirm" do
|
describe "GET /:website/import/google-analytics/confirm" do
|
||||||
setup [:create_user, :log_in, :create_new_site]
|
setup [:create_user, :log_in, :create_new_site]
|
||||||
|
|
||||||
test "renders confirmation form for Universal Analytics import", %{conn: conn, site: site} do
|
|
||||||
expect(
|
|
||||||
Plausible.HTTPClient.Mock,
|
|
||||||
:get,
|
|
||||||
fn _url, _headers ->
|
|
||||||
body = "fixture/ga_list_views.json" |> File.read!() |> Jason.decode!()
|
|
||||||
{:ok, %Finch.Response{body: body, status: 200}}
|
|
||||||
end
|
|
||||||
)
|
|
||||||
|
|
||||||
response =
|
|
||||||
conn
|
|
||||||
|> get("/#{site.domain}/import/google-analytics/confirm", %{
|
|
||||||
"property_or_view" => "57238190",
|
|
||||||
"access_token" => "token",
|
|
||||||
"refresh_token" => "foo",
|
|
||||||
"expires_at" => "2022-09-22T20:01:37.112777",
|
|
||||||
"start_date" => "2012-01-18",
|
|
||||||
"end_date" => "2022-09-22"
|
|
||||||
})
|
|
||||||
|> html_response(200)
|
|
||||||
|
|
||||||
action_url = PlausibleWeb.Router.Helpers.google_analytics_path(conn, :import, site.domain)
|
|
||||||
|
|
||||||
assert text_of_attr(response, "form", "action") == action_url
|
|
||||||
|
|
||||||
assert text_of_attr(response, ~s|input[name=access_token]|, "value") == "token"
|
|
||||||
assert text_of_attr(response, ~s|input[name=refresh_token]|, "value") == "foo"
|
|
||||||
|
|
||||||
assert text_of_attr(response, ~s|input[name=expires_at]|, "value") ==
|
|
||||||
"2022-09-22T20:01:37.112777"
|
|
||||||
|
|
||||||
assert text_of_attr(response, ~s|input[name=property_or_view]|, "value") == "57238190"
|
|
||||||
|
|
||||||
assert text_of_attr(response, ~s|input[name=start_date]|, "value") == "2012-01-18"
|
|
||||||
|
|
||||||
assert text_of_attr(response, ~s|input[name=end_date]|, "value") == "2022-09-22"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "renders confirmation form for Google Analytics 4 import", %{conn: conn, site: site} do
|
test "renders confirmation form for Google Analytics 4 import", %{conn: conn, site: site} do
|
||||||
expect(
|
expect(
|
||||||
Plausible.HTTPClient.Mock,
|
Plausible.HTTPClient.Mock,
|
||||||
@ -575,7 +441,7 @@ defmodule PlausibleWeb.GoogleAnalyticsControllerTest do
|
|||||||
response =
|
response =
|
||||||
conn
|
conn
|
||||||
|> get("/#{site.domain}/import/google-analytics/confirm", %{
|
|> get("/#{site.domain}/import/google-analytics/confirm", %{
|
||||||
"property_or_view" => "properties/428685444",
|
"property" => "properties/428685444",
|
||||||
"access_token" => "token",
|
"access_token" => "token",
|
||||||
"refresh_token" => "foo",
|
"refresh_token" => "foo",
|
||||||
"expires_at" => "2022-09-22T20:01:37.112777",
|
"expires_at" => "2022-09-22T20:01:37.112777",
|
||||||
@ -594,7 +460,7 @@ defmodule PlausibleWeb.GoogleAnalyticsControllerTest do
|
|||||||
assert text_of_attr(response, ~s|input[name=expires_at]|, "value") ==
|
assert text_of_attr(response, ~s|input[name=expires_at]|, "value") ==
|
||||||
"2022-09-22T20:01:37.112777"
|
"2022-09-22T20:01:37.112777"
|
||||||
|
|
||||||
assert text_of_attr(response, ~s|input[name=property_or_view]|, "value") ==
|
assert text_of_attr(response, ~s|input[name=property]|, "value") ==
|
||||||
"properties/428685444"
|
"properties/428685444"
|
||||||
|
|
||||||
assert text_of_attr(response, ~s|input[name=start_date]|, "value") == "2024-02-22"
|
assert text_of_attr(response, ~s|input[name=start_date]|, "value") == "2024-02-22"
|
||||||
@ -602,7 +468,7 @@ defmodule PlausibleWeb.GoogleAnalyticsControllerTest do
|
|||||||
assert text_of_attr(response, ~s|input[name=end_date]|, "value") == "2024-02-26"
|
assert text_of_attr(response, ~s|input[name=end_date]|, "value") == "2024-02-26"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "redirects to imports and exports on failed property/view retrieval with flash error",
|
test "redirects to imports and exports on failed property retrieval with flash error",
|
||||||
%{
|
%{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
site: site
|
site: site
|
||||||
@ -618,7 +484,7 @@ defmodule PlausibleWeb.GoogleAnalyticsControllerTest do
|
|||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> get("/#{site.domain}/import/google-analytics/confirm", %{
|
|> get("/#{site.domain}/import/google-analytics/confirm", %{
|
||||||
"property_or_view" => "properties/428685906",
|
"property" => "properties/428685906",
|
||||||
"access_token" => "token",
|
"access_token" => "token",
|
||||||
"refresh_token" => "foo",
|
"refresh_token" => "foo",
|
||||||
"expires_at" => "2022-09-22T20:01:37.112777",
|
"expires_at" => "2022-09-22T20:01:37.112777",
|
||||||
@ -653,7 +519,7 @@ defmodule PlausibleWeb.GoogleAnalyticsControllerTest do
|
|||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> get("/#{site.domain}/import/google-analytics/confirm", %{
|
|> get("/#{site.domain}/import/google-analytics/confirm", %{
|
||||||
"property_or_view" => "properties/428685906",
|
"property" => "properties/428685906",
|
||||||
"access_token" => "token",
|
"access_token" => "token",
|
||||||
"refresh_token" => "foo",
|
"refresh_token" => "foo",
|
||||||
"expires_at" => "2022-09-22T20:01:37.112777",
|
"expires_at" => "2022-09-22T20:01:37.112777",
|
||||||
@ -688,7 +554,7 @@ defmodule PlausibleWeb.GoogleAnalyticsControllerTest do
|
|||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> get("/#{site.domain}/import/google-analytics/confirm", %{
|
|> get("/#{site.domain}/import/google-analytics/confirm", %{
|
||||||
"property_or_view" => "properties/428685906",
|
"property" => "properties/428685906",
|
||||||
"access_token" => "token",
|
"access_token" => "token",
|
||||||
"refresh_token" => "foo",
|
"refresh_token" => "foo",
|
||||||
"expires_at" => "2022-09-22T20:01:37.112777",
|
"expires_at" => "2022-09-22T20:01:37.112777",
|
||||||
@ -723,7 +589,7 @@ defmodule PlausibleWeb.GoogleAnalyticsControllerTest do
|
|||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> get("/#{site.domain}/import/google-analytics/confirm", %{
|
|> get("/#{site.domain}/import/google-analytics/confirm", %{
|
||||||
"property_or_view" => "properties/428685906",
|
"property" => "properties/428685906",
|
||||||
"access_token" => "token",
|
"access_token" => "token",
|
||||||
"refresh_token" => "foo",
|
"refresh_token" => "foo",
|
||||||
"expires_at" => "2022-09-22T20:01:37.112777",
|
"expires_at" => "2022-09-22T20:01:37.112777",
|
||||||
@ -749,7 +615,7 @@ defmodule PlausibleWeb.GoogleAnalyticsControllerTest do
|
|||||||
test "creates Google Analytics 4 site import instance", %{conn: conn, site: site} do
|
test "creates Google Analytics 4 site import instance", %{conn: conn, site: site} do
|
||||||
conn =
|
conn =
|
||||||
post(conn, "/#{site.domain}/settings/google-import", %{
|
post(conn, "/#{site.domain}/settings/google-import", %{
|
||||||
"property_or_view" => "properties/123456",
|
"property" => "properties/123456",
|
||||||
"start_date" => "2018-03-01",
|
"start_date" => "2018-03-01",
|
||||||
"end_date" => "2022-03-01",
|
"end_date" => "2022-03-01",
|
||||||
"access_token" => "token",
|
"access_token" => "token",
|
||||||
@ -767,54 +633,9 @@ defmodule PlausibleWeb.GoogleAnalyticsControllerTest do
|
|||||||
assert site_import.status == SiteImport.pending()
|
assert site_import.status == SiteImport.pending()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "creates Universal Analytics site import instance", %{conn: conn, site: site} do
|
|
||||||
conn =
|
|
||||||
post(conn, "/#{site.domain}/settings/google-import", %{
|
|
||||||
"property_or_view" => "123456",
|
|
||||||
"start_date" => "2018-03-01",
|
|
||||||
"end_date" => "2022-03-01",
|
|
||||||
"access_token" => "token",
|
|
||||||
"refresh_token" => "foo",
|
|
||||||
"expires_at" => "2022-09-22T20:01:37.112777"
|
|
||||||
})
|
|
||||||
|
|
||||||
assert redirected_to(conn, 302) ==
|
|
||||||
PlausibleWeb.Router.Helpers.site_path(conn, :settings_imports_exports, site.domain)
|
|
||||||
|
|
||||||
[site_import] = Plausible.Imported.list_all_imports(site)
|
|
||||||
|
|
||||||
assert site_import.source == :universal_analytics
|
|
||||||
assert site_import.end_date == ~D[2022-03-01]
|
|
||||||
assert site_import.status == SiteImport.pending()
|
|
||||||
end
|
|
||||||
|
|
||||||
test "redirects to imports and exports when creating UA job", %{
|
|
||||||
conn: conn,
|
|
||||||
site: site
|
|
||||||
} do
|
|
||||||
conn =
|
|
||||||
post(conn, "/#{site.domain}/settings/google-import", %{
|
|
||||||
"property_or_view" => "123456",
|
|
||||||
"start_date" => "2018-03-01",
|
|
||||||
"end_date" => "2022-03-01",
|
|
||||||
"access_token" => "token",
|
|
||||||
"refresh_token" => "foo",
|
|
||||||
"expires_at" => "2022-09-22T20:01:37.112777"
|
|
||||||
})
|
|
||||||
|
|
||||||
assert redirected_to(conn, 302) ==
|
|
||||||
PlausibleWeb.Router.Helpers.site_path(conn, :settings_imports_exports, site.domain)
|
|
||||||
|
|
||||||
[site_import] = Plausible.Imported.list_all_imports(site)
|
|
||||||
|
|
||||||
assert site_import.source == :universal_analytics
|
|
||||||
assert site_import.end_date == ~D[2022-03-01]
|
|
||||||
assert site_import.status == SiteImport.pending()
|
|
||||||
end
|
|
||||||
|
|
||||||
test "schedules a Google Analytics 4 import job in Oban", %{conn: conn, site: site} do
|
test "schedules a Google Analytics 4 import job in Oban", %{conn: conn, site: site} do
|
||||||
post(conn, "/#{site.domain}/settings/google-import", %{
|
post(conn, "/#{site.domain}/settings/google-import", %{
|
||||||
"property_or_view" => "properties/123456",
|
"property" => "properties/123456",
|
||||||
"start_date" => "2018-03-01",
|
"start_date" => "2018-03-01",
|
||||||
"end_date" => "2022-03-01",
|
"end_date" => "2022-03-01",
|
||||||
"access_token" => "token",
|
"access_token" => "token",
|
||||||
@ -838,32 +659,6 @@ defmodule PlausibleWeb.GoogleAnalyticsControllerTest do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "schedules a Universal Analytics import job in Oban", %{conn: conn, site: site} do
|
|
||||||
post(conn, "/#{site.domain}/settings/google-import", %{
|
|
||||||
"property_or_view" => "123456",
|
|
||||||
"start_date" => "2018-03-01",
|
|
||||||
"end_date" => "2022-03-01",
|
|
||||||
"access_token" => "token",
|
|
||||||
"refresh_token" => "foo",
|
|
||||||
"expires_at" => "2022-09-22T20:01:37.112777"
|
|
||||||
})
|
|
||||||
|
|
||||||
assert [%{id: import_id, legacy: false}] = Plausible.Imported.list_all_imports(site)
|
|
||||||
|
|
||||||
assert_enqueued(
|
|
||||||
worker: Plausible.Workers.ImportAnalytics,
|
|
||||||
args: %{
|
|
||||||
"import_id" => import_id,
|
|
||||||
"view_id" => "123456",
|
|
||||||
"start_date" => "2018-03-01",
|
|
||||||
"end_date" => "2022-03-01",
|
|
||||||
"access_token" => "token",
|
|
||||||
"refresh_token" => "foo",
|
|
||||||
"token_expires_at" => "2022-09-22T20:01:37.112777"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "does not start another import when there's any other in progress for the same site", %{
|
test "does not start another import when there's any other in progress for the same site", %{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
site: site,
|
site: site,
|
||||||
@ -876,7 +671,7 @@ defmodule PlausibleWeb.GoogleAnalyticsControllerTest do
|
|||||||
)
|
)
|
||||||
|
|
||||||
post(conn, "/#{site.domain}/settings/google-import", %{
|
post(conn, "/#{site.domain}/settings/google-import", %{
|
||||||
"property_or_view" => "properties/123456",
|
"property" => "properties/123456",
|
||||||
"start_date" => "2018-03-01",
|
"start_date" => "2018-03-01",
|
||||||
"end_date" => "2022-03-01",
|
"end_date" => "2022-03-01",
|
||||||
"access_token" => "token",
|
"access_token" => "token",
|
||||||
@ -886,7 +681,7 @@ defmodule PlausibleWeb.GoogleAnalyticsControllerTest do
|
|||||||
|
|
||||||
conn =
|
conn =
|
||||||
post(conn, "/#{site.domain}/settings/google-import", %{
|
post(conn, "/#{site.domain}/settings/google-import", %{
|
||||||
"property_or_view" => "123456",
|
"property" => "123456",
|
||||||
"start_date" => "2018-03-01",
|
"start_date" => "2018-03-01",
|
||||||
"end_date" => "2022-03-01",
|
"end_date" => "2022-03-01",
|
||||||
"access_token" => "token",
|
"access_token" => "token",
|
||||||
@ -919,7 +714,7 @@ defmodule PlausibleWeb.GoogleAnalyticsControllerTest do
|
|||||||
|
|
||||||
conn =
|
conn =
|
||||||
post(conn, "/#{site.domain}/settings/google-import", %{
|
post(conn, "/#{site.domain}/settings/google-import", %{
|
||||||
"property_or_view" => "123456",
|
"property" => "123456",
|
||||||
"start_date" => "2023-03-01",
|
"start_date" => "2023-03-01",
|
||||||
"end_date" => "2022-03-01",
|
"end_date" => "2022-03-01",
|
||||||
"access_token" => "token",
|
"access_token" => "token",
|
||||||
|
Loading…
Reference in New Issue
Block a user