Skip to content

Commit d54c762

Browse files
tvanierHazAT
authored andcommitted
feat: Add beacon transport (#1498)
1 parent 1f3bdca commit d54c762

File tree

5 files changed

+122
-0
lines changed

5 files changed

+122
-0
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { SentryEvent, SentryResponse, Status } from '@sentry/types';
2+
import { getGlobalObject } from '@sentry/utils/misc';
3+
import { serialize } from '@sentry/utils/object';
4+
import { BaseTransport } from './base';
5+
6+
const global = getGlobalObject() as Window;
7+
8+
/** `sendBeacon` based transport */
9+
export class BeaconTransport extends BaseTransport {
10+
/**
11+
* @inheritDoc
12+
*/
13+
public async send(event: SentryEvent): Promise<SentryResponse> {
14+
const data = serialize(event);
15+
16+
const result = global.navigator.sendBeacon(this.url, data);
17+
18+
return {
19+
status: result ? Status.Success : Status.Failed,
20+
};
21+
}
22+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { BaseTransport } from './base';
22
export { FetchTransport } from './fetch';
33
export { XHRTransport } from './xhr';
4+
export { BeaconTransport } from './beacon';
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { expect } from 'chai';
2+
import { SinonStub, stub } from 'sinon';
3+
import { Status, Transports } from '../../src';
4+
5+
const testDSN = 'https://[email protected]/42';
6+
const transportUrl = 'https://sentry.io/api/42/store/?sentry_key=123&sentry_version=7';
7+
const payload = {
8+
event_id: '1337',
9+
message: 'Pickle Rick',
10+
user: {
11+
username: 'Morty',
12+
},
13+
};
14+
15+
let sendBeacon: SinonStub;
16+
let transport: Transports.BaseTransport;
17+
18+
describe('BeaconTransport', () => {
19+
beforeEach(() => {
20+
sendBeacon = stub(window.navigator, 'sendBeacon');
21+
transport = new Transports.BeaconTransport({ dsn: testDSN });
22+
});
23+
24+
afterEach(() => {
25+
sendBeacon.restore();
26+
});
27+
28+
it('inherits composeEndpointUrl() implementation', () => {
29+
expect(transport.url).equal(transportUrl);
30+
});
31+
32+
describe('send()', async () => {
33+
it('sends a request to Sentry servers', async () => {
34+
sendBeacon.returns(true);
35+
36+
return transport.send(payload).then(res => {
37+
expect(res.status).equal(Status.Success);
38+
expect(sendBeacon.calledOnce).equal(true);
39+
expect(sendBeacon.calledWith(transportUrl, JSON.stringify(payload))).equal(true);
40+
});
41+
});
42+
43+
it('rejects with failed status', async () => {
44+
sendBeacon.returns(false);
45+
46+
return transport.send(payload).catch(res => {
47+
expect(res.status).equal(Status.Failed);
48+
expect(sendBeacon.calledOnce).equal(true);
49+
expect(sendBeacon.calledWith(transportUrl, JSON.stringify(payload))).equal(true);
50+
});
51+
});
52+
});
53+
});

packages/utils/src/supports.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,17 @@ export function supportsFetch(): boolean {
7676
}
7777
}
7878

79+
/**
80+
* Tells whether current environment supports sendBeacon API
81+
* {@link supportsBeacon}.
82+
*
83+
* @returns Answer to the given question.
84+
*/
85+
export function supportsBeacon(): boolean {
86+
const global = getGlobalObject();
87+
return 'navigator' in global && 'sendBeacon' in global.navigator;
88+
}
89+
7990
/**
8091
* Tells whether current environment supports Referrer Policy API
8192
* {@link supportsReferrerPolicy}.

packages/utils/test/supports.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import * as misc from '../src/misc';
2+
import * as supports from '../src/supports';
3+
4+
describe('Supports', () => {
5+
let global: any;
6+
let getGlobalObject: any;
7+
8+
beforeEach(() => {
9+
global = {};
10+
getGlobalObject = jest.spyOn(misc, 'getGlobalObject');
11+
getGlobalObject.mockReturnValue(global);
12+
});
13+
14+
afterEach(() => {
15+
getGlobalObject.mockRestore();
16+
});
17+
18+
describe('supportsBeacon', () => {
19+
it('should return false if no navigator in global', () => {
20+
expect(supports.supportsBeacon()).toEqual(false);
21+
});
22+
23+
it('should return false if navigator and no sendBeacon in global', () => {
24+
global.navigator = {};
25+
expect(supports.supportsBeacon()).toEqual(false);
26+
});
27+
28+
it('should return true if navigator and sendBeacon in global', () => {
29+
global.navigator = {
30+
sendBeacon: jest.fn(),
31+
};
32+
expect(supports.supportsBeacon()).toEqual(true);
33+
});
34+
});
35+
});

0 commit comments

Comments
 (0)