Bläddra i källkod

SONARCLOUD-615 Add Google Tag Manager

tags/7.8
Siegfried Ehret 5 år sedan
förälder
incheckning
5a5f33146a

+ 2
- 1
server/sonar-process/src/main/java/org/sonar/process/ProcessProperties.java Visa fil

@@ -110,7 +110,8 @@ public class ProcessProperties {
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"),

+ 4
- 2
server/sonar-server/src/main/java/org/sonar/server/ui/ws/GlobalAction.java Visa fil

@@ -53,7 +53,8 @@ import static org.sonar.core.config.WebConstants.SONAR_LF_LOGO_URL;
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;

@@ -103,7 +104,8 @@ public class GlobalAction implements NavigationWsAction, Startable {
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));
}
}

+ 2
- 0
server/sonar-server/src/test/java/org/sonar/server/ui/ws/GlobalActionTest.java Visa fil

@@ -131,6 +131,7 @@ public class GlobalActionTest {
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();

@@ -138,6 +139,7 @@ public class GlobalActionTest {
" \"settings\": {" +
" \"sonar.prismic.accessToken\": \"secret\"," +
" \"sonar.analytics.ga.trackingId\": \"ga_id\"," +
" \"sonar.analytics.gtm.trackingId\": \"gtm_id\"," +
" \"sonar.homepage.url\": \"https://s3/homepage.json\"" +
" }" +
"}");

+ 2
- 6
server/sonar-web/src/main/js/app/components/App.tsx Visa fil

@@ -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}
</>
);

+ 56
- 19
server/sonar-web/src/main/js/app/components/PageTracker.tsx Visa fil

@@ -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
};
};


+ 12
- 5
server/sonar-web/src/main/js/app/components/SimpleSessionsContainer.tsx Visa fil

@@ -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>
</>
);
}

+ 78
- 0
server/sonar-web/src/main/js/app/components/__tests__/PageTracker-test.tsx Visa fil

@@ -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}
/>
);
}

+ 10
- 3
server/sonar-web/src/main/js/app/index.ts Visa fil

@@ -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);

+ 27
- 0
server/sonar-web/src/main/js/helpers/analytics.js Visa fil

@@ -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 };

Laddar…
Avbryt
Spara