@@ -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"), |
@@ -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)); | |||
} | |||
} |
@@ -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\"" + | |||
" }" + | |||
"}"); |
@@ -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} | |||
</> | |||
); |
@@ -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 | |||
}; | |||
}; | |||
@@ -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> | |||
</> | |||
); | |||
} |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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); |
@@ -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 }; |