diff --git a/CHANGES_UNRELEASED.md b/CHANGES_UNRELEASED.md index cff600230..30ec4b6c2 100644 --- a/CHANGES_UNRELEASED.md +++ b/CHANGES_UNRELEASED.md @@ -24,3 +24,6 @@ Use the template below to make assigning a version number during the release cut ### ✨ What's New ✨ - `active_experiments` is available to JEXL as a set containing slugs of all enrolled experiments ([#5227](https://github.com/mozilla/application-services/pull/5227)) - Added query method for behavioral targeting event store ([#5226](https://github.com/mozilla/application-services/pull/5226)) + +### ⚠️ Breaking Changes ⚠️ + - Changed the type of `customTargetingAttributes` in `NimbusAppSettings` to a `JSONObject`. The change will be breaking only for Android. ([#5229](https://github.com/mozilla/application-services/pull/5229)) diff --git a/components/nimbus/android/src/main/java/org/mozilla/experiments/nimbus/Nimbus.kt b/components/nimbus/android/src/main/java/org/mozilla/experiments/nimbus/Nimbus.kt index 0585252e0..a11596c21 100644 --- a/components/nimbus/android/src/main/java/org/mozilla/experiments/nimbus/Nimbus.kt +++ b/components/nimbus/android/src/main/java/org/mozilla/experiments/nimbus/Nimbus.kt @@ -309,7 +309,7 @@ data class NimbusAppInfo( * * Example: mapOf("userType": "casual", "isFirstTime": "true") */ - val customTargetingAttributes: Map = mapOf() + val customTargetingAttributes: JSONObject = JSONObject() ) /** diff --git a/components/nimbus/ios/Nimbus/Dictionary+.swift b/components/nimbus/ios/Nimbus/Dictionary+.swift new file mode 100644 index 000000000..00cc26e96 --- /dev/null +++ b/components/nimbus/ios/Nimbus/Dictionary+.swift @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla + * 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 Foundation + +internal extension Dictionary where Key == String, Value == Any { + func stringify() throws -> String { + let data = try JSONSerialization.data(withJSONObject: self) + guard let s = String(data: data, encoding: .utf8) else { + throw NimbusError.JsonError(message: "Unable to encode") + } + return s + } + + static func parse(jsonString string: String) throws -> [String: Any] { + guard let data = string.data(using: .utf8) else { + throw NimbusError.JsonError(message: "Unable to decode string into data") + } + let obj = try JSONSerialization.jsonObject(with: data) + guard let obj = obj as? [String: Any] else { + throw NimbusError.JsonError(message: "Unable to cast into JSONObject") + } + return obj + } +} diff --git a/components/nimbus/ios/Nimbus/Nimbus.swift b/components/nimbus/ios/Nimbus/Nimbus.swift index e46dd2457..5712af02d 100644 --- a/components/nimbus/ios/Nimbus/Nimbus.swift +++ b/components/nimbus/ios/Nimbus/Nimbus.swift @@ -139,13 +139,10 @@ extension Nimbus: FeaturesInterface { internal func getFeatureConfigVariablesJson(featureId: String) -> [String: Any]? { do { - if let string = try nimbusClient.getFeatureConfigVariables(featureId: featureId), - let data = string.data(using: .utf8) - { - return try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] - } else { + guard let string = try nimbusClient.getFeatureConfigVariables(featureId: featureId) else { return nil } + return try Dictionary.parse(jsonString: string) } catch NimbusError.DatabaseNotReady { GleanMetrics.NimbusHealth.cacheNotReadyForFeature.record( GleanMetrics.NimbusHealth.CacheNotReadyForFeatureExtra( @@ -324,8 +321,7 @@ extension Nimbus: GleanPlumbProtocol { } public func createMessageHelper(additionalContext: [String: Any]) throws -> GleanPlumbMessageHelper { - let data = try JSONSerialization.data(withJSONObject: additionalContext, options: []) - let string = String(data: data, encoding: .utf8) + let string = try additionalContext.stringify() return try createMessageHelper(string: string) } diff --git a/components/nimbus/ios/Nimbus/NimbusApi.swift b/components/nimbus/ios/Nimbus/NimbusApi.swift index c067d7082..18332cd48 100644 --- a/components/nimbus/ios/Nimbus/NimbusApi.swift +++ b/components/nimbus/ios/Nimbus/NimbusApi.swift @@ -164,7 +164,7 @@ public let remoteSettingsCollection = "nimbus-mobile-experiments" /// The specifc context is there to capture any context that the SDK doesn't need to be explictly aware of. /// public struct NimbusAppSettings { - public init(appName: String, channel: String, customTargetingAttributes: [String: String] = [String: String]()) { + public init(appName: String, channel: String, customTargetingAttributes: [String: Any] = [String: Any]()) { self.appName = appName self.channel = channel self.customTargetingAttributes = customTargetingAttributes @@ -172,7 +172,7 @@ public struct NimbusAppSettings { public let appName: String public let channel: String - public let customTargetingAttributes: [String: String] + public let customTargetingAttributes: [String: Any] } /// This error reporter is passed to `Nimbus` and any errors that are caught are reported via this type. diff --git a/components/nimbus/ios/Nimbus/NimbusCreate.swift b/components/nimbus/ios/Nimbus/NimbusCreate.swift index 45308b95f..39b0a6a99 100644 --- a/components/nimbus/ios/Nimbus/NimbusCreate.swift +++ b/components/nimbus/ios/Nimbus/NimbusCreate.swift @@ -95,7 +95,7 @@ public extension Nimbus { debugTag: "Nimbus.rs", installationDate: installationDateSinceEpoch, homeDirectory: nil, - customTargetingAttributes: appSettings.customTargetingAttributes + customTargetingAttributes: try? appSettings.customTargetingAttributes.stringify() ) } } diff --git a/components/nimbus/src/matcher.rs b/components/nimbus/src/matcher.rs index dd8c7c785..bdb01a892 100644 --- a/components/nimbus/src/matcher.rs +++ b/components/nimbus/src/matcher.rs @@ -9,7 +9,7 @@ //! provided by the consuming client. //! use serde_derive::*; -use std::collections::HashMap; +use serde_json::{Map, Value}; /// The `AppContext` object represents the parameters and characteristics of the /// consuming application that we are interested in for targeting purposes. The /// `app_name` and `channel` fields are not optional as they are expected @@ -52,5 +52,5 @@ pub struct AppContext { pub installation_date: Option, pub home_directory: Option, #[serde(flatten)] - pub custom_targeting_attributes: Option>, + pub custom_targeting_attributes: Option>, } diff --git a/components/nimbus/src/nimbus.udl b/components/nimbus/src/nimbus.udl index 068d2a482..45f24ac3f 100644 --- a/components/nimbus/src/nimbus.udl +++ b/components/nimbus/src/nimbus.udl @@ -17,7 +17,7 @@ dictionary AppContext { // the unix time, which is milliseconds since epoch i64? installation_date; string? home_directory; - record? custom_targeting_attributes; + JsonObject? custom_targeting_attributes; }; dictionary EnrolledExperiment { diff --git a/components/nimbus/src/tests/test_evaluator.rs b/components/nimbus/src/tests/test_evaluator.rs index 8d87ea56c..1307789ff 100644 --- a/components/nimbus/src/tests/test_evaluator.rs +++ b/components/nimbus/src/tests/test_evaluator.rs @@ -2,6 +2,8 @@ * 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/. */ +use serde_json::{json, Map, Value}; + use crate::enrollment::{EnrolledReason, EnrollmentStatus, NotEnrolledReason}; use crate::evaluator::{choose_branch, targeting}; use crate::{ @@ -327,17 +329,17 @@ fn test_targeting() { }) )); } -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; #[test] fn test_targeting_custom_targeting_attributes() { // Here's our valid jexl statement let expression_statement = - "app_id == '1010' && (app_version == '4.4' || locale == \"en-US\") && is_first_run == 'true' && ios_version == '8.8'"; + "app_id == '1010' && (app_version == '4.4' || locale == \"en-US\") && is_first_run == true && ios_version == '8.8'"; - let mut custom_targeting_attributes = HashMap::new(); - custom_targeting_attributes.insert("is_first_run".into(), "true".into()); - custom_targeting_attributes.insert("ios_version".into(), "8.8".into()); + let mut custom_targeting_attributes = Map::::new(); + custom_targeting_attributes.insert("is_first_run".into(), json!(true)); + custom_targeting_attributes.insert("ios_version".into(), json!("8.8")); // A matching context that includes the appropriate specific context let targeting_attributes = AppContext { app_name: "nimbus_test".to_string(), diff --git a/megazords/ios-rust/MozillaTestServices/MozillaTestServices.xcodeproj/project.pbxproj b/megazords/ios-rust/MozillaTestServices/MozillaTestServices.xcodeproj/project.pbxproj index b2f2863bb..ffd22ce5c 100644 --- a/megazords/ios-rust/MozillaTestServices/MozillaTestServices.xcodeproj/project.pbxproj +++ b/megazords/ios-rust/MozillaTestServices/MozillaTestServices.xcodeproj/project.pbxproj @@ -58,6 +58,7 @@ 1BF50F1927B1E19500A9C8A5 /* FxAccountManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BBAC54127AE065300DAFEF2 /* FxAccountManagerTests.swift */; }; 1BF50F1A27B1E19800A9C8A5 /* FxAccountMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BBAC53C27AE065300DAFEF2 /* FxAccountMocks.swift */; }; 1BFC469827C99F250034E0A5 /* Metrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BFC469627C99F250034E0A5 /* Metrics.swift */; }; + 3963A5862919A541001ED4C3 /* Dictionary+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3963A5852919A541001ED4C3 /* Dictionary+.swift */; }; 45CC574A28AD9C86006D55AA /* errorsupport.udl in Sources */ = {isa = PBXBuildFile; fileRef = 45CC574828AD9C31006D55AA /* errorsupport.udl */; }; /* End PBXBuildFile section */ @@ -167,6 +168,7 @@ 1BF50F2327B1E53E00A9C8A5 /* metrics.yaml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; name = metrics.yaml; path = ../../../components/nimbus/metrics.yaml; sourceTree = SOURCE_ROOT; }; 1BF50F2B27B1EB7D00A9C8A5 /* sdk_generator.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; name = sdk_generator.sh; path = "../../../../../components/external/glean/glean-core/ios/sdk_generator.sh"; sourceTree = SOURCE_ROOT; }; 1BFC469627C99F250034E0A5 /* Metrics.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Metrics.swift; path = MozillaTestServices/Generated/Metrics.swift; sourceTree = SOURCE_ROOT; }; + 3963A5852919A541001ED4C3 /* Dictionary+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "Dictionary+.swift"; path = "Nimbus/Dictionary+.swift"; sourceTree = ""; }; 45CC574528AD9C0B006D55AA /* errorFFI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = errorFFI.h; path = ../../../../components/support/error/ios/Generated/errorFFI.h; sourceTree = ""; }; 45CC574628AD9C0B006D55AA /* errorsupport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = errorsupport.swift; path = ../../../../components/support/error/ios/Generated/errorsupport.swift; sourceTree = ""; }; 45CC574828AD9C31006D55AA /* errorsupport.udl */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = errorsupport.udl; path = ../../../../components/support/error/src/errorsupport.udl; sourceTree = ""; }; @@ -199,6 +201,7 @@ 1BF50EF327B1DD7D00A9C8A5 /* Generated */, 1B3BC98E27B1D9D600229CF6 /* nimbus.udl */, 1B3BC97527B1D9B700229CF6 /* Collections+.swift */, + 3963A5852919A541001ED4C3 /* Dictionary+.swift */, 1B3BC97627B1D9B700229CF6 /* FeatureHolder.swift */, 1B3BC97B27B1D9B700229CF6 /* FeatureInterface.swift */, 1B3BC97927B1D9B700229CF6 /* FeatureVariables.swift */, @@ -635,6 +638,7 @@ 1B3BC94D27B1D7B700229CF6 /* RustLog.swift in Sources */, 1BF50F1627B1E18000A9C8A5 /* KeychainWrapper.swift in Sources */, 1BF50F1227B1E17B00A9C8A5 /* FxAccountManager.swift in Sources */, + 3963A5862919A541001ED4C3 /* Dictionary+.swift in Sources */, 1B3BC98D27B1D9B800229CF6 /* NimbusCreate.swift in Sources */, 1B3BC98727B1D9B800229CF6 /* Nimbus.swift in Sources */, 1B3BC98827B1D9B800229CF6 /* FeatureInterface.swift in Sources */, diff --git a/megazords/ios-rust/MozillaTestServices/MozillaTestServicesTests/NimbusTests.swift b/megazords/ios-rust/MozillaTestServices/MozillaTestServicesTests/NimbusTests.swift index fd3759128..238160d2f 100644 --- a/megazords/ios-rust/MozillaTestServices/MozillaTestServicesTests/NimbusTests.swift +++ b/megazords/ios-rust/MozillaTestServices/MozillaTestServicesTests/NimbusTests.swift @@ -373,6 +373,15 @@ class NimbusTests: XCTestCase { ) XCTAssertNotNil(disqualificationEventExtras!["enrollment_id"], "Experiment enrollment id must not be nil") } + + func testNimbusCreateWithJson() throws { + let appSettings = NimbusAppSettings(appName: "test", channel: "nightly", customTargetingAttributes: ["is_first_run": false, "is_test": true]) + let nimbus = try Nimbus.create(nil, appSettings: appSettings, dbPath: createDatabasePath()) + let helper = try nimbus.createMessageHelper() + + XCTAssertTrue(try helper.evalJexl(expression: "is_test")) + XCTAssertFalse(try helper.evalJexl(expression: "is_first_run")) + } } private extension Device {