Skip to content

Commit bdb02b2

Browse files
committed
fix(errors): properly suppressed datacite errors
1 parent 1d0337a commit bdb02b2

3 files changed

Lines changed: 62 additions & 46 deletions

File tree

src/app/core/interceptors/error.interceptor.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,33 @@
1-
import { throwError } from 'rxjs';
1+
import { EMPTY, throwError } from 'rxjs';
22
import { catchError } from 'rxjs/operators';
33

4-
import { HttpErrorResponse, HttpInterceptorFn } from '@angular/common/http';
4+
import { HttpContextToken, HttpErrorResponse, HttpInterceptorFn } from '@angular/common/http';
55
import { inject } from '@angular/core';
66
import { Router } from '@angular/router';
77

8+
import { SENTRY_TOKEN } from '@core/provider/sentry.provider';
89
import { hasViewOnlyParam } from '@osf/shared/helpers';
910
import { LoaderService, ToastService } from '@osf/shared/services';
1011

1112
import { ERROR_MESSAGES } from '../constants';
1213
import { AuthService } from '../services';
1314

15+
export const BYPASS_ERROR_INTERCEPTOR = new HttpContextToken<boolean>(() => false);
16+
1417
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
1518
const toastService = inject(ToastService);
1619
const loaderService = inject(LoaderService);
1720
const router = inject(Router);
1821
const authService = inject(AuthService);
22+
const sentry = inject(SENTRY_TOKEN);
1923

2024
return next(req).pipe(
2125
catchError((error: HttpErrorResponse) => {
2226
let errorMessage: string;
27+
if (req.context.get(BYPASS_ERROR_INTERCEPTOR)) {
28+
sentry.captureException(error);
29+
return EMPTY;
30+
}
2331

2432
if (error.error instanceof ErrorEvent) {
2533
errorMessage = error.error.message;

src/app/shared/services/datacite/datacite.service.spec.ts

Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ function assertSuccess(
4646
doi: string,
4747
event: DataciteEvent
4848
) {
49+
assertSendBeacon(dataciteTrackerAddress, dataciteTrackerRepoId, doi, event);
4950
const req = httpMock.expectOne(dataciteTrackerAddress);
5051
expect(req.request.method).toBe('POST');
5152
expect(req.request.body).toEqual({
@@ -58,6 +59,24 @@ function assertSuccess(
5859
req.flush({});
5960
}
6061

62+
function assertSendBeacon(
63+
dataciteTrackerAddress: string,
64+
dataciteTrackerRepoId: string,
65+
doi: string,
66+
event: DataciteEvent
67+
) {
68+
expect(navigator.sendBeacon).toBeCalledTimes(1);
69+
expect(navigator.sendBeacon).toHaveBeenCalledWith(
70+
dataciteTrackerAddress,
71+
JSON.stringify({
72+
n: event,
73+
u: window.location.href,
74+
i: dataciteTrackerRepoId,
75+
p: doi,
76+
})
77+
);
78+
}
79+
6180
describe('DataciteService', () => {
6281
let service: DataciteService;
6382
let sentry: jest.Mocked<any>;
@@ -67,10 +86,11 @@ describe('DataciteService', () => {
6786
const apiDomainUrl = 'https://osf.io';
6887
const dataciteTrackerRepoId = 'repo-123';
6988
describe('with proper configuration', () => {
70-
sentry = {
71-
captureException: jest.fn(),
72-
};
7389
beforeEach(() => {
90+
Object.defineProperty(navigator, 'sendBeacon', {
91+
configurable: true,
92+
value: jest.fn(() => false), // mock implementation that returns true
93+
});
7494
TestBed.configureTestingModule({
7595
providers: [
7696
DataciteService,
@@ -164,33 +184,15 @@ describe('DataciteService', () => {
164184
assertSuccess(httpMock, dataciteTrackerAddress, dataciteTrackerRepoId, doi, DataciteEvent.DOWNLOAD);
165185
});
166186

167-
it('should log error to sentry', (done: jest.DoneCallback) => {
168-
const doi = 'qwerty';
169-
const event = 'view';
170-
service.logIdentifiableView(buildObservable(doi)).subscribe({
171-
next: () => {},
172-
error: () => {
173-
throw new Error('The error should have been caught and suppressed by the service.');
174-
},
175-
complete: () => {
176-
expect(sentry.captureException).toHaveBeenCalled();
177-
178-
done();
179-
},
180-
});
187+
it('navigator success', () => {
188+
(navigator.sendBeacon as jest.Mock).mockReturnValueOnce(true);
181189

182-
const req = httpMock.expectOne(dataciteTrackerAddress);
183-
expect(req.request.method).toBe('POST');
184-
expect(req.request.body).toEqual({
185-
n: event,
186-
u: window.location.href,
187-
i: dataciteTrackerRepoId,
188-
p: doi,
189-
});
190-
expect(req.request.headers.get('Content-Type')).toBe('application/json');
190+
const doi = 'qwerty';
191+
const event = DataciteEvent.VIEW;
192+
service.logIdentifiableView(buildObservable(doi)).subscribe();
191193

192-
const mockError = new ProgressEvent('Internal Server Error');
193-
req.error(mockError);
194+
httpMock.expectNone(dataciteTrackerAddress);
195+
assertSendBeacon(dataciteTrackerAddress, dataciteTrackerRepoId, doi, event);
194196
});
195197
});
196198

src/app/shared/services/datacite/datacite.service.ts

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
11
import { EMPTY, filter, map, Observable, of, switchMap, take } from 'rxjs';
2-
import { catchError } from 'rxjs/operators';
32

4-
import { HttpClient } from '@angular/common/http';
3+
import { HttpClient, HttpContext } from '@angular/common/http';
54
import { inject, Injectable } from '@angular/core';
65

6+
import { BYPASS_ERROR_INTERCEPTOR } from '@core/interceptors';
77
import { ENVIRONMENT } from '@core/provider/environment.provider';
8-
import { SENTRY_TOKEN } from '@core/provider/sentry.provider';
98
import { Identifier, IdentifiersResponseJsonApi } from '@osf/shared/models';
109
import { DataciteEvent } from '@osf/shared/models/datacite/datacite-event.enum';
1110

1211
@Injectable({
1312
providedIn: 'root',
1413
})
1514
export class DataciteService {
16-
private readonly Sentry = inject(SENTRY_TOKEN);
1715
private readonly http: HttpClient = inject(HttpClient);
1816
private readonly environment = inject(ENVIRONMENT);
1917

@@ -91,17 +89,25 @@ export class DataciteService {
9189
i: this.dataciteTrackerRepoId,
9290
p: doi,
9391
};
94-
const headers = {
95-
'Content-Type': 'application/json',
96-
};
97-
return this.http.post(this.dataciteTrackerAddress, payload, { headers }).pipe(
98-
map(() => {
99-
return;
100-
}),
101-
catchError((err) => {
102-
this.Sentry.captureException(err);
103-
return of();
104-
})
105-
);
92+
const success = navigator.sendBeacon(this.dataciteTrackerAddress, JSON.stringify(payload));
93+
if (success) {
94+
return of(void 0);
95+
} else {
96+
const headers = {
97+
'Content-Type': 'application/json',
98+
};
99+
const context = new HttpContext();
100+
context.set(BYPASS_ERROR_INTERCEPTOR, true);
101+
return this.http
102+
.post(this.dataciteTrackerAddress, payload, {
103+
headers,
104+
context,
105+
})
106+
.pipe(
107+
map(() => {
108+
return;
109+
})
110+
);
111+
}
106112
}
107113
}

0 commit comments

Comments
 (0)