From 55a94bb6fba2aa26305e78007af1dd36cdb86f65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?berislav=20grgi=C4=8Dak?= Date: Tue, 19 Mar 2024 12:02:35 +0100 Subject: [PATCH] Add fatal errror listener (#1095) ## What is this PR doing? It allows developers to listen for fatal Playground errors and collect logs. ## What problem is it solving? This will allow Playground to collect fatal errors from user sessions in the future. It will also allow developers who use `startPlaygroundWeb` to listen for errors and create custom workflows. ## How is the problem addressed? All logs are now collected inside the logger including PHP request errors. When a PHP request error happens the logger dispatches a custom event that developers can listen. ## Testing Instructions - Checkout this branch - Start Playground `npm run dev` - Run Playground with a fatal error http://localhost:5400/website-server/#{%20%22landingPage%22:%20%22/wp-admin/%22,%20%22phpExtensionBundles%22:%20[%20%22kitchen-sink%22%20],%20%22features%22:%20{%20%22networking%22:%20true%20},%20%22steps%22:%20[%20{%20%22step%22:%20%22login%22%20},%20{%20%22step%22:%20%22writeFile%22,%20%22path%22:%20%22/wordpress/wp-content/mu-plugins/rewrite.php%22,%20%22data%22:%20%22%3C?php%20add_action(%20'shutdown',%20function()%20{%20post_message_to_js('test');%20}%20);%22%20}%20]%20} - Open the browser console and search verbose/debug logs for _PHP-WASM Fatal_ - There should be a error message from the logger - Add this code to the end of `packages/playground/website/src/main.tsx` ``` addFatalErrorListener(logger, (e) => { console.log('Playground logs', (e as CustomEvent).detail.logs); }); ``` - Reload Playground and confirm that the even has triggered with log data by checking the browser console for _Playground logs_ --- packages/php-wasm/logger/src/index.ts | 5 +- .../php-wasm/logger/src/{ => lib}/logger.ts | 91 ++++++++++++++++--- .../php-wasm/logger/src/test/logger.spec.ts | 23 +++++ .../php-wasm/universal/src/lib/base-php.ts | 6 ++ .../universal/src/lib/universal-php.ts | 9 ++ .../src/initialize-service-worker.ts | 27 +----- .../web/src/lib/register-service-worker.ts | 1 - packages/playground/website/src/main.tsx | 8 +- 8 files changed, 126 insertions(+), 44 deletions(-) rename packages/php-wasm/logger/src/{ => lib}/logger.ts (67%) create mode 100644 packages/php-wasm/logger/src/test/logger.spec.ts diff --git a/packages/php-wasm/logger/src/index.ts b/packages/php-wasm/logger/src/index.ts index 1ff09efd40..544a49c71b 100644 --- a/packages/php-wasm/logger/src/index.ts +++ b/packages/php-wasm/logger/src/index.ts @@ -1 +1,4 @@ -export * from './logger'; +// PHP.wasm requires WordPress Playground's Node polyfills. +import '@php-wasm/node-polyfills'; + +export * from './lib/logger'; diff --git a/packages/php-wasm/logger/src/logger.ts b/packages/php-wasm/logger/src/lib/logger.ts similarity index 67% rename from packages/php-wasm/logger/src/logger.ts rename to packages/php-wasm/logger/src/lib/logger.ts index 777edd1038..e3908e9cdf 100644 --- a/packages/php-wasm/logger/src/logger.ts +++ b/packages/php-wasm/logger/src/lib/logger.ts @@ -1,14 +1,27 @@ -import { UniversalPHP } from '@php-wasm/universal/src/lib/universal-php'; +import { + PHPRequestErrorEvent, + UniversalPHP, +} from '@php-wasm/universal/src/lib/universal-php'; /** * Log severity levels. */ -export type LogSeverity = 'debug' | 'info' | 'warn' | 'error' | 'fatal'; +export type LogSeverity = 'Debug' | 'Info' | 'Warn' | 'Error' | 'Fatal'; + +/** + * Log prefix. + */ +export type LogPrefix = 'Playground' | 'PHP-WASM'; /** * A logger for Playground. */ -export class Logger { - private readonly LOG_PREFIX = 'Playground'; +export class Logger extends EventTarget { + public readonly fatalErrorEvent = 'playground-fatal-error'; + + /** + * Log messages + */ + private logs: string[] = []; /** * Whether the window events are connected. @@ -26,6 +39,7 @@ export class Logger { private errorLogPath = '/wordpress/wp-content/debug.log'; constructor(errorLogPath?: string) { + super(); if (errorLogPath) { this.errorLogPath = errorLogPath; } @@ -52,7 +66,7 @@ export class Logger { private logWindowError(event: ErrorEvent) { this.log( `${event.message} in ${event.filename} on line ${event.lineno}:${event.colno}`, - 'fatal' + 'Error' ); } @@ -62,7 +76,7 @@ export class Logger { * @param PromiseRejectionEvent event */ private logUnhandledRejection(event: PromiseRejectionEvent) { - this.log(`${event.reason.stack}`, 'fatal'); + this.log(`${event.reason.stack}`, 'Error'); } /** @@ -101,6 +115,23 @@ export class Logger { this.lastPHPLogLength = log.length; } }); + playground.addEventListener('request.error', (event) => { + event = event as PHPRequestErrorEvent; + if (event.error) { + this.log( + `${event.error.message} ${event.error.stack}`, + 'Fatal', + 'PHP-WASM' + ); + this.dispatchEvent( + new CustomEvent(this.fatalErrorEvent, { + detail: { + logs: this.getLogs(), + }, + }) + ); + } + }); } /** @@ -134,22 +165,36 @@ export class Logger { * Format log message and severity and log it. * @param string message * @param LogSeverity severity + * @param string prefix */ - public formatMessage(message: string, severity: LogSeverity): string { + public formatMessage( + message: string, + severity: LogSeverity, + prefix: string + ): string { const now = this.formatLogDate(new Date()); - return `[${now}] ${this.LOG_PREFIX} ${severity}: ${message}`; + return `[${now}] ${prefix} ${severity}: ${message}`; } /** * Log message with severity and timestamp. * @param string message * @param LogSeverity severity + * @param string prefix */ - public log(message: string, severity?: LogSeverity): void { + public log( + message: string, + severity?: LogSeverity, + prefix?: LogPrefix + ): void { if (severity === undefined) { - severity = 'info'; + severity = 'Info'; } - const log = this.formatMessage(message, severity); + const log = this.formatMessage( + message, + severity, + prefix ?? 'Playground' + ); this.logRaw(log); } @@ -158,8 +203,17 @@ export class Logger { * @param string log */ public logRaw(log: string): void { + this.logs.push(log); console.debug(log); } + + /** + * Get all logs. + * @returns string[] + */ + public getLogs(): string[] { + return this.logs; + } } /** @@ -186,3 +240,18 @@ export function collectPhpLogs( ) { loggerInstance.addPlaygroundRequestEndListener(playground); } + +/** + * Add a listener for the fatal Playground errors. + * These errors include Playground errors like Asyncify errors. PHP errors won't trigger this event. + * The callback function will receive an Event object with logs in the detail property. + * + * @param loggerInstance The logger instance + * @param callback The callback function + */ +export function addFatalErrorListener( + loggerInstance: Logger, + callback: EventListenerOrEventListenerObject +) { + loggerInstance.addEventListener(loggerInstance.fatalErrorEvent, callback); +} diff --git a/packages/php-wasm/logger/src/test/logger.spec.ts b/packages/php-wasm/logger/src/test/logger.spec.ts new file mode 100644 index 0000000000..cf10435958 --- /dev/null +++ b/packages/php-wasm/logger/src/test/logger.spec.ts @@ -0,0 +1,23 @@ +import { NodePHP } from '@php-wasm/node'; +import { LatestSupportedPHPVersion } from '@php-wasm/universal'; +import { logger, addFatalErrorListener, collectPhpLogs } from '../lib/logger'; + +describe('Logger', () => { + let php: NodePHP; + beforeEach(async () => { + php = await NodePHP.load(LatestSupportedPHPVersion); + }); + it('Event listener should work', () => { + const listener = vi.fn(); + collectPhpLogs(logger, php); + addFatalErrorListener(logger, listener); + php.dispatchEvent({ + type: 'request.error', + error: new Error('test'), + }); + expect(listener).toBeCalledTimes(1); + + const logs = logger.getLogs(); + expect(logs.length).toBe(1); + }); +}); diff --git a/packages/php-wasm/universal/src/lib/base-php.ts b/packages/php-wasm/universal/src/lib/base-php.ts index 2c5ea22bb8..03fdaa202a 100644 --- a/packages/php-wasm/universal/src/lib/base-php.ts +++ b/packages/php-wasm/universal/src/lib/base-php.ts @@ -292,6 +292,12 @@ export abstract class BasePHP implements IsomorphicLocalPHP { throw error; } return response; + } catch (e) { + this.dispatchEvent({ + type: 'request.error', + error: e as Error, + }); + throw e; } finally { try { if (heapBodyPointer) { diff --git a/packages/php-wasm/universal/src/lib/universal-php.ts b/packages/php-wasm/universal/src/lib/universal-php.ts index 965b6e8040..dee30b2ae4 100644 --- a/packages/php-wasm/universal/src/lib/universal-php.ts +++ b/packages/php-wasm/universal/src/lib/universal-php.ts @@ -8,6 +8,14 @@ export interface PHPRequestEndEvent { type: 'request.end'; } +/** + * Represents an error event related to the PHP request. + */ +export interface PHPRequestErrorEvent { + type: 'request.error'; + error: Error; +} + /** * Represents a PHP runtime initialization event. */ @@ -29,6 +37,7 @@ export interface PHPRuntimeBeforeDestroyEvent { */ export type PHPEvent = | PHPRequestEndEvent + | PHPRequestErrorEvent | PHPRuntimeInitializedEvent | PHPRuntimeBeforeDestroyEvent; diff --git a/packages/php-wasm/web-service-worker/src/initialize-service-worker.ts b/packages/php-wasm/web-service-worker/src/initialize-service-worker.ts index 8033ce67fa..c36af3ecbc 100644 --- a/packages/php-wasm/web-service-worker/src/initialize-service-worker.ts +++ b/packages/php-wasm/web-service-worker/src/initialize-service-worker.ts @@ -2,12 +2,7 @@ declare const self: ServiceWorkerGlobalScope; import { awaitReply, getNextRequestId } from './messaging'; -import { - getURLScope, - isURLScoped, - removeURLScope, - setURLScope, -} from '@php-wasm/scopes'; +import { getURLScope, isURLScoped, setURLScope } from '@php-wasm/scopes'; /** * Run this function in the service worker to install the required event @@ -45,12 +40,6 @@ export function initializeServiceWorker(config: ServiceWorkerConfiguration) { return; } } - - console.debug( - `[ServiceWorker] Serving request: ${getRelativePart( - removeURLScope(url) - )}` - ); const responsePromise = handleRequest(event); if (responsePromise) { event.respondWith(responsePromise); @@ -122,22 +111,12 @@ export async function convertFetchEventToPHPRequest(event: FetchEvent) { `The URL ${url.toString()} is not scoped. This should not happen.` ); } - console.debug( - '[ServiceWorker] Forwarding a request to the Worker Thread', - { - message, - } - ); const requestId = await broadcastMessageExpectReply(message, scope); phpResponse = await awaitReply(self, requestId); // X-frame-options gets in a way when PHP is // being displayed in an iframe. delete phpResponse.headers['x-frame-options']; - - console.debug('[ServiceWorker] Response received from the main app', { - phpResponse, - }); } catch (e) { console.error(e, { url: url.toString() }); throw e; @@ -247,7 +226,3 @@ export function getRequestHeaders(request: Request) { }); return headers; } - -function getRelativePart(url: URL): string { - return url.toString().substring(url.origin.length); -} diff --git a/packages/php-wasm/web/src/lib/register-service-worker.ts b/packages/php-wasm/web/src/lib/register-service-worker.ts index 4853585024..2fa3cb0610 100644 --- a/packages/php-wasm/web/src/lib/register-service-worker.ts +++ b/packages/php-wasm/web/src/lib/register-service-worker.ts @@ -48,7 +48,6 @@ export async function registerServiceWorker< navigator.serviceWorker.addEventListener( 'message', async function onMessage(event) { - console.debug('[window][sw] Message from ServiceWorker', event); /** * Ignore events meant for other PHP instances to * avoid handling the same event twice. diff --git a/packages/playground/website/src/main.tsx b/packages/playground/website/src/main.tsx index c703e574ee..4774644df4 100644 --- a/packages/playground/website/src/main.tsx +++ b/packages/playground/website/src/main.tsx @@ -22,12 +22,14 @@ import { acquireOAuthTokenIfNeeded } from './github/acquire-oauth-token-if-neede import { GithubImportModal } from './github/github-import-form'; import { GithubExportMenuItem } from './components/toolbar-buttons/github-export-menu-item'; import { GithubExportModal } from './github/github-export-form'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; 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'; +collectWindowErrors(logger); + const query = new URL(document.location.href).searchParams; const blueprint = await resolveBlueprint(); @@ -83,10 +85,6 @@ function Main() { Partial >({}); - useEffect(() => { - collectWindowErrors(logger); - }, []); - return (