From 450a4ce3bbc642813972a258924e73dbb64fd570 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Thu, 8 Aug 2024 11:12:51 +0800 Subject: [PATCH] Add remote logging tool to beta tester (#50425) * Add remote logging beta tester tool * chore: Update log method return type to Promise * 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 --- packages/js/remote-logging/README.md | 2 +- .../add-remote-logging-beta-tester-tool | 4 + .../js/remote-logging/src/remote-logger.ts | 41 ++- packages/js/remote-logging/src/test/index.ts | 144 ++++---- .../js/remote-logging/typings/global.d.ts | 6 +- plugins/woocommerce-beta-tester/api/api.php | 1 + .../api/remote-logging/remote-logging.php | 126 +++++++ .../add-remote-logging-beta-tester-tool | 4 + plugins/woocommerce-beta-tester/package.json | 1 + .../woocommerce-beta-tester/src/app/app.js | 8 +- plugins/woocommerce-beta-tester/src/index.js | 3 + .../src/options/data/actions.js | 32 +- .../src/options/data/resolvers.js | 38 +-- .../src/remote-logging/index.tsx | 323 ++++++++++++++++++ .../register-exception-filter.tsx | 93 +++++ .../typing/global.d.ts | 9 + .../woocommerce-beta-tester.php | 23 ++ pnpm-lock.yaml | 5 +- 18 files changed, 719 insertions(+), 144 deletions(-) create mode 100644 packages/js/remote-logging/changelog/add-remote-logging-beta-tester-tool create mode 100644 plugins/woocommerce-beta-tester/api/remote-logging/remote-logging.php create mode 100644 plugins/woocommerce-beta-tester/changelog/add-remote-logging-beta-tester-tool create mode 100644 plugins/woocommerce-beta-tester/src/remote-logging/index.tsx create mode 100644 plugins/woocommerce-beta-tester/src/remote-logging/register-exception-filter.tsx create mode 100644 plugins/woocommerce-beta-tester/typing/global.d.ts diff --git a/packages/js/remote-logging/README.md b/packages/js/remote-logging/README.md index 8ae44259b4c..46a3b9aa45f 100644 --- a/packages/js/remote-logging/README.md +++ b/packages/js/remote-logging/README.md @@ -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`: Logs a message with the specified severity and optional extra data. +- `log(severity: LogSeverity, message: string, extraData?: object): Promise`: 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. diff --git a/packages/js/remote-logging/changelog/add-remote-logging-beta-tester-tool b/packages/js/remote-logging/changelog/add-remote-logging-beta-tester-tool new file mode 100644 index 00000000000..1e7f8794894 --- /dev/null +++ b/packages/js/remote-logging/changelog/add-remote-logging-beta-tester-tool @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Tweak logic for adding remote logging tool in beta tester diff --git a/packages/js/remote-logging/src/remote-logger.ts b/packages/js/remote-logging/src/remote-logger.ts index e88ccc6c76f..501a546a00f 100644 --- a/packages/js/remote-logging/src/remote-logger.ts +++ b/packages/js/remote-logging/src/remote-logger.ts @@ -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 ); } diff --git a/packages/js/remote-logging/src/test/index.ts b/packages/js/remote-logging/src/test/index.ts index a2542330bb5..155b32d2e70 100644 --- a/packages/js/remote-logging/src/test/index.ts +++ b/packages/js/remote-logging/src/test/index.ts @@ -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(); } ); diff --git a/packages/js/remote-logging/typings/global.d.ts b/packages/js/remote-logging/typings/global.d.ts index 2ba9695a964..e4db641ebd0 100644 --- a/packages/js/remote-logging/typings/global.d.ts +++ b/packages/js/remote-logging/typings/global.d.ts @@ -1,8 +1,8 @@ declare global { interface Window { - wcTracks: { - isEnabled: boolean; - }; + wcSettings?: { + isRemoteLoggingEnabled: boolean; + } } } diff --git a/plugins/woocommerce-beta-tester/api/api.php b/plugins/woocommerce-beta-tester/api/api.php index 62a4b4df2af..2d4a1fbd355 100644 --- a/plugins/woocommerce-beta-tester/api/api.php +++ b/plugins/woocommerce-beta-tester/api/api.php @@ -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'; diff --git a/plugins/woocommerce-beta-tester/api/remote-logging/remote-logging.php b/plugins/woocommerce-beta-tester/api/remote-logging/remote-logging.php new file mode 100644 index 00000000000..65199307fac --- /dev/null +++ b/plugins/woocommerce-beta-tester/api/remote-logging/remote-logging.php @@ -0,0 +1,126 @@ + '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 ); +} diff --git a/plugins/woocommerce-beta-tester/changelog/add-remote-logging-beta-tester-tool b/plugins/woocommerce-beta-tester/changelog/add-remote-logging-beta-tester-tool new file mode 100644 index 00000000000..c0b200cdfad --- /dev/null +++ b/plugins/woocommerce-beta-tester/changelog/add-remote-logging-beta-tester-tool @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add remote logging tool diff --git a/plugins/woocommerce-beta-tester/package.json b/plugins/woocommerce-beta-tester/package.json index aa0072e6cc4..f3e695c2d89 100644 --- a/plugins/woocommerce-beta-tester/package.json +++ b/plugins/woocommerce-beta-tester/package.json @@ -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", diff --git a/plugins/woocommerce-beta-tester/src/app/app.js b/plugins/woocommerce-beta-tester/src/app/app.js index a612bef66c6..50bd79036f9 100644 --- a/plugins/woocommerce-beta-tester/src/app/app.js +++ b/plugins/woocommerce-beta-tester/src/app/app.js @@ -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: , }, + { + name: 'remote-logging', + title: 'Remote Logging', + content: , + }, ] ); export function App() { diff --git a/plugins/woocommerce-beta-tester/src/index.js b/plugins/woocommerce-beta-tester/src/index.js index ab4fced9c3e..70b8dc79fe9 100644 --- a/plugins/woocommerce-beta-tester/src/index.js +++ b/plugins/woocommerce-beta-tester/src/index.js @@ -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(); diff --git a/plugins/woocommerce-beta-tester/src/options/data/actions.js b/plugins/woocommerce-beta-tester/src/options/data/actions.js index 1f3398281f3..b3a722b5c92 100644 --- a/plugins/woocommerce-beta-tester/src/options/data/actions.js +++ b/plugins/woocommerce-beta-tester/src/options/data/actions.js @@ -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; } } diff --git a/plugins/woocommerce-beta-tester/src/options/data/resolvers.js b/plugins/woocommerce-beta-tester/src/options/data/resolvers.js index 6c1ec7ce2ad..60bc82fb6a6 100644 --- a/plugins/woocommerce-beta-tester/src/options/data/resolvers.js +++ b/plugins/woocommerce-beta-tester/src/options/data/resolvers.js @@ -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, + } ); } diff --git a/plugins/woocommerce-beta-tester/src/remote-logging/index.tsx b/plugins/woocommerce-beta-tester/src/remote-logging/index.tsx new file mode 100644 index 00000000000..26e5b985f37 --- /dev/null +++ b/plugins/woocommerce-beta-tester/src/remote-logging/index.tsx @@ -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 ; + } + + return ( +
+

Remote Logging

+ { notice && ( +
+ setNotice( null ) } + > + { notice.message } + +
+ ) } + + { ! isRemoteLoggingEnabled && ( +

+ Enable remote logging to test log event functionality. +

+ ) } + + { ( wpEnvironment === 'local' || + wpEnvironment === 'development' ) && ( +
+ + Warning: You are in a { wpEnvironment } environment. + Remote logging may not work as expected. Please set + WP_ENVIRONMENT_TYPE to{ ' ' } + production + in your wp-config.php file to test remote logging. + +
+ ) } + + + +
+

