diff --git a/CHANGELOG.md b/CHANGELOG.md index 797d1833e6..ed6fef6ad0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ [Full changelog](https://github.com/mozilla/glean/compare/v61.1.0...main) +* Kotlin + * Accept a ping schedule map on initialize ([#2967](https://github.com/mozilla/glean/pull/2967)) +* Swift + * Accept a ping schedule map on initialize ([#2967](https://github.com/mozilla/glean/pull/2967)) + # v61.1.0 (2024-09-24) [Full changelog](https://github.com/mozilla/glean/compare/v61.0.0...v61.1.0) diff --git a/glean-core/android/src/main/java/mozilla/telemetry/glean/Glean.kt b/glean-core/android/src/main/java/mozilla/telemetry/glean/Glean.kt index 43dadab988..70692beabe 100644 --- a/glean-core/android/src/main/java/mozilla/telemetry/glean/Glean.kt +++ b/glean-core/android/src/main/java/mozilla/telemetry/glean/Glean.kt @@ -257,7 +257,7 @@ open class GleanInternalAPI internal constructor() { enableEventTimestamps = configuration.enableEventTimestamps, experimentationId = configuration.experimentationId, enableInternalPings = configuration.enableInternalPings, - pingSchedule = emptyMap(), + pingSchedule = configuration.pingSchedule, pingLifetimeThreshold = configuration.pingLifetimeThreshold.toULong(), pingLifetimeMaxTime = configuration.pingLifetimeMaxTime.toULong(), ) diff --git a/glean-core/android/src/main/java/mozilla/telemetry/glean/config/Configuration.kt b/glean-core/android/src/main/java/mozilla/telemetry/glean/config/Configuration.kt index d7537f20ad..ea97cd3418 100644 --- a/glean-core/android/src/main/java/mozilla/telemetry/glean/config/Configuration.kt +++ b/glean-core/android/src/main/java/mozilla/telemetry/glean/config/Configuration.kt @@ -27,6 +27,9 @@ import mozilla.telemetry.glean.net.PingUploader * @property delayPingLifetimeIo Whether Glean should delay persistence of data from metrics with ping lifetime. * @property pingLifetimeThreshold Write count threshold when to auto-flush. `0` disables it. * @property pingLifetimeMaxTime After what time to auto-flush (in milliseconds). 0 disables it. + * @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. */ data class Configuration @JvmOverloads constructor( val serverEndpoint: String = DEFAULT_TELEMETRY_ENDPOINT, @@ -44,6 +47,7 @@ data class Configuration @JvmOverloads constructor( val delayPingLifetimeIo: Boolean = true, val pingLifetimeThreshold: Int = 1000, val pingLifetimeMaxTime: Int = 0, + val pingSchedule: Map> = emptyMap(), ) { companion object { /** diff --git a/glean-core/android/src/test/java/mozilla/telemetry/glean/pings/RidealongPingTest.kt b/glean-core/android/src/test/java/mozilla/telemetry/glean/pings/RidealongPingTest.kt new file mode 100644 index 0000000000..d7933ac6cd --- /dev/null +++ b/glean-core/android/src/test/java/mozilla/telemetry/glean/pings/RidealongPingTest.kt @@ -0,0 +1,89 @@ +/* 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/. */ + +package mozilla.telemetry.glean.scheduler + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.telemetry.glean.Glean +import mozilla.telemetry.glean.delayMetricsPing +import mozilla.telemetry.glean.getContext +import mozilla.telemetry.glean.getMockWebServer +import mozilla.telemetry.glean.private.NoReasonCodes +import mozilla.telemetry.glean.private.PingType +import mozilla.telemetry.glean.resetGlean +import mozilla.telemetry.glean.testing.GleanTestRule +import mozilla.telemetry.glean.triggerWorkManager +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.util.concurrent.TimeUnit + +/** + * Testing behavior of custom pings. + * + * We already rely on the Rust side to test custom pings, + * but this enables us to test the upload mechanism specifically. + * + * Even if this seemingly duplicates some of the testing, this should be kept around. + */ +@RunWith(AndroidJUnit4::class) +class RidealongPingTest { + private val context = getContext() + private lateinit var server: MockWebServer + + @get:Rule + val gleanRule = GleanTestRule(context) + + @Before + fun setup() { + server = getMockWebServer() + } + + @After + fun teardown() { + server.shutdown() + } + + @Test + fun `sends a ride-along custom ping on baseline schedule`() { + delayMetricsPing(context) + resetGlean( + context, + Glean.configuration.copy( + serverEndpoint = "http://" + server.hostName + ":" + server.port, + pingSchedule = mapOf("baseline" to listOf("custom-ping")), + ), + clearStores = true, + uploadEnabled = true, + ) + + // Define a new custom ping inline. + PingType( + name = "custom-ping", + includeClientId = true, + sendIfEmpty = true, + preciseTimestamps = true, + includeInfoSections = true, + enabled = true, + schedulesPings = emptyList(), + reasonCodes = emptyList(), + ) + + Glean.handleBackgroundEvent() + // Trigger it to upload + triggerWorkManager(context) + + var request = server.takeRequest(2L, TimeUnit.SECONDS)!! + var docType = request.path!!.split("/")[3] + assertEquals("baseline", docType) + + request = server.takeRequest(2L, TimeUnit.SECONDS)!! + docType = request.path!!.split("/")[3] + assertEquals("custom-ping", docType) + } +} diff --git a/glean-core/ios/Glean.xcodeproj/project.pbxproj b/glean-core/ios/Glean.xcodeproj/project.pbxproj index 9497ce3ff4..2a2d53042b 100644 --- a/glean-core/ios/Glean.xcodeproj/project.pbxproj +++ b/glean-core/ios/Glean.xcodeproj/project.pbxproj @@ -62,6 +62,7 @@ CD0CADA427E216810015A997 /* glean.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDD08C8527E21104007C8400 /* glean.swift */; }; CD0F7CC026F0F27900EDA6A4 /* UrlMetric.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD0F7CBF26F0F27900EDA6A4 /* UrlMetric.swift */; }; CD0F7CC226F0F28900EDA6A4 /* UrlMetricTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD0F7CC126F0F28900EDA6A4 /* UrlMetricTests.swift */; }; + CD3682F32CAC110300B02F04 /* RidealongPingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD3682F22CAC10FE00B02F04 /* RidealongPingTests.swift */; }; CD38786D271DCCC700C097D8 /* libglean_ffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CD38786C271DCCC700C097D8 /* libglean_ffi.a */; }; CD70CF932850D69500FC2014 /* Gzip in Frameworks */ = {isa = PBXBuildFile; productRef = CD70CF922850D69500FC2014 /* Gzip */; }; CD70CF982850D77200FC2014 /* OHHTTPStubs in Frameworks */ = {isa = PBXBuildFile; productRef = CD70CF972850D77200FC2014 /* OHHTTPStubs */; }; @@ -144,6 +145,7 @@ CD07A4DD2BC808AE007A0F1C /* ObjectMetric.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjectMetric.swift; sourceTree = ""; }; CD0F7CBF26F0F27900EDA6A4 /* UrlMetric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlMetric.swift; sourceTree = ""; }; CD0F7CC126F0F28900EDA6A4 /* UrlMetricTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlMetricTests.swift; sourceTree = ""; }; + CD3682F22CAC10FE00B02F04 /* RidealongPingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RidealongPingTests.swift; sourceTree = ""; }; CD387868271D9CD100C097D8 /* glean.udl */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = glean.udl; path = ../../src/glean.udl; sourceTree = ""; }; CD38786C271DCCC700C097D8 /* libglean_ffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libglean_ffi.a; path = ../../target/libglean_ffi.a; sourceTree = ""; }; CD81DCF9282A8F9A00347965 /* RateMetric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RateMetric.swift; sourceTree = ""; }; @@ -372,6 +374,7 @@ isa = PBXGroup; children = ( 60691AEA28DD0BF200BDF31A /* BaselinePingTests.swift */, + CD3682F22CAC10FE00B02F04 /* RidealongPingTests.swift */, BF80AA5E2399305200A8B172 /* DeletionRequestPingTests.swift */, BF80AA5A2399301200A8B172 /* HttpPingUploaderTests.swift */, ); @@ -637,6 +640,7 @@ CD0F7CC226F0F28900EDA6A4 /* UrlMetricTests.swift in Sources */, BFCBD6AB246D55CC0032096D /* TestUtils.swift in Sources */, AC06529E26E034BF00D92D5E /* QuantityMetricTypeTest.swift in Sources */, + CD3682F32CAC110300B02F04 /* RidealongPingTests.swift in Sources */, 1F58921223C923C4007D2D80 /* MetricsPingSchedulerTests.swift in Sources */, CD9DA7852BC809BE00E18F31 /* ObjectMetricTests.swift in Sources */, 1FD4527723395EEB00F4C7E8 /* UuidMetricTests.swift in Sources */, diff --git a/glean-core/ios/Glean/Config/Configuration.swift b/glean-core/ios/Glean/Config/Configuration.swift index 7576fc5cea..99f4370be2 100644 --- a/glean-core/ios/Glean/Config/Configuration.swift +++ b/glean-core/ios/Glean/Config/Configuration.swift @@ -15,6 +15,7 @@ public struct Configuration { let enableInternalPings: Bool let pingLifetimeThreshold: Int let pingLifetimeMaxTime: Int + let pingSchedule: [String: [String]] struct Constants { static let defaultTelemetryEndpoint = "https://incoming.telemetry.mozilla.org" @@ -36,6 +37,9 @@ public struct Configuration { /// * enableInternalPings Whether to enable internal pings. /// * pingLifetimeThreshold Write count threshold when to auto-flush. `0` disables it. /// * pingLifetimeMaxTime After what time to auto-flush (in milliseconds). 0 disables it. + /// * 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. public init( maxEvents: Int32? = nil, channel: String? = nil, @@ -46,7 +50,8 @@ public struct Configuration { experimentationId: String? = nil, enableInternalPings: Bool = true, pingLifetimeThreshold: Int = 0, - pingLifetimeMaxTime: Int = 0 + pingLifetimeMaxTime: Int = 0, + pingSchedule: [String: [String]] = [:] ) { self.serverEndpoint = serverEndpoint ?? Constants.defaultTelemetryEndpoint self.maxEvents = maxEvents @@ -58,5 +63,6 @@ public struct Configuration { self.enableInternalPings = enableInternalPings self.pingLifetimeThreshold = pingLifetimeThreshold self.pingLifetimeMaxTime = pingLifetimeMaxTime + self.pingSchedule = pingSchedule } } diff --git a/glean-core/ios/Glean/Glean.swift b/glean-core/ios/Glean/Glean.swift index ea3829c82c..a9bd6664d1 100644 --- a/glean-core/ios/Glean/Glean.swift +++ b/glean-core/ios/Glean/Glean.swift @@ -196,7 +196,7 @@ public class Glean { enableEventTimestamps: configuration.enableEventTimestamps, experimentationId: configuration.experimentationId, enableInternalPings: configuration.enableInternalPings, - pingSchedule: [:], + pingSchedule: configuration.pingSchedule, pingLifetimeThreshold: UInt64(configuration.pingLifetimeThreshold), pingLifetimeMaxTime: UInt64(configuration.pingLifetimeMaxTime) ) diff --git a/glean-core/ios/GleanTests/RidealongPingTests.swift b/glean-core/ios/GleanTests/RidealongPingTests.swift new file mode 100644 index 0000000000..26a05783e3 --- /dev/null +++ b/glean-core/ios/GleanTests/RidealongPingTests.swift @@ -0,0 +1,62 @@ +/* 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 OHHTTPStubs +import OHHTTPStubsSwift +import XCTest + +final class RidealongPingTests: XCTestCase { + var expectation: XCTestExpectation? + + override func tearDown() { + Glean.shared.testDestroyGleanHandle() + expectation = nil + tearDownStubs() + } + + func testSendRidealongPingWithBaseline() { + + let configuration = Configuration(pingSchedule: ["baseline": ["ridealong"]]) + resetGleanDiscardingInitialPings(testCase: self, tag: "RidealongPingTests", configuration: configuration) + + // Register ping _after_ Glean has been initialized to avoid this being sent multiple times. + _ = Ping( + name: "ridealong", + includeClientId: true, + sendIfEmpty: true, + preciseTimestamps: true, + includeInfoSections: true, + enabled: true, + schedulesPings: [], + reasonCodes: [] + ) + + // We receive a baseline ping, and a ridealong ping. + // The order might vary. + var pingsToReceive = ["baseline", "ridealong"] + + stubServerReceive { pingType, _ in + XCTAssertTrue(!pingsToReceive.isEmpty, "No more pings expected") + XCTAssertTrue(pingsToReceive.contains(pingType), "Expected ping types: \(pingsToReceive), got \(pingType)") + pingsToReceive.removeAll(where: { $0 == pingType }) + + if pingsToReceive.isEmpty { + DispatchQueue.main.async { + // let the response get processed before we mark the expectation fulfilled + self.expectation?.fulfill() + } + } + } + + // Set up the expectation that will be fulfilled by the stub above + expectation = expectation(description: "Pings Received") + + Glean.shared.submitPingByName("baseline") + + waitForExpectations(timeout: 5.0) { error in + XCTAssertNil(error, "Test timed out waiting for upload: \(error!)") + } + } +} diff --git a/glean-core/ios/GleanTests/TestUtils.swift b/glean-core/ios/GleanTests/TestUtils.swift index 9568363a7b..12c3bc9db6 100644 --- a/glean-core/ios/GleanTests/TestUtils.swift +++ b/glean-core/ios/GleanTests/TestUtils.swift @@ -49,7 +49,10 @@ func stubServerReceive(callback: @escaping (String, [String: Any]?) -> Void) { /// /// This also prevents outgoing network requests during unit tests while /// still allowing us to use the default telemetry endpoint. -func resetGleanDiscardingInitialPings(testCase: XCTestCase, tag: String, clearStores: Bool = true) { +func resetGleanDiscardingInitialPings(testCase: XCTestCase, + tag: String, + clearStores: Bool = true, + configuration: Configuration = Configuration()) { let expectation = testCase.expectation(description: "\(tag): Ping Received") // We are using OHHTTPStubs combined with an XCTestExpectation in order to capture @@ -85,7 +88,7 @@ func resetGleanDiscardingInitialPings(testCase: XCTestCase, tag: String, clearSt let mps = MetricsPingScheduler(true) mps.updateSentDate(Date()) - Glean.shared.resetGlean(clearStores: clearStores) + Glean.shared.resetGlean(configuration: configuration, clearStores: clearStores) testCase.waitForExpectations(timeout: 5.0) { error in XCTAssertNil(error, "Test timed out waiting for upload: \(error!)")