Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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