fix(aio): constrain error logging to improve reporting (#22713)

The `Logger.error()` method now only accepts a single `Error` parameter
and passes this through to the error handler.
This allows the error handler to serialize the error more accurately.

The various places that use `Logger.error()` have been updated.

See #21943#issuecomment-370230047

PR Close #22713
This commit is contained in:
Pete Bacon Darwin 2018-03-12 11:05:36 +00:00 committed by Kara Erickson
parent 1f9734315d
commit 049757b237
11 changed files with 48 additions and 23 deletions

View File

@ -69,15 +69,20 @@ describe('DocumentService', () => {
it('should emit the not-found document if the document is not found on the server', () => { it('should emit the not-found document if the document is not found on the server', () => {
let currentDocument: DocumentContents|undefined; let currentDocument: DocumentContents|undefined;
const notFoundDoc = { id: FILE_NOT_FOUND_ID, contents: '<h1>Page Not Found</h1>' }; const notFoundDoc = { id: FILE_NOT_FOUND_ID, contents: '<h1>Page Not Found</h1>' };
const { docService } = getServices('missing/doc'); const { docService, logger } = getServices('missing/doc');
docService.currentDocument.subscribe(doc => currentDocument = doc); docService.currentDocument.subscribe(doc => currentDocument = doc);
// Initial request return 404. // Initial request return 404.
httpMock.expectOne({}).flush(null, {status: 404, statusText: 'NOT FOUND'}); httpMock.expectOne({}).flush(null, {status: 404, statusText: 'NOT FOUND'});
expect(logger.output.error).toEqual([
[jasmine.any(Error)]
]);
expect(logger.output.error[0][0].message).toEqual(`Document file not found at 'missing/doc'`);
// Subsequent request for not-found document. // Subsequent request for not-found document.
logger.output.error = [];
httpMock.expectOne(CONTENT_URL_PREFIX + 'file-not-found.json').flush(notFoundDoc); httpMock.expectOne(CONTENT_URL_PREFIX + 'file-not-found.json').flush(notFoundDoc);
expect(logger.output.error).toEqual([]); // does not report repeate errors
expect(currentDocument).toEqual(notFoundDoc); expect(currentDocument).toEqual(notFoundDoc);
}); });
@ -102,12 +107,17 @@ describe('DocumentService', () => {
let latestDocument: DocumentContents|undefined; let latestDocument: DocumentContents|undefined;
const doc1 = { contents: 'doc 1' }; const doc1 = { contents: 'doc 1' };
const doc2 = { contents: 'doc 2' }; const doc2 = { contents: 'doc 2' };
const { docService, locationService } = getServices('initial/doc'); const { docService, locationService, logger } = getServices('initial/doc');
docService.currentDocument.subscribe(doc => latestDocument = doc); docService.currentDocument.subscribe(doc => latestDocument = doc);
httpMock.expectOne({}).flush(null, {status: 500, statusText: 'Server Error'}); httpMock.expectOne({}).flush(null, {status: 500, statusText: 'Server Error'});
expect(latestDocument!.id).toEqual(FETCHING_ERROR_ID); expect(latestDocument!.id).toEqual(FETCHING_ERROR_ID);
expect(logger.output.error).toEqual([
[jasmine.any(Error)]
]);
expect(logger.output.error[0][0].message)
.toEqual(`Error fetching document 'initial/doc': (Http failure response for generated/docs/initial/doc.json: 500 Server Error)`);
locationService.go('new/doc'); locationService.go('new/doc');
httpMock.expectOne({}).flush(doc1); httpMock.expectOne({}).flush(doc1);

View File

@ -78,7 +78,7 @@ export class DocumentService {
private getFileNotFoundDoc(id: string): Observable<DocumentContents> { private getFileNotFoundDoc(id: string): Observable<DocumentContents> {
if (id !== FILE_NOT_FOUND_ID) { if (id !== FILE_NOT_FOUND_ID) {
this.logger.error(`Document file not found at '${id}'`); this.logger.error(new Error(`Document file not found at '${id}'`));
// using `getDocument` means that we can fetch the 404 doc contents from the server and cache it // using `getDocument` means that we can fetch the 404 doc contents from the server and cache it
return this.getDocument(FILE_NOT_FOUND_ID); return this.getDocument(FILE_NOT_FOUND_ID);
} else { } else {
@ -90,7 +90,7 @@ export class DocumentService {
} }
private getErrorDoc(id: string, error: HttpErrorResponse): Observable<DocumentContents> { private getErrorDoc(id: string, error: HttpErrorResponse): Observable<DocumentContents> {
this.logger.error('Error fetching document', error); this.logger.error(new Error(`Error fetching document '${id}': (${error.message})`));
this.cache.delete(id); this.cache.delete(id);
return Observable.of({ return Observable.of({
id: FETCHING_ERROR_ID, id: FETCHING_ERROR_ID,

View File

@ -66,7 +66,10 @@ describe('AnnouncementBarComponent', () => {
const request = httpMock.expectOne('generated/announcements.json'); const request = httpMock.expectOne('generated/announcements.json');
request.flush('some random response'); request.flush('some random response');
expect(component.announcement).toBeUndefined(); expect(component.announcement).toBeUndefined();
expect(mockLogger.output.error[0][0]).toContain('generated/announcements.json contains invalid data:'); expect(mockLogger.output.error).toEqual([
[jasmine.any(Error)]
]);
expect(mockLogger.output.error[0][0].message).toMatch(/^generated\/announcements\.json contains invalid data:/);
}); });
it('should handle a failed request for `announcements.json`', () => { it('should handle a failed request for `announcements.json`', () => {
@ -74,7 +77,10 @@ describe('AnnouncementBarComponent', () => {
const request = httpMock.expectOne('generated/announcements.json'); const request = httpMock.expectOne('generated/announcements.json');
request.error(new ErrorEvent('404')); request.error(new ErrorEvent('404'));
expect(component.announcement).toBeUndefined(); expect(component.announcement).toBeUndefined();
expect(mockLogger.output.error[0][0]).toContain('generated/announcements.json request failed:'); expect(mockLogger.output.error).toEqual([
[jasmine.any(Error)]
]);
expect(mockLogger.output.error[0][0].message).toMatch(/^generated\/announcements\.json request failed:/);
}); });
}); });

View File

@ -59,12 +59,12 @@ export class AnnouncementBarComponent implements OnInit {
ngOnInit() { ngOnInit() {
this.http.get<Announcement[]>(announcementsPath) this.http.get<Announcement[]>(announcementsPath)
.catch(error => { .catch(error => {
this.logger.error(`${announcementsPath} request failed: ${error.message}`); this.logger.error(new Error(`${announcementsPath} request failed: ${error.message}`));
return []; return [];
}) })
.map(announcements => this.findCurrentAnnouncement(announcements)) .map(announcements => this.findCurrentAnnouncement(announcements))
.catch(error => { .catch(error => {
this.logger.error(`${announcementsPath} contains invalid data: ${error.message}`); this.logger.error(new Error(`${announcementsPath} contains invalid data: ${error.message}`));
return []; return [];
}) })
.subscribe(announcement => this.announcement = announcement); .subscribe(announcement => this.announcement = announcement);

View File

@ -254,10 +254,14 @@ describe('CodeComponent', () => {
it('should display an error when copy fails', () => { it('should display an error when copy fails', () => {
const snackBar: MatSnackBar = TestBed.get(MatSnackBar); const snackBar: MatSnackBar = TestBed.get(MatSnackBar);
const copierService: CopierService = TestBed.get(CopierService); const copierService: CopierService = TestBed.get(CopierService);
const logger: TestLogger = TestBed.get(Logger);
spyOn(snackBar, 'open'); spyOn(snackBar, 'open');
spyOn(copierService, 'copyText').and.returnValue(false); spyOn(copierService, 'copyText').and.returnValue(false);
getButton().click(); getButton().click();
expect(snackBar.open).toHaveBeenCalledWith('Copy failed. Please try again!', '', { duration: 800 }); expect(snackBar.open).toHaveBeenCalledWith('Copy failed. Please try again!', '', { duration: 800 });
expect(logger.error).toHaveBeenCalledTimes(1);
expect(logger.error).toHaveBeenCalledWith(jasmine.any(Error));
expect(logger.error.calls.mostRecent().args[0].message).toMatch(/^ERROR copying code to clipboard:/);
}); });
}); });
}); });

View File

@ -148,7 +148,7 @@ export class CodeComponent implements OnChanges {
duration: 800, duration: 800,
}); });
} else { } else {
this.logger.error('ERROR copying code to clipboard:', code); this.logger.error(new Error(`ERROR copying code to clipboard: "${code}"`));
// failure snackbar alert // failure snackbar alert
this.snackbar.open('Copy failed. Please try again!', '', { this.snackbar.open('Copy failed. Please try again!', '', {
duration: 800, duration: 800,

View File

@ -33,8 +33,8 @@ export class PrettyPrinter {
.then( .then(
() => (window as any)['prettyPrintOne'], () => (window as any)['prettyPrintOne'],
err => { err => {
const msg = 'Cannot get prettify.js from server'; const msg = `Cannot get prettify.js from server: ${err.message}`;
this.logger.error(msg, err); this.logger.error(new Error(msg));
// return a pretty print fn that always fails. // return a pretty print fn that always fails.
return () => { throw new Error(msg); }; return () => { throw new Error(msg); };
}); });

View File

@ -577,8 +577,9 @@ describe('DocViewerComponent', () => {
expect(swapViewsSpy).not.toHaveBeenCalled(); expect(swapViewsSpy).not.toHaveBeenCalled();
expect(docViewer.nextViewContainer.innerHTML).toBe(''); expect(docViewer.nextViewContainer.innerHTML).toBe('');
expect(logger.output.error).toEqual([ expect(logger.output.error).toEqual([
[`[DocViewer] Error preparing document 'foo': ${error.stack}`], [jasmine.any(Error)]
]); ]);
expect(logger.output.error[0][0].message).toEqual(`[DocViewer] Error preparing document 'foo': ${error.stack}`);
expect(TestBed.get(Meta).addTag).toHaveBeenCalledWith({ name: 'googlebot', content: 'noindex' }); expect(TestBed.get(Meta).addTag).toHaveBeenCalledWith({ name: 'googlebot', content: 'noindex' });
expect(TestBed.get(Meta).addTag).toHaveBeenCalledWith({ name: 'robots', content: 'noindex' }); expect(TestBed.get(Meta).addTag).toHaveBeenCalledWith({ name: 'robots', content: 'noindex' });
}); });
@ -598,8 +599,9 @@ describe('DocViewerComponent', () => {
expect(swapViewsSpy).not.toHaveBeenCalled(); expect(swapViewsSpy).not.toHaveBeenCalled();
expect(docViewer.nextViewContainer.innerHTML).toBe(''); expect(docViewer.nextViewContainer.innerHTML).toBe('');
expect(logger.output.error).toEqual([ expect(logger.output.error).toEqual([
[`[DocViewer] Error preparing document 'bar': ${error.stack}`], [jasmine.any(Error)]
]); ]);
expect(logger.output.error[0][0].message).toEqual(`[DocViewer] Error preparing document 'bar': ${error.stack}`);
expect(TestBed.get(Meta).addTag).toHaveBeenCalledWith({ name: 'googlebot', content: 'noindex' }); expect(TestBed.get(Meta).addTag).toHaveBeenCalledWith({ name: 'googlebot', content: 'noindex' });
expect(TestBed.get(Meta).addTag).toHaveBeenCalledWith({ name: 'robots', content: 'noindex' }); expect(TestBed.get(Meta).addTag).toHaveBeenCalledWith({ name: 'robots', content: 'noindex' });
}); });
@ -619,8 +621,9 @@ describe('DocViewerComponent', () => {
expect(swapViewsSpy).not.toHaveBeenCalled(); expect(swapViewsSpy).not.toHaveBeenCalled();
expect(docViewer.nextViewContainer.innerHTML).toBe(''); expect(docViewer.nextViewContainer.innerHTML).toBe('');
expect(logger.output.error).toEqual([ expect(logger.output.error).toEqual([
[`[DocViewer] Error preparing document 'baz': ${error.stack}`], [jasmine.any(Error)]
]); ]);
expect(logger.output.error[0][0].message).toEqual(`[DocViewer] Error preparing document 'baz': ${error.stack}`);
expect(TestBed.get(Meta).addTag).toHaveBeenCalledWith({ name: 'googlebot', content: 'noindex' }); expect(TestBed.get(Meta).addTag).toHaveBeenCalledWith({ name: 'googlebot', content: 'noindex' });
expect(TestBed.get(Meta).addTag).toHaveBeenCalledWith({ name: 'robots', content: 'noindex' }); expect(TestBed.get(Meta).addTag).toHaveBeenCalledWith({ name: 'robots', content: 'noindex' });
}); });
@ -640,8 +643,9 @@ describe('DocViewerComponent', () => {
expect(swapViewsSpy).toHaveBeenCalledTimes(1); expect(swapViewsSpy).toHaveBeenCalledTimes(1);
expect(docViewer.nextViewContainer.innerHTML).toBe(''); expect(docViewer.nextViewContainer.innerHTML).toBe('');
expect(logger.output.error).toEqual([ expect(logger.output.error).toEqual([
[`[DocViewer] Error preparing document 'qux': ${error.stack}`], [jasmine.any(Error)]
]); ]);
expect(logger.output.error[0][0].message).toEqual(`[DocViewer] Error preparing document 'qux': ${error.stack}`);
expect(TestBed.get(Meta).addTag).toHaveBeenCalledWith({ name: 'googlebot', content: 'noindex' }); expect(TestBed.get(Meta).addTag).toHaveBeenCalledWith({ name: 'googlebot', content: 'noindex' });
expect(TestBed.get(Meta).addTag).toHaveBeenCalledWith({ name: 'robots', content: 'noindex' }); expect(TestBed.get(Meta).addTag).toHaveBeenCalledWith({ name: 'robots', content: 'noindex' });
}); });
@ -658,8 +662,9 @@ describe('DocViewerComponent', () => {
expect(swapViewsSpy).toHaveBeenCalledTimes(1); expect(swapViewsSpy).toHaveBeenCalledTimes(1);
expect(docViewer.nextViewContainer.innerHTML).toBe(''); expect(docViewer.nextViewContainer.innerHTML).toBe('');
expect(logger.output.error).toEqual([ expect(logger.output.error).toEqual([
[`[DocViewer] Error preparing document 'qux': ${error}`], [jasmine.any(Error)]
]); ]);
expect(logger.output.error[0][0].message).toEqual(`[DocViewer] Error preparing document 'qux': ${error}`);
expect(TestBed.get(Meta).addTag).toHaveBeenCalledWith({ name: 'googlebot', content: 'noindex' }); expect(TestBed.get(Meta).addTag).toHaveBeenCalledWith({ name: 'googlebot', content: 'noindex' });
expect(TestBed.get(Meta).addTag).toHaveBeenCalledWith({ name: 'robots', content: 'noindex' }); expect(TestBed.get(Meta).addTag).toHaveBeenCalledWith({ name: 'robots', content: 'noindex' });
}); });

