Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release/6.1.0 #702

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
Version 6.1.0 (2025-01-16)
--------------------------
Add new WebView interface (#700)

Version 6.0.6 (2024-09-12)
--------------------------
Set negative battery levels to null in mobile context (#698)
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
6.0.6
6.1.0
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ plugins {

subprojects {
group = 'com.snowplowanalytics'
version = '6.0.6'
version = '6.1.0'
repositories {
google()
maven {
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ systemProp.org.gradle.internal.http.socketTimeout=120000
SONATYPE_STAGING_PROFILE=comsnowplowanalytics
GROUP=com.snowplowanalytics
POM_ARTIFACT_ID=snowplow-android-tracker
VERSION_NAME=6.0.6
VERSION_NAME=6.1.0

POM_NAME=snowplow-android-tracker
POM_PACKAGING=aar
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
/*
* Copyright (c) 2015-present Snowplow Analytics Ltd. All rights reserved.
*
* This program is licensed to you under the Apache License Version 2.0,
* and you may not use this file except in compliance with the Apache License Version 2.0.
* You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the Apache License Version 2.0 is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
*/

package com.snowplowanalytics.snowplow.tracker

import android.content.Context
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.snowplowanalytics.core.constants.Parameters
import com.snowplowanalytics.core.constants.TrackerConstants
import com.snowplowanalytics.core.emitter.Executor
import com.snowplowanalytics.core.tracker.TrackerWebViewInterfaceV2
import com.snowplowanalytics.snowplow.Snowplow.createTracker
import com.snowplowanalytics.snowplow.Snowplow.removeAllTrackers
import com.snowplowanalytics.snowplow.configuration.NetworkConfiguration
import com.snowplowanalytics.snowplow.configuration.TrackerConfiguration
import com.snowplowanalytics.snowplow.controller.TrackerController
import com.snowplowanalytics.snowplow.network.HttpMethod
import com.snowplowanalytics.snowplow.util.EventSink
import org.json.JSONException
import org.json.JSONObject
import org.junit.After
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class TrackerWebViewInterfaceV2Test {
private var webInterface: TrackerWebViewInterfaceV2? = null

@Before
fun setUp() {
webInterface = TrackerWebViewInterfaceV2()
}

@After
fun tearDown() {
removeAllTrackers()
Executor.shutdown()
}

@Test
@Throws(JSONException::class, InterruptedException::class)
fun tracksEventWithAllOptions() {
val networkConnection = MockNetworkConnection(HttpMethod.GET, 200)
createTracker(
context,
"ns${Math.random()}",
NetworkConfiguration(networkConnection),
TrackerConfiguration("appId").base64encoding(false)
)

val data = "{\"schema\":\"iglu:etc\",\"data\":{\"key\":\"val\"}}"
val atomic = "{\"eventName\":\"pv\",\"trackerVersion\":\"webview\"," +
"\"useragent\":\"Chrome\",\"pageUrl\":\"http://snowplow.com\"," +
"\"pageTitle\":\"Snowplow\",\"referrer\":\"http://google.com\"," +
"\"pingXOffsetMin\":10,\"pingXOffsetMax\":20,\"pingYOffsetMin\":30," +
"\"pingYOffsetMax\":40,\"category\":\"cat\",\"action\":\"act\"," +
"\"property\":\"prop\",\"label\":\"lbl\",\"value\":10.0}"

webInterface!!.trackWebViewEvent(
selfDescribingEventData = data,
atomicProperties = atomic
)

waitForEvents(networkConnection)
assertEquals(1, networkConnection.countRequests())

val request = networkConnection.allRequests[0]
val payload = request.payload.map

assertEquals("pv", payload[Parameters.EVENT])
assertEquals("webview", payload[Parameters.TRACKER_VERSION])
assertEquals("Chrome", payload[Parameters.USERAGENT])
assertEquals("http://snowplow.com", payload[Parameters.PAGE_URL])
assertEquals("Snowplow", payload[Parameters.PAGE_TITLE])
assertEquals("http://google.com", payload[Parameters.PAGE_REFR])
assertEquals("10", payload[Parameters.PING_XOFFSET_MIN])
assertEquals("20", payload[Parameters.PING_XOFFSET_MAX])
assertEquals("30", payload[Parameters.PING_YOFFSET_MIN])
assertEquals("40", payload[Parameters.PING_YOFFSET_MAX])
assertEquals("cat", payload[Parameters.SE_CATEGORY])
assertEquals("act", payload[Parameters.SE_ACTION])
assertEquals("prop", payload[Parameters.SE_PROPERTY])
assertEquals("lbl", payload[Parameters.SE_LABEL])
assertEquals("10.0", payload[Parameters.SE_VALUE])

assertTrue(payload.containsKey(Parameters.UNSTRUCTURED))
val selfDescJson = JSONObject(payload[Parameters.UNSTRUCTURED] as String)
assertEquals(TrackerConstants.SCHEMA_UNSTRUCT_EVENT, selfDescJson.getString("schema"))
assertEquals(data, selfDescJson.getString("data"))
}

@Test
@Throws(JSONException::class, InterruptedException::class)
fun addsDefaultPropertiesIfNotProvided() {
val networkConnection = MockNetworkConnection(HttpMethod.GET, 200)
createTracker(
context,
"ns${Math.random()}",
NetworkConfiguration(networkConnection),
TrackerConfiguration("appId").base64encoding(false)
)

webInterface!!.trackWebViewEvent(atomicProperties = "{}")

waitForEvents(networkConnection)
assertEquals(1, networkConnection.countRequests())

val request = networkConnection.allRequests[0]
val payload = request.payload.map

assertEquals("ue", payload[Parameters.EVENT])

val trackerVersion = payload[Parameters.TRACKER_VERSION] as String?
assertTrue(trackerVersion?.startsWith("andr") ?: false)
}

@Test
@Throws(JSONException::class, InterruptedException::class)
fun tracksEventWithCorrectTracker() {
val eventSink1 = EventSink()
val eventSink2 = EventSink()

createTracker("ns1", eventSink1)
createTracker("ns2", eventSink2)
Thread.sleep(200)

// track an event using the second tracker
webInterface!!.trackWebViewEvent(
atomicProperties = "{}",
trackers = arrayOf("ns2")
)
Thread.sleep(200)

assertEquals(0, eventSink1.trackedEvents.size)
assertEquals(1, eventSink2.trackedEvents.size)

// tracks using default tracker if not specified
webInterface!!.trackWebViewEvent(atomicProperties = "{}")
Thread.sleep(200)

assertEquals(1, eventSink1.trackedEvents.size)
assertEquals(1, eventSink2.trackedEvents.size)
}

@Test
@Throws(JSONException::class, InterruptedException::class)
fun tracksEventWithEntity() {
val namespace = "ns" + Math.random().toString()
val eventSink = EventSink()
createTracker(namespace, eventSink)

webInterface!!.trackWebViewEvent(
atomicProperties = "{}",
entities = "[{\"schema\":\"iglu:com.example/etc\",\"data\":{\"key\":\"val\"}}]",
trackers = arrayOf(namespace)
)
Thread.sleep(200)
val events = eventSink.trackedEvents
assertEquals(1, events.size)

val relevantEntities = events[0].entities.filter { it.map["schema"] == "iglu:com.example/etc" }
assertEquals(1, relevantEntities.size)

val entityData = relevantEntities[0].map["data"] as HashMap<*, *>?
assertEquals("val", entityData?.get("key"))
}

@Test
@Throws(JSONException::class, InterruptedException::class)
fun addsEventNameAndSchemaForInspection() {
val namespace = "ns" + Math.random().toString()
val eventSink = EventSink()
createTracker(namespace, eventSink)

webInterface!!.trackWebViewEvent(
atomicProperties = "{\"eventName\":\"se\"}",
selfDescribingEventData = "{\"schema\":\"iglu:etc\",\"data\":{\"key\":\"val\"}}",
trackers = arrayOf(namespace)
)

Thread.sleep(200)
val events = eventSink.trackedEvents

assertEquals(1, events.size)
assertEquals("se", events[0].name)
assertEquals("iglu:etc", events[0].schema)
}

// --- PRIVATE
private val context: Context
get() = InstrumentationRegistry.getInstrumentation().targetContext

private fun createTracker(namespace: String, eventSink: EventSink): TrackerController {
val networkConfig = NetworkConfiguration(MockNetworkConnection(HttpMethod.POST, 200))
return createTracker(
context,
namespace = namespace,
network = networkConfig,
configurations = arrayOf(eventSink)
)
}

private fun waitForEvents(networkConnection: MockNetworkConnection) {
var i = 0
while (i < 10 && networkConnection.countRequests() == 0) {
Thread.sleep(1000)
i++
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -259,4 +259,11 @@ object Parameters {
const val DIAGNOSTIC_ERROR_STACK = "stackTrace"
const val DIAGNOSTIC_ERROR_CLASS_NAME = "className"
const val DIAGNOSTIC_ERROR_EXCEPTION_NAME = "exceptionName"

// Page Pings (for WebView tracking)
const val PING_XOFFSET_MIN = "pp_mix"
const val PING_XOFFSET_MAX = "pp_max"
const val PING_YOFFSET_MIN = "pp_miy"
const val PING_YOFFSET_MAX = "pp_may"
const val WEBVIEW_EVENT_DATA = "selfDescribingEventData"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright (c) 2015-present Snowplow Analytics Ltd. All rights reserved.
*
* This program is licensed to you under the Apache License Version 2.0,
* and you may not use this file except in compliance with the Apache License Version 2.0.
* You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the Apache License Version 2.0 is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
*/
package com.snowplowanalytics.core.event

import com.snowplowanalytics.core.constants.Parameters
import com.snowplowanalytics.snowplow.event.AbstractEvent
import com.snowplowanalytics.snowplow.payload.SelfDescribingJson

/**
* Allows the tracking of JavaScript events from WebViews.
*/
class WebViewReader(
val selfDescribingEventData: SelfDescribingJson? = null,
val eventName: String? = null,
val trackerVersion: String? = null,
val useragent: String? = null,
val pageUrl: String? = null,
val pageTitle: String? = null,
val referrer: String? = null,
val category: String? = null,
val action: String? = null,
val label: String? = null,
val property: String? = null,
val value: Double? = null,
val pingXOffsetMin: Int? = null,
val pingXOffsetMax: Int? = null,
val pingYOffsetMin: Int? = null,
val pingYOffsetMax: Int? = null
) : AbstractEvent() {

// Public methods
override val dataPayload: Map<String, Any?>
get() {
val payload = HashMap<String, Any?>()
if (selfDescribingEventData != null) payload[Parameters.WEBVIEW_EVENT_DATA] = selfDescribingEventData
if (eventName != null) payload[Parameters.EVENT] = eventName
if (trackerVersion != null) payload[Parameters.TRACKER_VERSION] = trackerVersion
if (useragent != null) payload[Parameters.USERAGENT] = useragent
if (pageUrl != null) payload[Parameters.PAGE_URL] = pageUrl
if (pageTitle != null) payload[Parameters.PAGE_TITLE] = pageTitle
if (referrer != null) payload[Parameters.PAGE_REFR] = referrer
if (category != null) payload[Parameters.SE_CATEGORY] = category
if (action != null) payload[Parameters.SE_ACTION] = action
if (label != null) payload[Parameters.SE_LABEL] = label
if (property != null) payload[Parameters.SE_PROPERTY] = property
if (value != null) payload[Parameters.SE_VALUE] = value
if (pingXOffsetMin != null) payload[Parameters.PING_XOFFSET_MIN] = pingXOffsetMin
if (pingXOffsetMax != null) payload[Parameters.PING_XOFFSET_MAX] = pingXOffsetMax
if (pingYOffsetMin != null) payload[Parameters.PING_YOFFSET_MIN] = pingYOffsetMin
if (pingYOffsetMax != null) payload[Parameters.PING_YOFFSET_MAX] = pingYOffsetMax
return payload
}
}
Loading
Loading