From f98481b962b4afadc5d294ba44997d679c7cbeb2 Mon Sep 17 00:00:00 2001 From: Thomas Vanier Date: Thu, 30 Aug 2018 11:04:02 -0700 Subject: [PATCH] add beacon transport --- packages/browser/src/transports/beacon.ts | 22 ++++++++ packages/browser/src/transports/index.ts | 1 + .../browser/test/transports/beacon.test.ts | 53 +++++++++++++++++++ packages/utils/src/supports.ts | 11 ++++ packages/utils/test/supports.test.ts | 35 ++++++++++++ 5 files changed, 122 insertions(+) create mode 100644 packages/browser/src/transports/beacon.ts create mode 100644 packages/browser/test/transports/beacon.test.ts create mode 100644 packages/utils/test/supports.test.ts diff --git a/packages/browser/src/transports/beacon.ts b/packages/browser/src/transports/beacon.ts new file mode 100644 index 000000000000..6e7ac89a3d84 --- /dev/null +++ b/packages/browser/src/transports/beacon.ts @@ -0,0 +1,22 @@ +import { SentryEvent, SentryResponse, Status } from '@sentry/types'; +import { getGlobalObject } from '@sentry/utils/misc'; +import { serialize } from '@sentry/utils/object'; +import { BaseTransport } from './base'; + +const global = getGlobalObject() as Window; + +/** `sendBeacon` based transport */ +export class BeaconTransport extends BaseTransport { + /** + * @inheritDoc + */ + public async send(event: SentryEvent): Promise { + const data = serialize(event); + + const result = global.navigator.sendBeacon(this.url, data); + + return { + status: result ? Status.Success : Status.Failed, + }; + } +} diff --git a/packages/browser/src/transports/index.ts b/packages/browser/src/transports/index.ts index 65323f6c3744..54db9ba9eb40 100644 --- a/packages/browser/src/transports/index.ts +++ b/packages/browser/src/transports/index.ts @@ -1,3 +1,4 @@ export { BaseTransport } from './base'; export { FetchTransport } from './fetch'; export { XHRTransport } from './xhr'; +export { BeaconTransport } from './beacon'; diff --git a/packages/browser/test/transports/beacon.test.ts b/packages/browser/test/transports/beacon.test.ts new file mode 100644 index 000000000000..bb99deb20671 --- /dev/null +++ b/packages/browser/test/transports/beacon.test.ts @@ -0,0 +1,53 @@ +import { expect } from 'chai'; +import { SinonStub, stub } from 'sinon'; +import { Status, Transports } from '../../src'; + +const testDSN = 'https://123@sentry.io/42'; +const transportUrl = 'https://sentry.io/api/42/store/?sentry_key=123&sentry_version=7'; +const payload = { + event_id: '1337', + message: 'Pickle Rick', + user: { + username: 'Morty', + }, +}; + +let sendBeacon: SinonStub; +let transport: Transports.BaseTransport; + +describe('BeaconTransport', () => { + beforeEach(() => { + sendBeacon = stub(window.navigator, 'sendBeacon'); + transport = new Transports.BeaconTransport({ dsn: testDSN }); + }); + + afterEach(() => { + sendBeacon.restore(); + }); + + it('inherits composeEndpointUrl() implementation', () => { + expect(transport.url).equal(transportUrl); + }); + + describe('send()', async () => { + it('sends a request to Sentry servers', async () => { + sendBeacon.returns(true); + + return transport.send(payload).then(res => { + expect(res.status).equal(Status.Success); + expect(sendBeacon.calledOnce).equal(true); + expect(sendBeacon.calledWith(transportUrl, JSON.stringify(payload))).equal(true); + }); + }); + + it('rejects with failed status', async () => { + sendBeacon.returns(false); + + return transport.send(payload).catch(res => { + expect(res.status).equal(Status.Failed); + expect(sendBeacon.calledOnce).equal(true); + expect(sendBeacon.calledWith(transportUrl, JSON.stringify(payload))).equal(true); + }); + }); + }); +}); diff --git a/packages/utils/src/supports.ts b/packages/utils/src/supports.ts index 71161e3263ce..9e1313aaf60a 100644 --- a/packages/utils/src/supports.ts +++ b/packages/utils/src/supports.ts @@ -76,6 +76,17 @@ export function supportsFetch(): boolean { } } +/** + * Tells whether current environment supports sendBeacon API + * {@link supportsBeacon}. + * + * @returns Answer to the given question. + */ +export function supportsBeacon(): boolean { + const global = getGlobalObject(); + return 'navigator' in global && 'sendBeacon' in global.navigator; +} + /** * Tells whether current environment supports Referrer Policy API * {@link supportsReferrerPolicy}. diff --git a/packages/utils/test/supports.test.ts b/packages/utils/test/supports.test.ts new file mode 100644 index 000000000000..caf868900daa --- /dev/null +++ b/packages/utils/test/supports.test.ts @@ -0,0 +1,35 @@ +import * as misc from '../src/misc'; +import * as supports from '../src/supports'; + +describe('Supports', () => { + let global: any; + let getGlobalObject: any; + + beforeEach(() => { + global = {}; + getGlobalObject = jest.spyOn(misc, 'getGlobalObject'); + getGlobalObject.mockReturnValue(global); + }); + + afterEach(() => { + getGlobalObject.mockRestore(); + }); + + describe('supportsBeacon', () => { + it('should return false if no navigator in global', () => { + expect(supports.supportsBeacon()).toEqual(false); + }); + + it('should return false if navigator and no sendBeacon in global', () => { + global.navigator = {}; + expect(supports.supportsBeacon()).toEqual(false); + }); + + it('should return true if navigator and sendBeacon in global', () => { + global.navigator = { + sendBeacon: jest.fn(), + }; + expect(supports.supportsBeacon()).toEqual(true); + }); + }); +});