View File

@ -162,7 +162,7 @@ export class DocViewerComponent implements DoCheck, OnDestroy {
.do(() => this.docRendered.emit()) .do(() => this.docRendered.emit())
.catch(err => { .catch(err => {
const errorMessage = (err instanceof Error) ? err.stack : err; const errorMessage = (err instanceof Error) ? err.stack : err;
this.logger.error(`[DocViewer] Error preparing document '${doc.id}': ${errorMessage}`); this.logger.error(new Error(`[DocViewer] Error preparing document '${doc.id}': ${errorMessage}`));
this.nextViewContainer.innerHTML = ''; this.nextViewContainer.innerHTML = '';
this.setNoIndex(true); this.setNoIndex(true);
return this.void$; return this.void$;

View File

@ -34,8 +34,9 @@ describe('logger service', () => {
describe('error', () => { describe('error', () => {
it('should delegate to ErrorHandler', () => { it('should delegate to ErrorHandler', () => {
logger.error('param1', 'param2', 'param3'); const err = new Error('some error message');
expect(errorHandler.handleError).toHaveBeenCalledWith('param1 param2 param3'); logger.error(err);
expect(errorHandler.handleError).toHaveBeenCalledWith(err);
}); });
}); });
}); });

View File

@ -13,9 +13,8 @@ export class Logger {
} }
} }
error(value: any, ...rest: any[]) { error(error: Error) {
const message = [value, ...rest].join(' '); this.errorHandler.handleError(error);
this.errorHandler.handleError(message);
} }
warn(value: any, ...rest: any[]) { warn(value: any, ...rest: any[]) {