import '../webpack'; import { isEmpty } from 'lodash'; import Vue from 'vue'; import axios from '~/lib/utils/axios_utils'; import { numberToHumanSize } from '~/lib/utils/number_utils'; import { s__ } from '~/locale'; import Translate from '~/vue_shared/translate'; import initPerformanceBarLog from './performance_bar_log'; import PerformanceBarService from './services/performance_bar_service'; import PerformanceBarStore from './stores/performance_bar_store'; Vue.use(Translate); const initPerformanceBar = (el) => { if (!el) { return undefined; } const performanceBarData = el.dataset; return new Vue({ el, name: 'PerformanceBarRoot', components: { PerformanceBarApp: () => import('./components/performance_bar_app.vue'), }, data() { const store = new PerformanceBarStore(); return { store, env: performanceBarData.env, requestId: performanceBarData.requestId, requestMethod: performanceBarData.requestMethod, peekUrl: performanceBarData.peekUrl, statsUrl: performanceBarData.statsUrl, }; }, mounted() { PerformanceBarService.registerInterceptor(this.peekUrl, this.addRequest); this.addRequest( this.requestId, window.location.href, undefined, undefined, this.requestMethod, ); this.loadRequestDetails(this.requestId); }, beforeDestroy() { PerformanceBarService.removeInterceptor(); }, methods: { addRequestManually(urlOrRequestId) { if (urlOrRequestId.startsWith('https://') || urlOrRequestId.startsWith('http://')) { // We don't need to do anything with the response, we just // want to trace the request. axios.get(urlOrRequestId); } else { this.addRequest(urlOrRequestId, urlOrRequestId); } }, addRequest(requestId, requestUrl, operationName, requestParams, methodVerb) { if (!this.store.canTrackRequest(requestUrl)) { return; } this.store.addRequest(requestId, requestUrl, operationName, requestParams, methodVerb); }, loadRequestDetails(requestId) { const request = this.store.findRequest(requestId); if (request && isEmpty(request.details)) { return PerformanceBarService.fetchRequestDetails(this.peekUrl, requestId) .then((res) => { this.store.addRequestDetails(requestId, res.data); if (this.requestId === requestId) this.collectFrontendPerformanceMetrics(); }) .catch(() => // eslint-disable-next-line no-console console.warn(`Error getting performance bar results for ${requestId}`), ); } return Promise.resolve(); }, collectFrontendPerformanceMetrics() { if (performance) { const navigationEntries = performance.getEntriesByType('navigation'); const paintEntries = performance.getEntriesByType('paint'); const resourceEntries = performance.getEntriesByType('resource'); let durationString = ''; let summary = {}; if (navigationEntries.length > 0) { const backend = Math.round(navigationEntries[0].responseEnd); const firstContentfulPaint = Math.round( paintEntries.find((entry) => entry.name === 'first-contentful-paint')?.startTime, ); const domContentLoaded = Math.round(navigationEntries[0].domContentLoadedEventEnd); summary = { [s__('PerformanceBar|Backend')]: backend, [s__('PerformanceBar|First Contentful Paint')]: firstContentfulPaint, [s__('PerformanceBar|DOM Content Loaded')]: domContentLoaded, }; durationString = `${backend} | ${firstContentfulPaint} | ${domContentLoaded}`; } let newEntries = resourceEntries.map(this.transformResourceEntry); this.updateFrontendPerformanceMetrics(durationString, summary, newEntries); if ('PerformanceObserver' in window) { // We start observing for more incoming timings const observer = new PerformanceObserver((list) => { newEntries = newEntries.concat(list.getEntries().map(this.transformResourceEntry)); this.updateFrontendPerformanceMetrics(durationString, summary, newEntries); }); observer.observe({ entryTypes: ['resource'] }); } } }, updateFrontendPerformanceMetrics(durationString, summary, requestEntries) { this.store.setRequestDetailsData(this.requestId, 'total', { duration: durationString, calls: requestEntries.length, details: requestEntries, summaryOptions: { hideDuration: true, }, summary, }); }, transformResourceEntry(entry) { return { start: entry.startTime, name: entry.name.replace(document.location.origin, ''), duration: Math.round(entry.duration), size: entry.transferSize ? numberToHumanSize(entry.transferSize) : 'cached', }; }, }, render(createElement) { return createElement('performance-bar-app', { props: { store: this.store, env: this.env, requestId: this.requestId, requestMethod: this.requestMethod, peekUrl: this.peekUrl, statsUrl: this.statsUrl, }, on: { 'add-request': this.addRequestManually, 'change-request': this.loadRequestDetails, }, }); }, }); }; initPerformanceBar(document.querySelector('#js-peek')); initPerformanceBarLog(); export default initPerformanceBar;