Skip to content

Commit

Permalink
Profile API implementation (#15)
Browse files Browse the repository at this point in the history
* Profile API implementation
* Update demo apps to show use of profile API
* Update README with Profile API docs
  • Loading branch information
bmilekic authored Nov 24, 2020
1 parent 8f70220 commit 67281ed
Show file tree
Hide file tree
Showing 10 changed files with 169 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public View onCreateView(@NonNull LayoutInflater inflater,

targetingDataView.setText(msg.toString());
mPublisherAdView.loadAd(adRequest.build());
profile();
witness();
});
});
Expand All @@ -77,6 +78,7 @@ public View onCreateView(@NonNull LayoutInflater inflater,

targetingDataView.setText(msg.toString());
mPublisherAdView.loadAd(adRequest.build());
profile();
witness();
});

Expand All @@ -90,9 +92,33 @@ public View onCreateView(@NonNull LayoutInflater inflater,
return root;
}

private void profile() {
HashMap<String,Object> traits = new HashMap<String,Object>();
traits.put("gender", "F");
traits.put("age", 38);
traits.put("hasAccount", true);

MainActivity.OPTABLE
.profile(traits)
.observe(getViewLifecycleOwner(), result -> {
final StringBuilder msg = new StringBuilder();
msg.append(targetingDataView.getText().toString());

if (result.getStatus() == OptableSDK.Status.SUCCESS) {
msg.append("\n\nSuccess calling profile API to set user traits.\n\n");
} else {
msg.append("\n\nOptableSDK Error: " + result.getMessage() + "\n\n");
}

targetingDataView.setText(msg.toString());
});
}

private void witness() {
HashMap<String, String> eventProperties = new HashMap<String, String>();
HashMap<String,Object> eventProperties = new HashMap<String,Object>();
eventProperties.put("exampleKey", "exampleValue");
eventProperties.put("exampleKey2", 123);
eventProperties.put("exampleKey3", false);

MainActivity.OPTABLE
.witness("GAMBannerFragment.loadAdButtonClicked", eventProperties)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@

<TextView
android:id="@+id/targetingDataView"
android:layout_width="282dp"
android:layout_height="168dp"
android:layout_width="291dp"
android:layout_height="346dp"
android:layout_marginTop="100dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
Expand Down
2 changes: 1 addition & 1 deletion DemoApp/DemoAppJava/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.0'
classpath 'com.android.tools.build:gradle:4.1.1'

// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class GAMBannerFragment : Fragment() {

targetingDataView.setText(msg)
mPublisherAdView.loadAd(adRequest.build())
profile()
witness()
})
}
Expand All @@ -75,6 +76,7 @@ class GAMBannerFragment : Fragment() {

targetingDataView.setText(msg)
mPublisherAdView.loadAd(adRequest.build())
profile()
witness()
}

Expand All @@ -88,11 +90,27 @@ class GAMBannerFragment : Fragment() {
return root
}

private fun profile() {
MainActivity.OPTABLE!!
.profile(
hashMapOf("gender" to "F", "age" to 38, "hasAccount" to true)
)
.observe(viewLifecycleOwner, Observer { result ->
var msg = targetingDataView.text.toString()
if (result.status == OptableSDK.Status.SUCCESS) {
msg += "\n\nSuccess calling profile API to set traits on user.\n\n"
} else {
msg += "\n\nOptableSDK Error: ${result.message}\n\n"
}
targetingDataView.setText(msg)
})
}

