Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* New `glean.upload.pending_pings_deleted` metric added to differentiate between deletions due to pending ping count or directory size limitations
* Default pending pings allowed before deletion raised from 250 to 500, and the directory size before deletion increased from 10MB to 50MB.
* New configuration fields added to allow integrating applications to override the maximum pending pings count and directory size limits.
* Add first-class Sessions support: configurable session management (`Auto`, `Lifecycle`, `Manual` modes), session-level sampling, `glean.session_start`/`glean.session_end` boundary events in the `events` ping, and per-event session metadata for downstream analysis ([bug 2020962](https://bugzilla.mozilla.org/show_bug.cgi?id=2020962))
* Rust
* **Experimental**: Introduce `glean-sym`, a Rust API built on top of the Glean UniFFI C FFI ([#3426](https://github.com/mozilla/glean/issues/3426))
* Android
Expand Down
2 changes: 2 additions & 0 deletions docs/user/user/pings/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ At the top-level, this ping contains the following keys:
- `ping_info`: The information [common to all pings](index.md#the-ping_info-section).

- `events`: An array of all of the events that have occurred since the last time the `events` ping was sent.
Glean also emits internal `glean.session_start` and `glean.session_end` boundary events into this array to delimit
sessions; these appear alongside application-recorded events.

Each entry in the `events` array is an object with the following properties:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,9 @@ open class GleanInternalAPI internal constructor() {
pingLifetimeMaxTime = configuration.pingLifetimeMaxTime.toULong(),
maxPendingPingsCount = configuration.maxPendingPingsCount?.toULong(),
maxPendingPingsDirectorySize = configuration.maxPendingPingsDirectorySize?.toULong(),
sessionMode = configuration.sessionMode,
sessionSampleRate = configuration.sessionSampleRate,
sessionInactivityTimeoutMs = configuration.sessionInactivityTimeoutMs.toULong(),
)
val clientInfo = getClientInfo(configuration, buildInfo)
val callbacks = OnGleanEventsImpl(this@GleanInternalAPI)
Expand Down Expand Up @@ -462,6 +465,30 @@ open class GleanInternalAPI internal constructor() {
*/
internal fun getDataDir(): File = this.gleanDataDir

/**
* Starts a session manually.
*
* Only has an effect when Glean is configured with [SessionMode.MANUAL].
* In `AUTO` or `LIFECYCLE` mode this is a no-op so automatic session
* state isn't corrupted.
*/
fun sessionStart() {
gleanSessionStart()
}

/**
* Ends a session manually.
*
* Only has an effect when Glean is configured with [SessionMode.MANUAL].
*
* @param reason An optional application-provided string attached to the
* `glean.session_end` boundary event for downstream analysis.
*/
@JvmOverloads
fun sessionEnd(reason: String? = null) {
gleanSessionEnd(reason)
}

/**
* Handle the foreground event and send the appropriate pings.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package mozilla.telemetry.glean.config

import mozilla.telemetry.glean.internal.LevelFilter
import mozilla.telemetry.glean.internal.SessionMode
import mozilla.telemetry.glean.net.HttpURLConnectionUploader
import mozilla.telemetry.glean.net.PingUploader

Expand Down Expand Up @@ -34,6 +35,10 @@ import mozilla.telemetry.glean.net.PingUploader
* When this limit is exceeded, the oldest pings are deleted. Defaults to 500.
* @property maxPendingPingsDirectorySize The maximum size in bytes of the pending pings directory.
* When this limit is exceeded, the oldest pings are deleted. Defaults to 50 MB.
* @property sessionMode How Glean manages session boundaries. Default: [SessionMode.AUTO].
* @property sessionSampleRate Session sampling rate (0.0–1.0). Default: `1.0`.
* @property sessionInactivityTimeoutMs Inactivity timeout (milliseconds) before AUTO-mode
* sessions expire. Default: 30 minutes.
*/
data class Configuration
@JvmOverloads
Expand All @@ -56,11 +61,19 @@ data class Configuration
val pingSchedule: Map<String, List<String>> = emptyMap(),
val maxPendingPingsCount: Long? = null,
val maxPendingPingsDirectorySize: Long? = null,
val sessionMode: SessionMode = SessionMode.AUTO,
val sessionSampleRate: Double = 1.0,
val sessionInactivityTimeoutMs: Long = DEFAULT_SESSION_INACTIVITY_TIMEOUT_MS,
) {
companion object {
/**
* The default server pings are sent to.
*/
const val DEFAULT_TELEMETRY_ENDPOINT = "https://incoming.telemetry.mozilla.org"

/**
* The default AUTO-mode session inactivity timeout: 30 minutes.
*/
const val DEFAULT_SESSION_INACTIVITY_TIMEOUT_MS: Long = 1_800_000L
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,8 @@ class GleanTest {
checkPingSchema(json)
if (docType == "events") {
assertEquals("inactive", json.getJSONObject("ping_info").getString("reason"))
assertEquals(1, json.getJSONArray("events").length())
// 2 events: glean.session_start (on foreground) + ui.click (recorded explicitly)
assertEquals(2, json.getJSONArray("events").length())
} else if (docType == "baseline") {
val seq = json.getJSONObject("ping_info").getInt("seq")

Expand Down Expand Up @@ -314,8 +315,14 @@ class GleanTest {
// Trigger worker task to upload the pings in the background
triggerWorkManager(context)

// Session recovery emits a session_end event for the previous abnormal session,
// which is flushed as an events ping before the dirty_startup baseline ping.
var request = server.takeRequest(20L, TimeUnit.SECONDS)!!
var docType = request.path!!.split("/")[3]
assertEquals("The first ping must be the session-recovery 'events' ping", "events", docType)

request = server.takeRequest(20L, TimeUnit.SECONDS)!!
docType = request.path!!.split("/")[3]
assertEquals("The received ping must be a 'baseline' ping", "baseline", docType)

var baselineJson = JSONObject(request.getPlainBody())
Expand Down
3 changes: 3 additions & 0 deletions glean-core/benchmark/benches/dispatcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ pub fn metric_dispatcher_benchmark(c: &mut Criterion) {
ping_lifetime_max_time: 0,
max_pending_pings_count: None,
max_pending_pings_directory_size: None,
session_mode: glean_core::SessionMode::Auto,
session_sample_rate: 1.0,
session_inactivity_timeout_ms: 1_800_000,
Comment thread
travis79 marked this conversation as resolved.
};
let client_info = ClientInfoMetrics::unknown();

Expand Down
9 changes: 9 additions & 0 deletions glean-core/benchmark/benches/lifetime_buffering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ pub fn delay_io_benchmark(c: &mut Criterion) {
ping_lifetime_max_time: 0,
max_pending_pings_count: None,
max_pending_pings_directory_size: None,
session_mode: glean_core::SessionMode::Auto,
session_sample_rate: 1.0,
session_inactivity_timeout_ms: 1_800_000,
};
let glean = Glean::new(cfg).unwrap();

Expand Down Expand Up @@ -77,6 +80,9 @@ pub fn delay_io_benchmark(c: &mut Criterion) {
ping_lifetime_max_time: 0,
max_pending_pings_count: None,
max_pending_pings_directory_size: None,
session_mode: glean_core::SessionMode::Auto,
session_sample_rate: 1.0,
session_inactivity_timeout_ms: 1_800_000,
};
let glean = Glean::new(cfg).unwrap();

Expand Down Expand Up @@ -119,6 +125,9 @@ pub fn delay_io_benchmark(c: &mut Criterion) {
ping_schedule: Default::default(),
ping_lifetime_threshold: 1000,
ping_lifetime_max_time: 0,
session_mode: glean_core::SessionMode::Auto,
session_sample_rate: 1.0,
session_inactivity_timeout_ms: 1_800_000,
};
let glean = Glean::new(cfg).unwrap();

Expand Down
3 changes: 3 additions & 0 deletions glean-core/examples/rkv-open.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ fn main() {
ping_lifetime_max_time: 0,
max_pending_pings_count: None,
max_pending_pings_directory_size: None,
session_mode: glean_core::SessionMode::Auto,
session_sample_rate: 1.0,
session_inactivity_timeout_ms: 1_800_000,
};

let client_info = ClientInfoMetrics::unknown();
Expand Down
11 changes: 11 additions & 0 deletions glean-core/glean-sym/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pub struct CommonMetricData {
pub lifetime: Lifetime,
pub disabled: bool,
pub dynamic_label: Option<DynamicLabelType>,
pub in_session: bool,
}

#[derive(uniffi::Record)]
Expand All @@ -49,6 +50,16 @@ pub struct RecordedEvent {
category: String,
name: String,
extra: Option<::std::collections::HashMap<String, String>>,
session_metadata: Option<SessionMetadata>,
}

#[derive(uniffi::Record, Debug)]
pub struct SessionMetadata {
pub session_id: String,
pub session_seq: u64,
pub event_seq: u64,
pub session_sample_rate: f64,
pub session_start_time: Option<String>,
}

#[derive(uniffi::Record)]
Expand Down
21 changes: 17 additions & 4 deletions glean-core/ios/Glean/Config/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ public struct Configuration {
let pingLifetimeThreshold: Int
let pingLifetimeMaxTime: Int
let pingSchedule: [String: [String]]
let sessionMode: SessionMode
let sessionSampleRate: Double
let sessionInactivityTimeoutMs: UInt64
let httpClient: PingUploader
let maxPendingPingsCount: UInt64?
let maxPendingPingsDirectorySize: UInt64?
Expand Down Expand Up @@ -44,11 +47,15 @@ public struct Configuration {
/// * pingSchedule A ping schedule map.
/// Maps a ping name to a list of pings to schedule along with it.
/// Only used if the ping's own ping schedule list is empty.
/// * httpClient An http uploader that supports the `PingUploader` protocol
/// * maxPendingPingsCount The maximum number of pending pings stored on disk.
/// When exceeded, the oldest pings are deleted. Defaults to 500.
/// * maxPendingPingsDirectorySize The maximum size in bytes of the pending pings directory.
/// When exceeded, the oldest pings are deleted. Defaults to 50 MB.
/// * sessionMode How Glean manages session boundaries. Default: `.auto`.
/// * sessionSampleRate Session sampling rate (0.0–1.0). Default: `1.0`.
/// * sessionInactivityTimeoutMs Inactivity timeout (ms) before AUTO-mode
/// sessions expire. Default: 30 minutes (1,800,000 ms).
/// * httpClient An http uploader that supports the `PingUploader` protocol
public init(
maxEvents: Int32? = nil,
channel: String? = nil,
Expand All @@ -61,9 +68,12 @@ public struct Configuration {
pingLifetimeThreshold: Int = 0,
pingLifetimeMaxTime: Int = 0,
pingSchedule: [String: [String]] = [:],
httpClient: PingUploader = HttpPingUploader(),
maxPendingPingsCount: UInt64? = nil,
maxPendingPingsDirectorySize: UInt64? = nil
maxPendingPingsDirectorySize: UInt64? = nil,
sessionMode: SessionMode = .auto,
sessionSampleRate: Double = 1.0,
sessionInactivityTimeoutMs: UInt64 = 1_800_000,
httpClient: PingUploader = HttpPingUploader()
) {
self.serverEndpoint =
serverEndpoint ?? Constants.defaultTelemetryEndpoint
Expand All @@ -77,8 +87,11 @@ public struct Configuration {
self.pingLifetimeThreshold = pingLifetimeThreshold
self.pingLifetimeMaxTime = pingLifetimeMaxTime
self.pingSchedule = pingSchedule
self.httpClient = httpClient
self.maxPendingPingsCount = maxPendingPingsCount
self.maxPendingPingsDirectorySize = maxPendingPingsDirectorySize
self.sessionMode = sessionMode
self.sessionSampleRate = sessionSampleRate
self.sessionInactivityTimeoutMs = sessionInactivityTimeoutMs
self.httpClient = httpClient
}
}
25 changes: 24 additions & 1 deletion glean-core/ios/Glean/Glean.swift
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,10 @@ public final class Glean: @unchecked Sendable {
pingLifetimeThreshold: UInt64(configuration.pingLifetimeThreshold),
pingLifetimeMaxTime: UInt64(configuration.pingLifetimeMaxTime),
maxPendingPingsCount: configuration.maxPendingPingsCount,
maxPendingPingsDirectorySize: configuration.maxPendingPingsDirectorySize
maxPendingPingsDirectorySize: configuration.maxPendingPingsDirectorySize,
sessionMode: configuration.sessionMode,
sessionSampleRate: configuration.sessionSampleRate,
sessionInactivityTimeoutMs: configuration.sessionInactivityTimeoutMs
)
let clientInfo = getClientInfo(configuration, buildInfo: buildInfo)
let callbacks = OnGleanEventsImpl(glean: self)
Expand Down Expand Up @@ -352,6 +355,26 @@ public final class Glean: @unchecked Sendable {
return self.initialized
}

/// Starts a session manually.
///
/// Only has an effect when Glean is configured with `SessionMode.manual`.
/// In `.auto` or `.lifecycle` mode this is a no-op so automatic session
/// state isn't corrupted.
public func sessionStart() {
gleanSessionStart()
}

/// Ends a session manually.
///
/// Only has an effect when Glean is configured with `SessionMode.manual`.
///
/// - parameters:
/// * reason: An optional application-provided string attached to the
/// `glean.session_end` boundary event for downstream analysis.
public func sessionEnd(reason: String? = nil) {
gleanSessionEnd(reason)
}

/// Handle foreground event and submit appropriate pings
func handleForegroundEvent() {
if !isActive {
Expand Down
10 changes: 6 additions & 4 deletions glean-core/ios/GleanTests/GleanTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -239,8 +239,9 @@ class GleanTests: XCTestCase {

// We expect only a single ping later
stubServerReceive { pingType, _ in
if pingType == "baseline" {
// Ignore initial "active" baseline ping
if pingType == "baseline" || pingType == "events" {
// Ignore initial "active" baseline ping and events pings
// (session boundary events are now flushed on startup).
return
}

Expand Down Expand Up @@ -381,8 +382,9 @@ class GleanTests: XCTestCase {

// We expect 10 pings later
stubServerReceive { pingType, _ in
if pingType == "baseline" {
// Ignore initial "active" baseline ping
if pingType == "baseline" || pingType == "events" {
// Ignore initial "active" baseline ping and events pings
// (session boundary events are now flushed on startup).
return
}

Expand Down
25 changes: 19 additions & 6 deletions glean-core/ios/GleanTests/Metrics/EventMetricTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,12 @@ class EventMetricTypeTests: XCTestCase {
XCTAssertEqual(1, snapshot3.count)
}

/// Filters out internal Glean session boundary events (category == "glean")
/// so tests can check only user-recorded events.
private func userEvents(from events: [Any]?) -> [[String: Any]] {
return (events as? [[String: Any]])?.filter { ($0["category"] as? String) != "glean" } ?? []
}

func testFlushQueuedEventsOnStartup() {
setupHttpResponseStub()
expectation = expectation(description: "Completed upload")
Expand All @@ -277,7 +283,10 @@ class EventMetricTypeTests: XCTestCase {

let events = lastPingJson?["events"] as? [Any]
XCTAssertNotNil(events)
XCTAssertEqual(1, events?.count)
// Session boundary events (glean.session_start/session_end) are also
// flushed on startup; filter to user-recorded events only.
let userEvts = userEvents(from: events)
XCTAssertEqual(1, userEvts.count)
}

private func getExtraValue(from event: Any?, for key: String) -> String {
Expand Down Expand Up @@ -314,8 +323,10 @@ class EventMetricTypeTests: XCTestCase {

let events = lastPingJson?["events"] as? [Any]
XCTAssertNotNil(events)
XCTAssertEqual(1, events?.count)
XCTAssertEqual("run1", getExtraValue(from: events![0], for: "some_extra"))
// Session boundary events are also flushed on startup; filter to user events.
let userEvts = userEvents(from: events)
XCTAssertEqual(1, userEvts.count)
XCTAssertEqual("run1", getExtraValue(from: userEvts[0], for: "some_extra"))

setupHttpResponseStub()
expectation = expectation(description: "Completed upload")
Expand All @@ -328,9 +339,11 @@ class EventMetricTypeTests: XCTestCase {

let events2 = lastPingJson?["events"] as? [Any]
XCTAssertNotNil(events2)
XCTAssertEqual(2, events2?.count)
XCTAssertEqual("pre-init", getExtraValue(from: events2![0], for: "some_extra"))
XCTAssertEqual("post-init", getExtraValue(from: events2![1], for: "some_extra"))
// Session boundary events are also present; filter to user events.
let userEvts2 = userEvents(from: events2)
XCTAssertEqual(2, userEvts2.count)
XCTAssertEqual("pre-init", getExtraValue(from: userEvts2[0], for: "some_extra"))
XCTAssertEqual("post-init", getExtraValue(from: userEvts2[1], for: "some_extra"))
}

func testEventLongExtraRecordsError() {
Expand Down
6 changes: 6 additions & 0 deletions glean-core/ios/GleanTests/Net/BaselinePingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,12 @@ final class BaselinePingTests: XCTestCase {

// Set up the test stub based on the default telemetry endpoint
stubServerReceive { pingType, json in
// Skip events pings: session boundary events (session_start/session_end)
// are now recorded to the "events" ping and may be flushed on startup.
if pingType == "events" {
return
}

XCTAssertEqual("baseline", pingType)
XCTAssert(json != nil)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,8 +259,9 @@ class MetricsPingSchedulerTests: XCTestCase {

// Set up the interception of the ping for inspection
stubServerReceive { pingType, json in
if pingType == "baseline" {
// Ignore initial "active" baseline ping
if pingType == "baseline" || pingType == "events" {
// Ignore initial "active" baseline ping and events pings
// (session boundary events are now flushed on startup).
return
}

Expand Down
Loading
Loading