From 6da2742bd3b59eee038f97e039cf93b776df5687 Mon Sep 17 00:00:00 2001 From: Alfonso Salces Date: Thu, 2 Nov 2023 11:25:22 +0100 Subject: [PATCH] MOBILE-4451 settings: Create error log page --- src/core/classes/site.ts | 18 +- src/core/features/settings/pages/dev/dev.html | 11 +- src/core/features/settings/pages/dev/dev.ts | 8 + .../settings/pages/error-log/error-log.html | 51 +++++ .../settings/pages/error-log/error-log.scss | 4 + .../settings/pages/error-log/error-log.ts | 39 ++++ .../features/settings/settings-lazy.module.ts | 6 + src/core/services/utils/dom.ts | 2 + src/core/services/ws.ts | 175 +++++++++++------- src/core/singletons/error-logs.ts | 53 ++++++ 10 files changed, 298 insertions(+), 69 deletions(-) create mode 100644 src/core/features/settings/pages/error-log/error-log.html create mode 100644 src/core/features/settings/pages/error-log/error-log.scss create mode 100644 src/core/features/settings/pages/error-log/error-log.ts create mode 100644 src/core/singletons/error-logs.ts diff --git a/src/core/classes/site.ts b/src/core/classes/site.ts index 0a7715b2c..e59beb358 100644 --- a/src/core/classes/site.ts +++ b/src/core/classes/site.ts @@ -64,6 +64,7 @@ import { CoreSiteError } from '@classes/errors/siteerror'; import { CoreUserAuthenticatedSupportConfig } from '@features/user/classes/support/authenticated-support-config'; import { CoreLoginHelper } from '@features/login/services/login-helper'; import { CorePath } from '@singletons/path'; +import { CoreErrorLogs } from '@singletons/error-logs'; /** * QR Code type enumeration. @@ -1156,7 +1157,15 @@ export class CoreSite { // Request not executed, enqueue again. this.enqueueRequest(request); } else if (response.error) { - request.deferred.reject(CoreTextUtils.parseJSON(response.exception || '')); + const rejectReason = CoreTextUtils.parseJSON(response.exception || '') as Error | undefined; + request.deferred.reject(rejectReason); + CoreErrorLogs.addErrorLog({ + method: request.method, + type: 'CoreSiteError', + message: response.exception ?? '', + time: new Date().getTime(), + data: request.data, + }); } else { let responseData = response.data ? CoreTextUtils.parseJSON(response.data) : {}; // Match the behaviour of CoreWSProvider.call when no response is expected. @@ -1170,6 +1179,13 @@ export class CoreSite { } catch (error) { // Error not specific to a single request, reject all promises. requests.forEach((request) => { + CoreErrorLogs.addErrorLog({ + method: request.method, + type: 'CoreSiteError', + message: String(error) ?? '', + time: new Date().getTime(), + data: request.data, + }); request.deferred.reject(error); }); } diff --git a/src/core/features/settings/pages/dev/dev.html b/src/core/features/settings/pages/dev/dev.html index 55aad8845..37d0b3114 100644 --- a/src/core/features/settings/pages/dev/dev.html +++ b/src/core/features/settings/pages/dev/dev.html @@ -34,8 +34,9 @@

Enable staging sites ({{stagingSitesCount}})

- + + + @@ -61,6 +62,12 @@ + + +

Error log

+
+
+

Disabled features

diff --git a/src/core/features/settings/pages/dev/dev.ts b/src/core/features/settings/pages/dev/dev.ts index 532f59c60..f643ed153 100644 --- a/src/core/features/settings/pages/dev/dev.ts +++ b/src/core/features/settings/pages/dev/dev.ts @@ -19,6 +19,7 @@ import { CoreSettingsHelper } from '@features/settings/services/settings-helper' import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins'; import { CoreUserTours } from '@features/usertours/services/user-tours'; import { CoreConfig } from '@services/config'; +import { CoreNavigator } from '@services/navigator'; import { CorePlatform } from '@services/platform'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; @@ -151,6 +152,13 @@ export class CoreSettingsDevPage implements OnInit { }); } + /** + * Open error log. + */ + openErrorLog(): void { + CoreNavigator.navigate('error-log'); + } + /** * Copies site info. */ diff --git a/src/core/features/settings/pages/error-log/error-log.html b/src/core/features/settings/pages/error-log/error-log.html new file mode 100644 index 000000000..3a8f56dee --- /dev/null +++ b/src/core/features/settings/pages/error-log/error-log.html @@ -0,0 +1,51 @@ + + + + + + + +

