Harden the Google Analytics account fetch

Three defects, all found reviewing the rebased branch.

The connection probe hit /v1alpha/accounts, which any analytics.readonly
grant can call, while the driver's first request is the account's
accessBindings — that additionally needs Administrator on the account
and the manage.users.readonly scope. An Editor connecting, or a user
declining the second scope on Google's granular consent screen, probed
green and then 403'd on every campaign fetch, leaving the source
permanently "Connected" with no rows. The probe now targets the same
accessBindings collection the driver reads.

A single unreadable property aborted the whole account. A property the
token cannot see, or one deleted between the list and the read, threw
away every binding already collected; 49 of 50 readable properties are
still worth reviewing, so 403 and 404 now skip that property. Anything
else still fails the fetch.

Fan-out errors named no resource: the account call, the property list
and each per-property call all returned the same "unexpected status"
string, so a 403 on one subproperty out of forty was unattributable.
Errors now carry the account or property ID.

The cassette gains a subproperty parented to another property (only
reachable through the ancestor filter, so it pins the hierarchy walk
that the filter change claimed) and a property returning 403.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
This commit is contained in:
Aurélien Sibiril
2026-07-25 09:00:13 +02:00
parent 20502fc0be
commit 728011c042
6 changed files with 150 additions and 25 deletions

View File

@@ -50,7 +50,12 @@ func googleAnalyticsRegistration() *Registration {
"https://www.googleapis.com/auth/analytics.readonly",
"https://www.googleapis.com/auth/analytics.manage.users.readonly",
},
ProbeURL: "https://analyticsadmin.googleapis.com/v1alpha/accounts?pageSize=1",
// BuildProbeURL targets the selected account's accessBindings rather
// than the accounts list: listing accounts only needs
// analytics.readonly, so a non-Administrator connection (or one where
// the user declined manage.users.readonly on Google's granular consent
// screen) would probe green and then 403 on every fetch.
BuildProbeURL: buildGoogleAnalyticsProbeURL,
NewDriver: func(_ context.Context, c *http.Client, conn *coredata.Connector, _ *log.Logger) (drivers.Driver, error) {
s, err := coredata.ConnectorSettings[coredata.GoogleAnalyticsConnectorSettings](conn)
if err != nil {

View File

@@ -642,6 +642,22 @@ func buildSegmentProbeURL(conn *coredata.Connector) (string, error) {
return u.String(), nil
}
// buildGoogleAnalyticsProbeURL targets the selected account's accessBindings,
// the driver's first call, so the probe fails for a connection that can list
// accounts but cannot read access bindings.
func buildGoogleAnalyticsProbeURL(conn *coredata.Connector) (string, error) {
s, err := coredata.ConnectorSettings[coredata.GoogleAnalyticsConnectorSettings](conn)
if err != nil {
return "", fmt.Errorf("cannot read google analytics connector settings: %w", err)
}
if s.AccountID == "" {
return "", fmt.Errorf("missing google analytics account ID")
}
return drivers.GoogleAnalyticsAccountBindingsProbeURL(s.AccountID)
}
// probeSquare checks a Square credential (OAuth Bearer token or Personal Access
// Token) with a GET /v2/merchants/me, sending the required Square-Version
// header. The endpoint returns 401 on a dead token and works for both OAuth and