Skip to content

Commit 27a6da6

Browse files
authored
139 show copyable and pastable error message to user when they encounter an issue (#144)
* feat: added error handler extension * feat: add error page and error handler * feat: added redirect * fix: remove test throw error
1 parent e0c748f commit 27a6da6

File tree

14 files changed

+387
-240
lines changed

14 files changed

+387
-240
lines changed

package-lock.json

Lines changed: 174 additions & 164 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"@angular/cli": "^14.2.5",
3434
"@angular/compiler-cli": "^14.2.5",
3535
"@angular/language-service": "^14.2.5",
36-
"@sentry/angular-ivy": "^7.48.0",
36+
"@sentry/angular": "^8.31.0",
3737
"@sentry/cli": "^2.17.5",
3838
"@types/file-saver": "^2.0.1",
3939
"@types/jest": "^28.1.8",

src/app/CustomErrorHandler.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Injectable } from '@angular/core';
2+
import { Router } from '@angular/router';
3+
import { SentryErrorHandler } from '@sentry/angular';
4+
import { environment } from 'src/environments/environment';
5+
import { TaskManagerService } from './services/task-manager.service';
6+
import { UserStateService } from './services/user-state-service';
7+
import { IErrorNavigationState } from './pages/participant/error-page/error-page.component';
8+
9+
@Injectable()
10+
export default class CustomErrorHandler extends SentryErrorHandler {
11+
constructor(
12+
private _router: Router,
13+
private taskManager: TaskManagerService,
14+
private userStateService: UserStateService
15+
) {
16+
super();
17+
}
18+
19+
handleError(error: string | Error): void {
20+
this._router.navigate(['/task-error'], {
21+
state: {
22+
taskIndex: this.taskManager?.currentStudyTask?.taskOrder,
23+
studyId: this.taskManager?.currentStudyTask?.studyId,
24+
stackTrace: error instanceof Error ? error.stack : error,
25+
userId: this.userStateService.currentlyLoggedInUserId,
26+
} as IErrorNavigationState,
27+
});
28+
if (environment.production) {
29+
super.handleError(error);
30+
}
31+
}
32+
}

src/app/app.module.ts

Lines changed: 28 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,32 @@
1+
import { HttpClient, HttpClientModule, HttpClientXsrfModule, HTTP_INTERCEPTORS } from '@angular/common/http';
2+
import { ErrorHandler, NgModule } from '@angular/core';
3+
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
14
import { BrowserModule } from '@angular/platform-browser';
2-
import { NgModule, ErrorHandler, APP_INITIALIZER } from '@angular/core';
3-
import * as Sentry from '@sentry/angular-ivy';
4-
import { AppRoutingModule } from './routing/app-routing.module';
5-
import { AppComponent } from './app.component';
6-
import { LoginComponent } from './pages/landing-page/login/login.component';
7-
import { Router } from '@angular/router';
85
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
9-
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
6+
import { AppComponent } from './app.component';
107
import { MaterialModule } from './modules/material/material.module';
11-
import { HttpClient, HttpClientModule, HttpClientXsrfModule, HTTP_INTERCEPTORS } from '@angular/common/http';
12-
import { CrowdSourceLoginComponent } from './pages/landing-page/crowdsource-login/crowdsource-login.component';
13-
import { ConfirmationComponent } from './services/confirmation/confirmation.component';
14-
import { LoaderComponent } from './services/loader/loader.component';
158
import { CreateUserDialogComponent } from './pages/admin/admin-dashboard/manage-users/create-user-dialog/create-user-dialog.component';
9+
import { AdminModule } from './pages/admin/admin.module';
10+
import { CrowdSourceLoginComponent } from './pages/landing-page/crowdsource-login/crowdsource-login.component';
1611
import { LandingPageComponent } from './pages/landing-page/landing-page.component';
12+
import { LoginComponent } from './pages/landing-page/login/login.component';
1713
import { RegisterComponent } from './pages/landing-page/register/register.component';
18-
import { AdminModule } from './pages/admin/admin.module';
1914
import { ParticipantModule } from './pages/participant/participant.module';
2015
import { TaskModule } from './pages/tasks/task.module';
21-
// import { ErrorInterceptor } from './interceptors/error.interceptor';
22-
import { SendResetPasswordComponent } from './pages/landing-page/forgot-password/send-reset-password/send-reset-password.component';
16+
import { AppRoutingModule } from './routing/app-routing.module';
17+
import { ConfirmationComponent } from './services/confirmation/confirmation.component';
18+
import { LoaderComponent } from './services/loader/loader.component';
19+
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
20+
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
21+
import { HttpCsrfInterceptor } from './interceptors/csrf.interceptor';
2322
import { ResetPasswordLoginComponent } from './pages/landing-page/forgot-password/change-password-page/reset-password-login.component';
23+
import { SendResetPasswordComponent } from './pages/landing-page/forgot-password/send-reset-password/send-reset-password.component';
24+
import { NotFoundComponent } from './pages/landing-page/not-found/not-found.component';
2425
import { StudyBackgroundComponent } from './pages/landing-page/study-background/study-background.component';
26+
import { OrganizationMemberModule } from './pages/organization-member/organization-member.module';
2527
import { SharedModule } from './pages/shared/shared.module';
26-
import { NotFoundComponent } from './pages/landing-page/not-found/not-found.component';
27-
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
28-
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
2928
import { SnackbarComponent } from './services/snackbar/snackbar.component';
30-
import { OrganizationMemberModule } from './pages/organization-member/organization-member.module';
31-
import { HttpCsrfInterceptor } from './interceptors/csrf.interceptor';
29+
import CustomErrorHandler from './CustomErrorHandler';
3230

