diff --git a/Crashlytics/Crashlytics/FIRCrashlytics.m b/Crashlytics/Crashlytics/FIRCrashlytics.m index 4d112cddad4..c1b6ce48c02 100644 --- a/Crashlytics/Crashlytics/FIRCrashlytics.m +++ b/Crashlytics/Crashlytics/FIRCrashlytics.m @@ -58,6 +58,12 @@ #import @import FirebaseSessions; +@import FirebaseRemoteConfigInterop; +#if SWIFT_PACKAGE +@import FirebaseCrashlyticsSwift; +#else // Swift Package Manager +#import "FirebaseCrashlytics/FirebaseCrashlytics-Swift.h" +#endif // Cocoapod #if TARGET_OS_IPHONE #import @@ -76,7 +82,10 @@ @protocol FIRCrashlyticsInstanceProvider @end -@interface FIRCrashlytics () +@interface FIRCrashlytics () @property(nonatomic) BOOL didPreviouslyCrash; @property(nonatomic, copy) NSString *googleAppID; @@ -91,6 +100,8 @@ @interface FIRCrashlytics () )analytics - sessions:(id)sessions { + sessions:(id)sessions + remoteConfig:(id)remoteConfig { self = [super init]; if (self) { @@ -157,6 +169,15 @@ - (instancetype)initWithApp:(FIRApp *)app [sessions registerWithSubscriber:self]; } + if (remoteConfig) { + FIRCLSDebugLog(@"Registering RemoteConfig SDK subscription for rollouts data"); + + _remoteConfigManager = [[FIRCLSRemoteConfigManager alloc] initWithRemoteConfig:remoteConfig]; + + // TODO(themisw): Import "firebase" from the interop in the future. + [remoteConfig registerRolloutsStateSubscriber:self for:@"firebase"]; + } + _reportUploader = [[FIRCLSReportUploader alloc] initWithManagerData:_managerData]; _existingReportManager = @@ -215,6 +236,7 @@ + (void)load { id analytics = FIR_COMPONENT(FIRAnalyticsInterop, container); id sessions = FIR_COMPONENT(FIRSessionsProvider, container); + id remoteConfig = FIR_COMPONENT(FIRRemoteConfigInterop, container); FIRInstallations *installations = [FIRInstallations installationsWithApp:container.app]; @@ -224,7 +246,8 @@ + (void)load { appInfo:NSBundle.mainBundle.infoDictionary installations:installations analytics:analytics - sessions:sessions]; + sessions:sessions + remoteConfig:remoteConfig]; }; FIRComponent *component = @@ -407,4 +430,10 @@ - (FIRSessionsSubscriberName)sessionsSubscriberName { return FIRSessionsSubscriberNameCrashlytics; } +#pragma mark - FIRRolloutsStateSubscriber +- (void)rolloutsStateDidChange:(FIRRolloutsState *_Nonnull)rolloutsState { + [_remoteConfigManager updateRolloutsStateWithRolloutsState:rolloutsState]; + // TODO(themisw): writing the rollout state change to persistence +} + @end diff --git a/Crashlytics/Crashlytics/Rollouts/CrashlyticsRemoteConfigManager.swift b/Crashlytics/Crashlytics/Rollouts/CrashlyticsRemoteConfigManager.swift new file mode 100644 index 00000000000..84acd6a4ade --- /dev/null +++ b/Crashlytics/Crashlytics/Rollouts/CrashlyticsRemoteConfigManager.swift @@ -0,0 +1,66 @@ +// Copyright 2023 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 FirebaseRemoteConfigInterop +import Foundation + +protocol CrashlyticsPersistentLog: NSObject { + func updateRolloutsStateToPersistence(rolloutAssignments: [RolloutAssignment]) +} + +@objc(FIRCLSRemoteConfigManager) +public class CrashlyticsRemoteConfigManager: NSObject { + public static let maxRolloutAssignments = 128 + public static let maxParameterValueLength = 256 + + var remoteConfig: RemoteConfigInterop + public private(set) var rolloutAssignment: [RolloutAssignment] = [] + weak var persistenceDelegate: CrashlyticsPersistentLog? + + @objc public init(remoteConfig: RemoteConfigInterop) { + self.remoteConfig = remoteConfig + } + + @objc public func updateRolloutsState(rolloutsState: RolloutsState) { + rolloutAssignment = normalizeRolloutAssignment(assignments: Array(rolloutsState.assignments)) + } +} + +private extension CrashlyticsRemoteConfigManager { + func normalizeRolloutAssignment(assignments: [RolloutAssignment]) -> [RolloutAssignment] { + var validatedAssignments = assignments + if assignments.count > CrashlyticsRemoteConfigManager.maxRolloutAssignments { + debugPrint("Rollouts excess the maximum number of assignments can pass to Crashlytics") + validatedAssignments = + Array(assignments[.. CrashlyticsRemoteConfigManager.maxParameterValueLength { + debugPrint( + "Rollouts excess the maximum length of parameter value can pass to Crashlytics", + assignment.parameterValue + ) + let upperBound = String.Index( + utf16Offset: CrashlyticsRemoteConfigManager.maxParameterValueLength, + in: assignment.parameterValue + ) + let slicedParameterValue = assignment.parameterValue[.. 10.5' s.dependency 'FirebaseInstallations', '~> 10.0' s.dependency 'FirebaseSessions', '~> 10.5' + s.dependency 'FirebaseRemoteConfigInterop', '~> 10.20' s.dependency 'PromisesObjC', '~> 2.1' s.dependency 'GoogleDataTransport', '~> 9.2' s.dependency 'GoogleUtilities/Environment', '~> 7.8' @@ -115,7 +116,8 @@ Pod::Spec.new do |s| :tvos => tvos_deployment_target } unit_tests.source_files = 'Crashlytics/UnitTests/*.[mh]', - 'Crashlytics/UnitTests/*/*.[mh]' + 'Crashlytics/UnitTests/*/*.[mh]', + 'Crashlytics/UnitTestsSwift/*.swift' unit_tests.resources = 'Crashlytics/UnitTests/Data/*', 'Crashlytics/UnitTests/*.clsrecord', 'Crashlytics/UnitTests/FIRCLSMachO/machO_data/*' diff --git a/Package.swift b/Package.swift index 746d7a3c1c0..82f7afba193 100644 --- a/Package.swift +++ b/Package.swift @@ -497,6 +497,7 @@ let package = Package( "FirebaseInstallations", "FirebaseSessions", "FirebaseRemoteConfigInterop", + "FirebaseCrashlyticsSwift", .product(name: "GoogleDataTransport", package: "GoogleDataTransport"), .product(name: "GULEnvironment", package: "GoogleUtilities"), .product(name: "FBLPromises", package: "Promises"), @@ -514,6 +515,7 @@ let package = Package( "upload-symbols", "CrashlyticsInputFiles.xcfilelist", "third_party/libunwind/LICENSE", + "Crashlytics/Rollouts/", ], sources: [ "Crashlytics/", @@ -542,6 +544,19 @@ let package = Package( .linkedFramework("SystemConfiguration", .when(platforms: [.iOS, .macOS, .tvOS])), ] ), + .target( + name: "FirebaseCrashlyticsSwift", + dependencies: ["FirebaseRemoteConfigInterop"], + path: "Crashlytics", + sources: [ + "Crashlytics/Rollouts/", + ] + ), + .testTarget( + name: "FirebaseCrashlyticsSwiftUnit", + dependencies: ["FirebaseCrashlyticsSwift"], + path: "Crashlytics/UnitTestsSwift/" + ), .testTarget( name: "FirebaseCrashlyticsUnit", dependencies: ["FirebaseCrashlytics", .product(name: "OCMock", package: "ocmock")],