Skip to content

Commit

Permalink
[Rollouts] Crashlytics Rollouts interop Integration (#12200)
Browse files Browse the repository at this point in the history
  • Loading branch information
themiswang committed Jan 23, 2024
1 parent e92874c commit 7c2ac50
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 5 deletions.
35 changes: 32 additions & 3 deletions Crashlytics/Crashlytics/FIRCrashlytics.m
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@
#import <GoogleDataTransport/GoogleDataTransport.h>

@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 <UIKit/UIKit.h>
Expand All @@ -76,7 +82,10 @@
@protocol FIRCrashlyticsInstanceProvider <NSObject>
@end

@interface FIRCrashlytics () <FIRLibrary, FIRCrashlyticsInstanceProvider, FIRSessionsSubscriber>
@interface FIRCrashlytics () <FIRLibrary,
FIRCrashlyticsInstanceProvider,
FIRSessionsSubscriber,
FIRRolloutsStateSubscriber>

@property(nonatomic) BOOL didPreviouslyCrash;
@property(nonatomic, copy) NSString *googleAppID;
Expand All @@ -91,6 +100,8 @@ @interface FIRCrashlytics () <FIRLibrary, FIRCrashlyticsInstanceProvider, FIRSes

@property(nonatomic, strong) FIRCLSAnalyticsManager *analyticsManager;

@property(nonatomic, strong) FIRCLSRemoteConfigManager *remoteConfigManager;

// Dependencies common to each of the Controllers
@property(nonatomic, strong) FIRCLSManagerData *managerData;

Expand All @@ -104,7 +115,8 @@ - (instancetype)initWithApp:(FIRApp *)app
appInfo:(NSDictionary *)appInfo
installations:(FIRInstallations *)installations
analytics:(id<FIRAnalyticsInterop>)analytics
sessions:(id<FIRSessionsProvider>)sessions {
sessions:(id<FIRSessionsProvider>)sessions
remoteConfig:(id<FIRRemoteConfigInterop>)remoteConfig {
self = [super init];

if (self) {
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -215,6 +236,7 @@ + (void)load {

id<FIRAnalyticsInterop> analytics = FIR_COMPONENT(FIRAnalyticsInterop, container);
id<FIRSessionsProvider> sessions = FIR_COMPONENT(FIRSessionsProvider, container);
id<FIRRemoteConfigInterop> remoteConfig = FIR_COMPONENT(FIRRemoteConfigInterop, container);

FIRInstallations *installations = [FIRInstallations installationsWithApp:container.app];

Expand All @@ -224,7 +246,8 @@ + (void)load {
appInfo:NSBundle.mainBundle.infoDictionary
installations:installations
analytics:analytics
sessions:sessions];
sessions:sessions
remoteConfig:remoteConfig];
};

FIRComponent *component =
Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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.maxRolloutAssignments])
}

_ = validatedAssignments.map { assignment in
if assignment.parameterValue.count > 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[..<upperBound]
assignment.parameterValue = String(slicedParameterValue)
}
}

return validatedAssignments
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// 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.
#if SWIFT_PACKAGE
@testable import FirebaseCrashlyticsSwift
#else
@testable import FirebaseCrashlytics
#endif
import FirebaseRemoteConfigInterop
import XCTest

class RemoteConfigConfigMock: RemoteConfigInterop {
func registerRolloutsStateSubscriber(_ subscriber: FirebaseRemoteConfigInterop
.RolloutsStateSubscriber,
for namespace: String) {}
}

final class CrashlyticsRemoteConfigManagerTests: XCTestCase {
let rollouts: RolloutsState = {
let assignment1 = RolloutAssignment(
rolloutId: "rollout_1",
variantId: "control",
templateVersion: 1,
parameterKey: "my_feature",
parameterValue: "false"
)
let assignment2 = RolloutAssignment(
rolloutId: "rollout_2",
variantId: "enabled",
templateVersion: 1,
parameterKey: "themis_big_feature",
parameterValue: "1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111"
)
let rollouts = RolloutsState(assignmentList: [assignment1, assignment2])
return rollouts
}()

let rcInterop = RemoteConfigConfigMock()

func testRemoteConfigManagerProperlyProcessRolloutsState() throws {
let rcManager = CrashlyticsRemoteConfigManager(remoteConfig: rcInterop)
rcManager.updateRolloutsState(rolloutsState: rollouts)
XCTAssertEqual(rcManager.rolloutAssignment.count, 2)

for assignment in rollouts.assignments {
if assignment.parameterKey == "themis_big_feature" {
XCTAssertEqual(
assignment.parameterValue.count,
CrashlyticsRemoteConfigManager.maxParameterValueLength
)
}
}
}
}
6 changes: 4 additions & 2 deletions FirebaseCrashlytics.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Pod::Spec.new do |s|
s.prefix_header_file = false

s.source_files = [
'Crashlytics/Crashlytics/**/*.{c,h,m,mm}',
'Crashlytics/Crashlytics/**/*.{c,h,m,mm,swift}',
'Crashlytics/Protogen/**/*.{c,h,m,mm}',
'Crashlytics/Shared/**/*.{c,h,m,mm}',
'Crashlytics/third_party/**/*.{c,h,m,mm}',
Expand Down Expand Up @@ -58,6 +58,7 @@ Pod::Spec.new do |s|
s.dependency 'FirebaseCore', '~> 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'
Expand Down Expand Up @@ -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/*'
Expand Down
15 changes: 15 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -514,6 +515,7 @@ let package = Package(
"upload-symbols",
"CrashlyticsInputFiles.xcfilelist",
"third_party/libunwind/LICENSE",
"Crashlytics/Rollouts/",
],
sources: [
"Crashlytics/",
Expand Down Expand Up @@ -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")],
Expand Down

0 comments on commit 7c2ac50

Please sign in to comment.