diff --git a/README.md b/README.md index efe1999..b140030 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ AppMetrica.reportEvent('Hello world'); ```js import AppMetrica from 'react-native-appmetrica'; -// Starts the statistics collection process. +// Start the statistics collection process. AppMetrica.activateWithApiKey('...KEY...'); // OR AppMetrica.activateWithConfig({ @@ -40,10 +40,40 @@ AppMetrica.activateWithConfig({ firstActivationAsUpdate: true, }); -// Sends a custom event message and additional parameters (optional). +// Send a custom event message and additional parameters (optional). AppMetrica.reportEvent('My event'); AppMetrica.reportEvent('My event', { foo: 'bar' }); // Send a custom error event. AppMetrica.reportError('My error'); + +// Send user profile with predefined attributes. +AppMetrica.reportUserProfile({ name: 'User 1', age: 87 }); + +// Send user profile with custom attributes. +AppMetrica.reportUserProfile({ + likesMusic: true, + addedToFavorites: '+1', + score: 150, +}); ``` +### Reporting user profile + +All predefined attributes are supported. Use `null` to reset them. + +```js +type UserProfileAttributes = { + name?: ?string, + gender?: 'female' | 'male' | string | void, + age?: ?number, + birthDate?: Date | [number] | [number, number] | [number, number, number] | void, + notificationsEnabled?: boolean, + /** custom attributes */ + [string]: string | number | boolean, +}; +``` + +Custom attributes are supported. They can't be reset for now. + +Use values like `'+1'`, `'-10'` for counters. Current limitation is any custom attribute which value started with `'+'` or `'-'` will be considered as a counter. + diff --git a/android/src/main/java/com/doochik/RNAppMetrica/AppMetricaModule.java b/android/src/main/java/com/doochik/RNAppMetrica/AppMetricaModule.java index 2fc19c6..97830d1 100644 --- a/android/src/main/java/com/doochik/RNAppMetrica/AppMetricaModule.java +++ b/android/src/main/java/com/doochik/RNAppMetrica/AppMetricaModule.java @@ -8,15 +8,23 @@ import android.util.Log; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableMapKeySetIterator; import java.lang.Exception; +import java.util.Calendar; +import java.util.Date; import org.json.JSONObject; import com.yandex.metrica.YandexMetrica; import com.yandex.metrica.YandexMetricaConfig; +import com.yandex.metrica.profile.UserProfile; +import com.yandex.metrica.profile.Attribute; +import com.yandex.metrica.profile.GenderAttribute; + +import static com.facebook.react.bridge.ReadableType.Array; public class AppMetricaModule extends ReactContextBaseJavaModule { final static String ModuleName = "AppMetrica"; @@ -82,8 +90,125 @@ public class AppMetricaModule extends ReactContextBaseJavaModule { YandexMetrica.setUserProfileID(profileID); } + @ReactMethod + public void reportUserProfile(ReadableMap params) { + UserProfile.Builder userProfileBuilder = UserProfile.newBuilder(); + ReadableMapKeySetIterator iterator = params.keySetIterator(); + + while (iterator.hasNextKey()) { + String key = iterator.nextKey(); + + switch (key) { + // predefined attributes + case "name": + userProfileBuilder.apply( + params.isNull(key) + ? Attribute.name().withValueReset() + : Attribute.name().withValue(params.getString(key)) + ); + break; + case "gender": + userProfileBuilder.apply( + params.isNull(key) + ? Attribute.gender().withValueReset() + : Attribute.gender().withValue( + params.getString(key).equals("female") + ? GenderAttribute.Gender.FEMALE + : params.getString(key).equals("male") + ? GenderAttribute.Gender.MALE + : GenderAttribute.Gender.OTHER + ) + ); + break; + case "age": + userProfileBuilder.apply( + params.isNull(key) + ? Attribute.birthDate().withValueReset() + : Attribute.birthDate().withAge(params.getInt(key)) + ); + break; + case "birthDate": + if (params.isNull(key)) { + userProfileBuilder.apply( + Attribute.birthDate().withValueReset() + ); + } else if (params.getType(key) == Array) { + // an array of [ year[, month][, day] ] + ReadableArray date = params.getArray(key); + if (date.size() == 1) { + userProfileBuilder.apply( + Attribute.birthDate().withBirthDate( + date.getInt(0) + ) + ); + } else if (date.size() == 2) { + userProfileBuilder.apply( + Attribute.birthDate().withBirthDate( + date.getInt(0), + date.getInt(1) + ) + ); + } else { + userProfileBuilder.apply( + Attribute.birthDate().withBirthDate( + date.getInt(0), + date.getInt(1), + date.getInt(2) + ) + ); + } + } else { + // number of milliseconds since Unix epoch + Date date = new Date((long)params.getInt(key)); + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + userProfileBuilder.apply( + Attribute.birthDate().withBirthDate(cal) + ); + } + break; + case "notificationsEnabled": + userProfileBuilder.apply( + params.isNull(key) + ? Attribute.notificationsEnabled().withValueReset() + : Attribute.notificationsEnabled().withValue(params.getBoolean(key)) + ); + break; + // custom attributes + default: + // TODO: come up with a syntax solution to reset custom attributes. `null` will break type checking here + switch (params.getType(key)) { + case Boolean: + userProfileBuilder.apply( + Attribute.customBoolean(key).withValue(params.getBoolean(key)) + ); + break; + case Number: + userProfileBuilder.apply( + Attribute.customNumber(key).withValue(params.getDouble(key)) + ); + break; + case String: + String value = params.getString(key); + if (value.startsWith("+") || value.startsWith("-")) { + userProfileBuilder.apply( + Attribute.customCounter(key).withDelta(Double.parseDouble(value)) + ); + } else { + userProfileBuilder.apply( + Attribute.customString(key).withValue(value) + ); + } + break; + } + } + } + + YandexMetrica.reportUserProfile(userProfileBuilder.build()); + } + private String convertReadableMapToJson(final ReadableMap readableMap) { - ReadableMapKeySetIterator iterator = readableMap.keySetIterator(); + ReadableMapKeySetIterator iterator = readableMap.keySetIterator(); JSONObject json = new JSONObject(); try { diff --git a/index.js b/index.js index 3b08d00..5081d1d 100644 --- a/index.js +++ b/index.js @@ -9,6 +9,15 @@ type ActivationConfig = { firstActivationAsUpdate?: boolean, }; +type UserProfileAttributes = { + name?: ?string, + gender?: 'female' | 'male' | string | void, + age?: ?number, + birthDate?: Date | [number] | [number, number] | [number, number, number] | void, + notificationsEnabled?: boolean, + [string]: string | number | boolean, +}; + export default { /** @@ -52,4 +61,24 @@ export default { setUserProfileID(userProfileId: string) { AppMetrica.setUserProfileID(userProfileId); }, + + /** + * Sets attributes of the user profile. + * @param {object} attributes + */ + reportUserProfile(attributes: UserProfileAttributes) { + const readyAttributes = {}; + Object.keys(attributes).forEach(key => { + if ( + key === 'birthDate' && + typeof attributes.birthDate === 'object' && + typeof attributes.birthDate.getTime === 'function' + ) { + readyAttributes.birthDate = attributes.birthDate.getTime(); + } else { + readyAttributes[key] = attributes[key]; + } + }); + AppMetrica.reportUserProfile(readyAttributes); + }, }; diff --git a/ios/RCTAppMetrica/RCTAppMetrica/RCTAppMetrica.m b/ios/RCTAppMetrica/RCTAppMetrica/RCTAppMetrica.m index a0c26f9..1b9b24f 100644 --- a/ios/RCTAppMetrica/RCTAppMetrica/RCTAppMetrica.m +++ b/ios/RCTAppMetrica/RCTAppMetrica/RCTAppMetrica.m @@ -42,4 +42,76 @@ RCT_EXPORT_METHOD(reportError:(NSString *)message) { RCT_EXPORT_METHOD(setUserProfileID:(NSString *)userProfileID) { [YMMYandexMetrica setUserProfileID:userProfileID]; } + +RCT_EXPORT_METHOD(reportUserProfile:(NSDictionary *)attributes) { + YMMMutableUserProfile *profile = [[YMMMutableUserProfile alloc] init]; + NSMutableArray *attrsArray = [NSMutableArray array]; + for (NSString* key in attributes) { + // predefined attributes + if ([key isEqual: @"name"]) { + if (attributes[key] == [NSNull null]) { + [attrsArray addObject:[[YMMProfileAttribute name] withValueReset]]; + } else { + [attrsArray addObject:[[YMMProfileAttribute name] withValue:[attributes[key] stringValue]]]; + } + } else if ([key isEqual: @"gender"]) { + if (attributes[key] == [NSNull null]) { + [attrsArray addObject:[[YMMProfileAttribute gender] withValueReset]]; + } else { + [attrsArray addObject:[[YMMProfileAttribute gender] withValue:[[attributes[key] stringValue] isEqual: @"female"] ? YMMGenderTypeFemale : [[attributes[key] stringValue] isEqual: @"male"] ? YMMGenderTypeMale : YMMGenderTypeOther]]; + } + } else if ([key isEqual: @"age"]) { + if (attributes[key] == [NSNull null]) { + [attrsArray addObject:[[YMMProfileAttribute birthDate] withValueReset]]; + } else { + [attrsArray addObject:[[YMMProfileAttribute birthDate] withAge:[attributes[key] intValue]]]; + } + } else if ([key isEqual: @"birthDate"]) { + if (attributes[key] == [NSNull null]) { + [attrsArray addObject:[[YMMProfileAttribute birthDate] withValueReset]]; + } else if ([attributes[key] isKindOfClass:[NSArray class]]) { + NSArray *date = [attributes[key] array]; + if ([date count] == 1) { + [attrsArray addObject:[[YMMProfileAttribute birthDate] withYear:[[date objectAtIndex:0] intValue]]]; + } else if ([[attributes[key] array] count] == 2) { + [attrsArray addObject:[[YMMProfileAttribute birthDate] withYear:[[date objectAtIndex:0] intValue] month:[[date objectAtIndex:1] intValue]]]; + } else if ([[attributes[key] array] count] == 3) { + [attrsArray addObject:[[YMMProfileAttribute birthDate] withYear:[[date objectAtIndex:0] intValue] month:[[date objectAtIndex:1] intValue] day:[[date objectAtIndex:2] intValue]]]; + } + } else { + // number of milliseconds since Unix epoch + NSDate *date = [attributes[key] date]; + NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian]; + NSDateComponents *dateComponents = + [gregorian components:(NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay) fromDate:date]; + [attrsArray addObject:[[YMMProfileAttribute birthDate] withDateComponents:dateComponents]]; + } + } else if ([key isEqual: @"notificationsEnabled"]) { + if (attributes[key] == [NSNull null]) { + [attrsArray addObject:[[YMMProfileAttribute notificationsEnabled] withValueReset]]; + } else { + [attrsArray addObject:[[YMMProfileAttribute notificationsEnabled] withValue:[attributes[key] boolValue]]]; + } + // custom attributes + } else { + // TODO: come up with a syntax solution to reset custom attributes. `null` will break type checking here + if ([attributes[key] isEqual: @YES] || [attributes[key] isEqual: @NO]) { + [attrsArray addObject:[[YMMProfileAttribute customBool:key] withValue:[attributes[key] boolValue]]]; + } else if ([attributes[key] isKindOfClass:[NSNumber class]]) { + [attrsArray addObject:[[YMMProfileAttribute customNumber:key] withValue:[attributes[key] doubleValue]]]; + // [NSNumber numberWithInt:[attributes[key] intValue]] + } else if ([attributes[key] isKindOfClass:[NSString class]]) { + if ([attributes[key] hasPrefix:@"+"] || [attributes[key] hasPrefix:@"-"]) { + [attrsArray addObject:[[YMMProfileAttribute customCounter:key] withDelta:[attributes[key] doubleValue]]]; + } else { + [attrsArray addObject:[[YMMProfileAttribute customString:key] withValue:attributes[key]]]; + } + } + } + } + + [profile applyFromArray: attrsArray]; + [YMMYandexMetrica reportUserProfile:[profile copy] onFailure:NULL]; +} + @end