Commit 7954f78c authored by Tim Zallmann's avatar Tim Zallmann

Catching startup calls with axios interceptors

Backend optimizations for startup Calls

Removed also the condition in head
Fix for application_helper spec startup_calls
parent 47a66706
import { isEmpty } from 'lodash';
import { mergeUrlParams } from './url_utility';
// We should probably not couple this utility to `gon.gitlab_url`
// Also, this would replace occurrences that aren't at the beginning of the string
const removeGitLabUrl = url => url.replace(gon.gitlab_url, '');
const getFullUrl = req => {
const url = removeGitLabUrl(req.url);
return mergeUrlParams(req.params || {}, url);
};
const setupAxiosStartupCalls = axios => {
const { startup_calls: startupCalls } = window.gl || {};
if (!startupCalls || isEmpty(startupCalls)) {
return;
}
// TODO: To save performance of future axios calls, we can
// remove this interceptor once the "startupCalls" have been loaded
axios.interceptors.request.use(req => {
const fullUrl = getFullUrl(req);
const existing = startupCalls[fullUrl];
if (existing) {
// eslint-disable-next-line no-param-reassign
req.adapter = () =>
existing.fetchCall.then(res =>
// eslint-disable-next-line promise/no-nesting
res.json().then(data => ({
data,
status: res.status,
statusText: res.statusText,
headers: res.headers,
config: req,
request: req,
})),
);
}
return req;
});
};
export default setupAxiosStartupCalls;
import axios from 'axios';
import csrf from './csrf';
import suppressAjaxErrorsDuringNavigation from './suppress_ajax_errors_during_navigation';
import setupAxiosStartupCalls from './axios_startup_calls';
axios.defaults.headers.common[csrf.headerKey] = csrf.token;
// Used by Rails to check if it is a valid XHR request
......@@ -14,6 +15,8 @@ axios.interceptors.request.use(config => {
return config;
});
setupAxiosStartupCalls(axios);
// Remove the global counter
axios.interceptors.response.use(
response => {
......
......@@ -29,15 +29,6 @@ const defaultClient = createDefaultClient(
});
},
readme(_, { url }) {
// Unfortunately hacky way of URL matching
const startupCall =
gl.startup_calls[`${url.replace(gon.gitlab_url, '')}?viewer=rich&format=json`];
if (startupCall?.fetchCall) {
return startupCall.fetchCall
.then(response => response.json())
.then(data => ({ ...data, __typename: 'ReadmeFile' }));
}
return axios
.get(url, { params: { viewer: 'rich', format: 'json' } })
.then(({ data }) => ({ ...data, __typename: 'ReadmeFile' }));
......
......@@ -25,49 +25,34 @@ export function fetchLogsTree(client, path, offset, resolver = null) {
const { projectPath } = client.readQuery({ query: getProjectPath });
const { escapedRef } = client.readQuery({ query: getRef });
const apiUrl = `${
gon.relative_url_root
}/${projectPath}/-/refs/${escapedRef}/logs_tree/${encodeURIComponent(path.replace(/^\//, ''))}`;
const parseLogsTreeResult = (data, headers) => {
const headerLogsOffset = headers['more-logs-offset'];
const { commits } = client.readQuery({ query: getCommits });
const newCommitData = [...commits, ...normalizeData(data, path)];
client.writeQuery({
query: getCommits,
data: { commits: newCommitData },
});
resolvers.forEach(r => resolveCommit(newCommitData, path, r));
fetchpromise = null;
fetchpromise = axios
.get(
`${gon.relative_url_root}/${projectPath}/-/refs/${escapedRef}/logs_tree/${encodeURIComponent(
path.replace(/^\//, ''),
)}`,
{
params: { format: 'json', offset },
},
)
.then(({ data, headers }) => {
const headerLogsOffset = headers['more-logs-offset'];
const { commits } = client.readQuery({ query: getCommits });
const newCommitData = [...commits, ...normalizeData(data, path)];
client.writeQuery({
query: getCommits,
data: { commits: newCommitData },
});
if (headerLogsOffset) {
fetchLogsTree(client, path, headerLogsOffset);
} else {
resolvers = [];
}
};
resolvers.forEach(r => resolveCommit(newCommitData, path, r));
// Checking if the startup call was already fired (Hacky URL Setup right now)
if (offset === '0' && gl?.startup_calls[`${apiUrl}?format=json&offset=0`]) {
fetchpromise = gl.startup_calls[`${apiUrl}?format=json&offset=0`].fetchCall;
let headers;
return fetchpromise
.then(response => {
headers = response.headers;
return response.json();
})
.then(data => {
parseLogsTreeResult(data, headers);
});
}
fetchpromise = null;
fetchpromise = axios
.get(apiUrl, {
params: { format: 'json', offset },
})
.then(({ data, headers }) => parseLogsTreeResult(data, headers));
if (headerLogsOffset) {
fetchLogsTree(client, path, headerLogsOffset);
} else {
resolvers = [];
}
});
return fetchpromise;
}
......@@ -339,9 +339,9 @@ module ApplicationHelper
@api_startup_calls
end
def add_page_startup_api_call(api_url)
def add_page_startup_api_call(api_path, options: {})
@api_startup_calls ||= {}
@api_startup_calls[api_url] = ""
@api_startup_calls[api_path] = options
end
def autocomplete_data_sources(object, noteable_type)
......
......@@ -25,8 +25,7 @@
%meta{ 'http-equiv' => 'X-UA-Compatible', content: 'IE=edge' }
- if page_startup_api_calls
= render 'layouts/startup_js'
= render 'layouts/startup_js'
-# Open Graph - http://ogp.me/
%meta{ property: 'og:type', content: "object" }
......
- if page_startup_api_calls
= javascript_tag nonce: true do
:plain
var gl = window.gl || {};
gl.startup_calls = #{page_startup_api_calls.to_json};
if (gl.startup_calls && window.fetch) {
Object.keys(gl.startup_calls).forEach(apiCall => {
gl.startup_calls[apiCall] = {
fetchCall: fetch(apiCall)
};
});
}
- return unless page_startup_api_calls.present?
= javascript_tag nonce: true do
:plain
var gl = window.gl || {};
gl.startup_calls = #{page_startup_api_calls.to_json};
if (gl.startup_calls && window.fetch) {
Object.keys(gl.startup_calls).forEach(apiCall => {
gl.startup_calls[apiCall] = {
fetchCall: fetch(apiCall)
};
});
}
......@@ -4,7 +4,7 @@
- project = local_assigns.fetch(:project) { @project }
- content_url = local_assigns.fetch(:content_url) { @tree.readme ? project_blob_path(@project, tree_join(@ref, @tree.readme.path)) : project_tree_path(@project, @ref) }
- show_auto_devops_callout = show_auto_devops_callout?(@project)
- add_page_startup_api_call "#{project_path(@project)}/-/refs/#{ref}/logs_tree/#{@path}?format=json&offset=0"
- add_page_startup_api_call logs_file_project_ref_path(@project, ref, @path, format: "json", offset: 0)
- if @tree.readme
- add_page_startup_api_call "#{project_blob_path(@project, tree_join(@ref, @tree.readme.path))}?viewer=rich&format=json"
......
......@@ -2,10 +2,6 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import { resolveCommit, fetchLogsTree } from '~/repository/log_tree';
window.gl = window.gl || {
startup_calls: {},
};
const mockData = [
{
commit: {
......@@ -19,17 +15,6 @@ const mockData = [
},
];
const expectedObject = {
__typename: 'LogTreeCommit',
commitPath: 'https://test.com',
committedDate: '2019-01-01',
fileName: 'index.js',
filePath: '/index.js',
message: 'testing message',
sha: '123',
type: 'blob',
};
describe('resolveCommit', () => {
it('calls resolve when commit found', () => {
const resolver = {
......@@ -99,34 +84,20 @@ describe('fetchLogsTree', () => {
expect(axios.get.mock.calls.length).toEqual(1);
}));
describe('using startupCall', () => {
const responseMock = {
status: 200,
headers: {},
json: () => new Promise(resolve => resolve(mockData)),
};
afterAll(() => {
window.gl.startup_calls = {};
});
it('uses startup call fetch when it is not done yet', () => {
window.gl.startup_calls = {
'/gitlab-org/gitlab-foss/-/refs/master/logs_tree/?format=json&offset=0': {
fetchCall: new Promise(resolve => resolve(responseMock)),
},
};
return fetchLogsTree(client, '', '0', resolver).then(() => {
expect(resolver.resolve).toHaveBeenCalledWith(expect.objectContaining(expectedObject));
expect(axios.get.mock.calls.length).toEqual(0);
});
});
});
it('calls entry resolver', () =>
fetchLogsTree(client, '', '0', resolver).then(() => {
expect(resolver.resolve).toHaveBeenCalledWith(expect.objectContaining(expectedObject));
expect(resolver.resolve).toHaveBeenCalledWith(
expect.objectContaining({
__typename: 'LogTreeCommit',
commitPath: 'https://test.com',
committedDate: '2019-01-01',
fileName: 'index.js',
filePath: '/index.js',
message: 'testing message',
sha: '123',
type: 'blob',
}),
);
}));
it('writes query to client', () =>
......@@ -134,7 +105,18 @@ describe('fetchLogsTree', () => {
expect(client.writeQuery).toHaveBeenCalledWith({
query: expect.anything(),
data: {
commits: [expect.objectContaining(expectedObject)],
commits: [
expect.objectContaining({
__typename: 'LogTreeCommit',
commitPath: 'https://test.com',
committedDate: '2019-01-01',
fileName: 'index.js',
filePath: '/index.js',
message: 'testing message',
sha: '123',
type: 'blob',
}),
],
},
});
}));
......
......@@ -212,8 +212,10 @@ describe ApplicationHelper do
describe '#page_startup_api_calls' do
it 'returns map containing JS Page Startup Calls' do
helper.add_page_startup_api_call("testURL")
startup_calls = helper.page_startup_api_calls
expect(startup_calls["testURL"]).to eq("")
expect(startup_calls["testURL"]).to eq({})
end
end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment