Skip to content

Commit

Permalink
Add fatal errror listener (#1095)
Browse files Browse the repository at this point in the history
## 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_
  • Loading branch information
bgrgicak authored Mar 19, 2024
1 parent 2ee9663 commit 55a94bb
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 44 deletions.
5 changes: 4 additions & 1 deletion packages/php-wasm/logger/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
export * from './logger';
// PHP.wasm requires WordPress Playground's Node polyfills.
import '@php-wasm/node-polyfills';

export * from './lib/logger';
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -26,6 +39,7 @@ export class Logger {
private errorLogPath = '/wordpress/wp-content/debug.log';

constructor(errorLogPath?: string) {
super();
if (errorLogPath) {
this.errorLogPath = errorLogPath;
}
Expand All @@ -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'
);
}

Expand All @@ -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');
}

/**
Expand Down Expand Up @@ -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(),
},
})
);
}
});
}

/**
Expand Down Expand Up @@ -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);
}

Expand All @@ -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;
}
}

/**
Expand All @@ -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);
}
23 changes: 23 additions & 0 deletions packages/php-wasm/logger/src/test/logger.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
6 changes: 6 additions & 0 deletions packages/php-wasm/universal/src/lib/base-php.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
9 changes: 9 additions & 0 deletions packages/php-wasm/universal/src/lib/universal-php.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -29,6 +37,7 @@ export interface PHPRuntimeBeforeDestroyEvent {
*/
export type PHPEvent =
| PHPRequestEndEvent
| PHPRequestErrorEvent
| PHPRuntimeInitializedEvent
| PHPRuntimeBeforeDestroyEvent;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -247,7 +226,3 @@ export function getRequestHeaders(request: Request) {
});
return headers;
}

function getRelativePart(url: URL): string {
return url.toString().substring(url.origin.length);
}
1 change: 0 additions & 1 deletion packages/php-wasm/web/src/lib/register-service-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 3 additions & 5 deletions packages/playground/website/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -83,10 +85,6 @@ function Main() {
Partial<ExportFormValues>
>({});

useEffect(() => {
collectWindowErrors(logger);
}, []);

return (
<PlaygroundContext.Provider value={{ storage }}>
<PlaygroundViewport
Expand Down

0 comments on commit 55a94bb

Please sign in to comment.