PHP Integration

+

Test PHP remote logging functionality:

+
+ + + + +
+ +
+ +

JavaScript Integration

+

Test JavaScript remote logging functionality:

+
+ + + + +
+
+ ); +} + +export default RemoteLogging; diff --git a/plugins/woocommerce-beta-tester/src/remote-logging/register-exception-filter.tsx b/plugins/woocommerce-beta-tester/src/remote-logging/register-exception-filter.tsx new file mode 100644 index 00000000000..210610d6575 --- /dev/null +++ b/plugins/woocommerce-beta-tester/src/remote-logging/register-exception-filter.tsx @@ -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} 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(); + } +}; diff --git a/plugins/woocommerce-beta-tester/typing/global.d.ts b/plugins/woocommerce-beta-tester/typing/global.d.ts new file mode 100644 index 00000000000..64c87d09659 --- /dev/null +++ b/plugins/woocommerce-beta-tester/typing/global.d.ts @@ -0,0 +1,9 @@ +declare global { + interface Window { + wcSettings: { + isRemoteLoggingEnabled: boolean; + }; + } +} + +export {}; diff --git a/plugins/woocommerce-beta-tester/woocommerce-beta-tester.php b/plugins/woocommerce-beta-tester/woocommerce-beta-tester.php index ca0e6b53604..2920fba45cc 100644 --- a/plugins/woocommerce-beta-tester/woocommerce-beta-tester.php +++ b/plugins/woocommerce-beta-tester/woocommerce-beta-tester.php @@ -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'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4561c2979e3..7ae9db3a61c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: