aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web
diff options
context:
space:
mode:
authorSiegfried Ehret <49895321+siegfried-ehret-sonarsource@users.noreply.github.com>2019-05-13 16:25:18 +0200
committerSonarTech <sonartech@sonarsource.com>2019-05-13 20:21:18 +0200
commit5a5f33146ac2e224b13c0dce33cf162c5ae70a6f (patch)
tree4d3854916efb1c647517a67fd11cdfaefa613cbb /server/sonar-web
parent1fd715525d4e4de351f32df2625e72ab2a9a22d5 (diff)
downloadsonarqube-5a5f33146ac2e224b13c0dce33cf162c5ae70a6f.tar.gz
sonarqube-5a5f33146ac2e224b13c0dce33cf162c5ae70a6f.zip
SONARCLOUD-615 Add Google Tag Manager
Diffstat (limited to 'server/sonar-web')
-rw-r--r--server/sonar-web/src/main/js/app/components/App.tsx8
-rw-r--r--server/sonar-web/src/main/js/app/components/PageTracker.tsx75
-rw-r--r--server/sonar-web/src/main/js/app/components/SimpleSessionsContainer.tsx17
-rw-r--r--server/sonar-web/src/main/js/app/components/__tests__/PageTracker-test.tsx78
-rw-r--r--server/sonar-web/src/main/js/app/index.ts13
-rw-r--r--server/sonar-web/src/main/js/helpers/analytics.js27
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 };