Skip to content

Commit 1bb74e1

Browse files
committed
feat(core): handle and log errors
create main-error component to display error
1 parent e04326c commit 1bb74e1

14 files changed

+242
-18
lines changed

angular.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
],
5050
"styles": [
5151
{
52-
"input": "node_modules/@angular/material/prebuilt-themes/purple-green.css"
52+
"input": "node_modules/@angular/material/prebuilt-themes/pink-bluegrey.css"
5353
},
5454
"src/styles.scss"
5555
],

src/app/core/core.module.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
import { HTTP_INTERCEPTORS } from '@angular/common/http';
2-
import { NgModule } from '@angular/core';
2+
import { ErrorHandler, NgModule } from '@angular/core';
33

44
import { AuthInterceptor } from './services/auth.interceptor';
5+
import { ErrorsHandler } from './services/errors-handler.service';
6+
import { ErrorsLogger } from './services/errors-logger.service';
57

68
const authInterceptor = {
79
multi: true,
810
provide: HTTP_INTERCEPTORS,
911
useClass: AuthInterceptor,
1012
};
1113

12-
const providers = [authInterceptor];
14+
const errorsHandler = {
15+
provide: ErrorHandler,
16+
useClass: ErrorsHandler,
17+
};
18+
19+
const providers = [authInterceptor, errorsHandler, ErrorsLogger];
1320

