Echo: Add config option to prevent duplicate page views for GA4 (#57619)

This commit is contained in:
Timur Olzhabayev 2022-11-09 15:09:19 +01:00 committed by GitHub
parent 463f993186
commit 008c554d7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 77 additions and 56 deletions

View File

@ -230,6 +230,9 @@ google_analytics_ua_id =
# Google Analytics 4 tracking code, only enabled if you specify an id here # Google Analytics 4 tracking code, only enabled if you specify an id here
google_analytics_4_id = google_analytics_4_id =
# When Google Analytics 4 Enhanced event measurement is enabled, we will try to avoid sending duplicate events and let Google Analytics 4 detect navigation changes, etc.
google_analytics_4_send_manual_page_views = false
# Google Tag Manager ID, only enabled if you specify an id here # Google Tag Manager ID, only enabled if you specify an id here
google_tag_manager_id = google_tag_manager_id =

View File

@ -237,6 +237,9 @@
# Google Analytics 4 tracking code, only enabled if you specify an id here # Google Analytics 4 tracking code, only enabled if you specify an id here
;google_analytics_4_id = ;google_analytics_4_id =
# When Google Analytics 4 Enhanced event measurement is enabled, we will try to avoid sending duplicate events and let Google Analytics 4 detect navigation changes, etc.
;google_analytics_4_send_manual_page_views = false
# Google Tag Manager ID, only enabled if you specify an id here # Google Tag Manager ID, only enabled if you specify an id here
;google_tag_manager_id = ;google_tag_manager_id =

View File

@ -211,6 +211,7 @@ export interface GrafanaConfig {
secretsManagerPluginEnabled: boolean; secretsManagerPluginEnabled: boolean;
googleAnalyticsId: string | undefined; googleAnalyticsId: string | undefined;
googleAnalytics4Id: string | undefined; googleAnalytics4Id: string | undefined;
googleAnalytics4SendManualPageViews: boolean;
rudderstackWriteKey: string | undefined; rudderstackWriteKey: string | undefined;
rudderstackDataPlaneUrl: string | undefined; rudderstackDataPlaneUrl: string | undefined;
rudderstackSdkUrl: string | undefined; rudderstackSdkUrl: string | undefined;

View File

@ -136,6 +136,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
}; };
googleAnalyticsId: undefined; googleAnalyticsId: undefined;
googleAnalytics4Id: undefined; googleAnalytics4Id: undefined;
googleAnalytics4SendManualPageViews = false;
rudderstackWriteKey: undefined; rudderstackWriteKey: undefined;
rudderstackDataPlaneUrl: undefined; rudderstackDataPlaneUrl: undefined;
rudderstackSdkUrl: undefined; rudderstackSdkUrl: undefined;

View File

@ -8,27 +8,28 @@ import (
) )
type IndexViewData struct { type IndexViewData struct {
User *CurrentUser User *CurrentUser
Settings map[string]interface{} Settings map[string]interface{}
AppUrl string AppUrl string
AppSubUrl string AppSubUrl string
GoogleAnalyticsId string GoogleAnalyticsId string
GoogleAnalytics4Id string GoogleAnalytics4Id string
GoogleTagManagerId string GoogleAnalytics4SendManualPageViews bool
NavTree *navtree.NavTreeRoot GoogleTagManagerId string
BuildVersion string NavTree *navtree.NavTreeRoot
BuildCommit string BuildVersion string
Theme string BuildCommit string
NewGrafanaVersionExists bool Theme string
NewGrafanaVersion string NewGrafanaVersionExists bool
AppName string NewGrafanaVersion string
AppNameBodyClass string AppName string
FavIcon template.URL AppNameBodyClass string
AppleTouchIcon template.URL FavIcon template.URL
AppTitle string AppleTouchIcon template.URL
Sentry *setting.Sentry AppTitle string
ContentDeliveryURL string Sentry *setting.Sentry
LoadingLogo template.URL ContentDeliveryURL string
LoadingLogo template.URL
// Nonce is a cryptographic identifier for use with Content Security Policy. // Nonce is a cryptographic identifier for use with Content Security Policy.
Nonce string Nonce string
} }

View File

