UNPKG

@bugspotter/sdk

Version:

Professional bug reporting SDK with screenshots, session replay, and automatic error capture for web applications

245 lines (244 loc) 14.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.BugReportModal = exports.FloatingButton = exports.validatePattern = exports.getPatternsByCategory = exports.getPattern = exports.createPatternConfig = exports.PatternBuilder = exports.PATTERN_CATEGORIES = exports.PATTERN_PRESETS = exports.DEFAULT_PATTERNS = exports.Sanitizer = exports.createSanitizer = exports.isWithinSizeLimit = exports.estimateCompressedReplaySize = exports.canvasToBlob = exports.compressReplayEvents = exports.DirectUploader = exports.createLogger = exports.configureLogger = exports.getLogger = exports.clearOfflineQueue = exports.getAuthHeaders = exports.submitWithAuth = exports.getCompressionRatio = exports.estimateSize = exports.compressImage = exports.decompressData = exports.compressData = exports.CircularBuffer = exports.DOMCollector = exports.MetadataCapture = exports.NetworkCapture = exports.ConsoleCapture = exports.ScreenshotCapture = exports.BugSpotter = void 0; exports.sanitize = sanitize; const screenshot_1 = require("./capture/screenshot"); const console_1 = require("./capture/console"); const network_1 = require("./capture/network"); const metadata_1 = require("./capture/metadata"); const compress_1 = require("./core/compress"); const button_1 = require("./widget/button"); const modal_1 = require("./widget/modal"); const collectors_1 = require("./collectors"); const sanitize_1 = require("./utils/sanitize"); const logger_1 = require("./utils/logger"); const transport_1 = require("./core/transport"); const logger = (0, logger_1.getLogger)(); class BugSpotter { constructor(config) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k; this.config = config; // Initialize sanitizer if enabled if (((_a = config.sanitize) === null || _a === void 0 ? void 0 : _a.enabled) !== false) { this.sanitizer = (0, sanitize_1.createSanitizer)({ enabled: (_c = (_b = config.sanitize) === null || _b === void 0 ? void 0 : _b.enabled) !== null && _c !== void 0 ? _c : true, patterns: (_d = config.sanitize) === null || _d === void 0 ? void 0 : _d.patterns, customPatterns: (_e = config.sanitize) === null || _e === void 0 ? void 0 : _e.customPatterns, excludeSelectors: (_f = config.sanitize) === null || _f === void 0 ? void 0 : _f.excludeSelectors, }); } this.screenshot = new screenshot_1.ScreenshotCapture(); this.console = new console_1.ConsoleCapture({ sanitizer: this.sanitizer }); this.network = new network_1.NetworkCapture({ sanitizer: this.sanitizer }); this.metadata = new metadata_1.MetadataCapture({ sanitizer: this.sanitizer }); // Note: DirectUploader is created per-report since it needs bugId // See submitBugReport() for initialization // Initialize DOM collector if replay is enabled if (((_g = config.replay) === null || _g === void 0 ? void 0 : _g.enabled) !== false) { this.domCollector = new collectors_1.DOMCollector({ duration: (_j = (_h = config.replay) === null || _h === void 0 ? void 0 : _h.duration) !== null && _j !== void 0 ? _j : 15, sampling: (_k = config.replay) === null || _k === void 0 ? void 0 : _k.sampling, sanitizer: this.sanitizer, }); this.domCollector.startRecording(); } // Initialize widget if enabled if (config.showWidget !== false) { this.widget = new button_1.FloatingButton(config.widgetOptions); this.widget.onClick(async () => { await this.handleBugReport(); }); } } static init(config) { if (!BugSpotter.instance) { BugSpotter.instance = new BugSpotter(config); } return BugSpotter.instance; } static getInstance() { return BugSpotter.instance || null; } /** * Capture bug report data * Note: Screenshot is captured for modal preview only (_screenshotPreview) * Actual file uploads use presigned URLs (screenshotKey/replayKey set after upload) */ async capture() { var _a, _b; const screenshotPreview = await this.screenshot.capture(); const replayEvents = (_b = (_a = this.domCollector) === null || _a === void 0 ? void 0 : _a.getEvents()) !== null && _b !== void 0 ? _b : []; return { console: this.console.getLogs(), network: this.network.getRequests(), metadata: this.metadata.capture(), replay: replayEvents, // Internal: screenshot preview for modal (not sent to API) _screenshotPreview: screenshotPreview, }; } async handleBugReport() { const report = await this.capture(); const modal = new modal_1.BugReportModal({ onSubmit: async (data) => { logger.log('Submitting bug:', Object.assign(Object.assign({}, data), { report })); // Send to endpoint if configured if (this.config.endpoint) { try { await this.submitBugReport(Object.assign(Object.assign({}, data), { report })); logger.log('Bug report submitted successfully'); } catch (error) { logger.error('Failed to submit bug report:', error); // Re-throw to allow UI to handle errors if needed throw error; } } }, }); modal.show(report._screenshotPreview || ''); } async submitBugReport(payload) { if (!this.config.endpoint) { throw new Error('No endpoint configured for bug report submission'); } const contentHeaders = { 'Content-Type': 'application/json', }; logger.warn(`Submitting bug report to ${this.config.endpoint}`); let body; try { // Try to compress the payload const originalSize = (0, compress_1.estimateSize)(payload); const compressed = await (0, compress_1.compressData)(payload); const compressedSize = compressed.byteLength; const ratio = (0, compress_1.getCompressionRatio)(originalSize, compressedSize); logger.log(`Payload compression: ${(originalSize / 1024).toFixed(1)}KB → ${(compressedSize / 1024).toFixed(1)}KB (${ratio}% reduction)`); // Use compression if it actually reduces size if (compressedSize < originalSize) { // Create a Blob from the compressed Uint8Array for proper binary upload // Use Uint8Array constructor to ensure clean ArrayBuffer (no extra padding bytes) body = new Blob([new Uint8Array(compressed)], { type: 'application/gzip' }); contentHeaders['Content-Encoding'] = 'gzip'; contentHeaders['Content-Type'] = 'application/gzip'; } else { body = JSON.stringify(payload); } } catch (error) { // Fallback to uncompressed if compression fails logger.warn('Compression failed, sending uncompressed payload:', error); body = JSON.stringify(payload); } // Determine auth configuration const auth = this.config.auth; // Submit with authentication, retry logic, and offline queue const response = await (0, transport_1.submitWithAuth)(this.config.endpoint, body, contentHeaders, { auth, retry: this.config.retry, offline: this.config.offline, }); logger.warn(`${JSON.stringify(response)}`); if (!response.ok) { const errorText = await response.text().catch(() => { return 'Unknown error'; }); throw new Error(`Failed to submit bug report: ${response.status} ${response.statusText}. ${errorText}`); } return response.json().catch(() => { return undefined; }); } getConfig() { return Object.assign({}, this.config); } destroy() { var _a, _b; this.console.destroy(); this.network.destroy(); (_a = this.domCollector) === null || _a === void 0 ? void 0 : _a.destroy(); (_b = this.widget) === null || _b === void 0 ? void 0 : _b.destroy(); BugSpotter.instance = undefined; } } exports.BugSpotter = BugSpotter; var screenshot_2 = require("./capture/screenshot"); Object.defineProperty(exports, "ScreenshotCapture", { enumerable: true, get: function () { return screenshot_2.ScreenshotCapture; } }); var console_2 = require("./capture/console"); Object.defineProperty(exports, "ConsoleCapture", { enumerable: true, get: function () { return console_2.ConsoleCapture; } }); var network_2 = require("./capture/network"); Object.defineProperty(exports, "NetworkCapture", { enumerable: true, get: function () { return network_2.NetworkCapture; } }); var metadata_2 = require("./capture/metadata"); Object.defineProperty(exports, "MetadataCapture", { enumerable: true, get: function () { return metadata_2.MetadataCapture; } }); // Export collector modules var collectors_2 = require("./collectors"); Object.defineProperty(exports, "DOMCollector", { enumerable: true, get: function () { return collectors_2.DOMCollector; } }); // Export core utilities var buffer_1 = require("./core/buffer"); Object.defineProperty(exports, "CircularBuffer", { enumerable: true, get: function () { return buffer_1.CircularBuffer; } }); // Export compression utilities var compress_2 = require("./core/compress"); Object.defineProperty(exports, "compressData", { enumerable: true, get: function () { return compress_2.compressData; } }); Object.defineProperty(exports, "decompressData", { enumerable: true, get: function () { return compress_2.decompressData; } }); Object.defineProperty(exports, "compressImage", { enumerable: true, get: function () { return compress_2.compressImage; } }); Object.defineProperty(exports, "estimateSize", { enumerable: true, get: function () { return compress_2.estimateSize; } }); Object.defineProperty(exports, "getCompressionRatio", { enumerable: true, get: function () { return compress_2.getCompressionRatio; } }); // Export transport and authentication var transport_2 = require("./core/transport"); Object.defineProperty(exports, "submitWithAuth", { enumerable: true, get: function () { return transport_2.submitWithAuth; } }); Object.defineProperty(exports, "getAuthHeaders", { enumerable: true, get: function () { return transport_2.getAuthHeaders; } }); Object.defineProperty(exports, "clearOfflineQueue", { enumerable: true, get: function () { return transport_2.clearOfflineQueue; } }); var logger_2 = require("./utils/logger"); Object.defineProperty(exports, "getLogger", { enumerable: true, get: function () { return logger_2.getLogger; } }); Object.defineProperty(exports, "configureLogger", { enumerable: true, get: function () { return logger_2.configureLogger; } }); Object.defineProperty(exports, "createLogger", { enumerable: true, get: function () { return logger_2.createLogger; } }); // Export upload utilities var uploader_1 = require("./core/uploader"); Object.defineProperty(exports, "DirectUploader", { enumerable: true, get: function () { return uploader_1.DirectUploader; } }); var upload_helpers_1 = require("./core/upload-helpers"); Object.defineProperty(exports, "compressReplayEvents", { enumerable: true, get: function () { return upload_helpers_1.compressReplayEvents; } }); Object.defineProperty(exports, "canvasToBlob", { enumerable: true, get: function () { return upload_helpers_1.canvasToBlob; } }); Object.defineProperty(exports, "estimateCompressedReplaySize", { enumerable: true, get: function () { return upload_helpers_1.estimateCompressedReplaySize; } }); Object.defineProperty(exports, "isWithinSizeLimit", { enumerable: true, get: function () { return upload_helpers_1.isWithinSizeLimit; } }); // Export sanitization utilities var sanitize_2 = require("./utils/sanitize"); Object.defineProperty(exports, "createSanitizer", { enumerable: true, get: function () { return sanitize_2.createSanitizer; } }); Object.defineProperty(exports, "Sanitizer", { enumerable: true, get: function () { return sanitize_2.Sanitizer; } }); // Export pattern configuration utilities var sanitize_3 = require("./utils/sanitize"); Object.defineProperty(exports, "DEFAULT_PATTERNS", { enumerable: true, get: function () { return sanitize_3.DEFAULT_PATTERNS; } }); Object.defineProperty(exports, "PATTERN_PRESETS", { enumerable: true, get: function () { return sanitize_3.PATTERN_PRESETS; } }); Object.defineProperty(exports, "PATTERN_CATEGORIES", { enumerable: true, get: function () { return sanitize_3.PATTERN_CATEGORIES; } }); Object.defineProperty(exports, "PatternBuilder", { enumerable: true, get: function () { return sanitize_3.PatternBuilder; } }); Object.defineProperty(exports, "createPatternConfig", { enumerable: true, get: function () { return sanitize_3.createPatternConfig; } }); Object.defineProperty(exports, "getPattern", { enumerable: true, get: function () { return sanitize_3.getPattern; } }); Object.defineProperty(exports, "getPatternsByCategory", { enumerable: true, get: function () { return sanitize_3.getPatternsByCategory; } }); Object.defineProperty(exports, "validatePattern", { enumerable: true, get: function () { return sanitize_3.validatePattern; } }); // Export widget components var button_2 = require("./widget/button"); Object.defineProperty(exports, "FloatingButton", { enumerable: true, get: function () { return button_2.FloatingButton; } }); var modal_2 = require("./widget/modal"); Object.defineProperty(exports, "BugReportModal", { enumerable: true, get: function () { return modal_2.BugReportModal; } }); /** * Convenience function to sanitize text with default PII patterns * Useful for quick sanitization without creating a Sanitizer instance * * @param text - Text to sanitize * @returns Sanitized text with PII redacted * * @example * ```typescript * const sanitized = sanitize('Email: user@example.com'); * // Returns: 'Email: [REDACTED]' * ``` */ function sanitize(text) { const sanitizer = (0, sanitize_1.createSanitizer)({ enabled: true, patterns: 'all', customPatterns: [], excludeSelectors: [], }); return sanitizer.sanitize(text); }