Error log

+
+ + + + + + +
+
+ + + + +
+

Trace

+

{{ error.message }}

+ + +

Method

+

{{ error.method }}

+
+ + +

Type

+

{{ error.type }}

+
+ + +

Data

+

{{ error.data | json }}

+
+ +
+ {{ error.time | coreFormatDate :'strftimedatetimeshort' }} +
+
+
+
+ + + + +
diff --git a/src/core/features/settings/pages/error-log/error-log.scss b/src/core/features/settings/pages/error-log/error-log.scss new file mode 100644 index 000000000..588b78e22 --- /dev/null +++ b/src/core/features/settings/pages/error-log/error-log.scss @@ -0,0 +1,4 @@ +.timestamp { + display: flex; + justify-content: end; +} diff --git a/src/core/features/settings/pages/error-log/error-log.ts b/src/core/features/settings/pages/error-log/error-log.ts new file mode 100644 index 000000000..19c9adb4d --- /dev/null +++ b/src/core/features/settings/pages/error-log/error-log.ts @@ -0,0 +1,39 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit } from '@angular/core'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreErrorLogs, CoreSettingsErrorLog } from '@singletons/error-logs'; + +/** + * Page that displays the error logs. + */ +@Component({ + selector: 'page-core-app-settings-error-log', + templateUrl: 'error-log.html', + styleUrls: ['./error-log.scss'], +}) +export class CoreSettingsErrorLogPage implements OnInit { + + errorLogs: CoreSettingsErrorLog[] = []; + + ngOnInit(): void { + this.errorLogs = CoreErrorLogs.getErrorLogs(); + } + + copyInfo(): void { + CoreUtils.copyToClipboard(JSON.stringify({ errors: this.errorLogs })); + } + +} diff --git a/src/core/features/settings/settings-lazy.module.ts b/src/core/features/settings/settings-lazy.module.ts index c5745ead9..fa8772f98 100644 --- a/src/core/features/settings/settings-lazy.module.ts +++ b/src/core/features/settings/settings-lazy.module.ts @@ -28,6 +28,7 @@ import { CoreSettingsAboutPage } from '@features/settings/pages/about/about'; import { CoreSettingsLicensesPage } from '@features/settings/pages/licenses/licenses'; import { CoreSettingsDeviceInfoPage } from '@features/settings/pages/deviceinfo/deviceinfo'; import { CoreSettingsDevPage } from '@features/settings/pages/dev/dev'; +import { CoreSettingsErrorLogPage } from '@features/settings/pages/error-log/error-log'; const sectionRoutes: Routes = [ { @@ -86,6 +87,10 @@ const routes: Routes = [ path: 'about/deviceinfo/dev', component: CoreSettingsDevPage, }, + { + path: 'about/deviceinfo/dev/error-log', + component: CoreSettingsErrorLogPage, + }, { path: 'about/licenses', component: CoreSettingsLicensesPage, @@ -106,6 +111,7 @@ const routes: Routes = [ CoreSettingsLicensesPage, CoreSettingsDeviceInfoPage, CoreSettingsDevPage, + CoreSettingsErrorLogPage, ], }) export class CoreSettingsLazyModule {} diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index 61d3becf7..3d8840dfe 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -60,6 +60,7 @@ import { CoreCancellablePromise } from '@classes/cancellable-promise'; import { CoreLang } from '@services/lang'; import { CorePasswordModalParams, CorePasswordModalResponse } from '@components/password-modal/password-modal'; import { CoreWSError } from '@classes/errors/wserror'; +import { CoreErrorLogs } from '@singletons/error-logs'; /* * "Utils" service with helper functions for UI, DOM elements and HTML code. @@ -613,6 +614,7 @@ export class CoreDomUtilsProvider { // We received an object instead of a string. Search for common properties. errorMessage = CoreTextUtils.getErrorMessageFromError(error); + CoreErrorLogs.addErrorLog({ message: JSON.stringify(error), type: errorMessage || '', time: new Date().getTime() }); if (!errorMessage) { // No common properties found, just stringify it. errorMessage = JSON.stringify(error); diff --git a/src/core/services/ws.ts b/src/core/services/ws.ts index 1f3db3a47..e227d4f48 100644 --- a/src/core/services/ws.ts +++ b/src/core/services/ws.ts @@ -43,6 +43,7 @@ import { CoreSiteError, CoreSiteErrorOptions } from '@classes/errors/siteerror'; import { CoreUserGuestSupportConfig } from '@features/user/classes/support/guest-support-config'; import { CoreSites } from '@services/sites'; import { CoreLang, CoreLangFormat } from './lang'; +import { CoreErrorLogs } from '@singletons/error-logs'; /** * This service allows performing WS calls and download/upload files. @@ -412,9 +413,25 @@ export class CoreWSProvider { let promise: Promise>; if (preSets.siteUrl === undefined) { - throw new CoreAjaxError(Translate.instant('core.unexpectederror')); + const unexpectedError = new CoreAjaxError(Translate.instant('core.unexpectederror')); + CoreErrorLogs.addErrorLog({ + method, + type: 'CoreAjaxError', + message: Translate.instant('core.unexpectederror'), + time: new Date().getTime(), + data, + }); + throw unexpectedError; } else if (!CoreNetwork.isOnline()) { - throw new CoreAjaxError(Translate.instant('core.networkerrormsg')); + const networkError = new CoreAjaxError(Translate.instant('core.networkerrormsg')); + CoreErrorLogs.addErrorLog({ + method, + type: 'CoreAjaxError', + message: Translate.instant('core.networkerrormsg'), + time: new Date().getTime(), + data, + }); + throw networkError; } if (preSets.responseExpected === undefined) { @@ -552,6 +569,10 @@ export class CoreWSProvider { } throw new CoreAjaxError(options, 1, data.status); + }).catch(error => { + const type = `CoreAjaxError - ${error.errorcode}`; + CoreErrorLogs.addErrorLog({ method, type, message: error, time: new Date().getTime(), data }); + throw error; }); } @@ -782,6 +803,15 @@ export class CoreWSProvider { throw new CoreError(Translate.instant('core.serverconnection', { details: CoreTextUtils.getErrorMessageFromError(error) ?? 'Unknown error', })); + }).catch(err => { + CoreErrorLogs.addErrorLog({ + method, + type: String(err), + message: String(err.exception), + time: new Date().getTime(), + data: ajaxData, + }); + throw err; }); } @@ -847,71 +877,84 @@ export class CoreWSProvider { */ // eslint-disable-next-line @typescript-eslint/no-explicit-any syncCall(method: string, data: any, preSets: CoreWSPreSets): T { - if (!preSets) { - throw new CoreError(Translate.instant('core.unexpectederror')); - } else if (!CoreNetwork.isOnline()) { - throw new CoreNetworkError(); + try { + if (!preSets) { + throw new CoreError(Translate.instant('core.unexpectederror')); + } else if (!CoreNetwork.isOnline()) { + throw new CoreNetworkError(); + } + + preSets.typeExpected = preSets.typeExpected || 'object'; + if (preSets.responseExpected === undefined) { + preSets.responseExpected = true; + } + + data = this.convertValuesToString(data || {}, preSets.cleanUnicode); + if (data == null) { + // Empty cleaned text found. + throw new CoreError(Translate.instant('core.unicodenotsupportedcleanerror')); + } + + data.wsfunction = method; + data.wstoken = preSets.wsToken; + const siteUrl = preSets.siteUrl + '/webservice/rest/server.php?moodlewsrestformat=json'; + + // Serialize data. + data = CoreInterceptor.serialize(data); + + // Perform sync request using XMLHttpRequest. + const xhr = new XMLHttpRequest(); + xhr.open('post', siteUrl, false); + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded;charset=utf-8'); + + xhr.send(data); + + // Get response. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data = ('response' in xhr) ? xhr.response : ( xhr).responseText; + + // Check status. + const status = Math.max(xhr.status === 1223 ? 204 : xhr.status, 0); + if (status < 200 || status >= 300) { + // Request failed. + throw new CoreError(data); + } + + // Treat response. + data = CoreTextUtils.parseJSON(data); + + // Some moodle web services return null. + // If the responseExpected value is set then so long as no data is returned, we create a blank object. + if ((!data || !data.data) && !preSets.responseExpected) { + data = {}; + } + + if (!data) { + throw new CoreError(Translate.instant('core.serverconnection', { + details: Translate.instant('core.errorinvalidresponse', { method }), + })); + } else if (typeof data != preSets.typeExpected) { + this.logger.warn('Response of type "' + typeof data + '" received, expecting "' + preSets.typeExpected + '"'); + throw new CoreError(Translate.instant('core.errorinvalidresponse', { method })); + } + + if (data.exception !== undefined || data.debuginfo !== undefined) { + throw new CoreWSError(data); + } + + return data; + } catch (err) { + let errorType = ''; + + if (err instanceof CoreError) { + errorType = 'CoreError'; + } else if (err instanceof CoreWSError) { + errorType = 'CoreWSError'; + } + + CoreErrorLogs.addErrorLog({ method, type: errorType, message: String(err), time: new Date().getTime(), data }); + throw err; } - - preSets.typeExpected = preSets.typeExpected || 'object'; - if (preSets.responseExpected === undefined) { - preSets.responseExpected = true; - } - - data = this.convertValuesToString(data || {}, preSets.cleanUnicode); - if (data == null) { - // Empty cleaned text found. - throw new CoreError(Translate.instant('core.unicodenotsupportedcleanerror')); - } - - data.wsfunction = method; - data.wstoken = preSets.wsToken; - const siteUrl = preSets.siteUrl + '/webservice/rest/server.php?moodlewsrestformat=json'; - - // Serialize data. - data = CoreInterceptor.serialize(data); - - // Perform sync request using XMLHttpRequest. - const xhr = new XMLHttpRequest(); - xhr.open('post', siteUrl, false); - xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded;charset=utf-8'); - - xhr.send(data); - - // Get response. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - data = ('response' in xhr) ? xhr.response : ( xhr).responseText; - - // Check status. - const status = Math.max(xhr.status === 1223 ? 204 : xhr.status, 0); - if (status < 200 || status >= 300) { - // Request failed. - throw new CoreError(data); - } - - // Treat response. - data = CoreTextUtils.parseJSON(data); - - // Some moodle web services return null. - // If the responseExpected value is set then so long as no data is returned, we create a blank object. - if ((!data || !data.data) && !preSets.responseExpected) { - data = {}; - } - - if (!data) { - throw new CoreError(Translate.instant('core.serverconnection', { - details: Translate.instant('core.errorinvalidresponse', { method }), - })); - } else if (typeof data != preSets.typeExpected) { - this.logger.warn('Response of type "' + typeof data + '" received, expecting "' + preSets.typeExpected + '"'); - throw new CoreError(Translate.instant('core.errorinvalidresponse', { method })); - } - - if (data.exception !== undefined || data.debuginfo !== undefined) { - throw new CoreWSError(data); - } - - return data; } /* diff --git a/src/core/singletons/error-logs.ts b/src/core/singletons/error-logs.ts new file mode 100644 index 000000000..8f828034b --- /dev/null +++ b/src/core/singletons/error-logs.ts @@ -0,0 +1,53 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import { Injectable } from '@angular/core'; +import { makeSingleton } from '@singletons'; + +/** + * Service that stores error logs in memory. + */ +@Injectable({ providedIn: 'root' }) +export class CoreErrorLogsService { + + protected errorLogs: CoreSettingsErrorLog[] = []; + + /** + * Retrieve error logs displayed in the DOM. + * + * @returns Error logs + */ + getErrorLogs(): CoreSettingsErrorLog[] { + return this.errorLogs; + } + + /** + * Add an error to error logs list. + * + * @param error Error. + */ + addErrorLog(error: CoreSettingsErrorLog): void { + this.errorLogs.push(error); + } + +} + +export const CoreErrorLogs = makeSingleton(CoreErrorLogsService); + +export type CoreSettingsErrorLog = { + data?: unknown; + message: string; + method?: string; + time: number; + type: string; +};