diff --git a/src/components/crash-message/crash-message.jsx b/src/components/crash-message/crash-message.jsx index aa4a3a9b1ac..67b003e2e87 100644 --- a/src/components/crash-message/crash-message.jsx +++ b/src/components/crash-message/crash-message.jsx @@ -5,6 +5,7 @@ import {FormattedMessage} from 'react-intl'; import styles from './crash-message.css'; import reloadIcon from './reload.svg'; +import { downloadLogs } from '../../lib/pm-log-capture.js'; const CrashMessage = props => (
@@ -25,7 +26,7 @@ const CrashMessage = props => ( defaultMessage={'We are so sorry, but it looks like the page has crashed.' + ' Please refresh your page to try' + ' again.' + - ' If the problem persists, please report this error to our Discord.'} + ' If the problem persists, please report the downloadable error below to our Discord.'} description="Message to inform the user that page has crashed." id="tw.gui.crashMessage.description" /> @@ -57,6 +58,12 @@ const CrashMessage = props => ( id="gui.crashMessage.reload" /> +
); diff --git a/src/components/menu-bar/menu-bar.jsx b/src/components/menu-bar/menu-bar.jsx index 7a504d204c7..7c8d216a1c0 100644 --- a/src/components/menu-bar/menu-bar.jsx +++ b/src/components/menu-bar/menu-bar.jsx @@ -85,7 +85,7 @@ import sharedMessages from '../../lib/shared-messages'; import SeeInsideButton from './tw-see-inside.jsx'; import { notScratchDesktop } from '../../lib/isScratchDesktop.js'; -//import { consoleLogs } from '../../lib/pm-log-capture.js'; +import { downloadLogs } from '../../lib/pm-log-capture.js'; const ariaMessages = defineMessages({ language: { @@ -207,6 +207,7 @@ class MenuBar extends React.Component { 'handleClickPackager', 'handleClickRestorePoints', 'handleClickSeeCommunity', + 'handleClickDownloadLogs', 'handleClickShare', 'handleKeyPress', 'handleLanguageMouseUp', @@ -422,21 +423,7 @@ class MenuBar extends React.Component { this.props.onRequestCloseAbout(); }; } - /* - - hidden until this is actually helpful for developers - - unhide when a solution is found for not blocking error tracking/using 3rd parties - handleClickDownloadLogs() { - const str = JSON.stringify(consoleLogs); - const a = document.createElement('a'); - a.style.display = 'none'; - document.body.append(a); - const url = window.URL.createObjectURL(new Blob([str])); - a.href = url; - a.download = 'pm-log-trace.json'; - a.click(); - window.URL.revokeObjectURL(url); - a.remove(); - }*/ + handleClickDownloadLogs() { downloadLogs(); } render() { const saveNowMessage = ( + + Download Logs + diff --git a/src/lib/analytics.js b/src/lib/analytics.js index b123c673c1f..5b795e69a52 100644 --- a/src/lib/analytics.js +++ b/src/lib/analytics.js @@ -4,3 +4,4 @@ const GoogleAnalytics = { }; export default GoogleAnalytics; + \ No newline at end of file diff --git a/src/lib/pm-log-capture.js b/src/lib/pm-log-capture.js index e22189b3da5..2e59c21baa8 100644 --- a/src/lib/pm-log-capture.js +++ b/src/lib/pm-log-capture.js @@ -1,3 +1,6 @@ +import JSZip from 'jszip'; +import uid from './uid'; + /** * String.prototype.indexOf, but it returns NaN not -1 on failure * @param {string} str The string to check in @@ -56,7 +59,7 @@ const push = (type, message, trace) => { trace }); }; -const _parseFirefoxStack = stack => stack.split('\n') +const _parseFirefoxStack = stack => stack.split('\n').slice(1) .map(line => { const at = line.indexOf('@'); const secondCol = line.lastIndexOf(':'); @@ -87,7 +90,7 @@ const _parseFirefoxStack = stack => stack.split('\n') origin }; }); -const _parseChromeStack = stack => stack.split('\n').slice(1) +const _parseChromeStack = stack => stack.split('\n').slice(2) .map(line => { // we have no use for the human readable fluff line = line.slice(7); @@ -142,18 +145,83 @@ const parseStack = (stack, url, line, column) => { if (stack.split('\n', 2)[0].includes('@')) return _parseFirefoxStack(stack); return _parseChromeStack(stack); }; +const downloadLogs = async () => { + const files = new JSZip(); + files.file('logs.json', JSON.stringify(consoleLogs)); + const index = {}; + // get files + // sadly, this may just dead ass fail to get files due to blob lifecycle + // and i dont want to make these files get stored at runtime, cause poopy doo doo ram + for (const log of consoleLogs) { + for (const trace of log.trace) { + if (index[trace.url]) continue; + const id = uid(); + const content = await fetch(trace.url) + .then(res => res.ok ? res.text() : null) + .catch(() => {}); + if (!content) continue; + files.file(id, content); + index[trace.url] = id; + } + } + files.file('index.json', JSON.stringify(index)); + let blob = await files.generateAsync({ type: 'blob', compression: 'DEFLATE' }); + let filename = 'pm-error-download.pml'; + /* actually, this is a bad idea + // if we can, include the project + if (vm) { + filename = 'pm-error-download.pmp'; + const archive = vm._saveProjectZip(); + archive.file('logs.json', blob); + blob = await archive.generateAsync({ + type: 'blob', + mimeType: 'application/x.scratch.sb3', + compression: 'DEFLATE' + }); + } + */ + const a = document.createElement('a'); + a.style.display = 'none'; + document.body.append(a); + const url = window.URL.createObjectURL(blob); + a.href = url; + a.download = filename; + a.click(); + window.URL.revokeObjectURL(url); + a.remove(); +}; +window.downloadLogs = downloadLogs; window.addEventListener('error', e => push('error', e.message, parseStack(e.error.stack, e.filename, e.lineno, e.colno))); window.addEventListener('unhandledrejection', e => push('promiseError', e.reason, [])); -for (const name of ['log', 'warn', 'error', 'debug', 'info']) { - const item = window.console[name]; - window.console[name] = (...args) => { - let stack = []; - if (browserHasStack) stack = parseStack(new Error().stack); - push(name, args, stack); - item(...args); - }; +class StackTrace extends Error { + constructor() { + super(''); + if (this.stack.split('\n', 2)[0].includes('@')) + this.stack = this.stack + .split('\n') + .slice(2, 3) + .join('\n'); + else { + // chrome is weird ngl + const lines = this.stack + .split('\n') + .slice(0, 3); + lines.splice(1, 2); + this.stack = lines.join('\n'); + } + } } - -export { consoleLogs, parseStack, push }; +if (!String(window.location.href).startsWith(`http://localhost:`)) { + for (const name of ['log', 'warn', 'error', 'debug', 'info']) { + const item = window.console[name]; + window.console[name] = (...args) => { + let stack = []; + if (browserHasStack) stack = parseStack(new Error().stack); + push(name, args, stack); + item.call(console, ...args, new StackTrace()); + }; + } +} +export { consoleLogs, parseStack, push, downloadLogs }; diff --git a/src/lib/uid.js b/src/lib/uid.js index 187ab3b50e4..2c8d093841b 100644 --- a/src/lib/uid.js +++ b/src/lib/uid.js @@ -6,10 +6,10 @@ * Legal characters for the unique ID. * Should be all on a US keyboard. No XML special characters or control codes. * Removed $ due to issue 251. + * Removed all symbols due to use in files * @private */ -const soup_ = '!#%()*+,-./:;=?@[]^_`{|}~' + - 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; +const soup_ = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; /** * Generate a unique ID, from Blockly. This should be globally unique.