diff --git a/packages/jaeger-ui/package.json b/packages/jaeger-ui/package.json index 647aab2aeb..3d5f4564ef 100644 --- a/packages/jaeger-ui/package.json +++ b/packages/jaeger-ui/package.json @@ -74,7 +74,6 @@ "react": "^18.2.0", "react-circular-progressbar": "^2.1.0", "react-dom": "^18.2.0", - "react-ga": "^3.3.1", "react-helmet": "^6.1.0", "react-icons": "^4.10.1", "react-is": "^18.2.0", diff --git a/packages/jaeger-ui/src/utils/tracking/ga.test.js b/packages/jaeger-ui/src/utils/tracking/ga.test.js index d9944851d1..dbe48d9bdd 100644 --- a/packages/jaeger-ui/src/utils/tracking/ga.test.js +++ b/packages/jaeger-ui/src/utils/tracking/ga.test.js @@ -12,10 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import ReactGA from 'react-ga'; import * as GA from './ga'; -import * as utils from './utils'; -import { getVersionInfo, getAppEnvironment } from '../constants'; +import { getAppEnvironment } from '../constants'; jest.mock('./conv-raven-to-ga', () => () => ({ category: 'jaeger/a', @@ -35,7 +33,6 @@ function getStr(len) { } describe('google analytics tracking', () => { - let calls; let tracking; beforeAll(() => { @@ -54,16 +51,18 @@ describe('google analytics tracking', () => { }); beforeEach(() => { - getVersionInfo.mockReturnValue('{}'); - calls = ReactGA.testModeAPI.calls; - calls.length = 0; + jest.useFakeTimers(); + // Set an arbitrary date so that we can test the date-based dimension + jest.setSystemTime(new Date('2023-01-01')); + window.dataLayer = []; }); describe('init', () => { it('check init function (no cookies)', () => { tracking.init(); - expect(calls).toEqual([ - ['create', 'UA-123456', 'auto'], + expect(window.dataLayer).toEqual([ + ['js', new Date()], + ['config', 'UA-123456'], [ 'set', { @@ -78,8 +77,9 @@ describe('google analytics tracking', () => { it('check init function (no cookies)', () => { document.cookie = 'page=1;'; tracking.init(); - expect(calls).toEqual([ - ['create', 'UA-123456', 'auto'], + expect(window.dataLayer).toEqual([ + ['js', new Date()], + ['config', 'UA-123456'], [ 'set', { @@ -96,33 +96,33 @@ describe('google analytics tracking', () => { describe('trackPageView', () => { it('tracks a page view', () => { tracking.trackPageView('a', 'b'); - expect(calls).toEqual([['send', { hitType: 'pageview', page: 'ab' }]]); + expect(window.dataLayer).toEqual([['event', 'page_view', { page_path: 'ab' }]]); }); it('ignores search when it is falsy', () => { tracking.trackPageView('a'); - expect(calls).toEqual([['send', { hitType: 'pageview', page: 'a' }]]); + expect(window.dataLayer).toEqual([['event', 'page_view', { page_path: 'a' }]]); }); }); describe('trackError', () => { it('tracks an error', () => { tracking.trackError('a'); - expect(calls).toEqual([ - ['send', { hitType: 'exception', exDescription: expect.any(String), exFatal: false }], + expect(window.dataLayer).toEqual([ + ['event', 'exception', { description: expect.any(String), fatal: false }], ]); }); it('ensures "jaeger" is prepended', () => { tracking.trackError('a'); - expect(calls).toEqual([['send', { hitType: 'exception', exDescription: 'jaeger/a', exFatal: false }]]); + expect(window.dataLayer).toEqual([['event', 'exception', { description: 'jaeger/a', fatal: false }]]); }); it('truncates if needed', () => { const str = `jaeger/${getStr(200)}`; tracking.trackError(str); - expect(calls).toEqual([ - ['send', { hitType: 'exception', exDescription: str.slice(0, 149), exFatal: false }], + expect(window.dataLayer).toEqual([ + ['event', 'exception', { description: str.slice(0, 149), fatal: false }], ]); }); }); @@ -132,13 +132,12 @@ describe('google analytics tracking', () => { const category = 'jaeger/some-category'; const action = 'some-action'; tracking.trackEvent(category, action); - expect(calls).toEqual([ + expect(window.dataLayer).toEqual([ [ - 'send', + 'event', + 'some-action', { - hitType: 'event', - eventCategory: category, - eventAction: action, + event_category: category, }, ], ]); @@ -148,22 +147,19 @@ describe('google analytics tracking', () => { const category = 'some-category'; const action = 'some-action'; tracking.trackEvent(category, action); - expect(calls).toEqual([ - ['send', { hitType: 'event', eventCategory: `jaeger/${category}`, eventAction: action }], - ]); + expect(window.dataLayer).toEqual([['event', 'some-action', { event_category: `jaeger/${category}` }]]); }); it('truncates values, if needed', () => { const str = `jaeger/${getStr(600)}`; tracking.trackEvent(str, str, str); - expect(calls).toEqual([ + expect(window.dataLayer).toEqual([ [ - 'send', + 'event', + str.slice(0, 499), { - hitType: 'event', - eventCategory: str.slice(0, 149), - eventAction: str.slice(0, 499), - eventLabel: str.slice(0, 499), + event_category: str.slice(0, 149), + event_label: str.slice(0, 499), }, ], ]); @@ -171,10 +167,12 @@ describe('google analytics tracking', () => { }); it('converting raven-js errors', () => { - window.onunhandledrejection({ reason: new Error('abc') }); - expect(calls).toEqual([ - ['send', { hitType: 'exception', exDescription: expect.any(String), exFatal: false }], - ['send', { hitType: 'event', eventCategory: expect.any(String), eventAction: expect.any(String) }], + window.onunhandledrejection({ + reason: new Error('abc'), + }); + expect(window.dataLayer).toEqual([ + ['event', 'exception', { description: expect.any(String), fatal: false }], + ['event', expect.any(String), { event_category: expect.any(String) }], ]); }); @@ -206,14 +204,18 @@ describe('google analytics tracking', () => { ); }); + /* eslint-disable no-console */ it('isDebugMode = true', () => { // eslint-disable-next-line no-import-assign - utils.logTrackingCalls = jest.fn(); + console.log = jest.fn(); + trackingDebug.init(); + expect(console.log).toHaveBeenCalledTimes(4); + trackingDebug.trackError(); trackingDebug.trackEvent('jaeger/some-category', 'some-action'); trackingDebug.trackPageView('a', 'b'); - expect(utils.logTrackingCalls).toHaveBeenCalledTimes(4); + expect(console.log).toHaveBeenCalledTimes(7); }); }); }); diff --git a/packages/jaeger-ui/src/utils/tracking/ga.tsx b/packages/jaeger-ui/src/utils/tracking/ga.tsx index a85d681e56..b7bdfa52ec 100644 --- a/packages/jaeger-ui/src/utils/tracking/ga.tsx +++ b/packages/jaeger-ui/src/utils/tracking/ga.tsx @@ -13,17 +13,25 @@ // limitations under the License. import _get from 'lodash/get'; -import ReactGA from 'react-ga'; import Raven, { RavenOptions, RavenTransportOptions } from 'raven-js'; import convRavenToGa from './conv-raven-to-ga'; import { TNil } from '../../types'; import { Config } from '../../types/config'; import { IWebAnalyticsFunc } from '../../types/tracking'; -import { logTrackingCalls } from './utils'; import { getAppEnvironment, shouldDebugGoogleAnalytics } from '../constants'; import parseQuery from '../parseQuery'; +// Modify the `window` object to have an additional attribute `dataLayer` +// This is required by the gtag.js script to work + +// eslint-disable-next-line @typescript-eslint/naming-convention +interface WindowWithGATracking extends Window { + dataLayer: (string | object)[][] | undefined; +} + +declare let window: WindowWithGATracking; + const isTruish = (value?: string | string[]) => { return Boolean(value) && value !== '0' && value !== 'false'; }; @@ -50,16 +58,29 @@ const GA: IWebAnalyticsFunc = (config: Config, versionShort: string, versionLong return isTest || isDebugMode || (isProd && Boolean(gaID)); }; + // Function to add a new event to the Google Analytics dataLayer + const gtag = (...args: (string | object)[]) => { + if (window !== undefined) + if (window.dataLayer !== undefined && Array.isArray(window.dataLayer)) { + window.dataLayer.push(args); + if (isDebugMode) { + // eslint-disable-next-line no-console + console.log('[GA Tracking]', ...args); + } + } + }; + const trackError = (description: string) => { let msg = description; if (!/^jaeger/i.test(msg)) { msg = `jaeger/${msg}`; } msg = msg.slice(0, 149); - ReactGA.exception({ description: msg, fatal: false }); - if (isDebugMode) { - logTrackingCalls(); - } + + gtag('event', 'exception', { + description: msg, + fatal: false, + }); }; const trackEvent = ( @@ -90,10 +111,12 @@ const GA: IWebAnalyticsFunc = (config: Config, versionShort: string, versionLong if (value != null) { event.value = Math.round(value); } - ReactGA.event(event); - if (isDebugMode) { - logTrackingCalls(); - } + + gtag('event', event.action, { + event_category: event.category, + ...(event.label && { event_label: event.label }), + ...(event.value && { event_value: event.value }), + }); }; const trackRavenError = (ravenData: RavenTransportOptions) => { @@ -107,18 +130,34 @@ const GA: IWebAnalyticsFunc = (config: Config, versionShort: string, versionLong return; } - const gaConfig = { testMode: isTest || isDebugMode, titleCase: false, debug: true }; - ReactGA.initialize(gaID || 'debug-mode', gaConfig); - ReactGA.set({ + const gtagUrl = 'https://www.googletagmanager.com/gtag/js'; + const GA_MEASUREMENT_ID = gaID || 'debug-mode'; + + // Load the script asynchronously + const script = document.createElement('script'); + script.async = true; + script.src = `${gtagUrl}?id=${GA_MEASUREMENT_ID}`; + document.body.appendChild(script); + + // Initialize the dataLayer and send initial configuration data + window.dataLayer = window.dataLayer || []; + gtag('js', new Date()); + gtag('config', GA_MEASUREMENT_ID); + gtag('set', { appId: 'github.com/jaegertracing/jaeger-ui', appName: 'Jaeger UI', appVersion: versionLong, }); + if (cookiesToDimensions !== undefined) { (cookiesToDimensions as unknown as Array<{ cookie: string; dimension: string }>).forEach( ({ cookie, dimension }: { cookie: string; dimension: string }) => { const match = ` ${document.cookie}`.match(new RegExp(`[; ]${cookie}=([^\\s;]*)`)); - if (match) ReactGA.set({ [dimension]: match[1] }); + if (match) { + gtag('set', { + [dimension]: match[1], + }); + } // eslint-disable-next-line no-console else console.warn(`${cookie} not present in cookies, could not set dimension: ${dimension}`); } @@ -145,17 +184,13 @@ const GA: IWebAnalyticsFunc = (config: Config, versionShort: string, versionLong Raven.captureException(evt.reason); }; } - if (isDebugMode) { - logTrackingCalls(); - } }; const trackPageView = (pathname: string, search: string | TNil) => { const pagePath = search ? `${pathname}${search}` : pathname; - ReactGA.pageview(pagePath); - if (isDebugMode) { - logTrackingCalls(); - } + gtag('event', 'page_view', { + page_path: pagePath, + }); }; return { diff --git a/packages/jaeger-ui/src/utils/tracking/utils.test.js b/packages/jaeger-ui/src/utils/tracking/utils.test.js deleted file mode 100644 index 30c61067d3..0000000000 --- a/packages/jaeger-ui/src/utils/tracking/utils.test.js +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) 2021 The Jaeger Authors. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import * as utils from './utils'; - -describe('utils', () => { - describe('logTrackingCalls', () => { - it('dry run', () => { - expect(utils.logTrackingCalls()).toBeUndefined(); - }); - }); -}); diff --git a/packages/jaeger-ui/src/utils/tracking/utils.tsx b/packages/jaeger-ui/src/utils/tracking/utils.tsx deleted file mode 100644 index 75f826da59..0000000000 --- a/packages/jaeger-ui/src/utils/tracking/utils.tsx +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) 2021 The Jaeger Authors. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import ReactGA from 'react-ga'; - -// eslint-disable-next-line import/prefer-default-export -export const logTrackingCalls = () => { - const calls = ReactGA.testModeAPI.calls; - for (let i = 0; i < calls.length; i++) { - // eslint-disable-next-line no-console - console.log('[react-ga]', ...calls[i]); - } - calls.length = 0; -}; diff --git a/yarn.lock b/yarn.lock index ff03dec9b6..9eee9613bc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8504,11 +8504,6 @@ react-fast-compare@^3.1.1: resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA== -react-ga@^3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/react-ga/-/react-ga-3.3.1.tgz#d8e1f4e05ec55ed6ff944dcb14b99011dfaf9504" - integrity sha512-4Vc0W5EvXAXUN/wWyxvsAKDLLgtJ3oLmhYYssx+YzphJpejtOst6cbIHCIyF50Fdxuf5DDKqRYny24yJ2y7GFQ== - react-helmet@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/react-helmet/-/react-helmet-6.1.0.tgz#a750d5165cb13cf213e44747502652e794468726"