@@ -7,16 +7,31 @@ import { gzipSync, strToU8 } from "fflate";
77import type Platform from "../../platform/index.js" ;
88import type { Configuration } from "../config.js" ;
99import { GLEAN_VERSION } from "../constants.js" ;
10+ import { Context } from "../context.js" ;
1011import log , { LoggingLevel } from "../log.js" ;
1112import type { Observer as PingsDatabaseObserver , PingInternalRepresentation } from "../pings/database.js" ;
1213import type PingsDatabase from "../pings/database.js" ;
1314import type PlatformInfo from "../platform_info.js" ;
14- import type { UploadResult } from "./uploader.js" ;
15+ import type { UploadResult } from "./uploader.js" ;
1516import type Uploader from "./uploader.js" ;
1617import { UploadResultStatus } from "./uploader.js" ;
1718
1819const LOG_TAG = "core.Upload" ;
1920
21+ /**
22+ * Policies for ping storage, uploading and requests.
23+ */
24+ export class Policy {
25+ constructor (
26+ // The maximum recoverable failures allowed per uploading window.
27+ //
28+ // Limiting this is necessary to avoid infinite loops on requesting upload tasks.
29+ readonly maxRecoverableFailures : number = 3 ,
30+ // The maximum size in bytes a ping body may have to be eligible for upload.
31+ readonly maxPingBodySize : number = 1024 * 1024 // 1MB
32+ ) { }
33+ }
34+
2035interface QueuedPing extends PingInternalRepresentation {
2136 identifier : string
2237}
@@ -33,6 +48,14 @@ const enum PingUploaderStatus {
3348 Cancelling ,
3449}
3550
51+ // Error to be thrown in case the final ping body is larger than MAX_PING_BODY_SIZE.
52+ class PingBodyOverflowError extends Error {
53+ constructor ( message ?: string ) {
54+ super ( message ) ;
55+ this . name = "PingBodyOverflow" ;
56+ }
57+ }
58+
3659/**
3760 * A ping uploader. Manages a queue of pending pings to upload.
3861 *
@@ -57,13 +80,13 @@ class PingUploader implements PingsDatabaseObserver {
5780
5881 private readonly platformInfo : PlatformInfo ;
5982 private readonly serverEndpoint : string ;
60- private readonly pingsDatabase : PingsDatabase ;
61-
62- // Whether or not Glean was initialized, as reported by the
63- // singleton object.
64- private initialized = false ;
6583
66- constructor ( config : Configuration , platform : Platform , pingsDatabase : PingsDatabase ) {
84+ constructor (
85+ config : Configuration ,
86+ platform : Platform ,
87+ private readonly pingsDatabase : PingsDatabase ,
88+ private readonly policy = new Policy
89+ ) {
6790 this . queue = [ ] ;
6891 this . status = PingUploaderStatus . Idle ;
6992 // Initialize the ping uploader with either the platform defaults or a custom
@@ -74,17 +97,6 @@ class PingUploader implements PingsDatabaseObserver {
7497 this . pingsDatabase = pingsDatabase ;
7598 }
7699
77- /**
78- * Signals that initialization of Glean was completed.
79- *
80- * This is required in order to not depend on the Glean object.
81- *
82- * @param state An optional state to set the initialization status to.
83- */
84- setInitialized ( state ?: boolean ) : void {
85- this . initialized = state ?? true ;
86- }
87-
88100 /**
89101 * Enqueues a new ping at the end of the line.
90102 *
@@ -139,21 +151,33 @@ class PingUploader implements PingsDatabaseObserver {
139151 } ;
140152
141153 const stringifiedBody = JSON . stringify ( ping . payload ) ;
154+ // We prefer using `strToU8` instead of TextEncoder directly,
155+ // because it will polyfill TextEncoder if it's not present in the environment.
156+ // Environments that don't provide TextEncoder are IE and most importantly QML.
157+ const encodedBody = strToU8 ( stringifiedBody ) ;
158+
159+ let finalBody : string | Uint8Array ;
160+ let bodySizeInBytes : number ;
142161 try {
143- const compressedBody = gzipSync ( strToU8 ( stringifiedBody ) ) ;
162+ finalBody = gzipSync ( encodedBody ) ;
163+ bodySizeInBytes = finalBody . length ;
144164 headers [ "Content-Encoding" ] = "gzip" ;
145- headers [ "Content-Length" ] = compressedBody . length . toString ( ) ;
146- return {
147- headers,
148- payload : compressedBody
149- } ;
150165 } catch {
151- headers [ "Content-Length" ] = stringifiedBody . length . toString ( ) ;
152- return {
153- headers,
154- payload : stringifiedBody
155- } ;
166+ finalBody = stringifiedBody ;
167+ bodySizeInBytes = encodedBody . length ;
168+ }
169+
170+ if ( bodySizeInBytes > this . policy . maxPingBodySize ) {
171+ throw new PingBodyOverflowError (
172+ `Body for ping ${ ping . identifier } exceeds ${ this . policy . maxPingBodySize } bytes. Discarding.`
173+ ) ;
156174 }
175+
176+ headers [ "Content-Length" ] = bodySizeInBytes . toString ( ) ;
177+ return {
178+ headers,
179+ payload : finalBody
180+ } ;
157181 }
158182
159183 /**
@@ -163,7 +187,7 @@ class PingUploader implements PingsDatabaseObserver {
163187 * @returns The status number of the response or `undefined` if unable to attempt upload.
164188 */
165189 private async attemptPingUpload ( ping : QueuedPing ) : Promise < UploadResult > {
166- if ( ! this . initialized ) {
190+ if ( ! Context . initialized ) {
167191 log (
168192 LOG_TAG ,
169193 "Attempted to upload a ping, but Glean is not initialized yet. Ignoring." ,
@@ -172,17 +196,24 @@ class PingUploader implements PingsDatabaseObserver {
172196 return { result : UploadResultStatus . RecoverableFailure } ;
173197 }
174198
175- const finalPing = await this . preparePingForUpload ( ping ) ;
176- const result = await this . uploader . post (
177- // We are sure that the applicationId is not `undefined` at this point,
178- // this function is only called when submitting a ping
179- // and that function return early when Glean is not initialized.
180- `${ this . serverEndpoint } ${ ping . path } ` ,
181- finalPing . payload ,
182- finalPing . headers
183- ) ;
184-
185- return result ;
199+ try {
200+ const finalPing = await this . preparePingForUpload ( ping ) ;
201+ return await this . uploader . post (
202+ // We are sure that the applicationId is not `undefined` at this point,
203+ // this function is only called when submitting a ping
204+ // and that function return early when Glean is not initialized.
205+ `${ this . serverEndpoint } ${ ping . path } ` ,
206+ finalPing . payload ,
207+ finalPing . headers
208+ ) ;
209+ } catch ( e ) {
210+ log ( LOG_TAG , [ "Error trying to build ping request:" , e ] , LoggingLevel . Warn ) ;
211+ // An unrecoverable failure will make sure the offending ping is removed from the queue and
212+ // deleted from the database, which is what we want here.
213+ return {
214+ result : UploadResultStatus . UnrecoverableFailure
215+ } ;
216+ }
186217 }
187218
188219 /**
@@ -232,7 +263,7 @@ class PingUploader implements PingsDatabaseObserver {
232263 if ( result === UploadResultStatus . UnrecoverableFailure || ( status && status >= 400 && status < 500 ) ) {
233264 log (
234265 LOG_TAG ,
235- `Unrecoverable upload failure while attempting to send ping ${ identifier } . Error was ${ status ?? "no status" } .` ,
266+ `Unrecoverable upload failure while attempting to send ping ${ identifier } . Error was: ${ status ?? "no status" } .` ,
236267 LoggingLevel . Warn
237268 ) ;
238269 await this . pingsDatabase . deletePing ( identifier ) ;
@@ -261,7 +292,7 @@ class PingUploader implements PingsDatabaseObserver {
261292 this . enqueuePing ( nextPing ) ;
262293 }
263294
264- if ( retries >= 3 ) {
295+ if ( retries >= this . policy . maxRecoverableFailures ) {
265296 log (
266297 LOG_TAG ,
267298 "Reached maximum recoverable failures for the current uploading window. You are done." ,
0 commit comments