Skip to content

Commit 526ab4a

Browse files
committed
Merge branch 'release/6.1.0'
2 parents 05fdaa6 + f60c7db commit 526ab4a

File tree

10 files changed

+478
-16
lines changed

10 files changed

+478
-16
lines changed

CHANGELOG

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
Version 6.1.0 (2025-01-16)
2+
--------------------------
3+
Add new WebView interface (#700)
4+
15
Version 6.0.6 (2024-09-12)
26
--------------------------
37
Set negative battery levels to null in mobile context (#698)

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
6.0.6
1+
6.1.0

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ plugins {
2222

2323
subprojects {
2424
group = 'com.snowplowanalytics'
25-
version = '6.0.6'
25+
version = '6.1.0'
2626
repositories {
2727
google()
2828
maven {

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ systemProp.org.gradle.internal.http.socketTimeout=120000
3131
SONATYPE_STAGING_PROFILE=comsnowplowanalytics
3232
GROUP=com.snowplowanalytics
3333
POM_ARTIFACT_ID=snowplow-android-tracker
34-
VERSION_NAME=6.0.6
34+
VERSION_NAME=6.1.0
3535

3636
POM_NAME=snowplow-android-tracker
3737
POM_PACKAGING=aar
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+
}

0 commit comments

Comments
 (0)