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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
* Implement glean-noop as a feature of glean-sym ([#3541](https://github.com/mozilla/glean/pull/3541))
* Support pings ([#3544](https://github.com/mozilla/glean/pull/3544))
* Implement the event metric ([#3534](https://github.com/mozilla/glean/pull/3534))
* iOS
* Implement the custom distribution metric type ([#3572](https://github.com/mozilla/glean/pull/3572))
* Python
* Implement the custom distribution metric type ([#3572](https://github.com/mozilla/glean/pull/3572))

# v69.0.0 (2026-06-22)

Expand Down
106 changes: 98 additions & 8 deletions docs/user/reference/metrics/custom_distribution.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,25 @@ Graphics.INSTANCE.checkerboardPeak().accumulateSamples(listOf(23));
```

</div>
<div data-lang="Swift" class="tab"></div>
<div data-lang="Python" class="tab"></div>
<div data-lang="Swift" class="tab">

```Swift
import Glean

Graphics.checkerboardPeak.accumulateSamples([23])
```

</div>
<div data-lang="Python" class="tab">

```Python
from glean import load_metrics
metrics = load_metrics("metrics.yaml")

metrics.graphics.checkerboard_peak.accumulate_samples([23])
```

</div>
<div data-lang="Rust" class="tab">

```Rust
Expand Down Expand Up @@ -111,8 +128,25 @@ Graphics.INSTANCE.checkerboardPeak().accumulateSingleSample(23);
```

</div>
<div data-lang="Swift" class="tab"></div>
<div data-lang="Python" class="tab"></div>
<div data-lang="Swift" class="tab">

```Swift
import Glean

Graphics.checkerboardPeak.accumulateSingleSample(23)
```

</div>
<div data-lang="Python" class="tab">

```Python
from glean import load_metrics
metrics = load_metrics("metrics.yaml")

metrics.graphics.checkerboard_peak.accumulate_single_sample(23)
```

</div>
<div data-lang="Rust" class="tab">

```Rust
Expand Down Expand Up @@ -258,8 +292,42 @@ assertEquals(1L, snapshot.count);
```

</div>
<div data-lang="Swift" class="tab"></div>
<div data-lang="Python" class="tab"></div>
<div data-lang="Swift" class="tab">

```Swift
import Glean

// Get snapshot
let snapshot = try! Graphics.checkerboardPeak.testGetValue()

// Does the sum have the expected value?
XCTAssertEqual(23, snapshot.sum)

// Does the count have the expected value?
XCTAssertEqual(1, snapshot.count)

// Buckets are indexed by their lower bound.
XCTAssertEqual(1L, snapshot.values[19])
```

</div>
<div data-lang="Python" class="tab">

```Python
from glean import load_metrics
metrics = load_metrics("metrics.yaml")

# Does the sum have the expected value?
assert 23 == metrics.graphics.checkerboard_peak.test_get_value().sum

# Does the count have the expected value?
assert 1 == metrics.graphics.checkerboard_peak.test_get_value().count

# Buckets are indexed by their lower bound.
assert 1 == metrics.graphics.checkerboard_peak.test_get_value().values[19]
```

</div>
<div data-lang="Rust" class="tab">

```Rust
Expand Down Expand Up @@ -349,8 +417,30 @@ assertEquals(
```

</div>
<div data-lang="Swift" class="tab"></div>
<div data-lang="Python" class="tab"></div>
<div data-lang="Swift" class="tab">

```Swift
import Glean

/// Did the metric receive a negative value?
XCTAssertEqual(
0,
Graphics.checkerboardPeak.testGetNumRecordedErrors(.invalidValue)
)
```

</div>
<div data-lang="Python" class="tab">

```Python
from glean import load_metrics
metrics = load_metrics("metrics.yaml")

# Were any of the values negative and thus caused an error to be recorded?
assert 0 == metrics.graphics.checkerboard_peak.test_get_num_recorded_errors(ErrorType.INVALID_VALUE)
```

</div>
<div data-lang="Rust" class="tab">

```Rust
Expand Down
8 changes: 8 additions & 0 deletions glean-core/ios/Glean.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@
CD70CF932850D69500FC2014 /* Gzip in Frameworks */ = {isa = PBXBuildFile; productRef = CD70CF922850D69500FC2014 /* Gzip */; };
CD70CF982850D77200FC2014 /* OHHTTPStubs in Frameworks */ = {isa = PBXBuildFile; productRef = CD70CF972850D77200FC2014 /* OHHTTPStubs */; };
CD70CF9A2850D79200FC2014 /* OHHTTPStubsSwift in Frameworks */ = {isa = PBXBuildFile; productRef = CD70CF992850D79200FC2014 /* OHHTTPStubsSwift */; };
CD72C3C03020E355009B6DCF /* CustomDistributionMetric.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD72C3BF3020E355009B6DCF /* CustomDistributionMetric.swift */; };
CD72C3C23020E3FF009B6DCF /* CustomDistributionMetricTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD72C3C13020E3FF009B6DCF /* CustomDistributionMetricTests.swift */; };
CD81DCFA282A8F9B00347965 /* RateMetric.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD81DCF9282A8F9A00347965 /* RateMetric.swift */; };
CD81DCFC282A911400347965 /* RateMetricTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD81DCFB282A911400347965 /* RateMetricTests.swift */; };
CD9DA7852BC809BE00E18F31 /* ObjectMetricTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD9DA7842BC809BE00E18F31 /* ObjectMetricTests.swift */; };
Expand Down Expand Up @@ -162,6 +164,8 @@
CD3682F22CAC10FE00B02F04 /* RidealongPingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RidealongPingTests.swift; sourceTree = "<group>"; };
CD387868271D9CD100C097D8 /* glean.udl */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = glean.udl; path = ../../src/glean.udl; sourceTree = "<group>"; };
CD38786C271DCCC700C097D8 /* libglean_ffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libglean_ffi.a; path = ../../target/libglean_ffi.a; sourceTree = "<group>"; };
CD72C3BF3020E355009B6DCF /* CustomDistributionMetric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDistributionMetric.swift; sourceTree = "<group>"; };
CD72C3C13020E3FF009B6DCF /* CustomDistributionMetricTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDistributionMetricTests.swift; sourceTree = "<group>"; };
CD81DCF9282A8F9A00347965 /* RateMetric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RateMetric.swift; sourceTree = "<group>"; };
CD81DCFB282A911400347965 /* RateMetricTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RateMetricTests.swift; sourceTree = "<group>"; };
CD9DA7842BC809BE00E18F31 /* ObjectMetricTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjectMetricTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -347,6 +351,7 @@
AC06529B26E032E300D92D5E /* QuantityMetric.swift */,
1F6A8FEF233C049D007837D5 /* BooleanMetric.swift */,
BF43A8C6232A4BA400545310 /* CounterMetric.swift */,
CD72C3BF3020E355009B6DCF /* CustomDistributionMetric.swift */,
CD81DCF9282A8F9A00347965 /* RateMetric.swift */,
1F6A8FF3233C0A91007837D5 /* DatetimeMetric.swift */,
BF6C53B1232F870C00E3B43A /* Ping.swift */,
Expand All @@ -371,6 +376,7 @@
AC06529D26E034BF00D92D5E /* QuantityMetricTypeTest.swift */,
1F6A8FF1233C068A007837D5 /* BooleanMetricTypeTest.swift */,
BF43A8CC232A615200545310 /* CounterMetricTests.swift */,
CD72C3C13020E3FF009B6DCF /* CustomDistributionMetricTests.swift */,
CD81DCFB282A911400347965 /* RateMetricTests.swift */,
1F6A8FF5233C1555007837D5 /* DatetimeMetricTypeTests.swift */,
BF6C53B3232F872B00E3B43A /* PingTests.swift */,
Expand Down Expand Up @@ -638,6 +644,7 @@
CDBFB4DC27C3FA520045CCB9 /* Dispatchers.swift in Sources */,
EDC21C8F2EE22CCB0042D53E /* GleanUploadTaskProvider.swift in Sources */,
1F6058932314863400307A9F /* Configuration.swift in Sources */,
CD72C3C03020E355009B6DCF /* CustomDistributionMetric.swift in Sources */,
BF2E57052334B77D00364D92 /* EventMetric.swift in Sources */,
BF10008023548B0500064051 /* MemoryDistributionMetric.swift in Sources */,
BF93C698224BFC57006CE7D8 /* Glean.swift in Sources */,
Expand Down Expand Up @@ -681,6 +688,7 @@
8AF3BEA12E60EC670007A9ED /* PingUploaderTests.swift in Sources */,
BFAED50A2369752400DF293D /* StringListMetricTests.swift in Sources */,
60691AEB28DD0BF200BDF31A /* BaselinePingTests.swift in Sources */,
CD72C3C23020E3FF009B6DCF /* CustomDistributionMetricTests.swift in Sources */,
BF890561232BC227003CA2BA /* StringMetricTests.swift in Sources */,
CD0F7CC226F0F28900EDA6A4 /* UrlMetricTests.swift in Sources */,
EDC21B942EE20B2C0042D53E /* PingUploadSchedulerTests.swift in Sources */,
Expand Down
20 changes: 20 additions & 0 deletions glean-core/ios/Glean/Metrics/CustomDistributionMetric.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/* 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/. */

/// This implements the developer facing API for recording custom distribution metrics.
///
/// Custom distributions are histograms with the following parameters that are settable on a
/// per-metric basis:
///
/// - `rangeMin`/`rangeMax`: The minimum and maximum values
/// - `bucketCount`: The number of histogram buckets
/// - `histogramType`: Whether the bucketing is linear or exponential
///
/// This metric exists primarily for backward compatibility with histograms in
/// legacy (pre-Glean) telemetry, and its use is not recommended for newly-created
/// metrics.
///
/// Instances of this class type are automatically generated by the parsers at build time,
/// allowing developers to record values that were previously registered in the metrics.yaml file.
public typealias CustomDistributionMetricType = CustomDistributionMetric
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/* 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/. */

@testable import Glean
import XCTest

class CustomDistributionTypeTests: XCTestCase {
override func setUp() {
resetGleanDiscardingInitialPings(testCase: self, tag: "CustomDistributionTypeTests")
}

override func tearDown() {
tearDownStubs()
}

func testTiminingDistributionSavesToStorage() {
let metric = CustomDistributionMetricType(CommonMetricData(
category: "telemetry",
name: "custom_distribution",
sendInPings: ["store1"],
lifetime: .ping,
disabled: false
),
0,
100,
100,
.linear
)

// Accumulate a few values
metric.accumulateSamples([1, 2, 3])

// Check that data was properly recorded.
// We can only check the count, as we don't control the time.
let snapshot = metric.testGetValue()!
let sum = snapshot.values.values.reduce(0, +)
XCTAssertEqual(3, sum)

// Check the sum
XCTAssertEqual(1 + 2 + 3, snapshot.sum)
// Check that the 1L fell into the first value bucket
XCTAssertEqual(1, snapshot.values[1])
// Check that the 2L fell into the second value bucket
XCTAssertEqual(1, snapshot.values[2])
// Check that the 3L fell into the third value bucket
XCTAssertEqual(1, snapshot.values[3])
}

func testCustomDistributionMustNotRecordIfDisabled() {
let metric = CustomDistributionMetricType(CommonMetricData(
category: "telemetry",
name: "custom_distribution",
sendInPings: ["store1"],
lifetime: .ping,
disabled: true
), 0, 100, 100, .linear
)

metric.accumulateSamples([1])
XCTAssertNil(metric.testGetValue())
}

func testCustomDistributionGetValueReturnsNilIfNothingIsStored() {
let metric = CustomDistributionMetricType(CommonMetricData(
category: "telemetry",
name: "custom_distribution",
sendInPings: ["store1"],
lifetime: .application,
disabled: false
), 0, 100, 100, .linear
)

XCTAssertNil(metric.testGetValue())
}

func testCustomDistributionSavesToSecondaryPings() {
// Define a custom distribution metric which will be stored in multiple stores
let metric = CustomDistributionMetricType(CommonMetricData(
category: "telemetry",
name: "custom_distribution",
sendInPings: ["store1", "store2", "store3"],
lifetime: .application,
disabled: false
), 0, 100, 100, .linear
)

// Accumulate a few values
metric.accumulateSamples([1, 2, 3])

// Check that data was properly recorded in the second ping.
var snapshot = metric.testGetValue("store2")!

// Check the sum
XCTAssertEqual(1+2+3, snapshot.sum)
// Check that the 1L fell into the first value bucket
XCTAssertEqual(1, snapshot.values[1])
// Check that the 2L fell into the second value bucket
XCTAssertEqual(1, snapshot.values[2])
// Check that the 3L fell into the third value bucket
XCTAssertEqual(1, snapshot.values[3])

// Check that data was properly recorded in the second ping.
snapshot = metric.testGetValue("store3")!

// Check the sum
XCTAssertEqual(1+2+3, snapshot.sum)
// Check that the 1L fell into the first value bucket
XCTAssertEqual(1, snapshot.values[1])
// Check that the 2L fell into the second value bucket
XCTAssertEqual(1, snapshot.values[2])
// Check that the 3L fell into the third value bucket
XCTAssertEqual(1, snapshot.values[3])
}
}
1 change: 1 addition & 0 deletions glean-core/python/glean/_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"timespan": metrics.TimespanMetricType,
"timing_distribution": metrics.TimingDistributionMetricType,
"uuid": metrics.UuidMetricType,
"custom_distribution": metrics.CustomDistributionMetricType,
}


Expand Down
6 changes: 5 additions & 1 deletion glean-core/python/glean/metrics/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,18 @@
from .._uniffi import AttributionMetrics
from .._uniffi import CommonMetricData
from .._uniffi import DistributionMetrics
from .._uniffi import HistogramType
from .._uniffi import LabeledMetricData
from .._uniffi import Lifetime
from .._uniffi import MemoryUnit
from .._uniffi import RecordedExperiment
from .._uniffi import TimerId
from .._uniffi import TimeUnit
from .._uniffi import RecordedExperiment

# Re-export some metrics directly
from .._uniffi import BooleanMetric as BooleanMetricType
from .._uniffi import CounterMetric as CounterMetricType
from .._uniffi import CustomDistributionMetric as CustomDistributionMetricType
from .._uniffi import DenominatorMetric as DenominatorMetricType
from .._uniffi import MemoryDistributionMetric as MemoryDistributionMetricType
from .._uniffi import NumeratorMetric as NumeratorMetricType
Expand Down Expand Up @@ -52,6 +54,7 @@
"BooleanMetricType",
"CommonMetricData",
"CounterMetricType",
"CustomDistributionMetricType",
"DatetimeMetricType",
"DenominatorMetricType",
"DistributionMetrics",
Expand Down Expand Up @@ -82,4 +85,5 @@
"TimingDistributionMetricType",
"UrlMetricType",
"UuidMetricType",
"HistogramType",
]
Loading
Loading