-
Notifications
You must be signed in to change notification settings - Fork 108
/
PinpointAnalyticsInstrumentationTest.kt
442 lines (419 loc) · 19.8 KB
/
PinpointAnalyticsInstrumentationTest.kt
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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
/*
* Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package com.amplifyframework.analytics.pinpoint
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import android.util.Pair
import androidx.annotation.RawRes
import androidx.test.core.app.ApplicationProvider
import aws.sdk.kotlin.services.pinpoint.PinpointClient
import aws.sdk.kotlin.services.pinpoint.model.EndpointLocation
import aws.sdk.kotlin.services.pinpoint.model.EndpointResponse
import aws.sdk.kotlin.services.pinpoint.model.GetEndpointRequest
import com.amplifyframework.analytics.AnalyticsEvent
import com.amplifyframework.analytics.AnalyticsProperties
import com.amplifyframework.analytics.UserProfile
import com.amplifyframework.analytics.pinpoint.models.AWSPinpointUserProfile
import com.amplifyframework.analytics.pinpoint.targeting.endpointProfile.EndpointProfile
import com.amplifyframework.auth.AuthPlugin
import com.amplifyframework.auth.cognito.AWSCognitoAuthPlugin
import com.amplifyframework.core.Amplify
import com.amplifyframework.hub.HubChannel
import com.amplifyframework.hub.HubEvent
import com.amplifyframework.logging.AndroidLoggingPlugin
import com.amplifyframework.logging.LogLevel
import com.amplifyframework.testutils.HubAccumulator
import com.amplifyframework.testutils.Resources
import com.amplifyframework.testutils.Sleep
import com.amplifyframework.testutils.sync.SynchronousAuth
import java.util.UUID
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.runBlocking
import org.json.JSONException
import org.junit.Assert
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
class PinpointAnalyticsInstrumentationTest {
@Before
fun flushEvents() {
val context = ApplicationProvider.getApplicationContext<Context>()
@RawRes val resourceId = Resources.getRawResourceId(context, CREDENTIALS_RESOURCE_NAME)
val userAndPasswordPair = readCredentialsFromResource(context, resourceId)
synchronousAuth.signOut()
synchronousAuth.signIn(
userAndPasswordPair!!.first,
userAndPasswordPair.second
)
val hubAccumulator =
HubAccumulator.create(HubChannel.ANALYTICS, AnalyticsChannelEventName.FLUSH_EVENTS, 1).start()
Amplify.Analytics.flushEvents()
val events = hubAccumulator.await(10, TimeUnit.SECONDS)
pinpointClient = Amplify.Analytics.getPlugin("awsPinpointAnalyticsPlugin").escapeHatch as
PinpointClient
uniqueId = preferences.getString(UNIQUE_ID_KEY, "error-no-unique-id")!!
Assert.assertNotEquals(uniqueId, "error-no-unique-id")
}
/**
* Record a basic analytics event and verify that it has been recorded using Analytics
* pinpoint client.
*/
@Test
fun recordEventStoresPassedBasicAnalyticsEvent() {
val hubAccumulator =
HubAccumulator.create(HubChannel.ANALYTICS, AnalyticsChannelEventName.FLUSH_EVENTS, 2).start()
// Arrange: Create an event
val eventName = "Amplify-event" + UUID.randomUUID().toString()
val event = AnalyticsEvent.builder()
.name(eventName)
.addProperty("AnalyticsStringProperty", "Pancakes")
.addProperty("AnalyticsBooleanProperty", true)
.addProperty("AnalyticsDoubleProperty", 3.14)
.addProperty("AnalyticsIntegerProperty", 42)
.build()
// Act: Record the event
Amplify.Analytics.recordEvent(event)
Sleep.milliseconds(DEFAULT_TIMEOUT)
Amplify.Analytics.flushEvents()
val hubEvents = hubAccumulator.await(10, TimeUnit.SECONDS)
val submittedEvents = combineAndFilterEvents(hubEvents)
Assert.assertEquals(1, submittedEvents.size.toLong())
val submittedEvent = submittedEvents[0]
Assert.assertEquals(submittedEvent.name, eventName)
Assert.assertEquals("Pancakes", submittedEvent.properties["AnalyticsStringProperty"].value)
Assert.assertEquals("true", submittedEvent.properties["AnalyticsBooleanProperty"].value)
Assert.assertEquals(3.14, submittedEvent.properties["AnalyticsDoubleProperty"].value)
Assert.assertEquals(42.0, submittedEvent.properties["AnalyticsIntegerProperty"].value)
}
/**
* Record a basic analytics event and test that events are flushed from local database by calling submitEvents.
*/
@Test
fun testFlushEventHubEvent() {
val analyticsHubEventAccumulator =
HubAccumulator.create(HubChannel.ANALYTICS, AnalyticsChannelEventName.FLUSH_EVENTS, 3)
.start()
val eventName1 = "Amplify-event" + UUID.randomUUID().toString()
val event1 = AnalyticsEvent.builder()
.name(eventName1)
.addProperty("DemoProperty1", "DemoValue1")
.addProperty("DemoDoubleProperty2", 2.0)
.build()
Amplify.Analytics.recordEvent(event1)
Sleep.milliseconds(DEFAULT_TIMEOUT)
val eventName2 = "Amplify-event" + UUID.randomUUID().toString()
val event2 = AnalyticsEvent.builder()
.name(eventName2)
.addProperty("DemoProperty1", "DemoValue1")
.addProperty("DemoProperty2", 2.0)
.build()
Amplify.Analytics.recordEvent(event2)
Sleep.milliseconds(DEFAULT_TIMEOUT)
Amplify.Analytics.flushEvents()
val hubEvents = analyticsHubEventAccumulator.await(10, TimeUnit.SECONDS)
val hubEvent = analyticsHubEventAccumulator.awaitFirst()
val submittedEvents = combineAndFilterEvents(hubEvents)
Assert.assertEquals(2, submittedEvents.size.toLong())
Assert.assertEquals(AnalyticsChannelEventName.FLUSH_EVENTS.eventName, hubEvent.name)
Assert.assertEquals(eventName1, submittedEvents[0].name)
Assert.assertEquals(eventName2, submittedEvents[1].name)
}
/**
* Record a basic analytics event and test that events are flushed from local database periodically.
*/
@Test
fun testAutoFlush() {
val analyticsHubEventAccumulator1 =
HubAccumulator.create(HubChannel.ANALYTICS, AnalyticsChannelEventName.FLUSH_EVENTS, 1)
.start()
val event1 = AnalyticsEvent.builder()
.name("Amplify-event" + UUID.randomUUID().toString())
.addProperty("DemoProperty1", "DemoValue1")
.addProperty("DemoDoubleProperty2", 2.0)
.build()
Amplify.Analytics.recordEvent(event1)
// Autosubmitter runs every 10 secs, so waiting for 15sec to receive the hub event
val hubEvents1 = analyticsHubEventAccumulator1.await(EVENT_FLUSH_TIMEOUT_WAIT, TimeUnit.SECONDS)
Assert.assertEquals(1, hubEvents1.size.toLong())
val analyticsHubEventAccumulator2 =
HubAccumulator.create(HubChannel.ANALYTICS, AnalyticsChannelEventName.FLUSH_EVENTS, 1)
.start()
val event2 = AnalyticsEvent.builder()
.name("Amplify-event" + UUID.randomUUID().toString())
.addProperty("DemoProperty1", "DemoValue1")
.addProperty("DemoProperty2", 2.0)
.build()
Amplify.Analytics.recordEvent(event2)
// Autosubmitter runs every 10 secs, so waiting for 15sec to receive the hub event
val hubEvents2 = analyticsHubEventAccumulator1.await(EVENT_FLUSH_TIMEOUT_WAIT, TimeUnit.SECONDS)
Assert.assertEquals(1, hubEvents2.size.toLong())
}
/**
* Registers a global property and ensures that all recorded events have the global property.
*/
@Test
fun registerGlobalPropertiesAddsGivenPropertiesToRecordedEvents() {
val analyticsHubEventAccumulator =
HubAccumulator.create(HubChannel.ANALYTICS, AnalyticsChannelEventName.FLUSH_EVENTS, 3)
.start()
// Arrange: Register global properties and create an event
Amplify.Analytics.registerGlobalProperties(
AnalyticsProperties.builder()
.add("GlobalProperty", "GlobalValue")
.build()
)
val event = AnalyticsEvent.builder()
.name("Amplify-event" + UUID.randomUUID().toString())
.addProperty("LocalProperty", "LocalValue")
.build()
// Act: Record two events: the one created above and another just with a key
Amplify.Analytics.recordEvent(event)
Sleep.milliseconds(DEFAULT_TIMEOUT)
Amplify.Analytics.recordEvent("amplify-test-event")
Sleep.milliseconds(DEFAULT_TIMEOUT)
Amplify.Analytics.flushEvents()
val hubEvents = analyticsHubEventAccumulator.await(10, TimeUnit.SECONDS)
val submittedEvents = combineAndFilterEvents(hubEvents)
Assert.assertEquals(2, submittedEvents.size.toLong())
val event1Attributes = submittedEvents[0].properties
val event2Attributes = submittedEvents[1].properties
// Global properties are attached to all events
Assert.assertEquals("GlobalValue", event1Attributes["GlobalProperty"].value)
Assert.assertEquals("GlobalValue", event2Attributes["GlobalProperty"].value)
// Local properties are only attached is passed explicitly as in event
Assert.assertEquals("LocalValue", event1Attributes["LocalProperty"].value)
}
/**
* Registers a global property and then Unregisters the global property
* and ensures that it is respected in events recorded thereafter.
*
*/
@Test
fun unregisterGlobalPropertiesRemovesGivenProperties() {
val analyticsHubEventAccumulator =
HubAccumulator.create(HubChannel.ANALYTICS, AnalyticsChannelEventName.FLUSH_EVENTS, 3)
.start()
// Arrange: Register a global property
Amplify.Analytics.registerGlobalProperties(
AnalyticsProperties.builder()
.add("GlobalProperty", "globalVal")
.build()
)
// Act: Record an event, unregister the global property, and record another event
Amplify.Analytics.recordEvent("amplify-test-event-with-property")
Sleep.milliseconds(DEFAULT_TIMEOUT)
Amplify.Analytics.unregisterGlobalProperties("GlobalProperty")
Amplify.Analytics.recordEvent("amplify-test-event-without-property")
Sleep.milliseconds(DEFAULT_TIMEOUT)
Amplify.Analytics.flushEvents()
val hubEvents = analyticsHubEventAccumulator.await(10, TimeUnit.SECONDS)
val submittedEvents = combineAndFilterEvents(hubEvents)
// Assert: Ensure there are two events, the first has attributes, and the second doesn't
Assert.assertEquals(2, submittedEvents.size.toLong())
Assert.assertEquals("globalVal", submittedEvents[0].properties["GlobalProperty"].value)
Assert.assertEquals(0, submittedEvents[1].properties.size().toLong())
}
/**
* The [AWSPinpointAnalyticsPlugin.identifyUser] method should set
* an [EndpointProfile] on the [PinpointClient], containing
* all provided Amplify attributes.
*/
@Test
fun testIdentifyUserWithDefaultProfile() {
val location = testLocation
val properties = endpointProperties
val userProfile = AWSPinpointUserProfile.builder()
.name("test-user")
.email("user@test.com")
.plan("test-plan")
.location(location)
.customProperties(properties)
.build()
Amplify.Analytics.identifyUser(UUID.randomUUID().toString(), userProfile)
Sleep.milliseconds(PINPOINT_ROUNDTRIP_TIMEOUT)
val endpointResponse = fetchEndpointResponse()
assertCommonEndpointResponseProperties(endpointResponse)
assert(null == endpointResponse.user!!.userAttributes)
}
/**
* [AWSPinpointUserProfile] extends [UserProfile] to include
* [AWSPinpointUserProfile.userAttributes] which is specific to Pinpoint. This test is very
* similar to testIdentifyUserWithDefaultProfile, but it adds user attributes in addition
* to the endpoint attributes.
*/
@Test
fun testIdentifyUserWithUserAttributes() {
val location = testLocation
val properties = endpointProperties
val userAttributes = userAttributes
val pinpointUserProfile = AWSPinpointUserProfile.builder()
.name("test-user")
.email("user@test.com")
.plan("test-plan")
.location(location)
.customProperties(properties)
.userAttributes(userAttributes)
.build()
val uuid = UUID.randomUUID().toString()
Amplify.Analytics.identifyUser(uuid, pinpointUserProfile)
Sleep.milliseconds(PINPOINT_ROUNDTRIP_TIMEOUT)
var endpointResponse = fetchEndpointResponse()
var retry_count = 0
while (retry_count < MAX_RETRIES && endpointResponse.attributes.isNullOrEmpty()) {
Sleep.milliseconds(DEFAULT_TIMEOUT)
endpointResponse = fetchEndpointResponse()
retry_count++
}
assertCommonEndpointResponseProperties(endpointResponse)
Assert.assertEquals(
"User attribute value",
endpointResponse.user!!
.userAttributes!!
["SomeUserAttribute"]!!
[0]
)
}
private fun fetchEndpointResponse(): EndpointResponse {
var endpointResponse: EndpointResponse? = null
runBlocking {
endpointResponse = pinpointClient.getEndpoint(
GetEndpointRequest.invoke {
this.applicationId = appId
this.endpointId = uniqueId
}
).endpointResponse
}
assert(null != endpointResponse)
return endpointResponse!!
}
private fun assertCommonEndpointResponseProperties(endpointResponse: EndpointResponse) {
Log.d("PinpointAnalyticsInstrumentationTest", endpointResponse.toString())
val attributes = endpointResponse.attributes!!
Assert.assertEquals("user@test.com", attributes["email"]!![0])
Assert.assertEquals("test-user", attributes["name"]!![0])
Assert.assertEquals("test-plan", attributes["plan"]!![0])
val endpointProfileLocation: EndpointLocation = endpointResponse.location!!
Assert.assertEquals(47.6154086, endpointProfileLocation.latitude!!, 0.1)
Assert.assertEquals((-122.3349685), endpointProfileLocation.longitude!!, 0.1)
Assert.assertEquals("98122", endpointProfileLocation.postalCode)
Assert.assertEquals("Seattle", endpointProfileLocation.city)
Assert.assertEquals("WA", endpointProfileLocation.region)
Assert.assertEquals("USA", endpointProfileLocation.country)
Assert.assertEquals("TestStringValue", attributes["TestStringProperty"]!![0])
Assert.assertEquals(1.0, endpointResponse.metrics!!["TestDoubleProperty"]!!, 0.1)
}
private val userAttributes: AnalyticsProperties
get() = AnalyticsProperties.builder()
.add("SomeUserAttribute", "User attribute value")
.build()
private val endpointProperties: AnalyticsProperties
get() {
return AnalyticsProperties.builder()
.add("TestStringProperty", "TestStringValue")
.add("TestDoubleProperty", 1.0)
.build()
}
private val testLocation: UserProfile.Location
get() {
return UserProfile.Location.builder()
.latitude(47.6154086)
.longitude(-122.3349685)
.postalCode("98122")
.city("Seattle")
.region("WA")
.country("USA")
.build()
}
private fun combineAndFilterEvents(hubEvents: List<HubEvent<*>>): MutableList<AnalyticsEvent> {
val result = mutableListOf<AnalyticsEvent>()
hubEvents.forEach {
if ((it.data as List<*>).size != 0) {
(it.data as ArrayList<*>).forEach { event ->
if (!(event as AnalyticsEvent).name.startsWith("_session")) {
result.add(event)
}
}
}
}
return result
}
companion object {
private const val EVENT_FLUSH_TIMEOUT_WAIT = 15 /* seconds */
private const val CREDENTIALS_RESOURCE_NAME = "credentials"
private const val CONFIGURATION_NAME = "amplifyconfiguration"
private const val COGNITO_CONFIGURATION_TIMEOUT = 10 * 1000L
private const val PINPOINT_ROUNDTRIP_TIMEOUT = 10 * 1000L
private const val DEFAULT_TIMEOUT = 5 * 1000L
private const val MAX_RETRIES = 10
private const val UNIQUE_ID_KEY = "UniqueId"
private const val PREFERENCES_AND_FILE_MANAGER_SUFFIX = "515d6767-01b7-49e5-8273-c8d11b0f331d"
private lateinit var synchronousAuth: SynchronousAuth
private lateinit var preferences: SharedPreferences
private lateinit var appId: String
private lateinit var uniqueId: String
private lateinit var pinpointClient: PinpointClient
@BeforeClass
@JvmStatic
fun setupBefore() {
val context = ApplicationProvider.getApplicationContext<Context>()
@RawRes val resourceId = Resources.getRawResourceId(context, CONFIGURATION_NAME)
appId = readAppIdFromResource(context, resourceId)
preferences = context.getSharedPreferences(
"${appId}$PREFERENCES_AND_FILE_MANAGER_SUFFIX",
Context.MODE_PRIVATE
)
setUniqueId()
Amplify.Auth.addPlugin(AWSCognitoAuthPlugin() as AuthPlugin<*>)
Amplify.addPlugin(AWSPinpointAnalyticsPlugin())
Amplify.Logging.addPlugin(AndroidLoggingPlugin(LogLevel.DEBUG))
Amplify.configure(context)
Sleep.milliseconds(COGNITO_CONFIGURATION_TIMEOUT)
synchronousAuth = SynchronousAuth.delegatingTo(Amplify.Auth)
}
private fun setUniqueId() {
uniqueId = UUID.randomUUID().toString()
preferences.edit().putString(UNIQUE_ID_KEY, uniqueId).commit()
}
private fun readCredentialsFromResource(context: Context, @RawRes resourceId: Int): Pair<String, String>? {
val resource = Resources.readAsJson(context, resourceId)
var userCredentials: Pair<String, String>? = null
return try {
val credentials = resource.getJSONArray("credentials")
for (index in 0 until credentials.length()) {
val credential = credentials.getJSONObject(index)
val username = credential.getString("username")
val password = credential.getString("password")
userCredentials = Pair(username, password)
}
userCredentials
} catch (jsonReadingFailure: JSONException) {
throw RuntimeException(jsonReadingFailure)
}
}
private fun readAppIdFromResource(context: Context, @RawRes resourceId: Int): String {
val resource = Resources.readAsJson(context, resourceId)
return try {
val analyticsJson = resource.getJSONObject("analytics")
val pluginsJson = analyticsJson.getJSONObject("plugins")
val pluginJson = pluginsJson.getJSONObject("awsPinpointAnalyticsPlugin")
val pinpointJson = pluginJson.getJSONObject("pinpointAnalytics")
pinpointJson.getString("appId")
} catch (jsonReadingFailure: JSONException) {
throw RuntimeException(jsonReadingFailure)
}
}
}
}