Skip to content

Commit

Permalink
Merge branch 'release-v4.0.0-pre.1' into release
Browse files Browse the repository at this point in the history
  • Loading branch information
Dexterp37 committed Dec 1, 2023
2 parents c138e71 + 9166152 commit 245c9ae
Show file tree
Hide file tree
Showing 18 changed files with 263 additions and 71 deletions.
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
# Unreleased changes

[Full changelog](https://github.com/mozilla/glean.js/compare/v4.0.0-pre.0...main)
[Full changelog](https://github.com/mozilla/glean.js/compare/v4.0.0-pre.1...main)

# v4.0.0-pre.1 (2023-12-01)

[Full changelog](https://github.com/mozilla/glean.js/compare/v4.0.0-pre.0...v4.0.0-pre.1)

[#1834](https://github.com/mozilla/glean.js/pull/1834): Added support for `navigator.sendBeacon`. This is not turned on by default and needs to be enabled manually.

# v4.0.0-pre.0 (2023-11-27)

[Full changelog](https://github.com/mozilla/glean.js/compare/v3.0.0...v4.0.0-pre.0)

[#1808](https://github.com/mozilla/glean.js/pull/1808): **BREAKING CHANGE**: Make glean.js fully synchronous.

# v3.0.0 (2023-11-16)

[Full changelog](https://github.com/mozilla/glean.js/compare/v3.0.0-pre.1...v3.0.0)
Expand Down
4 changes: 2 additions & 2 deletions glean/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion glean/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@mozilla/glean",
"version": "4.0.0-pre.0",
"version": "4.0.0-pre.1",
"description": "An implementation of the Glean SDK, a modern cross-platform telemetry client, for JavaScript environments.",
"type": "module",
"sideEffects": false,
Expand Down
2 changes: 1 addition & 1 deletion glean/src/core/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const GLEAN_SCHEMA_VERSION = 1;
//
// PACKAGE_VERSION is defined as a global by webpack,
// we need a default here for testing when the app is not build with webpack.
export const GLEAN_VERSION = "4.0.0-pre.0";
export const GLEAN_VERSION = "4.0.0-pre.1";

// The name of a "ping" that will include Glean ping_info metrics,
// such as ping sequence numbers.
Expand Down
5 changes: 2 additions & 3 deletions glean/src/core/glean.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,8 +268,7 @@ namespace Glean {
Context.applicationId = sanitizeApplicationId(applicationId);

// The configuration constructor will throw in case config has any incorrect prop.
const correctConfig = new Configuration(config);
Context.config = correctConfig;
Context.config = new Configuration(config);

// Pre-set debug options for Glean from browser SessionStorage values.
setDebugOptionsFromSessionStorage();
Expand All @@ -283,7 +282,7 @@ namespace Glean {
Context.pingsDatabase = new PingsDatabase();
Context.errorManager = new ErrorManager();

pingUploader = new PingUploadManager(correctConfig, Context.pingsDatabase);
pingUploader = new PingUploadManager(Context.config, Context.pingsDatabase);

Context.initialized = true;

Expand Down
11 changes: 11 additions & 0 deletions glean/src/core/upload/ping_body_overflow_error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/* 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/. */

// Error to be thrown in case the final ping body is larger than MAX_PING_BODY_SIZE.
export class PingBodyOverflowError extends Error {
constructor(message?: string) {
super(message);
this.name = "PingBodyOverflow";
}
}
50 changes: 50 additions & 0 deletions glean/src/core/upload/ping_request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/* 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 { gzipSync, strToU8 } from "fflate";
import { PingBodyOverflowError } from "./ping_body_overflow_error.js";

/**
* Represents a payload to be transmitted by an uploading mechanism.
*/
export default class PingRequest<BodyType extends string | Uint8Array> {
constructor (
readonly identifier: string,
readonly headers: Record<string, string>,
readonly payload: BodyType,
readonly maxBodySize: number
) {}

asCompressedPayload(): PingRequest<string | Uint8Array> {
// If this is already gzipped, do nothing.
if (this.headers["Content-Encoding"] == "gzip") {
return this;
}

// We prefer using `strToU8` instead of TextEncoder directly,
// because it will polyfill TextEncoder if it's not present in the environment.
// For example, IE doesn't provide TextEncoder.
const encodedBody = strToU8(this.payload as string);

let finalBody: string | Uint8Array;
const headers = this.headers;
try {
finalBody = gzipSync(encodedBody);
headers["Content-Encoding"] = "gzip";
headers["Content-Length"] = finalBody.length.toString();
} catch {
// Fall back to whatever we had.
return this;
}

if (finalBody.length > this.maxBodySize) {
throw new PingBodyOverflowError(
`Body for ping ${this.identifier} exceeds ${this.maxBodySize} bytes. Discarding.`
);
}

return new PingRequest<string | Uint8Array>(this.identifier, headers, finalBody, this.maxBodySize);
}
}

11 changes: 10 additions & 1 deletion glean/src/core/upload/uploader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
* 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 "./ping_request.js";

// The timeout, in milliseconds, to use for all operations with the server.
export const DEFAULT_UPLOAD_TIMEOUT_MS = 10_000;

Expand Down Expand Up @@ -51,7 +53,14 @@ export abstract class Uploader {
* @param headers Optional headers to include in the request
* @returns The status code of the response.
*/
abstract post(url: string, body: string | Uint8Array, headers?: Record<string, string>): Promise<UploadResult>;
abstract post(url: string, pingRequest: PingRequest<string | Uint8Array>): Promise<UploadResult>;

/**
* Whether or not this uploader supports submitting custom headers.
*
* @returns whether or not custom headers are supported.
*/
abstract supportsCustomHeaders(): boolean;
}

export default Uploader;
59 changes: 21 additions & 38 deletions glean/src/core/upload/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
* 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 { gzipSync, strToU8 } from "fflate";

import type { QueuedPing } from "./manager.js";
import type Uploader from "./uploader.js";
import type { UploadTask } from "./task.js";
Expand All @@ -14,17 +12,11 @@ import Policy from "./policy.js";
import { UploadResult, UploadResultStatus } from "./uploader.js";
import { UploadTaskTypes } from "./task.js";
import { GLEAN_VERSION } from "../constants.js";
import { PingBodyOverflowError } from "./ping_body_overflow_error.js";
import PingRequest from "./ping_request.js";

const PING_UPLOAD_WORKER_LOG_TAG = "core.Upload.PingUploadWorker";

// Error to be thrown in case the final ping body is larger than MAX_PING_BODY_SIZE.
class PingBodyOverflowError extends Error {
constructor(message?: string) {
super(message);
this.name = "PingBodyOverflow";
}
}

class PingUploadWorker {
// Whether or not someone is blocking on the currentJob.
isBlocking = false;
Expand All @@ -47,10 +39,7 @@ class PingUploadWorker {
* @param ping The ping to include the headers in.
* @returns The updated ping.
*/
private buildPingRequest(ping: QueuedPing): {
headers: Record<string, string>;
payload: string | Uint8Array;
} {
private buildPingRequest(ping: QueuedPing): PingRequest<string | Uint8Array> {
let headers = ping.headers || {};
headers = {
...ping.headers,
Expand All @@ -60,33 +49,15 @@ class PingUploadWorker {
};

const stringifiedBody = JSON.stringify(ping.payload);
// We prefer using `strToU8` instead of TextEncoder directly,
// because it will polyfill TextEncoder if it's not present in the environment.
// Environments that don't provide TextEncoder are IE and most importantly QML.
const encodedBody = strToU8(stringifiedBody);

let finalBody: string | Uint8Array;
let bodySizeInBytes: number;
try {
finalBody = gzipSync(encodedBody);
bodySizeInBytes = finalBody.length;
headers["Content-Encoding"] = "gzip";
} catch {
finalBody = stringifiedBody;
bodySizeInBytes = encodedBody.length;
}

if (bodySizeInBytes > this.policy.maxPingBodySize) {
if (stringifiedBody.length > this.policy.maxPingBodySize) {
throw new PingBodyOverflowError(
`Body for ping ${ping.identifier} exceeds ${this.policy.maxPingBodySize}bytes. Discarding.`
);
}

headers["Content-Length"] = bodySizeInBytes.toString();
return {
headers,
payload: finalBody
};
headers["Content-Length"] = stringifiedBody.length.toString();
return new PingRequest(ping.identifier, headers, stringifiedBody, this.policy.maxPingBodySize);
}

/**
Expand All @@ -99,12 +70,24 @@ class PingUploadWorker {
try {
const finalPing = this.buildPingRequest(ping);

let safeUploader = this.uploader;
if (!this.uploader.supportsCustomHeaders()) {
// Some options require us to submit custom headers. Unfortunately not all the
// uploaders support them (e.g. `sendBeacon`). In case headers are required, switch
// back to the default uploader that, for now, supports headers.
const needsHeaders = !(
(Context.config.sourceTags === undefined) && (Context.config.debugViewTag === undefined)
);
if (needsHeaders) {
safeUploader = Context.platform.uploader;
}
}

// The POST call has to be asynchronous. Once the API call is triggered,
// we rely on the browser's "keepalive" header.
return this.uploader.post(
return safeUploader.post(
`${this.serverEndpoint}${ping.path}`,
finalPing.payload,
finalPing.headers
finalPing
);
} catch (e) {
log(
Expand Down
1 change: 1 addition & 0 deletions glean/src/entry/web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ import platform from "../platform/browser/web/index.js";
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 base(platform);
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,34 @@
* 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 { DEFAULT_UPLOAD_TIMEOUT_MS, UploadResultStatus, UploadResult } from "../../core/upload/uploader.js";

const LOG_TAG = "platform.browser.Uploader";
const LOG_TAG = "platform.browser.FetchUploader";

class BrowserUploader extends Uploader {
class BrowserFetchUploader extends Uploader {
timeoutMs: number = DEFAULT_UPLOAD_TIMEOUT_MS;

async post(
url: string,
body: string | Uint8Array,
headers: Record<string, string> = {},
pingRequest: PingRequest<string | Uint8Array>,
keepalive = true
): Promise<UploadResult> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);

// We expect to have a gzipped payload.
const gzipRequest = pingRequest.asCompressedPayload();

let response;
try {
response = await fetch(url.toString(), {
headers,
headers: gzipRequest.headers,
method: "POST",
body: body,
body: gzipRequest.payload,
keepalive,
// Strips any cookies or authorization headers from the request.
credentials: "omit",
Expand All @@ -42,7 +46,7 @@ class BrowserUploader extends Uploader {
// Try again without `keepalive`, because that may be the issue.
// This problem was observed in chromium versions below v81.
// See: https://chromium.googlesource.com/chromium/src/+/26d70b36dd1c18244fb17b91d275332c8b73eab3
return this.post(url, body, headers, false);
return this.post(url, gzipRequest, false);
}

// From MDN: "A fetch() promise will reject with a TypeError
Expand All @@ -63,6 +67,10 @@ class BrowserUploader extends Uploader {
clearTimeout(timeout);
return new UploadResult(UploadResultStatus.Success, response.status);
}

supportsCustomHeaders(): boolean {
return true;
}
}

export default new BrowserUploader();
export default new BrowserFetchUploader();
42 changes: 42 additions & 0 deletions glean/src/platform/browser/sendbeacon_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 { DEFAULT_UPLOAD_TIMEOUT_MS, UploadResultStatus, UploadResult } from "../../core/upload/uploader.js";

const LOG_TAG = "platform.browser.SendBeaconUploader";

class BrowserSendBeaconUploader extends Uploader {
timeoutMs: number = DEFAULT_UPLOAD_TIMEOUT_MS;

// eslint-disable-next-line @typescript-eslint/require-await
async post(
url: string,
pingRequest: PingRequest<string | Uint8Array>
): Promise<UploadResult> {
// While the most appropriate type would be "application/json",
// using that would cause to send CORS preflight requests. We
// instead send the content as plain text and rely on the backend
// to do the appropriate validation/parsing.
const wasQueued = navigator.sendBeacon(url, pingRequest.payload);
if (wasQueued) {
// Unfortunately we don't know much more other than data was enqueued,
// it is the agent's responsibility to manage the submission. The only
// 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 the agent says there's a problem, there's not so much we can do.
return new UploadResult(UploadResultStatus.UnrecoverableFailure);
}

supportsCustomHeaders(): boolean {
return false;
}
}

export default new BrowserSendBeaconUploader();
2 changes: 1 addition & 1 deletion glean/src/platform/browser/web/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import type Platform from "../../index.js";

import uploader from "../uploader.js";
import uploader from "../fetch_uploader.js";
import info from "./platform_info.js";
import Storage from "./storage.js";

Expand Down
Loading

0 comments on commit 245c9ae

Please sign in to comment.