Skip to content

Commit 43e28a9

Browse files
authored
Wenxi/intercom destination (#40)
* add intercom dependencies * add intercom destination * fix initialization issue * add logs * bug fix * bug fix * add usage to application * bump up sample destination project minSdkVersion to 21 * add unit tests * bug fix * add comments and license * address comments * address comments again
1 parent 34d797b commit 43e28a9

File tree

4 files changed

+546
-5
lines changed

4 files changed

+546
-5
lines changed

samples/kotlin-android-app-destinations/build.gradle

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ android {
1111
defaultConfig {
1212
multiDexEnabled true
1313
applicationId "com.segment.analytics.destinations"
14-
minSdkVersion 16
14+
minSdkVersion 21
1515
targetSdkVersion 30
1616
versionCode 3
1717
versionName "2.0"
@@ -34,6 +34,11 @@ android {
3434
kotlinOptions {
3535
jvmTarget = '1.8'
3636
}
37+
testOptions {
38+
unitTests.all {
39+
useJUnitPlatform()
40+
}
41+
}
3742
}
3843

3944
dependencies {
@@ -47,6 +52,10 @@ dependencies {
4752
// When using the BoM, you don't specify versions in Firebase library dependencies
4853
implementation 'com.google.firebase:firebase-analytics-ktx'
4954

55+
// Intercom
56+
implementation 'io.intercom.android:intercom-sdk-base:10.1.1'
57+
implementation 'io.intercom.android:intercom-sdk-fcm:10.1.1'
58+
5059
implementation project(':android')
5160

5261
implementation 'androidx.multidex:multidex:2.0.1'
@@ -63,7 +72,11 @@ dependencies {
6372
implementation 'androidx.lifecycle:lifecycle-process:2.3.1'
6473
implementation 'androidx.lifecycle:lifecycle-common-java8:2.3.1'
6574

75+
testImplementation 'io.mockk:mockk:1.10.6'
6676
testImplementation 'junit:junit:4.+'
77+
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.2'
78+
testImplementation platform("org.junit:junit-bom:5.7.2")
79+
testImplementation "org.junit.jupiter:junit-jupiter"
6780
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
6881
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
6982
}

samples/kotlin-android-app-destinations/src/main/java/com/segment/analytics/destinations/MainApplication.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
package com.segment.analytics.destinations
22

33
import android.app.Application
4-
import com.segment.analytics.destinations.plugins.AmplitudeSession
5-
import com.segment.analytics.destinations.plugins.FirebaseDestination
6-
import com.segment.analytics.destinations.plugins.MixpanelDestination
7-
import com.segment.analytics.destinations.plugins.WebhookPlugin
4+
import com.segment.analytics.destinations.plugins.*
85
import com.segment.analytics.kotlin.android.Analytics
96
import com.segment.analytics.kotlin.core.Analytics
107
import java.util.concurrent.Executors
@@ -40,5 +37,8 @@ class MainApplication : Application() {
4037

4138
// Try out Firebase Destination
4239
analytics.add(FirebaseDestination(applicationContext))
40+
41+
// Try out Intercom destination
42+
analytics.add(IntercomDestination(this))
4343
}
4444
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
package com.segment.analytics.destinations.plugins
2+
3+
import android.app.Application
4+
import com.segment.analytics.kotlin.android.plugins.AndroidLifecycle
5+
import com.segment.analytics.kotlin.core.*
6+
import com.segment.analytics.kotlin.core.platform.DestinationPlugin
7+
import com.segment.analytics.kotlin.core.platform.Plugin
8+
import com.segment.analytics.kotlin.core.platform.plugins.log
9+
import com.segment.analytics.kotlin.core.utilities.*
10+
import io.intercom.android.sdk.Company
11+
import io.intercom.android.sdk.Intercom
12+
import io.intercom.android.sdk.UserAttributes
13+
import io.intercom.android.sdk.identity.Registration
14+
import kotlinx.serialization.json.*
15+
16+
/*
17+
This is an example of the Intercom device-mode destination plugin that can be integrated with
18+
Segment analytics.
19+
Note: This plugin is NOT SUPPORTED by Segment. It is here merely as an example,
20+
and for your convenience should you find it useful.
21+
# Instructions for adding Intercom:
22+
- In your app-module build.gradle file add the following:
23+
```
24+
...
25+
dependencies {
26+
...
27+
// Intercom
28+
implementation 'io.intercom.android:intercom-sdk-base:10.1.1'
29+
implementation 'io.intercom.android:intercom-sdk-fcm:10.1.1'
30+
}
31+
```
32+
- Copy this entire IntercomDestination.kt file into your project's codebase.
33+
- Go to your project's codebase and wherever u initialize the analytics client add these lines
34+
```
35+
val intercom = IntercomDestination()
36+
analytics.add(intercom)
37+
```
38+
39+
Note: due to the inclusion of Intercom partner integration your minSdk cannot be smaller than 21
40+
41+
MIT License
42+
Copyright (c) 2021 Segment
43+
Permission is hereby granted, free of charge, to any person obtaining a copy
44+
of this software and associated documentation files (the "Software"), to deal
45+
in the Software without restriction, including without limitation the rights
46+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
47+
copies of the Software, and to permit persons to whom the Software is
48+
furnished to do so, subject to the following conditions:
49+
The above copyright notice and this permission notice shall be included in all
50+
copies or substantial portions of the Software.
51+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
52+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
53+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
54+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
55+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
56+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
57+
SOFTWARE.
58+
*/
59+
60+
class IntercomDestination(
61+
private val application: Application
62+
): DestinationPlugin(), AndroidLifecycle {
63+
64+
override val key: String = "Intercom"
65+
private var mobileApiKey: String = ""
66+
private var appId: String = ""
67+
lateinit var intercom: Intercom
68+
private set
69+
70+
override fun update(settings: Settings, type: Plugin.UpdateType) {
71+
// if we've already set up this singleton SDK, can't do it again, so skip.
72+
if (type != Plugin.UpdateType.Initial) return
73+
super.update(settings, type)
74+
75+
settings.integrations[key]?.jsonObject?.let {
76+
mobileApiKey = it.getString("mobileApiKey") ?: ""
77+
appId = it.getString("appId") ?: ""
78+
}
79+
80+
Intercom.initialize(application, mobileApiKey, appId)
81+
this.intercom = Intercom.client()
82+
}
83+
84+
override fun track(payload: TrackEvent): BaseEvent? {
85+
val result = super.track(payload)
86+
87+
val eventName = payload.event
88+
val properties = payload.properties
89+
90+
if (!properties.isNullOrEmpty()) {
91+
val price = buildJsonObject{
92+
val amount = properties.getDouble(REVENUE) ?: properties.getDouble(TOTAL)
93+
amount?.let {
94+
put(AMOUNT, it * 100)
95+
}
96+
97+
properties.getString(CURRENCY)?.let {
98+
put(CURRENCY, it)
99+
}
100+
}
101+
102+
val eventProperties = buildJsonObject {
103+
if (!price.isNullOrEmpty()) {
104+
put(PRICE, price)
105+
}
106+
107+
properties.forEach { (key, value) ->
108+
// here we are only interested in primitive values and not maps or collections
109+
if (key !in setOf("products", REVENUE, TOTAL, CURRENCY)
110+
&& value is JsonPrimitive) {
111+
put(key, value)
112+
}
113+
}
114+
}
115+
116+
intercom.logEvent(eventName, eventProperties)
117+
analytics.log("Intercom.client().logEvent($eventName, $eventProperties)")
118+
}
119+
else {
120+
intercom.logEvent(eventName)
121+
analytics.log("Intercom.client().logEvent($eventName)")
122+
}
123+
124+
return result
125+
}
126+
127+
override fun identify(payload: IdentifyEvent): BaseEvent? {
128+
val result = super.identify(payload)
129+
130+
val userId = payload.userId
131+
if (userId.isEmpty()) {
132+
intercom.registerUnidentifiedUser()
133+
analytics.log("Intercom.client().registerUnidentifiedUser()")
134+
}
135+
else {
136+
val registration = Registration.create().withUserId(userId)
137+
intercom.registerIdentifiedUser(registration)
138+
analytics.log("Intercom.client().registerIdentifiedUser(registration)")
139+
}
140+
141+
val intercomOptions = payload.integrations["Intercom"]?.safeJsonObject
142+
intercomOptions?.getString("userHash")?.let {
143+
intercom.setUserHash(it)
144+
}
145+
146+
if (!payload.traits.isNullOrEmpty() && !intercomOptions.isNullOrEmpty()) {
147+
setUserAttributes(payload.traits, intercomOptions)
148+
}
149+
else {
150+
setUserAttributes(payload.traits, null)
151+
}
152+
153+
return result
154+
}
155+
156+
override fun group(payload: GroupEvent): BaseEvent? {
157+
val result = super.group(payload)
158+
159+
if (payload.groupId.isNotEmpty()) {
160+
val traits = buildJsonObject {
161+
putAll(payload.traits)
162+
put("id", payload.groupId)
163+
}
164+
val company = setCompany(traits)
165+
val userAttributes = UserAttributes.Builder()
166+
.withCompany(company)
167+
.build()
168+
intercom.updateUser(userAttributes)
169+
}
170+
171+
return result
172+
}
173+
174+
override fun reset() {
175+
super.reset()
176+
intercom.logout()
177+
analytics.log("Intercom.client().reset()")
178+
}
179+
180+
private fun setUserAttributes(traits: Traits, intercomOptions: JsonObject?) {
181+
val builder = UserAttributes.Builder()
182+
183+
traits.getString(NAME)?.let { builder.withName(it) }
184+
traits.getString(EMAIL)?.let { builder.withEmail(it) }
185+
traits.getString(PHONE)?.let { builder.withPhone(it) }
186+
187+
intercomOptions?.let {
188+
builder.withLanguageOverride(it.getString(LANGUAGE_OVERRIDE))
189+
builder.withSignedUpAt(it.getLong(CREATED_AT))
190+
builder.withUnsubscribedFromEmails(it.getBoolean(UNSUBSCRIBED_FROM_EMAILS))
191+
}
192+
193+
traits[COMPANY]?.safeJsonObject?.let {
194+
val company = setCompany(it)
195+
builder.withCompany(company)
196+
}
197+
198+
traits.forEach { (key, value) ->
199+
// here we are only interested in primitive values and not maps or collections
200+
if (value is JsonPrimitive &&
201+
key !in setOf(NAME, EMAIL, PHONE, "userId", "anonymousId")) {
202+
builder.withCustomAttribute(key, value.toContent())
203+
}
204+
}
205+
206+
intercom.updateUser(builder.build())
207+
analytics.log("Intercom.client().updateUser(userAttributes)")
208+
}
209+
210+
private fun setCompany(company: JsonObject): Company {
211+
val builder = Company.Builder()
212+
company.getString("id")?.let {
213+
builder.withCompanyId(it)
214+
} ?: return builder.build()
215+
216+
company.getString(NAME)?.let { builder.withName(it) }
217+
company.getLong(CREATED_AT)?.let { builder.withCreatedAt(it) }
218+
company.getInt(MONTHLY_SPEND)?.let { builder.withMonthlySpend(it) }
219+
company.getString(PLAN)?.let { builder.withPlan(it) }
220+
221+
company.forEach { (key, value) ->
222+
// here we are only interested in primitive values and not maps or collections
223+
if (value is JsonPrimitive &&
224+
key !in setOf("id", NAME, CREATED_AT, MONTHLY_SPEND, PLAN)
225+
) {
226+
builder.withCustomAttribute(key, value.toContent())
227+
}
228+
}
229+
230+
return builder.build()
231+
}
232+
233+
companion object {
234+
235+
// Intercom common specced attributes
236+
private const val NAME = "name"
237+
private const val CREATED_AT = "createdAt"
238+
private const val COMPANY = "company"
239+
private const val PRICE = "price"
240+
private const val AMOUNT = "amount"
241+
private const val CURRENCY = "currency"
242+
243+
// Intercom specced user attributes
244+
private const val EMAIL = "email"
245+
private const val PHONE = "phone"
246+
private const val LANGUAGE_OVERRIDE = "languageOverride"
247+
private const val UNSUBSCRIBED_FROM_EMAILS = "unsubscribedFromEmails"
248+
249+
// Intercom specced group attributes
250+
private const val MONTHLY_SPEND = "monthlySpend"
251+
private const val PLAN = "plan"
252+
253+
// Segment specced properties
254+
private const val REVENUE = "revenue"
255+
private const val TOTAL = "total"
256+
}
257+
}

0 commit comments

Comments
 (0)