diff --git a/packages/docs/site/docs/13-contributing/07-crash-reports.md b/packages/docs/site/docs/13-contributing/07-crash-reports.md index 927e644031..89e8420ea7 100644 --- a/packages/docs/site/docs/13-contributing/07-crash-reports.md +++ b/packages/docs/site/docs/13-contributing/07-crash-reports.md @@ -12,7 +12,7 @@ When Playground crashes on Playground.WordPress.net users are able to submit a c ## Development Logs are sent to the [logger API on Playground.WordPress.net](https://github.com/WordPress/wordpress-playground/blob/c52d7dbd94dbe3ffc57adde4d9844545ade97f93/packages/playground/website/public/logger.php). The logger API is a simple REST API that accepts a POST request with a `message` parameter. -The API validates the message and then sends it to the [#playground-logs channel on the Making WordPress Slack(https://wordpress.slack.com/archives/C06Q5DCKZ3L). +The API validates the message and then sends it to the [#playground-logs channel on the Making WordPress Slack](https://wordpress.slack.com/archives/C06Q5DCKZ3L). ### Slack app diff --git a/packages/php-wasm/logger/src/lib/logger.ts b/packages/php-wasm/logger/src/lib/logger.ts index e3908e9cdf..1757f69891 100644 --- a/packages/php-wasm/logger/src/lib/logger.ts +++ b/packages/php-wasm/logger/src/lib/logger.ts @@ -127,6 +127,7 @@ export class Logger extends EventTarget { new CustomEvent(this.fatalErrorEvent, { detail: { logs: this.getLogs(), + source: event.source, }, }) ); diff --git a/packages/php-wasm/universal/src/lib/base-php.ts b/packages/php-wasm/universal/src/lib/base-php.ts index 03fdaa202a..2a6568ba8f 100644 --- a/packages/php-wasm/universal/src/lib/base-php.ts +++ b/packages/php-wasm/universal/src/lib/base-php.ts @@ -288,6 +288,8 @@ export abstract class BasePHP implements IsomorphicLocalPHP { ); // @ts-ignore error.output = output; + // @ts-ignore + error.source = 'request'; console.error(error); throw error; } @@ -296,6 +298,8 @@ export abstract class BasePHP implements IsomorphicLocalPHP { this.dispatchEvent({ type: 'request.error', error: e as Error, + // Distinguish between PHP request and PHP-wasm errors + source: (e as any).source ?? 'php-wasm', }); throw e; } finally { diff --git a/packages/php-wasm/universal/src/lib/universal-php.ts b/packages/php-wasm/universal/src/lib/universal-php.ts index dee30b2ae4..1bbc2b7757 100644 --- a/packages/php-wasm/universal/src/lib/universal-php.ts +++ b/packages/php-wasm/universal/src/lib/universal-php.ts @@ -14,6 +14,7 @@ export interface PHPRequestEndEvent { export interface PHPRequestErrorEvent { type: 'request.error'; error: Error; + source?: 'request' | 'php-wasm'; } /** diff --git a/packages/playground/website/.htaccess b/packages/playground/website/.htaccess index 48d804b221..add2396e67 100644 --- a/packages/playground/website/.htaccess +++ b/packages/playground/website/.htaccess @@ -5,7 +5,7 @@ AddEncoding x-gzip .gz Header unset ETag Header set Cache-Control "max-age=0, no-cache, no-store, must-revalidate" - + Header set Access-Control-Allow-Origin "*" Header unset ETag Header set Cache-Control "max-age=0, no-cache, no-store, must-revalidate" diff --git a/packages/playground/website/public/logger.php b/packages/playground/website/public/logger.php index ad80a97daa..4f99e8ee2c 100644 --- a/packages/playground/website/public/logger.php +++ b/packages/playground/website/public/logger.php @@ -40,52 +40,24 @@ function response($ok, $error = null) die(json_encode($response_data)); } -/** - * Validate the message format - * - * @param string $message - The message to validate - * @return bool - If the message is valid - */ -function validate_message($message) -{ - // Validate description. Description is required - preg_match('/(?<=What happened\?\n\n)(.*?)(?=\n\nLogs)/s', $message, $description); - if (empty($description)) { - return false; - } - - // Validate logs if exists. Logs need to match the PHP error log format - preg_match('/(?<=Logs\n\n)(.*?)(?=\n\nUrl)/s', $message, $logs); - if (!empty($logs)) { - $logs = $logs[0]; - if (preg_match('/\[\d{2}-[A-Za-z]{3}-\d{4} \d{2}:\d{2}:\d{2} UTC\](.*)/s', $logs) !== 1) { - return false; - } - } - - // Validate URL if exists - preg_match('/(?<=Url\n\n)(.*)/s', $message, $url); - if (!empty($url)) { - $url = $url[0]; - if (filter_var($url, FILTER_VALIDATE_URL) === false) { - return false; - } - } - - return true; -} - if (empty($token)) { response(false, 'No token provided'); } -if (!isset($_POST['message'])) { - response(false, 'No message provided'); +if (!isset($_POST['description']) || empty($_POST['description'])) { + response(false, 'No description provided'); } -$text = $_POST['message']; +$text = "What happened?\n\n" . $_POST['description']; -if (!validate_message($text)) { - response(false, 'Invalid message'); +if (isset($_POST['logs']) && !empty($_POST['logs'])) { + $text .= "\n\nLogs\n\n" . $_POST['logs']; +} + +if (isset($_POST['url'])) { + if (filter_var($_POST['url'], FILTER_VALIDATE_URL) === false) { + response(false, 'Invalid URL'); + } + $text .= "\n\nUrl\n\n" . $_POST['url']; } $text = urlencode($text); diff --git a/packages/playground/website/src/components/error-report-modal/index.tsx b/packages/playground/website/src/components/error-report-modal/index.tsx new file mode 100644 index 0000000000..3783512ca6 --- /dev/null +++ b/packages/playground/website/src/components/error-report-modal/index.tsx @@ -0,0 +1,182 @@ +import { useEffect, useState } from 'react'; +import Modal from '../modal'; +import { addFatalErrorListener, logger } from '@php-wasm/logger'; +import { Button, TextareaControl, TextControl } from '@wordpress/components'; + +import css from './style.module.css'; + +import { usePlaygroundContext } from '../../playground-context'; + +export function ErrorReportModal() { + const { showErrorModal, setShowErrorModal } = usePlaygroundContext(); + const [loading, setLoading] = useState(false); + const [text, setText] = useState(''); + const [logs, setLogs] = useState(''); + const [url, setUrl] = useState(''); + const [submitted, setSubmitted] = useState(false); + const [submitError, setSubmitError] = useState(''); + + useEffect(() => { + addFatalErrorListener(logger, (e) => { + const error = e as CustomEvent; + if (error.detail?.source === 'php-wasm') { + setShowErrorModal(true); + } + }); + }, [setShowErrorModal]); + + useEffect(() => { + resetForm(); + if (showErrorModal) { + setLogs(logger.getLogs().join('')); + setUrl(window.location.href); + } + }, [showErrorModal, setShowErrorModal, logs, setLogs]); + + function resetForm() { + setText(''); + setLogs(''); + setUrl(''); + } + + function resetSubmission() { + setSubmitted(false); + setSubmitError(''); + } + + function onClose() { + setShowErrorModal(false); + resetForm(); + resetSubmission(); + } + + async function onSubmit() { + setLoading(true); + const formdata = new FormData(); + formdata.append('description', text); + if (logs) { + formdata.append('logs', logs); + } + if (url) { + formdata.append('url', url); + } + try { + const response = await fetch( + 'https://playground.wordpress.net/logger.php', + { + method: 'POST', + body: formdata, + } + ); + setSubmitted(true); + + const body = await response.json(); + if (!body.ok) { + throw new Error(body.error); + } + + setSubmitError(''); + resetForm(); + } catch (e) { + setSubmitError((e as Error).message); + } finally { + setLoading(false); + } + } + + function getTitle() { + if (!submitted) { + return 'Report error'; + } else if (submitError) { + return 'Failed to report the error'; + } else { + return 'Thank you for reporting the error'; + } + } + + function getContent() { + if (!submitted) { + return ( + <> + Playground crashed because of an error. You can help resolve + the issue by sharing the error details with us. + + ); + } else if (submitError) { + return ( + <> + We were unable to submit the error report. Please try again + or open an{' '} + + issue on GitHub. + + + ); + } else { + return ( + <> + Your report has been submitted to the{' '} + + Making WordPress #playground-logs Slack channel + {' '} + and will be reviewed by the team. + + ); + } + } + + /** + * Show the form if the error has not been submitted or if there was an error submitting it. + * + * @return {boolean} + */ + function showForm() { + return !submitted || submitError; + } + + return ( + +
+

