Skip to content
Merged
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
1 change: 1 addition & 0 deletions FirebaseRemoteConfig/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# v7.5.0
- [fixed] Fixed bug that was incorrectly flagging ABT experiment payloads as invalid. (#7184)
- [changed] Standardize support for Firebase products that integrate with Remote Config. (#7094)

# v7.1.0
- [changed] Add support for other Firebase products to integrate with Remote Config. (#6692)
Expand Down
20 changes: 16 additions & 4 deletions FirebaseRemoteConfig/Sources/RCNPersonalization.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,28 @@
NS_ASSUME_NONNULL_BEGIN

static NSString *const kAnalyticsOriginPersonalization = @"fp";
static NSString *const kAnalyticsPullEvent = @"_fpc";
static NSString *const kArmKey = @"_fpid";
static NSString *const kArmValue = @"_fpct";

static NSString *const kExternalEvent = @"personalization_assignment";
static NSString *const kExternalRcParameterParam = @"arm_key";
static NSString *const kExternalArmValueParam = @"arm_value";
static NSString *const kPersonalizationId = @"personalizationId";
static NSString *const kExternalPersonalizationIdParam = @"personalization_id";
static NSString *const kArmIndex = @"armIndex";
static NSString *const kExternalArmIndexParam = @"arm_index";
static NSString *const kGroup = @"group";
static NSString *const kExternalGroupParam = @"group";

static NSString *const kInternalEvent = @"_fpc";
static NSString *const kChoiceId = @"choiceId";
static NSString *const kInternalChoiceIdParam = @"_fpid";

@interface RCNPersonalization : NSObject

/// Analytics connector
@property(nonatomic, strong) id<FIRAnalyticsInterop> _Nullable analytics;

@property(atomic, strong) NSMutableDictionary *loggedChoiceIds;

- (instancetype)init NS_UNAVAILABLE;

/// Designated initializer.
Expand All @@ -37,7 +49,7 @@ static NSString *const kPersonalizationId = @"personalizationId";

/// Called when an arm is pulled from Remote Config. If the arm is personalized, log information to
/// Google in another thread.
- (void)logArmActive:(NSString *)key config:(NSDictionary *)config;
- (void)logArmActive:(NSString *)rcParameter config:(NSDictionary *)config;

@end

Expand Down
34 changes: 27 additions & 7 deletions FirebaseRemoteConfig/Sources/RCNPersonalization.m
Original file line number Diff line number Diff line change
Expand Up @@ -25,28 +25,48 @@ - (instancetype)initWithAnalytics:(id<FIRAnalyticsInterop> _Nullable)analytics {
self = [super init];
if (self) {
self->_analytics = analytics;
self->_loggedChoiceIds = [[NSMutableDictionary alloc] init];
}
return self;
}

- (void)logArmActive:(NSString *)key config:(NSDictionary *)config {
- (void)logArmActive:(NSString *)rcParameter config:(NSDictionary *)config {
NSDictionary *ids = config[RCNFetchResponseKeyPersonalizationMetadata];
NSDictionary<NSString *, FIRRemoteConfigValue *> *values = config[RCNFetchResponseKeyEntries];
if (ids.count < 1 || values.count < 1 || !values[key]) {
if (ids.count < 1 || values.count < 1 || !values[rcParameter]) {
return;
}

NSDictionary *metadata = ids[key];
if (!metadata || metadata[kPersonalizationId] == nil) {
NSDictionary *metadata = ids[rcParameter];
if (!metadata) {
return;
}

NSString *choiceId = metadata[kChoiceId];
if (choiceId == nil) {
return;
}

// Listeners like logArmActive() are dispatched to a serial queue, so loggedChoiceIds should
// contain any previously logged RC parameter / choice ID pairs.
if (self->_loggedChoiceIds[rcParameter] == choiceId) {
return;
}
self->_loggedChoiceIds[rcParameter] = choiceId;

[self->_analytics logEventWithOrigin:kAnalyticsOriginPersonalization
name:kAnalyticsPullEvent
name:kExternalEvent
parameters:@{
kArmKey : metadata[kPersonalizationId],
kArmValue : values[key].stringValue
kExternalRcParameterParam : rcParameter,
kExternalArmValueParam : values[rcParameter].stringValue,
kExternalPersonalizationIdParam : metadata[kPersonalizationId],
kExternalArmIndexParam : metadata[kArmIndex],
kExternalGroupParam : metadata[kGroup]
}];

[self->_analytics logEventWithOrigin:kAnalyticsOriginPersonalization
name:kInternalEvent
parameters:@{kInternalChoiceIdParam : choiceId}];
}

@end
129 changes: 102 additions & 27 deletions FirebaseRemoteConfig/Tests/Unit/RCNPersonalizationTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,22 @@ - (void)setUp {
initWithData:[@"value3" dataUsingEncoding:NSUTF8StringEncoding]
source:FIRRemoteConfigSourceRemote]
},
RCNFetchResponseKeyPersonalizationMetadata :
@{@"key1" : @{kPersonalizationId : @"id1"}, @"key2" : @{kPersonalizationId : @"id2"}}
RCNFetchResponseKeyPersonalizationMetadata : @{
@"key1" : @{
kPersonalizationId : @"p13n1",
kArmIndex : @0,
kChoiceId : @"id1",
kGroup : @"BASELINE"
},
@"key2" :
@{kPersonalizationId : @"p13n2", kArmIndex : @1, kChoiceId : @"id2", kGroup : @"P13N"}
}
};

_fakeLogs = [[NSMutableArray alloc] init];
_analyticsMock = OCMProtocolMock(@protocol(FIRAnalyticsInterop));
OCMStub([_analyticsMock logEventWithOrigin:kAnalyticsOriginPersonalization
name:kAnalyticsPullEvent
name:[OCMArg isKindOfClass:[NSString class]]
parameters:[OCMArg isKindOfClass:[NSDictionary class]]])
.andDo(^(NSInvocation *invocation) {
__unsafe_unretained NSDictionary *bundle;
Expand Down Expand Up @@ -108,7 +116,10 @@ - (void)testNonPersonalizationKey {

OCMVerify(never(),
[_analyticsMock logEventWithOrigin:kAnalyticsOriginPersonalization
name:kAnalyticsPullEvent
name:[OCMArg checkWithBlock:^BOOL(NSString *value) {
return [value isEqualToString:kExternalEvent] ||
[value isEqualToString:kInternalEvent];
}]
parameters:[OCMArg isKindOfClass:[NSDictionary class]]]);
XCTAssertEqual([_fakeLogs count], 0);
}
Expand All @@ -118,51 +129,106 @@ - (void)testSinglePersonalizationKey {

[_personalization logArmActive:@"key1" config:_configContainer];

OCMVerify(times(1),
OCMVerify(times(2),
[_analyticsMock logEventWithOrigin:kAnalyticsOriginPersonalization
name:kAnalyticsPullEvent
name:[OCMArg checkWithBlock:^BOOL(NSString *value) {
return [value isEqualToString:kExternalEvent] ||
[value isEqualToString:kInternalEvent];
}]
parameters:[OCMArg isKindOfClass:[NSDictionary class]]]);
XCTAssertEqual([_fakeLogs count], 1);
XCTAssertEqual([_fakeLogs count], 2);

NSDictionary *logParams = @{
kExternalRcParameterParam : @"key1",
kExternalArmValueParam : @"value1",
kExternalPersonalizationIdParam : @"p13n1",
kExternalArmIndexParam : @0,
kExternalGroupParam : @"BASELINE"
};
XCTAssertEqualObjects(_fakeLogs[0], logParams);

NSDictionary *params = @{kArmKey : @"id1", kArmValue : @"value1"};
XCTAssertEqualObjects(_fakeLogs[0], params);
NSDictionary *internalLogParams = @{kInternalChoiceIdParam : @"id1"};
XCTAssertEqualObjects(_fakeLogs[1], internalLogParams);
}

- (void)testMultiplePersonalizationKeys {
[_fakeLogs removeAllObjects];

[_personalization logArmActive:@"key1" config:_configContainer];
[_personalization logArmActive:@"key2" config:_configContainer];
[_personalization logArmActive:@"key1" config:_configContainer];

OCMVerify(times(2),
OCMVerify(times(4),
[_analyticsMock logEventWithOrigin:kAnalyticsOriginPersonalization
name:kAnalyticsPullEvent
name:[OCMArg checkWithBlock:^BOOL(NSString *value) {
return [value isEqualToString:kExternalEvent] ||
[value isEqualToString:kInternalEvent];
}]
parameters:[OCMArg isKindOfClass:[NSDictionary class]]]);
XCTAssertEqual([_fakeLogs count], 2);
XCTAssertEqual([_fakeLogs count], 4);

NSDictionary *logParams1 = @{
kExternalRcParameterParam : @"key1",
kExternalArmValueParam : @"value1",
kExternalPersonalizationIdParam : @"p13n1",
kExternalArmIndexParam : @0,
kExternalGroupParam : @"BASELINE"
};
XCTAssertEqualObjects(_fakeLogs[0], logParams1);

NSDictionary *internalLogParams1 = @{kInternalChoiceIdParam : @"id1"};
XCTAssertEqualObjects(_fakeLogs[1], internalLogParams1);

NSDictionary *params1 = @{kArmKey : @"id1", kArmValue : @"value1"};
XCTAssertEqualObjects(_fakeLogs[0], params1);
NSDictionary *logParams2 = @{
kExternalRcParameterParam : @"key2",
kExternalArmValueParam : @"value2",
kExternalPersonalizationIdParam : @"p13n2",
kExternalArmIndexParam : @1,
kExternalGroupParam : @"P13N"
};
XCTAssertEqualObjects(_fakeLogs[2], logParams2);

NSDictionary *params2 = @{kArmKey : @"id2", kArmValue : @"value2"};
XCTAssertEqualObjects(_fakeLogs[1], params2);
NSDictionary *internalLogParams2 = @{kInternalChoiceIdParam : @"id2"};
XCTAssertEqualObjects(_fakeLogs[3], internalLogParams2);
}

- (void)testRemoteConfigIntegration {
[_fakeLogs removeAllObjects];

FIRRemoteConfigFetchAndActivateCompletion fetchAndActivateCompletion =
^void(FIRRemoteConfigFetchAndActivateStatus status, NSError *error) {
OCMVerify(times(2), [self->_analyticsMock
OCMVerify(times(4), [self->_analyticsMock
logEventWithOrigin:kAnalyticsOriginPersonalization
name:kAnalyticsPullEvent
name:[OCMArg checkWithBlock:^BOOL(NSString *value) {
return [value isEqualToString:kExternalEvent] ||
[value isEqualToString:kInternalEvent];
}]
parameters:[OCMArg isKindOfClass:[NSDictionary class]]]);
XCTAssertEqual([self->_fakeLogs count], 2);

NSDictionary *params1 = @{kArmKey : @"id1", kArmValue : @"value1"};
XCTAssertEqualObjects(self->_fakeLogs[0], params1);

NSDictionary *params2 = @{kArmKey : @"id2", kArmValue : @"value2"};
XCTAssertEqualObjects(self->_fakeLogs[1], params2);
XCTAssertEqual([self->_fakeLogs count], 4);

NSDictionary *logParams1 = @{
kExternalRcParameterParam : @"key1",
kExternalArmValueParam : @"value1",
kExternalPersonalizationIdParam : @"p13n1",
kExternalArmIndexParam : @0,
kExternalGroupParam : @"BASELINE"
};
XCTAssertEqualObjects(self->_fakeLogs[0], logParams1);

NSDictionary *internalLogParams1 = @{kInternalChoiceIdParam : @"id1"};
XCTAssertEqualObjects(self->_fakeLogs[1], internalLogParams1);

NSDictionary *logParams2 = @{
kExternalRcParameterParam : @"key1",
kExternalArmValueParam : @"value1",
kExternalPersonalizationIdParam : @"p13n1",
kExternalArmIndexParam : @0,
kExternalGroupParam : @"BASELINE"
};
XCTAssertEqualObjects(self->_fakeLogs[2], logParams2);

NSDictionary *internalLogParams2 = @{kInternalChoiceIdParam : @"id2"};
XCTAssertEqualObjects(self->_fakeLogs[3], internalLogParams2);
};

[_configInstance fetchAndActivateWithCompletionHandler:fetchAndActivateCompletion];
Expand Down Expand Up @@ -190,8 +256,17 @@ + (id)mockResponseHandler {
NSDictionary *response = @{
RCNFetchResponseKeyState : RCNFetchResponseKeyStateUpdate,
RCNFetchResponseKeyEntries : @{@"key1" : @"value1", @"key2" : @"value2", @"key3" : @"value3"},
RCNFetchResponseKeyPersonalizationMetadata :
@{@"key1" : @{kPersonalizationId : @"id1"}, @"key2" : @{kPersonalizationId : @"id2"}}
RCNFetchResponseKeyPersonalizationMetadata : @{
@"key1" : @{
kPersonalizationId : @"p13n1",
kArmIndex : @0,
kChoiceId : @"id1",
kGroup : @"BASELINE"
},
@"key2" :
@{kPersonalizationId : @"p13n2", kArmIndex : @1, kChoiceId : @"id2", kGroup : @"P13N"}
}

};
return [OCMArg invokeBlockWithArgs:[NSJSONSerialization dataWithJSONObject:response
options:0
Expand Down