mirror of
https://github.com/agoramachina/claude-exporter.git
synced 2026-07-10 00:12:08 +00:00
Add Contact & Diagnostics section to Options (v1.10.3)
New Options section with three buttons: - Email developer: opens a mailto with pre-filled subject (incl. version) and a short body template - Generate diagnostics: downloads a sanitized JSON bundle the user can attach to a bug report - Clear log: wipes the captured error ring buffer Error capture (utils.js): - initErrorCapture(context) registers window.error + unhandledrejection listeners that push entries to a 50-entry FIFO ring buffer in chrome.storage.local (errorLog key) - Sanitization runs at capture time: UUID-shaped substrings (chat / org / project IDs that may appear in fetch URLs or stack traces) replaced with "<id>" before they hit storage - Re-entry guard prevents the listener from looping on its own writes - Called from popup, browse, content, options. Skipped in background -- it's tiny and would need importScripts overhead for marginal value. Diagnostics bundle includes: extension/browser version, navigator UA + platform + language, counts of modelSnapshots / exportTimestamps / errors, current preferences (dateFormat, timeFormat, modelDisplay), orgIdConfigured boolean (NOT the value), and the error log. No conversation content. No org ID. Nothing is sent anywhere -- user downloads the file and chooses whether to attach. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
85da733d62
commit
a55d0e7490
13 changed files with 363 additions and 2 deletions
|
|
@ -4,6 +4,9 @@ if (window.claudeExporterContentScriptLoaded) {
|
|||
} else {
|
||||
window.claudeExporterContentScriptLoaded = true;
|
||||
|
||||
// Capture unhandled errors for diagnostics (sanitized, stored in chrome.storage.local)
|
||||
if (typeof initErrorCapture === 'function') initErrorCapture('content');
|
||||
|
||||
// Note: Organization ID is now stored in extension settings
|
||||
// Users need to configure it in the extension options page
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Claude Exporter Beta",
|
||||
"version": "1.10.2",
|
||||
"version": "1.10.3",
|
||||
"description": "Export conversations and artifacts from Claude.ai",
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
|
|
|
|||
|
|
@ -277,6 +277,16 @@
|
|||
<div id="modelDisplayStatus" class="status"></div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Contact & Diagnostics</h3>
|
||||
<p>Found a bug or have feedback? Email the developer — and if you're reporting a bug, attach a diagnostics file so the issue is easier to investigate.</p>
|
||||
<button id="emailDevBtn" style="margin-right: 8px;">Email developer</button>
|
||||
<button id="generateDiagnosticsBtn" style="margin-right: 8px;">Generate diagnostics</button>
|
||||
<button id="clearDiagnosticsBtn">Clear log</button>
|
||||
<p class="example" style="margin-top: 8px;">The diagnostics file contains: extension & browser version, counts of stored snapshots / export records, current preferences, whether an org ID is configured (but not the value), and the last 50 captured errors. All UUIDs (conversation, org, project IDs) are redacted before they're ever stored. Nothing is sent anywhere automatically — you choose whether to attach the file.</p>
|
||||
<div id="contactStatus" class="status"></div>
|
||||
</div>
|
||||
|
||||
<script src="utils.js"></script>
|
||||
<script src="options.js"></script>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
// Capture unhandled errors for diagnostics (sanitized, stored in chrome.storage.local)
|
||||
if (typeof initErrorCapture === 'function') initErrorCapture('options');
|
||||
|
||||
// Load saved settings
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
chrome.storage.sync.get(['organizationId'], (result) => {
|
||||
|
|
@ -124,6 +127,27 @@ document.querySelectorAll('input[name="modelDisplay"]').forEach((radio) => {
|
|||
});
|
||||
});
|
||||
|
||||
// Contact & Diagnostics
|
||||
document.getElementById('emailDevBtn').addEventListener('click', () => {
|
||||
const version = chrome.runtime.getManifest().version;
|
||||
const subject = encodeURIComponent(`Claude Exporter Bug Report — v${version}`);
|
||||
const body = encodeURIComponent('Describe the issue here. If this is a bug, please attach the diagnostics file generated from the Options page.\n\n');
|
||||
window.location.href = `mailto:agoramachina@gmail.com?subject=${subject}&body=${body}`;
|
||||
});
|
||||
|
||||
document.getElementById('generateDiagnosticsBtn').addEventListener('click', () => {
|
||||
generateDiagnostics((success, message) => {
|
||||
showStatus('contactStatus', message, success ? 'success' : 'error');
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('clearDiagnosticsBtn').addEventListener('click', () => {
|
||||
if (!confirm('Clear the captured error log? This cannot be undone.')) return;
|
||||
clearDiagnosticsLog((success, message) => {
|
||||
showStatus('contactStatus', message, success ? 'success' : 'error');
|
||||
});
|
||||
});
|
||||
|
||||
// Helper functions
|
||||
function showStatus(elementId, message, type) {
|
||||
const statusEl = document.getElementById(elementId);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
// Capture unhandled errors for diagnostics (sanitized, stored in chrome.storage.local)
|
||||
if (typeof initErrorCapture === 'function') initErrorCapture('popup');
|
||||
|
||||
// Get organization ID from storage (fallback)
|
||||
async function getStoredOrgId() {
|
||||
return new Promise((resolve) => {
|
||||
|
|
|
|||
136
chrome/utils.js
136
chrome/utils.js
|
|
@ -914,6 +914,141 @@ function importBackup(file, onComplete) {
|
|||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
// ----- Error capture & diagnostics -----
|
||||
// Captures unhandled errors and rejected promises into a ring buffer in
|
||||
// chrome.storage.local. The user can later download a sanitized diagnostics
|
||||
// bundle (Options page → Contact & Diagnostics) to attach to a bug report.
|
||||
// Sanitization runs at capture time: any UUID-looking substring (chat / org /
|
||||
// project IDs that may appear in fetch URLs or stack traces) is replaced with
|
||||
// "<id>" so we never persist identifiers.
|
||||
|
||||
const CE_UUID_REGEX = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;
|
||||
const CE_ERROR_LOG_MAX = 50;
|
||||
|
||||
function sanitizeForDiagnostics(value) {
|
||||
if (typeof value !== 'string') return value;
|
||||
return value.replace(CE_UUID_REGEX, '<id>');
|
||||
}
|
||||
|
||||
function initErrorCapture(context) {
|
||||
if (typeof chrome === 'undefined' || !chrome.storage || !chrome.storage.local) return;
|
||||
|
||||
// Re-entry guard: if our own push() throws, don't loop into the listener.
|
||||
let suppressed = false;
|
||||
|
||||
const push = (entry) => {
|
||||
if (suppressed) return;
|
||||
suppressed = true;
|
||||
try {
|
||||
chrome.storage.local.get(['errorLog'], (result) => {
|
||||
try {
|
||||
const log = Array.isArray(result.errorLog) ? result.errorLog : [];
|
||||
log.push(entry);
|
||||
if (log.length > CE_ERROR_LOG_MAX) {
|
||||
log.splice(0, log.length - CE_ERROR_LOG_MAX);
|
||||
}
|
||||
chrome.storage.local.set({ errorLog: log }, () => { suppressed = false; });
|
||||
} catch (e) { suppressed = false; }
|
||||
});
|
||||
} catch (e) { suppressed = false; }
|
||||
};
|
||||
|
||||
const target = (typeof globalThis !== 'undefined') ? globalThis : self;
|
||||
|
||||
target.addEventListener('error', (event) => {
|
||||
push({
|
||||
ts: new Date().toISOString(),
|
||||
level: 'error',
|
||||
context,
|
||||
msg: sanitizeForDiagnostics(String(event.message || '')),
|
||||
source: event.filename ? sanitizeForDiagnostics(String(event.filename)) : null,
|
||||
line: event.lineno || null,
|
||||
col: event.colno || null,
|
||||
stack: event.error && event.error.stack ? sanitizeForDiagnostics(String(event.error.stack)) : null
|
||||
});
|
||||
});
|
||||
|
||||
target.addEventListener('unhandledrejection', (event) => {
|
||||
const reason = event.reason;
|
||||
const msg = reason && reason.message ? String(reason.message)
|
||||
: (reason !== undefined ? String(reason) : '(no reason)');
|
||||
push({
|
||||
ts: new Date().toISOString(),
|
||||
level: 'unhandledrejection',
|
||||
context,
|
||||
msg: sanitizeForDiagnostics(msg),
|
||||
stack: reason && reason.stack ? sanitizeForDiagnostics(String(reason.stack)) : null
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Build a sanitized diagnostics bundle and trigger a download. Callers may
|
||||
// pass an onComplete(success, message) callback for status reporting.
|
||||
function generateDiagnostics(onComplete) {
|
||||
const manifest = chrome.runtime.getManifest();
|
||||
|
||||
chrome.storage.local.get(
|
||||
['errorLog', 'modelSnapshots', 'exportTimestamps', 'dateFormat', 'timeFormat', 'modelDisplay'],
|
||||
(local) => {
|
||||
chrome.storage.sync.get(['organizationId'], (sync) => {
|
||||
const errorLog = Array.isArray(local.errorLog) ? local.errorLog : [];
|
||||
const diagnostics = {
|
||||
_meta: {
|
||||
app: 'claude-exporter',
|
||||
diagnosticsVersion: 1,
|
||||
generatedAt: new Date().toISOString()
|
||||
},
|
||||
extension: {
|
||||
name: manifest.name,
|
||||
version: manifest.version
|
||||
},
|
||||
environment: {
|
||||
userAgent: (typeof navigator !== 'undefined' && navigator.userAgent) || null,
|
||||
platform: (typeof navigator !== 'undefined' && navigator.platform) || null,
|
||||
language: (typeof navigator !== 'undefined' && navigator.language) || null
|
||||
},
|
||||
preferences: {
|
||||
dateFormat: local.dateFormat || 'mdy',
|
||||
timeFormat: local.timeFormat || '12h',
|
||||
modelDisplay: local.modelDisplay === 'current' ? 'current' : 'original',
|
||||
orgIdConfigured: !!(sync && sync.organizationId)
|
||||
},
|
||||
counts: {
|
||||
modelSnapshots: Object.keys(local.modelSnapshots || {}).length,
|
||||
exportTimestamps: Object.keys(local.exportTimestamps || {}).length,
|
||||
errors: errorLog.length
|
||||
},
|
||||
errors: errorLog
|
||||
};
|
||||
|
||||
const now = new Date();
|
||||
const ymd = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}`;
|
||||
const hms = `${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}${String(now.getSeconds()).padStart(2, '0')}`;
|
||||
|
||||
const blob = new Blob([JSON.stringify(diagnostics, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `claude-exporter-diagnostics-${ymd}-${hms}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
if (onComplete) {
|
||||
onComplete(true, `Diagnostics downloaded — ${errorLog.length} error(s) captured, all IDs redacted.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function clearDiagnosticsLog(onComplete) {
|
||||
chrome.storage.local.set({ errorLog: [] }, () => {
|
||||
if (onComplete) onComplete(true, 'Diagnostics log cleared.');
|
||||
});
|
||||
}
|
||||
|
||||
// Functions are available globally in the browser context
|
||||
// In Node (vitest), expose them via module.exports for testing
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
|
|
@ -936,5 +1071,6 @@ if (typeof module !== 'undefined' && module.exports) {
|
|||
backupExtensionData,
|
||||
importBackup,
|
||||
mergeStorageData,
|
||||
sanitizeForDiagnostics,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
# Changelog
|
||||
|
||||
## [1.10.3]
|
||||
|
||||
- New Options section: **Contact & Diagnostics**
|
||||
- **Email developer** — opens a mailto with a pre-filled subject including the version and a short body template
|
||||
- **Generate diagnostics** — downloads `claude-exporter-diagnostics-YYYYMMDD-HHMMSS.json` (extension/browser version, counts of stored records, current preferences, `orgIdConfigured` boolean, and the last 50 captured errors)
|
||||
- **Clear log** — wipes the captured error ring buffer
|
||||
- Each context (popup / browse / content script / options) now registers `error` and `unhandledrejection` listeners that push sanitized entries to a 50-entry ring buffer in `chrome.storage.local` (`errorLog` key, FIFO). All UUIDs are replaced with `<id>` at capture time so identifiers are never persisted.
|
||||
- Privacy stance: nothing is transmitted automatically. The diagnostics file stays local until the user chooses to attach it. Org ID itself is never included (only a boolean indicating whether one is configured). No conversation content is captured.
|
||||
|
||||
## [1.10.2]
|
||||
|
||||
- Removed "Test connection" from the browse settings dropdown — it's already available in Advanced Options (next to Save Settings)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ if (window.claudeExporterContentScriptLoaded) {
|
|||
window.claudeExporterContentScriptLoaded = true;
|
||||
console.log('[Claude Exporter] Content script loading for first time');
|
||||
|
||||
// Capture unhandled errors for diagnostics (sanitized, stored in chrome.storage.local)
|
||||
if (typeof initErrorCapture === 'function') initErrorCapture('content');
|
||||
|
||||
// Note: Organization ID is now stored in extension settings
|
||||
// Users need to configure it in the extension options page
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Claude Exporter Beta",
|
||||
"version": "1.10.2",
|
||||
"version": "1.10.3",
|
||||
"description": "Export conversations and artifacts from Claude.ai",
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
|
|
|
|||
|
|
@ -277,6 +277,16 @@
|
|||
<div id="modelDisplayStatus" class="status"></div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Contact & Diagnostics</h3>
|
||||
<p>Found a bug or have feedback? Email the developer — and if you're reporting a bug, attach a diagnostics file so the issue is easier to investigate.</p>
|
||||
<button id="emailDevBtn" style="margin-right: 8px;">Email developer</button>
|
||||
<button id="generateDiagnosticsBtn" style="margin-right: 8px;">Generate diagnostics</button>
|
||||
<button id="clearDiagnosticsBtn">Clear log</button>
|
||||
<p class="example" style="margin-top: 8px;">The diagnostics file contains: extension & browser version, counts of stored snapshots / export records, current preferences, whether an org ID is configured (but not the value), and the last 50 captured errors. All UUIDs (conversation, org, project IDs) are redacted before they're ever stored. Nothing is sent anywhere automatically — you choose whether to attach the file.</p>
|
||||
<div id="contactStatus" class="status"></div>
|
||||
</div>
|
||||
|
||||
<script src="utils.js"></script>
|
||||
<script src="options.js"></script>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
// Capture unhandled errors for diagnostics (sanitized, stored in chrome.storage.local)
|
||||
if (typeof initErrorCapture === 'function') initErrorCapture('options');
|
||||
|
||||
// Load saved settings
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
chrome.storage.sync.get(['organizationId'], (result) => {
|
||||
|
|
@ -124,6 +127,27 @@ document.querySelectorAll('input[name="modelDisplay"]').forEach((radio) => {
|
|||
});
|
||||
});
|
||||
|
||||
// Contact & Diagnostics
|
||||
document.getElementById('emailDevBtn').addEventListener('click', () => {
|
||||
const version = chrome.runtime.getManifest().version;
|
||||
const subject = encodeURIComponent(`Claude Exporter Bug Report — v${version}`);
|
||||
const body = encodeURIComponent('Describe the issue here. If this is a bug, please attach the diagnostics file generated from the Options page.\n\n');
|
||||
window.location.href = `mailto:agoramachina@gmail.com?subject=${subject}&body=${body}`;
|
||||
});
|
||||
|
||||
document.getElementById('generateDiagnosticsBtn').addEventListener('click', () => {
|
||||
generateDiagnostics((success, message) => {
|
||||
showStatus('contactStatus', message, success ? 'success' : 'error');
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('clearDiagnosticsBtn').addEventListener('click', () => {
|
||||
if (!confirm('Clear the captured error log? This cannot be undone.')) return;
|
||||
clearDiagnosticsLog((success, message) => {
|
||||
showStatus('contactStatus', message, success ? 'success' : 'error');
|
||||
});
|
||||
});
|
||||
|
||||
// Helper functions
|
||||
function showStatus(elementId, message, type) {
|
||||
const statusEl = document.getElementById(elementId);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
// Capture unhandled errors for diagnostics (sanitized, stored in chrome.storage.local)
|
||||
if (typeof initErrorCapture === 'function') initErrorCapture('popup');
|
||||
|
||||
// Get organization ID from storage (fallback)
|
||||
async function getStoredOrgId() {
|
||||
return new Promise((resolve) => {
|
||||
|
|
|
|||
136
firefox/utils.js
136
firefox/utils.js
|
|
@ -914,6 +914,141 @@ function importBackup(file, onComplete) {
|
|||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
// ----- Error capture & diagnostics -----
|
||||
// Captures unhandled errors and rejected promises into a ring buffer in
|
||||
// chrome.storage.local. The user can later download a sanitized diagnostics
|
||||
// bundle (Options page → Contact & Diagnostics) to attach to a bug report.
|
||||
// Sanitization runs at capture time: any UUID-looking substring (chat / org /
|
||||
// project IDs that may appear in fetch URLs or stack traces) is replaced with
|
||||
// "<id>" so we never persist identifiers.
|
||||
|
||||
const CE_UUID_REGEX = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;
|
||||
const CE_ERROR_LOG_MAX = 50;
|
||||
|
||||
function sanitizeForDiagnostics(value) {
|
||||
if (typeof value !== 'string') return value;
|
||||
return value.replace(CE_UUID_REGEX, '<id>');
|
||||
}
|
||||
|
||||
function initErrorCapture(context) {
|
||||
if (typeof chrome === 'undefined' || !chrome.storage || !chrome.storage.local) return;
|
||||
|
||||
// Re-entry guard: if our own push() throws, don't loop into the listener.
|
||||
let suppressed = false;
|
||||
|
||||
const push = (entry) => {
|
||||
if (suppressed) return;
|
||||
suppressed = true;
|
||||
try {
|
||||
chrome.storage.local.get(['errorLog'], (result) => {
|
||||
try {
|
||||
const log = Array.isArray(result.errorLog) ? result.errorLog : [];
|
||||
log.push(entry);
|
||||
if (log.length > CE_ERROR_LOG_MAX) {
|
||||
log.splice(0, log.length - CE_ERROR_LOG_MAX);
|
||||
}
|
||||
chrome.storage.local.set({ errorLog: log }, () => { suppressed = false; });
|
||||
} catch (e) { suppressed = false; }
|
||||
});
|
||||
} catch (e) { suppressed = false; }
|
||||
};
|
||||
|
||||
const target = (typeof globalThis !== 'undefined') ? globalThis : self;
|
||||
|
||||
target.addEventListener('error', (event) => {
|
||||
push({
|
||||
ts: new Date().toISOString(),
|
||||
level: 'error',
|
||||
context,
|
||||
msg: sanitizeForDiagnostics(String(event.message || '')),
|
||||
source: event.filename ? sanitizeForDiagnostics(String(event.filename)) : null,
|
||||
line: event.lineno || null,
|
||||
col: event.colno || null,
|
||||
stack: event.error && event.error.stack ? sanitizeForDiagnostics(String(event.error.stack)) : null
|
||||
});
|
||||
});
|
||||
|
||||
target.addEventListener('unhandledrejection', (event) => {
|
||||
const reason = event.reason;
|
||||
const msg = reason && reason.message ? String(reason.message)
|
||||
: (reason !== undefined ? String(reason) : '(no reason)');
|
||||
push({
|
||||
ts: new Date().toISOString(),
|
||||
level: 'unhandledrejection',
|
||||
context,
|
||||
msg: sanitizeForDiagnostics(msg),
|
||||
stack: reason && reason.stack ? sanitizeForDiagnostics(String(reason.stack)) : null
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Build a sanitized diagnostics bundle and trigger a download. Callers may
|
||||
// pass an onComplete(success, message) callback for status reporting.
|
||||
function generateDiagnostics(onComplete) {
|
||||
const manifest = chrome.runtime.getManifest();
|
||||
|
||||
chrome.storage.local.get(
|
||||
['errorLog', 'modelSnapshots', 'exportTimestamps', 'dateFormat', 'timeFormat', 'modelDisplay'],
|
||||
(local) => {
|
||||
chrome.storage.sync.get(['organizationId'], (sync) => {
|
||||
const errorLog = Array.isArray(local.errorLog) ? local.errorLog : [];
|
||||
const diagnostics = {
|
||||
_meta: {
|
||||
app: 'claude-exporter',
|
||||
diagnosticsVersion: 1,
|
||||
generatedAt: new Date().toISOString()
|
||||
},
|
||||
extension: {
|
||||
name: manifest.name,
|
||||
version: manifest.version
|
||||
},
|
||||
environment: {
|
||||
userAgent: (typeof navigator !== 'undefined' && navigator.userAgent) || null,
|
||||
platform: (typeof navigator !== 'undefined' && navigator.platform) || null,
|
||||
language: (typeof navigator !== 'undefined' && navigator.language) || null
|
||||
},
|
||||
preferences: {
|
||||
dateFormat: local.dateFormat || 'mdy',
|
||||
timeFormat: local.timeFormat || '12h',
|
||||
modelDisplay: local.modelDisplay === 'current' ? 'current' : 'original',
|
||||
orgIdConfigured: !!(sync && sync.organizationId)
|
||||
},
|
||||
counts: {
|
||||
modelSnapshots: Object.keys(local.modelSnapshots || {}).length,
|
||||
exportTimestamps: Object.keys(local.exportTimestamps || {}).length,
|
||||
errors: errorLog.length
|
||||
},
|
||||
errors: errorLog
|
||||
};
|
||||
|
||||
const now = new Date();
|
||||
const ymd = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}`;
|
||||
const hms = `${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}${String(now.getSeconds()).padStart(2, '0')}`;
|
||||
|
||||
const blob = new Blob([JSON.stringify(diagnostics, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `claude-exporter-diagnostics-${ymd}-${hms}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
if (onComplete) {
|
||||
onComplete(true, `Diagnostics downloaded — ${errorLog.length} error(s) captured, all IDs redacted.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function clearDiagnosticsLog(onComplete) {
|
||||
chrome.storage.local.set({ errorLog: [] }, () => {
|
||||
if (onComplete) onComplete(true, 'Diagnostics log cleared.');
|
||||
});
|
||||
}
|
||||
|
||||
// Functions are available globally in the browser context
|
||||
// In Node (vitest), expose them via module.exports for testing
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
|
|
@ -936,5 +1071,6 @@ if (typeof module !== 'undefined' && module.exports) {
|
|||
backupExtensionData,
|
||||
importBackup,
|
||||
mergeStorageData,
|
||||
sanitizeForDiagnostics,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue