Skip to content

Commit

Permalink
Merge pull request #1866 from mozilla/1867821/sendbeacon-fallback-upl…
Browse files Browse the repository at this point in the history
…oader

Bug 1867821 - Implement an uploader that defaults to sendBeacon and falls back to…
  • Loading branch information
rosahbruno committed Feb 1, 2024
2 parents e59e8ed + 3a2e59e commit b1404d0
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 9 deletions.
1 change: 1 addition & 0 deletions glean/src/entry/web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ import { base } from "./base.js";

export { default as Uploader, UploadResult, UploadResultStatus } from "../core/upload/uploader.js";
export {default as BrowserSendBeaconUploader} from "../platform/browser/sendbeacon_uploader.js";
export {default as BrowserSendBeaconFallbackUploader} from "../platform/browser/sendbeacon_fallback_uploader.js";
export default base(platform);
42 changes: 42 additions & 0 deletions glean/src/platform/browser/sendbeacon_fallback_uploader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import type PingRequest from "../../core/upload/ping_request.js";

import log, { LoggingLevel } from "../../core/log.js";
import Uploader from "../../core/upload/uploader.js";
import BrowserFetchUploader from "./fetch_uploader.js";
import BrowserSendBeaconUploader from "./sendbeacon_uploader.js";
import { UploadResultStatus } from "../../core/upload/uploader.js";
import type { UploadResult } from "../../core/upload/uploader.js";

const LOG_TAG = "platform.browser.SendBeaconFallbackUploader";

class BrowserSendBeaconFallbackUploader extends Uploader {
fetchUploader = BrowserFetchUploader;
sendBeaconUploader = BrowserSendBeaconUploader;

// eslint-disable-next-line @typescript-eslint/require-await
async post(
url: string,
pingRequest: PingRequest<string | Uint8Array>
): Promise<UploadResult> {

// Try `sendBeacon` first,
// fall back to `fetch` if `sendBeacon` reports an error.
const beaconStatus = await this.sendBeaconUploader.post(url, pingRequest, false);
if (beaconStatus.result == UploadResultStatus.Success) {
return beaconStatus;
}
log(LOG_TAG, "The `sendBeacon` call was not serviced by the browser. Falling back to the fetch uploader.", LoggingLevel.Warn);

return this.fetchUploader.post(url, pingRequest);
}

supportsCustomHeaders(): boolean {
return false;
}
}

export default new BrowserSendBeaconFallbackUploader();
7 changes: 5 additions & 2 deletions glean/src/platform/browser/sendbeacon_uploader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ class BrowserSendBeaconUploader extends Uploader {
// eslint-disable-next-line @typescript-eslint/require-await
async post(
url: string,
pingRequest: PingRequest<string | Uint8Array>
pingRequest: PingRequest<string | Uint8Array>,
errorLog = true,
): Promise<UploadResult> {
// While the most appropriate type would be "application/json",
// using that would cause to send CORS preflight requests. We
Expand All @@ -29,7 +30,9 @@ class BrowserSendBeaconUploader extends Uploader {
// thing we can do is remove this from our internal queue.
return new UploadResult(UploadResultStatus.Success, 200);
}
log(LOG_TAG, "The `sendBeacon` call was not serviced by the browser. Deleting data.", LoggingLevel.Error);
if (errorLog) {
log(LOG_TAG, "The `sendBeacon` call was not serviced by the browser. Deleting data.", LoggingLevel.Error);
}
// If the agent says there's a problem, there's not so much we can do.
return new UploadResult(UploadResultStatus.UnrecoverableFailure);
}
Expand Down
104 changes: 104 additions & 0 deletions glean/tests/unit/platform/browser/sendbeacon_fallback_uploader.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import "jsdom-global/register";
import assert from "assert";
import sinon from "sinon";
import nock from "nock";
import fetch from "node-fetch";

import BrowserSendBeaconFallbackUploader from "../../../../src/platform/browser/sendbeacon_fallback_uploader";
import { UploadResult, UploadResultStatus } from "../../../../src/core/upload/uploader";
import PingRequest from "../../../../src/core/upload/ping_request";

const sandbox = sinon.createSandbox();

const MOCK_ENDPOINT = "http://www.example.com";

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// eslint-disable-next-line jsdoc/require-jsdoc
function setGlobalSendBeacon() {
global.navigator.sendBeacon = (url: string, content: string): boolean => {
void fetch(url, {
body: content,
method: "POST",
});

return true;
};
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
global.fetch = fetch;

describe("Uploader/BrowserSendBeaconFallback", function () {
beforeEach(function() {
setGlobalSendBeacon();
});

afterEach(function () {
sandbox.restore();
});

it("returns the correct status for successful requests", async function () {
const TEST_PING_CONTENT = {"my-test-value": 40721};
for (const status of [200, 400, 500]) {
nock(MOCK_ENDPOINT).post(/./i, body => {
return JSON.stringify(body) == JSON.stringify(TEST_PING_CONTENT);
}).reply(status);

const response = BrowserSendBeaconFallbackUploader.post(MOCK_ENDPOINT, new PingRequest("abc", {}, JSON.stringify(TEST_PING_CONTENT), 1024));
// When using sendBeacon, we can't really tell if something was correctly uploaded
// or not. All we can know is if the request was enqueued, so we always expect 200.
const expectedResponse = new UploadResult(UploadResultStatus.Success, 200);
assert.deepStrictEqual(
await response,
expectedResponse
);
}
});

it("returns the correct status after fallback", async function () {
const TEST_PING_CONTENT = {"my-test-value": 40721};
nock(MOCK_ENDPOINT).post(/./i).reply(200);

// Reset `fetch` to a known state.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
global.fetch = fetch;

// Ensure `sendBeacon` fails.
global.navigator.sendBeacon = () => false;

const response = BrowserSendBeaconFallbackUploader.post(MOCK_ENDPOINT, new PingRequest("abc", {}, JSON.stringify(TEST_PING_CONTENT), 1024));
const expectedResponse = new UploadResult(UploadResultStatus.Success, 200);
assert.deepStrictEqual(
await response,
expectedResponse
);
});

it("returns the correct status when both uploads fail", async function () {
nock(MOCK_ENDPOINT).post(/./i).replyWithError({
message: "something awful happened",
code: "AWFUL_ERROR",
});

// Reset `fetch` to a known state.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
global.fetch = fetch;

// Ensure `sendBeacon` fails.
global.navigator.sendBeacon = () => false;

const response = BrowserSendBeaconFallbackUploader.post(MOCK_ENDPOINT, new PingRequest("abc", {}, "{}", 1024));
const expectedResponse = new UploadResult(UploadResultStatus.RecoverableFailure);
assert.deepStrictEqual(
await response,
expectedResponse
);
});
});
21 changes: 14 additions & 7 deletions glean/tests/unit/platform/browser/sendbeacon_uploader.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,23 @@ const MOCK_ENDPOINT = "http://www.example.com";

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
global.navigator.sendBeacon = (url: string, content: string): boolean => {
void fetch(url, {
body: content,
method: "POST",
});
// eslint-disable-next-line jsdoc/require-jsdoc
function setGlobalSendBeacon() {
global.navigator.sendBeacon = (url: string, content: string): boolean => {
void fetch(url, {
body: content,
method: "POST",
});

return true;
};
return true;
};
}

describe("Uploader/BrowserSendBeacon", function () {
beforeEach(function() {
setGlobalSendBeacon();
});

afterEach(function () {
sandbox.restore();
});
Expand Down

0 comments on commit b1404d0

Please sign in to comment.