SONARCLOUD_ENABLED("sonar.sonarcloud.enabled", "false"),
SONARCLOUD_HOMEPAGE_URL("sonar.homepage.url", ""),
SONAR_PRISMIC_ACCESS_TOKEN("sonar.prismic.accessToken", ""),
- SONAR_ANALYTICS_TRACKING_ID("sonar.analytics.ga.trackingId", ""),
+ SONAR_ANALYTICS_GA_TRACKING_ID("sonar.analytics.ga.trackingId", ""),
+ SONAR_ANALYTICS_GTM_TRACKING_ID("sonar.analytics.gtm.trackingId", ""),
ONBOARDING_TUTORIAL_SHOW_TO_NEW_USERS("sonar.onboardingTutorial.showToNewUsers", "true"),
BITBUCKETCLOUD_APP_KEY("sonar.bitbucketcloud.appKey", "sonarcloud"),
import static org.sonar.core.config.WebConstants.SONAR_LF_LOGO_WIDTH_PX;
import static org.sonar.process.ProcessProperties.Property.SONARCLOUD_ENABLED;
import static org.sonar.process.ProcessProperties.Property.SONARCLOUD_HOMEPAGE_URL;
-import static org.sonar.process.ProcessProperties.Property.SONAR_ANALYTICS_TRACKING_ID;
+import static org.sonar.process.ProcessProperties.Property.SONAR_ANALYTICS_GTM_TRACKING_ID;
+import static org.sonar.process.ProcessProperties.Property.SONAR_ANALYTICS_GA_TRACKING_ID;
import static org.sonar.process.ProcessProperties.Property.SONAR_PRISMIC_ACCESS_TOKEN;
import static org.sonar.process.ProcessProperties.Property.SONAR_UPDATECENTER_ACTIVATE;
boolean isOnSonarCloud = config.getBoolean(SONARCLOUD_ENABLED.getKey()).orElse(false);
if (isOnSonarCloud) {
this.systemSettingValuesByKey.put(SONAR_PRISMIC_ACCESS_TOKEN.getKey(), config.get(SONAR_PRISMIC_ACCESS_TOKEN.getKey()).orElse(null));
- this.systemSettingValuesByKey.put(SONAR_ANALYTICS_TRACKING_ID.getKey(), config.get(SONAR_ANALYTICS_TRACKING_ID.getKey()).orElse(null));
+ this.systemSettingValuesByKey.put(SONAR_ANALYTICS_GA_TRACKING_ID.getKey(), config.get(SONAR_ANALYTICS_GA_TRACKING_ID.getKey()).orElse(null));
+ this.systemSettingValuesByKey.put(SONAR_ANALYTICS_GTM_TRACKING_ID.getKey(), config.get(SONAR_ANALYTICS_GTM_TRACKING_ID.getKey()).orElse(null));
this.systemSettingValuesByKey.put(SONARCLOUD_HOMEPAGE_URL.getKey(), config.get(SONARCLOUD_HOMEPAGE_URL.getKey()).orElse(null));
}
}
settings.setProperty("sonar.sonarcloud.enabled", true);
settings.setProperty("sonar.prismic.accessToken", "secret");
settings.setProperty("sonar.analytics.ga.trackingId", "ga_id");
+ settings.setProperty("sonar.analytics.gtm.trackingId", "gtm_id");
settings.setProperty("sonar.homepage.url", "https://s3/homepage.json");
init();
" \"settings\": {" +
" \"sonar.prismic.accessToken\": \"secret\"," +
" \"sonar.analytics.ga.trackingId\": \"ga_id\"," +
+ " \"sonar.analytics.gtm.trackingId\": \"gtm_id\"," +
" \"sonar.homepage.url\": \"https://s3/homepage.json\"" +
" }" +
"}");
*/
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';
render() {
return (
<>
- <Helmet defaultTitle={getInstance()}>
- {this.props.enableGravatar && this.renderPreconnectLink()}
- </Helmet>
- <PageTracker />
+ <PageTracker>{this.props.enableGravatar && this.renderPreconnectLink()}</PageTracker>
{this.props.children}
</>
);
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
};
};
*/
import * as React from 'react';
import GlobalFooterContainer from './GlobalFooterContainer';
+import { lazyLoad } from '../../components/lazyLoad';
+
+const PageTracker = lazyLoad(() => import('./PageTracker'));
interface Props {
children?: React.ReactNode;
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>
+ </>
);
}
--- /dev/null
+/*
+ * 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}
+ />
+ );
+}
);
} 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);
--- /dev/null
+/*
+ * 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 };