MOBILE-4451 settings: Create error log page

main
Alfonso Salces 2023-11-02 11:25:22 +01:00
parent 99c1eb5376
commit 6da2742bd3
10 changed files with 298 additions and 69 deletions

View File

@ -64,6 +64,7 @@ import { CoreSiteError } from '@classes/errors/siteerror';
import { CoreUserAuthenticatedSupportConfig } from '@features/user/classes/support/authenticated-support-config'; import { CoreUserAuthenticatedSupportConfig } from '@features/user/classes/support/authenticated-support-config';
import { CoreLoginHelper } from '@features/login/services/login-helper'; import { CoreLoginHelper } from '@features/login/services/login-helper';
import { CorePath } from '@singletons/path'; import { CorePath } from '@singletons/path';
import { CoreErrorLogs } from '@singletons/error-logs';
/** /**
* QR Code type enumeration. * QR Code type enumeration.
@ -1156,7 +1157,15 @@ export class CoreSite {
// Request not executed, enqueue again. // Request not executed, enqueue again.
this.enqueueRequest(request); this.enqueueRequest(request);
} else if (response.error) { } 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 { } else {
let responseData = response.data ? CoreTextUtils.parseJSON(response.data) : {}; let responseData = response.data ? CoreTextUtils.parseJSON(response.data) : {};
// Match the behaviour of CoreWSProvider.call when no response is expected. // Match the behaviour of CoreWSProvider.call when no response is expected.
@ -1170,6 +1179,13 @@ export class CoreSite {
} catch (error) { } catch (error) {
// Error not specific to a single request, reject all promises. // Error not specific to a single request, reject all promises.
requests.forEach((request) => { requests.forEach((request) => {
CoreErrorLogs.addErrorLog({
method: request.method,
type: 'CoreSiteError',
message: String(error) ?? '',
time: new Date().getTime(),
data: request.data,
});
request.deferred.reject(error); request.deferred.reject(error);
}); });
} }

View File

@ -34,8 +34,9 @@
<ion-label> <ion-label>
<h2>Enable staging sites ({{stagingSitesCount}})</h2> <h2>Enable staging sites ({{stagingSitesCount}})</h2>
</ion-label> </ion-label>
<ion-toggle [(ngModel)]="enableStagingSites" (ionChange)="setEnabledStagingSites($event.detail.checked)" <ion-toggle [(ngModel)]="enableStagingSites" (ionChange)="setEnabledStagingSites($event.detail.checked)" slot="end">
slot="end"></ion-toggle> </ion-toggle>
</ion-item> </ion-item>
<ng-container *ngIf="siteId"> <ng-container *ngIf="siteId">
<ion-item class="ion-text-wrap"> <ion-item class="ion-text-wrap">
@ -61,6 +62,12 @@
</ion-button> </ion-button>
</ion-item> </ion-item>
<ion-item class="ion-text-wrap" (click)="openErrorLog()" [detail]="true" button>
<ion-label>
<p class="item-heading">Error log</p>
</ion-label>
</ion-item>
<ion-item-divider> <ion-item-divider>
<ion-label> <ion-label>
<h2>Disabled features</h2> <h2>Disabled features</h2>

View File

@ -19,6 +19,7 @@ import { CoreSettingsHelper } from '@features/settings/services/settings-helper'
import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins'; import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins';
import { CoreUserTours } from '@features/usertours/services/user-tours'; import { CoreUserTours } from '@features/usertours/services/user-tours';
import { CoreConfig } from '@services/config'; import { CoreConfig } from '@services/config';
import { CoreNavigator } from '@services/navigator';
import { CorePlatform } from '@services/platform'; import { CorePlatform } from '@services/platform';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom'; 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. * Copies site info.
*/ */

View File

@ -0,0 +1,51 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [text]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>
<h1>Error log</h1>
</ion-title>
<ion-buttons slot="end" *ngIf="errorLogs.length">
<ion-button fill="clear" (click)="copyInfo()" [attr.aria-label]="'core.settings.copyinfo' | translate">
<ion-icon slot="icon-only" name="fas-clipboard" aria-hidden="true"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list *ngIf="errorLogs.length; else noLogs">
<ion-item button lines="full" class="ion-text-wrap" *ngFor="let error of errorLogs">
<div class="ion-padding" [collapsible-item]="96">
<p class="item-heading">Trace</p>
<p class="ion-text-wrap">{{ error.message }}</p>
<ng-container *ngIf="error.method">
<p class="item-heading">Method</p>
<p class="ion-text-wrap">{{ error.method }}</p>
</ng-container>
<ng-container *ngIf="error.type">
<p class="item-heading">Type</p>
<p class="ion-text-wrap">{{ error.type }}</p>
</ng-container>
<ng-container *ngIf="error.data">
<p class="item-heading">Data</p>
<p class="ion-text-wrap">{{ error.data | json }}</p>
</ng-container>
<div *ngIf="error.time">
<span class="ion-text-end">{{ error.time | coreFormatDate :'strftimedatetimeshort' }}</span>
</div>
</div>
</ion-item>
</ion-list>
<ng-template #noLogs>
<core-empty-box message="No logs available" icon="fas-clipboard-question">
</core-empty-box>
</ng-template>
</ion-content>

