1 |
efrain |
1 |
// Copyright 2019 Google
|
|
|
2 |
//
|
|
|
3 |
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
4 |
// you may not use this file except in compliance with the License.
|
|
|
5 |
// You may obtain a copy of the License at
|
|
|
6 |
//
|
|
|
7 |
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
8 |
//
|
|
|
9 |
// Unless required by applicable law or agreed to in writing, software
|
|
|
10 |
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
11 |
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
12 |
// See the License for the specific language governing permissions and
|
|
|
13 |
// limitations under the License.
|
|
|
14 |
|
|
|
15 |
#import "FirebaseABTesting/Sources/ABTConditionalUserPropertyController.h"
|
|
|
16 |
|
|
|
17 |
#import "FirebaseABTesting/Sources/ABTConstants.h"
|
|
|
18 |
#import "FirebaseABTesting/Sources/Public/FirebaseABTesting/FIRLifecycleEvents.h"
|
|
|
19 |
#import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h"
|
|
|
20 |
#import "Interop/Analytics/Public/FIRAnalyticsInterop.h"
|
|
|
21 |
|
|
|
22 |
@implementation ABTConditionalUserPropertyController {
|
|
|
23 |
dispatch_queue_t _analyticOperationQueue;
|
|
|
24 |
id<FIRAnalyticsInterop> _Nullable _analytics;
|
|
|
25 |
}
|
|
|
26 |
|
|
|
27 |
/// Returns the ABTConditionalUserPropertyController singleton.
|
|
|
28 |
+ (instancetype)sharedInstanceWithAnalytics:(id<FIRAnalyticsInterop> _Nullable)analytics {
|
|
|
29 |
static ABTConditionalUserPropertyController *sharedInstance = nil;
|
|
|
30 |
static dispatch_once_t onceToken = 0;
|
|
|
31 |
dispatch_once(&onceToken, ^{
|
|
|
32 |
sharedInstance = [[ABTConditionalUserPropertyController alloc] initWithAnalytics:analytics];
|
|
|
33 |
});
|
|
|
34 |
return sharedInstance;
|
|
|
35 |
}
|
|
|
36 |
|
|
|
37 |
- (instancetype)initWithAnalytics:(id<FIRAnalyticsInterop> _Nullable)analytics {
|
|
|
38 |
self = [super init];
|
|
|
39 |
if (self) {
|
|
|
40 |
_analyticOperationQueue =
|
|
|
41 |
dispatch_queue_create("com.google.FirebaseABTesting.analytics", DISPATCH_QUEUE_SERIAL);
|
|
|
42 |
_analytics = analytics;
|
|
|
43 |
}
|
|
|
44 |
return self;
|
|
|
45 |
}
|
|
|
46 |
|
|
|
47 |
#pragma mark - experiments proxy methods on Firebase Analytics
|
|
|
48 |
|
|
|
49 |
- (NSArray *)experimentsWithOrigin:(NSString *)origin {
|
|
|
50 |
return [_analytics conditionalUserProperties:origin propertyNamePrefix:@""];
|
|
|
51 |
}
|
|
|
52 |
|
|
|
53 |
- (void)clearExperiment:(NSString *)experimentID
|
|
|
54 |
variantID:(NSString *)variantID
|
|
|
55 |
withOrigin:(NSString *)origin
|
|
|
56 |
payload:(ABTExperimentPayload *)payload
|
|
|
57 |
events:(FIRLifecycleEvents *)events {
|
|
|
58 |
// Payload always overwrite event names.
|
|
|
59 |
NSString *clearExperimentEventName = events.clearExperimentEventName;
|
|
|
60 |
if (payload && payload.clearEventToLog && payload.clearEventToLog.length) {
|
|
|
61 |
clearExperimentEventName = payload.clearEventToLog;
|
|
|
62 |
}
|
|
|
63 |
|
|
|
64 |
[_analytics clearConditionalUserProperty:experimentID
|
|
|
65 |
forOrigin:origin
|
|
|
66 |
clearEventName:clearExperimentEventName
|
|
|
67 |
clearEventParameters:@{experimentID : variantID}];
|
|
|
68 |
|
|
|
69 |
FIRLogDebug(kFIRLoggerABTesting, @"I-ABT000015", @"Clear Experiment ID %@, variant ID %@.",
|
|
|
70 |
experimentID, variantID);
|
|
|
71 |
}
|
|
|
72 |
|
|
|
73 |
- (void)setExperimentWithOrigin:(NSString *)origin
|
|
|
74 |
payload:(ABTExperimentPayload *)payload
|
|
|
75 |
events:(FIRLifecycleEvents *)events
|
|
|
76 |
policy:(ABTExperimentPayloadExperimentOverflowPolicy)policy {
|
|
|
77 |
NSInteger maxNumOfExperiments = [self maxNumberOfExperimentsOfOrigin:origin];
|
|
|
78 |
if (maxNumOfExperiments < 0) {
|
|
|
79 |
return;
|
|
|
80 |
}
|
|
|
81 |
|
|
|
82 |
// Clear experiments if overflow
|
|
|
83 |
NSArray *experiments = [self experimentsWithOrigin:origin];
|
|
|
84 |
if (!experiments) {
|
|
|
85 |
FIRLogInfo(kFIRLoggerABTesting, @"I-ABT000003",
|
|
|
86 |
@"Failed to get conditional user properties from Firebase Analytics.");
|
|
|
87 |
return;
|
|
|
88 |
}
|
|
|
89 |
|
|
|
90 |
if (maxNumOfExperiments <= experiments.count) {
|
|
|
91 |
ABTExperimentPayloadExperimentOverflowPolicy overflowPolicy =
|
|
|
92 |
[self overflowPolicyWithPayload:payload originalPolicy:policy];
|
|
|
93 |
id experimentToClear = experiments.firstObject;
|
|
|
94 |
if (overflowPolicy == ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest &&
|
|
|
95 |
experimentToClear) {
|
|
|
96 |
NSString *expID = [self experimentIDOfExperiment:experimentToClear];
|
|
|
97 |
NSString *varID = [self variantIDOfExperiment:experimentToClear];
|
|
|
98 |
|
|
|
99 |
[self clearExperiment:expID variantID:varID withOrigin:origin payload:payload events:events];
|
|
|
100 |
FIRLogDebug(kFIRLoggerABTesting, @"I-ABT000016",
|
|
|
101 |
@"Clear experiment ID %@ variant ID %@ due to "
|
|
|
102 |
@"overflow policy.",
|
|
|
103 |
expID, varID);
|
|
|
104 |
|
|
|
105 |
} else {
|
|
|
106 |
FIRLogDebug(kFIRLoggerABTesting, @"I-ABT000017",
|
|
|
107 |
@"Experiment ID %@ variant ID %@ won't be set due to "
|
|
|
108 |
@"overflow policy.",
|
|
|
109 |
payload.experimentId, payload.variantId);
|
|
|
110 |
|
|
|
111 |
return;
|
|
|
112 |
}
|
|
|
113 |
}
|
|
|
114 |
|
|
|
115 |
// Clear experiment if other variant ID exists.
|
|
|
116 |
NSString *experimentID = payload.experimentId;
|
|
|
117 |
NSString *variantID = payload.variantId;
|
|
|
118 |
for (id experiment in experiments) {
|
|
|
119 |
NSString *expID = [self experimentIDOfExperiment:experiment];
|
|
|
120 |
NSString *varID = [self variantIDOfExperiment:experiment];
|
|
|
121 |
if ([expID isEqualToString:experimentID] && ![varID isEqualToString:variantID]) {
|
|
|
122 |
FIRLogDebug(kFIRLoggerABTesting, @"I-ABT000018",
|
|
|
123 |
@"Clear experiment ID %@ with variant ID %@ because "
|
|
|
124 |
@"only one variant ID can be existed "
|
|
|
125 |
@"at any time.",
|
|
|
126 |
expID, varID);
|
|
|
127 |
[self clearExperiment:expID variantID:varID withOrigin:origin payload:payload events:events];
|
|
|
128 |
}
|
|
|
129 |
}
|
|
|
130 |
|
|
|
131 |
// Set experiment
|
|
|
132 |
NSDictionary<NSString *, id> *experiment = [self createExperimentFromOrigin:origin
|
|
|
133 |
payload:payload
|
|
|
134 |
events:events];
|
|
|
135 |
|
|
|
136 |
[_analytics setConditionalUserProperty:experiment];
|
|
|
137 |
|
|
|
138 |
FIRLogDebug(kFIRLoggerABTesting, @"I-ABT000019",
|
|
|
139 |
@"Set conditional user property, experiment ID %@ with "
|
|
|
140 |
@"variant ID %@ triggered event %@.",
|
|
|
141 |
experimentID, variantID, payload.triggerEvent);
|
|
|
142 |
|
|
|
143 |
// Log setEvent (experiment lifecycle event to be set when an experiment is set)
|
|
|
144 |
[self logEventWithOrigin:origin payload:payload events:events];
|
|
|
145 |
}
|
|
|
146 |
|
|
|
147 |
- (NSMutableDictionary<NSString *, id> *)createExperimentFromOrigin:(NSString *)origin
|
|
|
148 |
payload:(ABTExperimentPayload *)payload
|
|
|
149 |
events:(FIRLifecycleEvents *)events {
|
|
|
150 |
NSMutableDictionary<NSString *, id> *experiment = [[NSMutableDictionary alloc] init];
|
|
|
151 |
NSString *experimentID = payload.experimentId;
|
|
|
152 |
NSString *variantID = payload.variantId;
|
|
|
153 |
|
|
|
154 |
NSDictionary *eventParams = @{experimentID : variantID};
|
|
|
155 |
|
|
|
156 |
[experiment setValue:origin forKey:kABTExperimentDictionaryOriginKey];
|
|
|
157 |
|
|
|
158 |
NSTimeInterval creationTimestamp = (double)(payload.experimentStartTimeMillis / ABT_MSEC_PER_SEC);
|
|
|
159 |
[experiment setValue:@(creationTimestamp) forKey:kABTExperimentDictionaryCreationTimestampKey];
|
|
|
160 |
[experiment setValue:experimentID forKey:kABTExperimentDictionaryExperimentIDKey];
|
|
|
161 |
[experiment setValue:variantID forKey:kABTExperimentDictionaryVariantIDKey];
|
|
|
162 |
|
|
|
163 |
// For the experiment to be immediately activated/triggered, its trigger event must be null.
|
|
|
164 |
// Double check if payload's trigger event is empty string, it must be set to null to trigger.
|
|
|
165 |
if (payload && payload.triggerEvent && payload.triggerEvent.length) {
|
|
|
166 |
[experiment setValue:payload.triggerEvent forKey:kABTExperimentDictionaryTriggeredEventNameKey];
|
|
|
167 |
} else {
|
|
|
168 |
[experiment setValue:nil forKey:kABTExperimentDictionaryTriggeredEventNameKey];
|
|
|
169 |
}
|
|
|
170 |
|
|
|
171 |
// Set timeout event name and params.
|
|
|
172 |
NSString *timeoutEventName = events.timeoutExperimentEventName;
|
|
|
173 |
if (payload && payload.timeoutEventToLog && payload.timeoutEventToLog.length) {
|
|
|
174 |
timeoutEventName = payload.timeoutEventToLog;
|
|
|
175 |
}
|
|
|
176 |
NSDictionary<NSString *, id> *timeoutEvent = [self eventDictionaryWithOrigin:origin
|
|
|
177 |
eventName:timeoutEventName
|
|
|
178 |
params:eventParams];
|
|
|
179 |
[experiment setValue:timeoutEvent forKey:kABTExperimentDictionaryTimedOutEventKey];
|
|
|
180 |
|
|
|
181 |
// Set trigger timeout information on how long to wait for trigger event.
|
|
|
182 |
NSTimeInterval triggerTimeout = (double)(payload.triggerTimeoutMillis / ABT_MSEC_PER_SEC);
|
|
|
183 |
[experiment setValue:@(triggerTimeout) forKey:kABTExperimentDictionaryTriggerTimeoutKey];
|
|
|
184 |
|
|
|
185 |
// Set activate event name and params.
|
|
|
186 |
NSString *activateEventName = events.activateExperimentEventName;
|
|
|
187 |
if (payload && payload.activateEventToLog && payload.activateEventToLog.length) {
|
|
|
188 |
activateEventName = payload.activateEventToLog;
|
|
|
189 |
}
|
|
|
190 |
NSDictionary<NSString *, id> *triggeredEvent = [self eventDictionaryWithOrigin:origin
|
|
|
191 |
eventName:activateEventName
|
|
|
192 |
params:eventParams];
|
|
|
193 |
[experiment setValue:triggeredEvent forKey:kABTExperimentDictionaryTriggeredEventKey];
|
|
|
194 |
|
|
|
195 |
// Set time to live information for how long the experiment lasts.
|
|
|
196 |
NSTimeInterval timeToLive = (double)(payload.timeToLiveMillis / ABT_MSEC_PER_SEC);
|
|
|
197 |
[experiment setValue:@(timeToLive) forKey:kABTExperimentDictionaryTimeToLiveKey];
|
|
|
198 |
|
|
|
199 |
// Set expired event name and params.
|
|
|
200 |
NSString *expiredEventName = events.expireExperimentEventName;
|
|
|
201 |
if (payload && payload.ttlExpiryEventToLog && payload.ttlExpiryEventToLog.length) {
|
|
|
202 |
expiredEventName = payload.ttlExpiryEventToLog;
|
|
|
203 |
}
|
|
|
204 |
NSDictionary<NSString *, id> *expiredEvent = [self eventDictionaryWithOrigin:origin
|
|
|
205 |
eventName:expiredEventName
|
|
|
206 |
params:eventParams];
|
|
|
207 |
[experiment setValue:expiredEvent forKey:kABTExperimentDictionaryExpiredEventKey];
|
|
|
208 |
return experiment;
|
|
|
209 |
}
|
|
|
210 |
|
|
|
211 |
- (NSDictionary<NSString *, id> *)
|
|
|
212 |
eventDictionaryWithOrigin:(nonnull NSString *)origin
|
|
|
213 |
eventName:(nonnull NSString *)eventName
|
|
|
214 |
params:(nonnull NSDictionary<NSString *, NSString *> *)params {
|
|
|
215 |
return @{
|
|
|
216 |
kABTEventDictionaryOriginKey : origin,
|
|
|
217 |
kABTEventDictionaryNameKey : eventName,
|
|
|
218 |
kABTEventDictionaryTimestampKey : @([NSDate date].timeIntervalSince1970),
|
|
|
219 |
kABTEventDictionaryParametersKey : params
|
|
|
220 |
};
|
|
|
221 |
}
|
|
|
222 |
|
|
|
223 |
#pragma mark - experiment properties
|
|
|
224 |
- (NSString *)experimentIDOfExperiment:(id)experiment {
|
|
|
225 |
if (!experiment) {
|
|
|
226 |
return @"";
|
|
|
227 |
}
|
|
|
228 |
return [experiment valueForKey:kABTExperimentDictionaryExperimentIDKey];
|
|
|
229 |
}
|
|
|
230 |
|
|
|
231 |
- (NSString *)variantIDOfExperiment:(id)experiment {
|
|
|
232 |
if (!experiment) {
|
|
|
233 |
return @"";
|
|
|
234 |
}
|
|
|
235 |
return [experiment valueForKey:kABTExperimentDictionaryVariantIDKey];
|
|
|
236 |
}
|
|
|
237 |
|
|
|
238 |
- (NSInteger)maxNumberOfExperimentsOfOrigin:(NSString *)origin {
|
|
|
239 |
if (!_analytics) {
|
|
|
240 |
return 0;
|
|
|
241 |
}
|
|
|
242 |
return [_analytics maxUserProperties:origin];
|
|
|
243 |
}
|
|
|
244 |
|
|
|
245 |
#pragma mark - analytics internal methods
|
|
|
246 |
|
|
|
247 |
- (void)logEventWithOrigin:(NSString *)origin
|
|
|
248 |
payload:(ABTExperimentPayload *)payload
|
|
|
249 |
events:(FIRLifecycleEvents *)events {
|
|
|
250 |
NSString *setExperimentEventName = events.setExperimentEventName;
|
|
|
251 |
if (payload && payload.setEventToLog && payload.setEventToLog.length) {
|
|
|
252 |
setExperimentEventName = payload.setEventToLog;
|
|
|
253 |
}
|
|
|
254 |
NSDictionary<NSString *, NSString *> *params;
|
|
|
255 |
params = payload.experimentId ? @{payload.experimentId : payload.variantId} : @{};
|
|
|
256 |
[_analytics logEventWithOrigin:origin name:setExperimentEventName parameters:params];
|
|
|
257 |
}
|
|
|
258 |
|
|
|
259 |
#pragma mark - helper
|
|
|
260 |
|
|
|
261 |
- (BOOL)isExperiment:(id)experiment theSameAsPayload:(ABTExperimentPayload *)payload {
|
|
|
262 |
NSString *experimentID = [self experimentIDOfExperiment:experiment];
|
|
|
263 |
NSString *variantID = [self variantIDOfExperiment:experiment];
|
|
|
264 |
return [experimentID isEqualToString:payload.experimentId] &&
|
|
|
265 |
[variantID isEqualToString:payload.variantId];
|
|
|
266 |
}
|
|
|
267 |
|
|
|
268 |
- (ABTExperimentPayloadExperimentOverflowPolicy)
|
|
|
269 |
overflowPolicyWithPayload:(ABTExperimentPayload *)payload
|
|
|
270 |
originalPolicy:(ABTExperimentPayloadExperimentOverflowPolicy)originalPolicy {
|
|
|
271 |
if ([payload overflowPolicyIsValid]) {
|
|
|
272 |
return payload.overflowPolicy;
|
|
|
273 |
}
|
|
|
274 |
if (originalPolicy == ABTExperimentPayloadExperimentOverflowPolicyIgnoreNewest ||
|
|
|
275 |
originalPolicy == ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest) {
|
|
|
276 |
return originalPolicy;
|
|
|
277 |
}
|
|
|
278 |
return ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest;
|
|
|
279 |
}
|
|
|
280 |
|
|
|
281 |
@end
|