3331
export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
3432
return new TranslateHttpLoader(http, '../assets/translate/', '.json');
@@ -94,20 +92,18 @@ export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
9492
},
9593
{
9694
provide: ErrorHandler,
97-
useValue: Sentry.createErrorHandler({
98-
showDialog: true,
99-
}),
100-
},
101-
{
102-
provide: Sentry.TraceService,
103-
deps: [Router],
104-
},
105-
{
106-
provide: APP_INITIALIZER,
107-
useFactory: () => () => {},
108-
deps: [Sentry.TraceService],
109-
multi: true,
95+
useClass: CustomErrorHandler,
11096
},
97+
// {
98+
// provide: Sentry.TraceService,
99+
// deps: [Router],
100+
// },
101+
// {
102+
// provide: APP_INITIALIZER,
103+
// useFactory: () => () => {},
104+
// deps: [Sentry.TraceService],
105+
// multi: true,
106+
// },
111107
],
112108
bootstrap: [AppComponent],
113109
})
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<div style="width: 100%; min-height: 100vh; padding: 4rem 0">
2+
<div class="flex-column-center">
3+
<div style="width: 50%">
4+
<h6 style="font-size: 2rem; color: salmon" class="mb-4">We encountered an error.</h6>
5+
<div style="font-size: 1.5rem; line-height: normal" class="mb-4">
6+
While running the task, we ran into an error. In order to report this and help us fix it as fast as
7+
possible, please do the following:
8+
</div>
9+
<div class="steps-container">
10+
<div class="number-container text">1</div>
11+
<div class="content-container">
12+
<span class="my-3" style="font-size: 1.5rem; line-height: normal">
13+
The task information and error is displayed below. Please click the
14+
<span style="color: #3f51b5">Copy Error</span> button to copy it to your clipboard.
15+
</span>
16+
<div style="background-color: rgb(233, 233, 233); padding: 1rem; border-radius: 8px">
17+
<div style="text-align: start" class="mb-2">
18+
<button mat-flat-button [cdkCopyToClipboard]="copyableErrString" color="primary">
19+
<span>Copy Error</span>
20+
<mat-icon class="ml-3">content_copy</mat-icon>
21+
</button>
22+
</div>
23+
<div style="color: salmon; max-height: 200px; overflow-y: auto; white-space: pre-line">
24+
{{ copyableErrString }}
25+
</div>
26+
</div>
27+
</div>
28+
</div>
29+
<div class="steps-container">
30+
<div class="number-container text">2</div>
31+
<div class="content-container">
32+
<span class="my-2" style="font-size: 1.5rem; line-height: normal">
33+
Paste the error into an email and send it to
34+
<a href="mailto:[email protected]">sharplab.neuro&#64;mcgill.ca</a>
35+
</span>
36+
</div>
37+
</div>
38+
<div class="steps-container">
39+
<div class="number-container text">3</div>
40+
<div class="content-container">
41+
<span class="my-2" style="font-size: 1.5rem; line-height: normal"> Go back to the main menu.</span>
42+
<span style="font-size: 1.5rem; line-height: normal">
43+
Thank for your participation and your assistance!
44+
</span>
45+
<div class="mt-2">
46+
<button mat-flat-button color="primary" (click)="navigateToMain()">Back to main menu</button>
47+
</div>
48+
</div>
49+
</div>
50+
</div>
51+
</div>
52+
</div>

