Преглед на файлове

[Config] Port 'ConfigExperiment' to Swift (#14179)

Nick Cooke преди 1 година
родител
ревизия
0092ca798e

+ 2 - 0
FirebaseABTesting/Sources/ABTExperimentPayload.m

@@ -14,6 +14,8 @@
 
 #import "FirebaseABTesting/Sources/Private/ABTExperimentPayload.h"
 
+#import "FirebaseABTesting/Sources/Public/FirebaseABTesting/FIRExperimentController.h"
+
 static NSString *const kExperimentPayloadKeyExperimentID = @"experimentId";
 static NSString *const kExperimentPayloadKeyVariantID = @"variantId";
 

+ 2 - 9
FirebaseABTesting/Sources/Private/ABTExperimentPayload.h

@@ -14,16 +14,9 @@
 
 #import <Foundation/Foundation.h>
 
-NS_ASSUME_NONNULL_BEGIN
+#import "FirebaseABTesting/Sources/Public/FirebaseABTesting/FIRExperimentController.h"
 
-/// Policy for handling the case where there's an overflow of experiments for an installation
-/// instance.
-typedef NS_ENUM(int32_t, ABTExperimentPayloadExperimentOverflowPolicy) {
-  ABTExperimentPayloadExperimentOverflowPolicyUnrecognizedValue = 999,
-  ABTExperimentPayloadExperimentOverflowPolicyUnspecified = 0,
-  ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest = 1,
-  ABTExperimentPayloadExperimentOverflowPolicyIgnoreNewest = 2,
-};
+NS_ASSUME_NONNULL_BEGIN
 
 @interface ABTExperimentLite : NSObject
 @property(nonatomic, readonly, copy) NSString *experimentId;

+ 10 - 4
FirebaseABTesting/Sources/Public/FirebaseABTesting/FIRExperimentController.h

@@ -14,15 +14,21 @@
 
 #import <Foundation/Foundation.h>
 
+#import "FIRLifecycleEvents.h"
+
 @class ABTExperimentPayload;
 
-// Forward declaration to avoid importing into the module header
-typedef NS_ENUM(int32_t, ABTExperimentPayloadExperimentOverflowPolicy);
+/// Policy for handling the case where there's an overflow of experiments for an installation
+/// instance.
+typedef NS_ENUM(int32_t, ABTExperimentPayloadExperimentOverflowPolicy) {
+  ABTExperimentPayloadExperimentOverflowPolicyUnrecognizedValue = 999,
+  ABTExperimentPayloadExperimentOverflowPolicyUnspecified = 0,
+  ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest = 1,
+  ABTExperimentPayloadExperimentOverflowPolicyIgnoreNewest = 2,
+};
 
 NS_ASSUME_NONNULL_BEGIN
 
-@class FIRLifecycleEvents;
-
 /// The default experiment overflow policy, that is to discard the experiment with the oldest start
 /// time when users start the experiment on the web console.
 extern const ABTExperimentPayloadExperimentOverflowPolicy FIRDefaultExperimentOverflowPolicy;

+ 0 - 1
FirebaseRemoteConfig/Sources/FIRRemoteConfig.m

@@ -23,7 +23,6 @@
 #import "FirebaseRemoteConfig/Sources/Private/RCNConfigFetch.h"
 #import "FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h"
 #import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h"
-#import "FirebaseRemoteConfig/Sources/RCNConfigExperiment.h"
 #import "FirebaseRemoteConfig/Sources/RCNConfigRealtime.h"
 #import "FirebaseRemoteConfig/Sources/RCNConfigValue_Internal.h"
 #import "FirebaseRemoteConfig/Sources/RCNPersonalization.h"

+ 0 - 38
FirebaseRemoteConfig/Sources/RCNConfigExperiment.h

@@ -1,38 +0,0 @@
-/*
- * Copyright 2019 Google
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#import <Foundation/Foundation.h>
-
-@class FIRExperimentController;
-@class RCNConfigDBManager;
-
-/// Handles experiment information update and persistence.
-@interface RCNConfigExperiment : NSObject
-
-/// Designated initializer;
-- (nonnull instancetype)initWithDBManager:(RCNConfigDBManager *_Nullable)DBManager
-                     experimentController:(FIRExperimentController *_Nullable)controller
-    NS_DESIGNATED_INITIALIZER;
-
-/// Use `initWithDBManager:` instead.
-- (nonnull instancetype)init NS_UNAVAILABLE;
-
-/// Update/Persist experiment information from config fetch response.
-- (void)updateExperimentsWithResponse:(NSArray<NSDictionary<NSString *, id> *> *_Nullable)response;
-
-/// Update experiments to Firebase Analytics when `activateWithCompletion:` happens.
-- (void)updateExperimentsWithHandler:(nullable void (^)(NSError *_Nullable error))handler;
-@end

+ 0 - 202
FirebaseRemoteConfig/Sources/RCNConfigExperiment.m

@@ -1,202 +0,0 @@
-/*
- * Copyright 2019 Google
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#import "FirebaseRemoteConfig/Sources/RCNConfigExperiment.h"
-
-#import "FirebaseABTesting/Sources/Private/FirebaseABTestingInternal.h"
-#import "FirebaseCore/Extension/FirebaseCoreInternal.h"
-#import "FirebaseRemoteConfig/Sources/RCNConfigDefines.h"
-
-#import "FirebaseRemoteConfig/FirebaseRemoteConfig-Swift.h"
-
-static NSString *const kExperimentMetadataKeyLastStartTime = @"last_experiment_start_time";
-
-static NSString *const kServiceOrigin = @"frc";
-static NSString *const kMethodNameLatestStartTime =
-    @"latestExperimentStartTimestampBetweenTimestamp:andPayloads:";
-
-@interface RCNConfigExperiment ()
-@property(nonatomic, strong)
-    NSMutableArray<NSData *> *experimentPayloads;  ///< Experiment payloads.
-@property(nonatomic, strong)
-    NSMutableDictionary<NSString *, id> *experimentMetadata;  ///< Experiment metadata
-@property(nonatomic, strong)
-    NSMutableArray<NSData *> *activeExperimentPayloads;      ///< Activated experiment payloads.
-@property(nonatomic, strong) RCNConfigDBManager *DBManager;  ///< Database Manager.
-@property(nonatomic, strong) FIRExperimentController *experimentController;
-@property(nonatomic, strong) NSDateFormatter *experimentStartTimeDateFormatter;
-@end
-
-@implementation RCNConfigExperiment
-/// Designated initializer
-- (instancetype)initWithDBManager:(RCNConfigDBManager *)DBManager
-             experimentController:(FIRExperimentController *)controller {
-  self = [super init];
-  if (self) {
-    _experimentPayloads = [[NSMutableArray alloc] init];
-    _experimentMetadata = [[NSMutableDictionary alloc] init];
-    _activeExperimentPayloads = [[NSMutableArray alloc] init];
-    _experimentStartTimeDateFormatter = [[NSDateFormatter alloc] init];
-    [_experimentStartTimeDateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"];
-    [_experimentStartTimeDateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
-    // Locale needs to be hardcoded. See
-    // https://developer.apple.com/library/ios/#qa/qa1480/_index.html for more details.
-    [_experimentStartTimeDateFormatter
-        setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]];
-    [_experimentStartTimeDateFormatter setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"UTC"]];
-
-    _DBManager = DBManager;
-    _experimentController = controller;
-    [self loadExperimentFromTable];
-  }
-  return self;
-}
-
-typedef void (^RCNDBCompletion)(BOOL success, NSDictionary *result);
-- (void)loadExperimentFromTable {
-  if (!_DBManager) {
-    return;
-  }
-  __weak RCNConfigExperiment *weakSelf = self;
-  RCNDBCompletion completionHandler = ^(BOOL success, NSDictionary<NSString *, id> *result) {
-    RCNConfigExperiment *strongSelf = weakSelf;
-    if (strongSelf == nil) {
-      return;
-    }
-    if (result[@RCNExperimentTableKeyPayload]) {
-      [strongSelf->_experimentPayloads removeAllObjects];
-      for (NSData *experiment in result[@RCNExperimentTableKeyPayload]) {
-        NSError *error;
-        id experimentPayloadJSON = [NSJSONSerialization JSONObjectWithData:experiment
-                                                                   options:kNilOptions
-                                                                     error:&error];
-        if (!experimentPayloadJSON || error) {
-          FIRLogWarning(kFIRLoggerRemoteConfig, @"I-RCN000031",
-                        @"Experiment payload could not be parsed as JSON.");
-        } else {
-          [strongSelf->_experimentPayloads addObject:experiment];
-        }
-      }
-    }
-    if (result[@RCNExperimentTableKeyMetadata]) {
-      strongSelf->_experimentMetadata = [result[@RCNExperimentTableKeyMetadata] mutableCopy];
-    }
-
-    /// Load activated experiments payload and metadata.
-    if (result[@RCNExperimentTableKeyActivePayload]) {
-      [strongSelf->_activeExperimentPayloads removeAllObjects];
-      for (NSData *experiment in result[@RCNExperimentTableKeyActivePayload]) {
-        NSError *error;
-        id experimentPayloadJSON = [NSJSONSerialization JSONObjectWithData:experiment
-                                                                   options:kNilOptions
-                                                                     error:&error];
-        if (!experimentPayloadJSON || error) {
-          FIRLogWarning(kFIRLoggerRemoteConfig, @"I-RCN000031",
-                        @"Activated experiment payload could not be parsed as JSON.");
-        } else {
-          [strongSelf->_activeExperimentPayloads addObject:experiment];
-        }
-      }
-    }
-  };
-  [_DBManager loadExperimentWithCompletionHandler:completionHandler];
-}
-
-- (void)updateExperimentsWithResponse:(NSArray<NSDictionary<NSString *, id> *> *)response {
-  // cache fetched experiment payloads.
-  [_experimentPayloads removeAllObjects];
-  [_DBManager deleteExperimentTableForKey:@RCNExperimentTableKeyPayload];
-
-  for (NSDictionary<NSString *, id> *experiment in response) {
-    NSError *error;
-    NSData *JSONPayload = [NSJSONSerialization dataWithJSONObject:experiment
-                                                          options:kNilOptions
-                                                            error:&error];
-    if (!JSONPayload || error) {
-      FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000030",
-                  @"Invalid experiment payload to be serialized.");
-    } else {
-      [_experimentPayloads addObject:JSONPayload];
-      [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyPayload
-                                         value:JSONPayload
-                             completionHandler:nil];
-    }
-  }
-}
-
-- (void)updateExperimentsWithHandler:(void (^)(NSError *_Nullable))handler {
-  FIRLifecycleEvents *lifecycleEvent = [[FIRLifecycleEvents alloc] init];
-
-  // Get the last experiment start time prior to the latest payload.
-  NSTimeInterval lastStartTime =
-      [_experimentMetadata[kExperimentMetadataKeyLastStartTime] doubleValue];
-
-  // Update the last experiment start time with the latest payload.
-  [self updateExperimentStartTime];
-  [self.experimentController
-      updateExperimentsWithServiceOrigin:kServiceOrigin
-                                  events:lifecycleEvent
-                                  policy:ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest
-                           lastStartTime:lastStartTime
-                                payloads:_experimentPayloads
-                       completionHandler:handler];
-
-  /// Update activated experiments payload and metadata in DB.
-  [self updateActiveExperimentsInDB];
-}
-
-- (void)updateExperimentStartTime {
-  NSTimeInterval existingLastStartTime =
-      [_experimentMetadata[kExperimentMetadataKeyLastStartTime] doubleValue];
-
-  NSTimeInterval latestStartTime =
-      [self latestStartTimeWithExistingLastStartTime:existingLastStartTime];
-
-  _experimentMetadata[kExperimentMetadataKeyLastStartTime] = @(latestStartTime);
-
-  if (![NSJSONSerialization isValidJSONObject:_experimentMetadata]) {
-    FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000028",
-                @"Invalid fetched experiment metadata to be serialized.");
-    return;
-  }
-  NSError *error;
-  NSData *serializedExperimentMetadata =
-      [NSJSONSerialization dataWithJSONObject:_experimentMetadata
-                                      options:NSJSONWritingPrettyPrinted
-                                        error:&error];
-  [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyMetadata
-                                     value:serializedExperimentMetadata
-                         completionHandler:nil];
-}
-
-- (void)updateActiveExperimentsInDB {
-  /// Put current fetched experiment payloads into activated experiment DB.
-  [_activeExperimentPayloads removeAllObjects];
-  [_DBManager deleteExperimentTableForKey:@RCNExperimentTableKeyActivePayload];
-  for (NSData *experiment in _experimentPayloads) {
-    [_activeExperimentPayloads addObject:experiment];
-    [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyActivePayload
-                                       value:experiment
-                           completionHandler:nil];
-  }
-}
-
-- (NSTimeInterval)latestStartTimeWithExistingLastStartTime:(NSTimeInterval)existingLastStartTime {
-  return [self.experimentController
-      latestExperimentStartTimestampBetweenTimestamp:existingLastStartTime
-                                         andPayloads:_experimentPayloads];
-}
-@end

+ 0 - 1
FirebaseRemoteConfig/Sources/RCNConfigFetch.m

@@ -22,7 +22,6 @@
 #import "FirebaseInstallations/Source/Library/Private/FirebaseInstallationsInternal.h"
 #import "FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h"
 #import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h"
-#import "FirebaseRemoteConfig/Sources/RCNConfigExperiment.h"
 
 #import "FirebaseRemoteConfig/FirebaseRemoteConfig-Swift.h"
 

+ 193 - 0
FirebaseRemoteConfig/SwiftNew/ConfigExperiment.swift

@@ -0,0 +1,193 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import FirebaseABTesting
+import Foundation
+
+// TODO(ncooke3): Once everything is ported, the `@objc` and `public` access
+// can be removed.
+
+/// Handles experiment information update and persistence.
+@objc(RCNConfigExperiment) public final class ConfigExperiment: NSObject {
+  private static let experimentMetadataKeyLastStartTime = "last_experiment_start_time"
+  private static let serviceOrigin = "frc"
+
+  @objc private var experimentPayloads: [Data]
+  @objc private var experimentMetadata: [String: Any]
+  @objc private var activeExperimentPayloads: [Data]
+  private let dbManager: ConfigDBManager
+  // TODO(ncooke3): This property could be made non-optional after ensuring the
+  // unit tests properly configure the default app. This is because the
+  // experiment controller comes from the ABTesting component.
+  private let experimentController: ExperimentController?
+  private let experimentStartTimeDateFormatter: DateFormatter
+
+  /// Designated initializer;
+  @objc public init(DBManager: ConfigDBManager,
+                    experimentController controller: ExperimentController?) {
+    experimentPayloads = []
+    experimentMetadata = [:]
+    activeExperimentPayloads = []
+    experimentStartTimeDateFormatter = {
+      let dateFormatter = DateFormatter()
+      dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
+      // Locale needs to be hardcoded. See
+      // https://developer.apple.com/library/ios/#qa/qa1480/_index.html for more details.
+      dateFormatter.locale = Locale(identifier: "en_US_POSIX")
+      dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
+      return dateFormatter
+    }()
+    dbManager = DBManager
+    experimentController = controller
+    super.init()
+    loadExperimentFromTable()
+  }
+
+  @objc private func loadExperimentFromTable() {
+    let completionHandler: (Bool, [String: Any]?) -> Void = { [weak self] _, result in
+      guard let self else { return }
+
+      if result?[ConfigConstants.experimentTableKeyPayload] != nil {
+        self.experimentPayloads.removeAll()
+        if let experiments = result?[ConfigConstants.experimentTableKeyPayload] as? [Data] {
+          for experiment in experiments {
+            do {
+              try JSONSerialization.jsonObject(with: experiment)
+              self.experimentPayloads.append(experiment)
+            } catch {
+              RCLog.warning("I-RCN000031", "Experiment payload could not be parsed as JSON.")
+            }
+          }
+        }
+      }
+
+      if let experimentTable =
+        result?[ConfigConstants.experimentTableKeyMetadata] as? [String: Any] {
+        self.experimentMetadata = experimentTable
+      }
+
+      if result?[ConfigConstants.experimentTableKeyActivePayload] != nil {
+        self.activeExperimentPayloads.removeAll()
+        if let experiments = result?[ConfigConstants.experimentTableKeyActivePayload] as? [Data] {
+          for experiment in experiments {
+            do {
+              try JSONSerialization.jsonObject(with: experiment)
+              self.activeExperimentPayloads.append(experiment)
+            } catch {
+              RCLog.warning(
+                "I-RCN000031",
+                "Activated experiment payload could not be parsed as JSON."
+              )
+            }
+          }
+        }
+      }
+    }
+
+    dbManager.loadExperiment(completionHandler: completionHandler)
+  }
+
+  /// Update/Persist experiment information from config fetch response.
+  @objc public func updateExperiments(withResponse response: [[String: Any]]?) {
+    // Cache fetched experiment payloads.
+    experimentPayloads.removeAll()
+    dbManager.deleteExperimentTable(forKey: ConfigConstants.experimentTableKeyPayload)
+
+    if let response {
+      for experiment in response {
+        do {
+          let jsonData = try JSONSerialization.data(withJSONObject: experiment)
+          experimentPayloads.append(jsonData)
+          dbManager
+            .insertExperimentTable(
+              withKey: ConfigConstants.experimentTableKeyPayload,
+              value: jsonData
+            )
+        } catch {
+          RCLog.error("I-RCN000030", "Invalid experiment payload to be serialized.")
+        }
+      }
+    }
+  }
+
+  /// Update experiments to Firebase Analytics when `activateWithCompletion:` happens.
+  @objc public func updateExperiments(handler: (((any Error)?) -> Void)? = nil) {
+    let lifecycleEvent = LifecycleEvents()
+
+    // Get the last experiment start time prior to the latest payload.
+    let lastStartTime = experimentMetadata[Self.experimentMetadataKeyLastStartTime] as? Double
+
+    // Update the last experiment start time with the latest payload.
+    updateExperimentStartTime()
+    experimentController?
+      .updateExperiments(
+        withServiceOrigin: Self.serviceOrigin,
+        events: lifecycleEvent,
+        policy: .discardOldest,
+        lastStartTime: lastStartTime ?? 0,
+        payloads: experimentPayloads,
+        completionHandler: handler
+      )
+
+    // Update activated experiments payload and metadata in DB.
+    updateActiveExperimentsInDB()
+  }
+
+  @objc private func updateExperimentStartTime() {
+    let existingLastStartTime =
+      experimentMetadata[Self.experimentMetadataKeyLastStartTime] as? Double
+
+    let latestStartTime = latestStartTime(existingLastStartTime: existingLastStartTime ?? 0)
+
+    experimentMetadata[Self.experimentMetadataKeyLastStartTime] = latestStartTime
+
+    guard JSONSerialization.isValidJSONObject(experimentMetadata) else {
+      RCLog.error("I-RCN000028", "Invalid fetched experiment metadata to be serialized.")
+      return
+    }
+
+    if let serializedExperimentMetadata = try? JSONSerialization.data(
+      withJSONObject: experimentMetadata,
+      options: .prettyPrinted
+    ) {
+      dbManager
+        .insertExperimentTable(
+          withKey: ConfigConstants.experimentTableKeyMetadata,
+          value: serializedExperimentMetadata
+        )
+    }
+  }
+
+  @objc private func updateActiveExperimentsInDB() {
+    // Put current fetched experiment payloads into activated experiment DB.
+    activeExperimentPayloads.removeAll()
+    dbManager.deleteExperimentTable(forKey: ConfigConstants.experimentTableKeyActivePayload)
+    for data in experimentPayloads {
+      activeExperimentPayloads.append(data)
+      dbManager
+        .insertExperimentTable(
+          withKey: ConfigConstants.experimentTableKeyActivePayload,
+          value: data
+        )
+    }
+  }
+
+  private func latestStartTime(existingLastStartTime: Double) -> TimeInterval {
+    experimentController?
+      .latestExperimentStartTimestampBetweenTimestamp(
+        existingLastStartTime,
+        andPayloads: experimentPayloads
+      ) ?? 0
+  }
+}

+ 2 - 3
FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m

@@ -19,8 +19,6 @@
 
 @import FirebaseRemoteConfig;
 
-#import "FirebaseRemoteConfig/Sources/RCNConfigExperiment.h"
-
 #import "FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h"
 #import "FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h"
 #import "FirebaseRemoteConfig/Sources/RCNConfigDefines.h"
@@ -31,6 +29,8 @@
 
 #import "Interop/Analytics/Public/FIRAnalyticsInterop.h"
 
+#import "FirebaseRemoteConfig/FirebaseRemoteConfig-Swift.h"
+
 // Surface the internal FIRExperimentController initializer.
 @interface FIRExperimentController ()
 - (instancetype)initWithAnalytics:(nullable id<FIRAnalyticsInterop>)analytics;
@@ -40,7 +40,6 @@
 @property(nonatomic, copy) NSMutableArray *experimentPayloads;
 @property(nonatomic, copy) NSMutableDictionary *experimentMetadata;
 @property(nonatomic, copy) NSMutableArray *activeExperimentPayloads;
-@property(nonatomic, strong) RCNConfigDBManager *DBManager;
 - (NSTimeInterval)updateExperimentStartTime;
 - (void)loadExperimentFromTable;
 - (void)updateActiveExperimentsInDB;

+ 0 - 1
FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m

@@ -25,7 +25,6 @@
 #import "FirebaseRemoteConfig/Sources/Private/RCNConfigFetch.h"
 #import "FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h"
 #import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h"
-#import "FirebaseRemoteConfig/Sources/RCNConfigExperiment.h"
 #import "FirebaseRemoteConfig/Sources/RCNConfigRealtime.h"
 
 #import "FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h"