summaryrefslogtreecommitdiff
path: root/qa/qa/runtime/script_extensions/interceptor.js
blob: 9e98b0421b464e6d955c57e1b6c4f73f46f1bd63 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
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;
})();