src/app/pages/participant/error-page/error-page.component.scss

Whitespace-only changes.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { Component, OnInit } from '@angular/core';
2+
import { Router } from '@angular/router';
3+
import { SessionStorageService } from '../../../services/sessionStorage.service';
4+
import { SnackbarService } from '../../../services/snackbar/snackbar.service';
5+
import { Role } from 'src/app/models/enums';
6+
import { UserStateService } from 'src/app/services/user-state-service';
7+
8+
export interface IErrorNavigationState {
9+
studyId?: number;
10+
taskIndex?: number;
11+
stackTrace?: string;
12+
userId?: string;
13+
}
14+
15+
@Component({
16+
selector: 'app-error-page',
17+
templateUrl: './error-page.component.html',
18+
styleUrls: ['./error-page.component.scss', '../final-page/final-page.component.scss'],
19+
})
20+
export class ErrorPageComponent implements OnInit {
21+
constructor(
22+
private _snackbar: SnackbarService,
23+
private _router: Router,
24+
private _sessionStorage: SessionStorageService,
25+
private _userStateService: UserStateService
26+
) {
27+
const params = this._router.getCurrentNavigation()?.extras?.state as IErrorNavigationState;
28+
this.copyableErrString = `User ID: ${params?.userId}\nStudy ID: ${params?.studyId}\nTask Index: ${params?.taskIndex}\n\n${params?.stackTrace}`;
29+
this.copyableErrString = this.copyableErrString.trim();
30+
}
31+
32+
copyableErrString: string = '';
33+
34+
ngOnInit(): void {}
35+
36+
navigateToMain() {
37+
if (this._userStateService.isCrowdsourcedUser) {
38+
if (this._userStateService.currentlyRunningStudyId) {
39+
this._router.navigateByUrl(
40+
`crowdsource-participant?studyid=${this._userStateService.currentlyRunningStudyId}`
41+
);
42+
} else {
43+
this._router.navigate([`crowdsource-participant`]);
44+
}
45+
} else if (this._userStateService.userIsAdmin) {
46+
this._router.navigate([`admin-dashboard`]);
47+
} else if (this._userStateService.userIsParticipant) {
48+
this._router.navigate([`participant-dashboard`]);
49+
} else if (this._userStateService.userIsGuest || this._userStateService.userIsOrgMember) {
50+
this._router.navigate([`organization-member-dashboard`]);
51+
} else {
52+
throw new Error('Unsupported user role!');
53+
}
54+
}
55+
}

src/app/pages/participant/final-page/final-page.component.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import { Component, OnInit } from '@angular/core';
2-
import { SnackbarService } from '../../../services/snackbar/snackbar.service';
3-
import { UserService } from '../../../services/user.service';
4-
import { TaskManagerService } from '../../../services/task-manager.service';
5-
import { ActivatedRoute, Router } from '@angular/router';
2+
import { Router } from '@angular/router';
63
import { SessionStorageService } from '../../../services/sessionStorage.service';
4+
import { SnackbarService } from '../../../services/snackbar/snackbar.service';
75

86
@Component({
97
selector: 'app-final-page',

src/app/pages/participant/participant.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ import { FinalPageComponent } from './final-page/final-page.component';
1111
import { ConsentDialogComponent } from './participant-dashboard/participant-studies/consent-dialog/consent-dialog.component';
1212
import { TranslateModule } from '@ngx-translate/core';
1313
import { LanguageDialogComponent } from './participant-dashboard/language-dialog/language-dialog.component';
14+
import { ErrorPageComponent } from './error-page/error-page.component';
1415

1516
@NgModule({
1617
declarations: [
1718
ParticipantDashboardComponent,
1819
ParticipantStudiesComponent,
1920
FinalPageComponent,
21+
ErrorPageComponent,
2022
ConsentDialogComponent,
2123
LanguageDialogComponent,
2224
],

src/app/pages/tasks/task-playables/base-task.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component, OnDestroy, OnInit } from '@angular/core';
1+
import { Component, ErrorHandler, OnDestroy, OnInit } from '@angular/core';
22
import { Subject, Subscription } from 'rxjs';
33
import { wait } from 'src/app/common/commonMethods';
44
import { BaseParticipantData } from 'src/app/models/ParticipantData';

0 commit comments

Comments
 (0)