MOBILE-4693 error: Also display accordion for debug info in error modals

main
Dani Palou 2024-11-15 12:50:13 +01:00
parent 84781a7658
commit 3cfd550b58
10 changed files with 95 additions and 67 deletions

View File

@ -38,7 +38,7 @@ import { CorePromiseUtils } from '@singletons/promise-utils';
}) })
export class AddonModForumSearchPage implements OnInit { export class AddonModForumSearchPage implements OnInit {
loadMoreError: string | null = null; loadMoreError = false;
searchBanner: string | null = null; searchBanner: string | null = null;
resultsSource = new CoreSearchGlobalSearchResultsSource('', {}); resultsSource = new CoreSearchGlobalSearchResultsSource('', {});
forum?: AddonModForumData; forum?: AddonModForumData;
@ -128,7 +128,7 @@ export class AddonModForumSearchPage implements OnInit {
* Clear search results. * Clear search results.
*/ */
clearSearch(): void { clearSearch(): void {
this.loadMoreError = null; this.loadMoreError = false;
this.resultsSource.setQuery(''); this.resultsSource.setQuery('');
this.resultsSource.reset(); this.resultsSource.reset();
@ -152,7 +152,7 @@ export class AddonModForumSearchPage implements OnInit {
try { try {
await this.resultsSource?.load(); await this.resultsSource?.load();
} catch (error) { } catch (error) {
this.loadMoreError = CoreDomUtils.getErrorMessage(error); this.loadMoreError = true;
} finally { } finally {
complete(); complete();
} }

View File

@ -24,14 +24,26 @@ import { CoreErrorObject } from '@services/error-helper';
*/ */
export class CoreError extends Error { export class CoreError extends Error {
constructor(message?: string) { debug?: CoreErrorDebug;
constructor(message?: string, debug?: CoreErrorDebug) {
super(message); super(message);
// Fix prototype chain: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget // Fix prototype chain: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget
this.name = new.target.name; this.name = new.target.name;
Object.setPrototypeOf(this, new.target.prototype); Object.setPrototypeOf(this, new.target.prototype);
this.debug = debug;
} }
} }
/**
* Debug information of the error.
*/
export type CoreErrorDebug = {
code?: string; // Technical error code useful for technical assistance.
details: string; // Technical error details useful for technical assistance.
};
export type CoreAnyError = string | CoreError | CoreErrorObject | null | undefined; export type CoreAnyError = string | CoreError | CoreErrorObject | null | undefined;

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { CoreError } from '@classes/errors/error'; import { CoreError, CoreErrorDebug } from '@classes/errors/error';
import { CoreUserSupportConfig } from '@features/user/classes/support/support-config'; import { CoreUserSupportConfig } from '@features/user/classes/support/support-config';
/** /**
@ -20,7 +20,7 @@ import { CoreUserSupportConfig } from '@features/user/classes/support/support-co
*/ */
export class CoreSiteError extends CoreError { export class CoreSiteError extends CoreError {
debug?: CoreSiteErrorDebug; debug?: CoreErrorDebug;
supportConfig?: CoreUserSupportConfig; supportConfig?: CoreUserSupportConfig;
constructor(options: CoreSiteErrorOptions) { constructor(options: CoreSiteErrorOptions) {
@ -43,16 +43,11 @@ export class CoreSiteError extends CoreError {
} }
export type CoreSiteErrorDebug = {
code: string; // Technical error code useful for technical assistance.
details: string; // Technical error details useful for technical assistance.
};
export type CoreSiteErrorOptions = { export type CoreSiteErrorOptions = {
message: string; message: string;
// Debugging information. // Debugging information.
debug?: CoreSiteErrorDebug; debug?: CoreErrorDebug;
// Configuration to use to contact site support. If this attribute is present, it means // Configuration to use to contact site support. If this attribute is present, it means
// that the error warrants contacting support. // that the error warrants contacting support.

View File

@ -25,7 +25,7 @@ import {
CoreLoginSiteFinderSettings, CoreLoginSiteFinderSettings,
CoreLoginSiteSelectorListMethod, CoreLoginSiteSelectorListMethod,
} from '@features/login/services/login-helper'; } from '@features/login/services/login-helper';
import { CoreError } from '@classes/errors/error'; import { CoreError, CoreErrorDebug } from '@classes/errors/error';
import { CoreConstants } from '@/core/constants'; import { CoreConstants } from '@/core/constants';
import { Translate } from '@singletons'; import { Translate } from '@singletons';
import { CoreUrl, CoreUrlPartNames } from '@singletons/url'; import { CoreUrl, CoreUrlPartNames } from '@singletons/url';
@ -34,7 +34,7 @@ import { CoreCustomURLSchemes, CoreCustomURLSchemesHandleError } from '@services
import { CoreErrorHelper } from '@services/error-helper'; import { CoreErrorHelper } from '@services/error-helper';
import { CoreForms } from '@singletons/form'; import { CoreForms } from '@singletons/form';
import { AlertButton } from '@ionic/core'; import { AlertButton } from '@ionic/core';
import { CoreSiteError, CoreSiteErrorDebug } from '@classes/errors/siteerror'; import { CoreSiteError } from '@classes/errors/siteerror';
import { CoreUserSupport } from '@features/user/services/support'; import { CoreUserSupport } from '@features/user/services/support';
import { CoreErrorAccordion } from '@services/error-accordion'; import { CoreErrorAccordion } from '@services/error-accordion';
import { CoreUserSupportConfig } from '@features/user/classes/support/support-config'; import { CoreUserSupportConfig } from '@features/user/classes/support/support-config';
@ -419,7 +419,7 @@ export class CoreLoginSitePage implements OnInit {
*/ */
protected async showLoginIssue(url: string, error: CoreError): Promise<void> { protected async showLoginIssue(url: string, error: CoreError): Promise<void> {
let errorMessage = CoreDomUtils.getErrorMessage(error); let errorMessage = CoreDomUtils.getErrorMessage(error);
let debug: CoreSiteErrorDebug | undefined; let debug: CoreErrorDebug | undefined;
let errorTitle: string | undefined; let errorTitle: string | undefined;
let site: CoreUnauthenticatedSite | undefined; let site: CoreUnauthenticatedSite | undefined;
let supportConfig: CoreUserSupportConfig | undefined; let supportConfig: CoreUserSupportConfig | undefined;
@ -480,7 +480,7 @@ export class CoreLoginSitePage implements OnInit {
const containerElement = alertElement.querySelector('.core-error-accordion-container'); const containerElement = alertElement.querySelector('.core-error-accordion-container');
if (containerElement) { if (containerElement) {
await CoreErrorAccordion.render(containerElement, debug.code, debug.details); await CoreErrorAccordion.render(containerElement, debug.details, debug.code);
} }
} }
} }

View File

@ -26,7 +26,7 @@ import { CoreText } from '@singletons/text';
import { CoreObject } from '@singletons/object'; import { CoreObject } from '@singletons/object';
import { CoreConstants } from '@/core/constants'; import { CoreConstants } from '@/core/constants';
import { CoreSite } from '@classes/sites/site'; import { CoreSite } from '@classes/sites/site';
import { CoreError } from '@classes/errors/error'; import { CoreError, CoreErrorDebug } from '@classes/errors/error';
import { CoreWSError } from '@classes/errors/wserror'; import { CoreWSError } from '@classes/errors/wserror';
import { DomSanitizer, makeSingleton, Translate } from '@singletons'; import { DomSanitizer, makeSingleton, Translate } from '@singletons';
import { CoreLogger } from '@singletons/logger'; import { CoreLogger } from '@singletons/logger';
@ -57,7 +57,7 @@ import {
IDENTITY_PROVIDER_FEATURE_NAME_PREFIX, IDENTITY_PROVIDER_FEATURE_NAME_PREFIX,
} from '../constants'; } from '../constants';
import { LazyRoutesModule } from '@/app/app-routing.module'; import { LazyRoutesModule } from '@/app/app-routing.module';
import { CoreSiteError, CoreSiteErrorDebug } from '@classes/errors/siteerror'; import { CoreSiteError } from '@classes/errors/siteerror';
import { CoreQRScan } from '@services/qrscan'; import { CoreQRScan } from '@services/qrscan';
import { CoreLoadings } from '@services/loadings'; import { CoreLoadings } from '@services/loadings';
import { CoreErrorHelper } from '@services/error-helper'; import { CoreErrorHelper } from '@services/error-helper';
@ -949,7 +949,7 @@ export class CoreLoginHelperProvider {
* @param site Site instance. * @param site Site instance.
* @param debug Error debug information. * @param debug Error debug information.
*/ */
async showAppUnsupportedModal(siteUrl: string, site?: CoreUnauthenticatedSite, debug?: CoreSiteErrorDebug): Promise<void> { async showAppUnsupportedModal(siteUrl: string, site?: CoreUnauthenticatedSite, debug?: CoreErrorDebug): Promise<void> {
const siteName = await site?.getSiteName() ?? siteUrl; const siteName = await site?.getSiteName() ?? siteUrl;
await CoreDomUtils.showAlertWithOptions({ await CoreDomUtils.showAlertWithOptions({
@ -974,7 +974,7 @@ export class CoreLoginHelperProvider {
* @param siteUrl Site url. * @param siteUrl Site url.
* @param debug Error debug information. * @param debug Error debug information.
*/ */
async openInBrowserFallback(siteUrl: string, debug?: CoreSiteErrorDebug): Promise<void> { async openInBrowserFallback(siteUrl: string, debug?: CoreErrorDebug): Promise<void> {
CoreEvents.trigger(APP_UNSUPPORTED_CHURN, { siteUrl, debug }); CoreEvents.trigger(APP_UNSUPPORTED_CHURN, { siteUrl, debug });
await CoreOpener.openInBrowser(siteUrl, { showBrowserWarning: false }); await CoreOpener.openInBrowser(siteUrl, { showBrowserWarning: false });
@ -1677,7 +1677,7 @@ declare module '@singletons/events' {
*/ */
export interface CoreEventsData { export interface CoreEventsData {
[ALWAYS_SHOW_LOGIN_FORM_CHANGED]: { value: number }; [ALWAYS_SHOW_LOGIN_FORM_CHANGED]: { value: number };
[APP_UNSUPPORTED_CHURN]: { siteUrl: string; debug?: CoreSiteErrorDebug }; [APP_UNSUPPORTED_CHURN]: { siteUrl: string; debug?: CoreErrorDebug };
} }
} }

View File

@ -39,7 +39,7 @@ import { CorePromiseUtils } from '@singletons/promise-utils';
export class CoreSearchGlobalSearchPage implements OnInit, OnDestroy, AfterViewInit { export class CoreSearchGlobalSearchPage implements OnInit, OnDestroy, AfterViewInit {
courseId: number | null = null; courseId: number | null = null;
loadMoreError: string | null = null; loadMoreError = false;
searchBanner: string | null = null; searchBanner: string | null = null;
resultsSource = new CoreSearchGlobalSearchResultsSource('', {}); resultsSource = new CoreSearchGlobalSearchResultsSource('', {});
private filtersObserver?: CoreEventObserver; private filtersObserver?: CoreEventObserver;
@ -128,7 +128,7 @@ export class CoreSearchGlobalSearchPage implements OnInit, OnDestroy, AfterViewI
* Clear search results. * Clear search results.
*/ */
clearSearch(): void { clearSearch(): void {
this.loadMoreError = null; this.loadMoreError = false;
this.resultsSource.setQuery(''); this.resultsSource.setQuery('');
this.resultsSource.reset(); this.resultsSource.reset();
@ -172,7 +172,7 @@ export class CoreSearchGlobalSearchPage implements OnInit, OnDestroy, AfterViewI
try { try {
await this.resultsSource?.load(); await this.resultsSource?.load();
} catch (error) { } catch (error) {
this.loadMoreError = CoreDomUtils.getErrorMessage(error); this.loadMoreError = true;
} finally { } finally {
complete(); complete();
} }

View File

@ -38,11 +38,11 @@ export class CoreErrorAccordionService {
* Render an instance of the component into an HTML string. * Render an instance of the component into an HTML string.
* *
* @param element Root element. * @param element Root element.
* @param errorCode Error code.
* @param errorDetails Error details. * @param errorDetails Error details.
* @param errorCode Error code.
*/ */
async render(element: Element, errorCode: string, errorDetails: string): Promise<void> { async render(element: Element, errorDetails: string, errorCode?: string): Promise<void> {
const html = this.html(errorCode, errorDetails); const html = this.html(errorDetails, errorCode);
element.innerHTML = html; element.innerHTML = html;
@ -56,15 +56,15 @@ export class CoreErrorAccordionService {
* @param errorDetails Error details. * @param errorDetails Error details.
* @returns HTML. * @returns HTML.
*/ */
private html(errorCode: string, errorDetails: string): string { private html(errorDetails: string, errorCode?: string): string {
const contentId = CoreForms.uniqueId('error-accordion-content'); const contentId = CoreForms.uniqueId('error-accordion-content');
const errorCodeLabel = Translate.instant('core.errorcode', { errorCode }); const errorCodeLabel = errorCode ? Translate.instant('core.errorcode', { errorCode }) : undefined;
const hideDetailsLabel = Translate.instant('core.errordetailshide'); const hideDetailsLabel = Translate.instant('core.errordetailshide');
const showDetailsLabel = Translate.instant('core.errordetailsshow'); const showDetailsLabel = Translate.instant('core.errordetailsshow');
return ` return `
<div class="core-error-accordion"> <div class="core-error-accordion">
<h3 class="core-error-accordion--code">${errorCodeLabel}</h3> ${errorCodeLabel ? `<h3 class="core-error-accordion--code">${errorCodeLabel}</h3>` : ''}
<div id="${contentId}" class="core-error-accordion--details" role="region" aria-hidden="true"> <div id="${contentId}" class="core-error-accordion--details" role="region" aria-hidden="true">
<p>${errorDetails}</p> <p>${errorDetails}</p>
</div> </div>

View File

@ -13,10 +13,11 @@
// limitations under the License. // limitations under the License.
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { CoreAnyError, CoreError } from '@classes/errors/error'; import { CoreAnyError, CoreError, CoreErrorDebug } from '@classes/errors/error';
import { makeSingleton, Translate } from '@singletons'; import { makeSingleton, Translate } from '@singletons';
import { AlertButton } from '@ionic/angular'; import { AlertButton } from '@ionic/angular';
import { CoreWSError } from '@classes/errors/wserror'; import { CoreWSError } from '@classes/errors/wserror';
import { CoreText } from '@singletons/text';
/** /**
* Provider to provide some helper functions regarding files and packages. * Provider to provide some helper functions regarding files and packages.
@ -130,6 +131,39 @@ export class CoreErrorHelperService {
return builtMessage; return builtMessage;
} }
/**
* Get the debug info from an error object.
*
* @param error Error.
* @returns Error debug info, undefined if not found.
*/
getDebugInfoFromError(error?: CoreAnyError): CoreErrorDebug | undefined {
if (!error || typeof error === 'string') {
return;
}
if ('debug' in error) {
return error.debug;
}
// Escape the HTML of debug info so it is displayed as it is in the view.
const debugMessages: string[] = [];
if ('debuginfo' in error && error.debuginfo) {
debugMessages.push(CoreText.escapeHTML(error.debuginfo, false));
}
if ('backtrace' in error && error.backtrace) {
debugMessages.push(CoreText.replaceNewLines(
CoreText.escapeHTML(error.backtrace, false),
'<br>',
));
}
const debugMessage = debugMessages.join('<br><br>');
if (debugMessage) {
return { details: debugMessage };
}
}
/** /**
* Get the error message from an error object. * Get the error message from an error object.
* *

View File

@ -27,7 +27,7 @@ import {
CoreSiteConfig, CoreSiteConfig,
} from '@classes/sites/site'; } from '@classes/sites/site';
import { SQLiteDB, SQLiteDBRecordValues, SQLiteDBTableSchema } from '@classes/sqlitedb'; import { SQLiteDB, SQLiteDBRecordValues, SQLiteDBTableSchema } from '@classes/sqlitedb';
import { CoreError } from '@classes/errors/error'; import { CoreError, CoreErrorDebug } from '@classes/errors/error';
import { CoreLoginError, CoreLoginErrorOptions } from '@classes/errors/loginerror'; import { CoreLoginError, CoreLoginErrorOptions } from '@classes/errors/loginerror';
import { makeSingleton, Translate, Http } from '@singletons'; import { makeSingleton, Translate, Http } from '@singletons';
import { CoreLogger } from '@singletons/logger'; import { CoreLogger } from '@singletons/logger';
@ -64,7 +64,6 @@ import { CoreSiteInfo, CoreSiteInfoResponse, CoreSitePublicConfigResponse } from
import { CoreSiteWSPreSets } from '@classes/sites/authenticated-site'; import { CoreSiteWSPreSets } from '@classes/sites/authenticated-site';
import { firstValueFrom } from 'rxjs'; import { firstValueFrom } from 'rxjs';
import { CoreHTMLClasses } from '@singletons/html-classes'; import { CoreHTMLClasses } from '@singletons/html-classes';
import { CoreSiteErrorDebug } from '@classes/errors/siteerror';
import { CoreErrorHelper } from './error-helper'; import { CoreErrorHelper } from './error-helper';
import { CoreQueueRunner } from '@classes/queue-runner'; import { CoreQueueRunner } from '@classes/queue-runner';
import { CoreAppDB } from './app-db'; import { CoreAppDB } from './app-db';
@ -459,7 +458,7 @@ export class CoreSitesProvider {
critical: true, critical: true,
title: Translate.instant('core.cannotconnect'), title: Translate.instant('core.cannotconnect'),
message: Translate.instant('core.siteunavailablehelp', { site: siteUrl }), message: Translate.instant('core.siteunavailablehelp', { site: siteUrl }),
supportConfig: error.supportConfig, supportConfig: 'supportConfig' in error ? error.supportConfig : undefined,
debug: error.debug, debug: error.debug,
}; };
@ -688,7 +687,7 @@ export class CoreSitesProvider {
* @returns A promise rejected with the error info. * @returns A promise rejected with the error info.
*/ */
protected async treatInvalidAppVersion(result: number, siteId?: string): Promise<never> { protected async treatInvalidAppVersion(result: number, siteId?: string): Promise<never> {
let debug: CoreSiteErrorDebug | undefined; let debug: CoreErrorDebug | undefined;
let errorKey: string | undefined; let errorKey: string | undefined;
let translateParams = {}; let translateParams = {};

View File

@ -449,17 +449,6 @@ export class CoreDomUtilsProvider {
if (typeof error === 'object') { if (typeof error === 'object') {
if (this.debugDisplay) { if (this.debugDisplay) {
// Get the debug info. Escape the HTML so it is displayed as it is in the view.
if ('debuginfo' in error && error.debuginfo) {
extraInfo = '<br><br>' + CoreText.escapeHTML(error.debuginfo, false);
}
if ('backtrace' in error && error.backtrace) {
extraInfo += '<br><br>' + CoreText.replaceNewLines(
CoreText.escapeHTML(error.backtrace, false),
'<br>',
);
}
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(error); console.error(error);
} }
@ -1061,36 +1050,35 @@ export class CoreDomUtilsProvider {
if (typeof error !== 'string' && 'buttons' in error && typeof error.buttons !== 'undefined') { if (typeof error !== 'string' && 'buttons' in error && typeof error.buttons !== 'undefined') {
alertOptions.buttons = error.buttons; alertOptions.buttons = error.buttons;
} else if (error instanceof CoreSiteError) {
if (error.debug) {
alertOptions.message = `<p>${alertOptions.message}</p><div class="core-error-accordion-container"></div>`;
}
const supportConfig = error.supportConfig;
alertOptions.buttons = [Translate.instant('core.ok')];
if (supportConfig?.canContactSupport()) {
alertOptions.buttons.push({
text: Translate.instant('core.contactsupport'),
handler: () => CoreUserSupport.contact({
supportConfig,
subject: alertOptions.header,
message: `${error.debug?.code}\n\n${error.debug?.details}`,
}),
});
}
} else { } else {
alertOptions.buttons = [Translate.instant('core.ok')]; alertOptions.buttons = [Translate.instant('core.ok')];
} }
// For site errors, always show debug info.
const showDebugInfo = this.debugDisplay || error instanceof CoreSiteError;
const debugInfo = showDebugInfo && CoreErrorHelper.getDebugInfoFromError(error);
if (debugInfo) {
alertOptions.message = `<p>${message}</p><div class="core-error-accordion-container"></div>`;
}
if (error instanceof CoreSiteError && error.supportConfig?.canContactSupport()) {
alertOptions.buttons.push({
text: Translate.instant('core.contactsupport'),
handler: () => CoreUserSupport.contact({
supportConfig: error.supportConfig,
subject: alertOptions.header,
message: `${error.debug?.code}\n\n${error.debug?.details}`,
}),
});
}
const alertElement = await this.showAlertWithOptions(alertOptions, autocloseTime); const alertElement = await this.showAlertWithOptions(alertOptions, autocloseTime);
if (error instanceof CoreSiteError && error.debug) { if (debugInfo) {
const containerElement = alertElement.querySelector('.core-error-accordion-container'); const containerElement = alertElement.querySelector('.core-error-accordion-container');
if (containerElement) { if (containerElement) {
await CoreErrorAccordion.render(containerElement, error.debug.code, error.debug.details); await CoreErrorAccordion.render(containerElement, debugInfo.details, debugInfo.code);
} }
} }