View File

@ -0,0 +1,4 @@
.timestamp {
display: flex;
justify-content: end;
}

View File

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

View File

@ -28,6 +28,7 @@ import { CoreSettingsAboutPage } from '@features/settings/pages/about/about';
import { CoreSettingsLicensesPage } from '@features/settings/pages/licenses/licenses'; import { CoreSettingsLicensesPage } from '@features/settings/pages/licenses/licenses';
import { CoreSettingsDeviceInfoPage } from '@features/settings/pages/deviceinfo/deviceinfo'; import { CoreSettingsDeviceInfoPage } from '@features/settings/pages/deviceinfo/deviceinfo';
import { CoreSettingsDevPage } from '@features/settings/pages/dev/dev'; import { CoreSettingsDevPage } from '@features/settings/pages/dev/dev';
import { CoreSettingsErrorLogPage } from '@features/settings/pages/error-log/error-log';
const sectionRoutes: Routes = [ const sectionRoutes: Routes = [
{ {
@ -86,6 +87,10 @@ const routes: Routes = [
path: 'about/deviceinfo/dev', path: 'about/deviceinfo/dev',
component: CoreSettingsDevPage, component: CoreSettingsDevPage,
}, },
{
path: 'about/deviceinfo/dev/error-log',
component: CoreSettingsErrorLogPage,
},
{ {
path: 'about/licenses', path: 'about/licenses',
component: CoreSettingsLicensesPage, component: CoreSettingsLicensesPage,
@ -106,6 +111,7 @@ const routes: Routes = [
CoreSettingsLicensesPage, CoreSettingsLicensesPage,
CoreSettingsDeviceInfoPage, CoreSettingsDeviceInfoPage,
CoreSettingsDevPage, CoreSettingsDevPage,
CoreSettingsErrorLogPage,
], ],
}) })
export class CoreSettingsLazyModule {} export class CoreSettingsLazyModule {}

View File

@ -60,6 +60,7 @@ import { CoreCancellablePromise } from '@classes/cancellable-promise';
import { CoreLang } from '@services/lang'; import { CoreLang } from '@services/lang';
import { CorePasswordModalParams, CorePasswordModalResponse } from '@components/password-modal/password-modal'; import { CorePasswordModalParams, CorePasswordModalResponse } from '@components/password-modal/password-modal';
import { CoreWSError } from '@classes/errors/wserror'; import { CoreWSError } from '@classes/errors/wserror';
import { CoreErrorLogs } from '@singletons/error-logs';
/* /*
* "Utils" service with helper functions for UI, DOM elements and HTML code. * "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. // We received an object instead of a string. Search for common properties.
errorMessage = CoreTextUtils.getErrorMessageFromError(error); errorMessage = CoreTextUtils.getErrorMessageFromError(error);
CoreErrorLogs.addErrorLog({ message: JSON.stringify(error), type: errorMessage || '', time: new Date().getTime() });
if (!errorMessage) { if (!errorMessage) {
// No common properties found, just stringify it. // No common properties found, just stringify it.
errorMessage = JSON.stringify(error); errorMessage = JSON.stringify(error);

View File

@ -43,6 +43,7 @@ import { CoreSiteError, CoreSiteErrorOptions } from '@classes/errors/siteerror';
import { CoreUserGuestSupportConfig } from '@features/user/classes/support/guest-support-config'; import { CoreUserGuestSupportConfig } from '@features/user/classes/support/guest-support-config';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
import { CoreLang, CoreLangFormat } from './lang'; import { CoreLang, CoreLangFormat } from './lang';
import { CoreErrorLogs } from '@singletons/error-logs';
/** /**
* This service allows performing WS calls and download/upload files. * This service allows performing WS calls and download/upload files.
@ -412,9 +413,25 @@ export class CoreWSProvider {
let promise: Promise<HttpResponse<any>>; let promise: Promise<HttpResponse<any>>;
if (preSets.siteUrl === undefined) { 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()) { } 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) { if (preSets.responseExpected === undefined) {
@ -552,6 +569,10 @@ export class CoreWSProvider {
} }
throw new CoreAjaxError(options, 1, data.status); 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', { throw new CoreError(Translate.instant('core.serverconnection', {
details: CoreTextUtils.getErrorMessageFromError(error) ?? 'Unknown error', 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,6 +877,7 @@ export class CoreWSProvider {
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
syncCall<T = unknown>(method: string, data: any, preSets: CoreWSPreSets): T { syncCall<T = unknown>(method: string, data: any, preSets: CoreWSPreSets): T {
try {
if (!preSets) { if (!preSets) {
throw new CoreError(Translate.instant('core.unexpectederror')); throw new CoreError(Translate.instant('core.unexpectederror'));
} else if (!CoreNetwork.isOnline()) { } else if (!CoreNetwork.isOnline()) {
@ -912,6 +943,18 @@ export class CoreWSProvider {
} }
return 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;
}
} }
/* /*

View File

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