Skip to content

Commit 29fc7be

Browse files
author
brizental
committed
Implement PingType and ping maker
1 parent 2d923da commit 29fc7be

File tree

8 files changed

+453
-5
lines changed

8 files changed

+453
-5
lines changed

src/constants.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
// eslint-disable-next-line @typescript-eslint/no-var-requires
6+
const { version } = require("../package.json");
7+
8+
export const GLEAN_SCHEMA_VERSION = 1;
9+
10+
// The version for the current build of Glean.js
11+
export const GLEAN_VERSION = version;
12+
13+
// The name of a "ping" that will include Glean ping_info metrics,
14+
// such as ping sequence numbers.
15+
//
16+
// Note that this is not really a ping,
17+
// but a neat way to gather metrics in the metrics database.
18+
export const PING_INFO_STORAGE = "glean_ping_info";
19+
20+
// The name of a "ping" that will include the Glean client_info metrics,
21+
// such as ping sequence numbers.
22+
//
23+
// Note that this is not really a ping,
24+
// but a neat way to gather metrics in the metrics database.
25+
export const CLIENT_INFO_STORAGE = "glean_client_info";

src/glean.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ class Glean {
2525
}
2626
// Whether or not to record metrics.
2727
private _uploadEnabled: boolean;
28+
// The time the Glean object was initialized.
29+
private _startTime: Date;
2830
// Whether or not Glean has been initialized.
2931
private _initialized: boolean;
3032

