Skip to content

Commit

Permalink
Add EXP-4998 [Nimbus] Define and implement RecordedNimbusContext obje…
Browse files Browse the repository at this point in the history
…ct (#23737)

define and implement RecordedNimbusContext object
  • Loading branch information
jeddai authored Dec 17, 2024
1 parent da3ae61 commit a1a3226
Show file tree
Hide file tree
Showing 6 changed files with 340 additions and 13 deletions.
10 changes: 9 additions & 1 deletion firefox-ios/Client.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,8 @@
7BEB64441C7345600092C02E /* L10nSuite2SnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B3632D31C2983F000D12AF9 /* L10nSuite2SnapshotTests.swift */; };
7BEB64451C7345600092C02E /* SnapshotHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B60B0071BDE3AE10090C984 /* SnapshotHelper.swift */; };
7BEFC6801BFF68C30059C952 /* QuickActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BEFC67F1BFF68C30059C952 /* QuickActions.swift */; };
8015C8B52D0A31FE0093AED0 /* RecordedNimbusContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8015C8B42D0A31FE0093AED0 /* RecordedNimbusContextTests.swift */; };
80967F802D0A36890057F0C7 /* RecordedNimbusContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80967F7F2D0A36890057F0C7 /* RecordedNimbusContext.swift */; };
81020C922BB5AFA2007B8481 /* OnboardingMultipleChoiceButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81020C912BB5AFA2007B8481 /* OnboardingMultipleChoiceButtonView.swift */; };
81020C942BB5B026007B8481 /* OnboardingMultipleChoiceButtonViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81020C932BB5B026007B8481 /* OnboardingMultipleChoiceButtonViewModel.swift */; };
81055B562BAB7CE200E166B3 /* OnboardingMultipeChoiceButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81055B552BAB7CE200E166B3 /* OnboardingMultipeChoiceButtonModel.swift */; };
Expand Down Expand Up @@ -7167,7 +7169,9 @@
7F9D4F52B86570F837E0E9A6 /* ml */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ml; path = ml.lproj/ClearPrivateData.strings; sourceTree = "<group>"; };
7FAB476EB035F799E1BF1267 /* ka */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ka; path = ka.lproj/Intro.strings; sourceTree = "<group>"; };
7FCA45BDADEE92EF815319D3 /* sq */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sq; path = sq.lproj/ErrorPages.strings; sourceTree = "<group>"; };
8015C8B42D0A31FE0093AED0 /* RecordedNimbusContextTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordedNimbusContextTests.swift; sourceTree = "<group>"; };
80574E82B96F93CDE08B966B /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/Intro.strings; sourceTree = "<group>"; };
80967F7F2D0A36890057F0C7 /* RecordedNimbusContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordedNimbusContext.swift; sourceTree = "<group>"; };
809E4D488E089D47E5B2B7ED /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Storage.strings; sourceTree = "<group>"; };
80A64F9E9304EFF710BFF857 /* es-CL */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-CL"; path = "es-CL.lproj/Localizable.strings"; sourceTree = "<group>"; };
80B54D93BC553D6E6CDFAA0A /* cy */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cy; path = cy.lproj/AuthenticationManager.strings; sourceTree = "<group>"; };
Expand Down Expand Up @@ -10505,6 +10509,7 @@
9636D92627F5D71A00771F5E /* Messaging */,
D51EA5B82640697100334331 /* Settings */,
C80685D026A0C93900DCD895 /* UserResearch.swift */,
80967F7F2D0A36890057F0C7 /* RecordedNimbusContext.swift */,
);
path = Experiments;
sourceTree = "<group>";
Expand Down Expand Up @@ -12856,6 +12861,7 @@
children = (
C8E1BC0928085AA700C62964 /* NimbusFeatureFlagLayerTests.swift */,
39C137962655798A003DC662 /* NimbusIntegrationTests.swift */,
8015C8B42D0A31FE0093AED0 /* RecordedNimbusContextTests.swift */,
);
path = Nimbus;
sourceTree = "<group>";
Expand Down Expand Up @@ -15516,7 +15522,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "OUTPUT_DIR=\"${SRCROOT}/Client/Generated/Metrics/\"\n# remove old Metrics file if present\nrm \"${SRCROOT}/Client/Generated/Metrics.swift\"\nbash $PWD/bin/sdk_generator.sh -g Glean -o $OUTPUT_DIR\n";
shellScript = "OUTPUT_DIR=\"${SRCROOT}/Client/Generated/Metrics/\"\n# remove old Metrics file if present\nrm \"${SRCROOT}/Client/Generated/Metrics/Metrics.swift\"\nbash $PWD/bin/sdk_generator.sh -g Glean -o $OUTPUT_DIR\n";
};
5FA2232C27F6FA69005B3D87 /* Nimbus Feature Manifest Generator Script */ = {
isa = PBXShellScriptBuildPhase;
Expand Down Expand Up @@ -16094,6 +16100,7 @@
745DAB301CDAAFAA00D44181 /* RecentlyClosedTabsPanel.swift in Sources */,
D0B9483D22A18B78002F4AA1 /* TextFieldTableViewCell.swift in Sources */,
8A454D322CB8170D009436D9 /* PocketManager.swift in Sources */,
80967F802D0A36890057F0C7 /* RecordedNimbusContext.swift in Sources */,
8AF99B4F29EF1BA700108DEC /* BrowserDelegate.swift in Sources */,
C87D8B802818333F00A6307D /* NimbusManager.swift in Sources */,
8A4AC0EB28C929D700439F83 /* URLSessionDataTaskProtocol.swift in Sources */,
Expand Down Expand Up @@ -16944,6 +16951,7 @@
2F697F7E1A9FD22D009E03AE /* SearchEnginesManagerTests.swift in Sources */,
2197DF8A287624BF00215624 /* LibraryViewModelTests.swift in Sources */,
5A3A7DDC2889EC5D0065F81A /* BookmarksHandlerMock.swift in Sources */,
8015C8B52D0A31FE0093AED0 /* RecordedNimbusContextTests.swift in Sources */,
2F44FA1B1A9D426A00FD20CC /* TestHashExtensions.swift in Sources */,
5AF6254928A58BB400A90253 /* MockHistoryHighlightsDelegate.swift in Sources */,
8A04136B2825ABEA00D20B10 /* SponsoredTileTelemetryTests.swift in Sources */,
Expand Down
2 changes: 1 addition & 1 deletion firefox-ios/Client/Application/AppLaunchUtil.swift
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ class AppLaunchUtil {
}

private func initializeExperiments() {
Experiments.intialize()
Experiments.initialize()
}

private func updateSessionCount() {
Expand Down
24 changes: 13 additions & 11 deletions firefox-ios/Client/Experiments/Experiments.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import Common
import Foundation
import Shared

import struct MozillaAppServices.NimbusAppSettings
import class MozillaAppServices.NimbusBuilder
import class MozillaAppServices.NimbusDisabled
import typealias MozillaAppServices.NimbusErrorReporter
import protocol MozillaAppServices.NimbusEventStore
import protocol MozillaAppServices.NimbusInterface
import protocol MozillaAppServices.NimbusMessagingHelperProtocol
import struct MozillaAppServices.NimbusAppSettings
import typealias MozillaAppServices.NimbusErrorReporter

private let nimbusAppName = "firefox_ios"
private let NIMBUS_URL_KEY = "NimbusURL"
Expand Down Expand Up @@ -78,6 +78,7 @@ enum Experiments {
shared.globalUserParticipation = studiesSetting && telemetrySetting
}
}

static func setLocalExperimentData(payload: String?, storage: UserDefaults = .standard) {
guard let payload = payload else {
storage.removeObject(forKey: NIMBUS_LOCAL_DATA_KEY)
Expand Down Expand Up @@ -166,7 +167,7 @@ enum Experiments {
private static func getAppSettings(isFirstRun: Bool) -> NimbusAppSettings {
let isPhone = UIDevice.current.userInterfaceIdiom == .phone

let customTargetingAttributes: [String: Any] = [
let customTargetingAttributes: [String: Any] = [
"isFirstRun": "\(isFirstRun)",
"is_first_run": isFirstRun,
"is_phone": isPhone,
Expand Down Expand Up @@ -203,9 +204,13 @@ enum Experiments {
let bundles = [
Bundle.main,
Strings.bundle,
Strings.bundle.fallbackTranslationBundle(language: "en-US")
Strings.bundle.fallbackTranslationBundle(language: "en-US"),
].compactMap { $0 }

let nimbusRecordedContext = RecordedNimbusContext(isFirstRun: isFirstRun,
isReviewCheckerEnabled: isReviewCheckerEnabled(),
isDefaultBrowser: true) // TODO: Update after https://github.com/mozilla-mobile/firefox-ios/pull/23754 merges

return NimbusBuilder(dbPath: dbPath)
.with(url: remoteSettingsURL)
.using(previewCollection: usePreviewCollection())
Expand All @@ -215,6 +220,7 @@ enum Experiments {
.with(bundles: bundles)
.with(featureManifest: FxNimbus.shared)
.with(commandLineArgs: CommandLine.arguments)
.with(recordedContext: nimbusRecordedContext)
.build(appInfo: getAppSettings(isFirstRun: isFirstRun))
}

Expand All @@ -228,7 +234,7 @@ enum Experiments {
/// - Parameters:
/// - fireURL: an optional file URL that stores the initial experiments document.
/// - firstRun: a flag indicating that this is the first time that the app has been run.
static func intialize() {
static func initialize() {
// Getting the singleton first time initializes it.
let nimbus = Experiments.shared

Expand All @@ -249,13 +255,9 @@ extension Experiments {
return try? sdk.createMessageHelper(additionalContext: context)
}

public static var messaging: GleanPlumbMessageManagerProtocol = {
GleanPlumbMessageManager()
}()
public static var messaging: GleanPlumbMessageManagerProtocol = GleanPlumbMessageManager()

public static var events: NimbusEventStore = {
sdk.events
}()
public static var events: NimbusEventStore = sdk.events

public static var sdk: NimbusInterface = shared
}
Expand Down
191 changes: 191 additions & 0 deletions firefox-ios/Client/Experiments/RecordedNimbusContext.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
// 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 Common
import Foundation
import Glean
import Shared

import func MozillaAppServices.getCalculatedAttributes
import func MozillaAppServices.getLocaleTag
import struct MozillaAppServices.JsonObject
import protocol MozillaAppServices.RecordedContext
import MozillaRustComponents

private extension Double? {
func toInt64() -> Int64? {
guard let self = self else { return nil }
return Int64(self)
}
}

private extension Int32? {
func toInt64() -> Int64? {
guard let self = self else { return nil }
return Int64(self)
}
}

class RecordedNimbusContext: RecordedContext {
/**
* The following constants are string constants of the keys that appear in the [EVENT_QUERIES] map.
*/
static let DAYS_OPENED_IN_LAST_28: String = "days_opened_in_last_28"

/**
* [EVENT_QUERIES] is a map of keys to Nimbus SDK EventStore queries.
*/
static let EVENT_QUERIES = [
DAYS_OPENED_IN_LAST_28: "'events.app_opened'|eventCountNonZero('Days', 28, 0)",
]

var isFirstRun: Bool
var isPhone: Bool
var isReviewCheckerEnabled: Bool
var isDefaultBrowser: Bool
var appVersion: String?
var region: String?
var language: String?
var locale: String
var daysSinceInstall: Int32?
var daysSinceUpdate: Int32?

private var eventQueries: [String: String]
private var eventQueryValues: [String: Double] = [:]

private var logger: Logger

init(isFirstRun: Bool,
isReviewCheckerEnabled: Bool,
isDefaultBrowser: Bool,
eventQueries: [String: String] = RecordedNimbusContext.EVENT_QUERIES,
isPhone: Bool = UIDevice.current.userInterfaceIdiom == .phone,
bundle: Bundle = Bundle.main,
logger: Logger = DefaultLogger.shared) {
self.logger = logger
logger.log("init start", level: .debug, category: .experiments)
self.eventQueries = eventQueries

self.isFirstRun = isFirstRun
self.isPhone = isPhone
self.isReviewCheckerEnabled = isReviewCheckerEnabled
self.isDefaultBrowser = isDefaultBrowser

let info = bundle.infoDictionary ?? [:]
appVersion = info["CFBundleShortVersionString"] as? String

locale = getLocaleTag()
var inferredDateInstalledOn: Date? {
guard
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last,
let attributes = try? FileManager.default.attributesOfItem(atPath: documentsURL.path)
else { return nil }
return attributes[.creationDate] as? Date
}
let installationDateSinceEpoch = inferredDateInstalledOn.map {
Int64(($0.timeIntervalSince1970 * 1000).rounded())
}
guard let dbPath = Experiments.dbPath else {
self.logger.log("Unable to obtain dbPath, skipping calculating attributes",
level: .warning,
category: .experiments)
return
}
guard let calculatedAttributes = try? getCalculatedAttributes(installationDate: installationDateSinceEpoch,
dbPath: dbPath,
locale: locale)
else { return }

daysSinceInstall = calculatedAttributes.daysSinceInstall
daysSinceUpdate = calculatedAttributes.daysSinceUpdate
language = calculatedAttributes.language
region = calculatedAttributes.region
self.logger.log("init end", level: .debug, category: .experiments)
}

/**
* [getEventQueries] is called by the Nimbus SDK Rust code to retrieve the map of event
* queries. The are then executed against the Nimbus SDK's EventStore to retrieve their values.
*
* @return Map<String, String>
*/
func getEventQueries() -> [String: String] {
logger.log("getEventQueries", level: .debug, category: .experiments)
return eventQueries
}

/**
* [record] is called when experiment enrollments are evolved. It should apply the
* [RecordedNimbusContext]'s values to a [NimbusSystem.RecordedNimbusContextObject] instance,
* and use that instance to record the values to Glean.
*/
func record() {
logger.log("record start", level: .debug, category: .experiments)
let eventQueryValuesObject = GleanMetrics.NimbusSystem.RecordedNimbusContextObjectItemEventQueryValuesObject(
daysOpenedInLast28: eventQueryValues[RecordedNimbusContext.DAYS_OPENED_IN_LAST_28].toInt64()
)

GleanMetrics.NimbusSystem.recordedNimbusContext.set(
GleanMetrics.NimbusSystem.RecordedNimbusContextObject(
isFirstRun: isFirstRun,
eventQueryValues: eventQueryValuesObject,
isReviewCheckerEnabled: isReviewCheckerEnabled,
isPhone: isPhone,
appVersion: appVersion,
locale: locale,
daysSinceInstall: daysSinceInstall.toInt64(),
daysSinceUpdate: daysSinceUpdate.toInt64(),
language: language,
region: region,
isDefaultBrowser: isDefaultBrowser
)
)
logger.log("record end", level: .debug, category: .experiments)
}

/**
* [setEventQueryValues] is called by the Nimbus SDK Rust code after the event queries have been
* executed. The [eventQueryValues] should be written back to the Kotlin object.
*
* @param [eventQueryValues] The values for each query after they have been executed in the
* Nimbus SDK Rust environment.
*/
func setEventQueryValues(eventQueryValues: [String: Double]) {
logger.log("setEventQueryValues", level: .debug, category: .experiments)
self.eventQueryValues = eventQueryValues
}

/**
* [toJson] is called by the Nimbus SDK Rust code after the event queries have been executed,
* and before experiment enrollments have been evolved. The value returned from this method
* will be applied directly to the Nimbus targeting context, and its keys/values take
* precedence over those in the main Nimbus targeting context.
*
* @return JsonObject
*/
func toJson() -> JsonObject {
logger.log("toJson start", level: .debug, category: .experiments)
guard let data = try? JSONSerialization.data(withJSONObject: [
"is_first_run": isFirstRun,
"isFirstRun": "\(isFirstRun)",
"is_phone": isPhone,
"is_review_checker_enabled": isReviewCheckerEnabled,
"events": eventQueryValues,
"app_version": appVersion as Any,
"region": region as Any,
"language": language as Any,
"locale": locale as Any,
"days_since_install": daysSinceInstall as Any,
"days_since_update": daysSinceUpdate as Any,
"is_default_browser": isDefaultBrowser,
]),
let jsonString = NSString(data: data, encoding: String.Encoding.utf8.rawValue) as? String
else {
logger.log("toJson error thrown while creating JSON string", level: .warning, category: .experiments)
return "{}"
}
logger.log("toJson end", level: .debug, category: .experiments, extra: ["json": jsonString])
return jsonString
}
}
44 changes: 44 additions & 0 deletions firefox-ios/Client/metrics.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6121,3 +6121,47 @@ server_knobs:
- [email protected]
- [email protected]
expires: "2025-07-01"

nimbus_system:
recorded_nimbus_context:
type: object
structure:
type: object
properties:
is_first_run:
type: boolean
event_query_values:
type: object
properties:
days_opened_in_last_28:
type: number
is_review_checker_enabled:
type: boolean
is_phone:
type: boolean
app_version:
type: string
locale:
type: string
days_since_install:
type: number
days_since_update:
type: number
language:
type: string
region:
type: string
is_default_browser:
type: boolean
description: |
The Nimbus context object that is recorded to Glean
bugs:
- 'https://github.com/mozilla-mobile/firefox-ios/issues/23736'
data_reviews:
- 'https://github.com/mozilla-mobile/firefox-ios/pull/23737#issuecomment-2541639916'
data_sensitivity:
- interaction
notification_emails:
- [email protected]
- [email protected]
expires: never
Loading

0 comments on commit a1a3226

Please sign in to comment.