summaryrefslogtreecommitdiff
path: root/qa/qa/runtime/script_extensions/interceptor.js
diff options
context:
space:
mode:
Diffstat (limited to 'qa/qa/runtime/script_extensions/interceptor.js')
-rw-r--r--qa/qa/runtime/script_extensions/interceptor.js158
1 files changed, 158 insertions, 0 deletions
diff --git a/qa/qa/runtime/script_extensions/interceptor.js b/qa/qa/runtime/script_extensions/interceptor.js
new file mode 100644
index 00000000000..9e98b0421b4
--- /dev/null
+++ b/qa/qa/runtime/script_extensions/interceptor.js
@@ -0,0 +1,158 @@
+(() => {
+ const CACHE_NAME = 'INTERCEPTOR_CACHE';
+
+ /**
+ * Fetches and parses JSON from the sessionStorage cache
+ * @returns {(Object)}
+ */
+ const getCache = () => {
+ return JSON.parse(sessionStorage.getItem(CACHE_NAME));
+ };
+
+ /**
+ * Commits an object to the sessionStorage cache
+ * @param {Object} data
+ */
+ const saveCache = (data) => {
+ sessionStorage.setItem(CACHE_NAME, JSON.stringify(data));
+ };
+
+ /**
+ * Checks if the cache is available
+ * and if the current context has access to it
+ * @returns {boolean} can we access the cache?
+ */
+ const checkCache = () => {
+ try {
+ getCache();
+ return true;
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.warn(`Couldn't access cache: ${error.toString()}`);
+ return false;
+ }
+ };
+
+ /**
+ * @callback cacheCommitCallback
+ * @param {object} cache
+ * @return {object} mutated cache
+ */
+
+ /**
+ * If the cache is available, takes a callback function that is called
+ * with an object returned from getCache,
+ * and saves whatever is returned from the callback function
+ * to the cache
+ * @param {cacheCommitCallback} cb
+ */
+ const commitToCache = (cb) => {
+ if (checkCache()) {
+ const cache = cb(getCache());
+ saveCache(cache);
+ }
+ };
+
+ window.Interceptor = {
+ saveCache,
+ commitToCache,
+ getCache,
+ checkCache,
+ activeFetchRequests: 0,
+ };
+
+ const pureFetch = window.fetch;
+ const pureXHROpen = window.XMLHttpRequest.prototype.open;
+
+ /**
+ * Replacement for XMLHttpRequest.prototype.open
+ * listens for complete xhr events
+ * if the xhr response has a status code higher than 400
+ * then commit request/response metadata to the cache
+ * @param method intercepted HTTP method (GET|POST|etc..)
+ * @param url intercepted HTTP url
+ * @param args intercepted XHR arguments (credentials, headers, options
+ * @return {Promise} the result of the original XMLHttpRequest.prototype.open implementation
+ */
+ function interceptXhr(method, url, ...args) {
+ this.addEventListener(
+ 'readystatechange',
+ () => {
+ const self = this;
+ if (this.readyState === XMLHttpRequest.DONE) {
+ if (this.status >= 400 || this.status === 0) {
+ commitToCache((cache) => {
+ // eslint-disable-next-line no-param-reassign
+ cache.errors ||= [];
+ cache.errors.push({
+ status: self.status === 0 ? -1 : self.status,
+ url,
+ method,
+ headers: { 'x-request-id': self.getResponseHeader('x-request-id') },
+ });
+ return cache;
+ });
+ }
+ }
+ },
+ false,
+ );
+ return pureXHROpen.apply(this, [method, url, ...args]);
+ }
+
+ /**
+ * Replacement for fetch implementation
+ * tracks active requests, and commits metadata to the cache
+ * if the response is not ok or was cancelled.
+ * Additionally tracks activeFetchRequests on the Interceptor object
+ * @param url target HTTP url
+ * @param opts fetch options, including request method, body, etc
+ * @param args additional fetch arguments
+ * @returns {Promise<"success"|"error">} the result of the original fetch call
+ */
+ async function interceptedFetch(url, opts, ...args) {
+ const method = opts && opts.method ? opts.method : 'GET';
+ window.Interceptor.activeFetchRequests += 1;
+ try {
+ const response = await pureFetch(url, opts, ...args);
+ window.Interceptor.activeFetchRequests += -1;
+ const clone = response.clone();
+
+ if (!clone.ok) {
+ commitToCache((cache) => {
+ // eslint-disable-next-line no-param-reassign
+ cache.errors ||= [];
+ cache.errors.push({
+ status: clone.status,
+ url,
+ method,
+ headers: { 'x-request-id': clone.headers.get('x-request-id') },
+ });
+ return cache;
+ });
+ }
+ return response;
+ } catch (error) {
+ commitToCache((cache) => {
+ // eslint-disable-next-line no-param-reassign
+ cache.errors ||= [];
+ cache.errors.push({
+ status: -1,
+ url,
+ method,
+ });
+ return cache;
+ });
+
+ window.Interceptor.activeFetchRequests += -1;
+ throw error;
+ }
+ }
+
+ if (checkCache()) {
+ saveCache({});
+ }
+
+ window.fetch = interceptedFetch;
+ window.XMLHttpRequest.prototype.open = interceptXhr;
+})();