1421
@NgModule({
1522
providers,

src/app/core/services/auth.interceptor.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
2-
import { Injectable } from '@angular/core';
3-
import { Observable } from 'rxjs';
4-
import { delay, retryWhen, scan } from 'rxjs/operators';
2+
import { ErrorHandler, Injectable } from '@angular/core';
3+
import { Observable, of } from 'rxjs';
4+
import { catchError, delay, retryWhen, scan, tap } from 'rxjs/operators';
55

66
@Injectable()
77
export class AuthInterceptor implements HttpInterceptor {
8+
constructor(private readonly errorHandler: ErrorHandler) {}
9+
810
intercept(request: HttpRequest<{}>, next: HttpHandler): Observable<HttpEvent<{}>> {
911
const newRequest = request.clone({
1012
setHeaders: {
@@ -15,13 +17,23 @@ export class AuthInterceptor implements HttpInterceptor {
1517
return next.handle(newRequest).pipe(
1618
retryWhen(err =>
1719
err.pipe(
20+
tap(error => {
21+
if (error.status !== 503) {
22+
throw error;
23+
}
24+
}),
1825
scan(retryCount => {
19-
if (retryCount < 2) {
26+
if (retryCount < 5) {
2027
return retryCount + 1;
2128
}
2229
throw new Error('Retry limit exceeded!');
2330
}, 0), // tslint:disable-line:align
2431
delay(2000),
32+
catchError(error => {
33+
this.errorHandler.handleError(error);
34+
35+
return of(error);
36+
}),
2537
),
2638
),
2739
);
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { HttpErrorResponse } from '@angular/common/http';
2+
import { ErrorHandler, Injectable, Injector, NgZone } from '@angular/core';
3+
import { Router } from '@angular/router';
4+
import { tap } from 'rxjs/operators';
5+
6+
import { ErrorsLogger } from './errors-logger.service';
7+
8+
@Injectable()
9+
export class ErrorsHandler implements ErrorHandler {
10+
// Because the ErrorsHandler is created before the providers,
11+
// We’ll have to use the Injector to get them.
12+
constructor(private readonly injector: Injector) {}
13+
14+
handleError(error: Error | HttpErrorResponse): void {
15+
if (error instanceof HttpErrorResponse) {
16+
this.handleHttpError(error);
17+
} else {
18+
this.logError(error);
19+
}
20+
}
21+
22+
private handleHttpError(error: HttpErrorResponse): void {
23+
if (!navigator.onLine) {
24+
console.warn('No Internet Connection');
25+
26+
return;
27+
}
28+
29+
if (error.status === 401) {
30+
localStorage.removeItem('auth');
31+
const router = this.injector.get(Router);
32+
const returnUrl = window.location.pathname + window.location.search;
33+
router.navigate(['/account/login'], { queryParams: { returnUrl } });
34+
} else if (error.status === 403 || error.status >= 500) {
35+
this.logError(error);
36+
}
37+
}
38+
39+
private logError(error: Error): void {
40+
const zone = this.injector.get(NgZone);
41+
zone.run(() => {
42+
const logger = this.injector.get(ErrorsLogger);
43+
logger
44+
.logError(error)
45+
.pipe(
46+
tap(_ => {
47+
setTimeout(() => document.body.click(), 0);
48+
const router = this.injector.get(Router);
49+
router.navigateByUrl('/error');
50+
}),
51+
)
52+
.toPromise();
53+
});
54+
}
55+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { LocationStrategy, PathLocationStrategy } from '@angular/common';
2+
import { HttpErrorResponse } from '@angular/common/http';
3+
import { Injectable } from '@angular/core';
4+
import { Observable, of } from 'rxjs';
5+
6+
import { environment } from '~/env/environment';
7+
8+
export interface ErrorWithContext extends Error {
9+
appId: string;
10+
id: string;
11+
status: number | null;
12+
time: number;
13+
url: string;
14+
user: string;
15+
}
16+
17+
@Injectable()
18+
export class ErrorsLogger {
19+
constructor(private readonly locationStrategy: LocationStrategy) {}
20+
21+
logError(error: Error): Observable<Error> {
22+
if (!environment.production) {
23+
console.error(error);
24+
}
25+
26+
// TODO: Send error to server.
27+
return of(this.addContextInfo(error));
28+
}
29+
30+
private addContextInfo(error: Error): ErrorWithContext {
31+
const name = error.name || null;
32+
const appId = environment.clientId;
33+
const user = '-';
34+
const time = new Date().getTime();
35+
const id = `${appId}-${user}-${time}`;
36+
const location = this.locationStrategy;
37+
const url = location instanceof PathLocationStrategy ? location.path() : '';
38+
const status = (error as HttpErrorResponse).status || null;
39+
const message = error.message || error.toString();
40+
const stack = error.stack;
41+
const errorWithContext = { name, appId, user, time, id, url, status, message, stack };
42+
43+
return errorWithContext;
44+
}
45+
}

src/app/main/main-routing.module.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import { NgModule } from '@angular/core';
22
import { RouterModule, Routes } from '@angular/router';
33

4+
import { MainErrorComponent } from './pages';
5+
46
const routes: Routes = [
57
{ path: '', pathMatch: 'full', redirectTo: 'general' },
8+
{ path: 'error', component: MainErrorComponent },
69
{ path: 'general', loadChildren: '../features/general/general.module#GeneralModule' },
7-
{ path: '**', redirectTo: 'general' },
10+
{ path: '**', redirectTo: 'error' },
811
];
912

1013
@NgModule({

src/app/main/main.module.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
1-
import { LayoutModule } from '@angular/cdk/layout';
2-
import { HttpClientModule } from '@angular/common/http';
1+
import { CommonModule } from '@angular/common';
32
import { NgModule } from '@angular/core';
43
import { MatBadgeModule, MatIconModule, MatListModule, MatMenuModule, MatToolbarModule } from '@angular/material';
5-
import { BrowserModule } from '@angular/platform-browser';
6-
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
7-
8-
import { CoreModule } from '~/core/core.module';
94

105
import { components } from './components';
116
import { containers } from './containers';
@@ -16,13 +11,16 @@ const declarations = [...components, ...containers, ...pages];
1611

1712
export const vendorImports = [MatBadgeModule, MatIconModule, MatListModule, MatMenuModule, MatToolbarModule];
1813

19-
const imports = [MainRoutingModule, BrowserAnimationsModule, BrowserModule, CoreModule, HttpClientModule, LayoutModule, ...vendorImports];
14+
const imports = [CommonModule, MainRoutingModule, ...vendorImports];
2015

2116
const _exports = [MainRootComponent];
2217

18+
const providers = [];
19+
2320
@NgModule({
2421
declarations,
2522
imports,
23+
providers,
2624
exports: _exports,
2725
})
2826
export class MainModule {}

src/app/main/pages/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import { MainErrorComponent } from './main-error/main-error.component';
12
import { MainRootComponent } from './main-root/main-root.component';
23

3-
export const pages = [MainRootComponent];
4+
export const pages = [MainErrorComponent, MainRootComponent];
45

5-
export { MainRootComponent };
6+
export { MainErrorComponent, MainRootComponent };
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<div class="main-error">
2+
<h2 class="main-error-title">Oops!</h2>
3+
<h3 class="main-error-subtitle">Sorry Something Went Wrong!</h3>
4+
<a routerLink="/" mat-raised-button>
5+
<mat-icon>home</mat-icon> Go To Home
6+
</a>
7+
<a href="mailto:loktionov129@gmail.com" mat-raised-button>
8+
<mat-icon>mail</mat-icon> Contact Support
9+
</a>
10+
</div>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.main-error {
2+
text-align: center;
3+
4+
.mat-raised-button {
5+
margin: 0 4px 0 8px;
6+
}
7+
}

0 commit comments

Comments
 (0)