diff --git a/conf/defaults.ini b/conf/defaults.ini index 4409523309f..08c6f70c192 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -210,7 +210,10 @@ check_for_updates = true check_for_plugin_updates = true # Google Analytics universal tracking code, only enabled if you specify an id here -google_analytics_ua_id = +google_analytics_ua_id = + +# Google Analytics 4 tracking code, only enabled if you specify an id here +google_analytics_4_id = # Google Tag Manager ID, only enabled if you specify an id here google_tag_manager_id = diff --git a/conf/sample.ini b/conf/sample.ini index 4bf3e78c300..67cd5c164a5 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -218,6 +218,9 @@ # Google Analytics universal tracking code, only enabled if you specify an id here ;google_analytics_ua_id = +# Google Analytics 4 tracking code, only enabled if you specify an id here +;google_analytics_4_id = + # Google Tag Manager ID, only enabled if you specify an id here ;google_tag_manager_id = diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index d1a7e7850eb..59a9a7c32ae 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -474,6 +474,10 @@ Set to false disables checking for new versions of installed plugins from https: If you want to track Grafana usage via Google analytics specify _your_ Universal Analytics ID here. By default this feature is disabled. +### google_analytics_4_id + +If you want to track Grafana usage via Google Analytics 4 specify _your_ GA4 ID here. By default this feature is disabled. + ### google_tag_manager_id Google Tag Manager ID, only enabled if you enter an ID here. diff --git a/package.json b/package.json index ca649b4375c..1f67e84a0bc 100644 --- a/package.json +++ b/package.json @@ -124,6 +124,7 @@ "@types/eslint": "8.4.5", "@types/file-saver": "2.0.5", "@types/google.analytics": "^0.0.42", + "@types/gtag.js": "^0.0.11", "@types/history": "4.7.11", "@types/hoist-non-react-statics": "3.3.1", "@types/jest": "28.1.6", diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts index a8b50fbbec9..2f0b426d6b0 100644 --- a/packages/grafana-data/src/types/config.ts +++ b/packages/grafana-data/src/types/config.ts @@ -210,6 +210,7 @@ export interface GrafanaConfig { feedbackLinksEnabled: boolean; secretsManagerPluginEnabled: boolean; googleAnalyticsId: string | undefined; + googleAnalytics4Id: string | undefined; rudderstackWriteKey: string | undefined; rudderstackDataPlaneUrl: string | undefined; rudderstackSdkUrl: string | undefined; diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index b0b84b51e32..e8ab648f636 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -134,6 +134,7 @@ export class GrafanaBootConfig implements GrafanaConfig { enabled: true, }; googleAnalyticsId: undefined; + googleAnalytics4Id: undefined; rudderstackWriteKey: undefined; rudderstackDataPlaneUrl: undefined; rudderstackSdkUrl: undefined; diff --git a/pkg/api/dtos/index.go b/pkg/api/dtos/index.go index a6b44c9f843..9ae6797231c 100644 --- a/pkg/api/dtos/index.go +++ b/pkg/api/dtos/index.go @@ -12,6 +12,7 @@ type IndexViewData struct { AppUrl string AppSubUrl string GoogleAnalyticsId string + GoogleAnalytics4Id string GoogleTagManagerId string NavTree []*NavLink BuildVersion string diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 05062a600be..4410963c6e1 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -117,6 +117,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i "profileEnabled": setting.ProfileEnabled, "queryHistoryEnabled": hs.Cfg.QueryHistoryEnabled, "googleAnalyticsId": setting.GoogleAnalyticsId, + "googleAnalytics4Id": setting.GoogleAnalytics4Id, "rudderstackWriteKey": setting.RudderstackWriteKey, "rudderstackDataPlaneUrl": setting.RudderstackDataPlaneUrl, "rudderstackSdkUrl": setting.RudderstackSdkUrl, diff --git a/pkg/api/index.go b/pkg/api/index.go index 97f8b7d7240..a4069de409b 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -840,6 +840,7 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat AppUrl: appURL, AppSubUrl: appSubURL, GoogleAnalyticsId: setting.GoogleAnalyticsId, + GoogleAnalytics4Id: setting.GoogleAnalytics4Id, GoogleTagManagerId: setting.GoogleTagManagerId, BuildVersion: setting.BuildVersion, BuildCommit: setting.BuildCommit, diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 10e8fba80d4..4c5f064033e 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -136,6 +136,7 @@ var ( // analytics GoogleAnalyticsId string + GoogleAnalytics4Id string GoogleTagManagerId string RudderstackDataPlaneUrl string RudderstackWriteKey string @@ -956,6 +957,7 @@ func (cfg *Cfg) Load(args CommandLineArgs) error { cfg.CheckForGrafanaUpdates = analytics.Key("check_for_updates").MustBool(true) cfg.CheckForPluginUpdates = analytics.Key("check_for_plugin_updates").MustBool(true) GoogleAnalyticsId = analytics.Key("google_analytics_ua_id").String() + GoogleAnalytics4Id = analytics.Key("google_analytics_4_id").String() GoogleTagManagerId = analytics.Key("google_tag_manager_id").String() RudderstackWriteKey = analytics.Key("rudderstack_write_key").String() RudderstackDataPlaneUrl = analytics.Key("rudderstack_data_plane_url").String() diff --git a/public/app/app.ts b/public/app/app.ts index e018e7ddf54..941d11d0417 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -55,6 +55,7 @@ import { Echo } from './core/services/echo/Echo'; import { reportPerformance } from './core/services/echo/EchoSrv'; import { PerformanceBackend } from './core/services/echo/backends/PerformanceBackend'; import { ApplicationInsightsBackend } from './core/services/echo/backends/analytics/ApplicationInsightsBackend'; +import { GA4EchoBackend } from './core/services/echo/backends/analytics/GA4Backend'; import { GAEchoBackend } from './core/services/echo/backends/analytics/GABackend'; import { RudderstackBackend } from './core/services/echo/backends/analytics/RudderstackBackend'; import { GrafanaJavascriptAgentBackend } from './core/services/echo/backends/grafana-javascript-agent/GrafanaJavascriptAgentBackend'; @@ -256,6 +257,14 @@ function initEchoSrv() { ); } + if (config.googleAnalytics4Id) { + registerEchoBackend( + new GA4EchoBackend({ + googleAnalyticsId: config.googleAnalytics4Id, + }) + ); + } + if (config.rudderstackWriteKey && config.rudderstackDataPlaneUrl) { registerEchoBackend( new RudderstackBackend({ diff --git a/public/app/core/services/echo/backends/analytics/GA4Backend.ts b/public/app/core/services/echo/backends/analytics/GA4Backend.ts new file mode 100644 index 00000000000..15e118b786d --- /dev/null +++ b/public/app/core/services/echo/backends/analytics/GA4Backend.ts @@ -0,0 +1,58 @@ +import $ from 'jquery'; + +import { CurrentUserDTO } from '@grafana/data'; +import { EchoBackend, EchoEventType, PageviewEchoEvent } from '@grafana/runtime'; + +import { getUserIdentifier } from '../../utils'; + +declare global { + interface Window { + dataLayer: unknown[]; + } +} + +export interface GA4EchoBackendOptions { + googleAnalyticsId: string; + user?: CurrentUserDTO; +} + +export class GA4EchoBackend implements EchoBackend { + supportedEvents = [EchoEventType.Pageview]; + trackedUserId: number | null = null; + + constructor(public options: GA4EchoBackendOptions) { + const url = `https://www.googletagmanager.com/gtag/js?id=${options.googleAnalyticsId}`; + + $.ajax({ + url, + dataType: 'script', + cache: true, + }); + + window.dataLayer = window.dataLayer || []; + window.gtag = function gtag() { + window.dataLayer.push(arguments); + }; + window.gtag('js', new Date()); + + const configOptions: Gtag.CustomParams = {}; + + if (options.user) { + configOptions.user_id = getUserIdentifier(options.user); + } + + window.gtag('config', options.googleAnalyticsId, configOptions); + } + + addEvent = (e: PageviewEchoEvent) => { + if (!window.gtag) { + return; + } + + window.gtag('set', 'page_path', e.payload.page); + window.gtag('event', 'page_view'); + }; + + // Not using Echo buffering, addEvent above sends events to GA as soon as they appear + flush = () => {}; +} diff --git a/yarn.lock b/yarn.lock index c7f24a81e8c..a08e0dd3c53 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11570,6 +11570,13 @@ __metadata: languageName: node linkType: hard +"@types/gtag.js@npm:^0.0.11": + version: 0.0.11 + resolution: "@types/gtag.js@npm:0.0.11" + checksum: fe3f0550f609a903fd23b2b7eae2771ad6c9f314c7458320759e8e328b2c148b7d3bff0462d05e3ffe6e800dbd4dfacf174cbeb29796b31813a8bb2fbecca1cc + languageName: node + linkType: hard + "@types/hast@npm:^2.0.0": version: 2.3.4 resolution: "@types/hast@npm:2.3.4" @@ -22224,6 +22231,7 @@ __metadata: "@types/eslint": 8.4.5 "@types/file-saver": 2.0.5 "@types/google.analytics": ^0.0.42 + "@types/gtag.js": ^0.0.11 "@types/history": 4.7.11 "@types/hoist-non-react-statics": 3.3.1 "@types/jest": 28.1.6