Skip to content

Commit 7ec5327

Browse files
committed
Support for auto-identify on newsletter traffic
- Introduce Companion.eidFromURI() and tryIdentifyFromURI() which are helpers looking for "oeid" in the query parameters of incoming intent data URIs and, when found, calling identify() with them automatically - Update README docs
1 parent a339f4c commit 7ec5327

7 files changed

Lines changed: 155 additions & 11 deletions

File tree

.idea/gradle.xml

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/misc.xml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ Kotlin SDK for integrating with optable-sandbox from an Android application.
1010
- [Targeting API](#targeting-api)
1111
- [Witness API](#witness-api)
1212
- [Integrating GAM360](#integrating-gam360)
13+
- [Identifying visitors arriving from Email newsletters](#identifying-visitors-arriving-from-email-newsletters)
14+
- [Insert oeid into your Email newsletter template](#insert-oeid-into-your-email-newsletter-template)
15+
- [Capture clicks on deep links in your application](#capture-clicks-on-deep-links-in-your-application)
16+
- [Call tryIdentifyFromURI SDK API](#call-tryidentifyfromuri-sdk-api)
1317
- [Demo Applications](#demo-applications)
1418
- [Building](#building)
1519

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

5256
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`:
5357

54-
Kotlin:
58+
#### Kotlin
5559

5660
```kotlin
5761
import co.optable.android_sdk.OptableSDK
@@ -72,7 +76,7 @@ class MainActivity : AppCompatActivity() {
7276
}
7377
```
7478

75-
Java:
79+
#### Java
7680

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

108112
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:
109113

110-
Kotlin:
114+
#### Kotlin
111115

112116
```kotlin
113117
import co.optable.android_sdk.OptableSDK
@@ -133,7 +137,7 @@ MainActivity.OPTABLE!!
133137
})
134138
```
135139

136-
Java:
140+
#### Java
137141

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

171175
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:
172176

173-
Kotlin:
177+
#### Kotlin
174178

175179
```kotlin
176180
import co.optable.android_sdk.OptableSDK
@@ -195,7 +199,7 @@ MainActivity.OPTABLE!!
195199
})
196200
```
197201

198-
Java:
202+
#### Java
199203

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

226230
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:
227231

228-
Kotlin:
232+
#### Kotlin
229233

230234
```kotlin
231235
import co.optable.android_sdk.OptableSDK
@@ -245,7 +249,7 @@ MainActivity.OPTABLE!!
245249
})
246250
```
247251

248-
Java:
252+
#### Java
249253

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

276280
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:
277281

278-
Kotlin:
282+
#### Kotlin
279283

280284
```kotlin
281285
import co.optable.android_sdk.OptableSDK
@@ -311,7 +315,7 @@ MainActivity.OPTABLE!!
311315
})
312316
```
313317

314-
Java:
318+
#### Java
315319

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

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

354+
## Identifying visitors arriving from Email newsletters
355+
356+
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.
357+
358+
### Insert oeid into your Email newsletter template
359+
360+
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:
361+
362+
```
363+
oeid={{${email_address} | downcase | sha2}}
364+
```
365+
366+
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.
367+
368+
### Capture clicks on deep links in your application
369+
370+
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.
371+
372+
### Call tryIdentifyFromURI SDK API
373+
374+
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.
375+
376+
For example, you can call `getData()` on the incoming `Intent` from your `onCreate()` activity lifecycle callback as follows:
377+
378+
#### Kotlin
379+
380+
```kotlin
381+
override fun onCreate(savedInstanceState: Bundle?) {
382+
super.onCreate(savedInstanceState)
383+
setContentView(R.layout.main)
384+
...
385+
val data: Uri? = intent?.data
386+
if (data != null) {
387+
MainActivity.OPTABLE!!.tryIdentifyFromURI(data)
388+
}
389+
...
390+
}
391+
```
392+
393+
#### Java
394+
395+
```java
396+
@Override
397+
public void onCreate(Bundle savedInstanceState) {
398+
super.onCreate(savedInstanceState);
399+
setContentView(R.layout.main);
400+
...
401+
Intent intent = getIntent();
402+
Uri data = intent.getData();
403+
if (data != null) {
404+
MainActivity.OPTABLE.tryIdentifyFromURI(data);
405+
}
406+
...
407+
}
408+
```
409+
350410
## Demo Applications
351411

352412
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.

android_sdk/build.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ plugins {
55
apply plugin: 'com.android.library'
66
apply plugin: 'kotlin-android'
77
apply plugin: 'kotlin-android-extensions'
8+
apply plugin: 'de.mobilej.unmock'
9+
10+
unMock {
11+
keep "android.net.Uri"
12+
keepStartingWith "libcore."
13+
keepAndRename "java.nio.charset.Charsets" to "xjava.nio.charset.Charsets"
14+
}
815

916
android {
1017
compileSdkVersion 30

android_sdk/src/main/java/co/optable/android_sdk/OptableSDK.kt

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package co.optable.android_sdk
66

77
import android.content.Context
8+
import android.net.Uri
89
import android.text.TextUtils
910
import androidx.lifecycle.LiveData
1011
import androidx.lifecycle.MutableLiveData
@@ -163,6 +164,23 @@ class OptableSDK @JvmOverloads constructor(context: Context, host: String, app:
163164
return this.identify(idList)
164165
}
165166

167+
/*
168+
* tryIdentifyFromURI(uri) is a helper that attempts to find a valid-looking "oeid"
169+
* parameter in the specified uri's query string parameters and, if found, calls
170+
* this.identify(listOf(oeid)).
171+
*
172+
* The use for this is when handling incoming app universal/deep links which might
173+
* contain an "oeid" value with the SHA256(downcase(email)) of an incoming user, such
174+
* as encoded links in newsletter Emails sent by the application developer.
175+
*/
176+
fun tryIdentifyFromURI(uri: Uri) {
177+
val oeid = Companion.eidFromURI(uri)
178+
179+
if (oeid.length > 0) {
180+
this.identify(listOf(oeid))
181+
}
182+
}
183+
166184
/*
167185
* targeting() calls the Optable Sandbox "targeting" API, which returns the key-value targeting
168186
* data matching the user/device/app.
@@ -260,5 +278,29 @@ class OptableSDK @JvmOverloads constructor(context: Context, host: String, app:
260278
fun cid(ppid: String): String {
261279
return "c:" + ppid.trim()
262280
}
281+
282+
/*
283+
* eidFromURI(uri) is a helper that returns a type-prefixed ID based on the query string
284+
* oeid=sha256value parameters in the specified uri, if one is found. Otherwise, it returns
285+
* an empty string.
286+
*
287+
* The use for this is when handling incoming deep links which might contain an "oeid" value
288+
* with the SHA256(downcase(email)) of a user, such as encoded links in newsletter Emails
289+
* sent by the application developer. Such hashed Email values can be used in calls to
290+
* identify()
291+
*/
292+
fun eidFromURI(uri: Uri): String {
293+
// We first convert the Uri to a lowercase string then re-parse it so that we are
294+
// not dependent on case-sensitivity of the "oeid" query parameter:
295+
var oeid = Uri.parse(uri.toString().toLowerCase()).getQueryParameter("oeid")
296+
297+
if ((oeid == null) || (oeid.length != 64) ||
298+
(oeid.matches("^[a-f0-9]$".toRegex(RegexOption.IGNORE_CASE))))
299+
{
300+
return ""
301+
}
302+
303+
return "e:" + oeid.toLowerCase()
304+
}
263305
}
264306
}

android_sdk/src/test/java/co/optable/android_sdk/OptableSDKUnitTest.kt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55
package co.optable.android_sdk
66

7+
import android.net.Uri
78
import org.junit.Test
89
import org.junit.Assert.*
910

@@ -61,4 +62,36 @@ class OptableSDKUnitTest {
6162

6263
assertNotEquals(unexpected, OptableSDK.cid("foobarBAZ-01234#98765.!!!"))
6364
}
65+
66+
@Test
67+
fun eidFromURI_isCorrect() {
68+
val url = "http://some.domain.com/some/path?some=query&something=else&oeid=a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3&foo=bar&baz"
69+
val expected = "e:a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3"
70+
71+
assertEquals(expected, OptableSDK.eidFromURI(Uri.parse(url)))
72+
}
73+
74+
@Test
75+
fun eidFromURI_returnsEmptyWhenOeidAbsent() {
76+
val url = "http://some.domain.com/some/path?some=query&something=else"
77+
val expected = ""
78+
79+
assertEquals(expected, OptableSDK.eidFromURI(Uri.parse(url)))
80+
}
81+
82+
@Test
83+
fun eidFromURI_expectsSHA256() {
84+
val url = "http://some.domain.com/some/path?some=query&something=else&oeid=AAAAAAAa665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3&foo=bar&baz"
85+
val expected = ""
86+
87+
assertEquals(expected, OptableSDK.eidFromURI(Uri.parse(url)))
88+
}
89+
90+
@Test
91+
fun eidFromURI_ignoresCase() {
92+
val url = "http://some.domain.com/some/path?some=query&something=else&oEId=A665A45920422F9D417E4867EFDC4FB8A04A1F3FFF1FA07E998E86f7f7A27AE3&foo=bar&baz"
93+
val expected = "e:a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3"
94+
95+
assertEquals(expected, OptableSDK.eidFromURI(Uri.parse(url)))
96+
}
6497
}

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ buildscript {
88
dependencies {
99
classpath "com.android.tools.build:gradle:4.0.1"
1010
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
11+
classpath "de.mobilej.unmock:UnMockPlugin:0.7.6"
1112

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

0 commit comments

Comments
 (0)