@bugspotter/sdk 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +69 -0
- package/LICENSE +21 -0
- package/README.md +639 -0
- package/dist/bugspotter.min.js +2 -0
- package/dist/bugspotter.min.js.LICENSE.txt +14 -0
- package/dist/capture/base-capture.d.ts +34 -0
- package/dist/capture/base-capture.js +23 -0
- package/dist/capture/capture-lifecycle.d.ts +24 -0
- package/dist/capture/capture-lifecycle.js +2 -0
- package/dist/capture/console.d.ts +29 -0
- package/dist/capture/console.js +107 -0
- package/dist/capture/metadata.d.ts +21 -0
- package/dist/capture/metadata.js +76 -0
- package/dist/capture/network.d.ts +32 -0
- package/dist/capture/network.js +135 -0
- package/dist/capture/screenshot.d.ts +19 -0
- package/dist/capture/screenshot.js +52 -0
- package/dist/collectors/dom.d.ts +67 -0
- package/dist/collectors/dom.js +164 -0
- package/dist/collectors/index.d.ts +2 -0
- package/dist/collectors/index.js +5 -0
- package/dist/core/buffer.d.ts +50 -0
- package/dist/core/buffer.js +88 -0
- package/dist/core/circular-buffer.d.ts +42 -0
- package/dist/core/circular-buffer.js +77 -0
- package/dist/core/compress.d.ts +49 -0
- package/dist/core/compress.js +245 -0
- package/dist/core/offline-queue.d.ts +76 -0
- package/dist/core/offline-queue.js +301 -0
- package/dist/core/transport.d.ts +73 -0
- package/dist/core/transport.js +352 -0
- package/dist/core/upload-helpers.d.ts +32 -0
- package/dist/core/upload-helpers.js +79 -0
- package/dist/core/uploader.d.ts +70 -0
- package/dist/core/uploader.js +185 -0
- package/dist/index.d.ts +140 -0
- package/dist/index.esm.js +205 -0
- package/dist/index.js +244 -0
- package/dist/utils/logger.d.ts +28 -0
- package/dist/utils/logger.js +84 -0
- package/dist/utils/sanitize-patterns.d.ts +103 -0
- package/dist/utils/sanitize-patterns.js +282 -0
- package/dist/utils/sanitize.d.ts +73 -0
- package/dist/utils/sanitize.js +254 -0
- package/dist/widget/button.d.ts +33 -0
- package/dist/widget/button.js +143 -0
- package/dist/widget/components/dom-element-cache.d.ts +62 -0
- package/dist/widget/components/dom-element-cache.js +105 -0
- package/dist/widget/components/form-validator.d.ts +66 -0
- package/dist/widget/components/form-validator.js +115 -0
- package/dist/widget/components/pii-detection-display.d.ts +64 -0
- package/dist/widget/components/pii-detection-display.js +142 -0
- package/dist/widget/components/redaction-canvas.d.ts +95 -0
- package/dist/widget/components/redaction-canvas.js +230 -0
- package/dist/widget/components/screenshot-processor.d.ts +44 -0
- package/dist/widget/components/screenshot-processor.js +191 -0
- package/dist/widget/components/style-manager.d.ts +37 -0
- package/dist/widget/components/style-manager.js +296 -0
- package/dist/widget/components/template-manager.d.ts +66 -0
- package/dist/widget/components/template-manager.js +198 -0
- package/dist/widget/modal.d.ts +62 -0
- package/dist/widget/modal.js +299 -0
- package/docs/CDN.md +213 -0
- package/docs/FRAMEWORK_INTEGRATION.md +1104 -0
- package/docs/PUBLISHING.md +550 -0
- package/docs/SESSION_REPLAY.md +381 -0
- package/package.json +90 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* DirectUploader
|
|
4
|
+
* Handles direct client-to-storage uploads using presigned URLs
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.DirectUploader = void 0;
|
|
8
|
+
/**
|
|
9
|
+
* DirectUploader handles uploading files directly to storage using presigned URLs
|
|
10
|
+
* This bypasses the API server for file data, reducing memory usage and improving performance
|
|
11
|
+
*/
|
|
12
|
+
class DirectUploader {
|
|
13
|
+
constructor(config) {
|
|
14
|
+
this.config = config;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Upload a screenshot file directly to storage
|
|
18
|
+
* @param file - Screenshot file or Blob
|
|
19
|
+
* @param onProgress - Optional progress callback
|
|
20
|
+
* @returns Upload result with storage key
|
|
21
|
+
*/
|
|
22
|
+
async uploadScreenshot(file, onProgress) {
|
|
23
|
+
return this.uploadFile(file, 'screenshot', 'screenshot.png', onProgress);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Upload a compressed session replay directly to storage
|
|
27
|
+
* @param compressedData - Gzip-compressed replay data
|
|
28
|
+
* @param onProgress - Optional progress callback
|
|
29
|
+
* @returns Upload result with storage key
|
|
30
|
+
*/
|
|
31
|
+
async uploadReplay(compressedData, onProgress) {
|
|
32
|
+
return this.uploadFile(compressedData, 'replay', 'replay.gz', onProgress);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Upload an attachment file directly to storage
|
|
36
|
+
* @param file - Attachment file
|
|
37
|
+
* @param onProgress - Optional progress callback
|
|
38
|
+
* @returns Upload result with storage key
|
|
39
|
+
*/
|
|
40
|
+
async uploadAttachment(file, onProgress) {
|
|
41
|
+
return this.uploadFile(file, 'attachment', file.name, onProgress);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Generic file upload method
|
|
45
|
+
* 1. Request presigned URL from API
|
|
46
|
+
* 2. Upload file directly to storage using presigned URL
|
|
47
|
+
* 3. Confirm upload with API
|
|
48
|
+
*/
|
|
49
|
+
async uploadFile(file, fileType, filename, onProgress) {
|
|
50
|
+
try {
|
|
51
|
+
// Step 1: Get presigned upload URL
|
|
52
|
+
const presignedUrlResponse = await this.requestPresignedUrl(fileType, filename);
|
|
53
|
+
if (!presignedUrlResponse.success) {
|
|
54
|
+
return {
|
|
55
|
+
success: false,
|
|
56
|
+
error: presignedUrlResponse.error || 'Failed to get presigned URL',
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const { uploadUrl, storageKey } = presignedUrlResponse.data;
|
|
60
|
+
// Step 2: Upload file to storage using presigned URL
|
|
61
|
+
const uploadSuccess = await this.uploadToStorage(uploadUrl, file, onProgress);
|
|
62
|
+
if (!uploadSuccess) {
|
|
63
|
+
return {
|
|
64
|
+
success: false,
|
|
65
|
+
error: 'Failed to upload file to storage',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// Step 3: Confirm upload with API
|
|
69
|
+
const confirmSuccess = await this.confirmUpload(fileType);
|
|
70
|
+
if (!confirmSuccess) {
|
|
71
|
+
return {
|
|
72
|
+
success: false,
|
|
73
|
+
error: 'Failed to confirm upload',
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
success: true,
|
|
78
|
+
storageKey,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
return {
|
|
83
|
+
success: false,
|
|
84
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Request a presigned URL from the API
|
|
90
|
+
*/
|
|
91
|
+
async requestPresignedUrl(fileType, filename) {
|
|
92
|
+
try {
|
|
93
|
+
const response = await fetch(`${this.config.apiEndpoint}/api/v1/uploads/presigned-url`, {
|
|
94
|
+
method: 'POST',
|
|
95
|
+
headers: {
|
|
96
|
+
'Content-Type': 'application/json',
|
|
97
|
+
'x-api-key': this.config.apiKey,
|
|
98
|
+
},
|
|
99
|
+
body: JSON.stringify({
|
|
100
|
+
projectId: this.config.projectId,
|
|
101
|
+
bugId: this.config.bugId,
|
|
102
|
+
fileType,
|
|
103
|
+
filename,
|
|
104
|
+
}),
|
|
105
|
+
});
|
|
106
|
+
if (!response.ok) {
|
|
107
|
+
const errorText = await response.text();
|
|
108
|
+
return {
|
|
109
|
+
success: false,
|
|
110
|
+
error: `HTTP ${response.status}: ${errorText}`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const result = await response.json();
|
|
114
|
+
return {
|
|
115
|
+
success: result.success,
|
|
116
|
+
data: result.data,
|
|
117
|
+
error: result.error,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
return {
|
|
122
|
+
success: false,
|
|
123
|
+
error: error instanceof Error ? error.message : 'Network error',
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Upload file to storage using presigned URL
|
|
129
|
+
* Uses XMLHttpRequest for progress tracking
|
|
130
|
+
*/
|
|
131
|
+
uploadToStorage(uploadUrl, file, onProgress) {
|
|
132
|
+
return new Promise((resolve) => {
|
|
133
|
+
const xhr = new XMLHttpRequest();
|
|
134
|
+
// Track upload progress
|
|
135
|
+
if (onProgress) {
|
|
136
|
+
xhr.upload.addEventListener('progress', (event) => {
|
|
137
|
+
if (event.lengthComputable) {
|
|
138
|
+
onProgress({
|
|
139
|
+
loaded: event.loaded,
|
|
140
|
+
total: event.total,
|
|
141
|
+
percentage: Math.round((event.loaded / event.total) * 100),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
// Handle completion
|
|
147
|
+
xhr.addEventListener('load', () => {
|
|
148
|
+
resolve(xhr.status >= 200 && xhr.status < 300);
|
|
149
|
+
});
|
|
150
|
+
// Handle errors
|
|
151
|
+
xhr.addEventListener('error', () => {
|
|
152
|
+
resolve(false);
|
|
153
|
+
});
|
|
154
|
+
xhr.addEventListener('abort', () => {
|
|
155
|
+
resolve(false);
|
|
156
|
+
});
|
|
157
|
+
// Send file
|
|
158
|
+
xhr.open('PUT', uploadUrl);
|
|
159
|
+
xhr.setRequestHeader('Content-Type', file.type || 'application/octet-stream');
|
|
160
|
+
xhr.send(file);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Confirm successful upload with the API
|
|
165
|
+
*/
|
|
166
|
+
async confirmUpload(fileType) {
|
|
167
|
+
try {
|
|
168
|
+
const response = await fetch(`${this.config.apiEndpoint}/api/v1/reports/${this.config.bugId}/confirm-upload`, {
|
|
169
|
+
method: 'POST',
|
|
170
|
+
headers: {
|
|
171
|
+
'Content-Type': 'application/json',
|
|
172
|
+
'x-api-key': this.config.apiKey,
|
|
173
|
+
},
|
|
174
|
+
body: JSON.stringify({
|
|
175
|
+
fileType,
|
|
176
|
+
}),
|
|
177
|
+
});
|
|
178
|
+
return response.ok;
|
|
179
|
+
}
|
|
180
|
+
catch (_a) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
exports.DirectUploader = DirectUploader;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { BrowserMetadata } from './capture/metadata';
|
|
2
|
+
import { type FloatingButtonOptions } from './widget/button';
|
|
3
|
+
import type { eventWithTime } from '@rrweb/types';
|
|
4
|
+
import { type AuthConfig, type RetryConfig } from './core/transport';
|
|
5
|
+
import type { OfflineConfig } from './core/offline-queue';
|
|
6
|
+
export declare class BugSpotter {
|
|
7
|
+
private static instance;
|
|
8
|
+
private config;
|
|
9
|
+
private screenshot;
|
|
10
|
+
private console;
|
|
11
|
+
private network;
|
|
12
|
+
private metadata;
|
|
13
|
+
private domCollector?;
|
|
14
|
+
private widget?;
|
|
15
|
+
private sanitizer?;
|
|
16
|
+
constructor(config: BugSpotterConfig);
|
|
17
|
+
static init(config: BugSpotterConfig): BugSpotter;
|
|
18
|
+
static getInstance(): BugSpotter | null;
|
|
19
|
+
/**
|
|
20
|
+
* Capture bug report data
|
|
21
|
+
* Note: Screenshot is captured for modal preview only (_screenshotPreview)
|
|
22
|
+
* Actual file uploads use presigned URLs (screenshotKey/replayKey set after upload)
|
|
23
|
+
*/
|
|
24
|
+
capture(): Promise<BugReport>;
|
|
25
|
+
private handleBugReport;
|
|
26
|
+
private submitBugReport;
|
|
27
|
+
getConfig(): Readonly<BugSpotterConfig>;
|
|
28
|
+
destroy(): void;
|
|
29
|
+
}
|
|
30
|
+
export interface BugSpotterConfig {
|
|
31
|
+
endpoint?: string;
|
|
32
|
+
showWidget?: boolean;
|
|
33
|
+
widgetOptions?: FloatingButtonOptions;
|
|
34
|
+
/** Authentication configuration */
|
|
35
|
+
auth?: AuthConfig;
|
|
36
|
+
/** Retry configuration for failed requests */
|
|
37
|
+
retry?: RetryConfig;
|
|
38
|
+
/** Offline queue configuration */
|
|
39
|
+
offline?: OfflineConfig;
|
|
40
|
+
replay?: {
|
|
41
|
+
/** Enable session replay recording (default: true) */
|
|
42
|
+
enabled?: boolean;
|
|
43
|
+
/** Duration in seconds to keep replay events (default: 15, max recommended: 30) */
|
|
44
|
+
duration?: number;
|
|
45
|
+
/** Sampling configuration for performance optimization */
|
|
46
|
+
sampling?: {
|
|
47
|
+
/** Throttle mousemove events in milliseconds (default: 50) */
|
|
48
|
+
mousemove?: number;
|
|
49
|
+
/** Throttle scroll events in milliseconds (default: 100) */
|
|
50
|
+
scroll?: number;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
sanitize?: {
|
|
54
|
+
/** Enable PII sanitization (default: true) */
|
|
55
|
+
enabled?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* PII patterns to detect and mask
|
|
58
|
+
* - Can be a preset name: 'all', 'minimal', 'financial', 'contact', 'gdpr', 'pci', etc.
|
|
59
|
+
* - Or an array of pattern names: ['email', 'phone', 'ip']
|
|
60
|
+
*/
|
|
61
|
+
patterns?: 'all' | 'minimal' | 'financial' | 'contact' | 'identification' | 'kazakhstan' | 'gdpr' | 'pci' | Array<'email' | 'phone' | 'creditcard' | 'ssn' | 'iin' | 'ip' | 'custom'>;
|
|
62
|
+
/** Custom regex patterns for PII detection */
|
|
63
|
+
customPatterns?: Array<{
|
|
64
|
+
name: string;
|
|
65
|
+
regex: RegExp;
|
|
66
|
+
description?: string;
|
|
67
|
+
examples?: string[];
|
|
68
|
+
priority?: number;
|
|
69
|
+
}>;
|
|
70
|
+
/** CSS selectors to exclude from sanitization */
|
|
71
|
+
excludeSelectors?: string[];
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export interface BugReportPayload {
|
|
75
|
+
title: string;
|
|
76
|
+
description: string;
|
|
77
|
+
report: BugReport;
|
|
78
|
+
}
|
|
79
|
+
export interface BugReport {
|
|
80
|
+
screenshotKey?: string;
|
|
81
|
+
console: Array<{
|
|
82
|
+
level: string;
|
|
83
|
+
message: string;
|
|
84
|
+
timestamp: number;
|
|
85
|
+
stack?: string;
|
|
86
|
+
}>;
|
|
87
|
+
network: Array<{
|
|
88
|
+
url: string;
|
|
89
|
+
method: string;
|
|
90
|
+
status: number;
|
|
91
|
+
duration: number;
|
|
92
|
+
timestamp: number;
|
|
93
|
+
error?: string;
|
|
94
|
+
}>;
|
|
95
|
+
metadata: BrowserMetadata;
|
|
96
|
+
replay?: eventWithTime[];
|
|
97
|
+
replayKey?: string;
|
|
98
|
+
_screenshotPreview?: string;
|
|
99
|
+
}
|
|
100
|
+
export type { BrowserMetadata } from './capture/metadata';
|
|
101
|
+
export { ScreenshotCapture } from './capture/screenshot';
|
|
102
|
+
export { ConsoleCapture } from './capture/console';
|
|
103
|
+
export { NetworkCapture } from './capture/network';
|
|
104
|
+
export { MetadataCapture } from './capture/metadata';
|
|
105
|
+
export { DOMCollector } from './collectors';
|
|
106
|
+
export type { DOMCollectorConfig } from './collectors';
|
|
107
|
+
export { CircularBuffer } from './core/buffer';
|
|
108
|
+
export type { CircularBufferConfig } from './core/buffer';
|
|
109
|
+
export { compressData, decompressData, compressImage, estimateSize, getCompressionRatio, } from './core/compress';
|
|
110
|
+
export { submitWithAuth, getAuthHeaders, clearOfflineQueue } from './core/transport';
|
|
111
|
+
export type { AuthConfig, TransportOptions, RetryConfig } from './core/transport';
|
|
112
|
+
export type { OfflineConfig } from './core/offline-queue';
|
|
113
|
+
export type { Logger, LogLevel, LoggerConfig } from './utils/logger';
|
|
114
|
+
export { getLogger, configureLogger, createLogger } from './utils/logger';
|
|
115
|
+
export { DirectUploader } from './core/uploader';
|
|
116
|
+
export type { UploadResult } from './core/uploader';
|
|
117
|
+
export { compressReplayEvents, canvasToBlob, estimateCompressedReplaySize, isWithinSizeLimit, } from './core/upload-helpers';
|
|
118
|
+
export { createSanitizer, Sanitizer } from './utils/sanitize';
|
|
119
|
+
export type { PIIPattern, CustomPattern, SanitizeConfig } from './utils/sanitize';
|
|
120
|
+
export { DEFAULT_PATTERNS, PATTERN_PRESETS, PATTERN_CATEGORIES, PatternBuilder, createPatternConfig, getPattern, getPatternsByCategory, validatePattern, } from './utils/sanitize';
|
|
121
|
+
export type { PIIPatternName, PatternDefinition } from './utils/sanitize';
|
|
122
|
+
export { FloatingButton } from './widget/button';
|
|
123
|
+
export type { FloatingButtonOptions } from './widget/button';
|
|
124
|
+
export { BugReportModal } from './widget/modal';
|
|
125
|
+
export type { BugReportData, BugReportModalOptions, PIIDetection } from './widget/modal';
|
|
126
|
+
export type { eventWithTime } from '@rrweb/types';
|
|
127
|
+
/**
|
|
128
|
+
* Convenience function to sanitize text with default PII patterns
|
|
129
|
+
* Useful for quick sanitization without creating a Sanitizer instance
|
|
130
|
+
*
|
|
131
|
+
* @param text - Text to sanitize
|
|
132
|
+
* @returns Sanitized text with PII redacted
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```typescript
|
|
136
|
+
* const sanitized = sanitize('Email: user@example.com');
|
|
137
|
+
* // Returns: 'Email: [REDACTED]'
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
export declare function sanitize(text: string): string;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { ScreenshotCapture } from './capture/screenshot';
|
|
2
|
+
import { ConsoleCapture } from './capture/console';
|
|
3
|
+
import { NetworkCapture } from './capture/network';
|
|
4
|
+
import { MetadataCapture } from './capture/metadata';
|
|
5
|
+
import { compressData, estimateSize, getCompressionRatio } from './core/compress';
|
|
6
|
+
import { FloatingButton } from './widget/button';
|
|
7
|
+
import { BugReportModal } from './widget/modal';
|
|
8
|
+
import { DOMCollector } from './collectors';
|
|
9
|
+
import { createSanitizer } from './utils/sanitize';
|
|
10
|
+
import { getLogger } from './utils/logger';
|
|
11
|
+
import { submitWithAuth } from './core/transport';
|
|
12
|
+
const logger = getLogger();
|
|
13
|
+
export class BugSpotter {
|
|
14
|
+
constructor(config) {
|
|
15
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
16
|
+
this.config = config;
|
|
17
|
+
// Initialize sanitizer if enabled
|
|
18
|
+
if (((_a = config.sanitize) === null || _a === void 0 ? void 0 : _a.enabled) !== false) {
|
|
19
|
+
this.sanitizer = createSanitizer({
|
|
20
|
+
enabled: (_c = (_b = config.sanitize) === null || _b === void 0 ? void 0 : _b.enabled) !== null && _c !== void 0 ? _c : true,
|
|
21
|
+
patterns: (_d = config.sanitize) === null || _d === void 0 ? void 0 : _d.patterns,
|
|
22
|
+
customPatterns: (_e = config.sanitize) === null || _e === void 0 ? void 0 : _e.customPatterns,
|
|
23
|
+
excludeSelectors: (_f = config.sanitize) === null || _f === void 0 ? void 0 : _f.excludeSelectors,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
this.screenshot = new ScreenshotCapture();
|
|
27
|
+
this.console = new ConsoleCapture({ sanitizer: this.sanitizer });
|
|
28
|
+
this.network = new NetworkCapture({ sanitizer: this.sanitizer });
|
|
29
|
+
this.metadata = new MetadataCapture({ sanitizer: this.sanitizer });
|
|
30
|
+
// Note: DirectUploader is created per-report since it needs bugId
|
|
31
|
+
// See submitBugReport() for initialization
|
|
32
|
+
// Initialize DOM collector if replay is enabled
|
|
33
|
+
if (((_g = config.replay) === null || _g === void 0 ? void 0 : _g.enabled) !== false) {
|
|
34
|
+
this.domCollector = new DOMCollector({
|
|
35
|
+
duration: (_j = (_h = config.replay) === null || _h === void 0 ? void 0 : _h.duration) !== null && _j !== void 0 ? _j : 15,
|
|
36
|
+
sampling: (_k = config.replay) === null || _k === void 0 ? void 0 : _k.sampling,
|
|
37
|
+
sanitizer: this.sanitizer,
|
|
38
|
+
});
|
|
39
|
+
this.domCollector.startRecording();
|
|
40
|
+
}
|
|
41
|
+
// Initialize widget if enabled
|
|
42
|
+
if (config.showWidget !== false) {
|
|
43
|
+
this.widget = new FloatingButton(config.widgetOptions);
|
|
44
|
+
this.widget.onClick(async () => {
|
|
45
|
+
await this.handleBugReport();
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
static init(config) {
|
|
50
|
+
if (!BugSpotter.instance) {
|
|
51
|
+
BugSpotter.instance = new BugSpotter(config);
|
|
52
|
+
}
|
|
53
|
+
return BugSpotter.instance;
|
|
54
|
+
}
|
|
55
|
+
static getInstance() {
|
|
56
|
+
return BugSpotter.instance || null;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Capture bug report data
|
|
60
|
+
* Note: Screenshot is captured for modal preview only (_screenshotPreview)
|
|
61
|
+
* Actual file uploads use presigned URLs (screenshotKey/replayKey set after upload)
|
|
62
|
+
*/
|
|
63
|
+
async capture() {
|
|
64
|
+
var _a, _b;
|
|
65
|
+
const screenshotPreview = await this.screenshot.capture();
|
|
66
|
+
const replayEvents = (_b = (_a = this.domCollector) === null || _a === void 0 ? void 0 : _a.getEvents()) !== null && _b !== void 0 ? _b : [];
|
|
67
|
+
return {
|
|
68
|
+
console: this.console.getLogs(),
|
|
69
|
+
network: this.network.getRequests(),
|
|
70
|
+
metadata: this.metadata.capture(),
|
|
71
|
+
replay: replayEvents,
|
|
72
|
+
// Internal: screenshot preview for modal (not sent to API)
|
|
73
|
+
_screenshotPreview: screenshotPreview,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
async handleBugReport() {
|
|
77
|
+
const report = await this.capture();
|
|
78
|
+
const modal = new BugReportModal({
|
|
79
|
+
onSubmit: async (data) => {
|
|
80
|
+
logger.log('Submitting bug:', Object.assign(Object.assign({}, data), { report }));
|
|
81
|
+
// Send to endpoint if configured
|
|
82
|
+
if (this.config.endpoint) {
|
|
83
|
+
try {
|
|
84
|
+
await this.submitBugReport(Object.assign(Object.assign({}, data), { report }));
|
|
85
|
+
logger.log('Bug report submitted successfully');
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
logger.error('Failed to submit bug report:', error);
|
|
89
|
+
// Re-throw to allow UI to handle errors if needed
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
modal.show(report._screenshotPreview || '');
|
|
96
|
+
}
|
|
97
|
+
async submitBugReport(payload) {
|
|
98
|
+
if (!this.config.endpoint) {
|
|
99
|
+
throw new Error('No endpoint configured for bug report submission');
|
|
100
|
+
}
|
|
101
|
+
const contentHeaders = {
|
|
102
|
+
'Content-Type': 'application/json',
|
|
103
|
+
};
|
|
104
|
+
logger.warn(`Submitting bug report to ${this.config.endpoint}`);
|
|
105
|
+
let body;
|
|
106
|
+
try {
|
|
107
|
+
// Try to compress the payload
|
|
108
|
+
const originalSize = estimateSize(payload);
|
|
109
|
+
const compressed = await compressData(payload);
|
|
110
|
+
const compressedSize = compressed.byteLength;
|
|
111
|
+
const ratio = getCompressionRatio(originalSize, compressedSize);
|
|
112
|
+
logger.log(`Payload compression: ${(originalSize / 1024).toFixed(1)}KB → ${(compressedSize / 1024).toFixed(1)}KB (${ratio}% reduction)`);
|
|
113
|
+
// Use compression if it actually reduces size
|
|
114
|
+
if (compressedSize < originalSize) {
|
|
115
|
+
// Create a Blob from the compressed Uint8Array for proper binary upload
|
|
116
|
+
// Use Uint8Array constructor to ensure clean ArrayBuffer (no extra padding bytes)
|
|
117
|
+
body = new Blob([new Uint8Array(compressed)], { type: 'application/gzip' });
|
|
118
|
+
contentHeaders['Content-Encoding'] = 'gzip';
|
|
119
|
+
contentHeaders['Content-Type'] = 'application/gzip';
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
body = JSON.stringify(payload);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
// Fallback to uncompressed if compression fails
|
|
127
|
+
logger.warn('Compression failed, sending uncompressed payload:', error);
|
|
128
|
+
body = JSON.stringify(payload);
|
|
129
|
+
}
|
|
130
|
+
// Determine auth configuration
|
|
131
|
+
const auth = this.config.auth;
|
|
132
|
+
// Submit with authentication, retry logic, and offline queue
|
|
133
|
+
const response = await submitWithAuth(this.config.endpoint, body, contentHeaders, {
|
|
134
|
+
auth,
|
|
135
|
+
retry: this.config.retry,
|
|
136
|
+
offline: this.config.offline,
|
|
137
|
+
});
|
|
138
|
+
logger.warn(`${JSON.stringify(response)}`);
|
|
139
|
+
if (!response.ok) {
|
|
140
|
+
const errorText = await response.text().catch(() => {
|
|
141
|
+
return 'Unknown error';
|
|
142
|
+
});
|
|
143
|
+
throw new Error(`Failed to submit bug report: ${response.status} ${response.statusText}. ${errorText}`);
|
|
144
|
+
}
|
|
145
|
+
return response.json().catch(() => {
|
|
146
|
+
return undefined;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
getConfig() {
|
|
150
|
+
return Object.assign({}, this.config);
|
|
151
|
+
}
|
|
152
|
+
destroy() {
|
|
153
|
+
var _a, _b;
|
|
154
|
+
this.console.destroy();
|
|
155
|
+
this.network.destroy();
|
|
156
|
+
(_a = this.domCollector) === null || _a === void 0 ? void 0 : _a.destroy();
|
|
157
|
+
(_b = this.widget) === null || _b === void 0 ? void 0 : _b.destroy();
|
|
158
|
+
BugSpotter.instance = undefined;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
export { ScreenshotCapture } from './capture/screenshot';
|
|
162
|
+
export { ConsoleCapture } from './capture/console';
|
|
163
|
+
export { NetworkCapture } from './capture/network';
|
|
164
|
+
export { MetadataCapture } from './capture/metadata';
|
|
165
|
+
// Export collector modules
|
|
166
|
+
export { DOMCollector } from './collectors';
|
|
167
|
+
// Export core utilities
|
|
168
|
+
export { CircularBuffer } from './core/buffer';
|
|
169
|
+
// Export compression utilities
|
|
170
|
+
export { compressData, decompressData, compressImage, estimateSize, getCompressionRatio, } from './core/compress';
|
|
171
|
+
// Export transport and authentication
|
|
172
|
+
export { submitWithAuth, getAuthHeaders, clearOfflineQueue } from './core/transport';
|
|
173
|
+
export { getLogger, configureLogger, createLogger } from './utils/logger';
|
|
174
|
+
// Export upload utilities
|
|
175
|
+
export { DirectUploader } from './core/uploader';
|
|
176
|
+
export { compressReplayEvents, canvasToBlob, estimateCompressedReplaySize, isWithinSizeLimit, } from './core/upload-helpers';
|
|
177
|
+
// Export sanitization utilities
|
|
178
|
+
export { createSanitizer, Sanitizer } from './utils/sanitize';
|
|
179
|
+
// Export pattern configuration utilities
|
|
180
|
+
export { DEFAULT_PATTERNS, PATTERN_PRESETS, PATTERN_CATEGORIES, PatternBuilder, createPatternConfig, getPattern, getPatternsByCategory, validatePattern, } from './utils/sanitize';
|
|
181
|
+
// Export widget components
|
|
182
|
+
export { FloatingButton } from './widget/button';
|
|
183
|
+
export { BugReportModal } from './widget/modal';
|
|
184
|
+
/**
|
|
185
|
+
* Convenience function to sanitize text with default PII patterns
|
|
186
|
+
* Useful for quick sanitization without creating a Sanitizer instance
|
|
187
|
+
*
|
|
188
|
+
* @param text - Text to sanitize
|
|
189
|
+
* @returns Sanitized text with PII redacted
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* ```typescript
|
|
193
|
+
* const sanitized = sanitize('Email: user@example.com');
|
|
194
|
+
* // Returns: 'Email: [REDACTED]'
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
197
|
+
export function sanitize(text) {
|
|
198
|
+
const sanitizer = createSanitizer({
|
|
199
|
+
enabled: true,
|
|
200
|
+
patterns: 'all',
|
|
201
|
+
customPatterns: [],
|
|
202
|
+
excludeSelectors: [],
|
|
203
|
+
});
|
|
204
|
+
return sanitizer.sanitize(text);
|
|
205
|
+
}
|