Skip to content
Draft
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/compare/v67.2.0...main)

* General
* 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
* Add support for determining `client_info.os_version` for Android binaries ([#3434](https://github.com/mozilla/glean/pull/3434))

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 @@ -270,6 +270,9 @@ open class GleanInternalAPI internal constructor() {
pingSchedule = configuration.pingSchedule,
pingLifetimeThreshold = configuration.pingLifetimeThreshold.toULong(),
pingLifetimeMaxTime = configuration.pingLifetimeMaxTime.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 @@ -460,6 +463,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 All @@ -30,6 +31,10 @@ import mozilla.telemetry.glean.net.PingUploader
* @property 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.
* @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 @@ -50,11 +55,19 @@ data class Configuration
val pingLifetimeThreshold: Int = 1000,
val pingLifetimeMaxTime: Int = 0,
val pingSchedule: Map<String, List<String>> = emptyMap(),
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 @@ -83,6 +83,9 @@ pub fn metric_dispatcher_benchmark(c: &mut Criterion) {
ping_schedule: Default::default(),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a note to the very first line of the diff for the first commit, because one still can't leave a comment for the whole commit (or the message).

Thanks for splitting it up into individual commits. That's certainly helpful, though unfortunately this commit is not working on its own, introducing use of the SessionMode before it's defined.
I think I can cope with it in review though, just comments might be a bit out of order then.

ping_lifetime_threshold: 0,
ping_lifetime_max_time: 0,
session_mode: glean_core::SessionMode::Auto,
session_sample_rate: 1.0,
session_inactivity_timeout_ms: 1_800_000,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gnah, I really wish we could make this a Duration. But eh, it's the UDL-exposed type, we stick with integers for now.

};
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 @@ -31,6 +31,9 @@ pub fn delay_io_benchmark(c: &mut Criterion) {
ping_schedule: Default::default(),
ping_lifetime_threshold: 0,
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 Expand Up @@ -73,6 +76,9 @@ pub fn delay_io_benchmark(c: &mut Criterion) {
ping_schedule: Default::default(),
ping_lifetime_threshold: 0,
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 Expand Up @@ -115,6 +121,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 @@ -51,6 +51,9 @@ fn main() {
ping_schedule: HashMap::default(),
ping_lifetime_threshold: 0,
ping_lifetime_max_time: 0,
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
13 changes: 13 additions & 0 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

struct Constants {
Expand All @@ -42,6 +45,10 @@ 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.
/// * 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,
Expand All @@ -55,6 +62,9 @@ public struct Configuration {
pingLifetimeThreshold: Int = 0,
pingLifetimeMaxTime: Int = 0,
pingSchedule: [String: [String]] = [:],
sessionMode: SessionMode = .auto,
sessionSampleRate: Double = 1.0,
sessionInactivityTimeoutMs: UInt64 = 1_800_000,
httpClient: PingUploader = HttpPingUploader()
) {
self.serverEndpoint =
Expand All @@ -69,6 +79,9 @@ public struct Configuration {
self.pingLifetimeThreshold = pingLifetimeThreshold
self.pingLifetimeMaxTime = pingLifetimeMaxTime
self.pingSchedule = pingSchedule
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 @@ -212,7 +212,10 @@ public final class Glean: @unchecked Sendable {
enableInternalPings: configuration.enableInternalPings,
pingSchedule: configuration.pingSchedule,
pingLifetimeThreshold: UInt64(configuration.pingLifetimeThreshold),
pingLifetimeMaxTime: UInt64(configuration.pingLifetimeMaxTime)
pingLifetimeMaxTime: UInt64(configuration.pingLifetimeMaxTime),
sessionMode: configuration.sessionMode,
sessionSampleRate: configuration.sessionSampleRate,
sessionInactivityTimeoutMs: configuration.sessionInactivityTimeoutMs
)
let clientInfo = getClientInfo(configuration, buildInfo: buildInfo)
let callbacks = OnGleanEventsImpl(glean: self)
Expand Down Expand Up @@ -350,6 +353,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