UNPKG

@critters/next

Version:

Secure bug reporting library for Next.js applications

392 lines (391 loc) 14.8 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.startConsoleCapture = startConsoleCapture; exports.stopConsoleCapture = stopConsoleCapture; exports.startNetworkCapture = startNetworkCapture; exports.stopNetworkCapture = stopNetworkCapture; exports.startUserInteractionCapture = startUserInteractionCapture; exports.getConsoleLogs = getConsoleLogs; exports.getNetworkRequests = getNetworkRequests; exports.getUserInteractions = getUserInteractions; exports.getBrowserData = getBrowserData; exports.clearCapturedData = clearCapturedData; exports.startAllCollectors = startAllCollectors; exports.stopAllCollectors = stopAllCollectors; const utils_1 = require("./utils"); const config_1 = require("./config"); // Store for captured console logs const consoleLogs = []; // Store for captured network requests const networkRequests = []; // Store for user interactions const userInteractions = { user_path: [], }; // Original console methods let originalConsole = null; // Original fetch method let originalFetch = null; // Original XMLHttpRequest prototype methods let originalXHROpen = null; let originalXHRSend = null; /** * Start capturing console logs */ function startConsoleCapture() { if (!(0, utils_1.isBrowser)() || originalConsole) return; const config = config_1.BugTrackerConfig.getInstance(); if (!config.shouldCaptureConsole()) return; // Store original methods originalConsole = { log: console.log, info: console.info, warn: console.warn, error: console.error, }; // Override console methods console.log = function (...args) { const message = args .map((arg) => typeof arg === "object" ? JSON.stringify(arg) : String(arg)) .join(" "); consoleLogs.push({ level: "log", message, timestamp: (0, utils_1.getTimestamp)(), }); // Call original method originalConsole.log.apply(console, args); }; console.info = function (...args) { const message = args .map((arg) => typeof arg === "object" ? JSON.stringify(arg) : String(arg)) .join(" "); consoleLogs.push({ level: "info", message, timestamp: (0, utils_1.getTimestamp)(), }); // Call original method originalConsole.info.apply(console, args); }; console.warn = function (...args) { const message = args .map((arg) => typeof arg === "object" ? JSON.stringify(arg) : String(arg)) .join(" "); consoleLogs.push({ level: "warn", message, timestamp: (0, utils_1.getTimestamp)(), }); // Call original method originalConsole.warn.apply(console, args); }; console.error = function (...args) { const message = args .map((arg) => typeof arg === "object" ? JSON.stringify(arg) : String(arg)) .join(" "); consoleLogs.push({ level: "error", message, timestamp: (0, utils_1.getTimestamp)(), }); // Call original method originalConsole.error.apply(console, args); }; } /** * Stop capturing console logs */ function stopConsoleCapture() { if (!(0, utils_1.isBrowser)() || !originalConsole) return; // Restore original methods console.log = originalConsole.log; console.info = originalConsole.info; console.warn = originalConsole.warn; console.error = originalConsole.error; originalConsole = null; } /** * Start capturing network requests */ function startNetworkCapture() { if (!(0, utils_1.isBrowser)()) return; const config = config_1.BugTrackerConfig.getInstance(); if (!config.shouldCaptureNetworkRequests()) return; // Capture fetch requests if (!originalFetch && window.fetch) { originalFetch = window.fetch; window.fetch = function (input, init) { return __awaiter(this, void 0, void 0, function* () { const startTime = Date.now(); let url; let method; // Handle different input types if (typeof input === "string") { url = input; method = (init === null || init === void 0 ? void 0 : init.method) || "GET"; } else if (input instanceof URL) { url = input.toString(); method = (init === null || init === void 0 ? void 0 : init.method) || "GET"; } else { // input is a Request object url = input.url; method = (init === null || init === void 0 ? void 0 : init.method) || input.method || "GET"; } // Create network request entry const requestEntry = { url, method, request_headers: (init === null || init === void 0 ? void 0 : init.headers) ? Object.fromEntries(Object.entries(init.headers)) : undefined, request_body: (init === null || init === void 0 ? void 0 : init.body) ? JSON.stringify(init.body) : undefined, }; try { const response = yield originalFetch.call(window, input, init); const duration = Date.now() - startTime; // Clone the response to read its body const clonedResponse = response.clone(); let responseBody; try { const contentType = response.headers.get("content-type"); if (contentType && contentType.includes("application/json")) { responseBody = yield clonedResponse.json(); } else if (contentType && contentType.includes("text/")) { responseBody = yield clonedResponse.text(); } } catch (e) { // Ignore errors when reading response body } // Update request entry with response data requestEntry.status = response.status; requestEntry.duration = duration; requestEntry.response_headers = Object.fromEntries(Array.from(response.headers.entries())); requestEntry.response_body = responseBody; networkRequests.push(requestEntry); return response; } catch (error) { // Update request entry with error information requestEntry.status = 0; requestEntry.duration = Date.now() - startTime; requestEntry.response_body = { error: String(error) }; networkRequests.push(requestEntry); throw error; } }); }; } // Capture XMLHttpRequest if (!originalXHROpen && window.XMLHttpRequest) { originalXHROpen = XMLHttpRequest.prototype.open; originalXHRSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function (method, url) { // Store request data on the XHR instance this.__requestData = { method, url, startTime: 0, request_headers: {}, }; return originalXHROpen.apply(this, arguments); }; // Override setRequestHeader to capture request headers const originalSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader; XMLHttpRequest.prototype.setRequestHeader = function (name, value) { if (this.__requestData) { this.__requestData.request_headers[name] = value; } return originalSetRequestHeader.apply(this, arguments); }; XMLHttpRequest.prototype.send = function (body) { const xhr = this; const requestData = xhr.__requestData; if (requestData) { requestData.startTime = Date.now(); requestData.request_body = body ? String(body) : undefined; // Add event listener to capture response xhr.addEventListener("loadend", function () { const duration = Date.now() - requestData.startTime; // Get response headers const responseHeaders = {}; const headerString = xhr.getAllResponseHeaders(); const headerPairs = headerString.split("\r\n"); for (const headerPair of headerPairs) { if (headerPair.trim()) { const [name, value] = headerPair.split(": "); responseHeaders[name] = value; } } // Try to parse response body let responseBody; try { const contentType = xhr.getResponseHeader("content-type"); if (contentType && contentType.includes("application/json")) { responseBody = JSON.parse(xhr.responseText); } else { responseBody = xhr.responseText; } } catch (e) { responseBody = xhr.responseText; } // Create network request entry networkRequests.push({ url: requestData.url, method: requestData.method, status: xhr.status, duration, request_headers: requestData.request_headers, response_headers: responseHeaders, request_body: requestData.request_body, response_body: responseBody, }); }); } return originalXHRSend.apply(this, arguments); }; } } /** * Stop capturing network requests */ function stopNetworkCapture() { if (!(0, utils_1.isBrowser)()) return; // Restore original fetch if (originalFetch) { window.fetch = originalFetch; originalFetch = null; } // Restore original XMLHttpRequest methods if (originalXHROpen && originalXHRSend) { XMLHttpRequest.prototype.open = originalXHROpen; XMLHttpRequest.prototype.send = originalXHRSend; originalXHROpen = null; originalXHRSend = null; } } /** * Start capturing user interactions */ function startUserInteractionCapture() { if (!(0, utils_1.isBrowser)()) return; const config = config_1.BugTrackerConfig.getInstance(); if (!config.shouldCaptureUserInteractions()) return; // Track page navigation if (window.history) { const originalPushState = window.history.pushState; const originalReplaceState = window.history.replaceState; window.history.pushState = function () { const result = originalPushState.apply(this, arguments); // Record navigation recordUserAction("navigation", window.location.pathname); userInteractions.user_path.push(window.location.pathname); return result; }; window.history.replaceState = function () { const result = originalReplaceState.apply(this, arguments); // Record navigation recordUserAction("navigation", window.location.pathname); return result; }; // Listen for popstate event window.addEventListener("popstate", () => { recordUserAction("navigation", window.location.pathname); userInteractions.user_path.push(window.location.pathname); }); } // Track clicks window.addEventListener("click", (event) => { const target = event.target; const tagName = target.tagName.toLowerCase(); const id = target.id; const className = target.className; recordUserAction("click", `${tagName}${id ? `#${id}` : ""}${className ? `.${className}` : ""}`); }); // Track form submissions window.addEventListener("submit", (event) => { const form = event.target; const id = form.id; const action = form.action; recordUserAction("form_submit", `form${id ? `#${id}` : ""} -> ${action}`); }); } /** * Record a user action */ function recordUserAction(action, details) { userInteractions.last_action = `${action}${details ? `: ${details}` : ""}`; userInteractions.last_action_timestamp = (0, utils_1.getTimestamp)(); } /** * Get captured console logs */ function getConsoleLogs() { return [...consoleLogs]; } /** * Get captured network requests */ function getNetworkRequests() { return [...networkRequests]; } /** * Get captured user interactions */ function getUserInteractions() { return Object.assign({}, userInteractions); } /** * Get browser information */ function getBrowserData() { return (0, utils_1.getBrowserInfo)(); } /** * Clear all captured data */ function clearCapturedData() { consoleLogs.length = 0; networkRequests.length = 0; userInteractions.user_path = []; userInteractions.last_action = undefined; userInteractions.last_action_timestamp = undefined; } /** * Start all data collectors */ function startAllCollectors() { startConsoleCapture(); startNetworkCapture(); startUserInteractionCapture(); } /** * Stop all data collectors */ function stopAllCollectors() { stopConsoleCapture(); stopNetworkCapture(); }