Add remote logging tool to beta tester (#50425)

* Add remote logging beta tester tool

* chore: Update log method return type to Promise<boolean>

* Update pnpm-lock.yaml

* Reformat

* Check window.wcSettings?.isRemoteLoggingEnabled

* Add changelogs

* Fix test

* Update toggle_remote_logging

* Fix toggle_remote_logging

* Improve message

* Fix lint
This commit is contained in:
Chi-Hsuan Huang 2024-08-08 11:12:51 +08:00 committed by GitHub
parent e4bb2c2317
commit 450a4ce3bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 719 additions and 144 deletions

View File

@ -75,7 +75,7 @@ addFilter(
### API Reference
- `init(config: RemoteLoggerConfig): void`: Initializes the remote logger with the given configuration.
- `log(severity: LogSeverity, message: string, extraData?: object): Promise<void>`: Logs a message with the specified severity and optional extra data.
- `log(severity: LogSeverity, message: string, extraData?: object): Promise<boolean>`: Logs a message with the specified severity and optional extra data.
- `captureException(error: Error, extraData?: object): void`: Captures an error and sends it to the remote API.
For more detailed information about types and interfaces, refer to the source code and inline documentation.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: tweak
Tweak logic for adding remote logging tool in beta tester

View File

@ -69,10 +69,10 @@ export class RemoteLogger {
severity: Exclude< LogData[ 'severity' ], undefined >,
message: string,
extraData?: Partial< Exclude< LogData, 'message' | 'severity' > >
) {
): Promise< boolean > {
if ( ! message ) {
debug( 'Empty message' );
return;
return false;
}
const logData: LogData = mergeLogData( DEFAULT_LOG_DATA, {
@ -82,7 +82,7 @@ export class RemoteLogger {
} );
debug( 'Logging:', logData );
await this.sendLog( logData );
return await this.sendLog( logData );
}
/**
@ -144,10 +144,10 @@ export class RemoteLogger {
*
* @param logData - The log data to be sent.
*/
private async sendLog( logData: LogData ): Promise< void > {
private async sendLog( logData: LogData ): Promise< boolean > {
if ( isDevelopmentEnvironment ) {
debug( 'Skipping send log in development environment' );
return;
return false;
}
const body = new window.FormData();
@ -166,13 +166,19 @@ export class RemoteLogger {
'https://public-api.wordpress.com/rest/v1.1/logstash'
) as string;
await window.fetch( endpoint, {
const response = await window.fetch( endpoint, {
method: 'POST',
body,
} );
if ( ! response.ok ) {
throw new Error( `response body: ${ response.body }` );
}
return true;
} catch ( error ) {
// eslint-disable-next-line no-console
console.error( 'Failed to send log to API:', error );
return false;
}
}
@ -368,13 +374,13 @@ let logger: RemoteLogger | null = null;
*
* @return {boolean} - Returns true if remote logging is enabled and the logger is initialized, otherwise false.
*/
function canLog(): boolean {
if ( ! getSetting( 'isRemoteLoggingEnabled', false ) ) {
function canLog( _logger: RemoteLogger | null ): _logger is RemoteLogger {
if ( ! window.wcSettings?.isRemoteLoggingEnabled ) {
debug( 'Remote logging is disabled.' );
return false;
}
if ( ! logger ) {
if ( ! _logger ) {
warnLog( 'RemoteLogger is not initialized. Call init() first.' );
return false;
}
@ -390,7 +396,7 @@ function canLog(): boolean {
*
*/
export function init( config: RemoteLoggerConfig ) {
if ( ! getSetting( 'isRemoteLoggingEnabled', false ) ) {
if ( ! window.wcSettings?.isRemoteLoggingEnabled ) {
debug( 'Remote logging is disabled.' );
return;
}
@ -424,15 +430,16 @@ export async function log(
severity: Exclude< LogData[ 'severity' ], undefined >,
message: string,
extraData?: Partial< Exclude< LogData, 'message' | 'severity' > >
): Promise< void > {
if ( ! canLog() ) {
return;
): Promise< boolean > {
if ( ! canLog( logger ) ) {
return false;
}
try {
await logger?.log( severity, message, extraData );
return await logger.log( severity, message, extraData );
} catch ( error ) {
errorLog( 'Failed to send log:', error );
return false;
}
}
@ -446,12 +453,12 @@ export async function captureException(
error: Error,
extraData?: Partial< LogData >
) {
if ( ! canLog() ) {
return;
if ( ! canLog( logger ) ) {
return false;
}
try {
await logger?.error( error, extraData );
await logger.error( error, extraData );
} catch ( _error ) {
errorLog( 'Failed to send log:', _error );
}

View File

@ -3,8 +3,6 @@
*/
import '@wordpress/jest-console';
import { addFilter, removeFilter } from '@wordpress/hooks';
import { getSetting } from '@woocommerce/settings';
/**
* Internal dependencies
*/
@ -18,12 +16,6 @@ import {
} from '../remote-logger';
import { fetchMock } from './__mocks__/fetch';
jest.mock( '@woocommerce/settings', () => ( {
getSetting: jest.fn().mockReturnValue( {
isRemoteLoggingEnabled: true,
} ),
} ) );
jest.mock( 'tracekit', () => ( {
computeStackTrace: jest.fn().mockReturnValue( {
name: 'Error',
@ -102,49 +94,57 @@ describe( 'RemoteLogger', () => {
} );
describe( 'error', () => {
it( 'should send an error to the API with default data', async () => {
const error = new Error( 'Test error' );
await logger.error( error );
it( 'should send an error to the API with default data', async () => {
const error = new Error( 'Test error' );
await logger.error( error );
expect( fetchMock ).toHaveBeenCalledWith(
'https://public-api.wordpress.com/rest/v1.1/js-error',
expect.objectContaining( {
method: 'POST',
body: expect.any( FormData ),
} )
);
expect( fetchMock ).toHaveBeenCalledWith(
'https://public-api.wordpress.com/rest/v1.1/js-error',
expect.objectContaining( {
method: 'POST',
body: expect.any( FormData ),
} )
);
const formData = fetchMock.mock.calls[0][1].body;
const payload = JSON.parse(formData.get('error'));
expect( payload['message'] ).toBe( 'Test error' );
expect( payload['severity'] ).toBe( 'error' );
expect( payload['trace'] ).toContain( '#1 at testFunction (http://example.com/woocommerce/assets/js/admin/app.min.js:1:1)' );
} );
const formData = fetchMock.mock.calls[ 0 ][ 1 ].body;
const payload = JSON.parse( formData.get( 'error' ) );
expect( payload[ 'message' ] ).toBe( 'Test error' );
expect( payload[ 'severity' ] ).toBe( 'error' );
expect( payload[ 'trace' ] ).toContain(
'#1 at testFunction (http://example.com/woocommerce/assets/js/admin/app.min.js:1:1)'
);
} );
it( 'should send an error to the API with extra data', async () => {
const error = new Error( 'Test error' );
const extraData = {
severity: 'warning' as const,
tags: ['custom-tag'],
};
await logger.error( error, extraData );
it( 'should send an error to the API with extra data', async () => {
const error = new Error( 'Test error' );
const extraData = {
severity: 'warning' as const,
tags: [ 'custom-tag' ],
};
await logger.error( error, extraData );
expect( fetchMock ).toHaveBeenCalledWith(
'https://public-api.wordpress.com/rest/v1.1/js-error',
expect.objectContaining( {
method: 'POST',
body: expect.any( FormData ),
} )
);
expect( fetchMock ).toHaveBeenCalledWith(
'https://public-api.wordpress.com/rest/v1.1/js-error',
expect.objectContaining( {
method: 'POST',
body: expect.any( FormData ),
} )
);
const formData = fetchMock.mock.calls[0][1].body;
const payload = JSON.parse(formData.get('error'));
expect( payload['message'] ).toBe( 'Test error' );
expect( payload['severity'] ).toBe( 'warning' );
expect( payload['tags'] ).toEqual( ["woocommerce", "js", "custom-tag"]);
expect( payload['trace'] ).toContain( '#1 at testFunction (http://example.com/woocommerce/assets/js/admin/app.min.js:1:1)' );
} );
} );
const formData = fetchMock.mock.calls[ 0 ][ 1 ].body;
const payload = JSON.parse( formData.get( 'error' ) );
expect( payload[ 'message' ] ).toBe( 'Test error' );
expect( payload[ 'severity' ] ).toBe( 'warning' );
expect( payload[ 'tags' ] ).toEqual( [
'woocommerce',
'js',
'custom-tag',
] );
expect( payload[ 'trace' ] ).toContain(
'#1 at testFunction (http://example.com/woocommerce/assets/js/admin/app.min.js:1:1)'
);
} );
} );
describe( 'handleError', () => {
it( 'should send an error to the API', async () => {
@ -305,31 +305,23 @@ describe( 'RemoteLogger', () => {
} );
} );
global.window.wcSettings = {
isRemoteLoggingEnabled: true,
};
describe( 'init', () => {
beforeEach( () => {
jest.clearAllMocks();
( getSetting as jest.Mock ).mockImplementation(
( key, defaultValue ) => {
if ( key === 'isRemoteLoggingEnabled' ) {
return true;
}
return defaultValue;
}
);
global.window.wcSettings = {
isRemoteLoggingEnabled: true,
};
} );
it( 'should not initialize or log when remote logging is disabled', () => {
// Mock the getSetting function to return false for isRemoteLoggingEnabled
( getSetting as jest.Mock ).mockImplementation(
( key, defaultValue ) => {
if ( key === 'isRemoteLoggingEnabled' ) {
return false;
}
return defaultValue;
}
);
global.window.wcSettings = {
isRemoteLoggingEnabled: false,
};
init( { errorRateLimitMs: 1000 } );
log( 'info', 'Test message' );
expect( fetchMock ).not.toHaveBeenCalled();
@ -353,15 +345,9 @@ describe( 'init', () => {
describe( 'log', () => {
it( 'should not log if remote logging is disabled', () => {
( getSetting as jest.Mock ).mockImplementation(
( key, defaultValue ) => {
if ( key === 'isRemoteLoggingEnabled' ) {
return false;
}
return defaultValue;
}
);
global.window.wcSettings = {
isRemoteLoggingEnabled: false,
};
log( 'info', 'Test message' );
expect( fetchMock ).not.toHaveBeenCalled();
} );
@ -369,15 +355,9 @@ describe( 'log', () => {
describe( 'captureException', () => {
it( 'should not log error if remote logging is disabled', () => {
( getSetting as jest.Mock ).mockImplementation(
( key, defaultValue ) => {
if ( key === 'isRemoteLoggingEnabled' ) {
return false;
}
return defaultValue;
}
);
global.window.wcSettings = {
isRemoteLoggingEnabled: false,
};
captureException( new Error( 'Test error' ) );
expect( fetchMock ).not.toHaveBeenCalled();
} );

View File

@ -1,8 +1,8 @@
declare global {
interface Window {
wcTracks: {
isEnabled: boolean;
};
wcSettings?: {
isRemoteLoggingEnabled: boolean;
}
}
}

View File

@ -63,3 +63,4 @@ require 'live-branches/manifest.php';
require 'live-branches/install.php';
require 'remote-spec-validator/class-wca-test-helper-remote-spec-validator.php';
require 'remote-inbox-notifications/class-wca-test-helper-remote-inbox-notifications.php';
require 'remote-logging/remote-logging.php';

View File

@ -0,0 +1,126 @@
<?php
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Internal\Logging\RemoteLogger;
register_woocommerce_admin_test_helper_rest_route(
'/remote-logging/status',
'get_remote_logging_status',
array(
'methods' => 'GET',
)
);
register_woocommerce_admin_test_helper_rest_route(
'/remote-logging/toggle',
'toggle_remote_logging',
array(
'methods' => 'POST',
'args' => array(
'enable' => array(
'required' => true,
'type' => 'boolean',
'sanitize_callback' => 'rest_sanitize_boolean',
),
),
)
);
register_woocommerce_admin_test_helper_rest_route(
'/remote-logging/log-event',
'log_remote_event',
array(
'methods' => 'POST',
)
);
register_woocommerce_admin_test_helper_rest_route(
'/remote-logging/reset-rate-limit',
'reset_php_rate_limit',
array(
'methods' => 'POST',
)
);
/**
* Get the remote logging status.
*
* @return WP_REST_Response The response object.
*/
function get_remote_logging_status() {
$remote_logger = wc_get_container()->get( RemoteLogger::class );
return new WP_REST_Response(
array(
'isEnabled' => $remote_logger->is_remote_logging_allowed(),
'wpEnvironment' => wp_get_environment_type(),
),
200
);
}
/**
* Toggle remote logging on or off.
*
* @param WP_REST_Request $request The request object.
* @return WP_REST_Response The response object.
*/
function toggle_remote_logging( $request ) {
$enable = $request->get_param( 'enable' );
if ( $enable ) {
update_option( 'woocommerce_feature_remote_logging_enabled', 'yes' );
update_option( 'woocommerce_allow_tracking', 'yes' );
update_option( 'woocommerce_remote_variant_assignment', 1 );
} else {
update_option( 'woocommerce_feature_remote_logging_enabled', 'no' );
}
$remote_logger = wc_get_container()->get( RemoteLogger::class );
return new WP_REST_Response(
array(
'isEnabled' => $remote_logger->is_remote_logging_allowed(),
),
200
);
}
/**
* Log a remote event for testing purposes.
*
* @return WP_REST_Response The response object.
*/
function log_remote_event() {
$remote_logger = wc_get_container()->get( RemoteLogger::class );
$result = $remote_logger->handle(
time(),
'critical',
'Test PHP event from WC Beta Tester',
array( 'source' => 'wc-beta-tester' )
);
if ( $result ) {
return new WP_REST_Response( array( 'message' => 'Remote event logged successfully.' ), 200 );
} else {
return new WP_REST_Response( array( 'message' => 'Failed to log remote event.' ), 500 );
}
}
/**
* Reset the PHP rate limit.
*
* @return WP_REST_Response The response object.
*/
function reset_php_rate_limit() {
global $wpdb;
$wpdb->query(
"DELETE FROM {$wpdb->prefix}wc_rate_limits"
);
WC_Cache_Helper::invalidate_cache_group( WC_Rate_Limiter::CACHE_GROUP );
return new WP_REST_Response( array( 'success' => true ), 200 );
}

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add remote logging tool

View File

@ -17,6 +17,7 @@
"@types/wordpress__plugins": "3.0.0",
"@woocommerce/dependency-extraction-webpack-plugin": "workspace:*",
"@woocommerce/eslint-plugin": "workspace:*",
"@woocommerce/remote-logging": "workspace:*",
"@wordpress/env": "^9.7.0",
"@wordpress/prettier-config": "2.17.0",
"@wordpress/scripts": "^19.2.4",

View File

@ -7,14 +7,13 @@ import { applyFilters } from '@wordpress/hooks';
/**
* Internal dependencies
*/
import { AdminNotes } from '../admin-notes';
import { default as Tools } from '../tools';
import { default as Options } from '../options';
import { default as Experiments } from '../experiments';
import { default as Features } from '../features';
import { default as RestAPIFilters } from '../rest-api-filters';
import RemoteSpecValidator from '../remote-spec-validator';
import RemoteInboxNotifications from '../remote-inbox-notifications';
import RemoteLogging from '../remote-logging';
const tabs = applyFilters( 'woocommerce_admin_test_helper_tabs', [
{
@ -47,6 +46,11 @@ const tabs = applyFilters( 'woocommerce_admin_test_helper_tabs', [
title: 'Remote Inbox Notifications',
content: <RemoteInboxNotifications />,
},
{
name: 'remote-logging',
title: 'Remote Logging',
content: <RemoteLogging />,
},
] );
export function App() {

View File

@ -2,6 +2,7 @@
* External dependencies
*/
import { createRoot } from '@wordpress/element';
import { addFilter } from '@wordpress/hooks';
/**
* Internal dependencies
@ -10,6 +11,7 @@ import { App } from './app';
import './index.scss';
import './example-fills/experimental-woocommerce-wcpay-feature';
import { registerProductEditorDevTools } from './product-editor-dev-tools';
import { registerExceptionFilter } from './remote-logging/register-exception-filter';
const appRoot = document.getElementById(
'woocommerce-admin-test-helper-app-root'
@ -20,3 +22,4 @@ if ( appRoot ) {
}
registerProductEditorDevTools();
registerExceptionFilter();

View File

@ -43,24 +43,26 @@ export function setNotice( notice ) {
}
export function* deleteOption( optionName ) {
try {
yield apiFetch( {
method: 'DELETE',
path: `${ API_NAMESPACE }/options/${ optionName }`,
} );
yield {
type: TYPES.DELETE_OPTION,
optionName,
};
} catch {
throw new Error();
}
yield apiFetch( {
method: 'DELETE',
path: `${ API_NAMESPACE }/options/${ optionName }`,
} );
yield {
type: TYPES.DELETE_OPTION,
optionName,
};
}
export function* saveOption( optionName, newOptionValue ) {
try {
const payload = {};
payload[ optionName ] = JSON.parse( newOptionValue );
try {
// If the option value is a JSON string, parse it.
payload[ optionName ] = JSON.parse( newOptionValue );
} catch ( error ) {
// If it's not a JSON string, just use the value as is.
payload[ optionName ] = newOptionValue;
}
yield apiFetch( {
method: 'POST',
path: '/wc-admin/options',
@ -71,11 +73,11 @@ export function* saveOption( optionName, newOptionValue ) {
status: 'success',
message: optionName + ' has been saved.',
} );
} catch {
} catch ( error ) {
yield setNotice( {
status: 'error',
message: 'Unable to save ' + optionName,
} );
throw new Error();
throw error;
}
}

View File

@ -17,14 +17,10 @@ export function* getOptions( search ) {
yield setLoadingState( true );
try {
const response = yield apiFetch( {
path,
} );
yield setOptions( response );
} catch ( error ) {
throw new Error();
}
const response = yield apiFetch( {
path,
} );
yield setOptions( response );
}
export function* getOptionForEditing( optionName ) {
@ -41,21 +37,17 @@ export function* getOptionForEditing( optionName ) {
const path = '/wc-admin/options?options=' + optionName;
try {
const response = yield apiFetch( {
path,
} );
const response = yield apiFetch( {
path,
} );
let content = response[ optionName ];
if ( typeof content === 'object' ) {
content = JSON.stringify( response[ optionName ], null, 2 );
}
yield setOptionForEditing( {
name: optionName,
content,
} );
} catch ( error ) {
throw new Error( error );
let content = response[ optionName ];
if ( typeof content === 'object' ) {
content = JSON.stringify( response[ optionName ], null, 2 );
}
yield setOptionForEditing( {
name: optionName,
content,
} );
}

View File

@ -0,0 +1,323 @@
/**
* External dependencies
*/
import { useState, useEffect } from '@wordpress/element';
import { Button, ToggleControl, Notice, Spinner } from '@wordpress/components';
import apiFetch from '@wordpress/api-fetch';
import { log, init as initRemoteLogging } from '@woocommerce/remote-logging';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore no types
// eslint-disable-next-line @woocommerce/dependency-group
import { dispatch } from '@wordpress/data';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore no types
// eslint-disable-next-line @woocommerce/dependency-group
import { STORE_KEY as OPTIONS_STORE_NAME } from '../options/data/constants';
export const API_NAMESPACE = '/wc-admin-test-helper';
interface RemoteLoggingStatus {
isEnabled: boolean;
wpEnvironment: string;
}
interface NoticeState {
status: 'success' | 'error' | 'warning' | 'info';
message: string;
}
function RemoteLogging() {
const [ isRemoteLoggingEnabled, setIsRemoteLoggingEnabled ] = useState<
boolean | null
>( null );
const [ wpEnvironment, setWpEnvironment ] = useState< string >( '' );
const [ notice, setNotice ] = useState< NoticeState | null >( null );
useEffect( () => {
const fetchRemoteLoggingStatus = async () => {
try {
const response: RemoteLoggingStatus = await apiFetch( {
path: `${ API_NAMESPACE }/remote-logging/status`,
} );
setIsRemoteLoggingEnabled( response.isEnabled );
setWpEnvironment( response.wpEnvironment );
} catch ( error ) {
setNotice( {
status: 'error',
message: 'Failed to fetch remote logging status.',
} );
}
};
fetchRemoteLoggingStatus();
}, [] );
const toggleRemoteLogging = async () => {
try {
const response: RemoteLoggingStatus = await apiFetch( {
path: `${ API_NAMESPACE }/remote-logging/toggle`,
method: 'POST',
data: { enable: ! isRemoteLoggingEnabled },
} );
setIsRemoteLoggingEnabled( response.isEnabled );
window.wcSettings.isRemoteLoggingEnabled = response.isEnabled;
} catch ( error ) {
setNotice( {
status: 'error',
message: `Failed to update remote logging status. ${ JSON.stringify(
error
) }`,
} );
}
if ( window.wcSettings.isRemoteLoggingEnabled ) {
initRemoteLogging( {
errorRateLimitMs: 60000, // 1 minute
} );
}
};
const simulatePhpException = async ( context: 'core' | 'beta-tester' ) => {
try {
await dispatch( OPTIONS_STORE_NAME ).saveOption(
'wc_beta_tester_simulate_woocommerce_php_error',
context
);
setNotice( {
status: 'success',
message: `Please refresh your browser to trigger the PHP exception in ${ context } context.`,
} );
} catch ( error ) {
setNotice( {
status: 'error',
message: `Failed to trigger PHP exception test in ${ context } context. ${ JSON.stringify(
error
) }`,
} );
}
};
const logPhpEvent = async () => {
try {
await apiFetch( {
path: `${ API_NAMESPACE }/remote-logging/log-event`,
method: 'POST',
} );
setNotice( {
status: 'success',
message: 'Remote event logged successfully.',
} );
} catch ( error ) {
setNotice( {
status: 'error',
message: `Failed to log remote event.`,
} );
}
};
const resetPhpRateLimit = async () => {
try {
await apiFetch( {
path: `${ API_NAMESPACE }/remote-logging/reset-rate-limit`,
method: 'POST',
} );
setNotice( {
status: 'success',
message: 'PHP rate limit reset successfully.',
} );
} catch ( error ) {
setNotice( {
status: 'error',
message: `Failed to reset PHP rate limit. ${ JSON.stringify(
error
) }`,
} );
}
};
const simulateException = async ( context: 'core' | 'beta-tester' ) => {
try {
await dispatch( OPTIONS_STORE_NAME ).saveOption(
'wc_beta_tester_simulate_woocommerce_js_error',
context
);
if ( context === 'core' ) {
setNotice( {
status: 'success',
message: `Please go to WooCommerce pages to trigger the JS exception in woocommerce context.`,
} );
} else {
setNotice( {
status: 'success',
message:
'Please refresh your browser to trigger the JS exception in woocommerce beta tester context.',
} );
}
} catch ( error ) {
setNotice( {
status: 'error',
message: `Failed to set up JS exception test`,
} );
}
};
const logJsEvent = async () => {
try {
const result = await log(
'info',
'Test JS event from WooCommerce Beta Tester',
{
extra: {
source: 'wc-beta-tester',
},
}
);
if ( ! result ) {
throw new Error();
}
setNotice( {
status: 'success',
message: 'JS event logged successfully.',
} );
} catch ( error ) {
setNotice( {
status: 'error',
message:
'Failed to log JS event. Try enabling debug mode `window.localStorage.setItem( "debug", "wc:remote-logging" )` to see the details.',
} );
}
};
const resetJsRateLimit = () => {
window.localStorage.removeItem(
'wc_remote_logging_last_error_sent_time'
);
setNotice( {
status: 'success',
message: 'JS rate limit reset successfully.',
} );
};
if ( isRemoteLoggingEnabled === null ) {
return <Spinner />;
}
return (
<div id="wc-admin-test-helper-remote-logging">
<h2>Remote Logging</h2>
{ notice && (
<div style={ { marginBottom: '12px' } }>
<Notice
status={ notice.status }
onRemove={ () => setNotice( null ) }
>
{ notice.message }
</Notice>
</div>
) }
{ ! isRemoteLoggingEnabled && (
<p className="helper-text" style={ { marginBottom: '12px' } }>
Enable remote logging to test log event functionality.
</p>
) }
{ ( wpEnvironment === 'local' ||
wpEnvironment === 'development' ) && (
<div style={ { marginBottom: '12px' } }>
<Notice status="warning" isDismissible={ false }>
Warning: You are in a { wpEnvironment } environment.
Remote logging may not work as expected. Please set
<code>WP_ENVIRONMENT_TYPE</code> to{ ' ' }
<code>production</code>
in your wp-config.php file to test remote logging.
</Notice>
</div>
) }
<ToggleControl
label="Enable Remote Logging"
checked={ isRemoteLoggingEnabled }
onChange={ toggleRemoteLogging }
/>
<hr />
<h3>PHP Integration</h3>
<p>Test PHP remote logging functionality:</p>
<div className="button-group" style={ { marginBottom: '20px' } }>
<Button
variant="secondary"
onClick={ () => simulatePhpException( 'core' ) }
style={ { marginRight: '10px' } }
>
Simulate Core Exception
</Button>
<Button
variant="secondary"
onClick={ () => simulatePhpException( 'beta-tester' ) }
style={ { marginRight: '10px' } }
>
Simulate Beta Tester Exception
</Button>
<Button
variant="secondary"
onClick={ logPhpEvent }
disabled={ ! isRemoteLoggingEnabled }
style={ { marginRight: '10px' } }
>
Log PHP Event
</Button>
<Button
variant="secondary"
onClick={ resetPhpRateLimit }
disabled={ ! isRemoteLoggingEnabled }
>
Reset Rate Limit
</Button>
</div>
<hr className="section-divider" style={ { margin: '20px 0' } } />
<h3>JavaScript Integration</h3>
<p>Test JavaScript remote logging functionality:</p>
<div className="button-group" style={ { marginBottom: '20px' } }>
<Button
variant="secondary"
onClick={ () => simulateException( 'core' ) }
style={ { marginRight: '10px' } }
>
Simulate Core Exception
</Button>
<Button
variant="secondary"
onClick={ () => simulateException( 'beta-tester' ) }
style={ { marginRight: '10px' } }
>
Simulate Beta Tester Exception
</Button>
<Button
variant="secondary"
onClick={ logJsEvent }
disabled={ ! isRemoteLoggingEnabled }
style={ { marginRight: '10px' } }
>
Log Event
</Button>
<Button
variant="secondary"
onClick={ resetJsRateLimit }
disabled={ ! isRemoteLoggingEnabled }
>
Reset Rate Limit
</Button>
</div>
</div>
);
}
export default RemoteLogging;

View File

@ -0,0 +1,93 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
/* eslint-disable @woocommerce/dependency-group */
/**
* External dependencies
*/
import { addFilter } from '@wordpress/hooks';
import apiFetch from '@wordpress/api-fetch';
// @ts-ignore no types
import { dispatch } from '@wordpress/data';
/**
* Internal dependencies
*/
import { API_NAMESPACE } from './';
// @ts-ignore no types
import { STORE_KEY as OPTIONS_STORE_NAME } from '../options/data/constants';
/**
* Retrieves the options for simulating a WooCommerce JavaScript error.
*
* @return {Promise<Array|null>} The options if available, null otherwise.
*/
const getSimulateErrorOptions = async () => {
try {
const path = `${ API_NAMESPACE }/options?search=wc_beta_tester_simulate_woocommerce_js_error`;
const options = await apiFetch<
[
{
option_value: string;
option_name: string;
option_id: number;
}
]
>( {
path,
} );
return options && options.length > 0 ? options : null;
} catch ( error ) {
// eslint-disable-next-line no-console
console.error( 'Error retrieving simulate error options:', error );
return null;
}
};
/**
* Deletes the option used for simulating WooCommerce JavaScript errors.
*/
const deleteSimulateErrorOption = async () => {
await dispatch( OPTIONS_STORE_NAME ).deleteOption(
'wc_beta_tester_simulate_woocommerce_js_error'
);
};
/**
* Adds a filter to throw an exception in the WooCommerce core context.
*/
const addCoreExceptionFilter = () => {
addFilter( 'woocommerce_admin_pages_list', 'wc-beta-tester', () => {
deleteSimulateErrorOption();
throw new Error(
'Test JS exception in WC Core context via WC Beta Tester'
);
} );
};
/**
* Throws an exception specific to the WooCommerce Beta Tester context.
*/
const throwBetaTesterException = () => {
throw new Error( 'Test JS exception from WooCommerce Beta Tester' );
};
/**
* Registers an exception filter for simulating JavaScript errors in WooCommerce.
* This function is used for testing purposes in the WooCommerce Beta Tester plugin.
*/
export const registerExceptionFilter = async () => {
const options = await getSimulateErrorOptions();
if ( ! options ) {
return;
}
const context = options[ 0 ].option_value;
if ( context === 'core' ) {
addCoreExceptionFilter();
} else {
deleteSimulateErrorOption();
throwBetaTesterException();
}
};

View File

@ -0,0 +1,9 @@
declare global {
interface Window {
wcSettings: {
isRemoteLoggingEnabled: boolean;
};
}
}
export {};

View File

@ -138,5 +138,28 @@ add_action(
}
);
/**
* Simulate a WooCommerce error for remote logging testing.
*
* @throws Exception A simulated WooCommerce error if the option is set.
*/
function simulate_woocommerce_error() {
throw new Exception( 'Simulated WooCommerce error for remote logging test' );
}
$simulate_error = get_option( 'wc_beta_tester_simulate_woocommerce_php_error', false );
if ( $simulate_error ) {
delete_option( 'wc_beta_tester_simulate_woocommerce_php_error' );
if ( 'core' === $simulate_error ) {
add_action( 'woocommerce_loaded', 'simulate_woocommerce_error' );
} elseif ( 'beta-tester' === $simulate_error ) {
throw new Exception( 'Test PHP exception from WooCommerce Beta Tester' );
}
}
// Initialize the live branches feature.
require_once dirname( __FILE__ ) . '/includes/class-wc-beta-tester-live-branches.php';

View File

@ -3916,6 +3916,9 @@ importers:
'@woocommerce/eslint-plugin':
specifier: workspace:*
version: link:../../packages/js/eslint-plugin
'@woocommerce/remote-logging':
specifier: workspace:*
version: link:../../packages/js/remote-logging
'@wordpress/env':
specifier: ^9.7.0
version: 9.7.0
@ -21800,7 +21803,7 @@ packages:
react-with-direction@1.4.0:
resolution: {integrity: sha512-ybHNPiAmaJpoWwugwqry9Hd1Irl2hnNXlo/2SXQBwbLn/jGMauMS2y9jw+ydyX5V9ICryCqObNSthNt5R94xpg==}
peerDependencies:
react: ^0.14 || ^15 || ^16
react: ^17.0.2
react-dom: ^0.14 || ^15 || ^16
react-with-styles-interface-css@4.0.3: