Apptimize

Apptimize is a mobile app testing and growth platform that allows customers to rapidly iterate throughout the app development process.

Apptimize can be used in conjunction with Braze to complement your growth marketing / CRM strategies with product UI testing by syncing experiments and data across both platforms.

Use Cases

With Braze and Apptimize together, you can leverage both platforms in conjunction to create powerful end-to-end experiences:

  • Synchronize the in-app and CRM marketing experiences for a custom promotion.
  • Test a new onboarding experience in Apptimize, and use Braze to nurture users throughout the new flow.
  • Concurrently test product feature configurations alongside their appropriate user messaging.
  • Tailor in-app experiences and their appropriate messaging for different segments of end users.

How It Works

Braze and Apptimize can be integrated together to pass data from SDK to SDK. You can sync active Apptimize A/B test groups back to Braze, allowing you to retarget the users in a particular Apptimize test within Braze via push, email, or In-App Messaging.

We have sample integration code which demonstrates how the Braze and Apptimize SDKs can pass data to power custom targeting and segmentation in Braze based on Apptimize experiment data.

This sample integration will set custom attributes on your users’ Braze User Profiles for the following Apptimize data:

  • The full list of active experiments that the user is currently enrolled in.
  • The full list of experiments the user has ever been enrolled in, including completed experiments.
  • The variant(s) the user has seen as part of an experiment participation.

Feature Flags are considered experiments where the only variant is whether the Feature Flag is on. If the Feature Flag is off, no data will be reported.

In addition, this integration will log a Braze Custom Event for the first participation event of an experiment. This can be done in one of two ways:

  • A custom event is generated with property data denoting the experiment name, the experiment ID, the variant name and the variant ID. You can then retarget users via real-time triggering using Braze’s Action-Based Delivery Campaigns and Canvases. Use these properties to identify the exact Apptimize Experiment that you want to trigger off of.
  • An attribute array is generated with entries for every participation that has occurred. Each participation is formatted as experiment_id_EXPERIMENT_ID:variant_id_VARIANT_ID:experiment_name_EXPERIMENT_NAME:variant_name_VARIANT_NAME

You can then use Braze’s Action-Based Delivery Campaigns or Canvases to send follow-on messages to users in real-time when these events are triggered.

Integration

iOS

In order to integrate with your app, import the following Appboy-Apptimize.m and Apptimize-Appboy.h files into your Xcode project, import the Appboy-Apptimize.h header into your AppDelegate implementation and add the following to didFinishLaunchingWithOptions after initializing both Appboy and Apptimize:

1
[ApptimizeAppboy setupExperimentTracking];

Appboy-Apptimize.h:

1
2
3
4
5
6
7
8
9
10
    //  Apptimize-Appboy.h

    #ifndef Apptimize_Appboy_h
    #define Apptimize_Appboy_h

    @interface ApptimizeAppboy : NSObject
    + (void)setupExperimentTracking;
    @end

    #endif /* Apptimize_Appboy_h */

