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

View File

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

View File

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

View File

@ -63,3 +63,4 @@ require 'live-branches/manifest.php';
require 'live-branches/install.php'; require 'live-branches/install.php';
require 'remote-spec-validator/class-wca-test-helper-remote-spec-validator.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-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", "@types/wordpress__plugins": "3.0.0",
"@woocommerce/dependency-extraction-webpack-plugin": "workspace:*", "@woocommerce/dependency-extraction-webpack-plugin": "workspace:*",
"@woocommerce/eslint-plugin": "workspace:*", "@woocommerce/eslint-plugin": "workspace:*",
"@woocommerce/remote-logging": "workspace:*",
"@wordpress/env": "^9.7.0", "@wordpress/env": "^9.7.0",
"@wordpress/prettier-config": "2.17.0", "@wordpress/prettier-config": "2.17.0",
"@wordpress/scripts": "^19.2.4", "@wordpress/scripts": "^19.2.4",

View File

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

View File

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

View File

@ -43,24 +43,26 @@ export function setNotice( notice ) {
} }
export function* deleteOption( optionName ) { export function* deleteOption( optionName ) {
try { yield apiFetch( {
yield apiFetch( { method: 'DELETE',
method: 'DELETE', path: `${ API_NAMESPACE }/options/${ optionName }`,
path: `${ API_NAMESPACE }/options/${ optionName }`, } );
} ); yield {
yield { type: TYPES.DELETE_OPTION,
type: TYPES.DELETE_OPTION, optionName,
optionName, };
};
} catch {
throw new Error();
}
} }
export function* saveOption( optionName, newOptionValue ) { export function* saveOption( optionName, newOptionValue ) {
try { try {
const payload = {}; 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( { yield apiFetch( {
method: 'POST', method: 'POST',
path: '/wc-admin/options', path: '/wc-admin/options',
@ -71,11 +73,11 @@ export function* saveOption( optionName, newOptionValue ) {
status: 'success', status: 'success',
message: optionName + ' has been saved.', message: optionName + ' has been saved.',
} ); } );
} catch { } catch ( error ) {
yield setNotice( { yield setNotice( {
status: 'error', status: 'error',
message: 'Unable to save ' + optionName, message: 'Unable to save ' + optionName,
} ); } );
throw new Error(); throw error;
} }
} }

View File

@ -17,14 +17,10 @@ export function* getOptions( search ) {
yield setLoadingState( true ); yield setLoadingState( true );
try { const response = yield apiFetch( {
const response = yield apiFetch( { path,
path, } );
} ); yield setOptions( response );
yield setOptions( response );
} catch ( error ) {
throw new Error();
}
} }
export function* getOptionForEditing( optionName ) { export function* getOptionForEditing( optionName ) {
@ -41,21 +37,17 @@ export function* getOptionForEditing( optionName ) {
const path = '/wc-admin/options?options=' + optionName; const path = '/wc-admin/options?options=' + optionName;
try { const response = yield apiFetch( {
const response = yield apiFetch( { path,
path, } );
} );
let content = response[ optionName ]; let content = response[ optionName ];
if ( typeof content === 'object' ) { if ( typeof content === 'object' ) {
content = JSON.stringify( response[ optionName ], null, 2 ); content = JSON.stringify( response[ optionName ], null, 2 );
}
yield setOptionForEditing( {
name: optionName,
content,
} );
} catch ( error ) {
throw new Error( error );
} }
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. // Initialize the live branches feature.
require_once dirname( __FILE__ ) . '/includes/class-wc-beta-tester-live-branches.php'; require_once dirname( __FILE__ ) . '/includes/class-wc-beta-tester-live-branches.php';

View File

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