diff options
author | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2019-05-23 09:04:02 +0200 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2019-05-23 20:21:09 +0200 |
commit | 8bcc788c0e45ab79e0f4ce80cda3363bb4588399 (patch) | |
tree | afe9569cc4db2d948851d19747966ce9e8e9ac32 /server | |
parent | 1f3ee626c1faa8f32aa15ce8dcc0ed333e6e57aa (diff) | |
download | sonarqube-8bcc788c0e45ab79e0f4ce80cda3363bb4588399.tar.gz sonarqube-8bcc788c0e45ab79e0f4ce80cda3363bb4588399.zip |
SONAR-12109 Web Analytics plugins (#1652)
Diffstat (limited to 'server')
26 files changed, 549 insertions, 185 deletions
diff --git a/server/sonar-process/src/main/java/org/sonar/process/ProcessProperties.java b/server/sonar-process/src/main/java/org/sonar/process/ProcessProperties.java index fd0a8b7dad2..a89a6863e9e 100644 --- a/server/sonar-process/src/main/java/org/sonar/process/ProcessProperties.java +++ b/server/sonar-process/src/main/java/org/sonar/process/ProcessProperties.java @@ -118,7 +118,6 @@ public class ProcessProperties { SONARCLOUD_ENABLED("sonar.sonarcloud.enabled", "false"), SONARCLOUD_HOMEPAGE_URL("sonar.homepage.url", ""), SONAR_PRISMIC_ACCESS_TOKEN("sonar.prismic.accessToken", ""), - 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"), diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java index 47947ffb55a..e47bbbe1d28 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java @@ -200,6 +200,7 @@ import org.sonar.server.text.MacroInterpreter; import org.sonar.server.ui.DeprecatedViews; import org.sonar.server.ui.PageDecorations; import org.sonar.server.ui.PageRepository; +import org.sonar.server.ui.WebAnalyticsLoaderImpl; import org.sonar.server.ui.ws.NavigationWsModule; import org.sonar.server.updatecenter.UpdateCenterModule; import org.sonar.server.user.NewUserNotifier; @@ -264,6 +265,7 @@ public class PlatformLevel4 extends PlatformLevel { BackendCleanup.class, IndexDefinitions.class, WebPagesFilter.class, + WebAnalyticsLoaderImpl.class, // batch BatchWsModule.class, diff --git a/server/sonar-web/src/main/js/app/utils/installExtensionsHandler.ts b/server/sonar-server/src/main/java/org/sonar/server/ui/WebAnalyticsLoader.java index adf94c2d84a..ce2f54f3065 100644 --- a/server/sonar-web/src/main/js/app/utils/installExtensionsHandler.ts +++ b/server/sonar-server/src/main/java/org/sonar/server/ui/WebAnalyticsLoader.java @@ -17,16 +17,18 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -const extensions: T.Dict<Function> = {}; +package org.sonar.server.ui; -const registerExtension = (key: string, start: Function) => { - extensions[key] = start; -}; +import java.util.Optional; +import org.sonar.api.server.ServerSide; -export default () => { - (window as any).registerExtension = registerExtension; -}; +@ServerSide +public interface WebAnalyticsLoader { -export const getExtensionFromCache = (key: string) => { - return extensions[key]; -}; + /** + * URL path to the JS file to be loaded by webapp, for instance "/static/foo/bar.js". + * It always starts with "/" and does not include the optional web context. + */ + Optional<String> getUrlPathToJs(); + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/ui/WebAnalyticsLoaderImpl.java b/server/sonar-server/src/main/java/org/sonar/server/ui/WebAnalyticsLoaderImpl.java new file mode 100644 index 00000000000..b82267262cc --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/ui/WebAnalyticsLoaderImpl.java @@ -0,0 +1,54 @@ +/* + * 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. + */ +package org.sonar.server.ui; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import org.sonar.api.utils.MessageException; +import org.sonar.api.web.WebAnalytics; + +public class WebAnalyticsLoaderImpl implements WebAnalyticsLoader { + + @Nullable + private final WebAnalytics analytics; + + public WebAnalyticsLoaderImpl(WebAnalytics[] analytics) { + if (analytics.length > 1) { + List<String> classes = Arrays.stream(analytics).map(a -> a.getClass().getName()).collect(Collectors.toList()); + throw MessageException.of("Limited to only one web analytics plugin. Found multiple implementations: " + classes); + } + this.analytics = analytics.length == 1 ? analytics[0] : null; + } + + public WebAnalyticsLoaderImpl() { + this.analytics = null; + } + + @Override + public Optional<String> getUrlPathToJs() { + return Optional.ofNullable(analytics) + .map(WebAnalytics::getUrlPathToJs) + .filter(path -> !path.startsWith("/") && !path.contains("..") && !path.contains("://")) + .map(path -> "/" + path); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/ui/ws/GlobalAction.java b/server/sonar-server/src/main/java/org/sonar/server/ui/ws/GlobalAction.java index de3f26f9f83..cd363fb12fd 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/ui/ws/GlobalAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/ui/ws/GlobalAction.java @@ -44,6 +44,7 @@ import org.sonar.server.organization.OrganizationFlags; import org.sonar.server.platform.WebServer; import org.sonar.server.ui.PageRepository; import org.sonar.server.ui.VersionFormatter; +import org.sonar.server.ui.WebAnalyticsLoader; import org.sonar.server.user.UserSession; import static org.sonar.api.CoreProperties.RATING_GRID; @@ -54,7 +55,6 @@ 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_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; @@ -80,10 +80,12 @@ public class GlobalAction implements NavigationWsAction, Startable { private final BranchFeatureProxy branchFeature; private final UserSession userSession; private final PlatformEditionProvider editionProvider; + private final WebAnalyticsLoader webAnalyticsLoader; public GlobalAction(PageRepository pageRepository, Configuration config, ResourceTypes resourceTypes, Server server, WebServer webServer, DbClient dbClient, OrganizationFlags organizationFlags, - DefaultOrganizationProvider defaultOrganizationProvider, BranchFeatureProxy branchFeature, UserSession userSession, PlatformEditionProvider editionProvider) { + DefaultOrganizationProvider defaultOrganizationProvider, BranchFeatureProxy branchFeature, UserSession userSession, PlatformEditionProvider editionProvider, + WebAnalyticsLoader webAnalyticsLoader) { this.pageRepository = pageRepository; this.config = config; this.resourceTypes = resourceTypes; @@ -95,6 +97,7 @@ public class GlobalAction implements NavigationWsAction, Startable { this.branchFeature = branchFeature; this.userSession = userSession; this.editionProvider = editionProvider; + this.webAnalyticsLoader = webAnalyticsLoader; this.systemSettingValuesByKey = new HashMap<>(); } @@ -104,7 +107,6 @@ 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_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)); } @@ -140,6 +142,7 @@ public class GlobalAction implements NavigationWsAction, Startable { writeBranchSupport(json); editionProvider.get().ifPresent(e -> json.prop("edition", e.name().toLowerCase(Locale.ENGLISH))); json.prop("standalone", webServer.isStandalone()); + writeWebAnalytics(json); json.endObject(); } } @@ -199,4 +202,7 @@ public class GlobalAction implements NavigationWsAction, Startable { json.prop("branchesEnabled", branchFeature.isEnabled()); } + private void writeWebAnalytics(JsonWriter json) { + webAnalyticsLoader.getUrlPathToJs().ifPresent(p -> json.prop("webAnalyticsJsPath", p)); + } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/ui/WebAnalyticsLoaderImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/ui/WebAnalyticsLoaderImplTest.java new file mode 100644 index 00000000000..e7be923df8e --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/ui/WebAnalyticsLoaderImplTest.java @@ -0,0 +1,89 @@ +/* + * 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. + */ +package org.sonar.server.ui; + +import org.junit.Test; +import org.sonar.api.utils.MessageException; +import org.sonar.api.web.WebAnalytics; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class WebAnalyticsLoaderImplTest { + + @Test + public void return_empty_if_no_analytics_plugin() { + assertThat(new WebAnalyticsLoaderImpl().getUrlPathToJs()).isEmpty(); + assertThat(new WebAnalyticsLoaderImpl(new WebAnalytics[0]).getUrlPathToJs()).isEmpty(); + } + + @Test + public void return_js_path_if_analytics_plugin_is_installed() { + WebAnalytics analytics = newWebAnalytics("api/google/analytics"); + WebAnalyticsLoaderImpl underTest = new WebAnalyticsLoaderImpl(new WebAnalytics[] {analytics}); + + assertThat(underTest.getUrlPathToJs()).hasValue("/api/google/analytics"); + } + + @Test + public void return_empty_if_path_starts_with_slash() { + WebAnalytics analytics = newWebAnalytics("/api/google/analytics"); + WebAnalyticsLoaderImpl underTest = new WebAnalyticsLoaderImpl(new WebAnalytics[] {analytics}); + + assertThat(underTest.getUrlPathToJs()).isEmpty(); + } + + @Test + public void return_empty_if_path_is_an_url() { + WebAnalytics analytics = newWebAnalytics("http://foo"); + WebAnalyticsLoaderImpl underTest = new WebAnalyticsLoaderImpl(new WebAnalytics[] {analytics}); + + assertThat(underTest.getUrlPathToJs()).isEmpty(); + } + + @Test + public void return_empty_if_path_has_up_operation() { + WebAnalytics analytics = newWebAnalytics("foo/../bar"); + WebAnalyticsLoaderImpl underTest = new WebAnalyticsLoaderImpl(new WebAnalytics[] {analytics}); + + assertThat(underTest.getUrlPathToJs()).isEmpty(); + } + + @Test + public void fail_if_multiple_analytics_plugins_are_installed() { + WebAnalytics analytics1 = newWebAnalytics("foo"); + WebAnalytics analytics2 = newWebAnalytics("bar"); + + Throwable thrown = catchThrowable(() -> new WebAnalyticsLoaderImpl(new WebAnalytics[] {analytics1, analytics2})); + + assertThat(thrown) + .isInstanceOf(MessageException.class) + .hasMessage("Limited to only one web analytics plugin. Found multiple implementations: [" + + analytics1.getClass().getName() + ", " + analytics2.getClass().getName() + "]"); + } + + private static WebAnalytics newWebAnalytics(String path) { + WebAnalytics analytics = mock(WebAnalytics.class); + when(analytics.getUrlPathToJs()).thenReturn(path); + return analytics; + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/ui/ws/GlobalActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/ui/ws/GlobalActionTest.java index 19b40373e8b..918fb5af4bf 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/ui/ws/GlobalActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/ui/ws/GlobalActionTest.java @@ -44,6 +44,7 @@ import org.sonar.server.organization.TestOrganizationFlags; import org.sonar.server.platform.WebServer; import org.sonar.server.tester.UserSessionRule; import org.sonar.server.ui.PageRepository; +import org.sonar.server.ui.WebAnalyticsLoader; import org.sonar.server.ws.WsActionTester; import org.sonar.updatecenter.common.Version; @@ -68,6 +69,7 @@ public class GlobalActionTest { private DefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.fromUuid("foo"); private BranchFeatureRule branchFeature = new BranchFeatureRule(); private PlatformEditionProvider editionProvider = mock(PlatformEditionProvider.class); + private WebAnalyticsLoader webAnalyticsLoader = mock(WebAnalyticsLoader.class); private WsActionTester ws; @@ -130,7 +132,6 @@ public class GlobalActionTest { public void return_sonarcloud_settings() { 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,7 +139,6 @@ public class GlobalActionTest { assertJson(call()).isSimilarTo("{" + " \"settings\": {" + " \"sonar.prismic.accessToken\": \"secret\"," + - " \"sonar.analytics.ga.trackingId\": \"ga_id\"," + " \"sonar.analytics.gtm.trackingId\": \"gtm_id\"," + " \"sonar.homepage.url\": \"https://s3/homepage.json\"" + " }" + @@ -313,6 +313,25 @@ public class GlobalActionTest { assertJson(json).isSimilarTo("{\"edition\":\"developer\"}"); } + @Test + public void web_analytics_js_path_is_not_returned_if_not_defined() { + init(); + when(webAnalyticsLoader.getUrlPathToJs()).thenReturn(Optional.empty()); + + String json = call(); + assertThat(json).doesNotContain("webAnalyticsJsPath"); + } + + @Test + public void web_analytics_js_path_is_returned_if_defined() { + init(); + String path = "static/googleanalytics/analytics.js"; + when(webAnalyticsLoader.getUrlPathToJs()).thenReturn(Optional.of(path)); + + String json = call(); + assertJson(json).isSimilarTo("{\"webAnalyticsJsPath\":\"" + path + "\"}"); + } + private void init() { init(new org.sonar.api.web.page.Page[] {}, new ResourceTypeTree[] {}); } @@ -332,7 +351,7 @@ public class GlobalActionTest { }}); pageRepository.start(); GlobalAction wsAction = new GlobalAction(pageRepository, settings.asConfig(), new ResourceTypes(resourceTypeTrees), server, - webServer, dbClient, organizationFlags, defaultOrganizationProvider, branchFeature, userSession, editionProvider); + webServer, dbClient, organizationFlags, defaultOrganizationProvider, branchFeature, userSession, editionProvider, webAnalyticsLoader); ws = new WsActionTester(wsAction); wsAction.start(); } diff --git a/server/sonar-web/package.json b/server/sonar-web/package.json index aafb7aaa2f5..cca4351ea04 100644 --- a/server/sonar-web/package.json +++ b/server/sonar-web/package.json @@ -29,7 +29,6 @@ "react-day-picker": "7.3.0", "react-dom": "16.8.5", "react-draggable": "3.2.1", - "react-ga": "2.5.7", "react-helmet": "5.2.0", "react-intl": "2.8.0", "react-modal": "3.8.1", 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 322f2eb7c75..0af0e6bec16 100644 --- a/server/sonar-web/src/main/js/app/components/PageTracker.tsx +++ b/server/sonar-web/src/main/js/app/components/PageTracker.tsx @@ -18,35 +18,32 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ 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 { Location, withRouter } from '../../components/hoc/withRouter'; import { gtm } from '../../helpers/analytics'; +import { installScript, getWebAnalyticsPageHandlerFromCache } from '../../helpers/extensions'; import { getInstance } from '../../helpers/system'; +import { getGlobalSettingValue, Store, getAppState } from '../../store/rootReducer'; -interface StateProps { - trackingIdGA?: string; +interface Props { + location: Location; trackingIdGTM?: string; + webAnalytics?: string; } -type Props = WithRouterProps & StateProps; - interface State { lastLocation?: string; } export class PageTracker extends React.Component<Props, State> { - state: State = { - lastLocation: undefined - }; + state: State = {}; componentDidMount() { - const { trackingIdGA, trackingIdGTM } = this.props; + const { trackingIdGTM, webAnalytics } = this.props; - if (trackingIdGA) { - GoogleAnalytics.initialize(trackingIdGA); + if (webAnalytics && !getWebAnalyticsPageHandlerFromCache()) { + installScript(webAnalytics, 'head'); } if (trackingIdGTM) { @@ -55,51 +52,39 @@ export class PageTracker extends React.Component<Props, State> { } trackPage = () => { - const { location, trackingIdGA, trackingIdGTM } = this.props; + const { location, 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 - }); + const { dataLayer } = window as any; + const locationChanged = location.pathname !== lastLocation; + const webAnalyticsPageChange = getWebAnalyticsPageHandlerFromCache(); + + if (webAnalyticsPageChange && locationChanged) { + this.setState({ lastLocation: location.pathname }); + setTimeout(() => webAnalyticsPageChange(location.pathname), 500); + } else if (dataLayer && dataLayer.push && trackingIdGTM && location.pathname !== '/') { + this.setState({ lastLocation: location.pathname }); + setTimeout(() => dataLayer.push({ event: 'render-end' }), 500); } }; render() { - const { trackingIdGA, trackingIdGTM } = this.props; - const tracking = { - ...((trackingIdGA || trackingIdGTM) && { onChangeClientState: this.trackPage }) - }; + const { trackingIdGTM, webAnalytics } = this.props; return ( - <Helmet defaultTitle={getInstance()} {...tracking}> + <Helmet + defaultTitle={getInstance()} + onChangeClientState={trackingIdGTM || webAnalytics ? this.trackPage : undefined}> {this.props.children} </Helmet> ); } } -const mapStateToProps = (state: Store): StateProps => { - const trackingIdGA = getGlobalSettingValue(state, 'sonar.analytics.ga.trackingId'); +const mapStateToProps = (state: Store) => { const trackingIdGTM = getGlobalSettingValue(state, 'sonar.analytics.gtm.trackingId'); - return { - trackingIdGA: trackingIdGA && trackingIdGA.value, - trackingIdGTM: trackingIdGTM && trackingIdGTM.value + trackingIdGTM: trackingIdGTM && trackingIdGTM.value, + webAnalytics: getAppState(state).webAnalyticsJsPath }; }; 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 index cd87a8f48ef..59f6fe13655 100644 --- 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 @@ -18,61 +18,69 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { mount } from 'enzyme'; import * as React from 'react'; +import { shallow } from 'enzyme'; import { PageTracker } from '../PageTracker'; -import { mockLocation, mockRouter } from '../../../helpers/testMocks'; +import { gtm } from '../../../helpers/analytics'; +import { mockLocation } from '../../../helpers/testMocks'; +import { installScript, getWebAnalyticsPageHandlerFromCache } from '../../../helpers/extensions'; + +jest.mock('../../../helpers/extensions', () => ({ + installScript: jest.fn().mockResolvedValue({}), + getWebAnalyticsPageHandlerFromCache: jest.fn().mockReturnValue(undefined) +})); + +jest.mock('../../../helpers/analytics', () => ({ gtm: jest.fn() })); jest.useFakeTimers(); beforeEach(() => { jest.clearAllTimers(); - - (window as any).dataLayer = []; - - document.getElementsByTagName = jest.fn().mockImplementation(() => { - return [document.body]; - }); + jest.clearAllMocks(); }); it('should not trigger if no analytics system is given', () => { - shallowRender(); - - expect(setTimeout).not.toHaveBeenCalled(); + const wrapper = shallowRender(); + expect(wrapper).toMatchSnapshot(); + expect(installScript).not.toHaveBeenCalled(); + expect(gtm).not.toHaveBeenCalled(); }); -it('should work for Google Analytics', () => { - const wrapper = shallowRender({ trackingIdGA: '123' }); - const instance = wrapper.instance(); - instance.trackPage(); +it('should work for WebAnalytics plugin', () => { + const pageChange = jest.fn(); + const webAnalytics = '/static/pluginKey/web_analytics.js'; + const wrapper = shallowRender({ webAnalytics }); + + expect(wrapper).toMatchSnapshot(); + expect(wrapper.find('HelmetWrapper').prop('onChangeClientState')).toBe( + wrapper.instance().trackPage + ); + expect(installScript).toBeCalledWith(webAnalytics, 'head'); + (getWebAnalyticsPageHandlerFromCache as jest.Mock).mockReturnValueOnce(pageChange); - expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 500); + wrapper.instance().trackPage(); + jest.runAllTimers(); + expect(pageChange).toHaveBeenCalledWith('/path'); }); it('should work for Google Tag Manager', () => { + (window as any).dataLayer = []; + const { dataLayer } = window as any; + const push = jest.spyOn(dataLayer, 'push'); 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); + expect(wrapper.find('HelmetWrapper').prop('onChangeClientState')).toBe( + wrapper.instance().trackPage + ); + expect(gtm).toBeCalled(); + expect(dataLayer).toHaveLength(0); + wrapper.instance().trackPage(); jest.runAllTimers(); - - expect(dataLayer).toHaveLength(2); + expect(push).toBeCalledWith({ event: 'render-end' }); + expect(dataLayer).toHaveLength(1); }); function shallowRender(props: Partial<PageTracker['props']> = {}) { - return mount<PageTracker>( - <PageTracker - location={mockLocation()} - params={{}} - router={mockRouter()} - routes={[]} - {...props} - /> - ); + return shallow<PageTracker>(<PageTracker location={mockLocation()} {...props} />); } diff --git a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/PageTracker-test.tsx.snap b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/PageTracker-test.tsx.snap new file mode 100644 index 00000000000..ec84971fa84 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/PageTracker-test.tsx.snap @@ -0,0 +1,16 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should not trigger if no analytics system is given 1`] = ` +<HelmetWrapper + defer={true} + encodeSpecialCharacters={true} +/> +`; + +exports[`should work for WebAnalytics plugin 1`] = ` +<HelmetWrapper + defer={true} + encodeSpecialCharacters={true} + onChangeClientState={[Function]} +/> +`; diff --git a/server/sonar-web/src/main/js/app/components/extensions/Extension.tsx b/server/sonar-web/src/main/js/app/components/extensions/Extension.tsx index 8937b43f07a..2e66aa797d3 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/Extension.tsx +++ b/server/sonar-web/src/main/js/app/components/extensions/Extension.tsx @@ -19,35 +19,29 @@ */ import * as React from 'react'; import Helmet from 'react-helmet'; -import { withRouter, WithRouterProps } from 'react-router'; import { injectIntl, InjectedIntlProps } from 'react-intl'; import { connect } from 'react-redux'; -import { getExtensionStart } from './utils'; -import { translate } from '../../../helpers/l10n'; import getStore from '../../utils/getStore'; +import { getExtensionStart } from '../../../helpers/extensions'; import { addGlobalErrorMessage } from '../../../store/globalMessages'; +import { translate } from '../../../helpers/l10n'; import { Store, getCurrentUser } from '../../../store/rootReducer'; +import { Location, Router, withRouter } from '../../../components/hoc/withRouter'; -interface OwnProps { - extension: { key: string; name: string }; - options?: {}; -} - -interface StateProps { +interface Props extends InjectedIntlProps { currentUser: T.CurrentUser; -} - -interface DispatchProps { + extension: T.Extension; + location: Location; onFail: (message: string) => void; + options?: T.Dict<any>; + router: Router; } -type Props = OwnProps & WithRouterProps & InjectedIntlProps & StateProps & DispatchProps; - interface State { extensionElement?: React.ReactElement<any>; } -class Extension extends React.PureComponent<Props, State> { +export class Extension extends React.PureComponent<Props, State> { container?: HTMLElement | null; stop?: Function; state: State = {}; @@ -93,8 +87,7 @@ class Extension extends React.PureComponent<Props, State> { }; startExtension() { - const { extension } = this.props; - getExtensionStart(extension.key).then(this.handleStart, this.handleFailure); + getExtensionStart(this.props.extension.key).then(this.handleStart, this.handleFailure); } stopExtension() { @@ -118,13 +111,10 @@ class Extension extends React.PureComponent<Props, State> { } } -function mapStateToProps(state: Store): StateProps { - return { currentUser: getCurrentUser(state) }; -} - -const mapDispatchToProps: DispatchProps = { onFail: addGlobalErrorMessage }; +const mapStateToProps = (state: Store) => ({ currentUser: getCurrentUser(state) }); +const mapDispatchToProps = { onFail: addGlobalErrorMessage }; -export default injectIntl<OwnProps & InjectedIntlProps>( +export default injectIntl( withRouter( connect( mapStateToProps, diff --git a/server/sonar-web/src/main/js/app/components/extensions/__tests__/Extension-test.tsx b/server/sonar-web/src/main/js/app/components/extensions/__tests__/Extension-test.tsx new file mode 100644 index 00000000000..42e27ab3a3e --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/extensions/__tests__/Extension-test.tsx @@ -0,0 +1,59 @@ +/* + * 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 * as React from 'react'; +import { shallow } from 'enzyme'; +import { Extension } from '../Extension'; +import { mockCurrentUser, mockLocation, mockRouter } from '../../../../helpers/testMocks'; +import { getExtensionStart } from '../../../../helpers/extensions'; +import { waitAndUpdate } from '../../../../helpers/testUtils'; + +jest.mock('../../../../helpers/extensions', () => ({ + getExtensionStart: jest.fn().mockResolvedValue(jest.fn()) +})); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +it('should render extension correctly', async () => { + const start = jest.fn().mockReturnValue(<div className="extension" />); + (getExtensionStart as jest.Mock).mockResolvedValue(start); + + const wrapper = shallowRender(); + expect(wrapper).toMatchSnapshot(); + expect(getExtensionStart).toBeCalledWith('foo'); + await waitAndUpdate(wrapper); + expect(start).toBeCalled(); + expect(wrapper).toMatchSnapshot(); +}); + +function shallowRender(props: Partial<Extension['props']> = {}) { + return shallow( + <Extension + currentUser={mockCurrentUser()} + extension={{ key: 'foo', name: 'Foo' }} + intl={{} as any} + location={mockLocation()} + onFail={jest.fn()} + router={mockRouter()} + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/Extension-test.tsx.snap b/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/Extension-test.tsx.snap new file mode 100644 index 00000000000..39b613e960d --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/Extension-test.tsx.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render extension correctly 1`] = ` +<div> + <HelmetWrapper + defer={true} + encodeSpecialCharacters={true} + title="Foo" + /> + <div /> +</div> +`; + +exports[`should render extension correctly 2`] = ` +<div> + <HelmetWrapper + defer={true} + encodeSpecialCharacters={true} + title="Foo" + /> + <div + className="extension" + /> +</div> +`; diff --git a/server/sonar-web/src/main/js/app/components/extensions/utils.ts b/server/sonar-web/src/main/js/app/components/extensions/utils.ts deleted file mode 100644 index 4d6d3d3dff1..00000000000 --- a/server/sonar-web/src/main/js/app/components/extensions/utils.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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 exposeLibraries from './exposeLibraries'; -import { getExtensionFromCache } from '../../utils/installExtensionsHandler'; -import { getBaseUrl } from '../../../helpers/urls'; - -function installScript(key: string) { - return new Promise(resolve => { - exposeLibraries(); - const scriptTag = document.createElement('script'); - scriptTag.src = `${getBaseUrl()}/static/${key}.js`; - scriptTag.onload = resolve; - document.getElementsByTagName('body')[0].appendChild(scriptTag); - }); -} - -export function getExtensionStart(key: string): Promise<Function> { - return new Promise((resolve, reject) => { - const fromCache = getExtensionFromCache(key); - if (fromCache) { - resolve(fromCache); - return; - } - - installScript(key).then(() => { - const start = getExtensionFromCache(key); - if (start) { - resolve(start); - } else { - reject(); - } - }, reject); - }); -} diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx index 01401510728..ab374580b9d 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx @@ -22,13 +22,13 @@ import { Link, withRouter, WithRouterProps } from 'react-router'; import CreateFormShim from '../../../../apps/portfolio/components/CreateFormShim'; import Dropdown from '../../../../components/controls/Dropdown'; import PlusIcon from '../../../../components/icons-components/PlusIcon'; -import { getExtensionStart } from '../../extensions/utils'; +import { OnboardingContextShape } from '../../OnboardingContext'; import { getComponentNavigation } from '../../../../api/nav'; +import { getExtensionStart } from '../../../../helpers/extensions'; import { translate } from '../../../../helpers/l10n'; import { isSonarCloud } from '../../../../helpers/system'; import { getPortfolioAdminUrl, getPortfolioUrl } from '../../../../helpers/urls'; import { hasGlobalPermission } from '../../../../helpers/users'; -import { OnboardingContextShape } from '../../OnboardingContext'; interface Props { appState: Pick<T.AppState, 'qualifiers'>; diff --git a/server/sonar-web/src/main/js/app/index.ts b/server/sonar-web/src/main/js/app/index.ts index 60375997e7c..8fd4bc68a9d 100644 --- a/server/sonar-web/src/main/js/app/index.ts +++ b/server/sonar-web/src/main/js/app/index.ts @@ -17,13 +17,14 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import installExtensionsHandler from './utils/installExtensionsHandler'; import { installGlobal, DEFAULT_LANGUAGE, requestMessages } from '../helpers/l10n'; +import { installExtensionsHandler, installWebAnalyticsHandler } from '../helpers/extensions'; import { request, parseJSON } from '../helpers/request'; import { getSystemStatus } from '../helpers/system'; import './styles/sonar.css'; installGlobal(); +installWebAnalyticsHandler(); if (isMainApp()) { installExtensionsHandler(); diff --git a/server/sonar-web/src/main/js/app/types.d.ts b/server/sonar-web/src/main/js/app/types.d.ts index 7ee7ed04cdd..685f85fc51c 100644 --- a/server/sonar-web/src/main/js/app/types.d.ts +++ b/server/sonar-web/src/main/js/app/types.d.ts @@ -102,6 +102,7 @@ declare namespace T { settings: T.Dict<string>; standalone?: boolean; version: string; + webAnalyticsJsPath?: string; } export interface Branch { diff --git a/server/sonar-web/src/main/js/apps/create/components/UpgradeOrganizationModal.tsx b/server/sonar-web/src/main/js/apps/create/components/UpgradeOrganizationModal.tsx index 27467f3420f..4184908eb4d 100644 --- a/server/sonar-web/src/main/js/apps/create/components/UpgradeOrganizationModal.tsx +++ b/server/sonar-web/src/main/js/apps/create/components/UpgradeOrganizationModal.tsx @@ -19,14 +19,14 @@ */ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import UpgradeOrganizationAdvantages from './UpgradeOrganizationAdvantages'; import BillingFormShim from './BillingFormShim'; +import UpgradeOrganizationAdvantages from './UpgradeOrganizationAdvantages'; import DeferredSpinner from '../../../components/common/DeferredSpinner'; import Modal from '../../../components/controls/Modal'; import { ResetButtonLink } from '../../../components/ui/buttons'; -import { getExtensionStart } from '../../../app/components/extensions/utils'; -import { translate } from '../../../helpers/l10n'; import { withCurrentUser } from '../../../components/hoc/withCurrentUser'; +import { getExtensionStart } from '../../../helpers/extensions'; +import { translate } from '../../../helpers/l10n'; const BillingForm = withCurrentUser(BillingFormShim); diff --git a/server/sonar-web/src/main/js/apps/create/components/__tests__/UpgradeOrganizationModal-test.tsx b/server/sonar-web/src/main/js/apps/create/components/__tests__/UpgradeOrganizationModal-test.tsx index e98729fb38e..292f352afe5 100644 --- a/server/sonar-web/src/main/js/apps/create/components/__tests__/UpgradeOrganizationModal-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/components/__tests__/UpgradeOrganizationModal-test.tsx @@ -20,10 +20,10 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import UpgradeOrganizationModal from '../UpgradeOrganizationModal'; -import { getExtensionStart } from '../../../../app/components/extensions/utils'; +import { getExtensionStart } from '../../../../helpers/extensions'; import { waitAndUpdate } from '../../../../helpers/testUtils'; -jest.mock('../../../../app/components/extensions/utils', () => ({ +jest.mock('../../../../helpers/extensions', () => ({ getExtensionStart: jest.fn().mockResolvedValue(undefined) })); diff --git a/server/sonar-web/src/main/js/apps/create/organization/PlanStep.tsx b/server/sonar-web/src/main/js/apps/create/organization/PlanStep.tsx index 3d23116b344..84d88a5e8a8 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/PlanStep.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/PlanStep.tsx @@ -24,7 +24,7 @@ import DeferredSpinner from '../../../components/common/DeferredSpinner'; import Step from '../../tutorials/components/Step'; import { SubmitButton } from '../../../components/ui/buttons'; import { withCurrentUser } from '../../../components/hoc/withCurrentUser'; -import { getExtensionStart } from '../../../app/components/extensions/utils'; +import { getExtensionStart } from '../../../helpers/extensions'; import { translate } from '../../../helpers/l10n'; const BillingForm = withCurrentUser(BillingFormShim); diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanStep-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanStep-test.tsx index 6baad0adb27..58a2f95af78 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanStep-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanStep-test.tsx @@ -24,10 +24,9 @@ import { Plan } from '../PlanSelect'; import { mockAlmOrganization } from '../../../../helpers/testMocks'; import { waitAndUpdate, submit } from '../../../../helpers/testUtils'; -jest.mock('../../../../app/components/extensions/utils', () => ({ +jest.mock('../../../../helpers/extensions', () => ({ getExtensionStart: jest.fn().mockResolvedValue(undefined) })); - const subscriptionPlans = [{ maxNcloc: 1000, price: 100 }]; it('should render and use free plan', async () => { diff --git a/server/sonar-web/src/main/js/helpers/__tests__/__snapshots__/extensions-test.ts.snap b/server/sonar-web/src/main/js/helpers/__tests__/__snapshots__/extensions-test.ts.snap new file mode 100644 index 00000000000..f535eda398a --- /dev/null +++ b/server/sonar-web/src/main/js/helpers/__tests__/__snapshots__/extensions-test.ts.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`installScript should add the given script in the dom 1`] = `"<script src=\\"custom_script.js\\"></script>"`; diff --git a/server/sonar-web/src/main/js/helpers/__tests__/extensions-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/extensions-test.ts new file mode 100644 index 00000000000..72ece2f775b --- /dev/null +++ b/server/sonar-web/src/main/js/helpers/__tests__/extensions-test.ts @@ -0,0 +1,90 @@ +/* + * 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 { + getExtensionFromCache, + getWebAnalyticsPageHandlerFromCache, + getExtensionStart, + installExtensionsHandler, + installScript, + installWebAnalyticsHandler +} from '../extensions'; +import exposeLibraries from '../../app/components/extensions/exposeLibraries'; + +jest.mock('../../app/components/extensions/exposeLibraries', () => ({ + default: jest.fn() +})); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('installExtensionsHandler & extensions.getExtensionFromCache', () => { + it('should register the global "registerExtension" function and retrieve extension', () => { + expect((window as any).registerExtension).toBeUndefined(); + installExtensionsHandler(); + expect((window as any).registerExtension).toEqual(expect.any(Function)); + + const start = jest.fn(); + (window as any).registerExtension('foo', start); + expect(getExtensionFromCache('foo')).toBe(start); + }); +}); + +describe('setWebAnalyticsPageChangeHandler & getWebAnalyticsPageHandlerFromCache', () => { + it('should register the global "setWebAnalyticsPageChangeHandler" function and retrieve analytics extension', () => { + expect((window as any).setWebAnalyticsPageChangeHandler).toBeUndefined(); + installWebAnalyticsHandler(); + expect((window as any).setWebAnalyticsPageChangeHandler).toEqual(expect.any(Function)); + + const pageChange = jest.fn(); + (window as any).setWebAnalyticsPageChangeHandler(pageChange); + expect(getWebAnalyticsPageHandlerFromCache()).toBe(pageChange); + }); +}); + +describe('installScript', () => { + it('should add the given script in the dom', () => { + installScript('custom_script.js'); + expect(document.body.innerHTML).toMatchSnapshot(); + }); +}); + +describe('getExtensionStart', () => { + it('should install the extension in the to dom', () => { + const start = jest.fn(); + const scriptTag = document.createElement('script'); + document.createElement = jest.fn().mockReturnValue(scriptTag); + installExtensionsHandler(); + + const result = getExtensionStart('bar'); + (window as any).registerExtension('bar', start); + (scriptTag.onload as Function)(); + + expect(exposeLibraries).toBeCalled(); + return expect(result).resolves.toBe(start); + }); + + it('should get the extension from the cache', () => { + const start = jest.fn(); + installExtensionsHandler(); + (window as any).registerExtension('baz', start); + return expect(getExtensionStart('baz')).resolves.toBe(start); + }); +}); diff --git a/server/sonar-web/src/main/js/helpers/extensions.ts b/server/sonar-web/src/main/js/helpers/extensions.ts new file mode 100644 index 00000000000..66a56cf3ed0 --- /dev/null +++ b/server/sonar-web/src/main/js/helpers/extensions.ts @@ -0,0 +1,73 @@ +/* + * 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 { getBaseUrl } from './urls'; +import exposeLibraries from '../app/components/extensions/exposeLibraries'; + +const WEB_ANALYTICS_EXTENSION = 'sq-web-analytics'; +const extensions: T.Dict<Function> = {}; + +function registerExtension(key: string, start: Function) { + extensions[key] = start; +} + +function setWebAnalyticsPageChangeHandler(pageHandler: (pathname: string) => void) { + registerExtension(WEB_ANALYTICS_EXTENSION, pageHandler); +} + +export function installExtensionsHandler() { + (window as any).registerExtension = registerExtension; +} + +export function installWebAnalyticsHandler() { + (window as any).setWebAnalyticsPageChangeHandler = setWebAnalyticsPageChangeHandler; +} + +export function getExtensionFromCache(key: string): Function | undefined { + return extensions[key]; +} + +export function getWebAnalyticsPageHandlerFromCache(): Function | undefined { + return extensions[WEB_ANALYTICS_EXTENSION]; +} + +export function installScript(url: string, target: 'body' | 'head' = 'body'): Promise<any> { + return new Promise(resolve => { + const scriptTag = document.createElement('script'); + scriptTag.src = `${getBaseUrl()}${url}`; + scriptTag.onload = resolve; + document.getElementsByTagName(target)[0].appendChild(scriptTag); + }); +} + +export async function getExtensionStart(key: string) { + const fromCache = getExtensionFromCache(key); + if (fromCache) { + return Promise.resolve(fromCache); + } + + exposeLibraries(); + await installScript(`/static/${key}.js`); + + const start = getExtensionFromCache(key); + if (start) { + return start; + } + return Promise.reject(); +} diff --git a/server/sonar-web/yarn.lock b/server/sonar-web/yarn.lock index d693976e8b5..fbb636f62db 100644 --- a/server/sonar-web/yarn.lock +++ b/server/sonar-web/yarn.lock @@ -8281,11 +8281,6 @@ react-fast-compare@^1.0.0: resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-1.0.0.tgz#813a039155e49b43ceffe99528fe5e9d97a6c938" integrity sha512-dcQpdWr62flXQJuM8/bVEY5/10ad2SYBUafp8H4q4WHR3fTA/MMlp8mpzX12I0CCoEJc1P6QdiMg7U+7lFS6Rw== -react-ga@2.5.7: - version "2.5.7" - resolved "https://registry.yarnpkg.com/react-ga/-/react-ga-2.5.7.tgz#1c80a289004bf84f84c26d46f3a6a6513081bf2e" - integrity sha512-UmATFaZpEQDO96KFjB5FRLcT6hFcwaxOmAJZnjrSiFN/msTqylq9G+z5Z8TYzN/dbamDTiWf92m6MnXXJkAivQ== - react-helmet@5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/react-helmet/-/react-helmet-5.2.0.tgz#a81811df21313a6d55c5f058c4aeba5d6f3d97a7" |