@@ -38,6 +40,7 @@ class Glean {
3840
Use Glean.instance instead to access the Glean singleton.`);
3941
}
4042

43+
this._startTime = new Date();
4144
this._initialized = false;
4245
this._db = {
4346
metrics: new MetricsDatabase(),
@@ -96,6 +99,15 @@ class Glean {
9699
Glean.instance._uploadEnabled = value;
97100
}
98101

102+
/**
103+
* Gets the time this Glean instance was created.
104+
*
105+
* @returns The Date object representing the time this Glean instance was created.
106+
*/
107+
static get startTime(): Date {
108+
return Glean.instance._startTime;
109+
}
110+
99111
static get applicationId(): string | undefined {
100112
if (!Glean.instance._initialized) {
101113
console.error("Attempted to access the Glean.applicationId before Glean was initialized.");

src/metrics/types/datetime.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ export class DatetimeMetric extends Metric<DatetimeInternalRepresentation, strin
4040
super(v);
4141
}
4242

43+
static fromDate(v: Date, timeUnit: TimeUnit): DatetimeMetric {
44+
return new DatetimeMetric({
45+
timeUnit,
46+
timezone: v.getTimezoneOffset(),
47+
date: v.toISOString()
48+
});
49+
}
50+
4351
/**
4452
* Gets the datetime data as a Date object.
4553
*
@@ -198,11 +206,7 @@ class DatetimeMetricType extends MetricType {
198206
break;
199207
}
200208

201-
const metric = new DatetimeMetric({
202-
timeUnit: this.timeUnit,
203-
timezone: value.getTimezoneOffset(),
204-
date: value.toISOString(),
205-
});
209+
const metric = DatetimeMetric.fromDate(value, this.timeUnit);
206210
await Glean.metricsDatabase.record(this, metric);
207211
}
208212

src/pings/index.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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+
import { v4 as UUIDv4 } from "uuid";
6+
7+
import collectAndStorePing from "pings/maker";
8+
import Glean from "glean";
9+
10+
/**
11+
* Stores information about a ping.
12+
*
13+
* This is required so that given metric data queued on disk we can send
14+
* pings with the correct settings, e.g. whether it has a client_id.
15+
*/
16+
class PingType {
17+
/**
18+
* Creates a new ping type for the given name,
19+
* whether to include the client ID and whether to send this ping empty.
20+
*
21+
* @param name The name of the ping.
22+
* @param includeClientId Whether to include the client ID in the assembled ping when submitting.
23+
* @param sendIfEmtpy Whether the ping should be sent empty or not.
24+
* @param reasonCodes The valid reason codes for this ping.
25+
*/
26+
constructor (
27+
readonly name: string,
28+
readonly includeClientId: boolean,
29+
readonly sendIfEmtpy: boolean,
30+
readonly reasonCodes: string[]
31+
) {}
32+
33+
/**
34+
* Collects and submits a ping for eventual uploading.
35+
*
36+
* The ping content is assembled as soon as possible, but upload is not
37+
* guaranteed to happen immediately, as that depends on the upload policies.
38+
*
39+
* If the ping currently contains no content, it will not be sent,
40+
* unless it is configured to be sent if empty.
41+
*
42+
* @param reason The reason the ping was triggered. Included in the
43+
* `ping_info.reason` part of the payload.
44+
*
45+
* @returns Whether or not the ping was successfully submitted.
46+
*/
47+
async submit(reason?: string): Promise<boolean> {
48+
if (!Glean.uploadEnabled) {
49+
console.info("Glean disabled: not submitting any pings.");
50+
return false;
51+
}
52+
53+
let correctedReason = reason;
54+
if (reason && !this.reasonCodes.includes(reason)) {
55+
console.error(`Invalid reason code ${reason} from ${this.name}. Ignoring.`);
56+
correctedReason = undefined;
57+
}
58+
59+
const identifier = UUIDv4();
60+
await collectAndStorePing(identifier, this, correctedReason);
61+
return true;
62+
}
63+
}
64+
65+
export default PingType;

src/pings/maker.ts

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
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+
import { GLEAN_SCHEMA_VERSION, GLEAN_VERSION, PING_INFO_STORAGE, CLIENT_INFO_STORAGE } from "../constants";
6+
import CounterMetricType, { CounterMetric } from "metrics/types/counter";
7+
import DatetimeMetricType, { DatetimeMetric } from "metrics/types/datetime";
8+
import { Lifetime } from "metrics";
9+
import TimeUnit from "metrics/time_unit";
10+
import { ClientInfo, PingInfo, PingHeaders, PingPayload } from "pings/database";
11+
import PingType from "pings";
12+
import Glean from "glean";
13+
14+
/**
15+
* Gets, and then increments, the sequence number for a given ping.
16+
*
17+
* @param ping The ping for which we want to get the sequence number.
18+
*
19+
* @returns The current number (before incrementing).
20+
*/
21+
export async function getSequenceNumber(ping: PingType): Promise<number> {
22+
const seq = new CounterMetricType({
23+
name: `${ping.name}#sequence`,
24+
sendInPings: [PING_INFO_STORAGE],
25+
lifetime: Lifetime.User,
26+
disabled: false
27+
});
28+
29+
const currentSeqData = await Glean.metricsDatabase.getMetric(PING_INFO_STORAGE, seq);
30+
await seq.add(1);
31+
32+
if (currentSeqData) {
33+
// Creating a new counter metric validates that the metric stored is actually a number.
34+
// When we `add` we deal with getting rid of that number from storage,
35+
// no need to worry about that here.
36+
try {
37+
const metric = new CounterMetric(currentSeqData);
38+
return metric.payload();
39+
} catch(e) {
40+
console.warn(`Unexpected value found for sequence number in ping ${ping.name}. Ignoring.`);
41+
}
42+
}
43+
44+
return 0;
45+
}
46+
47+
/**
48+
* Gets the formatted start and end times for this ping
49+
* and updates for the next ping.
50+
*
51+
* @param ping The ping for which we want to get the times.
52+
*
53+
* @returns An object containing start and times in their payload format.
54+
*/
55+
export async function getStartEndTimes(ping: PingType): Promise<{ startTime: string, endTime: string }> {
56+
const start = new DatetimeMetricType({
57+
name: `${ping.name}#start`,
58+
sendInPings: [PING_INFO_STORAGE],
59+
lifetime: Lifetime.User,
60+
disabled: false
61+
}, TimeUnit.Minute);
62+
63+
// "startTime" is the time the ping was generated the last time.
64+
// If not available, we use the date the Glean object was initialized.
65+
const startTimeData = await Glean.metricsDatabase.getMetric(PING_INFO_STORAGE, start);
66+
let startTime: DatetimeMetric;
67+
if (startTimeData) {
68+
startTime = new DatetimeMetric(startTimeData);
69+
} else {
70+
startTime = DatetimeMetric.fromDate(Glean.startTime, TimeUnit.Minute);
71+
}
72+
73+
// Update the start time with the current time.
74+
const endTimeData = new Date();
75+
await start.set(endTimeData);
76+
const endTime = DatetimeMetric.fromDate(endTimeData, TimeUnit.Minute);
77+
78+
return {
79+
startTime: startTime.payload(),
80+
endTime: endTime.payload()
81+
};
82+
}
83+
84+
/**
85+
* Builds the `ping_info` section of a ping.
86+
*
87+
* @param ping The ping to build the `ping_info` section for.
88+
* @param reason The reason for submitting this ping.
89+
*
90+
* @returns The final `ping_info` section in its payload format.
91+
*/
92+
export async function buildPingInfoSection(ping: PingType, reason?: string): Promise<PingInfo> {
93+
const seq = await getSequenceNumber(ping);
94+
const { startTime, endTime } = await getStartEndTimes(ping);
95+
96+
const pingInfo: PingInfo = {
97+
seq,
98+
start_time: startTime,
99+
end_time: endTime
100+
};
101+
102+
if (reason) {
103+
pingInfo.reason = reason;
104+
}
105+
106+
return pingInfo;
107+
}
108+
109+
/**
110+
* Builds the `client_info` section of a ping.
111+
*
112+
* @param ping The ping to build the `client_info` section for.
113+
*
114+
* @returns The final `client_info` section in its payload format.
115+
*/
116+
export async function buildClientInfoSection(ping: PingType): Promise<ClientInfo> {
117+
let clientInfo = await Glean.metricsDatabase.getPingMetrics(CLIENT_INFO_STORAGE, true);
118+
if (!clientInfo) {
119+
// TODO: Watch Bug 1685705 and change behaviour in here accordingly.
120+
console.warn("Empty client info data. Will submit anyways.");
121+
clientInfo = {};
122+
}
123+
124+
let finalClientInfo: ClientInfo = {
125+
"telemetry_sdk_build": GLEAN_VERSION
126+
};
127+
for (const metricType in clientInfo) {
128+
finalClientInfo = { ...finalClientInfo, ...clientInfo[metricType] };
129+
}
130+
131+
if (!ping.includeClientId) {
132+
delete finalClientInfo["client_id"];
133+
}
134+
135+
return finalClientInfo;
136+
}
137+
138+
/**
139+
* Gathers all the headers to be included to the final ping request.
140+
*
141+
* This guarantees that if headers are disabled after the ping collection,
142+
* ping submission will still contain the desired headers.
143+
*
144+
* The current headers gathered here are:
145+
* - [X-Debug-Id]
146+
* - [X-Source-Tags]
147+
*
148+
* @returns An object with the headers to include
149+
* or `undefined` if there are no headers to include.
150+
*/
151+
export async function getPingHeaders(): Promise<PingHeaders | undefined> {
152+
// TODO: Returning nothing for now until Bug 1685718 is resolved.
153+
return;
154+
}
155+
156+
/**
157+
* Collects a snapshot for the given ping from storage and attach required meta information.
158+
*
159+
* @param ping The ping to collect for.
160+
* @param reason An optional reason code to include in the ping.
161+
*
162+
* @returns A fully assembled JSON representation of the ping payload.
163+
* If there is no data stored for the ping, `undefined` is returned.
164+
*/
165+
export async function collectPing(ping: PingType, reason?: string): Promise<PingPayload | undefined> {
166+
const metricsData = await Glean.metricsDatabase.getPingMetrics(ping.name, true);
167+
if (!metricsData && !ping.sendIfEmtpy) {
168+
console.info(`Storage for ${ping.name} empty. Bailing out.`);
169+
return;
170+
} else if (!metricsData) {
171+
console.info(`Storage for ${ping.name} empty. Ping will still be sent.`);
172+
}
173+
174+
const metrics = metricsData ? { metrics: metricsData } : {};
175+
const pingInfo = await buildPingInfoSection(ping, reason);
176+
const clientInfo = await buildClientInfoSection(ping);
177+
return {
178+
...metrics,
179+
ping_info: pingInfo,
180+
client_info: clientInfo,
181+
};
182+
}
183+
184+
/**
185+
* Build a pings submition path.
186+
*
187+
* @param identifier The pings UUID identifier.
188+
* @param ping The ping to build a path for.
189+
*
190+
* @returns The final submission path.
191+
*/
192+
function makePath(identifier: string, ping: PingType): string {
193+
return `/submit/${Glean.applicationId}/${ping.name}/${GLEAN_SCHEMA_VERSION}/${identifier}`;
194+
}
195+
196+
/**
197+
* Collects and stores a ping on the pings database.
198+
*
199+
* @param identifier The pings UUID identifier.
200+
* @param ping The ping to submit.
201+
* @param reason An optional reason code to include in the ping.
202+
*
203+
* @returns A promise that is resolved once collection and storing is done.
204+
*/
205+
export async function collectAndStorePing(identifier: string, ping: PingType, reason?: string): Promise<void> {
206+
const payload = await collectPing(ping, reason);
207+
if (!payload) {
208+
return;
209+
}
210+
const headers = await getPingHeaders();
211+
return Glean.pingsDatabase.recordPing(
212+
makePath(identifier, ping),
213+
identifier,
214+
payload,
215+
headers
216+
);
217+
}
218+
219+
export default collectAndStorePing;

0 commit comments

Comments
 (0)