Skip to content

Commit 693f638

Browse files
authored
Add new WebView Interface (#700)
* Add new webview classes * Add tests * Add another test * Start adding support for self-desc events * Use MockNetworkConnection in tests * Allow self-describing event data * Add another test * Subscribe using new interface * Remove unused imports * Remove spaces * Add more test sleep * Fix small review comments * Improve the API * Update tests to use EventSink where possible * Update test * Add test for default eventname and trackerversion
1 parent 05fdaa6 commit 693f638

File tree

6 files changed

+471
-13
lines changed

6 files changed

+471
-13
lines changed
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
/*
2+
* Copyright (c) 2015-present Snowplow Analytics Ltd. All rights reserved.
3+
*
4+
* This program is licensed to you under the Apache License Version 2.0,
5+
* and you may not use this file except in compliance with the Apache License Version 2.0.
6+
* You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
7+
*
8+
* Unless required by applicable law or agreed to in writing,
9+
* software distributed under the Apache License Version 2.0 is distributed on an
10+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
12+
*/
13+
14+
package com.snowplowanalytics.snowplow.tracker
15+
16+
import android.content.Context
17+
import androidx.test.ext.junit.runners.AndroidJUnit4
18+
import androidx.test.platform.app.InstrumentationRegistry
19+
import com.snowplowanalytics.core.constants.Parameters
20+
import com.snowplowanalytics.core.constants.TrackerConstants
21+
import com.snowplowanalytics.core.emitter.Executor
22+
import com.snowplowanalytics.core.tracker.TrackerWebViewInterfaceV2
23+
import com.snowplowanalytics.snowplow.Snowplow.createTracker
24+
import com.snowplowanalytics.snowplow.Snowplow.removeAllTrackers
25+
import com.snowplowanalytics.snowplow.configuration.NetworkConfiguration
26+
import com.snowplowanalytics.snowplow.configuration.TrackerConfiguration
27+
import com.snowplowanalytics.snowplow.controller.TrackerController
28+
import com.snowplowanalytics.snowplow.network.HttpMethod
29+
import com.snowplowanalytics.snowplow.util.EventSink
30+
import org.json.JSONException
31+
import org.json.JSONObject
32+
import org.junit.After
33+
import org.junit.Assert.*
34+
import org.junit.Before
35+
import org.junit.Test
36+
import org.junit.runner.RunWith
37+
38+
@RunWith(AndroidJUnit4::class)
39+
class TrackerWebViewInterfaceV2Test {
40+
private var webInterface: TrackerWebViewInterfaceV2? = null
41+
42+
@Before
43+
fun setUp() {
44+
webInterface = TrackerWebViewInterfaceV2()
45+
}
46+
47+
@After
48+
fun tearDown() {
49+
removeAllTrackers()
50+
Executor.shutdown()
51+
}
52+
53+
@Test
54+
@Throws(JSONException::class, InterruptedException::class)
55+
fun tracksEventWithAllOptions() {
56+
val networkConnection = MockNetworkConnection(HttpMethod.GET, 200)
57+
createTracker(
58+
context,
59+
"ns${Math.random()}",
60+
NetworkConfiguration(networkConnection),
61+
TrackerConfiguration("appId").base64encoding(false)
62+
)
63+
64+
val data = "{\"schema\":\"iglu:etc\",\"data\":{\"key\":\"val\"}}"
65+
val atomic = "{\"eventName\":\"pv\",\"trackerVersion\":\"webview\"," +
66+
"\"useragent\":\"Chrome\",\"pageUrl\":\"http://snowplow.com\"," +
67+
"\"pageTitle\":\"Snowplow\",\"referrer\":\"http://google.com\"," +
68+
"\"pingXOffsetMin\":10,\"pingXOffsetMax\":20,\"pingYOffsetMin\":30," +
69+
"\"pingYOffsetMax\":40,\"category\":\"cat\",\"action\":\"act\"," +
70+
"\"property\":\"prop\",\"label\":\"lbl\",\"value\":10.0}"
71+
72+
webInterface!!.trackWebViewEvent(
73+
selfDescribingEventData = data,
74+
atomicProperties = atomic
75+
)
76+
77+
waitForEvents(networkConnection)
78+
assertEquals(1, networkConnection.countRequests())
79+
80+
val request = networkConnection.allRequests[0]
81+
val payload = request.payload.map
82+
83+
assertEquals("pv", payload[Parameters.EVENT])
84+
assertEquals("webview", payload[Parameters.TRACKER_VERSION])
85+
assertEquals("Chrome", payload[Parameters.USERAGENT])
86+
assertEquals("http://snowplow.com", payload[Parameters.PAGE_URL])
87+
assertEquals("Snowplow", payload[Parameters.PAGE_TITLE])
88+
assertEquals("http://google.com", payload[Parameters.PAGE_REFR])
89+
assertEquals("10", payload[Parameters.PING_XOFFSET_MIN])
90+
assertEquals("20", payload[Parameters.PING_XOFFSET_MAX])
91+
assertEquals("30", payload[Parameters.PING_YOFFSET_MIN])
92+
assertEquals("40", payload[Parameters.PING_YOFFSET_MAX])
93+
assertEquals("cat", payload[Parameters.SE_CATEGORY])
94+
assertEquals("act", payload[Parameters.SE_ACTION])
95+
assertEquals("prop", payload[Parameters.SE_PROPERTY])
96+
assertEquals("lbl", payload[Parameters.SE_LABEL])
97+
assertEquals("10.0", payload[Parameters.SE_VALUE])
98+
99+
assertTrue(payload.containsKey(Parameters.UNSTRUCTURED))
100+
val selfDescJson = JSONObject(payload[Parameters.UNSTRUCTURED] as String)
101+
assertEquals(TrackerConstants.SCHEMA_UNSTRUCT_EVENT, selfDescJson.getString("schema"))
102+
assertEquals(data, selfDescJson.getString("data"))
103+
}
104+
105+
@Test
106+
@Throws(JSONException::class, InterruptedException::class)
107+
fun addsDefaultPropertiesIfNotProvided() {
108+
val networkConnection = MockNetworkConnection(HttpMethod.GET, 200)
109+
createTracker(
110+
context,
111+
"ns${Math.random()}",
112+
NetworkConfiguration(networkConnection),
113+
TrackerConfiguration("appId").base64encoding(false)
114+
)
115+
116+
webInterface!!.trackWebViewEvent(atomicProperties = "{}")
117+
118+
waitForEvents(networkConnection)
119+
assertEquals(1, networkConnection.countRequests())
120+
121+
val request = networkConnection.allRequests[0]
122+
val payload = request.payload.map
123+
124+
assertEquals("ue", payload[Parameters.EVENT])
125+
126+
val trackerVersion = payload[Parameters.TRACKER_VERSION] as String?
127+
assertTrue(trackerVersion?.startsWith("andr") ?: false)
128+
}
129+
130+
@Test
131+
@Throws(JSONException::class, InterruptedException::class)
132+
fun tracksEventWithCorrectTracker() {
133+
val eventSink1 = EventSink()
134+
val eventSink2 = EventSink()
135+
136+
createTracker("ns1", eventSink1)
137+
createTracker("ns2", eventSink2)
138+
Thread.sleep(200)
139+
140+
// track an event using the second tracker
141+
webInterface!!.trackWebViewEvent(
142+
atomicProperties = "{}",
143+
trackers = arrayOf("ns2")
144+
)
145+
Thread.sleep(200)
146+
147+
assertEquals(0, eventSink1.trackedEvents.size)
148+
assertEquals(1, eventSink2.trackedEvents.size)
149+
150+
// tracks using default tracker if not specified
151+
webInterface!!.trackWebViewEvent(atomicProperties = "{}")
152+
Thread.sleep(200)
153+
154+
assertEquals(1, eventSink1.trackedEvents.size)
155+
assertEquals(1, eventSink2.trackedEvents.size)
156+
}
157+
158+
@Test
159+
@Throws(JSONException::class, InterruptedException::class)
160+
fun tracksEventWithEntity() {
161+
val namespace = "ns" + Math.random().toString()
162+
val eventSink = EventSink()
163+
createTracker(namespace, eventSink)
164+
165+
webInterface!!.trackWebViewEvent(
166+
atomicProperties = "{}",
167+
entities = "[{\"schema\":\"iglu:com.example/etc\",\"data\":{\"key\":\"val\"}}]",
168+
trackers = arrayOf(namespace)
169+
)
170+
Thread.sleep(200)
171+
val events = eventSink.trackedEvents
172+
assertEquals(1, events.size)
173+
174+
val relevantEntities = events[0].entities.filter { it.map["schema"] == "iglu:com.example/etc" }
175+
assertEquals(1, relevantEntities.size)
176+
177+
val entityData = relevantEntities[0].map["data"] as HashMap<*, *>?
178+
assertEquals("val", entityData?.get("key"))
179+
}
180+
181+
@Test
182+
@Throws(JSONException::class, InterruptedException::class)
183+
fun addsEventNameAndSchemaForInspection() {
184+
val namespace = "ns" + Math.random().toString()
185+
val eventSink = EventSink()
186+
createTracker(namespace, eventSink)
187+
188+
webInterface!!.trackWebViewEvent(
189+
atomicProperties = "{\"eventName\":\"se\"}",
190+
selfDescribingEventData = "{\"schema\":\"iglu:etc\",\"data\":{\"key\":\"val\"}}",
191+
trackers = arrayOf(namespace)
192+
)
193+
194+
Thread.sleep(200)
195+
val events = eventSink.trackedEvents
196+
197+
assertEquals(1, events.size)
198+
assertEquals("se", events[0].name)
199+
assertEquals("iglu:etc", events[0].schema)
200+
}
201+
202+
// --- PRIVATE
203+
private val context: Context
204+
get() = InstrumentationRegistry.getInstrumentation().targetContext
205+
206+
private fun createTracker(namespace: String, eventSink: EventSink): TrackerController {
207+
val networkConfig = NetworkConfiguration(MockNetworkConnection(HttpMethod.POST, 200))
208+
return createTracker(
209+
context,
210+
namespace = namespace,
211+
network = networkConfig,
212+
configurations = arrayOf(eventSink)
213+
)
214+
}
215+
216+
private fun waitForEvents(networkConnection: MockNetworkConnection) {
217+
var i = 0
218+
while (i < 10 && networkConnection.countRequests() == 0) {
219+
Thread.sleep(1000)
220+
i++
221+
}
222+
}
223+
}

snowplow-tracker/src/main/java/com/snowplowanalytics/core/constants/Parameters.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,4 +259,11 @@ object Parameters {
259259
const val DIAGNOSTIC_ERROR_STACK = "stackTrace"
260260
const val DIAGNOSTIC_ERROR_CLASS_NAME = "className"
261261
const val DIAGNOSTIC_ERROR_EXCEPTION_NAME = "exceptionName"
262+
263+
// Page Pings (for WebView tracking)
264+
const val PING_XOFFSET_MIN = "pp_mix"
265+
const val PING_XOFFSET_MAX = "pp_max"
266+
const val PING_YOFFSET_MIN = "pp_miy"
267+
const val PING_YOFFSET_MAX = "pp_may"
268+
const val WEBVIEW_EVENT_DATA = "selfDescribingEventData"
262269
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright (c) 2015-present Snowplow Analytics Ltd. All rights reserved.
3+
*
4+
* This program is licensed to you under the Apache License Version 2.0,
5+
* and you may not use this file except in compliance with the Apache License Version 2.0.
6+
* You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
7+
*
8+
* Unless required by applicable law or agreed to in writing,
9+
* software distributed under the Apache License Version 2.0 is distributed on an
10+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
12+
*/
13+
package com.snowplowanalytics.core.event
14+
15+
import com.snowplowanalytics.core.constants.Parameters
16+
import com.snowplowanalytics.snowplow.event.AbstractEvent
17+
import com.snowplowanalytics.snowplow.payload.SelfDescribingJson
18+
19+
/**
20+
* Allows the tracking of JavaScript events from WebViews.
21+
*/
22+
class WebViewReader(
23+
val selfDescribingEventData: SelfDescribingJson? = null,
24+
val eventName: String? = null,
25+
val trackerVersion: String? = null,
26+
val useragent: String? = null,
27+
val pageUrl: String? = null,
28+
val pageTitle: String? = null,
29+
val referrer: String? = null,
30+
val category: String? = null,
31+
val action: String? = null,
32+
val label: String? = null,
33+
val property: String? = null,
34+
val value: Double? = null,
35+
val pingXOffsetMin: Int? = null,
36+
val pingXOffsetMax: Int? = null,
37+
val pingYOffsetMin: Int? = null,
38+
val pingYOffsetMax: Int? = null
39+
) : AbstractEvent() {
40+
41+
// Public methods
42+
override val dataPayload: Map<String, Any?>
43+
get() {
44+
val payload = HashMap<String, Any?>()
45+
if (selfDescribingEventData != null) payload[Parameters.WEBVIEW_EVENT_DATA] = selfDescribingEventData
46+
if (eventName != null) payload[Parameters.EVENT] = eventName
47+
if (trackerVersion != null) payload[Parameters.TRACKER_VERSION] = trackerVersion
48+
if (useragent != null) payload[Parameters.USERAGENT] = useragent
49+
if (pageUrl != null) payload[Parameters.PAGE_URL] = pageUrl
50+
if (pageTitle != null) payload[Parameters.PAGE_TITLE] = pageTitle
51+
if (referrer != null) payload[Parameters.PAGE_REFR] = referrer
52+
if (category != null) payload[Parameters.SE_CATEGORY] = category
53+
if (action != null) payload[Parameters.SE_ACTION] = action
54+
if (label != null) payload[Parameters.SE_LABEL] = label
55+
if (property != null) payload[Parameters.SE_PROPERTY] = property
56+
if (value != null) payload[Parameters.SE_VALUE] = value
57+
if (pingXOffsetMin != null) payload[Parameters.PING_XOFFSET_MIN] = pingXOffsetMin
58+
if (pingXOffsetMax != null) payload[Parameters.PING_XOFFSET_MAX] = pingXOffsetMax
59+
if (pingYOffsetMin != null) payload[Parameters.PING_YOFFSET_MIN] = pingYOffsetMin
60+
if (pingYOffsetMax != null) payload[Parameters.PING_YOFFSET_MAX] = pingYOffsetMax
61+
return payload
62+
}
63+
}

snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/TrackerEvent.kt

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ package com.snowplowanalytics.core.tracker
1414

1515
import com.snowplowanalytics.core.constants.Parameters
1616
import com.snowplowanalytics.core.constants.TrackerConstants
17+
import com.snowplowanalytics.core.event.WebViewReader
1718
import com.snowplowanalytics.core.statemachine.StateMachineEvent
1819
import com.snowplowanalytics.core.statemachine.TrackerState
1920
import com.snowplowanalytics.core.statemachine.TrackerStateSnapshot
@@ -39,6 +40,7 @@ class TrackerEvent @JvmOverloads constructor(event: Event, state: TrackerStateSn
3940
var trueTimestamp: Long?
4041
var isPrimitive = false
4142
var isService: Boolean
43+
var isWebView = false
4244

4345
init {
4446
entities = event.entities.toMutableList()
@@ -56,12 +58,20 @@ class TrackerEvent @JvmOverloads constructor(event: Event, state: TrackerStateSn
5658
}
5759

5860
isService = event is TrackerError
59-
if (event is AbstractPrimitive) {
60-
name = event.name
61-
isPrimitive = true
62-
} else {
63-
schema = (event as? AbstractSelfDescribing)?.schema
64-
isPrimitive = false
61+
when (event) {
62+
is WebViewReader -> {
63+
name = payload[Parameters.EVENT]?.toString()
64+
schema = getWebViewSchema()
65+
isWebView = true
66+
}
67+
is AbstractPrimitive -> {
68+
name = event.name
69+
isPrimitive = true
70+
}
71+
else -> {
72+
schema = (event as? AbstractSelfDescribing)?.schema
73+
isPrimitive = false
74+
}
6575
}
6676
}
6777

@@ -100,16 +110,19 @@ class TrackerEvent @JvmOverloads constructor(event: Event, state: TrackerStateSn
100110
}
101111

102112
fun wrapPropertiesToPayload(toPayload: Payload, base64Encoded: Boolean) {
103-
if (isPrimitive) {
104-
toPayload.addMap(payload)
105-
} else {
106-
wrapSelfDescribingToPayload(toPayload, base64Encoded)
113+
when {
114+
isWebView -> wrapWebViewToPayload(toPayload, base64Encoded)
115+
isPrimitive -> toPayload.addMap(payload)
116+
else -> wrapSelfDescribingEventToPayload(toPayload, base64Encoded)
107117
}
108118
}
119+
120+
private fun getWebViewSchema(): String? {
121+
val selfDescribingData = payload[Parameters.WEBVIEW_EVENT_DATA] as SelfDescribingJson?
122+
return selfDescribingData?.map?.get(Parameters.SCHEMA)?.toString()
123+
}
109124

110-
private fun wrapSelfDescribingToPayload(toPayload: Payload, base64Encoded: Boolean) {
111-
val schema = schema ?: return
112-
val data = SelfDescribingJson(schema, payload)
125+
private fun addSelfDescribingDataToPayload(toPayload: Payload, base64Encoded: Boolean, data: SelfDescribingJson) {
113126
val unstructuredEventPayload = HashMap<String?, Any?>()
114127
unstructuredEventPayload[Parameters.SCHEMA] = TrackerConstants.SCHEMA_UNSTRUCT_EVENT
115128
unstructuredEventPayload[Parameters.DATA] = data.map
@@ -120,4 +133,17 @@ class TrackerEvent @JvmOverloads constructor(event: Event, state: TrackerStateSn
120133
Parameters.UNSTRUCTURED
121134
)
122135
}
136+
137+
private fun wrapWebViewToPayload(toPayload: Payload, base64Encoded: Boolean) {
138+
val selfDescribingData = payload[Parameters.WEBVIEW_EVENT_DATA] as SelfDescribingJson?
139+
if (selfDescribingData != null) {
140+
addSelfDescribingDataToPayload(toPayload, base64Encoded, selfDescribingData)
141+
}
142+
toPayload.addMap(payload.filterNot { it.key == Parameters.WEBVIEW_EVENT_DATA })
143+
}
144+
145+
private fun wrapSelfDescribingEventToPayload(toPayload: Payload, base64Encoded: Boolean) {
146+
val schema = schema ?: return
147+
addSelfDescribingDataToPayload(toPayload, base64Encoded, SelfDescribingJson(schema, payload))
148+
}
123149
}

0 commit comments

Comments
 (0)