diff --git a/package.json b/package.json index 26aecb37..f0365dc4 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "build-dev": "ng build --configuration development --aot", "test": "ng test", "lint": "ng lint", - "deploy": "cd dist && scp ...", + "deploy": "cd dist && scp -o \"User=neuron\" -r . wecog.research.mcgill.ca:~/tmp-frontend", "build-deploy": "npm run build && npm run sentry:generate-sourcemaps && npm run sentry:upload-sourcemaps && npm run remove-maps && npm run deploy", "lint-fix": "ng lint --fix=true", "sentry:generate-sourcemaps": "sentry-cli sourcemaps inject ./dist", diff --git a/src/app/CustomErrorHandler.ts b/src/app/CustomErrorHandler.ts index f935d2cf..b08dfb66 100644 --- a/src/app/CustomErrorHandler.ts +++ b/src/app/CustomErrorHandler.ts @@ -7,6 +7,7 @@ import { UserStateService } from './services/user-state-service'; import { IErrorNavigationState } from './pages/error-page/error-page.component'; import { HttpErrorResponse } from '@angular/common/http'; import { SnackbarService } from './services/snackbar/snackbar.service'; +import { getSafeErrorInfo } from './common/error-utils'; @Injectable() export default class CustomErrorHandler extends SentryErrorHandler { @@ -35,7 +36,7 @@ export default class CustomErrorHandler extends SentryErrorHandler { state: { taskIndex: undefined, studyId: undefined, - stackTrace: error.message, + stackTrace: getSafeErrorInfo(error), userId: this.userStateService.currentlyLoggedInUserId, } as IErrorNavigationState, }); @@ -46,7 +47,7 @@ export default class CustomErrorHandler extends SentryErrorHandler { state: { taskIndex: this.taskManager?.currentStudyTask?.taskOrder, studyId: this.taskManager?.currentStudyTask?.studyId, - stackTrace: error instanceof Error ? error.stack : error, + stackTrace: getSafeErrorInfo(error), userId: this.userStateService.currentlyLoggedInUserId, } as IErrorNavigationState, }); diff --git a/src/app/app.module.ts b/src/app/app.module.ts index be3d94f1..bea0f45e 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -19,6 +19,7 @@ import { LoaderComponent } from './services/loader/loader.component'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateHttpLoader } from '@ngx-translate/http-loader'; import { HttpCsrfInterceptor } from './interceptors/csrf.interceptor'; +import { ErrorInterceptor } from './interceptors/error.interceptor'; import { ResetPasswordLoginComponent } from './pages/landing-page/forgot-password/change-password-page/reset-password-login.component'; import { SendResetPasswordComponent } from './pages/landing-page/forgot-password/send-reset-password/send-reset-password.component'; import { NotFoundComponent } from './pages/landing-page/not-found/not-found.component'; @@ -82,11 +83,11 @@ export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader { AppRoutingModule, ], providers: [ - // { - // provide: HTTP_INTERCEPTORS, - // useClass: ErrorInterceptor, - // multi: true, - // }, + { + provide: HTTP_INTERCEPTORS, + useClass: ErrorInterceptor, + multi: true, + }, { provide: HTTP_INTERCEPTORS, useClass: HttpCsrfInterceptor, @@ -96,16 +97,6 @@ export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader { provide: ErrorHandler, useClass: CustomErrorHandler, }, - // { - // provide: Sentry.TraceService, - // deps: [Router], - // }, - // { - // provide: APP_INITIALIZER, - // useFactory: () => () => {}, - // deps: [Sentry.TraceService], - // multi: true, - // }, ], bootstrap: [AppComponent], }) diff --git a/src/app/common/error-utils.ts b/src/app/common/error-utils.ts new file mode 100644 index 00000000..f28395b0 --- /dev/null +++ b/src/app/common/error-utils.ts @@ -0,0 +1,41 @@ +/** + * Utility functions for safely handling errors without causing DataCloneError + * when passing through Angular Router state + */ + +/** + * Safely extracts error information from any type of error object + * without passing non-serializable objects through router state + */ +export function getSafeErrorInfo(err?: any): string { + let errorMessage = 'An error occurred'; + let errorDetails = ''; + + if (err) { + if (typeof err === 'string') { + errorMessage = err; + } else if (err instanceof Error) { + errorMessage = err.message; + errorDetails = err.stack || ''; + } else if (err && typeof err === 'object') { + // Handle HttpErrorResponse and other objects + if (err.message) { + errorMessage = err.message; + } else if (err.error) { + errorMessage = typeof err.error === 'string' ? err.error : 'HTTP Error'; + } else if (err.status) { + errorMessage = `HTTP ${err.status}: ${err.statusText || 'Request failed'}`; + } + + // Safely extract additional details + if (err.status) { + errorDetails = `Status: ${err.status}`; + if (err.statusText) { + errorDetails += ` - ${err.statusText}`; + } + } + } + } + + return `${errorMessage}\n${errorDetails}`.trim(); +} diff --git a/src/app/interceptors/error.interceptor.ts b/src/app/interceptors/error.interceptor.ts index 5ab357e8..898a6c64 100644 --- a/src/app/interceptors/error.interceptor.ts +++ b/src/app/interceptors/error.interceptor.ts @@ -1,24 +1,38 @@ -// import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; -// import { Injectable } from '@angular/core'; -// import { Router } from '@angular/router'; -// import { Observable, throwError } from 'rxjs'; -// import { catchError } from 'rxjs/operators'; -// import { RouteNames } from '../models/enums'; -// import { SnackbarService } from '../services/snackbar/snackbar.service'; +import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Router } from '@angular/router'; +import { EMPTY, Observable, of, throwError } from 'rxjs'; +import { catchError } from 'rxjs/operators'; +import { RouteNames } from '../models/enums'; +import { SnackbarService } from '../services/snackbar/snackbar.service'; +import { ClearanceService } from '../services/clearance.service'; -// @Injectable() -// export class ErrorInterceptor implements HttpInterceptor { -// constructor(private snackbarService: SnackbarService, private router: Router) {} +@Injectable() +export class ErrorInterceptor implements HttpInterceptor { + constructor( + private snackbarService: SnackbarService, + private router: Router, + private clearanceService: ClearanceService + ) {} -// intercept(req: HttpRequest, next: HttpHandler): Observable> { -// return next.handle(req).pipe( -// catchError((err: HttpErrorResponse) => { -// if (err.status === 401) { -// this.snackbarService.openInfoSnackbar('Please login again to continue'); -// this.router.navigate([`/${RouteNames.LANDINGPAGE_LOGIN_BASEROUTE}`]); -// } -// return throwError(err.error); -// }) -// ); -// } -// } + intercept(req: HttpRequest, next: HttpHandler): Observable> { + return next.handle(req).pipe( + catchError((err: HttpErrorResponse) => { + if (err.status === 401) { + // JWT has expired - log the user out and redirect to login + this.snackbarService.openInfoSnackbar('Your session has expired. Please login again to continue.'); + + // Clear all cached data and session storage + this.clearanceService.clearServices(); + + // Redirect to login page + this.router.navigate([`/${RouteNames.LANDINGPAGE_LOGIN_BASEROUTE}`]); + + // do not propagate the error to the next interceptor + return EMPTY; + } + return throwError(err); + }) + ); + } +} diff --git a/src/app/pages/participant/participant-dashboard/participant-studies/participant-study/participant-study.component.ts b/src/app/pages/participant/participant-dashboard/participant-studies/participant-study/participant-study.component.ts index 3551ead3..ac0b4e25 100644 --- a/src/app/pages/participant/participant-dashboard/participant-studies/participant-study/participant-study.component.ts +++ b/src/app/pages/participant/participant-dashboard/participant-studies/participant-study/participant-study.component.ts @@ -111,8 +111,8 @@ export class ParticipantStudyComponent implements OnInit, OnDestroy { ) .subscribe( (_res) => {}, - (err: HttpStatus) => { - this.snackbar.openErrorSnackbar(err.message); + (err) => { + this.taskManager.handleErr(err); } ) .add(() => { diff --git a/src/app/services/task-manager.service.ts b/src/app/services/task-manager.service.ts index 6441354d..98d0d2a9 100644 --- a/src/app/services/task-manager.service.ts +++ b/src/app/services/task-manager.service.ts @@ -25,6 +25,7 @@ import { UserStateService } from './user-state-service'; import { CrowdSourcedUserService } from './crowdsourced-user.service'; import { snapshotToStudyTasks } from './utils'; import { IErrorNavigationState } from '../pages/error-page/error-page.component'; +import { getSafeErrorInfo } from '../common/error-utils'; @Injectable({ providedIn: 'root', @@ -150,7 +151,7 @@ export class TaskManagerService implements CanClear { taskIndex: this.currentStudyTask?.taskOrder, studyId: this.currentStudyTask?.studyId, userId: this.userStateService?.currentlyLoggedInUserId, - stackTrace: err instanceof Error ? err.stack : err, + stackTrace: getSafeErrorInfo(err), } as IErrorNavigationState, }); }