@@ -28,6 +28,7 @@ import okhttp3.mockwebserver.MockResponse
28
28
import okhttp3.mockwebserver.MockWebServer
29
29
import okhttp3.mockwebserver.RecordedRequest
30
30
import org.assertj.core.api.Assertions.assertThat
31
+ import org.assertj.core.api.Assertions.within
31
32
import org.junit.Test
32
33
import org.junit.runner.RunWith
33
34
@@ -49,12 +50,12 @@ class FunctionalTests {
49
50
}
50
51
51
52
/* *
52
- * In this scenario, the consumer application tracks more than 50 events-threshold during a flush interval.
53
+ * In this scenario, the consumer application tracks 51 events-threshold during a flush interval.
53
54
* The SDK will save the events to disk and send them in the next flush interval.
54
55
* At the end, when all events are sent, the SDK will delete the content of local storage file.
55
56
*/
56
57
@Test
57
- fun appTracksEventsAboveQueueSizeLimit () {
58
+ fun appTracksEventsDuringTheFlushInterval () {
58
59
ActivityScenario .launch(SampleActivity ::class .java).use { scenario ->
59
60
scenario.onActivity { activity: Activity ->
60
61
beforeEach(activity)
@@ -76,6 +77,47 @@ class FunctionalTests {
76
77
}
77
78
}
78
79
80
+ /* *
81
+ * In this scenario, the consumer app tracks 2 events during the first flush interval.
82
+ * Then, we validate, that after flush interval passed the SDK sends the events
83
+ * to Parse.ly servers.
84
+ *
85
+ * Then, the consumer app tracks another event and we validate that the SDK sends the event
86
+ * to Parse.ly servers as well.
87
+ */
88
+ @Test
89
+ fun appFlushesEventsAfterFlushInterval () {
90
+ ActivityScenario .launch(SampleActivity ::class .java).use { scenario ->
91
+ scenario.onActivity { activity: Activity ->
92
+ beforeEach(activity)
93
+ server.enqueue(MockResponse ().setResponseCode(200 ))
94
+ parselyTracker = initializeTracker(activity)
95
+
96
+ parselyTracker.trackPageview(" url" , null , null , null )
97
+ }
98
+
99
+ Thread .sleep((defaultFlushInterval / 2 ).inWholeMilliseconds)
100
+
101
+ scenario.onActivity {
102
+ parselyTracker.trackPageview(" url" , null , null , null )
103
+ }
104
+
105
+ Thread .sleep((defaultFlushInterval / 2 ).inWholeMilliseconds)
106
+
107
+ val firstRequestPayload = server.takeRequest(2000 , TimeUnit .MILLISECONDS )?.toMap()
108
+ assertThat(firstRequestPayload!! [" events" ]).hasSize(2 )
109
+
110
+ scenario.onActivity {
111
+ parselyTracker.trackPageview(" url" , null , null , null )
112
+ }
113
+
114
+ Thread .sleep(defaultFlushInterval.inWholeMilliseconds)
115
+
116
+ val secondRequestPayload = server.takeRequest(2000 , TimeUnit .MILLISECONDS )?.toMap()
117
+ assertThat(secondRequestPayload!! [" events" ]).hasSize(1 )
118
+ }
119
+ }
120
+
79
121
/* *
80
122
* In this scenario, the consumer application:
81
123
* 1. Goes to the background
@@ -113,6 +155,132 @@ class FunctionalTests {
113
155
}
114
156
}
115
157
158
+ /* *
159
+ * In this scenario we "stress test" the concurrency model to see if we have any conflict during
160
+ *
161
+ * - Unexpectedly high number of recorded events in small intervals (I/O locking)
162
+ * - Scenario in which a request is sent at the same time as new events are recorded
163
+ */
164
+ @Test
165
+ fun stressTest () {
166
+ val eventsToSend = 500
167
+
168
+ ActivityScenario .launch(SampleActivity ::class .java).use { scenario ->
169
+ scenario.onActivity { activity: Activity ->
170
+ beforeEach(activity)
171
+ server.enqueue(MockResponse ().setResponseCode(200 ))
172
+ parselyTracker = initializeTracker(activity)
173
+
174
+ repeat(eventsToSend) {
175
+ parselyTracker.trackPageview(" url" , null , null , null )
176
+ }
177
+ }
178
+
179
+ // Wait some time to give events chance to be saved in local data storage
180
+ Thread .sleep((defaultFlushInterval * 2 ).inWholeMilliseconds)
181
+
182
+ // Catch up to 10 requests. We don't know how many requests the device we test on will
183
+ // perform. It's probably more like 1-2, but we're on safe (not flaky) side here.
184
+ val requests = (1 .. 10 ).mapNotNull {
185
+ runCatching { server.takeRequest(100 , TimeUnit .MILLISECONDS ) }.getOrNull()
186
+ }.flatMap {
187
+ it.toMap()[" events" ]!!
188
+ }
189
+
190
+ assertThat(requests).hasSize(eventsToSend)
191
+ }
192
+ }
193
+
194
+ /* *
195
+ * In this scenario consumer app starts an engagement session and after 27150 ms,
196
+ * it stops the session.
197
+ *
198
+ * Intervals:
199
+ * With current implementation of `HeartbeatIntervalCalculator`, the next intervals are:
200
+ * - 10500ms for the first interval
201
+ * - 13650ms for the second interval
202
+ *
203
+ * So after ~27,2s we should observe
204
+ * - 2 `heartbeat` events from `startEngagement` + 1 `heartbeat` event caused by `stopEngagement` which is triggered during engagement interval
205
+ *
206
+ * Time off-differences in assertions are acceptable, because it's a time-sensitive test
207
+ */
208
+ @Test
209
+ fun engagementManagerTest () {
210
+ val engagementUrl = " engagementUrl"
211
+ var startTimestamp = Duration .ZERO
212
+ val firstInterval = 10500 .milliseconds
213
+ val secondInterval = 13650 .milliseconds
214
+ val pauseInterval = 3 .seconds
215
+ ActivityScenario .launch(SampleActivity ::class .java).use { scenario ->
216
+ // given
217
+ scenario.onActivity { activity: Activity ->
218
+ beforeEach(activity)
219
+ server.enqueue(MockResponse ().setResponseCode(200 ))
220
+ parselyTracker = initializeTracker(activity, flushInterval = 30 .seconds)
221
+
222
+ // when
223
+ startTimestamp = System .currentTimeMillis().milliseconds
224
+ parselyTracker.startEngagement(engagementUrl, null )
225
+ }
226
+
227
+ Thread .sleep((firstInterval + secondInterval + pauseInterval).inWholeMilliseconds)
228
+ parselyTracker.stopEngagement()
229
+
230
+ // then
231
+ val request = server.takeRequest(35 , TimeUnit .SECONDS )!! .toMap()[" events" ]!!
232
+
233
+ assertThat(
234
+ request.sortedBy { it.data.timestamp }
235
+ .filter { it.action == " heartbeat" }
236
+ ).hasSize(3 )
237
+ .satisfies({
238
+ val firstEvent = it[0 ]
239
+ val secondEvent = it[1 ]
240
+ val thirdEvent = it[2 ]
241
+
242
+ assertThat(firstEvent.data.timestamp).isCloseTo(
243
+ (startTimestamp + firstInterval).inWholeMilliseconds,
244
+ within(1 .seconds.inWholeMilliseconds)
245
+ )
246
+ assertThat(firstEvent.totalTime).isCloseTo(
247
+ firstInterval.inWholeMilliseconds,
248
+ within(100L )
249
+ )
250
+ assertThat(firstEvent.incremental).isCloseTo(
251
+ firstInterval.inWholeSeconds,
252
+ within(1L )
253
+ )
254
+
255
+ assertThat(secondEvent.data.timestamp).isCloseTo(
256
+ (startTimestamp + firstInterval + secondInterval).inWholeMilliseconds,
257
+ within(1 .seconds.inWholeMilliseconds)
258
+ )
259
+ assertThat(secondEvent.totalTime).isCloseTo(
260
+ (firstInterval + secondInterval).inWholeMilliseconds,
261
+ within(100L )
262
+ )
263
+ assertThat(secondEvent.incremental).isCloseTo(
264
+ secondInterval.inWholeSeconds,
265
+ within(1L )
266
+ )
267
+
268
+ assertThat(thirdEvent.data.timestamp).isCloseTo(
269
+ (startTimestamp + firstInterval + secondInterval + pauseInterval).inWholeMilliseconds,
270
+ within(1 .seconds.inWholeMilliseconds)
271
+ )
272
+ assertThat(thirdEvent.totalTime).isCloseTo(
273
+ (firstInterval + secondInterval + pauseInterval).inWholeMilliseconds,
274
+ within(100L )
275
+ )
276
+ assertThat(thirdEvent.incremental).isCloseTo(
277
+ (pauseInterval).inWholeSeconds,
278
+ within(1L )
279
+ )
280
+ })
281
+ }
282
+ }
283
+
116
284
private fun RecordedRequest.toMap (): Map <String , List <Event >> {
117
285
val listType: TypeReference <Map <String , List <Event >>> =
118
286
object : TypeReference <Map <String , List <Event >>>() {}
@@ -123,6 +291,15 @@ class FunctionalTests {
123
291
@JsonIgnoreProperties(ignoreUnknown = true )
124
292
data class Event (
125
293
@JsonProperty(" idsite" ) var idsite : String ,
294
+ @JsonProperty(" action" ) var action : String ,
295
+ @JsonProperty(" data" ) var data : ExtraData ,
296
+ @JsonProperty(" tt" ) var totalTime : Long ,
297
+ @JsonProperty(" inc" ) var incremental : Long ,
298
+ )
299
+
300
+ @JsonIgnoreProperties(ignoreUnknown = true )
301
+ data class ExtraData (
302
+ @JsonProperty(" ts" ) var timestamp : Long ,
126
303
)
127
304
128
305
private val locallyStoredEvents
@@ -137,19 +314,18 @@ class FunctionalTests {
137
314
activity : Activity ,
138
315
flushInterval : Duration = defaultFlushInterval
139
316
): ParselyTracker {
317
+ val field: Field = ParselyTracker ::class .java.getDeclaredField(" ROOT_URL" )
318
+ field.isAccessible = true
319
+ field.set(this , url)
140
320
return ParselyTracker .sharedInstance(
141
321
siteId, flushInterval.inWholeSeconds.toInt(), activity.application
142
- ).apply {
143
- val f: Field = this ::class .java.getDeclaredField(" ROOT_URL" )
144
- f.isAccessible = true
145
- f.set(this , url)
146
- }
322
+ )
147
323
}
148
324
149
325
private companion object {
150
326
const val siteId = " 123"
151
327
const val localStorageFileName = " parsely-events.ser"
152
- val defaultFlushInterval = 10 .seconds
328
+ val defaultFlushInterval = 5 .seconds
153
329
}
154
330
155
331
class SampleActivity : Activity ()
0 commit comments