private fun witness() {
MainActivity.OPTABLE!!
.witness(
"GAMBannerFragment.loadAdButtonClicked",
hashMapOf("exampleKey" to "exampleValue")
hashMapOf("exampleKey" to "exampleValue", "anotherExample" to 123, "foo" to false)
)
.observe(viewLifecycleOwner, Observer { result ->
var msg = targetingDataView.text.toString()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@

<TextView
android:id="@+id/targetingDataView"
android:layout_width="282dp"
android:layout_height="168dp"
android:layout_width="281dp"
android:layout_height="366dp"
android:layout_marginTop="100dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
Expand Down
2 changes: 1 addition & 1 deletion DemoApp/DemoAppKotlin/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.0'
classpath 'com.android.tools.build:gradle:4.1.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

// NOTE: Do not place your application dependencies here; they belong
Expand Down
59 changes: 56 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Kotlin SDK for integrating with optable-sandbox from an Android application.
- [Installing](#installing)
- [Using](#using)
- [Identify API](#identify-api)
- [Profile API](#profile-api)
- [Targeting API](#targeting-api)
- [Witness API](#witness-api)
- [Integrating GAM360](#integrating-gam360)
Expand Down Expand Up @@ -170,6 +171,58 @@ Since the `sendGoogleAdIDBoolean` value provided to `identify()` is `true`, the

The frequency of invocation of `identify` is up to you, however for optimal identity resolution we recommended to call the `identify()` method on your SDK instance every time you authenticate a user, as well as periodically, such as for example once every 15 to 60 minutes while the application is being actively used and an internet connection is available.

### Profile API

To associate key value traits with the user's device, for eventual audience assembly, you can call the profile API as follows:

#### Kotlin

```kotlin
import co.optable.android_sdk.OptableSDK
import my.org.app.MainActivity
import android.util.Log
...
MainActivity.OPTABLE!!
.profile(hashMapOf("gender" to "F", "age" to 38, "hasAccount" to true))
.observe(viewLifecycleOwner, Observer { result ->
if (result.status == OptableSDK.Status.SUCCESS) {
           Log.i("Profile API Success... ")
} else {
// result.status is OptableSDK.Status.ERROR
// result.message is the error message
Log.e("Profile API Error: ${result.message}")
}
})
```

#### Java

```java
import co.optable.android_sdk.OptableSDK;
import co.optable.demoappjava.MainActivity;
import android.util.Log;
import java.util.HashMap;
...
HashMap<String,Object> traits = new HashMap<String,Object>();
traits.put("gender", "F");
traits.put("age", 38);
traits.put("hasAccount", true);

MainActivity.OPTABLE
.profile(traits)
.observe(getViewLifecycleOwner(), result -> {
if (result.getStatus() == OptableSDK.Status.SUCCESS) {
Log.i(null, "Profile API Success... ");
} else {
// result.getStatus() is OptableSDK.Status.ERROR
// result.getMessage() is the error message
Log.e(null, "Profile API Error: " + result.getMessage());
}
});
```

The specified traits are associated with the user's device and can be used for matching during audience assembly. The traits are of type `OptableProfileTraits` which is an alias for `HashMap<String,Any>` (or `HashMap<String,Object>` in Java), and should consist only of key-value pairs where the key is a string and the value is either a string, number, or boolean.

### Targeting API

To get the targeting key values associated by the configured sandbox with the device in real-time, you can call the `targeting` API as follows:
Expand Down Expand Up @@ -299,7 +352,7 @@ import co.optable.demoappjava.MainActivity;
import android.util.Log;
import java.util.HashMap;
...
HashMap<String, String> eventProperties = new HashMap<String, String>();
HashMap<String,Object> eventProperties = new HashMap<String,Object>();
eventProperties.put("exampleKey", "exampleValue");

MainActivity.OPTABLE
Expand All @@ -315,7 +368,7 @@ MainActivity.OPTABLE
});
```

The specified event type and properties are associated with the logged event and which can be used for matching during audience assembly. The optional witness event properties are of type `OptableWitnessProperties` which is an alias for `HashMap<String,String>`, and should consist only of string key-value pairs.
The specified event type and properties are associated with the logged event and which can be used for matching during audience assembly. The optional witness event properties are of type `OptableWitnessProperties` which is an alias for `HashMap<String,Any>` (or `HashMap<String,Object>` in Java), and should consist only of key-value pairs where the key is a string and the value is either a string, number, or boolean.

### Integrating GAM360

Expand Down Expand Up @@ -432,7 +485,7 @@ override fun onCreate(savedInstanceState: Bundle?) {
}
```

#### Java
#### Java

```java
@Override
Expand Down
52 changes: 48 additions & 4 deletions android_sdk/src/main/java/co/optable/android_sdk/OptableSDK.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,23 @@ import java.security.MessageDigest
*/
typealias OptableIdentifyInput = List<String>

/*
* Profile API expects user traits:
*/
typealias OptableProfileTraits = HashMap<String,Any>

/*
* Witness API expects event properties:
*/
typealias OptableWitnessProperties = HashMap<String, String>
typealias OptableWitnessProperties = HashMap<String,Any>

/*
* Identify and Witness APIs usually just return {}... Void would be better but that results in
* retrofit2 error when parsing response, even when the API responded successfully, since {} is
* technically a HashMap:
* Identify, Profile, and Witness APIs usually just return {}... Void would be better but that
* results in retrofit2 error when parsing response, even when the API responded successfully,
* since {} is technically a HashMap:
*/
typealias OptableIdentifyResponse = HashMap<Any,Any>
typealias OptableProfileResponse = HashMap<Any,Any>
typealias OptableWitnessResponse = HashMap<Any,Any>

/*
Expand Down Expand Up @@ -181,6 +187,44 @@ class OptableSDK @JvmOverloads constructor(context: Context, host: String, app:
}
}

/*
* profile(traits) calls the Optable Sandbox "profile" API in order to associate the
* specified keyvalue OptableProfileTraits 'traits', which can be subsequently used for
* audience assembly.
*
* It is asynchronous, so the caller may call observe() on the returned LiveData and expect
* an instance of Response<OptableProfileResponse> in the result. Success can be checked by
* comparing result.status to OptableSDK.Status.SUCCESS. Note that result.data!! will point
* to an empty HashMap on success, and can therefore be ignored.
*/
fun profile(traits: OptableProfileTraits):
LiveData<Response<OptableProfileResponse>> {
val liveData = MutableLiveData<Response<OptableProfileResponse>>()
val client = this.client

GlobalScope.launch {
val response = client.Profile(traits)
when (response) {
is EdgeResponse.Success -> {
liveData.postValue(Response.success(response.body))
}
is EdgeResponse.ApiError -> {
liveData.postValue(Response.error(response.body))
}
is EdgeResponse.NetworkError -> {
liveData.postValue(Response.error(
Response.Error("NetworkError", "None")))
}
is EdgeResponse.UnknownError -> {
liveData.postValue(Response.error(
Response.Error("UnknownError", "None")))
}
}
}

return liveData
}

/*
* targeting() calls the Optable Sandbox "targeting" API, which returns the key-value targeting
* data matching the user/device/app.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,14 @@ class Client(private val config: Config, private val context: Context) {
return edgeService!!.Identify(this.config.app, idList)
}

suspend fun Profile(traits: OptableProfileTraits):
EdgeResponse<OptableProfileResponse, OptableSDK.Response.Error>
{
val profileBody = HashMap<String,Any>()
profileBody.put("traits", traits)
return edgeService!!.Profile(this.config.app, profileBody)
}

suspend fun Targeting():
EdgeResponse<OptableTargetingResponse, OptableSDK.Response.Error>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ interface EdgeService {
suspend fun Identify(@Path("app") app: String, @Body idList: OptableIdentifyInput):
EdgeResponse<OptableIdentifyResponse, OptableSDK.Response.Error>

@POST("/{app}/profile")
suspend fun Profile(@Path("app") app: String,
@Body profileBody: HashMap<String,Any>):
EdgeResponse<OptableProfileResponse, OptableSDK.Response.Error>

@GET("/{app}/targeting")
suspend fun Targeting(@Path("app") app: String):
EdgeResponse<OptableTargetingResponse, OptableSDK.Response.Error>
Expand Down

0 comments on commit 67281ed

Please sign in to comment.