|
| 1 | +/* |
| 2 | + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"). |
| 5 | + * You may not use this file except in compliance with the License. |
| 6 | + * A copy of the License is located at |
| 7 | + * |
| 8 | + * http://aws.amazon.com/apache2.0 |
| 9 | + * |
| 10 | + * or in the "license" file accompanying this file. This file is distributed |
| 11 | + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either |
| 12 | + * express or implied. See the License for the specific language governing |
| 13 | + * permissions and limitations under the License. |
| 14 | + */ |
| 15 | +package com.amplifyframework.analytics.pinpoint |
| 16 | + |
| 17 | +import android.content.Context |
| 18 | +import android.content.SharedPreferences |
| 19 | +import android.util.Log |
| 20 | +import android.util.Pair |
| 21 | +import androidx.annotation.RawRes |
| 22 | +import androidx.test.core.app.ApplicationProvider |
| 23 | +import aws.sdk.kotlin.services.pinpoint.PinpointClient |
| 24 | +import aws.sdk.kotlin.services.pinpoint.model.EndpointLocation |
| 25 | +import aws.sdk.kotlin.services.pinpoint.model.EndpointResponse |
| 26 | +import aws.sdk.kotlin.services.pinpoint.model.GetEndpointRequest |
| 27 | +import com.amplifyframework.analytics.AnalyticsEvent |
| 28 | +import com.amplifyframework.analytics.AnalyticsProperties |
| 29 | +import com.amplifyframework.analytics.UserProfile |
| 30 | +import com.amplifyframework.analytics.pinpoint.models.AWSPinpointUserProfile |
| 31 | +import com.amplifyframework.auth.AuthPlugin |
| 32 | +import com.amplifyframework.auth.cognito.AWSCognitoAuthPlugin |
| 33 | +import com.amplifyframework.core.Amplify |
| 34 | +import com.amplifyframework.hub.HubChannel |
| 35 | +import com.amplifyframework.hub.HubEvent |
| 36 | +import com.amplifyframework.testutils.HubAccumulator |
| 37 | +import com.amplifyframework.testutils.Resources |
| 38 | +import com.amplifyframework.testutils.Sleep |
| 39 | +import com.amplifyframework.testutils.sync.SynchronousAuth |
| 40 | +import java.util.UUID |
| 41 | +import java.util.concurrent.TimeUnit |
| 42 | +import kotlinx.coroutines.runBlocking |
| 43 | +import org.json.JSONException |
| 44 | +import org.junit.Assert |
| 45 | +import org.junit.Before |
| 46 | +import org.junit.BeforeClass |
| 47 | +import org.junit.Test |
| 48 | + |
| 49 | +class PinpointAnalyticsStressTest { |
| 50 | + |
| 51 | + companion object { |
| 52 | + private const val CREDENTIALS_RESOURCE_NAME = "credentials" |
| 53 | + private const val CONFIGURATION_NAME = "amplifyconfiguration" |
| 54 | + private const val COGNITO_CONFIGURATION_TIMEOUT = 5 * 1000L |
| 55 | + private const val PINPOINT_ROUNDTRIP_TIMEOUT = 1 * 1000L |
| 56 | + private const val FLUSH_TIMEOUT = 1 * 500L |
| 57 | + private const val RECORD_INSERTION_TIMEOUT = 1 * 1000L |
| 58 | + private const val UNIQUE_ID_KEY = "UniqueId" |
| 59 | + private const val PREFERENCES_AND_FILE_MANAGER_SUFFIX = "515d6767-01b7-49e5-8273-c8d11b0f331d" |
| 60 | + private lateinit var synchronousAuth: SynchronousAuth |
| 61 | + private lateinit var preferences: SharedPreferences |
| 62 | + private lateinit var appId: String |
| 63 | + private lateinit var uniqueId: String |
| 64 | + private lateinit var pinpointClient: PinpointClient |
| 65 | + |
| 66 | + @BeforeClass |
| 67 | + @JvmStatic |
| 68 | + fun setupBefore() { |
| 69 | + val context = ApplicationProvider.getApplicationContext<Context>() |
| 70 | + @RawRes val resourceId = Resources.getRawResourceId(context, CONFIGURATION_NAME) |
| 71 | + appId = readAppIdFromResource(context, resourceId) |
| 72 | + preferences = context.getSharedPreferences( |
| 73 | + "${appId}$PREFERENCES_AND_FILE_MANAGER_SUFFIX", |
| 74 | + Context.MODE_PRIVATE |
| 75 | + ) |
| 76 | + setUniqueId() |
| 77 | + Amplify.Auth.addPlugin(AWSCognitoAuthPlugin() as AuthPlugin<*>) |
| 78 | + Amplify.addPlugin(AWSPinpointAnalyticsPlugin()) |
| 79 | + Amplify.configure(context) |
| 80 | + Sleep.milliseconds(COGNITO_CONFIGURATION_TIMEOUT) |
| 81 | + synchronousAuth = SynchronousAuth.delegatingTo(Amplify.Auth) |
| 82 | + } |
| 83 | + |
| 84 | + private fun setUniqueId() { |
| 85 | + uniqueId = UUID.randomUUID().toString() |
| 86 | + preferences.edit().putString(UNIQUE_ID_KEY, uniqueId).commit() |
| 87 | + } |
| 88 | + |
| 89 | + private fun readCredentialsFromResource(context: Context, @RawRes resourceId: Int): Pair<String, String>? { |
| 90 | + val resource = Resources.readAsJson(context, resourceId) |
| 91 | + var userCredentials: Pair<String, String>? = null |
| 92 | + return try { |
| 93 | + val credentials = resource.getJSONArray("credentials") |
| 94 | + for (index in 0 until credentials.length()) { |
| 95 | + val credential = credentials.getJSONObject(index) |
| 96 | + val username = credential.getString("username") |
| 97 | + val password = credential.getString("password") |
| 98 | + userCredentials = Pair(username, password) |
| 99 | + } |
| 100 | + userCredentials |
| 101 | + } catch (jsonReadingFailure: JSONException) { |
| 102 | + throw RuntimeException(jsonReadingFailure) |
| 103 | + } |
| 104 | + } |
| 105 | + |
| 106 | + private fun readAppIdFromResource(context: Context, @RawRes resourceId: Int): String { |
| 107 | + val resource = Resources.readAsJson(context, resourceId) |
| 108 | + return try { |
| 109 | + val analyticsJson = resource.getJSONObject("analytics") |
| 110 | + val pluginsJson = analyticsJson.getJSONObject("plugins") |
| 111 | + val pluginJson = pluginsJson.getJSONObject("awsPinpointAnalyticsPlugin") |
| 112 | + val pinpointJson = pluginJson.getJSONObject("pinpointAnalytics") |
| 113 | + pinpointJson.getString("appId") |
| 114 | + } catch (jsonReadingFailure: JSONException) { |
| 115 | + throw RuntimeException(jsonReadingFailure) |
| 116 | + } |
| 117 | + } |
| 118 | + } |
| 119 | + |
| 120 | + @Before |
| 121 | + fun flushEvents() { |
| 122 | + val context = ApplicationProvider.getApplicationContext<Context>() |
| 123 | + @RawRes val resourceId = Resources.getRawResourceId(context, CREDENTIALS_RESOURCE_NAME) |
| 124 | + val userAndPasswordPair = readCredentialsFromResource(context, resourceId) |
| 125 | + synchronousAuth.signOut() |
| 126 | + synchronousAuth.signIn( |
| 127 | + userAndPasswordPair!!.first, |
| 128 | + userAndPasswordPair.second |
| 129 | + ) |
| 130 | + val hubAccumulator = |
| 131 | + HubAccumulator.create(HubChannel.ANALYTICS, AnalyticsChannelEventName.FLUSH_EVENTS, 1).start() |
| 132 | + Amplify.Analytics.flushEvents() |
| 133 | + hubAccumulator.await(10, TimeUnit.SECONDS) |
| 134 | + pinpointClient = Amplify.Analytics.getPlugin("awsPinpointAnalyticsPlugin").escapeHatch as |
| 135 | + PinpointClient |
| 136 | + uniqueId = preferences.getString(UNIQUE_ID_KEY, "error-no-unique-id")!! |
| 137 | + Assert.assertNotEquals(uniqueId, "error-no-unique-id") |
| 138 | + } |
| 139 | + |
| 140 | + /** |
| 141 | + * Calls Analytics.recordEvent on an event with 5 attributes 50 times |
| 142 | + */ |
| 143 | + @Test |
| 144 | + fun testMultipleRecordEvent() { |
| 145 | + var eventName: String |
| 146 | + val hubAccumulator = |
| 147 | + HubAccumulator.create(HubChannel.ANALYTICS, AnalyticsChannelEventName.FLUSH_EVENTS, 2).start() |
| 148 | + |
| 149 | + repeat(50) { |
| 150 | + eventName = "Amplify-event" + UUID.randomUUID().toString() |
| 151 | + val event = AnalyticsEvent.builder() |
| 152 | + .name(eventName) |
| 153 | + .addProperty("AnalyticsStringProperty", "Pancakes") |
| 154 | + .addProperty("AnalyticsBooleanProperty", true) |
| 155 | + .addProperty("AnalyticsDoubleProperty", 3.14) |
| 156 | + .addProperty("AnalyticsIntegerProperty", 42) |
| 157 | + .build() |
| 158 | + |
| 159 | + Amplify.Analytics.recordEvent(event) |
| 160 | + } |
| 161 | + |
| 162 | + Amplify.Analytics.flushEvents() |
| 163 | + val hubEvents = hubAccumulator.await(10, TimeUnit.SECONDS) |
| 164 | + val submittedEvents = combineAndFilterEvents(hubEvents) |
| 165 | + Assert.assertEquals(50, submittedEvents.size.toLong()) |
| 166 | + } |
| 167 | + |
| 168 | + /** |
| 169 | + * Calls Analytics.recordEvent on an event with 40 attributes 50 times |
| 170 | + */ |
| 171 | + @Test |
| 172 | + fun testLargeMultipleRecordEvent() { |
| 173 | + var eventName: String |
| 174 | + val hubAccumulator = |
| 175 | + HubAccumulator.create(HubChannel.ANALYTICS, AnalyticsChannelEventName.FLUSH_EVENTS, 2).start() |
| 176 | + |
| 177 | + repeat(50) { |
| 178 | + eventName = "Amplify-event" + UUID.randomUUID().toString() |
| 179 | + val event = AnalyticsEvent.builder() |
| 180 | + event.name(eventName) |
| 181 | + for (i in 1..50) { |
| 182 | + event.addProperty("AnalyticsStringProperty$i", "Pancakes") |
| 183 | + } |
| 184 | + |
| 185 | + Amplify.Analytics.recordEvent(event.build()) |
| 186 | + } |
| 187 | + |
| 188 | + Amplify.Analytics.flushEvents() |
| 189 | + val hubEvents = hubAccumulator.await(10, TimeUnit.SECONDS) |
| 190 | + val submittedEvents = combineAndFilterEvents(hubEvents) |
| 191 | + Assert.assertEquals(50, submittedEvents.size.toLong()) |
| 192 | + } |
| 193 | + |
| 194 | + /** |
| 195 | + * Calls Analytics.flushEvent 50 times |
| 196 | + */ |
| 197 | + @Test |
| 198 | + fun testMultipleFlushEvent() { |
| 199 | + val analyticsHubEventAccumulator = |
| 200 | + HubAccumulator.create(HubChannel.ANALYTICS, AnalyticsChannelEventName.FLUSH_EVENTS, 50) |
| 201 | + .start() |
| 202 | + val eventName = "Amplify-event" + UUID.randomUUID().toString() |
| 203 | + val event = AnalyticsEvent.builder() |
| 204 | + .name(eventName) |
| 205 | + .addProperty("AnalyticsStringProperty", "Pancakes") |
| 206 | + .build() |
| 207 | + Amplify.Analytics.recordEvent(event) |
| 208 | + |
| 209 | + repeat(50) { |
| 210 | + Amplify.Analytics.flushEvents() |
| 211 | + Sleep.milliseconds(FLUSH_TIMEOUT) |
| 212 | + } |
| 213 | + |
| 214 | + val hubEvents = analyticsHubEventAccumulator.await(10, TimeUnit.SECONDS) |
| 215 | + val submittedEvents = combineAndFilterEvents(hubEvents) |
| 216 | + Assert.assertEquals(1, submittedEvents.size.toLong()) |
| 217 | + Assert.assertEquals(eventName, submittedEvents[0].name) |
| 218 | + } |
| 219 | + |
| 220 | + /** |
| 221 | + * calls Analytics.recordEvent, then calls Analytics.flushEvent; 30 times |
| 222 | + */ |
| 223 | + @Test |
| 224 | + fun testFlushEvent_AfterRecordEvent() { |
| 225 | + var eventName: String |
| 226 | + val analyticsHubEventAccumulator = |
| 227 | + HubAccumulator.create(HubChannel.ANALYTICS, AnalyticsChannelEventName.FLUSH_EVENTS, 35) |
| 228 | + .start() |
| 229 | + |
| 230 | + repeat(30) { |
| 231 | + eventName = "Amplify-event" + UUID.randomUUID().toString() |
| 232 | + val event = AnalyticsEvent.builder() |
| 233 | + .name(eventName) |
| 234 | + .addProperty("AnalyticsStringProperty", "Pancakes") |
| 235 | + .build() |
| 236 | + Amplify.Analytics.recordEvent(event) |
| 237 | + Sleep.milliseconds(RECORD_INSERTION_TIMEOUT) |
| 238 | + Amplify.Analytics.flushEvents() |
| 239 | + Sleep.milliseconds(FLUSH_TIMEOUT) |
| 240 | + } |
| 241 | + val hubEvents = analyticsHubEventAccumulator.await(30, TimeUnit.SECONDS) |
| 242 | + val submittedEvents = combineAndFilterEvents(hubEvents) |
| 243 | + Assert.assertEquals(30, submittedEvents.size.toLong()) |
| 244 | + } |
| 245 | + |
| 246 | + /** |
| 247 | + * Calls Analytics.identifyUser on a user with few attributes 20 times |
| 248 | + */ |
| 249 | + @Test |
| 250 | + fun testMultipleIdentifyUser() { |
| 251 | + val location = testLocation |
| 252 | + val properties = endpointProperties |
| 253 | + val userProfile = AWSPinpointUserProfile.builder() |
| 254 | + .name("test-user") |
| 255 | + |
| 256 | + .plan("test-plan") |
| 257 | + .location(location) |
| 258 | + .customProperties(properties) |
| 259 | + .build() |
| 260 | + repeat(20) { |
| 261 | + Amplify.Analytics.identifyUser(UUID.randomUUID().toString(), userProfile) |
| 262 | + Sleep.milliseconds(PINPOINT_ROUNDTRIP_TIMEOUT) |
| 263 | + val endpointResponse = fetchEndpointResponse() |
| 264 | + assertCommonEndpointResponseProperties(endpointResponse) |
| 265 | + } |
| 266 | + } |
| 267 | + |
| 268 | + /** |
| 269 | + * Calls Analytics.identifyUser on a user with 100+ attributes 20 times |
| 270 | + */ |
| 271 | + @Test |
| 272 | + fun testLargeMultipleIdentifyUser() { |
| 273 | + val location = testLocation |
| 274 | + val properties = endpointProperties |
| 275 | + val userAttributes = largeUserAttributes |
| 276 | + val pinpointUserProfile = AWSPinpointUserProfile.builder() |
| 277 | + .name("test-user") |
| 278 | + |
| 279 | + .plan("test-plan") |
| 280 | + .location(location) |
| 281 | + .customProperties(properties) |
| 282 | + .userAttributes(userAttributes) |
| 283 | + .build() |
| 284 | + repeat(20) { |
| 285 | + Amplify.Analytics.identifyUser(UUID.randomUUID().toString(), pinpointUserProfile) |
| 286 | + Sleep.milliseconds(PINPOINT_ROUNDTRIP_TIMEOUT) |
| 287 | + val endpointResponse = fetchEndpointResponse() |
| 288 | + assertCommonEndpointResponseProperties(endpointResponse) |
| 289 | + } |
| 290 | + } |
| 291 | + |
| 292 | + private fun fetchEndpointResponse(): EndpointResponse { |
| 293 | + var endpointResponse: EndpointResponse? = null |
| 294 | + runBlocking { |
| 295 | + endpointResponse = pinpointClient.getEndpoint( |
| 296 | + GetEndpointRequest.invoke { |
| 297 | + this.applicationId = appId |
| 298 | + this.endpointId = uniqueId |
| 299 | + } |
| 300 | + ).endpointResponse |
| 301 | + } |
| 302 | + assert(null != endpointResponse) |
| 303 | + return endpointResponse!! |
| 304 | + } |
| 305 | + |
| 306 | + private fun assertCommonEndpointResponseProperties(endpointResponse: EndpointResponse) { |
| 307 | + Log.i("DEBUG", endpointResponse.toString()) |
| 308 | + val attributes = endpointResponse.attributes!! |
| 309 | + Assert.assertEquals( "[email protected]", attributes[ "email"] !![ 0]) |
| 310 | + Assert.assertEquals("test-user", attributes["name"]!![0]) |
| 311 | + Assert.assertEquals("test-plan", attributes["plan"]!![0]) |
| 312 | + val endpointProfileLocation: EndpointLocation = endpointResponse.location!! |
| 313 | + Assert.assertEquals(47.6154086, endpointProfileLocation.latitude, 0.1) |
| 314 | + Assert.assertEquals((-122.3349685), endpointProfileLocation.longitude, 0.1) |
| 315 | + Assert.assertEquals("98122", endpointProfileLocation.postalCode) |
| 316 | + Assert.assertEquals("Seattle", endpointProfileLocation.city) |
| 317 | + Assert.assertEquals("WA", endpointProfileLocation.region) |
| 318 | + Assert.assertEquals("USA", endpointProfileLocation.country) |
| 319 | + Assert.assertEquals("TestStringValue", attributes["TestStringProperty"]!![0]) |
| 320 | + Assert.assertEquals(1.0, endpointResponse.metrics!!["TestDoubleProperty"]!!, 0.1) |
| 321 | + } |
| 322 | + |
| 323 | + private val largeUserAttributes: AnalyticsProperties |
| 324 | + get() { |
| 325 | + val analyticsProperties = AnalyticsProperties.builder() |
| 326 | + for (i in 1..100) { |
| 327 | + analyticsProperties.add("SomeUserAttribute$i", "User attribute value") |
| 328 | + } |
| 329 | + return analyticsProperties.build() |
| 330 | + } |
| 331 | + |
| 332 | + private val endpointProperties: AnalyticsProperties |
| 333 | + get() { |
| 334 | + return AnalyticsProperties.builder() |
| 335 | + .add("TestStringProperty", "TestStringValue") |
| 336 | + .add("TestDoubleProperty", 1.0) |
| 337 | + .build() |
| 338 | + } |
| 339 | + private val testLocation: UserProfile.Location |
| 340 | + get() { |
| 341 | + return UserProfile.Location.builder() |
| 342 | + .latitude(47.6154086) |
| 343 | + .longitude(-122.3349685) |
| 344 | + .postalCode("98122") |
| 345 | + .city("Seattle") |
| 346 | + .region("WA") |
| 347 | + .country("USA") |
| 348 | + .build() |
| 349 | + } |
| 350 | + |
| 351 | + private fun combineAndFilterEvents(hubEvents: List<HubEvent<*>>): MutableList<AnalyticsEvent> { |
| 352 | + val result = mutableListOf<AnalyticsEvent>() |
| 353 | + hubEvents.forEach { |
| 354 | + if ((it.data as List<*>).isNotEmpty()) { |
| 355 | + (it.data as ArrayList<*>).forEach { event -> |
| 356 | + if (!(event as AnalyticsEvent).name.startsWith("_session")) { |
| 357 | + result.add(event) |
| 358 | + } |
| 359 | + } |
| 360 | + } |
| 361 | + } |
| 362 | + return result |
| 363 | + } |
| 364 | +} |
0 commit comments