Merge pull request #3849 from alfonso-salces/MOBILE-4451

MOBILE-4451 settings: Create error log page
main
Pau Ferrer Ocaña 2023-11-14 11:55:02 +01:00 committed by GitHub
commit 1624256ffb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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 { 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);
});
}

View File

@ -34,8 +34,9 @@
<ion-label>
<h2>Enable staging sites ({{stagingSitesCount}})</h2>
</ion-label>
<ion-toggle [(ngModel)]="enableStagingSites" (ionChange)="setEnabledStagingSites($event.detail.checked)"
slot="end"></ion-toggle>
<ion-toggle [(ngModel)]="enableStagingSites" (ionChange)="setEnabledStagingSites($event.detail.checked)" slot="end">
</ion-toggle>
</ion-item>
<ng-container *ngIf="siteId">
<ion-item class="ion-text-wrap">
@ -61,6 +62,12 @@
</ion-button>
</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-label>
<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 { 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.
*/

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 { 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 {}

View File

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

View File

@ -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<HttpResponse<any>>;
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,6 +877,7 @@ export class CoreWSProvider {
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
syncCall<T = unknown>(method: string, data: any, preSets: CoreWSPreSets): T {
try {
if (!preSets) {
throw new CoreError(Translate.instant('core.unexpectederror'));
} else if (!CoreNetwork.isOnline()) {
@ -912,6 +943,18 @@ export class CoreWSProvider {
}
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;
};