@bugspotter/sdk 0.1.0-alpha.2 → 0.1.2-alpha.5

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.
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ /**
3
+ * URL Helper Utilities
4
+ * Extract base API URL from endpoint configuration
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.InvalidEndpointError = void 0;
8
+ exports.stripEndpointSuffix = stripEndpointSuffix;
9
+ exports.getApiBaseUrl = getApiBaseUrl;
10
+ const logger_1 = require("./logger");
11
+ const logger = (0, logger_1.getLogger)();
12
+ /**
13
+ * Custom error for invalid endpoint URLs
14
+ */
15
+ class InvalidEndpointError extends Error {
16
+ constructor(endpoint, reason) {
17
+ super(`Invalid endpoint URL: ${endpoint}. ${reason}`);
18
+ this.endpoint = endpoint;
19
+ this.reason = reason;
20
+ this.name = 'InvalidEndpointError';
21
+ }
22
+ }
23
+ exports.InvalidEndpointError = InvalidEndpointError;
24
+ /**
25
+ * Strip known endpoint suffixes from path
26
+ * Removes /api/v1/reports path
27
+ */
28
+ function stripEndpointSuffix(path) {
29
+ // Use lastIndexOf to handle paths like '/prefix/api/v1/reports'
30
+ const reportsIndex = path.lastIndexOf('/api/v1/reports');
31
+ if (reportsIndex !== -1) {
32
+ return path.substring(0, reportsIndex);
33
+ }
34
+ // Remove trailing slash
35
+ return path.replace(/\/$/, '') || '';
36
+ }
37
+ /**
38
+ * Extract base API URL from endpoint
39
+ * Returns scheme + host + base path (without /api/v1/reports suffix)
40
+ *
41
+ * @example
42
+ * getApiBaseUrl('https://api.example.com/api/v1/reports')
43
+ * // Returns: 'https://api.example.com'
44
+ *
45
+ * @throws InvalidEndpointError if endpoint is not a valid absolute URL
46
+ */
47
+ function getApiBaseUrl(endpoint) {
48
+ if (!endpoint) {
49
+ throw new InvalidEndpointError('', 'No endpoint configured');
50
+ }
51
+ try {
52
+ const url = new URL(endpoint);
53
+ const basePath = stripEndpointSuffix(url.pathname);
54
+ return url.origin + basePath;
55
+ }
56
+ catch (error) {
57
+ const errorMessage = error instanceof Error ? error.message : String(error);
58
+ logger.error('Invalid endpoint URL - must be a valid absolute URL', {
59
+ endpoint,
60
+ error: errorMessage,
61
+ });
62
+ throw new InvalidEndpointError(endpoint, 'Must be a valid absolute URL (e.g., https://api.example.com/api/v1/reports)');
63
+ }
64
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * SDK version - auto-generated from package.json
3
+ * DO NOT EDIT THIS FILE MANUALLY
4
+ *
5
+ * This file is automatically generated during the build process.
6
+ * To update the version, modify package.json
7
+ */
8
+ export declare const VERSION = "0.1.2-alpha.5";
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ /**
3
+ * SDK version - auto-generated from package.json
4
+ * DO NOT EDIT THIS FILE MANUALLY
5
+ *
6
+ * This file is automatically generated during the build process.
7
+ * To update the version, modify package.json
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.VERSION = void 0;
11
+ exports.VERSION = '0.1.2-alpha.5';
@@ -1,7 +1,10 @@
1
1
  type ButtonPosition = 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
2
2
  export interface FloatingButtonOptions {
3
3
  position?: ButtonPosition;
4
+ /** Icon to display - can be text/emoji or 'svg' for default bug icon */
4
5
  icon?: string;
6
+ /** Custom SVG icon (overrides icon if provided) */
7
+ customSvg?: string;
5
8
  backgroundColor?: string;
6
9
  size?: number;
7
10
  offset?: {
@@ -9,6 +12,8 @@ export interface FloatingButtonOptions {
9
12
  y: number;
10
13
  };
11
14
  zIndex?: number;
15
+ /** Custom tooltip text */
16
+ tooltip?: string;
12
17
  }
13
18
  export declare class FloatingButton {
14
19
  private button;
@@ -1,13 +1,29 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FloatingButton = void 0;
4
+ // Professional bug report icon SVG
5
+ const DEFAULT_SVG_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
6
+ <path d="M8 2v4"/>
7
+ <path d="M16 2v4"/>
8
+ <path d="M12 12v5"/>
9
+ <circle cx="12" cy="10" r="4"/>
10
+ <path d="M9 16c-1.5 1-3 2-3 4h12c0-2-1.5-3-3-4"/>
11
+ <path d="M3 8h4"/>
12
+ <path d="M17 8h4"/>
13
+ <path d="M5 12h2"/>
14
+ <path d="M17 12h2"/>
15
+ <path d="M6 16h2"/>
16
+ <path d="M16 16h2"/>
17
+ </svg>`;
4
18
  const DEFAULT_BUTTON_OPTIONS = {
5
19
  position: 'bottom-right',
6
- icon: '🐛',
7
- backgroundColor: '#ef4444',
8
- size: 60,
20
+ icon: 'svg', // Use SVG icon by default
21
+ customSvg: undefined,
22
+ backgroundColor: '#2563eb', // Professional blue color
23
+ size: 56,
9
24
  offset: { x: 20, y: 20 },
10
25
  zIndex: 999999,
26
+ tooltip: 'Report an Issue',
11
27
  };
12
28
  const BUTTON_STYLES = {
13
29
  transition: 'transform 0.2s ease, box-shadow 0.2s ease',
@@ -23,7 +39,7 @@ const BUTTON_STYLES = {
23
39
  };
24
40
  class FloatingButton {
25
41
  constructor(options = {}) {
26
- var _a, _b, _c, _d, _e, _f;
42
+ var _a, _b, _c, _d, _e, _f, _g, _h;
27
43
  this.eventHandlers = new Map();
28
44
  this.handleMouseEnter = () => {
29
45
  this.button.style.transform = BUTTON_STYLES.transform.hover;
@@ -42,10 +58,12 @@ class FloatingButton {
42
58
  this.options = {
43
59
  position: (_a = options.position) !== null && _a !== void 0 ? _a : DEFAULT_BUTTON_OPTIONS.position,
44
60
  icon: (_b = options.icon) !== null && _b !== void 0 ? _b : DEFAULT_BUTTON_OPTIONS.icon,
45
- backgroundColor: (_c = options.backgroundColor) !== null && _c !== void 0 ? _c : DEFAULT_BUTTON_OPTIONS.backgroundColor,
46
- size: (_d = options.size) !== null && _d !== void 0 ? _d : DEFAULT_BUTTON_OPTIONS.size,
47
- offset: (_e = options.offset) !== null && _e !== void 0 ? _e : DEFAULT_BUTTON_OPTIONS.offset,
48
- zIndex: (_f = options.zIndex) !== null && _f !== void 0 ? _f : DEFAULT_BUTTON_OPTIONS.zIndex,
61
+ customSvg: (_c = options.customSvg) !== null && _c !== void 0 ? _c : DEFAULT_BUTTON_OPTIONS.customSvg,
62
+ backgroundColor: (_d = options.backgroundColor) !== null && _d !== void 0 ? _d : DEFAULT_BUTTON_OPTIONS.backgroundColor,
63
+ size: (_e = options.size) !== null && _e !== void 0 ? _e : DEFAULT_BUTTON_OPTIONS.size,
64
+ offset: (_f = options.offset) !== null && _f !== void 0 ? _f : DEFAULT_BUTTON_OPTIONS.offset,
65
+ zIndex: (_g = options.zIndex) !== null && _g !== void 0 ? _g : DEFAULT_BUTTON_OPTIONS.zIndex,
66
+ tooltip: (_h = options.tooltip) !== null && _h !== void 0 ? _h : DEFAULT_BUTTON_OPTIONS.tooltip,
49
67
  };
50
68
  this.button = this.createButton();
51
69
  // Ensure DOM is ready before appending
@@ -60,8 +78,18 @@ class FloatingButton {
60
78
  }
61
79
  createButton() {
62
80
  const btn = document.createElement('button');
63
- btn.textContent = this.options.icon;
64
- btn.setAttribute('aria-label', 'Report Bug');
81
+ // Set button content (SVG or text)
82
+ if (this.options.customSvg) {
83
+ btn.innerHTML = this.options.customSvg;
84
+ }
85
+ else if (this.options.icon === 'svg') {
86
+ btn.innerHTML = DEFAULT_SVG_ICON;
87
+ }
88
+ else {
89
+ btn.textContent = this.options.icon;
90
+ }
91
+ btn.setAttribute('aria-label', this.options.tooltip);
92
+ btn.setAttribute('title', this.options.tooltip);
65
93
  btn.setAttribute('data-bugspotter-exclude', 'true');
66
94
  btn.style.cssText = this.getButtonStyles();
67
95
  this.addHoverEffects(btn);
@@ -70,6 +98,9 @@ class FloatingButton {
70
98
  getButtonStyles() {
71
99
  const { position, size, offset, backgroundColor, zIndex } = this.options;
72
100
  const positionStyles = this.getPositionStyles(position, offset);
101
+ // SVG icons need slightly different sizing
102
+ const isSvgIcon = this.options.customSvg || this.options.icon === 'svg';
103
+ const iconSize = size * 0.5;
73
104
  return `
74
105
  position: fixed;
75
106
  ${positionStyles}
@@ -80,10 +111,11 @@ class FloatingButton {
80
111
  color: white;
81
112
  border: none;
82
113
  cursor: pointer;
83
- font-size: ${size * 0.5}px;
114
+ font-size: ${iconSize}px;
84
115
  display: flex;
85
116
  align-items: center;
86
117
  justify-content: center;
118
+ padding: ${isSvgIcon ? size * 0.25 : 0}px;
87
119
  box-shadow: ${BUTTON_STYLES.boxShadow.default};
88
120
  transition: ${BUTTON_STYLES.transition};
89
121
  z-index: ${zIndex};
@@ -125,7 +157,13 @@ class FloatingButton {
125
157
  this.button.style.display = 'none';
126
158
  }
127
159
  setIcon(icon) {
128
- this.button.textContent = icon;
160
+ this.options.icon = icon;
161
+ if (icon === 'svg') {
162
+ this.button.innerHTML = DEFAULT_SVG_ICON;
163
+ }
164
+ else {
165
+ this.button.textContent = icon;
166
+ }
129
167
  }
130
168
  setBackgroundColor(color) {
131
169
  this.button.style.backgroundColor = color;
@@ -15,7 +15,7 @@ export interface ValidationErrors {
15
15
  }
16
16
  export interface FormData {
17
17
  title: string;
18
- description: string;
18
+ description?: string;
19
19
  piiDetected: boolean;
20
20
  piiConfirmed: boolean;
21
21
  }
@@ -41,7 +41,7 @@ export declare class FormValidator {
41
41
  /**
42
42
  * Validate description field
43
43
  */
44
- validateDescription(description: string): string | null;
44
+ validateDescription(description?: string): string | null;
45
45
  /**
46
46
  * Validate single field by name
47
47
  */
@@ -58,9 +58,14 @@ class FormValidator {
58
58
  * Validate description field
59
59
  */
60
60
  validateDescription(description) {
61
+ // Description is optional
62
+ if (!description) {
63
+ return null;
64
+ }
61
65
  const trimmed = description.trim();
62
- if (!trimmed) {
63
- return 'Description is required';
66
+ // Whitespace-only is invalid (user attempted to provide content but it's meaningless)
67
+ if (trimmed.length === 0) {
68
+ return 'Description cannot be only whitespace';
64
69
  }
65
70
  if (trimmed.length < this.minDescriptionLength) {
66
71
  return `Description must be at least ${this.minDescriptionLength} characters`;
@@ -1,6 +1,6 @@
1
1
  export interface BugReportData {
2
2
  title: string;
3
- description: string;
3
+ description?: string;
4
4
  }
5
5
  export interface PIIDetection {
6
6
  type: string;
@@ -219,6 +219,7 @@ class BugReportModal {
219
219
  }
220
220
  }
221
221
  async handleSubmit(e) {
222
+ var _a;
222
223
  e.preventDefault();
223
224
  const elements = this.domCache.get();
224
225
  const formData = {
@@ -270,7 +271,7 @@ class BugReportModal {
270
271
  // Submit
271
272
  const bugReportData = {
272
273
  title: formData.title.trim(),
273
- description: formData.description.trim(),
274
+ description: (_a = formData.description) === null || _a === void 0 ? void 0 : _a.trim(),
274
275
  };
275
276
  try {
276
277
  await this.options.onSubmit(bugReportData);
@@ -85,22 +85,84 @@ console.log(report.replay); // Array of rrweb events
85
85
 
86
86
  ### 4. Event Transmission
87
87
 
88
- The replay events are included in the bug report payload:
88
+ The replay events are compressed and uploaded via presigned URLs using an optimized 3-request flow:
89
+
90
+ **Step 1: Create Bug Report with Presigned URLs**
89
91
 
90
92
  ```typescript
93
+ // Initial bug report with flags indicating which files will be uploaded
94
+ POST /api/v1/reports
91
95
  {
92
96
  title: "Bug title",
93
97
  description: "Bug description",
94
98
  report: {
95
- screenshot: "...",
96
99
  console: [...],
97
100
  network: [...],
98
- metadata: {...},
99
- replay: [...] // rrweb events
101
+ metadata: {...}
102
+ },
103
+ hasScreenshot: true, // SDK sets this if screenshot was captured
104
+ hasReplay: true // SDK sets this if replay events were recorded
105
+ }
106
+
107
+ // Response includes bug report ID AND presigned URLs
108
+ {
109
+ "success": true,
110
+ "data": {
111
+ "id": "bug-uuid-here",
112
+ "title": "Bug title",
113
+ "presignedUrls": {
114
+ "screenshot": {
115
+ "uploadUrl": "https://s3.amazonaws.com/...",
116
+ "storageKey": "screenshots/project/bug/screenshot.png"
117
+ },
118
+ "replay": {
119
+ "uploadUrl": "https://s3.amazonaws.com/...",
120
+ "storageKey": "replays/project/bug/replay.gz"
121
+ }
122
+ }
100
123
  }
101
124
  }
102
125
  ```
103
126
 
127
+ **Step 2: Upload Files Directly to S3**
128
+
129
+ ```typescript
130
+ // Upload screenshot to presigned URL (parallel with replay)
131
+ PUT https://s3.amazonaws.com/presigned-screenshot-url
132
+ Content-Type: image/png
133
+ <binary screenshot data>
134
+
135
+ // Upload compressed replay to presigned URL (parallel with screenshot)
136
+ PUT https://s3.amazonaws.com/presigned-replay-url
137
+ Content-Type: application/gzip
138
+ <compressed replay events>
139
+ ```
140
+
141
+ **Step 3: Confirm Uploads**
142
+
143
+ ```typescript
144
+ // Confirm screenshot upload
145
+ POST /api/v1/reports/{bugId}/confirm-upload
146
+ {
147
+ "fileType": "screenshot"
148
+ }
149
+
150
+ // Confirm replay upload
151
+ POST /api/v1/reports/{bugId}/confirm-upload
152
+ {
153
+ "fileType": "replay"
154
+ }
155
+ ```
156
+
157
+ **Optimized Flow Benefits:**
158
+
159
+ - **40% fewer HTTP requests** - 3 requests vs 5 in old flow
160
+ - **Reduces server load** - Files go directly to storage (S3)
161
+ - **Improves performance** - No API server bottleneck for large files
162
+ - **Better scalability** - Storage handles the bandwidth
163
+ - **Parallel uploads** - Screenshot and replay upload concurrently
164
+ - **Automatic compression** - Replay events are gzipped before upload
165
+
104
166
  ## Event Types
105
167
 
106
168
  The DOM collector captures the following event types:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bugspotter/sdk",
3
- "version": "0.1.0-alpha.2",
3
+ "version": "0.1.2-alpha.5",
4
4
  "description": "Professional bug reporting SDK with screenshots, session replay, and automatic error capture for web applications",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.esm.js",
@@ -16,13 +16,16 @@
16
16
  },
17
17
  "scripts": {
18
18
  "dev": "webpack --watch --mode development",
19
+ "generate-version": "node scripts/generate-version.js",
20
+ "prebuild": "npm run generate-version",
19
21
  "build": "npm run build:webpack && npm run build:esm && npm run build:cjs",
20
22
  "build:webpack": "webpack --mode production",
21
23
  "build:esm": "tsc --project tsconfig.build.json --module ES2020 --outDir dist/esm && shx mv dist/esm/index.js dist/index.esm.js && shx rm -rf dist/esm",
22
- "build:cjs": "tsc --project tsconfig.build.json --module CommonJS --outDir dist",
24
+ "build:cjs": "tsc --project tsconfig.cjs.json",
23
25
  "prepublishOnly": "npm run build && npm test",
24
26
  "lint": "eslint \"src/**/*.{ts,js}\"",
25
27
  "lint:fix": "eslint \"src/**/*.{ts,js}\" --fix",
28
+ "pretest": "npm run generate-version",
26
29
  "test": "vitest run",
27
30
  "test:watch": "vitest",
28
31
  "test:ui": "vitest --ui",
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Generate version.ts file from package.json
4
+ * This ensures VERSION is available in all build outputs (UMD, ESM, CJS)
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ try {
11
+ // Read package.json
12
+ const packageJsonPath = path.join(__dirname, '../package.json');
13
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
14
+
15
+ if (!packageJson.version) {
16
+ console.error('Error: No version found in package.json');
17
+ process.exit(1);
18
+ }
19
+
20
+ // Generate version.ts content with proper documentation
21
+ const versionFilePath = path.join(__dirname, '../src/version.ts');
22
+ const versionFileContent = `/**
23
+ * SDK version - auto-generated from package.json
24
+ * DO NOT EDIT THIS FILE MANUALLY
25
+ *
26
+ * This file is automatically generated during the build process.
27
+ * To update the version, modify package.json
28
+ */
29
+
30
+ export const VERSION = '${packageJson.version}';
31
+ `;
32
+
33
+ // Write the file
34
+ fs.writeFileSync(versionFilePath, versionFileContent, 'utf8');
35
+ console.log(`✓ Generated version.ts with version ${packageJson.version}`);
36
+ } catch (error) {
37
+ console.error('Error generating version.ts:', error.message);
38
+ process.exit(1);
39
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "extends": "../../tsconfig.json",
4
+ "compilerOptions": {
5
+ "target": "ES2017",
6
+ "module": "CommonJS",
7
+ "lib": ["ES2017", "DOM", "DOM.Iterable"],
8
+ "moduleResolution": "node",
9
+ "declaration": true,
10
+ "outDir": "./dist",
11
+ "isolatedModules": true
12
+ },
13
+ "include": ["src/**/*"],
14
+ "exclude": ["node_modules", "dist", "tests", "**/*.test.ts", "**/*.spec.ts"]
15
+ }