@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
JavaScript
;
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);
}