|
| 1 | +/* |
| 2 | + * Copyright 2018 Google |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + * you may not use this file except in compliance with the License. |
| 6 | + * You may obtain a copy of the License at |
| 7 | + * |
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | + * |
| 10 | + * Unless required by applicable law or agreed to in writing, software |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + * See the License for the specific language governing permissions and |
| 14 | + * limitations under the License. |
| 15 | + */ |
| 16 | + |
| 17 | +#import "FIRIAMAnalyticsEventLoggerImpl.h" |
| 18 | + |
| 19 | +#import <FirebaseAnalyticsInterop/FIRAnalyticsInterop.h> |
| 20 | +#import <FirebaseCore/FIRLogger.h> |
| 21 | +#import "FIRCore+InAppMessaging.h" |
| 22 | +#import "FIRIAMClearcutLogger.h" |
| 23 | + |
| 24 | +typedef void (^FIRAUserPropertiesCallback)(NSDictionary *userProperties); |
| 25 | + |
| 26 | +@interface FIRIAMAnalyticsEventLoggerImpl () |
| 27 | +@property(readonly, nonatomic) FIRIAMClearcutLogger *clearCutLogger; |
| 28 | +@property(readonly, nonatomic) id<FIRIAMTimeFetcher> timeFetcher; |
| 29 | +@property(readonly, nonatomic) long conversionTrackingExpiresInSeconds; |
| 30 | +@property(nonatomic, readonly) NSUserDefaults *userDefaults; |
| 31 | +@end |
| 32 | + |
| 33 | +// in these kFAXX constants, FA represents FirebaseAnalytics |
| 34 | +static NSString *const kFIREventOriginFIAM = @"fiam"; |
| 35 | +; |
| 36 | +static NSString *const kFAEventNameForImpression = @"firebase_in_app_message_impression"; |
| 37 | +static NSString *const kFAEventNameForAction = @"firebase_in_app_message_action"; |
| 38 | +static NSString *const kFAEventNameForDismiss = @"firebase_in_app_message_dismiss"; |
| 39 | + |
| 40 | +// In order to support tracking conversions from clicking a fiam event, we need to set |
| 41 | +// an analytics user property with the fiam message's campaign id. |
| 42 | +// This is the user property as kFIRUserPropertyLastNotification defined for FCM. |
| 43 | +// Unlike FCM, FIAM would only allow the user property to exist up to certain expiration time |
| 44 | +// after which, we stop attributing any further conversions to that fiam message click. |
| 45 | +// So we include kFAUserPropertyPrefixForFIAM as the prefix for the entry written by fiam SDK |
| 46 | +// to avoid removing entries written by FCM SDK |
| 47 | +static NSString *const kFAUserPropertyForLastNotification = @"_ln"; |
| 48 | +static NSString *const kFAUserPropertyPrefixForFIAM = @"fiam:"; |
| 49 | + |
| 50 | +// This user defaults key is for the entry to tell when we should remove the private user |
| 51 | +// property from a prior action url click to stop conversion attribution for a campaign |
| 52 | +static NSString *const kFIAMUserDefaualtsKeyForRemoveUserPropertyTimeInSeconds = |
| 53 | + @"firebase-iam-conversion-tracking-expires-in-seconds"; |
| 54 | + |
| 55 | +@implementation FIRIAMAnalyticsEventLoggerImpl { |
| 56 | + id<FIRAnalyticsInterop> _analytics; |
| 57 | +} |
| 58 | + |
| 59 | +- (instancetype)initWithClearcutLogger:(FIRIAMClearcutLogger *)ctLogger |
| 60 | + usingTimeFetcher:(id<FIRIAMTimeFetcher>)timeFetcher |
| 61 | + usingUserDefaults:(nullable NSUserDefaults *)userDefaults |
| 62 | + conversionExpires:(long)conversionExpiresInSeconds |
| 63 | + analytics:(nullable id<FIRAnalyticsInterop>)analytics { |
| 64 | + if (self = [super init]) { |
| 65 | + _clearCutLogger = ctLogger; |
| 66 | + _timeFetcher = timeFetcher; |
| 67 | + _analytics = analytics; |
| 68 | + _userDefaults = userDefaults ? userDefaults : [NSUserDefaults standardUserDefaults]; |
| 69 | + |
| 70 | + if (!_analytics) { |
| 71 | + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM280002", |
| 72 | + @"Firebase In App Messaging was not configured with FirebaseAnalytics."); |
| 73 | + } |
| 74 | + _conversionTrackingExpiresInSeconds = conversionExpiresInSeconds; |
| 75 | + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM280003", |
| 76 | + @"Conversion tracking from a click expires in %ld seconds", |
| 77 | + conversionExpiresInSeconds); |
| 78 | + |
| 79 | + // On startup, we make a check for conversion tracking expiration |
| 80 | + [self checkOnConversionTrackingExpiration]; |
| 81 | + } |
| 82 | + return self; |
| 83 | +} |
| 84 | + |
| 85 | +- (void)checkOnConversionTrackingExpiration { |
| 86 | + double expireTimeInSeconds = |
| 87 | + [_userDefaults doubleForKey:kFIAMUserDefaualtsKeyForRemoveUserPropertyTimeInSeconds]; |
| 88 | + |
| 89 | + double nowInSeconds = (double)[self.timeFetcher currentTimestampInSeconds]; |
| 90 | + |
| 91 | + if (expireTimeInSeconds < nowInSeconds) { |
| 92 | + dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); |
| 93 | + [_analytics |
| 94 | + checkLastNotificationForOrigin:kFIREventOriginFIAM |
| 95 | + queue:queue |
| 96 | + callback:^(NSString *_Nullable currentLastNotificationProperty) { |
| 97 | + if (currentLastNotificationProperty) { |
| 98 | + if ([self setAnalyticsUserPropertyForKey: |
| 99 | + kFAUserPropertyForLastNotification |
| 100 | + withValue:@"empty"]) { |
| 101 | + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM280005", |
| 102 | + @"User property for conversion tracking has " |
| 103 | + @"expired, set to be empty"); |
| 104 | + } |
| 105 | + } |
| 106 | + }]; |
| 107 | + } |
| 108 | +} |
| 109 | + |
| 110 | +- (NSDictionary *)constructFAEventParamsWithCampaignID:(NSString *)campaignID |
| 111 | + campaignName:(NSString *)campaignName { |
| 112 | + // event parameter names are aligned with definitions in event_names_util.cc |
| 113 | + return @{ |
| 114 | + @"_nmn" : campaignName ?: @"unknown", |
| 115 | + @"_nmid" : campaignID ?: @"unknown", |
| 116 | + @"_ndt" : @([self.timeFetcher currentTimestampInSeconds]) |
| 117 | + }; |
| 118 | +} |
| 119 | + |
| 120 | +- (void)logFAEventsForMessageImpressionWithcampaignID:(NSString *)campaignID |
| 121 | + campaignName:(NSString *)campaignName { |
| 122 | + if (_analytics) { |
| 123 | + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM280001", |
| 124 | + @"Log campaign impression Firebase Analytics event for campaign ID %@", campaignID); |
| 125 | + |
| 126 | + NSDictionary *params = [self constructFAEventParamsWithCampaignID:campaignID |
| 127 | + campaignName:campaignName]; |
| 128 | + [_analytics logEventWithOrigin:kFIREventOriginFIAM |
| 129 | + name:kFAEventNameForImpression |
| 130 | + parameters:params]; |
| 131 | + } |
| 132 | +} |
| 133 | + |
| 134 | +- (BOOL)setAnalyticsUserPropertyForKey:(NSString *)key withValue:(NSString *)value { |
| 135 | + if (!_analytics || !key || !value) { |
| 136 | + return NO; |
| 137 | + } |
| 138 | + [_analytics setUserPropertyWithOrigin:kFIREventOriginFIAM name:key value:value]; |
| 139 | + return YES; |
| 140 | +} |
| 141 | + |
| 142 | +- (void)logFAEventsForMessageActionWithCampaignID:(NSString *)campaignID |
| 143 | + campaignName:(NSString *)campaignName { |
| 144 | + if (_analytics) { |
| 145 | + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM280004", |
| 146 | + @"Log action click Firebase Analytics event for campaign ID %@", campaignID); |
| 147 | + |
| 148 | + NSDictionary *params = [self constructFAEventParamsWithCampaignID:campaignID |
| 149 | + campaignName:campaignName]; |
| 150 | + |
| 151 | + [_analytics logEventWithOrigin:kFIREventOriginFIAM |
| 152 | + name:kFAEventNameForAction |
| 153 | + parameters:params]; |
| 154 | + } |
| 155 | + |
| 156 | + // set a special user property so that conversion events can be queried based on that |
| 157 | + // for reporting purpose |
| 158 | + NSString *conversionTrackingUserPropertyValue = |
| 159 | + [NSString stringWithFormat:@"%@%@", kFAUserPropertyPrefixForFIAM, campaignID]; |
| 160 | + |
| 161 | + if ([self setAnalyticsUserPropertyForKey:kFAUserPropertyForLastNotification |
| 162 | + withValue:conversionTrackingUserPropertyValue]) { |
| 163 | + // reset the user property expiration time |
| 164 | + long expirationTime = (long)[self.timeFetcher currentTimestampInSeconds] + |
| 165 | + self.conversionTrackingExpiresInSeconds; |
| 166 | + |
| 167 | + [_userDefaults setDouble:expirationTime |
| 168 | + forKey:kFIAMUserDefaualtsKeyForRemoveUserPropertyTimeInSeconds]; |
| 169 | + |
| 170 | + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM280009", |
| 171 | + @"User property for conversion tracking was set for campaign %@ " |
| 172 | + "and expires at epoch time in seconds %ld", |
| 173 | + campaignID, expirationTime); |
| 174 | + } |
| 175 | +} |
| 176 | + |
| 177 | +- (void)logFAEventsForMessageDismissWithcampaignID:(NSString *)campaignID |
| 178 | + campaignName:(NSString *)campaignName { |
| 179 | + if (_analytics) { |
| 180 | + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM280007", |
| 181 | + @"Log message dismiss Firebase Analytics event for campaign ID %@", campaignID); |
| 182 | + |
| 183 | + NSDictionary *params = [self constructFAEventParamsWithCampaignID:campaignID |
| 184 | + campaignName:campaignName]; |
| 185 | + [_analytics logEventWithOrigin:kFIREventOriginFIAM |
| 186 | + name:kFAEventNameForDismiss |
| 187 | + parameters:params]; |
| 188 | + } |
| 189 | +} |
| 190 | + |
| 191 | +- (void)logAnalyticsEventForType:(FIRIAMAnalyticsLogEventType)eventType |
| 192 | + forCampaignID:(NSString *)campaignID |
| 193 | + withCampaignName:(NSString *)campaignName |
| 194 | + eventTimeInMs:(nullable NSNumber *)eventTimeInMs |
| 195 | + completion:(void (^)(BOOL success))completion { |
| 196 | + // log Firebase Analytics event first |
| 197 | + if (eventType == FIRIAMAnalyticsEventMessageImpression) { |
| 198 | + [self logFAEventsForMessageImpressionWithcampaignID:campaignID campaignName:campaignName]; |
| 199 | + } else if (eventType == FIRIAMAnalyticsEventActionURLFollow) { |
| 200 | + [self logFAEventsForMessageActionWithCampaignID:campaignID campaignName:campaignName]; |
| 201 | + } else if (eventType == FIRIAMAnalyticsEventMessageDismissAuto || |
| 202 | + eventType == FIRIAMAnalyticsEventMessageDismissClick) { |
| 203 | + [self logFAEventsForMessageDismissWithcampaignID:campaignID campaignName:campaignName]; |
| 204 | + } |
| 205 | + |
| 206 | + // and do clearcut logging as well |
| 207 | + [self.clearCutLogger logAnalyticsEventForType:eventType |
| 208 | + forCampaignID:campaignID |
| 209 | + withCampaignName:campaignName |
| 210 | + eventTimeInMs:eventTimeInMs |
| 211 | + completion:completion]; |
| 212 | +} |
| 213 | +@end |
0 commit comments