@critters/next
Version:
Secure bug reporting library for Next.js applications
392 lines (391 loc) • 14.8 kB
JavaScript
;
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();
}