Skip to content

Commit 0f369b5

Browse files
author
Beatriz Rizental
authored
Bug 1677440 - Implement a ping uploader (#24)
1 parent 693c2f5 commit 0f369b5

File tree

10 files changed

+879
-74
lines changed

10 files changed

+879
-74
lines changed

src/config.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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+
/**
6+
* The Configuration class describes how to configure Glean.
7+
*/
8+
interface Configuration {
9+
// The build identifier generated by the CI system (e.g. "1234/A").
10+
readonly appBuild?: string,
11+
// The user visible version string fro the application running Glean.js.
12+
readonly appDisplayVersion?: string,
13+
// The server pings are sent to.
14+
readonly serverEndpoint?: URL
15+
}
16+
17+
export default Configuration;

src/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,6 @@ export const CLIENT_INFO_STORAGE = "glean_client_info";
3232

3333
// We will set the client id to this client id in case upload is disabled.
3434
export const KNOWN_CLIENT_ID = "c0ffeec0-ffee-c0ff-eec0-ffeec0ffeec0";
35+
36+
// The default server pings are sent to.
37+
export const DEFAULT_TELEMETRY_ENDPOINT = "https://incoming.telemetry.mozilla.org";

src/glean.ts

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

5+
import { DEFAULT_TELEMETRY_ENDPOINT } from "./constants";
6+
import Configuration from "config";
57
import MetricsDatabase from "metrics/database";
68
import PingsDatabase from "pings/database";
9+
import PingUploader from "upload";
710
import { isUndefined, sanitizeApplicationId } from "utils";
811
import { CoreMetrics } from "internal_metrics";
912
import { Lifetime } from "metrics";
@@ -23,9 +26,12 @@ class Glean {
2326
private _initialized: boolean;
2427
// Instances of Glean's core metrics.
2528
private _coreMetrics: CoreMetrics;
29+
// The ping uploader.
30+
private _pingUploader: PingUploader
2631

2732
// Properties that will only be set on `initialize`.
2833
private _applicationId?: string;
34+
private _serverEndpoint?: URL;
2935

3036
private constructor() {
3137
if (!isUndefined(Glean._instance)) {
@@ -34,16 +40,21 @@ class Glean {
3440
Use Glean.instance instead to access the Glean singleton.`);
3541
}
3642

43+
this._pingUploader = new PingUploader();
3744
this._coreMetrics = new CoreMetrics();
3845
this._initialized = false;
3946
this._db = {
4047
metrics: new MetricsDatabase(),
41-
pings: new PingsDatabase()
48+
pings: new PingsDatabase(this._pingUploader)
4249
};
4350
// Temporarily setting this to true always, until Bug 1677444 is resolved.
4451
this._uploadEnabled = true;
4552
}
4653

54+
private static get pingUploader(): PingUploader {
55+
return Glean.instance._pingUploader;
56+
}
57+
4758
private static get coreMetrics(): CoreMetrics {
4859
return Glean.instance._coreMetrics;
4960
}
@@ -60,19 +71,24 @@ class Glean {
6071
* Initialize Glean. This method should only be called once, subsequent calls will be no-op.
6172
*
6273
* @param applicationId The application ID (will be sanitized during initialization).
63-
* @param appBuild The build identifier generated by the CI system (e.g. "1234/A").
64-
* @param appDisplayVersion The user visible version string fro the application running Glean.js.
74+
* @param config Glean configuration options.
6575
*/
66-
static async initialize(applicationId: string, appBuild?: string, appDisplayVersion?: string): Promise<void> {
76+
static async initialize(applicationId: string, config?: Configuration): Promise<void> {
6777
if (Glean.instance._initialized) {
6878
console.warn("Attempted to initialize Glean, but it has already been initialized. Ignoring.");
6979
return;
7080
}
7181

7282
Glean.instance._applicationId = sanitizeApplicationId(applicationId);
73-
await Glean.coreMetrics.initialize(appBuild, appDisplayVersion);
83+
Glean.instance._serverEndpoint = config
84+
? config.serverEndpoint : new URL(DEFAULT_TELEMETRY_ENDPOINT);
85+
await Glean.coreMetrics.initialize(config?.appBuild, config?.appDisplayVersion);
7486

7587
Glean.instance._initialized = true;
88+
89+
await Glean.pingUploader.scanPendingPings();
90+
// Even though this returns a promise, there is no need to block on it returning.
91+
Glean.pingUploader.triggerUpload();
7692
}
7793

7894
/**
@@ -119,6 +135,14 @@ class Glean {
119135
return Glean.instance._applicationId;
120136
}
121137

138+
static get serverEndpoint(): URL | undefined {
139+
if (!Glean.instance._initialized) {
140+
console.error("Attempted to access the Glean.serverEndpoint before Glean was initialized.");
141+
}
142+
143+
return Glean.instance._serverEndpoint;
144+
}
145+
122146
/**
123147
* **Test-only API**
124148
*
@@ -139,19 +163,23 @@ class Glean {
139163
* TODO: Only allow this function to be called on test mode (depends on Bug 1682771).
140164
*
141165
* @param applicationId The application ID (will be sanitized during initialization).
142-
* @param optionalInitializeArgs Optional arguments to be passed to `initialize`.
166+
* @param config Glean configuration options.
143167
*/
144-
static async testRestGlean(applicationId: string, ...optionalInitializeArgs: never[]): Promise<void> {
168+
static async testRestGlean(applicationId: string, config?: Configuration): Promise<void> {
145169
// Get back to an uninitialized state.
146170
Glean.instance._initialized = false;
147171
// Reset upload enabled state, not to inerfere with other tests.
148172
Glean.uploadEnabled = true;
173+
174+
// Stop ongoing jobs and clear pending pings queue.
175+
await Glean.pingUploader.clearPendingPingsQueue();
176+
149177
// Clear the databases.
150178
await Glean.metricsDatabase.clearAll();
151179
await Glean.pingsDatabase.clearAll();
152180

153-
// Initialize Glean.
154-
await Glean.initialize(applicationId, ...optionalInitializeArgs);
181+
// Re-Initialize Glean.
182+
await Glean.initialize(applicationId, config);
155183
}
156184
}
157185

src/pings/database.ts

Lines changed: 84 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import { Metrics as MetricsPayload } from "metrics/database";
66
import { Store } from "storage";
77
import PersistentStore from "storage/persistent";
8-
import { JSONObject } from "utils";
8+
import { isObject, isJSONValue, JSONObject, isString } from "utils";
99

1010
export interface PingInfo extends JSONObject {
1111
seq: number,
@@ -43,14 +43,46 @@ export interface PingPayload extends JSONObject {
4343
events?: JSONObject,
4444
}
4545

46+
export interface PingInternalRepresentation extends JSONObject {
47+
path: string,
48+
payload: PingPayload,
49+
headers?: Record<string, string>
50+
}
51+
4652
/**
47-
* Debug headers to be added to a ping.
53+
* Checks whether or not `v` is in the correct ping internal representation
54+
*
55+
* @param v The value to verify.
56+
*
57+
* @returns A special Typescript value (which compiles down to a boolean)
58+
* stating whether `v` is in the correct ping internal representation.
4859
*/
49-
export interface PingHeaders extends JSONObject {
50-
"X-Debug-Id"?: string,
51-
"X-Source-Tags"?: string,
60+
export function isValidPingInternalRepresentation(v: unknown): v is PingInternalRepresentation {
61+
if (isObject(v) && (Object.keys(v).length === 2 || Object.keys(v).length === 3)) {
62+
const hasValidPath = "path" in v && isString(v.path);
63+
const hasValidPayload = "payload" in v && isJSONValue(v.payload) && isObject(v.payload);
64+
const hasValidHeaders = (!("headers" in v)) || (isJSONValue(v.headers) && isObject(v.headers));
65+
if (!hasValidPath || !hasValidPayload || !hasValidHeaders) {
66+
return false;
67+
}
68+
return true;
69+
}
70+
return false;
5271
}
5372

73+
/**
74+
* An interface to be implemented by classes that wish to observe the pings database.
75+
*/
76+
export interface Observer {
77+
/**
78+
* Updates an observer about a new ping of a given id
79+
* that has just been recorded to the pings database.
80+
*
81+
* @param identifier The id of the ping that was just recorded.
82+
* @param ping An object containing the newly recorded ping path, payload and optionally headers.
83+
*/
84+
update(identifier: string, ping: PingInternalRepresentation): void;
85+
}
5486

5587
/**
5688
* The pings database is an abstraction layer on top of the underlying storage.
@@ -67,9 +99,11 @@ export interface PingHeaders extends JSONObject {
6799
*/
68100
class PingsDatabase {
69101
private store: Store;
102+
private observer?: Observer;
70103

71-
constructor() {
104+
constructor(observer?: Observer) {
72105
this.store = new PersistentStore("pings");
106+
this.observer = observer;
73107
}
74108

75109
/**
@@ -84,15 +118,51 @@ class PingsDatabase {
84118
path: string,
85119
identifier: string,
86120
payload: PingPayload,
87-
headers?: PingHeaders
121+
headers?: Record<string, string>
88122
): Promise<void> {
89-
await this.store.update([identifier], () => {
90-
const base = {
91-
path,
92-
payload
93-
};
94-
return headers ? { ...base, headers } : base;
95-
});
123+
const ping: PingInternalRepresentation = {
124+
path,
125+
payload
126+
};
127+
128+
if (headers) {
129+
ping.headers = headers;
130+
}
131+
132+
await this.store.update([identifier], () => ping);
133+
134+
// Notify the observer that a new ping has been added to the pings database.
135+
this.observer && this.observer.update(identifier, ping);
136+
}
137+
138+
/**
139+
* Deletes a specific ping from the database.
140+
*
141+
* @param identifier The identififer of the ping to delete.
142+
*/
143+
async deletePing(identifier: string): Promise<void> {
144+
await this.store.delete([identifier]);
145+
}
146+
147+
/**
148+
* Gets all pings from the pings database. Deletes any data in unexpected format that is found.
149+
*
150+
* @returns List of all currently stored pings.
151+
*/
152+
async getAllPings(): Promise<{ [id: string]: PingInternalRepresentation }> {
153+
const allStoredPings = await this.store._getWholeStore();
154+
const finalPings: { [ident: string]: PingInternalRepresentation } = {};
155+
for (const identifier in allStoredPings) {
156+
const ping = allStoredPings[identifier];
157+
if (isValidPingInternalRepresentation(ping)) {
158+
finalPings[identifier] = ping;
159+
} else {
160+
console.warn("Unexpected data found in pings database. Deleting.");
161+
await this.store.delete([identifier]);
162+
}
163+
}
164+
165+
return finalPings;
96166
}
97167

98168
/**

src/upload/adapter/index.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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+
/**
6+
* Uploader interface, actualy uploading logic varies per platform.
7+
*/
8+
export interface UploadAdapter {
9+
/**
10+
* Makes a POST request to a given url, with the given headers and body.
11+
*
12+
* @param url The URL to make the POST request
13+
* @param body The stringified body of this post request
14+
* @param headers Optional header to include in the request
15+
*
16+
* @returns The status code of the response.
17+
*/
18+
post(url: URL, body: string, headers?: Record<string, string>): Promise<number>;
19+
}
20+
21+
// Default export for tests sake.
22+
const MockUploadAdapter: UploadAdapter = {
23+
post(): Promise<number> {
24+
return Promise.resolve(200);
25+
}
26+
};
27+
28+
export default MockUploadAdapter;

0 commit comments

Comments
 (0)