Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/app/core/interceptors/error.interceptor.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,33 @@
import { throwError } from 'rxjs';
import { EMPTY, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

import { HttpErrorResponse, HttpInterceptorFn } from '@angular/common/http';
import { HttpContextToken, HttpErrorResponse, HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { Router } from '@angular/router';

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

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

export const BYPASS_ERROR_INTERCEPTOR = new HttpContextToken<boolean>(() => false);

export const errorInterceptor: HttpInterceptorFn = (req, next) => {
const toastService = inject(ToastService);
const loaderService = inject(LoaderService);
const router = inject(Router);
const authService = inject(AuthService);
const sentry = inject(SENTRY_TOKEN);

return next(req).pipe(
catchError((error: HttpErrorResponse) => {
let errorMessage: string;
if (req.context.get(BYPASS_ERROR_INTERCEPTOR)) {
sentry.captureException(error);
return EMPTY;
}

if (error.error instanceof ErrorEvent) {
errorMessage = error.error.message;
Expand Down
58 changes: 30 additions & 28 deletions src/app/shared/services/datacite/datacite.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ function assertSuccess(
doi: string,
event: DataciteEvent
) {
assertSendBeacon(dataciteTrackerAddress, dataciteTrackerRepoId, doi, event);
const req = httpMock.expectOne(dataciteTrackerAddress);
expect(req.request.method).toBe('POST');
expect(req.request.body).toEqual({
Expand All @@ -58,6 +59,24 @@ function assertSuccess(
req.flush({});
}

function assertSendBeacon(
dataciteTrackerAddress: string,
dataciteTrackerRepoId: string,
doi: string,
event: DataciteEvent
) {
expect(navigator.sendBeacon).toBeCalledTimes(1);
expect(navigator.sendBeacon).toHaveBeenCalledWith(
dataciteTrackerAddress,
JSON.stringify({
n: event,
u: window.location.href,
i: dataciteTrackerRepoId,
p: doi,
})
);
}

describe('DataciteService', () => {
let service: DataciteService;
let sentry: jest.Mocked<any>;
Expand All @@ -67,10 +86,11 @@ describe('DataciteService', () => {
const apiDomainUrl = 'https://osf.io';
const dataciteTrackerRepoId = 'repo-123';
describe('with proper configuration', () => {
sentry = {
captureException: jest.fn(),
};
beforeEach(() => {
Object.defineProperty(navigator, 'sendBeacon', {
configurable: true,
value: jest.fn(() => false),
});
TestBed.configureTestingModule({
providers: [
DataciteService,
Expand Down Expand Up @@ -164,33 +184,15 @@ describe('DataciteService', () => {
assertSuccess(httpMock, dataciteTrackerAddress, dataciteTrackerRepoId, doi, DataciteEvent.DOWNLOAD);
});

it('should log error to sentry', (done: jest.DoneCallback) => {
const doi = 'qwerty';
const event = 'view';
service.logIdentifiableView(buildObservable(doi)).subscribe({
next: () => {},
error: () => {
throw new Error('The error should have been caught and suppressed by the service.');
},
complete: () => {
expect(sentry.captureException).toHaveBeenCalled();

done();
},
});
it('navigator success', () => {
(navigator.sendBeacon as jest.Mock).mockReturnValueOnce(true);

const req = httpMock.expectOne(dataciteTrackerAddress);
expect(req.request.method).toBe('POST');
expect(req.request.body).toEqual({
n: event,
u: window.location.href,
i: dataciteTrackerRepoId,
p: doi,
});
expect(req.request.headers.get('Content-Type')).toBe('application/json');
const doi = 'qwerty';
const event = DataciteEvent.VIEW;
service.logIdentifiableView(buildObservable(doi)).subscribe();

const mockError = new ProgressEvent('Internal Server Error');
req.error(mockError);
httpMock.expectNone(dataciteTrackerAddress);
assertSendBeacon(dataciteTrackerAddress, dataciteTrackerRepoId, doi, event);
});
});

Expand Down
34 changes: 18 additions & 16 deletions src/app/shared/services/datacite/datacite.service.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import { EMPTY, filter, map, Observable, of, switchMap, take } from 'rxjs';
import { catchError } from 'rxjs/operators';

import { HttpClient } from '@angular/common/http';
import { HttpClient, HttpContext } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';

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

@Injectable({
providedIn: 'root',
})
export class DataciteService {
private readonly Sentry = inject(SENTRY_TOKEN);
private readonly http: HttpClient = inject(HttpClient);
private readonly environment = inject(ENVIRONMENT);

Expand Down Expand Up @@ -91,17 +89,21 @@ export class DataciteService {
i: this.dataciteTrackerRepoId,
p: doi,
};
const headers = {
'Content-Type': 'application/json',
};
return this.http.post(this.dataciteTrackerAddress, payload, { headers }).pipe(
map(() => {
return;
}),
catchError((err) => {
this.Sentry.captureException(err);
return of();
})
);
const success = navigator.sendBeacon(this.dataciteTrackerAddress, JSON.stringify(payload));
if (success) {
return of(void 0);
} else {
const headers = {
'Content-Type': 'application/json',
};
const context = new HttpContext();
context.set(BYPASS_ERROR_INTERCEPTOR, true);
return this.http
.post(this.dataciteTrackerAddress, payload, {
headers,
context,
})
.pipe(map(() => undefined));
}
}
}
Loading