Appboy-Apptimize.m:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
    //  Apptimize-Appboy.m

    #import <Foundation/Foundation.h>

    #import "Apptimize-Appboy.h"

    #import <Apptimize/Apptimize.h>
    #import <Apptimize/Apptimize+Variables.h>

    #import "Appboy.h"
    #import "ABKUser.h"

    // Key to store previous enrollment dictionary to check against to see if enrollment has changed
    NSString *const ApptimizeAppboyTestEnrollmentStorageKey = @"ApptimizeAppboyTestEnrollmentStorageKey";

    @implementation ApptimizeAppboy

    + (void)setupExperimentTracking
    {
        // Track for enrollment changes
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(apptimizeTestsProcessed:)
                                                     name:ApptimizeTestsProcessedNotification
                                                   object:nil];
        // Track for participation events
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(experimentDidGetViewed:)
                                                     name:ApptimizeTestRunNotification
                                                   object:nil];
    }

    + (void)apptimizeTestsProcessed:(NSNotification*)notification
    {
        NSLog(@"Appboy-Apptimize integration processing new Apptimize tests");
        [self updateForNewTests];
    }

    + (void)updateForNewTests
    {
        NSDictionary *savedEnrollmentDictionary = [[NSUserDefaults standardUserDefaults] objectForKey:ApptimizeAppboyTestEnrollmentStorageKey];
        NSDictionary *currentEnrollmentDictionary = [self getEnrollmentDictionaryFromTestInfo];

        BOOL enrollmentChanged = NO;

        for (id key in currentEnrollmentDictionary) {
            if (![savedEnrollmentDictionary[key] isEqualToString:currentEnrollmentDictionary[key]]) {
                enrollmentChanged = YES;
                NSString *testAttributeKey = [@"apptimize_test_" stringByAppendingString:key];
                [[Appboy sharedInstance].user addToCustomAttributeArrayWithKey:testAttributeKeyvalue :currentEnrollmentDictionary[key]];
            }
        }

        if (currentEnrollmentDictionary.count != savedEnrollmentDictionary.count) {
            enrollmentChanged = YES;
        }

        if (enrollmentChanged) {
            [[Appboy sharedInstance].user setCustomAttributeArrayWithKey:@"active_apptimize_tests" array:currentEnrollmentDictionary.allKeys];

            for (id key in currentEnrollmentDictionary.allKeys) {
                [[Appboy sharedInstance].user addToCustomAttributeArrayWithKey:@"all_apptimize_tests" value:key];
            }

            [[NSUserDefaults standardUserDefaults] setObject:currentEnrollmentDictionary forKey:ApptimizeAppboyTestEnrollmentStorageKey];
            [[NSUserDefaults standardUserDefaults] synchronize];
        }
    }

    // Dictionary with variant IDs keyed by test ID, both as NSStrings
    + (NSMutableDictionary *)getEnrollmentDictionaryFromTestInfo
    {
        NSMutableDictionary *enrollmentDictionary = [NSMutableDictionary dictionary];

        for(id key in [Apptimize testInfo]) {
            NSLog(@"key=%@ value=%@", key, [[Apptimize testInfo] objectForKey:key]);
            NSDictionary<ApptimizeTestInfo> *testInfo = [[Apptimize testInfo] objectForKey:key];
            enrollmentDictionary[[testInfo.testID stringValue]] = [testInfo.enrolledVariantID stringValue];
        }

        return enrollmentDictionary;
    }

    + (void)experimentDidGetViewed:(NSNotification*)notification
    {
        if (![notification.userInfo[ApptimizeTestFirstRunUserInfoKey] boolValue]) {
            return;
        }

        // Apptimize doesn't notify with IDs, so we iterate over all experiments to find the matching one.
        NSString *name = notification.userInfo[ApptimizeTestNameUserInfoKey];
        NSString *variant = notification.userInfo[ApptimizeVariantNameUserInfoKey];

        [[Apptimize testInfo] enumerateKeysAndObjectsUsingBlock:^(id key, id<ApptimizeTestInfo> experiment, BOOL *stop) {
            BOOL match = [experiment.testName isEqualToString:name] && [experiment.enrolledVariantName isEqualToString:variant];
            if (!match) {
                return;
            }

            // If you want to log a custom event for each participation
            [[Appboy sharedInstance] logCustomEvent:@"apptimize_experiment_viewed"
                                     withProperties: @{@"apptimize_experiment_name" : [experiment testName],
                                                          @"apptimize_variant_name" : [experiment enrolledVariantName],
                                                         @"apptimize_experiment_id" : [experiment testID],
                                                            @"apptimize_variant_id" : [experiment enrolledVariantID]}];

            // If you want a custom attribute array set for each participation
            [[Appboy sharedInstance].user addToCustomAttributeArrayWithKey:@"apptimize_experiments"
                                                                     value:[NSString stringWithFormat:@"experiment_id_%@:variant_id_%@:experiment_name_%@:variant_name_%@",
                                                                             [experiment testID], [experiment enrolledVariantID], [experiment testName], [experiment enrolledVariantName] ]];
            *stop = YES;
        }];
    }

    @end

Android

Import the ApptimizeAppboy.java class into your app and in your main Activity implementation, create a private member appboyApptimizeIntegration:

1
private ApptimizeAppboy appboyApptimizeIntegration;

Then in your onCreate method, after initializing Braze and Apptimize:

1
2
appboyApptimizeIntegration = new ApptimizeAppboy();
appboyApptimizeIntegration.configureExperimentTracking(this);

