Skip to content

Commit 726f9fb

Browse files
committed
Implement an uploader that defaults to sendBeacon and falls back to fetch
1 parent e59e8ed commit 726f9fb

File tree

3 files changed

+124
-0
lines changed

3 files changed

+124
-0
lines changed

glean/src/entry/web.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ import { base } from "./base.js";
77

88
export { default as Uploader, UploadResult, UploadResultStatus } from "../core/upload/uploader.js";
99
export {default as BrowserSendBeaconUploader} from "../platform/browser/sendbeacon_uploader.js";
10+
export {default as BrowserSendBeaconFallbackUploader} from "../platform/browser/sendbeacon_fallback_uploader.js";
1011
export default base(platform);
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
import type PingRequest from "../../core/upload/ping_request.js";
6+
7+
import log, { LoggingLevel } from "../../core/log.js";
8+
import Uploader from "../../core/upload/uploader.js";
9+
import BrowserFetchUploader from "./fetch_uploader.js";
10+
import BrowserSendBeaconUploader from "./sendbeacon_uploader.js";
11+
import { UploadResultStatus } from "../../core/upload/uploader.js";
12+
import type UploadResult from "../../core/upload/uploader.js";
13+
14+
const LOG_TAG = "platform.browser.SendBeaconFallbackUploader";
15+
16+
class BrowserSendBeaconFallbackUploader extends Uploader {
17+
fetchUploader = BrowserFetchUploader;
18+
sendBeaconUploader = BrowserSendBeaconUploader;
19+
20+
// eslint-disable-next-line @typescript-eslint/require-await
21+
async post(
22+
url: string,
23+
pingRequest: PingRequest<string | Uint8Array>
24+
): Promise<UploadResult> {
25+
26+
// Try `sendBeacon` first,
27+
// fall back to `fetch` if `sendBeacon` reports an error.
28+
const beaconStatus = await this.sendBeaconUploader.post(url, pingRequest);
29+
if (beaconStatus.result == UploadResultStatus.Success) {
30+
return beaconStatus;
31+
}
32+
log(LOG_TAG, "The `sendBeacon` call was not serviced by the browser. Falling back to the fetch uploader.", LoggingLevel.Warn);
33+
34+
return this.fetchUploader.post(url, pingRequest);
35+
}
36+
37+
supportsCustomHeaders(): boolean {
38+
return false;
39+
}
40+
}
41+
42+
export default new BrowserSendBeaconFallbackUploader();
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
import "jsdom-global/register";
6+
import assert from "assert";
7+
import sinon from "sinon";
8+
import nock from "nock";
9+
import fetch from "node-fetch";
10+
11+
import BrowserSendBeaconFallbackUploader from "../../../../src/platform/browser/sendbeacon_fallback_uploader";
12+
import { UploadResult, UploadResultStatus } from "../../../../src/core/upload/uploader";
13+
import PingRequest from "../../../../src/core/upload/ping_request";
14+
15+
const sandbox = sinon.createSandbox();
16+
17+
const MOCK_ENDPOINT = "http://www.example.com";
18+
19+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
20+
// @ts-ignore
21+
global.navigator.sendBeacon = (url: string, content: string): boolean => {
22+
void fetch(url, {
23+
body: content,
24+
method: "POST",
25+
});
26+
27+
return true;
28+
};
29+
global.fetch = fetch;
30+
31+
describe("Uploader/BrowserSendBeaconFallback", function () {
32+
afterEach(function () {
33+
sandbox.restore();
34+
});
35+
36+
it("returns the correct status for successful requests", async function () {
37+
const TEST_PING_CONTENT = {"my-test-value": 40721};
38+
for (const status of [200, 400, 500]) {
39+
nock(MOCK_ENDPOINT).post(/./i, body => {
40+
return JSON.stringify(body) == JSON.stringify(TEST_PING_CONTENT);
41+
}).reply(status);
42+
43+
const response = BrowserSendBeaconFallbackUploader.post(MOCK_ENDPOINT, new PingRequest("abc", {}, JSON.stringify(TEST_PING_CONTENT), 1024));
44+
// When using sendBeacon, we can't really tell if something was correctly uploaded
45+
// or not. All we can know is if the request was enqueued, so we always expect 200.
46+
const expectedResponse = new UploadResult(UploadResultStatus.Success, 200);
47+
assert.deepStrictEqual(
48+
await response,
49+
expectedResponse
50+
);
51+
}
52+
});
53+
54+
it("returns the correct status after fallback", async function () {
55+
const TEST_PING_CONTENT = {"my-test-value": 40721};
56+
nock(MOCK_ENDPOINT).post(/./i).reply(200);
57+
58+
global.navigator.sendBeacon = () => false;
59+
const response = BrowserSendBeaconFallbackUploader.post(MOCK_ENDPOINT, new PingRequest("abc", {}, JSON.stringify(TEST_PING_CONTENT), 1024));
60+
const expectedResponse = new UploadResult(UploadResultStatus.Success, 200);
61+
assert.deepStrictEqual(
62+
await response,
63+
expectedResponse
64+
);
65+
});
66+
67+
it("returns the correct status when both uploads fail", async function () {
68+
nock(MOCK_ENDPOINT).post(/./i).replyWithError({
69+
message: "something awful happened",
70+
code: "AWFUL_ERROR",
71+
});
72+
73+
global.navigator.sendBeacon = () => false;
74+
const response = BrowserSendBeaconFallbackUploader.post(MOCK_ENDPOINT, new PingRequest("abc", {}, "{}", 1024));
75+
const expectedResponse = new UploadResult(UploadResultStatus.RecoverableFailure);
76+
assert.deepStrictEqual(
77+
await response,
78+
expectedResponse
79+
);
80+
});
81+
});

0 commit comments

Comments
 (0)