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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

[Full changelog](https://github.com/mozilla/glean.js/compare/v0.28.0...main)

* [#1006](https://github.com/mozilla/glean.js/pull/1006): Implement the rate metric.

# v0.28.0 (2021-12-08)

[Full changelog](https://github.com/mozilla/glean.js/compare/v0.27.0...v0.28.0)
Expand Down
201 changes: 201 additions & 0 deletions glean/src/core/metrics/types/rate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
/* 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 { CommonMetricData } from "../index.js";
import { MetricType } from "../index.js";
import { Context } from "../../context.js";
import { Metric } from "../metric.js";
import { testOnly } from "../../utils.js";
import { isNumber, isObject } from "../../utils.js";
import type { JSONValue } from "../../utils.js";
import { ErrorType } from "../../error/error_type.js";

const LOG_TAG = "core.metrics.RateMetricType";

export type Rate = {
numerator: number,
denominator: number
};

export class RateMetric extends Metric<Rate, Rate> {
constructor(v: unknown) {
super(v);
}

get numerator(): number {
return this._inner.numerator;
}

get denominator(): number {
return this._inner.denominator;
}

validate(v: unknown): v is Rate {
if (!isObject(v) || Object.keys(v).length !== 2) {
return false;
}

const numeratorVerification = "numerator" in v && isNumber(v.numerator) && v.numerator >= 0;
const denominatorVerification = "denominator" in v && isNumber(v.denominator) && v.denominator >= 0;
return numeratorVerification && denominatorVerification;
}

payload(): Rate {
return {
numerator: this._inner.numerator,
denominator: this._inner.denominator
};
}
}

/*
* A rate metric.
*
* Used to determine the proportion of things via two counts:
* * A numerator defining the amount of times something happened,
* * A denominator counting the amount of times someting could have happened.
*
* Both numerator and denominator can only be incremented, not decremented.
*/
class RateMetricType extends MetricType {
constructor(meta: CommonMetricData) {
super("rate", meta);
}

/**
* Increases the numerator by amount.
*
* # Note
*
* Records an `InvalidValue` error if the `amount` is negative.
*
* @param amount The amount to increase by. Should be non-negative.
*/
addToNumerator(amount: number): void {
Context.dispatcher.launch(async () => {
if (!this.shouldRecord(Context.uploadEnabled)) {
return;
}

if (amount < 0) {
await Context.errorManager.record(
this,
ErrorType.InvalidValue,
`Added negative value ${amount} to numerator.`
);
return;
}

const transformFn = ((amount) => {
return (v?: JSONValue): RateMetric => {
let metric: RateMetric;
let result: number;
try {
metric = new RateMetric(v);
result = metric.numerator + amount;
} catch {
metric = new RateMetric({
numerator: amount,
denominator: 0
});
result = amount;
}

if (result > Number.MAX_SAFE_INTEGER) {
result = Number.MAX_SAFE_INTEGER;
}

metric.set({
numerator: result,
denominator: metric.denominator
});
return metric;
};
})(amount);

await Context.metricsDatabase.transform(this, transformFn);
});
}

/**
* Increases the denominator by amount.
*
* # Note
*
* Records an `InvalidValue` error if the `amount` is negative.
*
* @param amount The amount to increase by. Should be non-negative.
*/
addToDenominator(amount: number): void {
Context.dispatcher.launch(async () => {
if (!this.shouldRecord(Context.uploadEnabled)) {
return;
}

if (amount < 0) {
await Context.errorManager.record(
this,
ErrorType.InvalidValue,
`Added negative value ${amount} to denominator.`
);
return;
}

const transformFn = ((amount) => {
return (v?: JSONValue): RateMetric => {
let metric: RateMetric;
let result: number;
try {
metric = new RateMetric(v);
result = metric.denominator + amount;
} catch {
metric = new RateMetric({
numerator: 0,
denominator: amount
});
result = amount;
}

if (result > Number.MAX_SAFE_INTEGER) {
result = Number.MAX_SAFE_INTEGER;
}

metric.set({
numerator: metric.numerator,
denominator: result
});
return metric;
};
})(amount);

await Context.metricsDatabase.transform(this, transformFn);
});
}

/**
* Test-only API.**
*
* Gets the currently stored value as an object.
*
* # Note
*
* This function will return the Rate for convenience.
*
* This doesn't clear the stored value.
*
* @param ping the ping from which we want to retrieve this metrics value from.
* Defaults to the first value in `sendInPings`.
* @returns The value found in storage or `undefined` if nothing was found.
*/
@testOnly(LOG_TAG)
async testGetValue(ping: string = this.sendInPings[0]): Promise<Rate | undefined> {
let metric: Rate | undefined;
await Context.dispatcher.testLaunch(async () => {
metric = await Context.metricsDatabase.getMetric<Rate>(ping, this);
});
return metric;
}
}

export default RateMetricType;
2 changes: 2 additions & 0 deletions glean/src/core/metrics/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { BooleanMetric } from "./types/boolean.js";
import { CounterMetric } from "./types/counter.js";
import { DatetimeMetric } from "./types/datetime.js";
import { QuantityMetric } from "./types/quantity.js";
import { RateMetric } from "./types/rate.js";
import { StringMetric } from "./types/string.js";
import { StringListMetric } from "./types/string_list.js";
import { TextMetric } from "./types/text.js";
Expand All @@ -31,6 +32,7 @@ const METRIC_MAP: {
"labeled_counter": LabeledMetric,
"labeled_string": LabeledMetric,
"quantity": QuantityMetric,
"rate": RateMetric,
"string": StringMetric,
"string_list": StringListMetric,
"text": TextMetric,
Expand Down
2 changes: 2 additions & 0 deletions glean/src/index/qt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import DatetimeMetricType from "../core/metrics/types/datetime.js";
import EventMetricType from "../core/metrics/types/event.js";
import LabeledMetricType from "../core/metrics/types/labeled.js";
import QuantityMetricType from "../core/metrics/types/quantity.js";
import RateMetricType from "../core/metrics/types/rate.js";
import StringMetricType from "../core/metrics/types/string.js";
import StringListMetricType from "../core/metrics/types/string_list.js";
import TextMetricType from "../core/metrics/types/text.js";
Expand All @@ -35,6 +36,7 @@ export default {
EventMetricType,
LabeledMetricType,
QuantityMetricType,
RateMetricType,
StringMetricType,
StringListMetricType,
TimespanMetricType,
Expand Down
13 changes: 13 additions & 0 deletions glean/tests/integration/schema/metrics.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,19 @@ for_testing:
send_in_pings:
- testing
unit: sample
rate:
type: rate
description: |
Sample rate metric
bugs:
- https://bugzilla.mozilla.org/000000
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=000000#c3
notification_emails:
- me@mozilla.com
expires: never
send_in_pings:
- testing
url:
type: url
description: |
Expand Down
Loading