ApptimizeAppboy.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
    package com.apptimize.appboykit;

    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;
    import java.util.Map;
    import java.util.HashMap;
    import android.util.Log;

    import android.content.Context;

    import com.apptimize.Apptimize;
    import com.apptimize.ApptimizeTestInfo;
    import com.apptimize.Apptimize.OnExperimentsProcessedListener;
    import com.apptimize.Apptimize.OnExperimentRunListener;

    import com.appboy.Appboy;
    import com.appboy.AppboyUser;
    import com.appboy.models.outgoing.AppboyProperties;

    public class ApptimizeAppboy
            implements Apptimize.OnExperimentRunListener,
                       Apptimize.OnExperimentsProcessedListener {

        public void configureExperimentTracking(Context context) {
            appboyInstance = Appboy.getInstance(context);
            enrollmentStorage = new File(context.getDir("apptimize-appboy", Context.MODE_PRIVATE), ApptimizeAppboyTestEnrollmentStorage);

            Apptimize.setOnExperimentRunListener(this);
            Apptimize.addOnExperimentsProcessedListener(this);
        }

        @Override
        public void onExperimentRun(String experimentName, String variantName, boolean firstRun) {
            if (!firstRun) {
                return;
            }
            Map<String,ApptimizeTestInfo> testInfoMap = Apptimize.getTestInfo();

            if (testInfoMap == null) {
                return;
            }

            String experimentId = "";
            String variantId = "";

            Log.d("Apptimize-Appboy", "In onExperimentRun");

            for (ApptimizeTestInfo testInfo : testInfoMap.values()) {
                if (testInfo.getTestName().equals(experimentName) &&
                    testInfo.getEnrolledVariantName().equals(variantName)) {
                    experimentId = String.valueOf(testInfo.getTestId());
                    variantId = String.valueOf(testInfo.getEnrolledVariantId());
                }
            }
            Log.d("Apptimize-Appboy", "Logging participation for " + experimentName + ":" + experimentId + " and variant " + variantName + ":" + variantId);

            // If you want to log a custom event for each participation
            logParticipationEventAsEvent(experimentName, variantName, experimentId, variantId);

            // If you want a custom attribute array set for each participation
            logParticipationEventAsAttributes(experimentName, variantName, experimentId, variantId);
        }

        private void logParticipationEventAsEvent(String experimentName, String variantName, String experimentId, String variantId) {
            AppboyProperties eventProperties = new AppboyProperties();

            eventProperties.addProperty("apptimize_experiment_name", experimentName);
            eventProperties.addProperty("apptimize_variant_name", variantName);
            eventProperties.addProperty("apptimize_experiment_id", experimentId);
            eventProperties.addProperty("apptimize_variant_id", variantId);

            appboyInstance.logCustomEvent("apptimize_experiment_viewed", eventProperties);
        }

        private void logParticipationEventAsAttributes(String experimentName, String variantName, String experimentId, String variantId) {
            appboyInstance.getCurrentUser().addToCustomAttributeArray("apptimize_experiments",
                    "experiment_id_" + experimentId + ":variant_id_" + variantId + ":experiment_name_" + experimentName + ":variant_name_" + variantName);
        }

        @Override
        public void onExperimentsProcessed() {
            Map<String,String> currentEnrollmentDictionary = getEnrollmentDictionary();
            Map<String,String> savedEnrollmentDictionary = getPreviousEnrollmentDictionary();
            AppboyUser appboyUser = appboyInstance.getCurrentUser();

            boolean enrollmentChanged = false;

            Log.d("Apptimize-Appboy", "Processing experiments");

            for (String key : currentEnrollmentDictionary.keySet()) {
                if (savedEnrollmentDictionary == null ||
                    !currentEnrollmentDictionary.get(key).equals(savedEnrollmentDictionary.get(key))) {
                    Log.d("Apptimize-Appboy", "Found change in enrollment" + currentEnrollmentDictionary.get(key));
                    enrollmentChanged = true;
                    String testAttributeKey = "apptimize_test_" + key;
                    appboyUser.addToCustomAttributeArray(testAttributeKey, currentEnrollmentDictionary.get(key));
                }
            }

            if (currentEnrollmentDictionary.size() == 0 && savedEnrollmentDictionary.size() != 0) {
                enrollmentChanged = true;
            }

            if (enrollmentChanged) {
                Log.d("Apptimize-Appboy", "Enrollment changed");
                appboyUser.setCustomAttributeArray("active_apptimize_tests", currentEnrollmentDictionary.keySet().toArray(new String[0]));

                for (String key : currentEnrollmentDictionary.keySet()) {
                    appboyUser.addToCustomAttributeArray("all_apptimize_tests", key);
                }

                storePreviousEnrollmentDictionary(currentEnrollmentDictionary);
            }
        }

        private Map<String,String> getEnrollmentDictionary()
        {
            Map<String,String> enrollment = new HashMap<String,String>();
            Map<String,ApptimizeTestInfo> testInfoMap = Apptimize.getTestInfo();
            for (ApptimizeTestInfo testInfo : testInfoMap.values()) {
                Log.d("Apptimize-Appboy", "TestID: " + String.valueOf(testInfo.getTestId()) + " VariantID: " + String.valueOf(testInfo.getEnrolledVariantId()));
                enrollment.put(String.valueOf(testInfo.getTestId()), String.valueOf(testInfo.getEnrolledVariantId()));
            }
            return enrollment;
        }

        private Map<String,String> getPreviousEnrollmentDictionary()
        {
            ObjectInputStream enrollmentStream;
            try {
                enrollmentStream = new ObjectInputStream(new FileInputStream(enrollmentStorage));
            } catch(Exception e) {
                Log.d("Apptimize-Appboy", "Unable to open file");
                return null;
            }

            Map<String, String> previousEnrollment;
            try {
                 previousEnrollment = (Map<String,String>)enrollmentStream.readObject();
            } catch (Exception e) {
                Log.d("Apptimize-Appboy", "Unable to get previous enrollment");
                return null;
            }

            return previousEnrollment;
        }

        private void storePreviousEnrollmentDictionary(Map<String,String> enrollmentDictionary)
        {
            try {
                ObjectOutputStream enrollmentStream = new ObjectOutputStream(new FileOutputStream(enrollmentStorage));
                enrollmentStream.writeObject(enrollmentDictionary);
                enrollmentStream.flush();
                enrollmentStream.close();
            } catch (Exception e) {
                Log.d("Apptimize-Appboy", "Unable to save enrollment information");
            }

        }

        private Appboy appboyInstance;
        private File enrollmentStorage;

        private static String ApptimizeAppboyStorageDirectory;
        private static String ApptimizeAppboyTestEnrollmentStorage = "ApptimizeAppboyTestEnrollmentStorage";
    }
WAS THIS PAGE HELPFUL?
New Stuff!