Unify UA and GA4 import flow into one (#3888)

* Unify GA4 and UA import flow into one

* Clean up property and view data retrieval via Google HTTP APIs

* Turn `Map.get` into `Map.fetch!` in API response processing code

* Bump list account summaries page size limit to max of 200

* Show only views in legacy flow and fix legacy redirect after import start

* Move google analytics import actions tests to a separate module

* Extend Google Analytics controller tests

* DRY up `property?` predicate (h/t @RobertJoonas)
This commit is contained in:
Adrian Gruntkowski 2024-03-21 11:37:10 +01:00 committed by GitHub
parent 5f9465614b
commit d6e81670e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 622 additions and 462 deletions

View File

@ -0,0 +1,12 @@
{
"account": "accounts/28425555",
"createTime": "2024-02-22T10:50:15.462Z",
"currencyCode": "USD",
"displayName": "account.one - GA4",
"name": "properties/428685444",
"parent": "accounts/28425555",
"propertyType": "PROPERTY_TYPE_ORDINARY",
"serviceLevel": "GOOGLE_ANALYTICS_STANDARD",
"timeZone": "Europe/Warsaw",
"updateTime": "2024-02-22T10:50:15.462Z"
}

View File

@ -0,0 +1,38 @@
{
"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"
}
]
}

View File

@ -23,16 +23,38 @@ defmodule Plausible.Google.API do
def import_authorize_url(site_id, redirect_to, opts \\ []) do
legacy = Keyword.get(opts, :legacy, true)
ga4 = Keyword.get(opts, :ga4, false)
"https://accounts.google.com/o/oauth2/v2/auth?client_id=#{client_id()}&redirect_uri=#{redirect_uri()}&prompt=consent&response_type=code&access_type=offline&scope=#{@import_scope}&state=" <>
Jason.encode!([site_id, redirect_to, legacy, ga4])
Jason.encode!([site_id, redirect_to, legacy])
end
def fetch_access_token!(code) do
HTTP.fetch_access_token!(code)
end
def list_properties_and_views(access_token) do
with {:ok, properties} <- Plausible.Google.GA4.API.list_properties(access_token),
{:ok, views} <- Plausible.Google.UA.API.list_views(access_token) do
{:ok, properties ++ views}
end
end
def get_property_or_view(access_token, property_or_view) do
if property?(property_or_view) do
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
def get_analytics_start_date(access_token, property_or_view) do
if property?(property_or_view) do
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
def fetch_verified_properties(auth) do
with {:ok, access_token} <- maybe_refresh_token(auth),
{:ok, sites} <- Plausible.Google.HTTP.list_sites(access_token) do
@ -90,6 +112,8 @@ defmodule Plausible.Google.API do
end
end
def property?(value), do: String.starts_with?(value, "properties/")
defp do_refresh_token(refresh_token) do
case HTTP.refresh_auth_token(refresh_token) do
{:ok, %{"access_token" => new_access_token, "expires_in" => expires_in}} ->

View File

