diff options
author | Siegfried Ehret <49895321+siegfried-ehret-sonarsource@users.noreply.github.com> | 2019-05-13 16:25:18 +0200 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2019-05-13 20:21:18 +0200 |
commit | 5a5f33146ac2e224b13c0dce33cf162c5ae70a6f (patch) | |
tree | 4d3854916efb1c647517a67fd11cdfaefa613cbb /server/sonar-web | |
parent | 1fd715525d4e4de351f32df2625e72ab2a9a22d5 (diff) | |
download | sonarqube-5a5f33146ac2e224b13c0dce33cf162c5ae70a6f.tar.gz sonarqube-5a5f33146ac2e224b13c0dce33cf162c5ae70a6f.zip |
SONARCLOUD-615 Add Google Tag Manager
Diffstat (limited to 'server/sonar-web')
6 files changed, 185 insertions, 33 deletions
diff --git a/server/sonar-web/src/main/js/app/components/App.tsx b/server/sonar-web/src/main/js/app/components/App.tsx index 1d9378e9c74..82f566cbcee 100644 --- a/server/sonar-web/src/main/js/app/components/App.tsx +++ b/server/sonar-web/src/main/js/app/components/App.tsx @@ -19,10 +19,9 @@ */ import * as React from 'react'; import { connect } from 'react-redux'; -import Helmet from 'react-helmet'; import { fetchLanguages } from '../../store/rootActions'; import { fetchMyOrganizations } from '../../apps/account/organizations/actions'; -import { getInstance, isSonarCloud } from '../../helpers/system'; +import { isSonarCloud } from '../../helpers/system'; import { lazyLoad } from '../../components/lazyLoad'; import { getCurrentUser, getAppState, getGlobalSettingValue, Store } from '../../store/rootReducer'; import { isLoggedIn } from '../../helpers/users'; @@ -102,10 +101,7 @@ class App extends React.PureComponent<Props> { render() { return ( <> - <Helmet defaultTitle={getInstance()}> - {this.props.enableGravatar && this.renderPreconnectLink()} - </Helmet> - <PageTracker /> + <PageTracker>{this.props.enableGravatar && this.renderPreconnectLink()}</PageTracker> {this.props.children} </> ); diff --git a/server/sonar-web/src/main/js/app/components/PageTracker.tsx b/server/sonar-web/src/main/js/app/components/PageTracker.tsx index 9c8825be56f..322f2eb7c75 100644 --- a/server/sonar-web/src/main/js/app/components/PageTracker.tsx +++ b/server/sonar-web/src/main/js/app/components/PageTracker.tsx @@ -21,48 +21,85 @@ import * as React from 'react'; import * as GoogleAnalytics from 'react-ga'; import { withRouter, WithRouterProps } from 'react-router'; import { connect } from 'react-redux'; +import Helmet from 'react-helmet'; import { getGlobalSettingValue, Store } from '../../store/rootReducer'; +import { gtm } from '../../helpers/analytics'; +import { getInstance } from '../../helpers/system'; interface StateProps { - trackingId?: string; + trackingIdGA?: string; + trackingIdGTM?: string; } type Props = WithRouterProps & StateProps; -export class PageTracker extends React.PureComponent<Props> { +interface State { + lastLocation?: string; +} + +export class PageTracker extends React.Component<Props, State> { + state: State = { + lastLocation: undefined + }; + componentDidMount() { - if (this.props.trackingId) { - GoogleAnalytics.initialize(this.props.trackingId); - this.trackPage(); - } - } + const { trackingIdGA, trackingIdGTM } = this.props; - componentDidUpdate(prevProps: Props) { - const currentPage = this.props.location.pathname; - const prevPage = prevProps.location.pathname; + if (trackingIdGA) { + GoogleAnalytics.initialize(trackingIdGA); + } - if (currentPage !== prevPage) { - this.trackPage(); + if (trackingIdGTM) { + gtm(trackingIdGTM); } } trackPage = () => { - const { location, trackingId } = this.props; - if (trackingId) { - // More info on the "title and page not in sync" issue: https://github.com/nfl/react-helmet/issues/189 - setTimeout(() => GoogleAnalytics.pageview(location.pathname), 500); + const { location, trackingIdGA, trackingIdGTM } = this.props; + const { lastLocation } = this.state; + + if (location.pathname !== lastLocation) { + if (trackingIdGA) { + // More info on the "title and page not in sync" issue: https://github.com/nfl/react-helmet/issues/189 + setTimeout(() => GoogleAnalytics.pageview(location.pathname), 500); + } + + if (trackingIdGTM && location.pathname !== '/') { + setTimeout(() => { + const { dataLayer } = window as any; + if (dataLayer && dataLayer.push) { + dataLayer.push({ event: 'render-end' }); + } + }, 500); + } + + this.setState({ + lastLocation: location.pathname + }); } }; render() { - return null; + const { trackingIdGA, trackingIdGTM } = this.props; + const tracking = { + ...((trackingIdGA || trackingIdGTM) && { onChangeClientState: this.trackPage }) + }; + + return ( + <Helmet defaultTitle={getInstance()} {...tracking}> + {this.props.children} + </Helmet> + ); } } const mapStateToProps = (state: Store): StateProps => { - const trackingId = getGlobalSettingValue(state, 'sonar.analytics.ga.trackingId'); + const trackingIdGA = getGlobalSettingValue(state, 'sonar.analytics.ga.trackingId'); + const trackingIdGTM = getGlobalSettingValue(state, 'sonar.analytics.gtm.trackingId'); + return { - trackingId: trackingId && trackingId.value + trackingIdGA: trackingIdGA && trackingIdGA.value, + trackingIdGTM: trackingIdGTM && trackingIdGTM.value }; }; diff --git a/server/sonar-web/src/main/js/app/components/SimpleSessionsContainer.tsx b/server/sonar-web/src/main/js/app/components/SimpleSessionsContainer.tsx index 3a1a52548ed..32965ae187d 100644 --- a/server/sonar-web/src/main/js/app/components/SimpleSessionsContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/SimpleSessionsContainer.tsx @@ -19,6 +19,9 @@ */ import * as React from 'react'; import GlobalFooterContainer from './GlobalFooterContainer'; +import { lazyLoad } from '../../components/lazyLoad'; + +const PageTracker = lazyLoad(() => import('./PageTracker')); interface Props { children?: React.ReactNode; @@ -26,11 +29,15 @@ interface Props { export default function SimpleSessionsContainer({ children }: Props) { return ( - <div className="global-container"> - <div className="page-wrapper" id="container"> - {children} + <> + <PageTracker /> + + <div className="global-container"> + <div className="page-wrapper" id="container"> + {children} + </div> + <GlobalFooterContainer hideLoggedInInfo={true} /> </div> - <GlobalFooterContainer hideLoggedInInfo={true} /> - </div> + </> ); } diff --git a/server/sonar-web/src/main/js/app/components/__tests__/PageTracker-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/PageTracker-test.tsx new file mode 100644 index 00000000000..cd87a8f48ef --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/__tests__/PageTracker-test.tsx @@ -0,0 +1,78 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import { mount } from 'enzyme'; +import * as React from 'react'; +import { PageTracker } from '../PageTracker'; +import { mockLocation, mockRouter } from '../../../helpers/testMocks'; + +jest.useFakeTimers(); + +beforeEach(() => { + jest.clearAllTimers(); + + (window as any).dataLayer = []; + + document.getElementsByTagName = jest.fn().mockImplementation(() => { + return [document.body]; + }); +}); + +it('should not trigger if no analytics system is given', () => { + shallowRender(); + + expect(setTimeout).not.toHaveBeenCalled(); +}); + +it('should work for Google Analytics', () => { + const wrapper = shallowRender({ trackingIdGA: '123' }); + const instance = wrapper.instance(); + instance.trackPage(); + + expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 500); +}); + +it('should work for Google Tag Manager', () => { + const wrapper = shallowRender({ trackingIdGTM: '123' }); + const instance = wrapper.instance(); + const dataLayer = (window as any).dataLayer; + + expect(dataLayer).toHaveLength(1); + + instance.trackPage(); + + expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 500); + + jest.runAllTimers(); + + expect(dataLayer).toHaveLength(2); +}); + +function shallowRender(props: Partial<PageTracker['props']> = {}) { + return mount<PageTracker>( + <PageTracker + location={mockLocation()} + params={{}} + router={mockRouter()} + routes={[]} + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/app/index.ts b/server/sonar-web/src/main/js/app/index.ts index defd6a0c1d1..60375997e7c 100644 --- a/server/sonar-web/src/main/js/app/index.ts +++ b/server/sonar-web/src/main/js/app/index.ts @@ -42,9 +42,16 @@ if (isMainApp()) { ); } else { // login, maintenance or setup pages - Promise.all([loadMessages(), loadApp()]).then( - ([lang, startReactApp]) => { - startReactApp(lang, undefined, undefined); + + const appStatePromise: Promise<T.AppState> = new Promise(resolve => + loadAppState() + .then(data => resolve(data)) + .catch(() => resolve(undefined)) + ); + + Promise.all([loadMessages(), appStatePromise, loadApp()]).then( + ([lang, appState, startReactApp]) => { + startReactApp(lang, undefined, appState); }, error => { logError(error); diff --git a/server/sonar-web/src/main/js/helpers/analytics.js b/server/sonar-web/src/main/js/helpers/analytics.js new file mode 100644 index 00000000000..1531a816e9c --- /dev/null +++ b/server/sonar-web/src/main/js/helpers/analytics.js @@ -0,0 +1,27 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +// The body of the `gtm` function comes from Google Tag Manager docs; let's keep it like it was written. +// @ts-ignore +// prettier-ignore +// eslint-disable-next-line +const gtm = id => (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': new Date().getTime(),event:'gtm.js'});const f=d.getElementsByTagName(s)[0], j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);}(window,document,'script','dataLayer',id)); + +module.exports = { gtm }; |