@bugspotter/sdk
Version:
Professional bug reporting SDK with screenshots, session replay, and automatic error capture for web applications
246 lines (245 loc) • 8.29 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.compressData = compressData;
exports.decompressData = decompressData;
exports.compressImage = compressImage;
exports.getCompressionRatio = getCompressionRatio;
exports.estimateSize = estimateSize;
exports.resetCompressionCache = resetCompressionCache;
const pako_1 = __importDefault(require("pako"));
const logger_1 = require("../utils/logger");
/**
* Compression utilities for BugSpotter SDK
* Handles payload and image compression using browser-native APIs
*/
const logger = (0, logger_1.getLogger)();
// Configuration constants
const COMPRESSION_DEFAULTS = {
GZIP_LEVEL: 6, // Balanced speed/size ratio (0-9)
IMAGE_MAX_WIDTH: 1920,
IMAGE_MAX_HEIGHT: 1080,
IMAGE_WEBP_QUALITY: 0.8,
IMAGE_JPEG_QUALITY: 0.85,
IMAGE_LOAD_TIMEOUT: 3000, // milliseconds
};
// Singleton instances for performance
let textEncoder = null;
let textDecoder = null;
let webpSupportCache = null;
/**
* Get or create TextEncoder instance
*/
function getTextEncoder() {
if (!textEncoder) {
textEncoder = new TextEncoder();
}
return textEncoder;
}
/**
* Get or create TextDecoder instance
*/
function getTextDecoder() {
if (!textDecoder) {
textDecoder = new TextDecoder();
}
return textDecoder;
}
/**
* Convert data to string representation
*/
function dataToString(data) {
return typeof data === 'string' ? data : JSON.stringify(data);
}
/**
* Compress JSON or string data using gzip
* @param data - Data to compress (will be JSON stringified if object)
* @param config - Optional compression configuration
* @returns Compressed data as Uint8Array
*/
async function compressData(data, config) {
var _a;
try {
const jsonString = dataToString(data);
const encoder = getTextEncoder();
const uint8Data = encoder.encode(jsonString);
const gzipLevel = (_a = config === null || config === void 0 ? void 0 : config.gzipLevel) !== null && _a !== void 0 ? _a : COMPRESSION_DEFAULTS.GZIP_LEVEL;
// pako.gzip already returns Uint8Array, no need to wrap it
const compressed = pako_1.default.gzip(uint8Data, { level: gzipLevel });
return compressed;
}
catch (error) {
logger.error('Compression failed:', error);
throw error;
}
}
/**
* Try to parse string as JSON, return string if not valid JSON
*/
function tryParseJSON(jsonString) {
try {
return JSON.parse(jsonString);
}
catch (_a) {
return jsonString;
}
}
/**
* Decompress gzipped data back to original format
* Useful for testing and verification
* @param compressed - Compressed Uint8Array data
* @param config - Optional configuration
* @returns Decompressed and parsed data (or string if input was string)
*/
function decompressData(compressed, config) {
try {
const decompressed = pako_1.default.ungzip(compressed);
const decoder = getTextDecoder();
const jsonString = decoder.decode(decompressed);
return tryParseJSON(jsonString);
}
catch (error) {
if ((config === null || config === void 0 ? void 0 : config.verbose) !== false) {
(0, logger_1.getLogger)().error('Decompression failed:', error);
}
throw new Error(`Failed to decompress data: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Check if code is running in browser environment
*/
function isBrowserEnvironment() {
return (typeof document !== 'undefined' &&
typeof Image !== 'undefined' &&
typeof HTMLCanvasElement !== 'undefined');
}
/**
* Check if browser supports WebP format (cached result)
*/
function supportsWebP() {
if (webpSupportCache !== null) {
return webpSupportCache;
}
if (!isBrowserEnvironment()) {
webpSupportCache = false;
return false;
}
try {
const canvas = document.createElement('canvas');
webpSupportCache = canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0;
}
catch (_a) {
webpSupportCache = false;
}
return webpSupportCache;
}
/**
* Load image from base64 string with timeout
*/
function loadImage(base64, timeout) {
return new Promise((resolve, reject) => {
const img = new Image();
const timer = setTimeout(() => {
reject(new Error(`Image load timeout after ${timeout}ms`));
}, timeout);
img.onload = () => {
clearTimeout(timer);
resolve(img);
};
img.onerror = () => {
clearTimeout(timer);
reject(new Error('Failed to load image'));
};
img.src = base64;
});
}
/**
* Calculate resized dimensions maintaining aspect ratio
*/
function calculateResizedDimensions(width, height, maxWidth, maxHeight) {
let newWidth = width;
let newHeight = height;
if (newWidth > maxWidth) {
newHeight = (newHeight * maxWidth) / newWidth;
newWidth = maxWidth;
}
if (newHeight > maxHeight) {
newWidth = (newWidth * maxHeight) / newHeight;
newHeight = maxHeight;
}
return { width: newWidth, height: newHeight };
}
/**
* Optimize and compress screenshot image
* Converts to WebP if supported, resizes if too large, then compresses
* @param base64 - Base64 encoded image (PNG or other format)
* @param config - Optional compression configuration
* @returns Optimized base64 image string
*/
async function compressImage(base64, config) {
var _a, _b, _c, _d;
try {
if (!isBrowserEnvironment()) {
return base64;
}
const maxWidth = (_a = config === null || config === void 0 ? void 0 : config.imageMaxWidth) !== null && _a !== void 0 ? _a : COMPRESSION_DEFAULTS.IMAGE_MAX_WIDTH;
const maxHeight = (_b = config === null || config === void 0 ? void 0 : config.imageMaxHeight) !== null && _b !== void 0 ? _b : COMPRESSION_DEFAULTS.IMAGE_MAX_HEIGHT;
const webpQuality = (_c = config === null || config === void 0 ? void 0 : config.webpQuality) !== null && _c !== void 0 ? _c : COMPRESSION_DEFAULTS.IMAGE_WEBP_QUALITY;
const jpegQuality = (_d = config === null || config === void 0 ? void 0 : config.jpegQuality) !== null && _d !== void 0 ? _d : COMPRESSION_DEFAULTS.IMAGE_JPEG_QUALITY;
const timeout = COMPRESSION_DEFAULTS.IMAGE_LOAD_TIMEOUT;
const img = await loadImage(base64, timeout);
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('Failed to get 2D canvas context');
}
const { width, height } = calculateResizedDimensions(img.width, img.height, maxWidth, maxHeight);
canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0, width, height);
if (supportsWebP()) {
return canvas.toDataURL('image/webp', webpQuality);
}
else {
return canvas.toDataURL('image/jpeg', jpegQuality);
}
}
catch (error) {
if ((config === null || config === void 0 ? void 0 : config.verbose) !== false) {
(0, logger_1.getLogger)().error('Image compression failed:', error);
}
return base64;
}
}
/**
* Calculate compression ratio for analytics
* @param originalSize - Original data size in bytes
* @param compressedSize - Compressed data size in bytes
* @returns Compression ratio as percentage (0-100)
*/
function getCompressionRatio(originalSize, compressedSize) {
if (originalSize <= 0) {
return 0;
}
return Math.round((1 - compressedSize / originalSize) * 100);
}
/**
* Estimate payload size before compression
* @param data - Data to estimate
* @returns Estimated size in bytes
*/
function estimateSize(data) {
const jsonString = dataToString(data);
return getTextEncoder().encode(jsonString).length;
}
/**
* Reset cached instances (useful for testing)
* @internal
*/
function resetCompressionCache() {
textEncoder = null;
textDecoder = null;
webpSupportCache = null;
}