Skip to content

Commit b1404d0

Browse files
authored
Merge pull request #1866 from mozilla/1867821/sendbeacon-fallback-uploader
Bug 1867821 - Implement an uploader that defaults to sendBeacon and falls back to…
2 parents e59e8ed + 3a2e59e commit b1404d0

File tree

5 files changed

+166
-9
lines changed

5 files changed

+166
-9
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, false);
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();

glean/src/platform/browser/sendbeacon_uploader.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ class BrowserSendBeaconUploader extends Uploader {
1616
// eslint-disable-next-line @typescript-eslint/require-await
1717
async post(
1818
url: string,
19-
pingRequest: PingRequest<string | Uint8Array>
19+
pingRequest: PingRequest<string | Uint8Array>,
20+
errorLog = true,
2021
): Promise<UploadResult> {
2122
// While the most appropriate type would be "application/json",
2223
// using that would cause to send CORS preflight requests. We
@@ -29,7 +30,9 @@ class BrowserSendBeaconUploader extends Uploader {
2930
// thing we can do is remove this from our internal queue.
3031
return new UploadResult(UploadResultStatus.Success, 200);
3132
}
32-
log(LOG_TAG, "The `sendBeacon` call was not serviced by the browser. Deleting data.", LoggingLevel.Error);
33+
if (errorLog) {
34+
log(LOG_TAG, "The `sendBeacon` call was not serviced by the browser. Deleting data.", LoggingLevel.Error);
35+
}
3336
// If the agent says there's a problem, there's not so much we can do.
3437
return new UploadResult(UploadResultStatus.UnrecoverableFailure);
3538
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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+
// eslint-disable-next-line jsdoc/require-jsdoc
22+
function setGlobalSendBeacon() {
23+
global.navigator.sendBeacon = (url: string, content: string): boolean => {
24+
void fetch(url, {
25+
body: content,
26+
method: "POST",
27+
});
28+
29+
return true;
30+
};
31+
}
32+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
33+
// @ts-ignore
34+
global.fetch = fetch;
35+
36+
describe("Uploader/BrowserSendBeaconFallback", function () {
37+
beforeEach(function() {
38+
setGlobalSendBeacon();
39+
});
40+
41+
afterEach(function () {
42+
sandbox.restore();
43+
});
44+
45+
it("returns the correct status for successful requests", async function () {
46+
const TEST_PING_CONTENT = {"my-test-value": 40721};
47+
for (const status of [200, 400, 500]) {
48+
nock(MOCK_ENDPOINT).post(/./i, body => {
49+
return JSON.stringify(body) == JSON.stringify(TEST_PING_CONTENT);
50+
}).reply(status);
51+
52+
const response = BrowserSendBeaconFallbackUploader.post(MOCK_ENDPOINT, new PingRequest("abc", {}, JSON.stringify(TEST_PING_CONTENT), 1024));
53+
// When using sendBeacon, we can't really tell if something was correctly uploaded
54+
// or not. All we can know is if the request was enqueued, so we always expect 200.
55+
const expectedResponse = new UploadResult(UploadResultStatus.Success, 200);
56+
assert.deepStrictEqual(
57+
await response,
58+
expectedResponse
59+
);
60+
}
61+
});
62+
63+
it("returns the correct status after fallback", async function () {
64+
const TEST_PING_CONTENT = {"my-test-value": 40721};
65+
nock(MOCK_ENDPOINT).post(/./i).reply(200);
66+
67+
// Reset `fetch` to a known state.
68+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
69+
// @ts-ignore
70+
global.fetch = fetch;
71+
72+
// Ensure `sendBeacon` fails.
73+
global.navigator.sendBeacon = () => false;
74+
75+
const response = BrowserSendBeaconFallbackUploader.post(MOCK_ENDPOINT, new PingRequest("abc", {}, JSON.stringify(TEST_PING_CONTENT), 1024));
76+
const expectedResponse = new UploadResult(UploadResultStatus.Success, 200);
77+
assert.deepStrictEqual(
78+
await response,
79+
expectedResponse
80+
);
81+
});
82+
83+
it("returns the correct status when both uploads fail", async function () {
84+
nock(MOCK_ENDPOINT).post(/./i).replyWithError({
85+
message: "something awful happened",
86+
code: "AWFUL_ERROR",
87+
});
88+
89+
// Reset `fetch` to a known state.
90+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
91+
// @ts-ignore
92+
global.fetch = fetch;
93+
94+
// Ensure `sendBeacon` fails.
95+
global.navigator.sendBeacon = () => false;
96+
97+
const response = BrowserSendBeaconFallbackUploader.post(MOCK_ENDPOINT, new PingRequest("abc", {}, "{}", 1024));
98+
const expectedResponse = new UploadResult(UploadResultStatus.RecoverableFailure);
99+
assert.deepStrictEqual(
100+
await response,
101+
expectedResponse
102+
);
103+
});
104+
});

glean/tests/unit/platform/browser/sendbeacon_uploader.spec.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,23 @@ const MOCK_ENDPOINT = "http://www.example.com";
1818

1919
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
2020
// @ts-ignore
21-
global.navigator.sendBeacon = (url: string, content: string): boolean => {
22-
void fetch(url, {
23-
body: content,
24-
method: "POST",
25-
});
21+
// eslint-disable-next-line jsdoc/require-jsdoc
22+
function setGlobalSendBeacon() {
23+
global.navigator.sendBeacon = (url: string, content: string): boolean => {
24+
void fetch(url, {
25+
body: content,
26+
method: "POST",
27+
});
2628

27-
return true;
28-
};
29+
return true;
30+
};
31+
}
2932

3033
describe("Uploader/BrowserSendBeacon", function () {
34+
beforeEach(function() {
35+
setGlobalSendBeacon();
36+
});
37+
3138
afterEach(function () {
3239
sandbox.restore();
3340
});

0 commit comments

Comments
 (0)