Skip to content

Commit

Permalink
Merge pull request #13 from Optable/6-oeid-auto-identify
Browse files Browse the repository at this point in the history
Support for auto-identify on newsletter traffic
  • Loading branch information
bmilekic authored Nov 3, 2020
2 parents a339f4c + 7ec5327 commit c999319
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 11 deletions.
1 change: 1 addition & 0 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

80 changes: 70 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ Kotlin SDK for integrating with optable-sandbox from an Android application.
- [Targeting API](#targeting-api)
- [Witness API](#witness-api)
- [Integrating GAM360](#integrating-gam360)
- [Identifying visitors arriving from Email newsletters](#identifying-visitors-arriving-from-email-newsletters)
- [Insert oeid into your Email newsletter template](#insert-oeid-into-your-email-newsletter-template)
- [Capture clicks on deep links in your application](#capture-clicks-on-deep-links-in-your-application)
- [Call tryIdentifyFromURI SDK API](#call-tryidentifyfromuri-sdk-api)
- [Demo Applications](#demo-applications)
- [Building](#building)

Expand Down Expand Up @@ -51,7 +55,7 @@ Remember to replace `VERSION_TAG` with the latest or desired [SDK release](https

To configure an instance of the SDK integrating with an [Optable](https://optable.co/) sandbox running at hostname `sandbox.customer.com`, from a configured application origin identified by slug `my-app`, you can instantiate the SDK from an Activity or Application `Context`, such as for example the following application `MainActivity`:

Kotlin:
#### Kotlin

```kotlin
import co.optable.android_sdk.OptableSDK
Expand All @@ -72,7 +76,7 @@ class MainActivity : AppCompatActivity() {
}
```

Java:
#### Java

```java
import co.optable.android_sdk.OptableSDK;
Expand Down Expand Up @@ -107,7 +111,7 @@ However, since production sandboxes only listen to TLS traffic, the above is rea

To associate a user device with an authenticated identifier such as an Email address, or with other known IDs such as the Google Advertising ID, or even your own vendor or app level `PPID`, you can call the `identify` API as follows:

Kotlin:
#### Kotlin

```kotlin
import co.optable.android_sdk.OptableSDK
Expand All @@ -133,7 +137,7 @@ MainActivity.OPTABLE!!
})
```

Java:
#### Java

```java
import co.optable.android_sdk.OptableSDK;
Expand Down Expand Up @@ -170,7 +174,7 @@ The frequency of invocation of `identify` is up to you, however for optimal iden

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:

Kotlin:
#### Kotlin

```kotlin
import co.optable.android_sdk.OptableSDK
Expand All @@ -195,7 +199,7 @@ MainActivity.OPTABLE!!
})
```

Java:
#### Java

```java
import co.optable.android_sdk.OptableSDK;
Expand Down Expand Up @@ -225,7 +229,7 @@ On success, the resulting key values are typically sent as part of a subsequent

To send real-time event data from the user's device to the sandbox for eventual audience assembly, you can call the witness API as follows:

Kotlin:
#### Kotlin

```kotlin
import co.optable.android_sdk.OptableSDK
Expand All @@ -245,7 +249,7 @@ MainActivity.OPTABLE!!
})
```

Java:
#### Java

```java
import co.optable.android_sdk.OptableSDK;
Expand Down Expand Up @@ -275,7 +279,7 @@ The specified event type and properties are associated with the logged event and

We can further extend the above `targeting` example to show an integration with a [Google Ad Manager 360](https://admanager.google.com/home/) ad server account:

Kotlin:
#### Kotlin

```kotlin
import co.optable.android_sdk.OptableSDK
Expand Down Expand Up @@ -311,7 +315,7 @@ MainActivity.OPTABLE!!
})
```

Java:
#### Java

```java
import co.optable.android_sdk.OptableSDK;
Expand Down Expand Up @@ -347,6 +351,62 @@ MainActivity.OPTABLE.targeting().observe(getViewLifecycleOwner(), result -> {

Working examples are available in the Kotlin and Java SDK demo applications.

## Identifying visitors arriving from Email newsletters

If you send Email newsletters that contain links to your application (e.g., deep links), then you may want to automatically _identify_ visitors that have clicked on any such links via their Email address. Incoming application traffic which is originating from a subscriber click on a link in a newsletter is considered to be implicitly authenticated by the recipient of the Email, therefore serving as an excellent source of linking of online user identities.

### Insert oeid into your Email newsletter template

To enable automatic identification of visitors originating from your Email newsletter, you first need to include an **oeid** parameter in the query string of all links to your website in your Email newsletter template. The value of the **oeid** parameter should be set to the SHA256 hash of the lowercased Email address of the recipient. For example, if you are using [Braze](https://www.braze.com/) to send your newsletters, you can easily encode the SHA256 hash value of the recipient's Email address by setting the **oeid** parameter in the query string of any links to your application as follows:

```
oeid={{${email_address} | downcase | sha2}}
```

The above example uses various personalization tags as documented in [Braze's user guide](https://www.braze.com/docs/user_guide/personalization_and_dynamic_content/) to dynamically insert the required data into an **oeid** parameter, all of which should make up a _part_ of the destination URL in your template.

### Capture clicks on deep links in your application

In order for your application to open on devices where it is installed when a link to your domain is clicked, you need to [configure and prepare your application to handle deep links](https://developer.android.com/training/app-links/deep-linking) first.

### Call tryIdentifyFromURI SDK API

When Android launches your app after a user clicks on a link, it will start your app activity with your configured _intent filters_. You can then obtain the `Uri` of the link by calling `getData()`, and pass it to the SDK's `tryIdentifyFromURI()` API which will automatically look for `oeid` in the query parameters of the `Uri` and call `identify` with its value if found.

For example, you can call `getData()` on the incoming `Intent` from your `onCreate()` activity lifecycle callback as follows:

#### Kotlin

```kotlin
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main)
...
val data: Uri? = intent?.data
if (data != null) {
MainActivity.OPTABLE!!.tryIdentifyFromURI(data)
}
...
}
```

#### Java

```java
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
...
Intent intent = getIntent();
Uri data = intent.getData();
if (data != null) {
MainActivity.OPTABLE.tryIdentifyFromURI(data);
}
...
}
```

## Demo Applications

The Kotlin and Java demo applications show a working example of `identify`, `targeting`, and `witness` APIs, as well as an integration with the [Google Ad Manager 360](https://admanager.google.com/home/) ad server, enabling the targeting of ads served by GAM360 to audiences activated in the [Optable](https://optable.co/) sandbox.
Expand Down
7 changes: 7 additions & 0 deletions android_sdk/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ plugins {
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'de.mobilej.unmock'

unMock {
keep "android.net.Uri"
keepStartingWith "libcore."
keepAndRename "java.nio.charset.Charsets" to "xjava.nio.charset.Charsets"
}

android {
compileSdkVersion 30
Expand Down
42 changes: 42 additions & 0 deletions android_sdk/src/main/java/co/optable/android_sdk/OptableSDK.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package co.optable.android_sdk

import android.content.Context
import android.net.Uri
import android.text.TextUtils
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
Expand Down Expand Up @@ -163,6 +164,23 @@ class OptableSDK @JvmOverloads constructor(context: Context, host: String, app:
return this.identify(idList)
}

/*
* tryIdentifyFromURI(uri) is a helper that attempts to find a valid-looking "oeid"
* parameter in the specified uri's query string parameters and, if found, calls
* this.identify(listOf(oeid)).
*
* The use for this is when handling incoming app universal/deep links which might
* contain an "oeid" value with the SHA256(downcase(email)) of an incoming user, such
* as encoded links in newsletter Emails sent by the application developer.
*/
fun tryIdentifyFromURI(uri: Uri) {
val oeid = Companion.eidFromURI(uri)

if (oeid.length > 0) {
this.identify(listOf(oeid))
}
}

/*
* targeting() calls the Optable Sandbox "targeting" API, which returns the key-value targeting
* data matching the user/device/app.
Expand Down Expand Up @@ -260,5 +278,29 @@ class OptableSDK @JvmOverloads constructor(context: Context, host: String, app:
fun cid(ppid: String): String {
return "c:" + ppid.trim()
}

/*
* eidFromURI(uri) is a helper that returns a type-prefixed ID based on the query string
* oeid=sha256value parameters in the specified uri, if one is found. Otherwise, it returns
* an empty string.
*
* The use for this is when handling incoming deep links which might contain an "oeid" value
* with the SHA256(downcase(email)) of a user, such as encoded links in newsletter Emails
* sent by the application developer. Such hashed Email values can be used in calls to
* identify()
*/
fun eidFromURI(uri: Uri): String {
// We first convert the Uri to a lowercase string then re-parse it so that we are
// not dependent on case-sensitivity of the "oeid" query parameter:
var oeid = Uri.parse(uri.toString().toLowerCase()).getQueryParameter("oeid")

if ((oeid == null) || (oeid.length != 64) ||
(oeid.matches("^[a-f0-9]$".toRegex(RegexOption.IGNORE_CASE))))
{
return ""
}

return "e:" + oeid.toLowerCase()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/
package co.optable.android_sdk

import android.net.Uri
import org.junit.Test
import org.junit.Assert.*

Expand Down Expand Up @@ -61,4 +62,36 @@ class OptableSDKUnitTest {

assertNotEquals(unexpected, OptableSDK.cid("foobarBAZ-01234#98765.!!!"))
}

@Test
fun eidFromURI_isCorrect() {
val url = "http://some.domain.com/some/path?some=query&something=else&oeid=a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3&foo=bar&baz"
val expected = "e:a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3"

assertEquals(expected, OptableSDK.eidFromURI(Uri.parse(url)))
}

@Test
fun eidFromURI_returnsEmptyWhenOeidAbsent() {
val url = "http://some.domain.com/some/path?some=query&something=else"
val expected = ""

assertEquals(expected, OptableSDK.eidFromURI(Uri.parse(url)))
}

@Test
fun eidFromURI_expectsSHA256() {
val url = "http://some.domain.com/some/path?some=query&something=else&oeid=AAAAAAAa665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3&foo=bar&baz"
val expected = ""

assertEquals(expected, OptableSDK.eidFromURI(Uri.parse(url)))
}

@Test
fun eidFromURI_ignoresCase() {
val url = "http://some.domain.com/some/path?some=query&something=else&oEId=A665A45920422F9D417E4867EFDC4FB8A04A1F3FFF1FA07E998E86f7f7A27AE3&foo=bar&baz"
val expected = "e:a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3"

assertEquals(expected, OptableSDK.eidFromURI(Uri.parse(url)))
}
}
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ buildscript {
dependencies {
classpath "com.android.tools.build:gradle:4.0.1"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "de.mobilej.unmock:UnMockPlugin:0.7.6"

// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
Expand Down

0 comments on commit c999319

Please sign in to comment.