{getTitle()}

+

{getContent()}

+
+ {showForm() && ( + <> +
+ + + + +
+
+ + +
+ + )} +
+ ); +} diff --git a/packages/playground/website/src/components/error-report-modal/style.module.css b/packages/playground/website/src/components/error-report-modal/style.module.css new file mode 100644 index 0000000000..45f82e7e16 --- /dev/null +++ b/packages/playground/website/src/components/error-report-modal/style.module.css @@ -0,0 +1,19 @@ +.error-report-modal__header h2 { + font-size: 20px; +} + +.error-report-modal__header p { + font-size: 13px; +} + +.error-report-modal__textarea textarea { + resize: vertical; + width: 100% !important; +} + +.error-report-modal__footer { + margin-top: 20px; +} +.error-report-modal__error { + margin: 16px 0 0; +} diff --git a/packages/playground/website/src/components/modal/style.module.css b/packages/playground/website/src/components/modal/style.module.css index 21edb4f7f0..23aa58deb3 100644 --- a/packages/playground/website/src/components/modal/style.module.css +++ b/packages/playground/website/src/components/modal/style.module.css @@ -20,5 +20,4 @@ padding: 15px; text-align: left; max-height: 90vh; - overflow: auto; } diff --git a/packages/playground/website/src/components/toolbar-buttons/report-error.tsx b/packages/playground/website/src/components/toolbar-buttons/report-error.tsx new file mode 100644 index 0000000000..5233d3e544 --- /dev/null +++ b/packages/playground/website/src/components/toolbar-buttons/report-error.tsx @@ -0,0 +1,23 @@ +import { MenuItem } from '@wordpress/components'; +import { bug } from '@wordpress/icons'; + +import { usePlaygroundContext } from '../../playground-context'; + +type Props = { onClose: () => void }; +export function ReportError({ onClose }: Props) { + const { setShowErrorModal } = usePlaygroundContext(); + return ( + { + setShowErrorModal(true); + onClose(); + }} + > + Report error + + ); +} diff --git a/packages/playground/website/src/main.tsx b/packages/playground/website/src/main.tsx index 4774644df4..36be325e46 100644 --- a/packages/playground/website/src/main.tsx +++ b/packages/playground/website/src/main.tsx @@ -16,6 +16,7 @@ import { StorageType, StorageTypes } from './types'; import { ResetSiteMenuItem } from './components/toolbar-buttons/reset-site'; import { DownloadAsZipMenuItem } from './components/toolbar-buttons/download-as-zip'; import { RestoreFromZipMenuItem } from './components/toolbar-buttons/restore-from-zip'; +import { ReportError } from './components/toolbar-buttons/report-error'; import { resolveBlueprint } from './lib/resolve-blueprint'; import { GithubImportMenuItem } from './components/toolbar-buttons/github-import-menu-item'; import { acquireOAuthTokenIfNeeded } from './github/acquire-oauth-token-if-needed'; @@ -27,6 +28,7 @@ import { ExportFormValues } from './github/github-export-form/form'; import { joinPaths } from '@php-wasm/util'; import { PlaygroundContext } from './playground-context'; import { collectWindowErrors, logger } from '@php-wasm/logger'; +import { ErrorReportModal } from './components/error-report-modal'; collectWindowErrors(logger); @@ -80,13 +82,17 @@ if (currentConfiguration.wp === '6.3') { acquireOAuthTokenIfNeeded(); function Main() { + const [showErrorModal, setShowErrorModal] = useState(false); const [githubExportFiles, setGithubExportFiles] = useState(); const [githubExportValues, setGithubExportValues] = useState< Partial >({}); return ( - + + + diff --git a/packages/playground/website/src/playground-context.tsx b/packages/playground/website/src/playground-context.tsx index ac837945b4..906b418dd2 100644 --- a/packages/playground/website/src/playground-context.tsx +++ b/packages/playground/website/src/playground-context.tsx @@ -1,6 +1,9 @@ -import { createContext } from 'react'; +import { createContext, useContext } from 'react'; import { StorageType } from './types'; export const PlaygroundContext = createContext<{ storage: StorageType; -}>({ storage: 'none' }); + showErrorModal: boolean; + setShowErrorModal: (show: boolean) => void; +}>({ storage: 'none', showErrorModal: false, setShowErrorModal: () => {} }); +export const usePlaygroundContext = () => useContext(PlaygroundContext);