Skip to content
Closed
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
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
beta: 83e871927810f621bb3067b265649aa9505ad84f
main: e374045fac6f19383a04d087d2930ced5c1551fe
main: 6e32b4962597e60549f9fc289aa627cae508887c
release: 91d93ddd290c6fc70df44dbdc363fc5a0a6306c5
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ beta: 83e871927810f621bb3067b265649aa9505ad84f
esr115: 84341e2725284f1e82f53127f28289454daee783
esr128: ed38f9209e39bd7ad247c81a7c20c99c874e0a62
esr140: ef9fef7a9928dfbea50e89a256d44492c2d28ca1
main: e374045fac6f19383a04d087d2930ced5c1551fe
main: 6e32b4962597e60549f9fc289aa627cae508887c
release: 91d93ddd290c6fc70df44dbdc363fc5a0a6306c5
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ firefox-v150.0: a931ff36e4b0152c6a0e9d98a5106fa849a5c1d3
firefox-v150.1: 341e96bda4dd657970d3c0db7f71b08f9eadb395
firefox-v150.2: e50ecad2a9ff5721f95e66419b3c025aaa4b951c
firefox-v150.3: f8f596bfd2b5ca83a77d8bf54eb3d2cbec395671
main: a1f774839a3023e070a74b13f1773af661872e77
firefox-v151.0: c5f54f750bc1c32c555647aee66f17820eaac86b
main: e5dd824f75bff304dbe19b8a08bb0777d9393f95
release/v117: 43b690c450066d4dde1b2ccb93ed3714d244d502
release/v118: 89a7ea3c3372e5a4ef5b3c5b85499d26198d0524
release/v119: 7dc381f991c6d2a983c3ba7f0cffd880e8c29e7b
Expand Down Expand Up @@ -150,7 +151,7 @@ release/v150.0: 251a757be907f072173c4173dae2804c79e0170d
release/v150.1: 9998525d32525e507ad894bf734cc89d19f6ba62
release/v150.2: 3105b1d8b15ad82a4794b15ba3e09ed0bd7801d6
release/v150.3: a88b27cb23568f0b7e8f7e782e393a882e74fd6e
release/v151.0: c5f54f750bc1c32c555647aee66f17820eaac86b
release/v151.0: 86de6b1abdcd59e48dfa236ee323496583b24168
v117.0: 3b275ab1e4ccef769d84437033c0c68acfd2df8a
v117.1: 55ddcf473a65d106d43f6d2ae5af4076d6d03688
v117.2: 5cc367aa8857199cd8a54d53e8faf6696c5614c3
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
// 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 class MozillaAppServices.NimbusGleanPings
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)
}
}

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

/// TODO(FXIOS-12942): Implement proper thread-safety
final class RecordedNimbusContext: RecordedContext, @unchecked Sendable {
/**
* The following constants are string constants of the keys that appear in the [EVENT_QUERIES] map.
*/
static let DAYS_OPENED_IN_LAST_28 = "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 isDefaultBrowser: Bool
var isBottomToolbarUser: Bool
var hasEnabledTipsNotifications: Bool
var hasAcceptedTermsOfUse: Bool
var userDisabledAi: Bool
var isAppleIntelligenceAvailable: Bool
var cannotUseAppleIntelligence: Bool
var appVersion: String?
var region: String?
var language: String?
var locale: String
var daysSinceInstall: Int32?
var daysSinceUpdate: Int32?
var touExperiencePoints: Int32?

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

private var logger: Logger

init(isFirstRun: Bool,
isDefaultBrowser: Bool,
isBottomToolbarUser: Bool,
hasEnabledTipsNotifications: Bool,
hasAcceptedTermsOfUse: Bool,
userDisabledAi: Bool,
isAppleIntelligenceAvailable: Bool,
cannotUseAppleIntelligence: Bool,
eventQueries: [String: String] = RecordedNimbusContext.EVENT_QUERIES,
isPhone: Bool = UIDeviceDetails.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.isDefaultBrowser = isDefaultBrowser
self.isBottomToolbarUser = isBottomToolbarUser
self.hasEnabledTipsNotifications = hasEnabledTipsNotifications
self.hasAcceptedTermsOfUse = hasAcceptedTermsOfUse
self.userDisabledAi = userDisabledAi
self.isAppleIntelligenceAvailable = isAppleIntelligenceAvailable
self.cannotUseAppleIntelligence = cannotUseAppleIntelligence

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
touExperiencePoints = Experiments.touExperiencePoints(region: 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)

// Bring the ping into scope so that Glean knows it exists and includes NimbusSystem.recordedNimbusContext
_ = NimbusGleanPings.nimbusTargetingContext

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,
isPhone: isPhone,
appVersion: appVersion,
locale: locale,
daysSinceInstall: daysSinceInstall.toInt64(),
daysSinceUpdate: daysSinceUpdate.toInt64(),
language: language,
region: region,
isDefaultBrowser: isDefaultBrowser,
isBottomToolbarUser: isBottomToolbarUser,
hasEnabledTipsNotifications: hasEnabledTipsNotifications,
hasAcceptedTermsOfUse: hasAcceptedTermsOfUse,
userDisabledAi: userDisabledAi,
isAppleIntelligenceAvailable: isAppleIntelligenceAvailable,
cannotUseAppleIntelligence: cannotUseAppleIntelligence,
touExperiencePoints: touExperiencePoints.toInt64()
)
)
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,
"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,
"is_bottom_toolbar_user": isBottomToolbarUser,
"has_enabled_tips_notifications": hasEnabledTipsNotifications,
"has_accepted_terms_of_use": hasAcceptedTermsOfUse,
"user_disabled_ai": userDisabledAi,
"is_apple_intelligence_available": isAppleIntelligenceAvailable,
"cannot_use_apple_intelligence": cannotUseAppleIntelligence,
"tou_experience_points": touExperiencePoints as Any
]),
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
}
}
Loading
Loading