UNPKG

reactotron-react-native

Version:

A development tool to explore, inspect, and diagnose your React Native apps.

266 lines (244 loc) 8.1 kB
"use strict"; import { XHRInterceptor } from "../xhr-interceptor.js"; import { FetchInterceptor } from "../fetch-interceptor.js"; /** * Don't include the response bodies for images by default. */ const DEFAULT_CONTENT_TYPES_RX = /^(image)\/.*$/i; /** * Streaming response bodies must never be buffered for logging (it would defeat * the stream and grow memory unbounded), so we always skip their bodies. */ const STREAMING_CONTENT_TYPES_RX = /event-stream/i; const DEFAULTS = {}; /** * decodeURIComponent that falls back to the raw string on malformed * percent-encoding (e.g. `?q=%E0%A4%A`) instead of throwing. */ function tryDecodeURIComponent(value) { try { return decodeURIComponent(value); } catch (malformedUri) { return value; } } const networking = (pluginConfig = {}) => reactotron => { const options = Object.assign({}, DEFAULTS, pluginConfig); // a RegExp to suppress adding the body cuz it costs a lot to serialize const ignoreContentTypes = options.ignoreContentTypes || DEFAULT_CONTENT_TYPES_RX; // a XHR call tracker let reactotronCounter = 1000; // a temporary cache to hold requests so we can match up the data const requestCache = {}; /** * Fires when we talk to the server. * * @param {*} data - The data sent to the server. * @param {*} instance - The XMLHTTPRequest instance. */ function onSend(data, xhr) { if (options.ignoreUrls && options.ignoreUrls.test(xhr._url)) { xhr._skipReactotron = true; return; } // bump the counter reactotronCounter++; // tag xhr._trackingName = reactotronCounter; // cache requestCache[reactotronCounter] = { data, xhr, stopTimer: reactotron.startTimer() }; } /** * Fires when the server gives us a response. * * @param {number} status - The HTTP response status. * @param {boolean} timeout - Did we timeout? * @param {*} response - The response data. * @param {string} url - The URL we talked to. * @param {*} type - Not sure. * @param {*} xhr - The XMLHttpRequest instance. */ function onResponse(status, timeout, response, url, type, xhr) { if (xhr._skipReactotron) { return; } let params = null; const queryParamIdx = url ? url.indexOf("?") : -1; if (queryParamIdx > -1) { params = {}; url.substr(queryParamIdx + 1).split("&").forEach(pair => { const [key, value] = pair.split("="); if (key && value !== undefined) { params[key] = decodeURIComponent(value.replace(/\+/g, " ")); } }); } // fetch and clear the request data from the cache const rid = xhr._trackingName; const cachedRequest = requestCache[rid] || { xhr }; requestCache[rid] = null; // assemble the request object const { data, stopTimer } = cachedRequest; const tronRequest = { url: url || cachedRequest.xhr._url, method: xhr._method || null, data, headers: xhr._headers || null, params }; // what type of content is this? const contentType = xhr.responseHeaders && xhr.responseHeaders["content-type"] || xhr.responseHeaders && xhr.responseHeaders["Content-Type"] || ""; const sendResponse = responseBodyText => { let body = `~~~ skipped ~~~`; if (responseBodyText) { try { // all i am saying, is give JSON a chance... body = JSON.parse(responseBodyText); } catch (boom) { body = response; } } const tronResponse = { body, status, headers: xhr.responseHeaders || null }; reactotron.apiResponse(tronRequest, tronResponse, stopTimer ? stopTimer() : null); // TODO: Fix }; // can we use the real response? const useRealResponse = (typeof response === "string" || typeof response === "object") && !ignoreContentTypes.test(contentType || ""); // prepare the right body to send if (useRealResponse) { if (type === "blob" && typeof FileReader !== "undefined" && response) { // Disable reason: FileReader should be in global scope since RN 0.54 // eslint-disable-next-line no-undef const bReader = new FileReader(); const brListener = () => { sendResponse(bReader.result); bReader.removeEventListener("loadend", brListener); }; bReader.addEventListener("loadend", brListener); bReader.readAsText(response); } else { sendResponse(response); } } else { sendResponse(""); } } // expo/fetch request tracker (keyed by the interceptor's request id). // `null` marks a request we deliberately skipped (ignoreUrls). const fetchCache = {}; /** * Fires (synchronously) when an expo/fetch request is sent. */ function onFetchOpen(method, url, headers, data, id) { if (options.ignoreUrls && options.ignoreUrls.test(url)) { fetchCache[id] = null; return; } let params = null; const queryParamIdx = url ? url.indexOf("?") : -1; if (queryParamIdx > -1) { params = {}; url.substr(queryParamIdx + 1).split("&").forEach(pair => { const [key, value] = pair.split("="); if (key && value !== undefined) { params[key] = tryDecodeURIComponent(value.replace(/\+/g, " ")); } }); } fetchCache[id] = { tronRequest: { url, method, data, headers, params }, stopTimer: reactotron.startTimer() }; } /** * Fires (synchronously) when an expo/fetch response resolves or rejects. * The body is read off a clone, asynchronously, so the caller's response is * never blocked and streaming responses stay intact. */ function onFetchResponse(id, status, headers, response, error) { const cached = fetchCache[id]; delete fetchCache[id]; if (!cached) { return; } const { tronRequest, stopTimer } = cached; const report = body => reactotron.apiResponse(tronRequest, { body, status, headers }, stopTimer()); if (error || !response) { report(error instanceof Error ? error.message : String(error)); return; } const contentType = headers && headers["content-type"] || ""; if (ignoreContentTypes.test(contentType) || STREAMING_CONTENT_TYPES_RX.test(contentType)) { // Never read (and therefore never buffer) image or streaming bodies. report("~~~ skipped ~~~"); return; } // Clone synchronously (before the caller consumes the body), then read // asynchronously so we don't block the request. clone() itself can throw // if the body is already disturbed (e.g. another tool consumed it first). let clone; try { clone = response.clone(); } catch (cloneError) { report("~~~ unreadable ~~~"); return; } clone.text().then(text => { let body; try { // all i am saying, is give JSON a chance... body = JSON.parse(text); } catch (boom) { body = text; } report(body); }).catch(() => report("~~~ unreadable ~~~")); } return { onConnect: () => { // register our monkey-patch XHRInterceptor.setSendCallback(onSend); XHRInterceptor.setResponseCallback(onResponse); XHRInterceptor.enableInterception(); // expo/fetch (Expo SDK 56+) bypasses XHR, so instrument it too. // An explicitly passed fetch skips detection; otherwise expo/fetch is // auto-detected unless opted out. if (options.fetch) { FetchInterceptor.setOpenCallback(onFetchOpen); FetchInterceptor.setResponseCallback(onFetchResponse); FetchInterceptor.enableInterception(options.fetch); } else if (!options.ignoreExpoFetch) { FetchInterceptor.setOpenCallback(onFetchOpen); FetchInterceptor.setResponseCallback(onFetchResponse); FetchInterceptor.enableInterception(); } } }; }; export default networking; //# sourceMappingURL=networking.js.map