diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 8878e6c0d8..5d40ba9629 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,5 +1,8 @@ ## RELEASE NOTES +### Version 7.3.54-exui-4648-rc-1 +**EXUI-4648** Redact Sensitive Information Logged In through Console + ### Version 7.3.54 **EXUI-4636** Revert CME-220 and EXUI-2111 **EXUI-4637** Revert EXUI-2645 diff --git a/package.json b/package.json index 38447a3b4c..cb91480aec 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hmcts/ccd-case-ui-toolkit", - "version": "7.3.54", + "version": "7.3.54-exui-4648-rc-1", "engines": { "node": ">=20.19.0" }, diff --git a/projects/ccd-case-ui-toolkit/package.json b/projects/ccd-case-ui-toolkit/package.json index 6fdf4141f8..370fd0140c 100644 --- a/projects/ccd-case-ui-toolkit/package.json +++ b/projects/ccd-case-ui-toolkit/package.json @@ -1,6 +1,6 @@ { "name": "@hmcts/ccd-case-ui-toolkit", - "version": "7.3.54", + "version": "7.3.54-exui-4648-rc-1", "engines": { "node": ">=20.19.0" }, diff --git a/projects/ccd-case-ui-toolkit/src/lib/app-config.mock.ts b/projects/ccd-case-ui-toolkit/src/lib/app-config.mock.ts index cf17289af2..83ca64ed14 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/app-config.mock.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/app-config.mock.ts @@ -5,8 +5,11 @@ import { AccessManagementBasicViewMockModel, AccessManagementRequestReviewMockModel } from './app.config'; +import { StructuredLoggerService } from './shared/services/logging'; export class AppMockConfig implements AbstractAppConfig { + private readonly logger = new StructuredLoggerService(); + public getActivityBatchCollectionDelayMs(): number { return 0; } @@ -220,7 +223,7 @@ export class AppMockConfig implements AbstractAppConfig { } public logMessage(msg: string): void { - console.log(msg); + this.logger.info('Application log message.', { message: msg }); } public getEnableServiceSpecificMultiFollowups(): string[] { diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-edit-page/case-edit-page.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-edit-page/case-edit-page.component.ts index 15469dba6a..b4c503c54e 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-edit-page/case-edit-page.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-edit-page/case-edit-page.component.ts @@ -9,7 +9,7 @@ import { CaseEventData } from '../../../domain/case-event-data.model'; import { CaseEventTrigger } from '../../../domain/case-view/case-event-trigger.model'; import { CaseField } from '../../../domain/definition'; import { DRAFT_PREFIX } from '../../../domain/draft.model'; -import { AddressesService, LoadingService, MultipageComponentStateService } from '../../../services'; +import { AddressesService, LoadingService, MultipageComponentStateService, StructuredLoggerService } from '../../../services'; import { CaseFieldService } from '../../../services/case-fields/case-field.service'; import { FieldsUtils } from '../../../services/fields'; import { FormErrorService } from '../../../services/form/form-error.service'; @@ -65,6 +65,7 @@ export class CaseEditPageComponent implements OnInit, AfterViewChecked, OnDestro public dialogRefAfterClosedSub: Subscription; public saveDraftSub: Subscription; public caseFormValidationErrorsSub: Subscription; + private readonly logger = new StructuredLoggerService(); private static scrollToTop(): void { window.scrollTo(0, 0); @@ -409,7 +410,6 @@ export class CaseEditPageComponent implements OnInit, AfterViewChecked, OnDestro if (!this.caseEdit.isSubmitting && !this.currentPageIsNotValid()) { this.addressService.setMandatoryError(false); - console.log('Case Edit Error', this.caseEdit.error); if (this.caseEdit.validPageList.findIndex(page=> page.id === this.currentPage.id) === -1) { this.caseEdit.validPageList.push(this.currentPage); } @@ -648,7 +648,7 @@ export class CaseEditPageComponent implements OnInit, AfterViewChecked, OnDestro this.formErrorService .mapFieldErrors(this.caseEdit.error.details.field_errors, this.editForm?.controls?.['data'] as FormGroup, 'validation'); } - console.log('handleError ', error); + this.logger.error('Case edit page handled an error.', { error }); } private resetErrors(): void { diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/cases.service.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/cases.service.ts index 64eb15236a..14bd5aeabe 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/cases.service.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/cases.service.ts @@ -16,7 +16,7 @@ import { RoleRequestPayload, SpecificAccessRequest } from '../../../domain'; import { UserInfo } from '../../../domain/user/user-info.model'; -import { FieldsUtils, HttpErrorService, HttpService, LoadingService, OrderService, RetryUtil, SessionStorageService } from '../../../services'; +import { FieldsUtils, HttpErrorService, HttpService, LoadingService, OrderService, RetryUtil, SessionStorageService, StructuredLoggerService } from '../../../services'; import { LinkedCasesResponse } from '../../palette/linked-cases/domain/linked-cases.model'; import { CaseAccessUtils } from '../case-access-utils'; import { WizardPage } from '../domain'; @@ -26,6 +26,8 @@ import { CaseEditComponent } from '../case-edit'; @Injectable() export class CasesService { + private readonly logger = new StructuredLoggerService(); + // Internal (UI) API public static readonly V2_MEDIATYPE_CASE_VIEW = 'application/vnd.uk.gov.hmcts.ccd-data-store-api.ui-case-view.v2+json'; public static readonly V2_MEDIATYPE_START_CASE_TRIGGER = @@ -107,11 +109,10 @@ export class CasesService { const artificialDelay: number = this.appConfig.getTimeoutsCaseRetrievalArtificialDelay(); const timeoutPeriods = this.appConfig.getTimeoutsForCaseRetrieval(); - console.log(`Timeout periods: ${timeoutPeriods} seconds.`); if (timeoutPeriods && timeoutPeriods.length > 0 && timeoutPeriods[0] > 0) { http$ = this.retryUtil.pipeTimeoutMechanismOn(http$, artificialDelay, timeoutPeriods); } else { - console.warn('Skipping to pipe a retry mechanism!'); + this.logger.warn('Skipping retry mechanism for case view retrieval.'); } http$ = this.pipeErrorProcessor(http$); @@ -123,8 +124,11 @@ export class CasesService { private pipeErrorProcessor(in$: Observable): Observable { const out$ = in$.pipe(catchError(error => { - console.error(`Error while getting case view with getCaseViewV2! Error type: '${typeof error}, Error name: '${error?.name}'`); - console.error(error); + this.logger.error('Error while getting case view with getCaseViewV2.', { + error, + errorName: error?.name, + errorType: typeof error + }); this.errorService.setError(error); return throwError(error); })); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/event-completion-state-machine.service.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/event-completion-state-machine.service.ts index 7ebee515b0..37069c38fb 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/event-completion-state-machine.service.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/event-completion-state-machine.service.ts @@ -186,7 +186,6 @@ export class EventCompletionStateMachineService { public entryActionForStateFinal(state: State, context: EventCompletionStateMachineContext): void { // Final actions can be performed here, the state machine finished running - console.log('FINAL'); } public addTransitionsForStateCheckTasksCanBeCompleted(): void { diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/work-allocation.service.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/work-allocation.service.ts index 822965ac6b..7ddce35372 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/work-allocation.service.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/work-allocation.service.ts @@ -52,7 +52,6 @@ export class WorkAllocationService { // explicitly eat away 401 error and 400 error if (error && error.status && (error.status === 401 || error.status === 400)) { // do nothing - console.log('error status 401 or 400', error); } else { return throwError(error); } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-history/case-history.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-history/case-history.component.ts index a2ef385f1f..c0bc89f23b 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-history/case-history.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-history/case-history.component.ts @@ -7,6 +7,7 @@ import { CaseTab } from '../../domain/case-view/case-tab.model'; import { CaseView } from '../../domain/case-view/case-view.model'; import { HttpError } from '../../domain/http/http-error.model'; import { AlertService } from '../../services/alert/alert.service'; +import { StructuredLoggerService } from '../../services/logging'; import { OrderService } from '../../services/order/order.service'; import { CaseNotifier } from '../case-editor/services/case.notifier'; import { CaseHistory } from './domain/case-history.model'; @@ -19,6 +20,8 @@ import { CaseHistoryService } from './services/case-history.service'; standalone: false }) export class CaseHistoryComponent implements OnInit, OnDestroy { + private readonly logger = new StructuredLoggerService(); + public static readonly PARAM_EVENT_ID = 'eid'; private static readonly ERROR_MESSAGE = 'No case history to show'; @@ -59,7 +62,7 @@ export class CaseHistoryComponent implements OnInit, OnDestroy { this.tabs = this.sortTabFieldsAndFilterTabs(this.tabs); }), catchError(error => { - console.error(error); + this.logger.error('Error while getting case history.', { error }); if (error.status !== 401 && error.status !== 403) { this.alertService.error(error.message); } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-event-trigger/case-event-trigger.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-event-trigger/case-event-trigger.component.ts index f2552880b9..0e88e8eb00 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-event-trigger/case-event-trigger.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-event-trigger/case-event-trigger.component.ts @@ -56,7 +56,6 @@ export class CaseEventTriggerComponent implements OnInit, OnDestroy { if (this.activityPollingService.isEnabled) { this.ngZone.runOutsideAngular( () => { this.activitySubscription = this.postEditActivity().subscribe(() => { - // console.log('Posted EDIT activity and result is: ' + JSON.stringify(_resolved)); }); }); } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-view/case-view.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-view/case-view.component.ts index e86d27ac8e..06ef2b92a4 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-view/case-view.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-view/case-view.component.ts @@ -17,7 +17,6 @@ import { CasesService } from '../../case-editor/services/cases.service'; standalone: false }) export class CaseViewComponent implements OnInit, OnDestroy { - @Input() public case: string; @Input() @@ -79,8 +78,6 @@ export class CaseViewComponent implements OnInit, OnDestroy { private checkErrorGettingCaseView(error: any) { // TODO Should be logged to remote logging infrastructure - console.error('Called checkErrorGettingCaseView.'); - console.error(error); if (error.status !== 401 && error.status !== 403) { this.alertService.error(error.message); } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-viewer.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-viewer.component.ts index 09ed75122e..abcd22406f 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-viewer.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-viewer.component.ts @@ -52,7 +52,6 @@ export class CaseViewerComponent implements OnInit, OnDestroy { this.setUserAccessType(this.caseDetails); } else { this.caseSubscription = this.caseNotifier.caseView.subscribe((caseDetails) => { - console.info('Setting the case into case viewer component as retrieved from XHR request.'); this.caseDetails = caseDetails; this.setUserAccessType(this.caseDetails); }); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/services/case.resolver.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/services/case.resolver.ts index 23a9d2e262..67af542748 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/services/case.resolver.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/services/case.resolver.ts @@ -21,6 +21,7 @@ export class CaseResolver implements Resolve { public static defaultWAPage = '/work/my-work/list'; public static defaultPage = '/cases'; + // we need to run the CaseResolver on every child route of 'case/:jid/:ctid/:cid' // this is achieved with runGuardsAndResolvers: 'always' configuration // we cache the case view to avoid retrieving it for each child route @@ -42,12 +43,10 @@ export class CaseResolver implements Resolve { // Prevent resolving if eventId=queryManagementRespondQuery is in the URL if (currentUrl.includes(CaseResolver.EVENT_ID_QM_RESPOND_TO_QUERY)) { - console.info('Skipping resolve for event queryManagementRespondQuery.'); this.goToDefaultPage(); } if (!cid) { - console.info('No case ID available in the route. Will navigate to case list.'); // when redirected to case view after a case created, and the user has no READ access, // the post returns no id this.navigateToCaseList(); @@ -112,8 +111,6 @@ export class CaseResolver implements Resolve { } private processErrorInCaseFetch(error: any, caseReference: string) { - console.error('!!! processErrorInCaseFetch !!!'); - console.error(error); // TODO Should be logged to remote logging infrastructure if (error.status === 400) { this.router.navigate(['/search/noresults']); @@ -142,7 +139,6 @@ export class CaseResolver implements Resolve { // as discussed for EUI-5456, need functionality to go to default page private goToDefaultPage(): void { - console.info('Going to default page!'); const userDetails = safeJsonParse(this.sessionStorage.getItem(USER_DETAILS)); userDetails && userDetails.roles && !userDetails.roles.includes(PUI_CASE_MANAGER) diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/event-start/services/event-start-state-machine.service.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/event-start/services/event-start-state-machine.service.ts index aca909de2c..4deec79839 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/event-start/services/event-start-state-machine.service.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/event-start/services/event-start-state-machine.service.ts @@ -187,9 +187,7 @@ export class EventStartStateMachineService { task = context.tasks[0]; } - const taskStr = JSON.stringify(task); this.abstractConfig?.logMessage?.(`entryActionForStateOneTaskAssignedToUser: task_state ${task?.task_state} for task id ${task?.id}`); - console.log('entryActionForStateOneTaskAssignedToUser: setting client context task_data to ' + taskStr); // Store task to session const currentLanguage = context.cookieService.getCookie('exui-preferred-language'); const clientContext = { @@ -232,7 +230,6 @@ export class EventStartStateMachineService { public finalAction(state: State): void { // Final actions can be performed here, the state machine finished running - // console.log('FINAL', state); return; } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/address/write-address-field.component.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/address/write-address-field.component.spec.ts index b3fd2c90ba..0c60fbab80 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/address/write-address-field.component.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/address/write-address-field.component.spec.ts @@ -382,13 +382,19 @@ describe('WriteAddressFieldComponent', () => { const errorMessage = 'Service error'; const errorSubject = new Subject(); addressesService.getAddressesForPostcode.and.returnValue(errorSubject.asObservable()); - spyOn(console, 'log'); + const logSpy = spyOn(console, 'log'); + const errorSpy = spyOn(console, 'error'); writeAddressFieldComponent.postcode.setValue(POSTCODE); writeAddressFieldComponent.findAddress(); expect(writeAddressFieldComponent.loadingAddresses).toBeTruthy(); errorSubject.error(errorMessage); expect(writeAddressFieldComponent.loadingAddresses).toBeFalsy(); - expect(console.log).toHaveBeenCalledWith(`An error occurred retrieving addresses for postcode ${POSTCODE}. ${errorMessage}`); + expect(logSpy).not.toHaveBeenCalled(); + expect(errorSpy.calls.mostRecent().args[0]).toEqual(jasmine.objectContaining({ + level: 'error', + message: 'An error occurred retrieving addresses for postcode.' + })); + expect(JSON.stringify(errorSpy.calls.mostRecent().args[0])).not.toContain(POSTCODE); }); it('should not set loadingAddresses to true when postcode is missing', () => { diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/address/write-address-field.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/address/write-address-field.component.ts index ad17967e3b..159a5e3ba4 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/address/write-address-field.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/address/write-address-field.component.ts @@ -4,6 +4,7 @@ import { AddressValidationConstants } from '../../../commons/address-validation- import { FocusElementDirective } from '../../../directives/focus-element'; import { AddressModel } from '../../../domain/addresses/address.model'; import { AddressesService } from '../../../services/addresses/addresses.service'; +import { StructuredLoggerService } from '../../../services/logging'; import { AbstractFieldWriteComponent } from '../base-field/abstract-field-write.component'; import { WriteComplexFieldComponent } from '../complex/write-complex-field.component'; import { IsCompoundPipe } from '../utils/is-compound.pipe'; @@ -16,6 +17,8 @@ import { AddressOption } from './address-option.model'; standalone: false }) export class WriteAddressFieldComponent extends AbstractFieldWriteComponent implements OnInit, OnChanges { + private readonly logger = new StructuredLoggerService(); + @ViewChild('writeComplexFieldComponent', { static: false }) public writeComplexFieldComponent: WriteComplexFieldComponent; @@ -81,7 +84,7 @@ export class WriteAddressFieldComponent extends AbstractFieldWriteComponent impl ); }, (error) => { this.loadingAddresses = false; - console.log(`An error occurred retrieving addresses for postcode ${postcode}. ${error}`); + this.logger.error('An error occurred retrieving addresses for postcode.', { error }); }); this.addressList.setValue(undefined); this.refocusElement(); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/collection/write-collection-field.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/collection/write-collection-field.component.ts index 9f6e798a55..2839e23fbd 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/collection/write-collection-field.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/collection/write-collection-field.component.ts @@ -11,6 +11,7 @@ import { CaseField } from '../../../domain/definition/case-field.model'; import { Profile } from '../../../domain/profile/profile.model'; import { FieldsUtils } from '../../../services/fields/fields.utils'; import { FormValidatorsService } from '../../../services/form/form-validators.service'; +import { StructuredLoggerService } from '../../../services/logging'; import { ProfileNotifier } from '../../../services/profile/profile.notifier'; import { RemoveDialogComponent } from '../../dialogs/remove-dialog/remove-dialog.component'; import { AbstractFieldWriteComponent } from '../base-field/abstract-field-write.component'; @@ -31,6 +32,8 @@ type CollectionItem = { standalone: false }) export class WriteCollectionFieldComponent extends AbstractFieldWriteComponent implements OnInit, OnDestroy { + private readonly logger = new StructuredLoggerService(); + @Input() public caseFields: CaseField[] = []; @@ -195,7 +198,7 @@ export class WriteCollectionFieldComponent extends AbstractFieldWriteComponent i duration: 1000, offset: -150, }) - .subscribe(() => { }, console.error); + .subscribe(() => { }, error => this.logger.error('Error while scrolling collection item into view.', { error })); } this.focusLastItem(); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/document/write-document-field.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/document/write-document-field.component.ts index e7940f56f2..cedec6c9da 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/document/write-document-field.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/document/write-document-field.component.ts @@ -95,7 +95,6 @@ export class WriteDocumentFieldComponent extends AbstractFieldWriteComponent imp this.jurisdictionId = parts[parts.indexOf('case-create') + 1]; this.caseTypeId = parts[parts.indexOf('case-create') + 2]; this.caseId = null; - console.log(this.jurisdictionId); console.log(this.caseTypeId); } // use the documentManagement service to check if the document upload should use CDAM diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-check-your-answers/query-check-your-answers.component.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-check-your-answers/query-check-your-answers.component.spec.ts index 21133bddb9..a0004da75b 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-check-your-answers/query-check-your-answers.component.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-check-your-answers/query-check-your-answers.component.spec.ts @@ -873,12 +873,15 @@ describe('QueryCheckYourAnswersComponent', () => { component.fieldId = 'anyField'; component.qmCaseQueriesCollectionData = { anyField: { partyName: '', roleOnCase: '', caseMessages: [] } }; - spyOn(console, 'error'); + const errorSpy = spyOn(console, 'error'); spyOn(window, 'scrollTo'); component.submit(); - expect(console.error).toHaveBeenCalledWith('Error: No task to complete was found'); + expect(errorSpy.calls.mostRecent().args[0]).toEqual(jasmine.objectContaining({ + level: 'error', + message: 'No task to complete was found.' + })); // errorMessages should be set with fallback content expect(component.errorMessages?.length).toBeGreaterThan(0); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-check-your-answers/query-check-your-answers.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-check-your-answers/query-check-your-answers.component.ts index ea0aedffc5..9793ba103c 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-check-your-answers/query-check-your-answers.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-check-your-answers/query-check-your-answers.component.ts @@ -20,7 +20,8 @@ import { Task } from '../../../../../domain/work-allocation/Task'; import { QueryManagementService } from '../../services/query-management.service'; import { AlertService, - ErrorNotifierService + ErrorNotifierService, + StructuredLoggerService } from '../../../../../services'; @Component({ @@ -30,6 +31,8 @@ import { standalone: false }) export class QueryCheckYourAnswersComponent implements OnInit, OnDestroy { + private readonly logger = new StructuredLoggerService(); + private readonly RAISE_A_QUERY_EVENT_TRIGGER_ID = 'queryManagementRaiseQuery'; private readonly RESPOND_TO_QUERY_EVENT_TRIGGER_ID = 'queryManagementRespondQuery'; private readonly CASE_QUERIES_COLLECTION_ID = 'CaseQueriesCollection'; @@ -134,7 +137,7 @@ export class QueryCheckYourAnswersComponent implements OnInit, OnDestroy { if (error.status !== 401 && error.status !== 403) { this.errorNotifierService.announceError(error); this.alertService.error({ phrase: error.message }); - console.error('Error occurred while fetching event data:', error); + this.logger.error('Error occurred while fetching event data.', { error }); this.callbackErrorsSubject.next(error); } else { this.errorMessages = [ @@ -199,7 +202,7 @@ export class QueryCheckYourAnswersComponent implements OnInit, OnDestroy { error: (error) => this.handleError(error) }); } else { - console.error('Error: No task to complete was found'); + this.logger.error('No task to complete was found.'); this.errorMessages = [ { title: 'Error', @@ -245,7 +248,7 @@ export class QueryCheckYourAnswersComponent implements OnInit, OnDestroy { } private handleError(error: any): void { - console.error('Error in API calls:', error); + this.logger.error('Error in query management API calls.', { error }); this.isSubmitting = false; if (this.isServiceErrorFound(error)){ @@ -276,7 +279,7 @@ export class QueryCheckYourAnswersComponent implements OnInit, OnDestroy { public setCaseQueriesCollectionData(): void { if (!this.eventData) { - console.warn('Event data not available; skipping collection setup.'); + this.logger.warn('Event data not available; skipping collection setup.'); } this.queryManagementService.setCaseQueriesCollectionData( diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-confirmation/query-confirmation.component.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-confirmation/query-confirmation.component.spec.ts index bd85d69580..a92ede35b4 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-confirmation/query-confirmation.component.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-confirmation/query-confirmation.component.spec.ts @@ -168,11 +168,14 @@ describe('QueryConfirmationComponent', () => { component.eventResponseData = undefined as any; component.queryCreateContext = QueryCreateContext.RESPOND; - spyOn(console, 'warn'); + const warnSpy = spyOn(console, 'warn'); expect(() => component.resolveHmctsStaffRaisedQuery()).not.toThrow(); - expect(console.warn).toHaveBeenCalledWith('No event response data available.'); + expect(warnSpy.calls.mostRecent().args[0]).toEqual(jasmine.objectContaining({ + level: 'warn', + message: 'No event response data available.' + })); }); it('should call sessionStorageService.getItem when computing HMCTS staff flags on ngOnInit', () => { @@ -249,14 +252,17 @@ describe('QueryConfirmationComponent', () => { component.queryCreateContext = QueryCreateContext.RESPOND; component.eventResponseData = dummyCaseQueriesCollection; - spyOn(console, 'warn'); + const warnSpy = spyOn(console, 'warn'); component.resolveHmctsStaffRaisedQuery(); - expect(console.warn).toHaveBeenCalledWith( - 'No matching child found for messageId:', - 'nonexistent-id' - ); + expect(warnSpy.calls.mostRecent().args[0]).toEqual(jasmine.objectContaining({ + level: 'warn', + message: 'No matching child found for messageId.', + context: jasmine.objectContaining({ + messageId: 'nonexistent-id' + }) + })); // Component should exit early and not set isHmctsStaffRaisedQuery expect(component.isHmctsStaffRaisedQuery).toBeUndefined(); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-confirmation/query-confirmation.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-confirmation/query-confirmation.component.ts index 8dc4321757..8830f7372a 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-confirmation/query-confirmation.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-confirmation/query-confirmation.component.ts @@ -1,7 +1,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { QueryCreateContext } from '../../models/query-create-context.enum'; -import { SessionStorageService } from '../../../../../services'; +import { SessionStorageService, StructuredLoggerService } from '../../../../../services'; import { isInternalUser, isJudiciaryUser } from '../../../../../utils'; import { CaseQueriesCollection, QueryListData } from '../../models'; @@ -11,6 +11,8 @@ import { CaseQueriesCollection, QueryListData } from '../../models'; standalone: false }) export class QueryConfirmationComponent implements OnInit { + private readonly logger = new StructuredLoggerService(); + @Input() public queryCreateContext: QueryCreateContext; @Input() public callbackConfirmationMessageText: { [key: string]: string } = {}; @Input() public eventResponseData: CaseQueriesCollection; @@ -49,7 +51,7 @@ export class QueryConfirmationComponent implements OnInit { public resolveHmctsStaffRaisedQuery(): void { const messageId = this.route.snapshot.params.dataid; if (!this.eventResponseData) { - console.warn('No event response data available.'); + this.logger.warn('No event response data available.'); return; } @@ -68,7 +70,7 @@ export class QueryConfirmationComponent implements OnInit { .find((c) => c.parentId === messageId); if (!child) { - console.warn('No matching child found for messageId:', messageId); + this.logger.warn('No matching child found for messageId.', { messageId }); return; } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-write/query-write-raise-query/query-write-raise-query.component.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-write/query-write-raise-query/query-write-raise-query.component.spec.ts index 3f70cf153a..ae51a8f348 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-write/query-write-raise-query/query-write-raise-query.component.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-write/query-write-raise-query/query-write-raise-query.component.spec.ts @@ -154,10 +154,13 @@ describe('QueryWriteRaiseQueryComponent', () => { it('should warn and return false when eventData is null in setCaseQueriesCollectionData()', () => { - spyOn(console, 'warn'); + const warnSpy = spyOn(console, 'warn'); component.eventData = null; const result = component.setCaseQueriesCollectionData(); - expect(console.warn).toHaveBeenCalledWith('Event data not available; skipping collection setup.'); + expect(warnSpy.calls.mostRecent().args[0]).toEqual(jasmine.objectContaining({ + level: 'warn', + message: 'Event data not available; skipping collection setup.' + })); expect(result).toBeFalsy(); }); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-write/query-write-raise-query/query-write-raise-query.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-write/query-write-raise-query/query-write-raise-query.component.ts index 2d44b655a0..f778afb701 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-write/query-write-raise-query/query-write-raise-query.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-write/query-write-raise-query/query-write-raise-query.component.ts @@ -11,6 +11,7 @@ import { QmCaseQueriesCollection, QueryCreateContext, QueryListItem } from '../. import { EventCompletionParams } from '../../../../../case-editor/domain/event-completion-params.model'; import { QueryManagementService } from '../../../services'; import { ActivatedRoute } from '@angular/router'; +import { StructuredLoggerService } from '../../../../../../services'; @Component({ selector: 'ccd-query-write-raise-query', @@ -18,6 +19,8 @@ import { ActivatedRoute } from '@angular/router'; standalone: false }) export class QueryWriteRaiseQueryComponent implements OnChanges { + private readonly logger = new StructuredLoggerService(); + @Input() public formGroup: FormGroup; @Input() public submitted: boolean; @Input() public caseDetails: CaseView; @@ -72,7 +75,7 @@ export class QueryWriteRaiseQueryComponent implements OnChanges { public setCaseQueriesCollectionData(): boolean { if (!this.eventData) { - console.warn('Event data not available; skipping collection setup.'); + this.logger.warn('Event data not available; skipping collection setup.'); return false; } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-write/query-write-respond-to-query/query-write-respond-to-query.component.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-write/query-write-respond-to-query/query-write-respond-to-query.component.spec.ts index b82cb60dcc..723b92da3c 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-write/query-write-respond-to-query/query-write-respond-to-query.component.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-write/query-write-respond-to-query/query-write-respond-to-query.component.spec.ts @@ -139,16 +139,19 @@ describe('QueryWriteRespondToQueryComponent', () => { expect(component.caseDetails).toEqual(CASE_VIEW); })); - it('should log error if caseNotifier emits an error', fakeAsync(() => { + it('should log a redacted structured error if caseNotifier emits an error', fakeAsync(() => { const errorMessage = 'Error retrieving case details'; - spyOn(console, 'error'); + const consoleSpy = spyOn(console, 'error'); mockCaseNotifier.fetchAndRefresh.and.returnValue(throwError(() => errorMessage)); component.ngOnInit(); tick(); - expect(console.error).toHaveBeenCalledWith('Error retrieving case details:', errorMessage); + expect(consoleSpy.calls.mostRecent().args[0]).toEqual(jasmine.objectContaining({ + level: 'error', + message: 'Error retrieving case details.' + })); })); it('should emit value when hasResponded is called', () => { @@ -189,11 +192,14 @@ describe('QueryWriteRespondToQueryComponent', () => { expect(consoleSpy).not.toHaveBeenCalled(); }); - it('should log error if caseQueriesCollections[0] is undefined', () => { + it('should log a redacted structured error if caseQueriesCollections[0] is undefined', () => { component.caseQueriesCollections = [undefined as any]; const consoleSpy = spyOn(console, 'error'); component.ngOnChanges(); - expect(consoleSpy).toHaveBeenCalledWith('caseQueriesCollections[0] is undefined!', component.caseQueriesCollections); + expect(consoleSpy.calls.mostRecent().args[0]).toEqual(jasmine.objectContaining({ + level: 'error', + message: 'Case queries collection is undefined.' + })); }); it('should warn and return if no messageId is found', () => { @@ -203,7 +209,13 @@ describe('QueryWriteRespondToQueryComponent', () => { const warnSpy = spyOn(console, 'warn'); component.ngOnChanges(); - expect(warnSpy).toHaveBeenCalledWith('No messageId found in route params:', activatedRoute.snapshot.params); + expect(warnSpy.calls.mostRecent().args[0]).toEqual(jasmine.objectContaining({ + level: 'warn', + message: 'No messageId found in route params.', + context: jasmine.objectContaining({ + routeParams: activatedRoute.snapshot.params + }) + })); }); it('should filter parent query when responding to a child message', () => { @@ -248,10 +260,13 @@ describe('QueryWriteRespondToQueryComponent', () => { component.ngOnChanges(); - expect(warnSpy).toHaveBeenCalledWith( - 'No matching message found for ID:', - unmatchedMessageId - ); + expect(warnSpy.calls.mostRecent().args[0]).toEqual(jasmine.objectContaining({ + level: 'warn', + message: 'No matching message found for ID.', + context: jasmine.objectContaining({ + messageId: unmatchedMessageId + }) + })); expect(component.queryListData).toBeUndefined(); expect(component.queryItem).toBeUndefined(); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-write/query-write-respond-to-query/query-write-respond-to-query.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-write/query-write-respond-to-query/query-write-respond-to-query.component.ts index 044fb41485..c6d5bc0997 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-write/query-write-respond-to-query/query-write-respond-to-query.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-write/query-write-respond-to-query/query-write-respond-to-query.component.ts @@ -9,6 +9,7 @@ import { CaseNotifier } from '../../../../../case-editor/services'; import { RaiseQueryErrorMessage } from '../../../enums'; import { CaseQueriesCollection, QmCaseQueriesCollection, QueryCreateContext, QueryListData, QueryListItem } from '../../../models'; import { QueryManagementService } from '../../../services'; +import { StructuredLoggerService } from '../../../../../../services'; @Component({ selector: 'ccd-query-write-respond-to-query', templateUrl: './query-write-respond-to-query.component.html', @@ -17,6 +18,8 @@ import { QueryManagementService } from '../../../services'; }) export class QueryWriteRespondToQueryComponent implements OnInit, OnChanges { + private readonly logger = new StructuredLoggerService(); + @Input() public queryItem: QueryListItem; @Input() public formGroup: FormGroup; @Input() public queryCreateContext: QueryCreateContext; @@ -55,7 +58,7 @@ export class QueryWriteRespondToQueryComponent implements OnInit, OnChanges { this.caseDetails = caseDetails; }, error: (err) => { - console.error('Error retrieving case details:', err); + this.logger.error('Error retrieving case details.', { error: err }); } }); } @@ -67,13 +70,13 @@ export class QueryWriteRespondToQueryComponent implements OnInit, OnChanges { } if (!this.caseQueriesCollections[0]) { - console.error('caseQueriesCollections[0] is undefined!', this.caseQueriesCollections); + this.logger.error('Case queries collection is undefined.'); return; } this.messageId = this.route.snapshot.params?.dataid; if (!this.messageId) { - console.warn('No messageId found in route params:', this.route.snapshot.params); + this.logger.warn('No messageId found in route params.', { routeParams: this.route.snapshot.params }); return; } @@ -85,7 +88,7 @@ export class QueryWriteRespondToQueryComponent implements OnInit, OnChanges { )?.value; if (!matchingMessage) { - console.warn('No matching message found for ID:', this.messageId); + this.logger.warn('No matching message found for ID.', { messageId: this.messageId }); return; } @@ -116,7 +119,7 @@ export class QueryWriteRespondToQueryComponent implements OnInit, OnChanges { public setCaseQueriesCollectionData(): boolean { if (!this.eventData) { - console.warn('Event data not available; skipping collection setup.'); + this.logger.warn('Event data not available; skipping collection setup.'); return false; } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/services/query-management.service.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/services/query-management.service.spec.ts index 953203c4ca..ebd1a8f4c8 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/services/query-management.service.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/services/query-management.service.spec.ts @@ -144,9 +144,13 @@ describe('QueryManagementService', () => { service.setCaseQueriesCollectionData(eventData as any, QueryCreateContext.NEW_QUERY, caseDetails as any); // resolveFieldId logs the jurisdiction-specific error, and setCaseQueriesCollectionData logs a second message. - expect((console.error as jasmine.Spy).calls.argsFor(0)[0]).toBe( - 'Error: Multiple CaseQueriesCollections are not supported yet for the PUBLICLAW jurisdiction' - ); + expect((console.error as jasmine.Spy).calls.argsFor(0)[0]).toEqual(jasmine.objectContaining({ + level: 'error', + message: 'Multiple CaseQueriesCollections are not supported yet for this jurisdiction.', + context: jasmine.objectContaining({ + jurisdictionId: 'PUBLICLAW' + }) + })); expect(service.fieldId).toBeUndefined(); }); }); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/services/query-management.service.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/services/query-management.service.ts index 995585b4a4..49607ba8fe 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/services/query-management.service.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/services/query-management.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { Router } from '@angular/router'; import { CaseField, CaseEventTrigger, CaseView } from '../../../../../../lib/shared/domain'; import { QmCaseQueriesCollection, QueryCreateContext, QueryListItem, CaseQueriesCollection } from '../models'; -import { SessionStorageService } from '../../../../services'; +import { SessionStorageService, StructuredLoggerService } from '../../../../services'; import { isInternalUser, isJudiciaryUser, USER_DETAILS } from '../../../../utils'; import { safeJsonParse } from '../../../../json-utils'; import { QueryManagementUtils } from '../utils/query-management.utils'; @@ -19,6 +19,8 @@ import { FormGroup } from '@angular/forms'; @Injectable({ providedIn: 'root' }) export class QueryManagementService { + private readonly logger = new StructuredLoggerService(); + public caseQueriesCollections: CaseQueriesCollection[]; public fieldId: string; @@ -46,7 +48,7 @@ export class QueryManagementService { try { currentUserDetails = safeJsonParse(this.sessionStorageService.getItem(USER_DETAILS), {}); } catch (e) { - console.error('Could not parse USER_DETAILS from session storage:', e); + this.logger.error('Could not parse USER_DETAILS from session storage.', { error: e }); currentUserDetails = {}; } @@ -59,7 +61,7 @@ export class QueryManagementService { // Check if the field ID has been set dynamically if (!this.fieldId) { - console.error('Error: Field ID for CaseQueriesCollection not found. Cannot proceed with data generation.'); + this.logger.error('Field ID for CaseQueriesCollection not found. Cannot proceed with data generation.'); this.router.navigate(['/', 'service-down']); throw new Error('Field ID for CaseQueriesCollection not found. Aborting query data generation.'); } @@ -135,7 +137,7 @@ export class QueryManagementService { const resolvedFieldId = this.resolveFieldId(eventData, queryCreateContext, caseDetails, messageId); if (!resolvedFieldId) { - console.error('Failed to resolve fieldId for CaseQueriesCollection. Cannot proceed.'); + this.logger.error('Failed to resolve fieldId for CaseQueriesCollection. Cannot proceed.'); return; } @@ -167,7 +169,7 @@ export class QueryManagementService { ); if (!candidateFields?.length) { - console.warn('No editable CaseQueriesCollection fields found.'); + this.logger.warn('No editable CaseQueriesCollection fields found.'); return null; } @@ -199,13 +201,13 @@ export class QueryManagementService { return firstOrdered.id; } } else { - console.error(`Error: Multiple CaseQueriesCollections are not supported yet for the ${jurisdictionId} jurisdiction`); + this.logger.error('Multiple CaseQueriesCollections are not supported yet for this jurisdiction.', { jurisdictionId }); return null; } } // Step 4: Fallback — if none of the above succeeded - console.warn('Could not determine fieldId for context:', queryCreateContext); + this.logger.warn('Could not determine fieldId for context.', { queryCreateContext }); return null; } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/search-filters/search-filters.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/search-filters/search-filters.component.ts index 9c3b3dc1b1..b4fa55e5ac 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/search-filters/search-filters.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/search-filters/search-filters.component.ts @@ -9,6 +9,7 @@ import { CaseTypeLite } from '../../domain/definition/case-type-lite.model'; import { Jurisdiction } from '../../domain/definition/jurisdiction.model'; import { FieldsUtils } from '../../services/fields/fields.utils'; import { JurisdictionService } from '../../services/jurisdiction/jurisdiction.service'; +import { StructuredLoggerService } from '../../services/logging'; import { OrderService } from '../../services/order/order.service'; import { SearchService } from '../../services/search/search.service'; import { WindowService } from '../../services/window/window.service'; @@ -25,6 +26,8 @@ const CASE_TYPE_LOC_STORAGE = 'search-caseType'; }) export class SearchFiltersComponent implements OnInit { + private readonly logger = new StructuredLoggerService(); + public readonly PARAM_JURISDICTION = 'jurisdiction'; public readonly PARAM_CASE_TYPE = 'case-type'; public readonly PARAM_CASE_STATE = 'case-state'; @@ -145,7 +148,7 @@ export class SearchFiltersComponent implements OnInit { return localStorageJurisdiction; } } catch (e) { - console.log("Failed to retrieve jurisdiction from local storage"); + this.logger.error('Failed to retrieve jurisdiction from local storage.', { error: e }); this.windowService.setLocalStorage(JURISDICTION_LOC_STORAGE, null) } } @@ -210,9 +213,7 @@ export class SearchFiltersComponent implements OnInit { } }); this.getCaseFields(); - }, error => { - console.log('Search input fields request will be discarded reason: ', error.message); - }); + }, error => this.logger.error('Search input fields request will be discarded.', { error })); } public isJurisdictionSelected(): boolean { diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/workbasket-filters/workbasket-filters.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/workbasket-filters/workbasket-filters.component.ts index 4e5abc71a4..be89f91737 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/workbasket-filters/workbasket-filters.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/workbasket-filters/workbasket-filters.component.ts @@ -10,6 +10,7 @@ import { WorkbasketInputModel } from '../../domain/workbasket/workbasket-input.m import { AlertService } from '../../services/alert/alert.service'; import { FieldsUtils } from '../../services/fields/fields.utils'; import { JurisdictionService } from '../../services/jurisdiction/jurisdiction.service'; +import { StructuredLoggerService } from '../../services/logging'; import { OrderService } from '../../services/order/order.service'; import { WindowService } from '../../services/window/window.service'; import { WorkbasketInputFilterService } from '../../services/workbasket/workbasket-input-filter.service'; @@ -28,6 +29,8 @@ export class WorkbasketFiltersComponent implements OnInit { public static readonly PARAM_JURISDICTION = 'jurisdiction'; public static readonly PARAM_CASE_TYPE = 'case-type'; public static readonly PARAM_CASE_STATE = 'case-state'; + private readonly logger = new StructuredLoggerService(); + public caseFields: CaseField[]; @Input() @@ -237,9 +240,7 @@ export class WorkbasketFiltersComponent implements OnInit { } }); this.getCaseFields(); - }, error => { - console.log('Workbasket input fields request will be discarded reason: ', error.message); - }); + }, error => this.logger.error('Workbasket input fields request will be discarded.', { error })); } } else { this.resetCaseState(); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/directives/conditional-show/services/condition-parser.service.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/directives/conditional-show/services/condition-parser.service.ts index 81d13cf95c..af9bd4676d 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/directives/conditional-show/services/condition-parser.service.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/directives/conditional-show/services/condition-parser.service.ts @@ -1,9 +1,12 @@ import * as _score from 'underscore'; +import { StructuredLoggerService } from '../../../services/logging'; import { FieldsUtils } from '../../../services/fields/fields.utils'; import { ShowCondition } from '../../conditional-show/domain/conditional-show.model'; import peg from './condition.peg'; export class ConditionParser { + private static readonly logger = new StructuredLoggerService(); + /** * Parse the raw formula and output structured condition data * that can be used in evaluating show/hide logic @@ -186,13 +189,13 @@ export class ConditionParser { return (fields[head][arrayIndex] !== undefined) ? this.findValueForComplexCondition( fields[head][arrayIndex]['value'], tail[0], tail.slice(1), dropNumberPath.join('_')) : null; } catch (e) { - console.error('Error while parsing number', pathTail[0], e); + this.logger.error('Error while parsing form array path index.', { error: e, pathIndex: pathTail[0] }); } } } else { // EXUI-2460 - if path present then show error, otherwise log message to stop unnecessary console errors - path ? console.error('Path in formArray should start with ', head, ', full path: ', path) : - console.log('Path not present in formArray'); + path ? this.logger.error('Path in formArray should start with the expected field.', { expectedHead: head, path }) : + this.logger.info('Path not present in formArray.'); } } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/domain/definition/case-field.model.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/domain/definition/case-field.model.ts index a90e8a89f3..eb489564c8 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/domain/definition/case-field.model.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/domain/definition/case-field.model.ts @@ -2,6 +2,7 @@ import { Exclude, Expose, Type } from 'class-transformer'; import * as _ from 'underscore'; import { WizardPageField } from '../../components/case-editor/domain/wizard-page-field.model'; +import { StructuredLoggerService } from '../../services/logging'; import { Orderable } from '../order'; import { AccessControlList } from './access-control-list.model'; import { FieldTypeEnum } from './field-type-enum.model'; @@ -9,6 +10,8 @@ import { FieldType } from './field-type.model'; // @dynamic export class CaseField implements Orderable { + private static readonly logger = new StructuredLoggerService(); + public id: string; public hidden: boolean; public hiddenCannotChange: boolean; @@ -184,7 +187,7 @@ export class CaseField implements Orderable { return prefix + this.id; } } else { - console.log("Path too long, possible circular reference in case field hierarchy"); + CaseField.logger.error('Path too long, possible circular reference in case field hierarchy.'); return this.id; } } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/json-utils.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/json-utils.ts index ad5bb5d30e..f729460c44 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/json-utils.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/json-utils.ts @@ -1,3 +1,7 @@ +import { StructuredLoggerService } from './services/logging'; + +const logger = new StructuredLoggerService(); + export function safeJsonParse(value: string | null, fallback: T | null = null): T | null { if (!value) { return fallback; @@ -7,8 +11,7 @@ export function safeJsonParse(value: string | null, fallback: T | null = null return JSON.parse(value) as T; } catch (error) { // Log for diagnostics, then return fallback to avoid UI crashes. - // eslint-disable-next-line no-console - console.error('safeJsonParse failed to parse JSON', error); + logger.error('safeJsonParse failed to parse JSON.', { error }); return fallback; } } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/activity/activity.polling.service.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/activity/activity.polling.service.ts index 69a48bd96e..b4f1b99940 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/activity/activity.polling.service.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/activity/activity.polling.service.ts @@ -2,12 +2,15 @@ import { Injectable, NgZone } from '@angular/core'; import { EMPTY, Observable, Subject, Subscription } from 'rxjs'; import { AbstractAppConfig } from '../../../app.config'; import { Activity } from '../../domain/activity/activity.model'; +import { StructuredLoggerService } from '../logging'; import { ActivityService } from './activity.service'; import { polling, IOptions } from 'rx-polling-hmcts'; // @dynamic @Injectable() export class ActivityPollingService { + private readonly logger = new StructuredLoggerService(); + private readonly pendingRequests = new Map>(); private currentTimeoutHandle: any; private pollActivitiesSubscription: Subscription; @@ -46,7 +49,6 @@ export class ActivityPollingService { this.ngZone.runOutsideAngular(() => { this.currentTimeoutHandle = setTimeout( () => this.ngZone.run(() => { - // console.log('timeout: flushing requests') this.flushRequests(); }), this.batchCollectionDelayMs); @@ -54,7 +56,6 @@ export class ActivityPollingService { } if (this.pendingRequests.size >= this.maxRequestsPerBatch) { - // console.log('max pending hit: flushing requests'); this.flushRequests(); } return subject; @@ -95,19 +96,17 @@ export class ActivityPollingService { protected performBatchRequest(requests: Map>): void { const caseIds = Array.from(requests.keys()).join(); - // console.log('issuing batch request for cases: ' + caseIds); this.ngZone.runOutsideAngular( () => { // run polling outside angular zone so it does not trigger change detection this.pollActivitiesSubscription = this.pollActivities(caseIds).subscribe( // process activity inside zone so it triggers change detection for activity.component.ts (activities: Activity[]) => this.ngZone.run( () => { activities.forEach((activity) => { - // console.log('pushing activity: ' + activity.caseId); requests.get(activity.caseId).next(activity); }); }, (err) => { - console.log(`error: ${err}`); + this.logger.error('Error while polling activities.', { error: err }); Array.from(requests.values()).forEach((subject) => subject.error(err)); } ) diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/activity/activity.service.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/activity/activity.service.ts index 702d61e8e8..63f1d7f060 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/activity/activity.service.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/activity/activity.service.ts @@ -7,6 +7,7 @@ import { AbstractAppConfig } from '../../../app.config'; import { Activity } from '../../domain/activity/activity.model'; import { HttpError } from '../../domain/http/http-error.model'; import { HttpErrorService, HttpService, OptionsType } from '../http'; +import { StructuredLoggerService } from '../logging'; import { SessionStorageService } from '../session'; import { USER_DETAILS } from '../../utils'; import { safeJsonParse } from '../../json-utils'; @@ -17,6 +18,8 @@ export class ActivityService { public static get ACTIVITY_VIEW() { return 'view'; } public static get ACTIVITY_EDIT() { return 'edit'; } + private readonly logger = new StructuredLoggerService(); + constructor( private readonly http: HttpService, private readonly appConfig: AbstractAppConfig, @@ -61,7 +64,7 @@ export class ActivityService { map(response => response) ); } catch (error) { - console.log(`user may not be authenticated.${error}`); + this.logUserMayNotBeAuthenticated(error); } } @@ -76,7 +79,7 @@ export class ActivityService { map(response => response) ); } catch (error) { - console.log(`user may not be authenticated.${error}`); + this.logUserMayNotBeAuthenticated(error); } } @@ -94,4 +97,8 @@ export class ActivityService { private activityUrl(): string { return this.appConfig.getActivityUrl(); } + + private logUserMayNotBeAuthenticated(error: unknown): void { + this.logger.error('User may not be authenticated. Activity request was not sent.', { error }); + } } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/fields/fields.utils.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/fields/fields.utils.ts index 2ab42b88a3..471e7e4d83 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/fields/fields.utils.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/fields/fields.utils.ts @@ -11,10 +11,13 @@ import { CaseEventTrigger, CaseField, CaseTab, CaseView, FieldType, FieldTypeEnu import { UserTask } from '../../domain/work-allocation/Task'; import { FormatTranslatorService } from '../case-fields/format-translator.service'; import { safeJsonParse } from '../../json-utils'; +import { StructuredLoggerService } from '../logging'; // @dynamic @Injectable() export class FieldsUtils { + private static readonly logger = new StructuredLoggerService(); + private static readonly caseLevelCaseFlagsFieldId = 'caseFlags'; private static readonly currencyPipe: CurrencyPipe = new CurrencyPipe('en-GB'); private static readonly datePipe: DatePipe = new DatePipe(new FormatTranslatorService()); @@ -333,7 +336,7 @@ export class FieldsUtils { this.setDynamicListDefinition(field, field.field_type, rootCaseField); } } catch (error) { - console.log(error); + FieldsUtils.logger.error('Error setting dynamic list definition.', { error }); } }); } else if (caseFieldType.type === FieldsUtils.SERVER_RESPONSE_FIELD_TYPE_COLLECTION) { diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/http/http-error.service.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/http/http-error.service.ts index 1654013566..f0ca0c3562 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/http/http-error.service.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/http/http-error.service.ts @@ -4,10 +4,12 @@ import { Observable, throwError } from 'rxjs'; import { HttpError } from '../../domain/http/http-error.model'; import { AuthService } from '../auth/auth.service'; import { LoadingService } from '../loading'; +import { StructuredLoggerService } from '../logging'; @Injectable() export class HttpErrorService { + private static readonly logger = new StructuredLoggerService(); constructor(private readonly authService: AuthService, private readonly loadingService: LoadingService @@ -27,7 +29,7 @@ export class HttpErrorService { try { httpError = HttpError.from(error); } catch (e) { - console.error(e, e.message); + HttpErrorService.logger.error('Unable to convert HTTP error response.', { error: e }); } } if (!httpError.status) { @@ -55,8 +57,6 @@ export class HttpErrorService { } public handle(error: HttpErrorResponse | any, redirectIfNotAuthorised = true): Observable { - console.error('Handling error in http error service.'); - console.error(error); if (this.loadingService.hasSharedSpinner()){ this.loadingService.unregisterSharedSpinner(); } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/index.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/index.ts index 738f1ca0c2..523adbd544 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/index.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/index.ts @@ -16,6 +16,7 @@ export * from './http'; export * from './journey'; export * from './jurisdiction'; export * from './banners'; +export * from './logging'; export * from './navigation'; export * from './order'; export * from './profile'; diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/logging/index.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/logging/index.ts new file mode 100644 index 0000000000..e78efd5b54 --- /dev/null +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/logging/index.ts @@ -0,0 +1 @@ +export * from './structured-logger.service'; diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/logging/structured-logger.service.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/logging/structured-logger.service.spec.ts new file mode 100644 index 0000000000..2d17c510ed --- /dev/null +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/logging/structured-logger.service.spec.ts @@ -0,0 +1,108 @@ +import { StructuredLogEntry, StructuredLoggerService } from './structured-logger.service'; + +describe('StructuredLoggerService', () => { + let logger: StructuredLoggerService; + + beforeEach(() => { + logger = new StructuredLoggerService(); + }); + + it('should write structured warning logs with redacted sensitive fields', () => { + const warnSpy = spyOn(console, 'warn'); + + logger.warn('Request failed', { + safeField: 'visible', + authContext: { userId: 'sensitive-user' }, + authorisation: 'sensitive-authorisation', + token: 'secret-token', + nested: { + password: 'secret-password', + clientSecret: 'secret-client' + }, + headers: [ + { name: 'Authorization', value: 'Bearer header-token' }, + { key: 'refreshToken', values: ['refresh-token'] } + ] + }); + + const entry = warnSpy.calls.mostRecent().args[0] as StructuredLogEntry; + const context = entry.context as any; + + expect(entry.level).toBe('warn'); + expect(entry.message).toBe('Request failed'); + expect(entry.timestamp).toEqual(jasmine.any(String)); + expect(context.safeField).toBe('visible'); + expect(context.authContext).toBe('[REDACTED]'); + expect(context.authorisation).toBe('[REDACTED]'); + expect(context.token).toBe('[REDACTED]'); + expect(context.nested.password).toBe('[REDACTED]'); + expect(context.nested.clientSecret).toBe('[REDACTED]'); + expect(context.headers[0].value).toBe('[REDACTED]'); + expect(context.headers[1].values).toBe('[REDACTED]'); + expect(JSON.stringify(entry)).not.toContain('secret-token'); + expect(JSON.stringify(entry)).not.toContain('sensitive-user'); + expect(JSON.stringify(entry)).not.toContain('sensitive-authorisation'); + expect(JSON.stringify(entry)).not.toContain('secret-password'); + expect(JSON.stringify(entry)).not.toContain('secret-client'); + expect(JSON.stringify(entry)).not.toContain('header-token'); + expect(JSON.stringify(entry)).not.toContain('refresh-token'); + }); + + it('should redact sensitive values inside error messages', () => { + const errorSpy = spyOn(console, 'error'); + const error = new Error('Authorization: Bearer secret-token'); + (error as any).token = 'secret-token'; + + logger.error('Activity request failed', { error }); + + const entry = errorSpy.calls.mostRecent().args[0] as StructuredLogEntry; + + expect(JSON.stringify(entry)).toContain('Bearer [REDACTED]'); + expect(JSON.stringify(entry)).not.toContain('secret-token'); + }); + + it('should redact sensitive key value pairs inside strings', () => { + const warnSpy = spyOn(console, 'warn'); + + logger.warn('Request failed', { + safeField: 'caseId=123 password=secret-password auth context: secret-auth api key=secret-api-key' + }); + + const entry = warnSpy.calls.mostRecent().args[0] as StructuredLogEntry; + const context = entry.context as any; + + expect(context.safeField).toContain('caseId=123'); + expect(context.safeField).toContain('password=[REDACTED]'); + expect(context.safeField).toContain('auth context: [REDACTED]'); + expect(context.safeField).toContain('api key=[REDACTED]'); + expect(JSON.stringify(entry)).not.toContain('secret-password'); + expect(JSON.stringify(entry)).not.toContain('secret-auth'); + expect(JSON.stringify(entry)).not.toContain('secret-api-key'); + }); + + it('should redact standalone bearer tokens in string values', () => { + const warnSpy = spyOn(console, 'warn'); + + logger.warn('Request failed', { + safeField: 'Bearer abc.DEF_123-456~+/=' + }); + + const entry = warnSpy.calls.mostRecent().args[0] as StructuredLogEntry; + const context = entry.context as any; + + expect(context.safeField).toBe('Bearer [REDACTED]'); + expect(JSON.stringify(entry)).not.toContain('abc.DEF_123-456'); + }); + + it('should handle circular context safely', () => { + const warnSpy = spyOn(console, 'warn'); + const context: any = { safeField: 'visible' }; + context.self = context; + + logger.warn('Circular context', context); + + const entry = warnSpy.calls.mostRecent().args[0] as StructuredLogEntry; + + expect((entry.context as any).self).toBe('[Circular]'); + }); +}); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/logging/structured-logger.service.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/logging/structured-logger.service.ts new file mode 100644 index 0000000000..2286ca7dcd --- /dev/null +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/logging/structured-logger.service.ts @@ -0,0 +1,170 @@ +import { Injectable } from '@angular/core'; + +export type StructuredLogLevel = 'error' | 'warn' | 'info' | 'debug'; + +export interface StructuredLogEntry { + level: StructuredLogLevel; + message: string; + timestamp: string; + context?: unknown; +} + +@Injectable({ + providedIn: 'root' +}) +export class StructuredLoggerService { + private static readonly CIRCULAR_VALUE = '[Circular]'; + private static readonly MAX_DEPTH_VALUE = '[MaxDepth]'; + private static readonly MAX_REDACTION_DEPTH = 10; + private static readonly REDACTED_VALUE = '[REDACTED]'; + private static readonly KEY_VALUE_PATTERN = /\b([a-z][\w-]*(?:[ _-][a-z][\w-]*)?)([ \t]*[:=][ \t]*)((?:Bearer[ \t]+)?)([^,;&\s]+)/gi; + private static readonly SENSITIVE_KEY_PATTERN = /(password|passcode|pwd|secret|token|authori[sz]ation|authentication|auth[-_ ]?context|^auth$|api[-_ ]?key|cookie|session|credential)/i; + private static readonly BEARER_TOKEN_PATTERN = /\bBearer\s+([\w.~+/-]+=*)/gi; + + public debug(message: string, context?: unknown): void { + this.write('debug', message, context); + } + + public error(message: string, context?: unknown): void { + this.write('error', message, context); + } + + public info(message: string, context?: unknown): void { + this.write('info', message, context); + } + + public warn(message: string, context?: unknown): void { + this.write('warn', message, context); + } + + public redact(value: unknown): unknown { + return this.redactValue(value, new WeakSet(), false, 0); + } + + private write(level: StructuredLogLevel, message: string, context?: unknown): void { + const entry: StructuredLogEntry = { + level, + message, + timestamp: new Date().toISOString() + }; + + if (context !== undefined) { + entry.context = this.redact(context); + } + + switch (level) { + case 'error': + console.error(entry); + break; + case 'warn': + console.warn(entry); + break; + default: + console.log(entry); + } + } + + private redactValue(value: unknown, seen: WeakSet, redactCurrentValue: boolean, depth: number): unknown { + if (redactCurrentValue) { + return StructuredLoggerService.REDACTED_VALUE; + } + + if (value === null || value === undefined) { + return value; + } + + if (typeof value === 'string') { + return this.redactSensitiveString(value); + } + + if (typeof value !== 'object') { + return value; + } + + if (seen.has(value)) { + return StructuredLoggerService.CIRCULAR_VALUE; + } + + if (depth >= StructuredLoggerService.MAX_REDACTION_DEPTH) { + return StructuredLoggerService.MAX_DEPTH_VALUE; + } + + seen.add(value); + + let redactedValue: unknown; + if (value instanceof Date) { + redactedValue = value.toISOString(); + } else if (value instanceof Error) { + redactedValue = this.redactError(value, seen, depth); + } else if (Array.isArray(value)) { + redactedValue = value.map(item => this.redactValue(item, seen, false, depth + 1)); + } else { + redactedValue = this.redactObject(value as Record, seen, depth); + } + + seen.delete(value); + return redactedValue; + } + + private redactError(error: Error, seen: WeakSet, depth: number): Record { + const redactedError: Record = { + name: this.redactValue(error.name, seen, false, depth + 1), + message: this.redactValue(error.message, seen, false, depth + 1) + }; + + if (error.stack) { + redactedError.stack = this.redactValue(error.stack, seen, false, depth + 1); + } + + const errorContext = error as unknown as Record; + Object.keys(errorContext).forEach(key => { + redactedError[key] = this.redactValue(errorContext[key], seen, this.isSensitiveKey(key), depth + 1); + }); + + return redactedError; + } + + private redactObject(value: Record, seen: WeakSet, depth: number): Record { + const redactedValue: Record = {}; + const hasSensitiveNamedValue = this.hasSensitiveNamedValue(value); + + Object.keys(value).forEach(key => { + redactedValue[key] = this.redactValue( + value[key], + seen, + this.isSensitiveKey(key) || (hasSensitiveNamedValue && this.isValueKey(key)), + depth + 1 + ); + }); + + return redactedValue; + } + + private redactSensitiveString(value: string): string { + return value + .replace(StructuredLoggerService.KEY_VALUE_PATTERN, (match: string, key: string, separator: string, bearerPrefix: string) => { + return this.isSensitiveKey(key) ? `${key}${separator}${bearerPrefix}${StructuredLoggerService.REDACTED_VALUE}` : match; + }) + .replace(StructuredLoggerService.BEARER_TOKEN_PATTERN, 'Bearer [REDACTED]'); + } + + private hasSensitiveNamedValue(value: Record): boolean { + return Object.keys(value) + .some(key => { + const namedValue = value[key]; + return this.isNameKey(key) && typeof namedValue === 'string' && this.isSensitiveKey(namedValue); + }); + } + + private isNameKey(key: string): boolean { + return ['key', 'name'].includes(key.toLowerCase()); + } + + private isSensitiveKey(key: string): boolean { + return StructuredLoggerService.SENSITIVE_KEY_PATTERN.test(key); + } + + private isValueKey(key: string): boolean { + return ['value', 'values'].includes(key.toLowerCase()); + } +} diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/organisation/organisation.service.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/organisation/organisation.service.ts index eb52ae926b..0a40ca705f 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/organisation/organisation.service.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/organisation/organisation.service.ts @@ -3,6 +3,7 @@ import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { catchError, map, publishReplay, refCount, take } from 'rxjs/operators'; import { AbstractAppConfig } from '../../../app.config'; +import { StructuredLoggerService } from '../logging'; export interface OrganisationSuperUser { firstName: string; @@ -48,6 +49,7 @@ export interface OrganisationVm { @Injectable() export class OrganisationService { + private readonly logger = new StructuredLoggerService(); constructor(private readonly http: HttpClient, private readonly appconfig: AbstractAppConfig) {} @@ -82,7 +84,7 @@ export class OrganisationService { this.organisations$ = this.http.get(url) .pipe(map((orgs) => OrganisationService.mapOrganisation(orgs)), publishReplay(1, cacheTimeOut), refCount(), take(1), catchError(e => { - console.log(e); + this.logger.error('Error while retrieving active organisations.', { error: e }); // Handle error and return blank Observable array return of([]); })); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/session/session-storage.guard.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/session/session-storage.guard.ts index ed7bea3358..ce4b45e3c2 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/session/session-storage.guard.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/session/session-storage.guard.ts @@ -1,6 +1,7 @@ import { Inject, Injectable, Optional, InjectionToken } from '@angular/core'; import { CanActivate, Router } from '@angular/router'; import { safeJsonParse } from '../../json-utils'; +import { StructuredLoggerService } from '../logging'; import { SessionStorageService } from './session-storage.service'; export type SessionJsonErrorLogger = (error: unknown) => void; @@ -12,6 +13,8 @@ export const SessionJsonErrorLogger = new InjectionToken providedIn: 'root', }) export class SessionStorageGuard implements CanActivate { + private readonly logger = new StructuredLoggerService(); + constructor( private readonly sessionStorageService: SessionStorageService, private readonly router: Router, @@ -34,8 +37,7 @@ export class SessionStorageGuard implements CanActivate { if (this.errorLogger) { this.errorLogger(error); } else { - // eslint-disable-next-line no-console - console.error('Invalid userDetails in session storage', error); + this.logger.error('Invalid userDetails in session storage.', { error }); } this.router.navigate([this.errorRoute || '/session-error']); return false; diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/utils/retry/retry.service.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/utils/retry/retry.service.ts index fb18684b04..cb4289d774 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/utils/retry/retry.service.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/utils/retry/retry.service.ts @@ -1,6 +1,7 @@ import { Injectable } from '@angular/core'; import { Observable, throwError, timer } from 'rxjs'; import { delayWhen, finalize, mergeMap, retryWhen, tap, timeout } from 'rxjs/operators'; +import { StructuredLoggerService } from '../../logging'; class ArtificialDelayContext { private artificialDelayOn = true; @@ -35,14 +36,19 @@ class ArtificialDelayContext { @Injectable() export class RetryUtil { + private readonly logger = new StructuredLoggerService(); + public pipeTimeoutMechanismOn(in$: Observable, preferredArtificialDelay: number, timeoutPeriods: number[]): Observable { const artificialDelayContext = new ArtificialDelayContext(preferredArtificialDelay); - console.info(`Piping a retry mechanism with timeouts {${timeoutPeriods}}.`); - console.info(`Artificial delay will be applied: ${artificialDelayContext.shouldApplyArtificialDelay()}.`); + this.logger.info('Piping a retry mechanism with timeouts.', { timeoutPeriods }); + this.logger.info('Artificial delay setting resolved.', { artificialDelayApplied: artificialDelayContext.shouldApplyArtificialDelay() }); let out$ = in$; if (artificialDelayContext.shouldApplyArtificialDelay()) { - console.info(`Preferred artificial delay: ${preferredArtificialDelay} seconds. Actual delay selected: ${artificialDelayContext.getActualDelay()}`); + this.logger.info('Preferred artificial delay selected.', { + actualDelaySeconds: artificialDelayContext.getActualDelay(), + preferredDelaySeconds: preferredArtificialDelay + }); out$ = this.pipeArtificialDelayOn(out$, artificialDelayContext); } out$ = this.pipeTimeOutControlOn(out$, timeoutPeriods); @@ -52,7 +58,7 @@ export class RetryUtil { private pipeTimeOutControlOn(in$: Observable, timeoutPeriods: number[]): Observable { const timeOutAfterSeconds = timeoutPeriods[0]; - console.info(`Piping timeout control with ${timeOutAfterSeconds} seconds.`); + this.logger.info('Piping timeout control.', { timeoutSeconds: timeOutAfterSeconds }); const out$ = in$.pipe(timeout(timeOutAfterSeconds * 1000)); return out$; } @@ -61,19 +67,18 @@ export class RetryUtil { const retryStrategy = (errors) => { return errors.pipe( mergeMap((error: Error, i) => { - console.error(`Mapping error ${error?.name}, ${i}`); - console.error(error); + this.logger.error('Mapping retry error.', { error, errorName: error?.name, attempt: i }); if (error?.name === 'TimeoutError' && i === 0) { artificialDelayContext.turnOffArtificialDelays(); - console.info('Will retry, after a timeout error.'); + this.logger.info('Will retry after a timeout error.'); } else { - console.error('Will NOT retry.'); + this.logger.error('Will not retry request after error.', { error, errorName: error?.name, attempt: i }); throw error; } return timer(0); }), - finalize(() => console.log('We are done!'))); + finalize(() => undefined)); }; const out$ = in$.pipe(retryWhen(retryStrategy)); return out$; @@ -81,11 +86,11 @@ export class RetryUtil { private pipeArtificialDelayOn(in$: Observable, artificialDelayContext: ArtificialDelayContext): Observable { let out$ = in$.pipe(tap(() => { - console.log(`Artificially delaying for ${artificialDelayContext.getActualDelay()} seconds..`); + this.logger.info('Artificial delay started.', { delaySeconds: artificialDelayContext.getActualDelay() }); })); out$ = out$.pipe(delayWhen(() => timer(artificialDelayContext.getActualDelay() * 1000))); out$ = out$.pipe(tap(() => { - console.log(`Artificially delayed for ${artificialDelayContext.getActualDelay()} seconds..`); + this.logger.info('Artificial delay completed.', { delaySeconds: artificialDelayContext.getActualDelay() }); })); return out$; }