@ -124,6 +124,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i
"queryHistoryEnabled": hs.Cfg.QueryHistoryEnabled, "queryHistoryEnabled": hs.Cfg.QueryHistoryEnabled,
"googleAnalyticsId": setting.GoogleAnalyticsId, "googleAnalyticsId": setting.GoogleAnalyticsId,
"googleAnalytics4Id": setting.GoogleAnalytics4Id, "googleAnalytics4Id": setting.GoogleAnalytics4Id,
"GoogleAnalytics4SendManualPageViews": setting.GoogleAnalytics4SendManualPageViews,
"rudderstackWriteKey": setting.RudderstackWriteKey, "rudderstackWriteKey": setting.RudderstackWriteKey,
"rudderstackDataPlaneUrl": setting.RudderstackDataPlaneUrl, "rudderstackDataPlaneUrl": setting.RudderstackDataPlaneUrl,
"rudderstackSdkUrl": setting.RudderstackSdkUrl, "rudderstackSdkUrl": setting.RudderstackSdkUrl,

View File

@ -98,27 +98,28 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat
HelpFlags1: c.HelpFlags1, HelpFlags1: c.HelpFlags1,
HasEditPermissionInFolders: hasEditPerm, HasEditPermissionInFolders: hasEditPerm,
}, },
Settings: settings, Settings: settings,
Theme: prefs.Theme, Theme: prefs.Theme,
AppUrl: appURL, AppUrl: appURL,
AppSubUrl: appSubURL, AppSubUrl: appSubURL,
GoogleAnalyticsId: setting.GoogleAnalyticsId, GoogleAnalyticsId: setting.GoogleAnalyticsId,
GoogleAnalytics4Id: setting.GoogleAnalytics4Id, GoogleAnalytics4Id: setting.GoogleAnalytics4Id,
GoogleTagManagerId: setting.GoogleTagManagerId, GoogleAnalytics4SendManualPageViews: setting.GoogleAnalytics4SendManualPageViews,
BuildVersion: setting.BuildVersion, GoogleTagManagerId: setting.GoogleTagManagerId,
BuildCommit: setting.BuildCommit, BuildVersion: setting.BuildVersion,
NewGrafanaVersion: hs.grafanaUpdateChecker.LatestVersion(), BuildCommit: setting.BuildCommit,
NewGrafanaVersionExists: hs.grafanaUpdateChecker.UpdateAvailable(), NewGrafanaVersion: hs.grafanaUpdateChecker.LatestVersion(),
AppName: setting.ApplicationName, NewGrafanaVersionExists: hs.grafanaUpdateChecker.UpdateAvailable(),
AppNameBodyClass: "app-grafana", AppName: setting.ApplicationName,
FavIcon: "public/img/fav32.png", AppNameBodyClass: "app-grafana",
AppleTouchIcon: "public/img/apple-touch-icon.png", FavIcon: "public/img/fav32.png",
AppTitle: "Grafana", AppleTouchIcon: "public/img/apple-touch-icon.png",
NavTree: navTree, AppTitle: "Grafana",
Sentry: &hs.Cfg.Sentry, NavTree: navTree,
Nonce: c.RequestNonce, Sentry: &hs.Cfg.Sentry,
ContentDeliveryURL: hs.Cfg.GetContentDeliveryURL(hs.License.ContentDeliveryPrefix()), Nonce: c.RequestNonce,
LoadingLogo: "public/img/grafana_icon.svg", ContentDeliveryURL: hs.Cfg.GetContentDeliveryURL(hs.License.ContentDeliveryPrefix()),
LoadingLogo: "public/img/grafana_icon.svg",
} }
if !hs.AccessControl.IsDisabled() { if !hs.AccessControl.IsDisabled() {

View File

@ -137,13 +137,14 @@ var (
appliedEnvOverrides []string appliedEnvOverrides []string
// analytics // analytics
GoogleAnalyticsId string GoogleAnalyticsId string
GoogleAnalytics4Id string GoogleAnalytics4Id string
GoogleTagManagerId string GoogleAnalytics4SendManualPageViews bool
RudderstackDataPlaneUrl string GoogleTagManagerId string
RudderstackWriteKey string RudderstackDataPlaneUrl string
RudderstackSdkUrl string RudderstackWriteKey string
RudderstackConfigUrl string RudderstackSdkUrl string
RudderstackConfigUrl string
// LDAP // LDAP
LDAPEnabled bool LDAPEnabled bool
@ -995,6 +996,8 @@ func (cfg *Cfg) Load(args CommandLineArgs) error {
cfg.CheckForPluginUpdates = analytics.Key("check_for_plugin_updates").MustBool(true) cfg.CheckForPluginUpdates = analytics.Key("check_for_plugin_updates").MustBool(true)
GoogleAnalyticsId = analytics.Key("google_analytics_ua_id").String() GoogleAnalyticsId = analytics.Key("google_analytics_ua_id").String()
GoogleAnalytics4Id = analytics.Key("google_analytics_4_id").String() GoogleAnalytics4Id = analytics.Key("google_analytics_4_id").String()
GoogleAnalytics4SendManualPageViews = analytics.Key("google_analytics_4_send_manual_page_views").MustBool(false)
GoogleTagManagerId = analytics.Key("google_tag_manager_id").String() GoogleTagManagerId = analytics.Key("google_tag_manager_id").String()
RudderstackWriteKey = analytics.Key("rudderstack_write_key").String() RudderstackWriteKey = analytics.Key("rudderstack_write_key").String()
RudderstackDataPlaneUrl = analytics.Key("rudderstack_data_plane_url").String() RudderstackDataPlaneUrl = analytics.Key("rudderstack_data_plane_url").String()

View File

@ -268,6 +268,7 @@ function initEchoSrv() {
registerEchoBackend( registerEchoBackend(
new GA4EchoBackend({ new GA4EchoBackend({
googleAnalyticsId: config.googleAnalytics4Id, googleAnalyticsId: config.googleAnalytics4Id,
googleAnalytics4SendManualPageViews: config.googleAnalytics4SendManualPageViews,
}) })
); );
} }

View File

@ -11,16 +11,17 @@ declare global {
export interface GA4EchoBackendOptions { export interface GA4EchoBackendOptions {
googleAnalyticsId: string; googleAnalyticsId: string;
googleAnalytics4SendManualPageViews: boolean;
user?: CurrentUserDTO; user?: CurrentUserDTO;
} }
export class GA4EchoBackend implements EchoBackend<PageviewEchoEvent, GA4EchoBackendOptions> { export class GA4EchoBackend implements EchoBackend<PageviewEchoEvent, GA4EchoBackendOptions> {
supportedEvents = [EchoEventType.Pageview]; supportedEvents = [EchoEventType.Pageview];
trackedUserId: number | null = null; googleAnalytics4SendManualPageViews = false;
constructor(public options: GA4EchoBackendOptions) { constructor(public options: GA4EchoBackendOptions) {
const url = `https://www.googletagmanager.com/gtag/js?id=${options.googleAnalyticsId}`; const url = `https://www.googletagmanager.com/gtag/js?id=${options.googleAnalyticsId}`;
loadScript(url); loadScript(url, true);
window.dataLayer = window.dataLayer || []; window.dataLayer = window.dataLayer || [];
window.gtag = function gtag() { window.gtag = function gtag() {
@ -28,12 +29,15 @@ export class GA4EchoBackend implements EchoBackend<PageviewEchoEvent, GA4EchoBac
}; };
window.gtag('js', new Date()); window.gtag('js', new Date());
const configOptions: Gtag.CustomParams = {}; const configOptions: Gtag.CustomParams = {
page_path: window.location.pathname,
};
if (options.user) { if (options.user) {
configOptions.user_id = getUserIdentifier(options.user); configOptions.user_id = getUserIdentifier(options.user);
} }
this.googleAnalytics4SendManualPageViews = options.googleAnalytics4SendManualPageViews;
window.gtag('config', options.googleAnalyticsId, configOptions); window.gtag('config', options.googleAnalyticsId, configOptions);
} }
@ -41,9 +45,10 @@ export class GA4EchoBackend implements EchoBackend<PageviewEchoEvent, GA4EchoBac
if (!window.gtag) { if (!window.gtag) {
return; return;
} }
// this should prevent duplicate events in case enhanced tracking is enabled
window.gtag('set', 'page_path', e.payload.page); if (this.googleAnalytics4SendManualPageViews) {
window.gtag('event', 'page_view'); window.gtag('event', 'page_view', { page_path: e.payload.page });
}
}; };
// Not using Echo buffering, addEvent above sends events to GA as soon as they appear // Not using Echo buffering, addEvent above sends events to GA as soon as they appear

View File

@ -14,11 +14,12 @@ export function getUserIdentifier(user: CurrentUserDTO) {
return user.email; return user.email;
} }
export function loadScript(url: string) { export function loadScript(url: string, async = false) {
return new Promise((resolve) => { return new Promise((resolve) => {
const script = document.createElement('script'); const script = document.createElement('script');
script.onload = resolve; script.onload = resolve;
script.src = url; script.src = url;
script.async = async;
document.head.appendChild(script); document.head.appendChild(script);
}); });
} }