Skip to content

Commit

Permalink
Website: Add error report modal (#1102)
Browse files Browse the repository at this point in the history
This PR is missing data submission and shouldn't be merged.

## What is this PR doing?

When a PHP request fails a modal will load that allows users to report
errors to us.

## What problem is it solving?

It will help us collect Playground errors that we need to improve
stability.

## How is the problem addressed?

By allowing users to submit error reports to us.

## Testing Instructions

- Checkout this branch
- Start the local dev server `npm run dev`
- Update [this line
](https://github.com/WordPress/wordpress-playground/pull/1102/files#diff-46008bd779d2dd5cb639272c824aa9529721d171731bc22c6b073c0d73aab3c0R9)to
be true by default
- Confirm that a modal is opened
- Modify the input data
- Click _Report error_
- See the[ submitted report in
Slack](https://wordpress.slack.com/archives/C06Q5DCKZ3L)

![Screenshot from 2024-03-22
10-42-32](https://github.com/WordPress/wordpress-playground/assets/1199991/d39c6acc-5694-4e8e-babc-f88aa90748ad)

---------

Co-authored-by: Adam Zielinski <adam@adamziel.com>
  • Loading branch information
bgrgicak and adamziel authored Mar 27, 2024
1 parent c632e97 commit fb0eadc
Show file tree
Hide file tree
Showing 12 changed files with 257 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions packages/php-wasm/logger/src/lib/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ export class Logger extends EventTarget {
new CustomEvent(this.fatalErrorEvent, {
detail: {
logs: this.getLogs(),
source: event.source,
},
})
);
Expand Down
4 changes: 4 additions & 0 deletions packages/php-wasm/universal/src/lib/base-php.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions packages/php-wasm/universal/src/lib/universal-php.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface PHPRequestEndEvent {
export interface PHPRequestErrorEvent {
type: 'request.error';
error: Error;
source?: 'request' | 'php-wasm';
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/playground/website/.htaccess
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ AddEncoding x-gzip .gz
Header unset ETag
Header set Cache-Control "max-age=0, no-cache, no-store, must-revalidate"
</FilesMatch>
<FilesMatch "index\.js|blueprint-schema\.json|wp-cli.phar">
<FilesMatch "index\.js|blueprint-schema\.json|logger.php|wp-cli.phar">
Header set Access-Control-Allow-Origin "*"
Header unset ETag
Header set Cache-Control "max-age=0, no-cache, no-store, must-revalidate"
Expand Down
52 changes: 12 additions & 40 deletions packages/playground/website/public/logger.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
@@ -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{' '}
<a href="https://github.com/WordPress/wordpress-playground/issues/">
issue on GitHub.
</a>
</>
);
} else {
return (
<>
Your report has been submitted to the{' '}
<a href="https://wordpress.slack.com/archives/C06Q5DCKZ3L">
Making WordPress #playground-logs Slack channel
</a>{' '}
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 (
<Modal isOpen={showErrorModal} onRequestClose={onClose}>
<header className={css.errorReportModalHeader}>
<h2>{getTitle()}</h2>
<p>{getContent()}</p>
</header>
{showForm() && (
<>
<main>
<TextareaControl
label="What happened?"
help="Describe what caused the error and how can we reproduce it."
value={text}
onChange={setText}
className={css.errorReportModalTextarea}
required={true}
/>
<TextareaControl
label="Logs"
value={logs}
onChange={setLogs}
className={css.errorReportModalTextarea}
/>

<TextControl
label="Url"
value={url}
onChange={setUrl}
/>
</main>
<footer className={css.errorReportModalFooter}>
<Button
variant="primary"
onClick={onSubmit}
isBusy={loading}
disabled={loading || !text}
>
Report error
</Button>
<Button onClick={onClose}>Cancel</Button>
</footer>
</>
)}
</Modal>
);
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,4 @@
padding: 15px;
text-align: left;
max-height: 90vh;
overflow: auto;
}
Original file line number Diff line number Diff line change
@@ -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 (
<MenuItem
icon={bug}
iconPosition="left"
data-cy="report-error"
aria-label="Report an error in Playground"
onClick={() => {
setShowErrorModal(true);
onClose();
}}
>
Report error
</MenuItem>
);
}
9 changes: 8 additions & 1 deletion packages/playground/website/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);

Expand Down Expand Up @@ -80,13 +82,17 @@ if (currentConfiguration.wp === '6.3') {
acquireOAuthTokenIfNeeded();

function Main() {
const [showErrorModal, setShowErrorModal] = useState(false);
const [githubExportFiles, setGithubExportFiles] = useState<any[]>();
const [githubExportValues, setGithubExportValues] = useState<
Partial<ExportFormValues>
>({});

return (
<PlaygroundContext.Provider value={{ storage }}>
<PlaygroundContext.Provider
value={{ storage, showErrorModal, setShowErrorModal }}
>
<ErrorReportModal />
<PlaygroundViewport
storage={storage}
displayMode={displayMode}
Expand Down Expand Up @@ -115,6 +121,7 @@ function Main() {
storage={currentConfiguration.storage}
onClose={onClose}
/>
<ReportError onClose={onClose} />
<DownloadAsZipMenuItem onClose={onClose} />
<RestoreFromZipMenuItem onClose={onClose} />
<GithubImportMenuItem onClose={onClose} />
Expand Down
Loading

0 comments on commit fb0eadc

Please sign in to comment.