@ -25,29 +25,35 @@ defmodule Plausible.Google.GA4.API do
accounts
|> Enum.filter(& &1["propertySummaries"])
|> Enum.map(fn account ->
{"#{account["displayName"]} (#{account["account"]})",
%{"account" => account_id, "displayName" => account_name} = account
{"#{account_name} (#{account_id})",
Enum.map(account["propertySummaries"], fn property ->
{"#{property["displayName"]} (#{property["property"]})", property["property"]}
%{"displayName" => property_name, "property" => property_id} = property
{"#{property_name} (#{property_id})", property_id}
end)}
end)
{:ok, accounts}
error ->
error
{:error, cause} ->
{:error, cause}
end
end
def get_property(access_token, lookup_property) do
case list_properties(access_token) do
{:ok, properties} ->
property =
properties
|> Enum.map(&elem(&1, 1))
|> List.flatten()
|> Enum.find(fn {_name, property} -> property == lookup_property end)
case GA4.HTTP.get_property(access_token, lookup_property) do
{:ok, property} ->
%{"displayName" => property_name, "name" => property_id, "account" => account_id} =
property
{:ok, property}
{:ok,
%{
id: property_id,
name: "#{property_name} (#{property_id})",
account_id: account_id
}}
{:error, cause} ->
{:error, cause}

View File

@ -49,9 +49,8 @@ defmodule Plausible.Google.GA4.HTTP do
)
with {:ok, %{body: body}} <- response,
# File.write!("fixture/ga4_report_#{report_request.dataset}.json", Jason.encode!(body)),
{:ok, report} <- parse_report_from_response(body),
row_count <- Map.get(report, "rowCount"),
row_count <- Map.fetch!(report, "rowCount"),
{:ok, report} <- convert_to_maps(report) do
{:ok, {report, row_count}}
else
@ -130,7 +129,7 @@ defmodule Plausible.Google.GA4.HTTP do
end
def list_accounts_for_user(access_token) do
url = "#{admin_api_url()}/v1beta/accountSummaries"
url = "#{admin_api_url()}/v1beta/accountSummaries?pageSize=200"
headers = [{"Authorization", "Bearer #{access_token}"}]
@ -147,6 +146,30 @@ defmodule Plausible.Google.GA4.HTTP do
end
end
def get_property(access_token, property) do
url = "#{admin_api_url()}/v1beta/#{property}"
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, %HTTPClient.Non200Error{} = error} when error.reason.status in [404] ->
{:error, :not_found}
{:error, %HTTPClient.Non200Error{} = error} ->
Sentry.capture_message("Error retrieving Google property #{property}",
extra: %{error: error}
)
{:error, :unknown}
end
end
@earliest_valid_date "2015-08-14"
def get_analytics_start_date(access_token, property) do
params = %{

View File

@ -26,7 +26,11 @@ defmodule Plausible.Google.UA.API do
def list_views(access_token) do
case UA.HTTP.list_views_for_user(access_token) do
{:ok, %{"items" => views}} ->
views = Enum.group_by(views, &view_hostname/1, &view_names/1)
views =
views
|> Enum.group_by(&view_hostname/1, &view_names/1)
|> Enum.sort_by(fn {key, _} -> key end)
{:ok, views}
error ->
@ -40,18 +44,19 @@ defmodule Plausible.Google.UA.API do
Returns a single Google Analytics view if the user has access to it.
"""
def get_view(access_token, lookup_id) do
case list_views(access_token) do
{:ok, views} ->
view =
views
|> Map.values()
|> List.flatten()
|> Enum.find(fn {_name, id} -> id == lookup_id end)
with {:ok, views} <- list_views(access_token) do
views =
views
|> Enum.map(&elem(&1, 1))
|> List.flatten()
{:ok, view}
case Enum.find(views, fn {_name, id} -> id == lookup_id end) do
{view_name, view_id} ->
{:ok, %{id: view_id, name: "#{view_name}"}}
{:error, cause} ->
{:error, cause}
nil ->
{:error, :not_found}
end
end
end

View File

@ -136,7 +136,7 @@ defmodule Plausible.Google.UA.HTTP do
url = "#{reporting_api_url()}/v4/reports:batchGet"
headers = [{"Authorization", "Bearer #{access_token}"}]
case HTTPClient.post(url, headers, params) do
case HTTPClient.impl().post(url, headers, params) do
{:ok, %Finch.Response{body: body, status: 200}} ->
report = List.first(body["reports"])

View File

@ -750,16 +750,13 @@ defmodule PlausibleWeb.AuthController do
def google_auth_callback(conn, %{"code" => code, "state" => state}) do
res = Plausible.Google.API.fetch_access_token!(code)
[site_id, redirect_to, legacy, ga4] =
[site_id, redirect_to, legacy] =
case Jason.decode!(state) do
[site_id, redirect_to] ->
[site_id, redirect_to, true, false]
[site_id, redirect_to, true]
[site_id, redirect_to, legacy] ->
[site_id, redirect_to, legacy, false]
[site_id, redirect_to, legacy, ga4] ->
[site_id, redirect_to, legacy, ga4]
[site_id, redirect_to, legacy]
end
site = Repo.get(Plausible.Site, site_id)
@ -767,26 +764,15 @@ defmodule PlausibleWeb.AuthController do
case redirect_to do
"import" ->
if ga4 do
redirect(conn,
external:
Routes.google_analytics4_path(conn, :property_form, site.domain,
access_token: res["access_token"],
refresh_token: res["refresh_token"],
expires_at: NaiveDateTime.to_iso8601(expires_at)
)
)
else
redirect(conn,
external:
Routes.universal_analytics_path(conn, :view_id_form, site.domain,
access_token: res["access_token"],
refresh_token: res["refresh_token"],
expires_at: NaiveDateTime.to_iso8601(expires_at),
legacy: legacy
)
)
end
redirect(conn,
external:
Routes.google_analytics_path(conn, :property_or_view_form, site.domain,
access_token: res["access_token"],
refresh_token: res["refresh_token"],
expires_at: NaiveDateTime.to_iso8601(expires_at),
legacy: legacy
)
)
_ ->
id_token = res["id_token"]

View File

@ -1,143 +0,0 @@
defmodule PlausibleWeb.GoogleAnalytics4Controller do
use PlausibleWeb, :controller
plug(PlausibleWeb.RequireAccountPlug)
plug(PlausibleWeb.AuthorizeSiteAccess, [:owner, :admin, :super_admin])
def property_form(conn, %{
"access_token" => access_token,
"refresh_token" => refresh_token,
"expires_at" => expires_at
}) do
redirect_route = Routes.site_path(conn, :settings_imports_exports, conn.assigns.site.domain)
case Plausible.Google.GA4.API.list_properties(access_token) do
{:ok, properties} ->
conn
|> assign(:skip_plausible_tracking, true)
|> render("property_form.html",
access_token: access_token,
refresh_token: refresh_token,
expires_at: expires_at,
site: conn.assigns.site,
properties: properties,
layout: {PlausibleWeb.LayoutView, "focus.html"}
)
{:error, :authentication_failed} ->
conn
|> put_flash(
:error,
"We were unable to authenticate your Google Analytics account. Please check that you have granted us permission to 'See and download your Google Analytics data' and try again."
)
|> redirect(external: redirect_route)
{:error, _any} ->
conn
|> put_flash(
:error,
"We were unable to list your Google Analytics properties. If the problem persists, please contact support for assistance."
)
|> redirect(external: redirect_route)
end
end
def property(conn, %{
"property" => property,
"access_token" => access_token,
"refresh_token" => refresh_token,
"expires_at" => expires_at
}) do
site = conn.assigns.site
start_date = Plausible.Google.GA4.API.get_analytics_start_date(access_token, property)
case start_date do
{:ok, nil} ->
{:ok, properties} = Plausible.Google.GA4.API.list_properties(access_token)
conn
|> assign(:skip_plausible_tracking, true)
|> render("property_form.html",
access_token: access_token,
refresh_token: refresh_token,
expires_at: expires_at,
site: site,
properties: properties,
selected_property_error: "No data found. Nothing to import",
layout: {PlausibleWeb.LayoutView, "focus.html"}
)
{:ok, _date} ->
redirect(conn,
to:
Routes.google_analytics4_path(conn, :confirm, site.domain,
property: property,
access_token: access_token,
refresh_token: refresh_token,
expires_at: expires_at
)
)
end
end
def confirm(conn, %{
"property" => property,
"access_token" => access_token,
"refresh_token" => refresh_token,
"expires_at" => expires_at
}) do
site = conn.assigns.site
start_date = Plausible.Google.GA4.API.get_analytics_start_date(access_token, property)
end_date = Plausible.Sites.native_stats_start_date(site) || Timex.today(site.timezone)
{:ok, {property_name, property}} =
Plausible.Google.GA4.API.get_property(access_token, property)
conn
|> assign(:skip_plausible_tracking, true)
|> render("confirm.html",
access_token: access_token,
refresh_token: refresh_token,
expires_at: expires_at,
site: site,
selected_property: property,
selected_property_name: property_name,
start_date: start_date,
end_date: end_date,
layout: {PlausibleWeb.LayoutView, "focus.html"}
)
end
def import(conn, %{
"property" => property,
"start_date" => start_date,
"end_date" => end_date,
"access_token" => access_token,
"refresh_token" => refresh_token,
"expires_at" => expires_at
}) do
site = conn.assigns.site
current_user = conn.assigns.current_user
redirect_route = Routes.site_path(conn, :settings_imports_exports, site.domain)
{:ok, _} =
Plausible.Imported.GoogleAnalytics4.new_import(
site,
current_user,
property: property,
start_date: start_date,
end_date: end_date,
access_token: access_token,
refresh_token: refresh_token,
token_expires_at: expires_at
)
conn
|> put_flash(:success, "Import scheduled. An email will be sent when it completes.")
|> redirect(external: redirect_route)
end
end

View File

@ -1,4 +1,4 @@
defmodule PlausibleWeb.UniversalAnalyticsController do
defmodule PlausibleWeb.GoogleAnalyticsController do
use PlausibleWeb, :controller
plug(PlausibleWeb.RequireAccountPlug)
@ -6,7 +6,7 @@ defmodule PlausibleWeb.UniversalAnalyticsController do
plug(PlausibleWeb.AuthorizeSiteAccess, [:owner, :admin, :super_admin])
def user_metric_notice(conn, %{
"view_id" => view_id,
"property_or_view" => property_or_view,
"access_token" => access_token,
"refresh_token" => refresh_token,
"expires_at" => expires_at,
@ -18,7 +18,7 @@ defmodule PlausibleWeb.UniversalAnalyticsController do
|> assign(:skip_plausible_tracking, true)
|> render("user_metric_form.html",
site: site,
view_id: view_id,
property_or_view: property_or_view,
access_token: access_token,
refresh_token: refresh_token,
expires_at: expires_at,
@ -27,29 +27,38 @@ defmodule PlausibleWeb.UniversalAnalyticsController do
)
end
def view_id_form(conn, %{
def property_or_view_form(conn, %{
"access_token" => access_token,
"refresh_token" => refresh_token,
"expires_at" => expires_at,
"legacy" => legacy
}) do
site = conn.assigns.site
redirect_route =
if legacy == "true" do
Routes.site_path(conn, :settings_integrations, conn.assigns.site.domain)
Routes.site_path(conn, :settings_integrations, site.domain)
else
Routes.site_path(conn, :settings_imports_exports, conn.assigns.site.domain)
Routes.site_path(conn, :settings_imports_exports, site.domain)
end
case Plausible.Google.UA.API.list_views(access_token) do
{:ok, view_ids} ->
result =
if legacy == "true" do
Plausible.Google.UA.API.list_views(access_token)
else
Plausible.Google.API.list_properties_and_views(access_token)
end
case result do
{:ok, properties_and_views} ->
conn
|> assign(:skip_plausible_tracking, true)
|> render("view_id_form.html",
|> render("property_or_view_form.html",
access_token: access_token,
refresh_token: refresh_token,
expires_at: expires_at,
site: conn.assigns.site,
view_ids: view_ids,
properties_and_views: properties_and_views,
legacy: legacy,
layout: {PlausibleWeb.LayoutView, "focus.html"}
)
@ -74,62 +83,62 @@ defmodule PlausibleWeb.UniversalAnalyticsController do
# see https://stackoverflow.com/a/57416769
@google_analytics_new_user_metric_date ~D[2016-08-24]
def view_id(conn, %{
"view_id" => view_id,
def property_or_view(conn, %{
"property_or_view" => property_or_view,
"access_token" => access_token,
"refresh_token" => refresh_token,
"expires_at" => expires_at,
"legacy" => legacy
}) do
site = conn.assigns.site
start_date = Plausible.Google.UA.API.get_analytics_start_date(access_token, view_id)
start_date = Plausible.Google.API.get_analytics_start_date(access_token, property_or_view)
case start_date do
{:ok, nil} ->
{:ok, view_ids} = Plausible.Google.UA.API.list_views(access_token)
{:ok, properties_and_views} =
if legacy == "true" do
Plausible.Google.UA.API.list_views(access_token)
else
Plausible.Google.API.list_properties_and_views(access_token)
end
conn
|> assign(:skip_plausible_tracking, true)
|> render("view_id_form.html",
|> render("property_or_view_form.html",
access_token: access_token,
refresh_token: refresh_token,
expires_at: expires_at,
site: site,
view_ids: view_ids,
selected_view_id_error: "No data found. Nothing to import",
properties_and_views: properties_and_views,
selected_property_or_view_error: "No data found. Nothing to import",
legacy: legacy,
layout: {PlausibleWeb.LayoutView, "focus.html"}
)
{:ok, date} ->
if Timex.before?(date, @google_analytics_new_user_metric_date) do
redirect(conn,
to:
Routes.universal_analytics_path(conn, :user_metric_notice, site.domain,
view_id: view_id,
access_token: access_token,
refresh_token: refresh_token,
expires_at: expires_at,
legacy: legacy
)
)
else
redirect(conn,
to:
Routes.universal_analytics_path(conn, :confirm, site.domain,
view_id: view_id,
access_token: access_token,
refresh_token: refresh_token,
expires_at: expires_at,
legacy: legacy
)
)
end
action =
if Timex.before?(date, @google_analytics_new_user_metric_date) do
:user_metric_notice
else
:confirm
end
redirect(conn,
to:
Routes.google_analytics_path(conn, action, site.domain,
property_or_view: property_or_view,
access_token: access_token,
refresh_token: refresh_token,
expires_at: expires_at,
legacy: legacy
)
)
end
end
def confirm(conn, %{
"view_id" => view_id,
"property_or_view" => property_or_view,
"access_token" => access_token,
"refresh_token" => refresh_token,
"expires_at" => expires_at,
@ -137,11 +146,12 @@ defmodule PlausibleWeb.UniversalAnalyticsController do
}) do
site = conn.assigns.site
start_date = Plausible.Google.UA.API.get_analytics_start_date(access_token, view_id)
start_date = Plausible.Google.API.get_analytics_start_date(access_token, property_or_view)
end_date = Plausible.Sites.native_stats_start_date(site) || Timex.today(site.timezone)
{:ok, {view_name, view_id}} = Plausible.Google.UA.API.get_view(access_token, view_id)
{:ok, %{name: property_or_view_name, id: property_or_view}} =
Plausible.Google.API.get_property_or_view(access_token, property_or_view)
conn
|> assign(:skip_plausible_tracking, true)
@ -150,17 +160,18 @@ defmodule PlausibleWeb.UniversalAnalyticsController do
refresh_token: refresh_token,
expires_at: expires_at,
site: site,
selected_view_id: view_id,
selected_view_id_name: view_name,
selected_property_or_view: property_or_view,
selected_property_or_view_name: property_or_view_name,
start_date: start_date,
end_date: end_date,
property?: Plausible.Google.API.property?(property_or_view),
legacy: legacy,
layout: {PlausibleWeb.LayoutView, "focus.html"}
)
end
def import(conn, %{
"view_id" => view_id,
"property_or_view" => property_or_view,
"start_date" => start_date,
"end_date" => end_date,
"access_token" => access_token,
@ -178,11 +189,23 @@ defmodule PlausibleWeb.UniversalAnalyticsController do
Routes.site_path(conn, :settings_imports_exports, site.domain)
end
{:ok, _} =
if Plausible.Google.API.property?(property_or_view) do
{:ok, _} =
Plausible.Imported.GoogleAnalytics4.new_import(
site,
current_user,
property: property_or_view,
start_date: start_date,
end_date: end_date,
access_token: access_token,
refresh_token: refresh_token,
token_expires_at: expires_at
)
else
Plausible.Imported.UniversalAnalytics.new_import(
site,
current_user,
view_id: view_id,
view_id: property_or_view,
start_date: start_date,
end_date: end_date,
access_token: access_token,
@ -190,6 +213,7 @@ defmodule PlausibleWeb.UniversalAnalyticsController do
token_expires_at: expires_at,
legacy: legacy == "true"
)
end
conn
|> put_flash(:success, "Import scheduled. An email will be sent when it completes.")

View File

@ -373,27 +373,20 @@ defmodule PlausibleWeb.Router do
delete "/:website", SiteController, :delete_site
delete "/:website/stats", SiteController, :reset_stats
get "/:website/import/google-analytics/view-id",
UniversalAnalyticsController,
:view_id_form
get "/:website/import/google-analytics/property-or-view",
GoogleAnalyticsController,
:property_or_view_form
post "/:website/import/google-analytics/view-id", UniversalAnalyticsController, :view_id
post "/:website/import/google-analytics/property-or-view",
GoogleAnalyticsController,
:property_or_view
get "/:website/import/google-analytics/user-metric",
UniversalAnalyticsController,
GoogleAnalyticsController,
:user_metric_notice
get "/:website/import/google-analytics/confirm", UniversalAnalyticsController, :confirm
post "/:website/settings/google-import", UniversalAnalyticsController, :import
get "/:website/import/google-analytics4/property",
GoogleAnalytics4Controller,
:property_form
post "/:website/import/google-analytics4/property", GoogleAnalytics4Controller, :property
get "/:website/import/google-analytics4/confirm", GoogleAnalytics4Controller, :confirm
post "/:website/settings/google4-import", GoogleAnalytics4Controller, :import
get "/:website/import/google-analytics/confirm", GoogleAnalyticsController, :confirm
post "/:website/settings/google-import", GoogleAnalyticsController, :import
delete "/:website/settings/forget-imported", SiteController, :forget_imported
delete "/:website/settings/forget-import/:import_id", SiteController, :forget_import

View File

@ -1,4 +1,4 @@
<%= form_for @conn, Routes.universal_analytics_path(@conn, :import, @site.domain), [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, :import, @site.domain), [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>
<%= hidden_input(f, :access_token, value: @access_token) %>
@ -9,15 +9,28 @@
<%= case @start_date do %>
<% {:ok, start_date} -> %>
<div class="mt-6 text-sm text-gray-500 dark:text-gray-200">
Stats from this property and time period will be imported from your Google Analytics account to your Plausible dashboard
Stats from this
<%= if @property? do %>
property
<% else %>
view
<% end %>
and time period will be imported from your Google Analytics account to your Plausible dashboard
</div>
<div class="mt-6">
<%= styled_label(f, :view_id, "Google Analytics view") %>
<%= styled_label(
f,
:property_or_view,
"Google Analytics #{if @property?, do: "property", else: "view"}"
) %>
<span class="block w-full text-base dark:text-gray-100 sm:text-sm dark:bg-gray-800">
<%= @selected_view_id_name %>
<%= @selected_property_or_view_name %>
</span>
<%= hidden_input(f, :view_id, readonly: "true", value: @selected_view_id) %>
<%= hidden_input(f, :property_or_view,
readonly: "true",
value: @selected_property_or_view
) %>
</div>
<div class="flex justify-between mt-3">
<div class="w-36">

View File

@ -0,0 +1,23 @@
<%= form_for @conn, Routes.google_analytics_path(@conn, :property_or_view, @site.domain), [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>
<%= hidden_input(f, :access_token, value: @access_token) %>
<%= hidden_input(f, :refresh_token, value: @refresh_token) %>
<%= hidden_input(f, :expires_at, value: @expires_at) %>
<%= hidden_input(f, :legacy, value: @legacy) %>
<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.
</div>
<div class="mt-3">
<%= styled_label(f, :property_or_view, "Google Analytics property or view") %>
<%= styled_select(f, :property_or_view, @properties_and_views,
prompt: "(Choose property or view)",
required: "true"
) %>
<%= styled_error(@conn.assigns[:selected_property_or_view_error]) %>
</div>
<%= submit("Continue ->", class: "button mt-6") %>
<% end %>

View File

@ -34,8 +34,8 @@
<%= link("Continue ->",
to:
Routes.universal_analytics_path(@conn, :confirm, @site.domain,
view_id: @view_id,
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,

View File

@ -1,46 +0,0 @@
<%= form_for @conn, Routes.google_analytics4_path(@conn, :import, @site.domain), [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>
<%= hidden_input(f, :access_token, value: @access_token) %>
<%= hidden_input(f, :refresh_token, value: @refresh_token) %>
<%= hidden_input(f, :expires_at, value: @expires_at) %>
<%= case @start_date do %>
<% {:ok, start_date} -> %>
<div class="mt-6 text-sm text-gray-500 dark:text-gray-200">
Stats from this property and time period will be imported from your Google Analytics 4 account to your Plausible dashboard
</div>
<div class="mt-6">
<%= styled_label(f, :property, "Google Analytics 4 property") %>
<span class="block w-full text-base dark:text-gray-100 sm:text-sm dark:bg-gray-800">
<%= @selected_property_name %>
</span>
<%= hidden_input(f, :property, readonly: "true", value: @selected_property) %>
</div>
<div class="flex justify-between mt-3">
<div class="w-36">
<%= styled_label(f, :start_date, "From") %>
<span class="block w-full text-base dark:text-gray-100 sm:text-sm dark:bg-gray-800">
<%= PlausibleWeb.EmailView.date_format(start_date) %>
</span>
<%= hidden_input(f, :start_date, value: start_date, readonly: "true") %>
</div>
<div class="align-middle pt-4 dark:text-gray-100">&rarr;</div>
<div class="w-36">
<%= styled_label(f, :end_date, "To") %>
<span class="block w-full text-base dark:text-gray-100 sm:text-sm dark:bg-gray-800">
<%= PlausibleWeb.EmailView.date_format(@end_date) %>
</span>
<%= hidden_input(f, :end_date, value: @end_date, readonly: "true") %>
</div>
</div>
<% {:error, error} -> %>
<p class="text-gray-700 dark:text-gray-300 mt-6">
The following error occurred when fetching your Google Analytics 4 data.
</p>
<p class="text-red-700 font-medium mt-3"><%= error %></p>
<% end %>
<%= submit("Confirm import", class: "button mt-6") %>
<% end %>

View File

@ -1,19 +0,0 @@
<%= form_for @conn, Routes.google_analytics4_path(@conn, :property, @site.domain), [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 4</h2>
<%= hidden_input(f, :access_token, value: @access_token) %>
<%= hidden_input(f, :refresh_token, value: @refresh_token) %>
<%= hidden_input(f, :expires_at, value: @expires_at) %>
<div class="mt-6 text-sm text-gray-500 dark:text-gray-200">
Choose the property in your Google Analytics 4 account that will be imported to the <%= @site.domain %> dashboard.
</div>
<div class="mt-3">
<%= styled_label(f, :property, "Google Analytics 4 property") %>
<%= styled_select(f, :property, @properties, prompt: "(Choose property)", required: "true") %>
<%= styled_error(@conn.assigns[:selected_property_error]) %>
</div>
<%= submit("Continue ->", class: "button mt-6") %>
<% end %>

View File

@ -16,21 +16,7 @@
theme="bright"
href={Plausible.Google.API.import_authorize_url(@site.id, "import", legacy: false)}
>
<img src="/images/icon/universal_analytics_logo.svg" alt="New Universal Analytics import" />
</PlausibleWeb.Components.Generic.button_link>
<PlausibleWeb.Components.Generic.button_link
class="w-36 h-20"
theme="bright"
href={
Plausible.Google.API.import_authorize_url(@site.id, "import", legacy: false, ga4: true)
}
>
<img
src="/images/icon/google_analytics_4_logo.svg"
width="110"
alt="New Google Analytics 4 import"
/>
<img src="/images/icon/google_analytics_logo.svg" alt="Google Analytics import" />
</PlausibleWeb.Components.Generic.button_link>
<PlausibleWeb.Components.Generic.button_link

View File

@ -1,20 +0,0 @@
<%= form_for @conn, Routes.universal_analytics_path(@conn, :view_id, @site.domain), [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>
<%= hidden_input(f, :access_token, value: @access_token) %>
<%= hidden_input(f, :refresh_token, value: @refresh_token) %>
<%= hidden_input(f, :expires_at, value: @expires_at) %>
<%= hidden_input(f, :legacy, value: @legacy) %>
<div class="mt-6 text-sm text-gray-500 dark:text-gray-200">
Choose the view in your Google Analytics account that will be imported to the <%= @site.domain %> dashboard.
</div>
<div class="mt-3">
<%= styled_label(f, :view_id, "Google Analytics view") %>
<%= styled_select(f, :view_id, @view_ids, prompt: "(Choose view)", required: "true") %>
<%= styled_error(@conn.assigns[:selected_view_id_error]) %>
</div>
<%= submit("Continue ->", class: "button mt-6") %>
<% end %>

View File

@ -1,4 +0,0 @@
defmodule PlausibleWeb.GoogleAnalytics4View do
use PlausibleWeb, :view
use Plausible
end

View File

@ -0,0 +1,4 @@
defmodule PlausibleWeb.GoogleAnalyticsView do
use PlausibleWeb, :view
use Plausible
end

View File

@ -1,4 +0,0 @@
defmodule PlausibleWeb.UniversalAnalyticsView do
use PlausibleWeb, :view
use Plausible
end

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.0 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -390,10 +390,10 @@ defmodule Plausible.Google.APITest do
)
assert {:ok,
%{
"one.test" => [{"57238190 - one.test", "57238190"}],
"two.test" => [{"54460083 - two.test", "54460083"}]
}} == Google.UA.API.list_views("access_token")
[
{"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

View File

@ -31,14 +31,15 @@ defmodule Plausible.Google.GA4.APITest do
describe "get_property/2" do
test "returns tuple consisting of display name and value of a property" do
result = Jason.decode!(File.read!("fixture/ga4_list_properties.json"))
result = Jason.decode!(File.read!("fixture/ga4_get_property.json"))
expect(Plausible.HTTPClient.Mock, :get, fn _url, _opts ->
{:ok, %Finch.Response{status: 200, body: result}}
end)
assert {:ok, {"GA4 - Flood-It! (properties/153293282)", "properties/153293282"}} =
GA4.API.get_property("some_access_token", "properties/153293282")
assert {:ok,
%{name: "account.one - GA4 (properties/428685444)", id: "properties/428685444"}} =
GA4.API.get_property("some_access_token", "properties/428685444")
end
end

View File

@ -0,0 +1,336 @@
defmodule PlausibleWeb.GoogleAnalyticsControllerTest do
use PlausibleWeb.ConnCase, async: false
use Oban.Testing, repo: Plausible.Repo
import Mox
import Plausible.Test.Support.HTML
alias Plausible.Imported.SiteImport
require Plausible.Imported.SiteImport
setup :verify_on_exit!
describe "GET /:website/import/google-analytics/user-metric" do
setup [:create_user, :log_in, :create_new_site]
test "renders with link to confirmation page", %{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",
"legacy" => "true"
})
|> 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",
legacy: "true"
)
|> String.replace("&", "&amp;")
end
end
describe "GET /:website/import/google-analytics/property-or-view" do
setup [:create_user, :log_in, :create_new_site]
test "lists Google Analytics views (legacy)", %{conn: conn, site: site} do
expect(
Plausible.HTTPClient.Mock,
:get,
fn _url, _opts ->
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/property-or-view", %{
"access_token" => "token",
"refresh_token" => "foo",
"expires_at" => "2022-09-22T20:01:37.112777",
"legacy" => "true"
})
|> html_response(200)
assert response =~ "57238190 - one.test"
assert response =~ "54460083 - two.test"
end
test "lists Google Analytics views and properties", %{conn: conn, site: site} do
expect(
Plausible.HTTPClient.Mock,
:get,
fn _url, _opts ->
body = "fixture/ga4_list_properties.json" |> File.read!() |> Jason.decode!()
{:ok, %Finch.Response{body: body, status: 200}}
end
)
expect(
Plausible.HTTPClient.Mock,
:get,
fn _url, _opts ->
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/property-or-view", %{
"access_token" => "token",
"refresh_token" => "foo",
"expires_at" => "2022-09-22T20:01:37.112777",
"legacy" => "false"
})
|> html_response(200)
assert response =~ "57238190 - one.test"
assert response =~ "54460083 - two.test"
assert response =~ "account.one - GA4 (properties/428685906)"
assert response =~ "GA4 - Flood-It! (properties/153293282)"
assert response =~ "GA4 - Google Merch Shop (properties/213025502)"
end
end
describe "GET /:website/import/google-analytics/confirm" do
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,
:post,
fn _url, _headers, _params ->
body = "fixture/ga_start_date.json" |> File.read!() |> Jason.decode!()
{:ok, %Finch.Response{body: body, status: 200}}
end
)
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",
"legacy" => "true"
})
|> 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=legacy]|, "value") == "true"
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") ==
Date.to_iso8601(Date.utc_today())
end
test "renders confirmation form for Google Analytics 4 import", %{conn: conn, site: site} do
expect(
Plausible.HTTPClient.Mock,
:post,
fn _url, _headers, _params ->
body = "fixture/ga4_start_date.json" |> File.read!() |> Jason.decode!()
{:ok, %Finch.Response{body: body, status: 200}}
end
)
expect(
Plausible.HTTPClient.Mock,
:get,
fn _url, _headers ->
body = "fixture/ga4_get_property.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" => "properties/428685444",
"access_token" => "token",
"refresh_token" => "foo",
"expires_at" => "2022-09-22T20:01:37.112777",
"legacy" => "true"
})
|> 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=legacy]|, "value") == "true"
assert text_of_attr(response, ~s|input[name=property_or_view]|, "value") ==
"properties/428685444"
assert text_of_attr(response, ~s|input[name=start_date]|, "value") == "2024-02-22"
assert text_of_attr(response, ~s|input[name=end_date]|, "value") ==
Date.to_iso8601(Date.utc_today())
end
end
describe "POST /:website/settings/google-import" do
setup [:create_user, :log_in, :create_new_site]
test "creates Google Analytics 4 site import instance", %{conn: conn, site: site} do
conn =
post(conn, "/#{site.domain}/settings/google-import", %{
"property_or_view" => "properties/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",
"legacy" => "false"
})
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 == :google_analytics_4
assert site_import.end_date == ~D[2022-03-01]
assert site_import.status == SiteImport.pending()
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",
"legacy" => "true"
})
assert redirected_to(conn, 302) ==
PlausibleWeb.Router.Helpers.site_path(conn, :settings_integrations, 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 with legacy set to false", %{
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",
"legacy" => "false"
})
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
post(conn, "/#{site.domain}/settings/google-import", %{
"property_or_view" => "properties/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",
"legacy" => "false"
})
assert [%{id: import_id, legacy: false}] = Plausible.Imported.list_all_imports(site)
assert_enqueued(
worker: Plausible.Workers.ImportAnalytics,
args: %{
"import_id" => import_id,
"property" => "properties/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 "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",
"legacy" => "true"
})
assert [%{id: import_id, legacy: true}] = 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
end
end

View File

@ -1274,83 +1274,6 @@ defmodule PlausibleWeb.SiteControllerTest do
end
end
describe "GET /:website/import/google-analytics/view-id" do
setup [:create_user, :log_in, :create_new_site]
test "lists Google Analytics views", %{conn: conn, site: site} do
expect(
Plausible.HTTPClient.Mock,
:get,
fn _url, _body ->
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/view-id", %{
"access_token" => "token",
"refresh_token" => "foo",
"expires_at" => "2022-09-22T20:01:37.112777",
"legacy" => "true"
})
|> html_response(200)
assert response =~ "57238190 - one.test"
assert response =~ "54460083 - two.test"
end
end
describe "POST /:website/settings/google-import" do
setup [:create_user, :log_in, :create_new_site]
test "creates site import instance", %{conn: conn, site: site} do
post(conn, "/#{site.domain}/settings/google-import", %{
"view_id" => "123",
"start_date" => "2018-03-01",
"end_date" => "2022-03-01",
"access_token" => "token",
"refresh_token" => "foo",
"expires_at" => "2022-09-22T20:01:37.112777",
"legacy" => "true"
})
[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 an import job in Oban", %{conn: conn, site: site} do
post(conn, "/#{site.domain}/settings/google-import", %{
"view_id" => "123",
"start_date" => "2018-03-01",
"end_date" => "2022-03-01",
"access_token" => "token",
"refresh_token" => "foo",
"expires_at" => "2022-09-22T20:01:37.112777",
"legacy" => "true"
})
assert [%{id: import_id, legacy: true}] = Plausible.Imported.list_all_imports(site)
assert_enqueued(
worker: Plausible.Workers.ImportAnalytics,
args: %{
"import_id" => import_id,
"view_id" => "123",
"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
end
describe "DELETE /:website/settings/:forget_import/:import_id" do
setup [:create_user, :log_in, :create_new_site, :add_imported_data]