Skip to content

Commit

Permalink
Add API to decorate link with user/session info (close #639)
Browse files Browse the repository at this point in the history
  • Loading branch information
greg-el committed Sep 8, 2023
1 parent 9d541a5 commit f5f8690
Show file tree
Hide file tree
Showing 4 changed files with 270 additions and 25 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package com.snowplowanalytics.snowplow.tracker


import android.net.Uri
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.snowplowanalytics.core.tracker.CrossDeviceParameter
import com.snowplowanalytics.snowplow.Snowplow
import com.snowplowanalytics.snowplow.configuration.NetworkConfiguration
import com.snowplowanalytics.snowplow.configuration.SessionConfiguration
import com.snowplowanalytics.snowplow.configuration.SubjectConfiguration
import com.snowplowanalytics.snowplow.configuration.TrackerConfiguration
import com.snowplowanalytics.snowplow.controller.SessionController
import com.snowplowanalytics.snowplow.controller.TrackerController
import com.snowplowanalytics.snowplow.event.ScreenView
import com.snowplowanalytics.snowplow.network.HttpMethod
import com.snowplowanalytics.snowplow.util.TimeMeasure
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.TimeUnit


@RunWith(AndroidJUnit4::class)
class LinkDecoratorTest {
private lateinit var tracker: TrackerController
private lateinit var session: SessionController
private lateinit var userId: String
private val testLink = Uri.parse("http://example.com")
private fun matchesRegex(pattern: Regex, result: Uri) {
Assert.assertTrue(
"$result\ndoes not match expected: $pattern",
pattern.matches(result.toString())
)
}


@Before
fun before() {
tracker = getTracker()
session = tracker.session!!
userId = session.userId
}

@Test
fun testWithoutSession() {
val tracker = getTrackerNoSession()
val result = tracker.decorateLink(testLink)
Assert.assertEquals(null, result)
}

@Test
fun testDecorateUriWithNoParams() {
tracker.track(ScreenView("test"))

val pattern =
Regex("""http://example\.com\?_sp=$userId\.\d{13}\.${session.sessionId}\.decoratorTest\.mob\.subjectUserId""")
val result = tracker.decorateLink(testLink)

matchesRegex(pattern, result!!)
}

@Test
fun testDecorateUriWithExistingSpParam() {
tracker.track(ScreenView("test"))

val pattern =
Regex("""http://example\.com\?_sp=$userId\.\d{13}\.${session.sessionId}\.decoratorTest\.mob\.subjectUserId""")
val result = tracker.decorateLink(testLink.buildUpon().appendQueryParameter("_sp", "test").build())

matchesRegex(pattern, result!!)
}

@Test
fun testDecorateUriWithOtherParam() {
tracker.track(ScreenView("test"))

val pattern =
Regex("""http://example\.com\?a=b&_sp=$userId\.\d{13}\.${session.sessionId}\.decoratorTest\.mob\.subjectUserId""")
val result =
tracker.decorateLink(testLink.buildUpon().appendQueryParameter("a", "b").build())

matchesRegex(pattern, result!!)
}

@Test
fun testDecorateUriWithParameters() {
tracker.track(ScreenView("test"))

val sessionId = session.sessionId
val expectedParams = hashMapOf(
listOf(CrossDeviceParameter.SESSION_ID) to ".$sessionId",

listOf(
CrossDeviceParameter.SESSION_ID,
CrossDeviceParameter.SOURCE_ID
) to ".$sessionId.decoratorTest",

listOf(
CrossDeviceParameter.SESSION_ID,
CrossDeviceParameter.SOURCE_ID,
CrossDeviceParameter.SOURCE_PLATFORM
) to ".$sessionId.decoratorTest.mob",

listOf(
CrossDeviceParameter.SESSION_ID,
CrossDeviceParameter.SOURCE_ID,
CrossDeviceParameter.SOURCE_PLATFORM,
CrossDeviceParameter.USER_ID
) to ".$sessionId.decoratorTest.mob.subjectUserId",

listOf(
CrossDeviceParameter.SESSION_ID,
CrossDeviceParameter.SOURCE_PLATFORM,
) to ".$sessionId..mob",

listOf(
CrossDeviceParameter.SOURCE_ID,
CrossDeviceParameter.USER_ID
) to "..decoratorTest..subjectUserId",

listOf(
CrossDeviceParameter.USER_ID
) to "....subjectUserId",

emptyList<CrossDeviceParameter>() to "",
)

for ((param, spVal) in expectedParams.entries) {
val pattern =
Regex("""http://example\.com\?_sp=$userId\.\d{13}$spVal""")
val result = tracker.decorateLink(testLink, param)

matchesRegex(pattern, result!!)
}
}

fun getTracker(): TrackerController {
val context = InstrumentationRegistry.getInstrumentation().targetContext
// Setup tracker
val networkConfiguration = NetworkConfiguration("fake-url", HttpMethod.POST)

val trackerConfiguration = TrackerConfiguration("decoratorTest")
.sessionContext(true)

val subjectConfig = SubjectConfiguration().userId("subjectUserId")

val sessionConfiguration = SessionConfiguration(
TimeMeasure(6, TimeUnit.SECONDS),
TimeMeasure(30, TimeUnit.SECONDS),
)

return Snowplow.createTracker(
context,
"namespace" + Math.random(),
networkConfiguration,
trackerConfiguration,
sessionConfiguration,
subjectConfig
)
}

fun getTrackerNoSession(): TrackerController {
val context = InstrumentationRegistry.getInstrumentation().targetContext
// Setup tracker
val networkConfiguration = NetworkConfiguration("fake-url", HttpMethod.POST)

val trackerConfiguration = TrackerConfiguration("decoratorTest")
.sessionContext(false)


return Snowplow.createTracker(
context,
"namespace" + Math.random(),
networkConfiguration,
trackerConfiguration,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.snowplowanalytics.core.tracker

enum class CrossDeviceParameter {
SESSION_ID,
SOURCE_ID,
SOURCE_PLATFORM,
USER_ID,
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
*/
package com.snowplowanalytics.core.tracker

import android.net.Uri
import androidx.annotation.RestrictTo
import com.snowplowanalytics.core.Controller
import com.snowplowanalytics.core.ecommerce.EcommerceControllerImpl
Expand Down Expand Up @@ -42,7 +43,7 @@ class TrackerControllerImpl // Constructors
get() = serviceProvider.getOrMakeGlobalContextsController()
val sessionController: SessionControllerImpl
get() = serviceProvider.getOrMakeSessionController()

override val ecommerce: EcommerceControllerImpl
get() = serviceProvider.ecommerceController
override val session: SessionController?
Expand Down Expand Up @@ -70,6 +71,43 @@ class TrackerControllerImpl // Constructors
return tracker.track(event)
}


override fun decorateLink(uri: Uri, parameters: List<CrossDeviceParameter>): Uri? {
// UserId is a required parameter of `_sp`
if (this.session?.userId == null) {
return null
}

val values = hashMapOf(
CrossDeviceParameter.SESSION_ID to (this.session?.sessionId ?: ""),
CrossDeviceParameter.SOURCE_ID to this.appId,
CrossDeviceParameter.SOURCE_PLATFORM to this.devicePlatform.value,
CrossDeviceParameter.USER_ID to (this.subject.userId ?: "")
)

// Create our list of values in the required order
val spVals = listOf(
this.session?.userId, System.currentTimeMillis()
) + CrossDeviceParameter.values().map {
if (it in parameters) values[it] else ""
}


// Remove any existing `_sp` param if present
val builder = uri.buildUpon()
if (!uri.getQueryParameter("_sp").isNullOrBlank()) {
builder.clearQuery()
uri.queryParameterNames.forEach {
if (it != "_sp") builder.appendQueryParameter(it, uri.getQueryParameter(it))
}
}

return builder.appendQueryParameter(
"_sp",
spVals.joinToString(".").trimEnd('.')
).build()
}

override val version: String
get() = BuildConfig.TRACKER_LABEL
override val isTracking: Boolean
Expand All @@ -78,119 +116,119 @@ class TrackerControllerImpl // Constructors
// Getters and Setters
override val namespace: String
get() = tracker.namespace

override var appId: String
get() = tracker.appId
set(appId) {
dirtyConfig.appId = appId
tracker.appId = appId
}

override var devicePlatform: DevicePlatform
get() = tracker.platform
set(devicePlatform) {
dirtyConfig.devicePlatform = devicePlatform
tracker.platform = devicePlatform
}

override var base64encoding: Boolean
get() = tracker.base64Encoded
set(base64encoding) {
dirtyConfig.base64encoding = base64encoding
tracker.base64Encoded = base64encoding
}

override var logLevel: LogLevel
get() = tracker.logLevel
set(logLevel) {
dirtyConfig.logLevel = logLevel
tracker.logLevel = logLevel
}

override var loggerDelegate: LoggerDelegate?
get() = Logger.delegate
set(loggerDelegate) {
dirtyConfig.loggerDelegate = loggerDelegate
Logger.delegate = loggerDelegate
}

override var applicationContext: Boolean
get() = tracker.applicationContext
set(applicationContext) {
dirtyConfig.applicationContext = applicationContext
tracker.applicationContext = applicationContext
}

override var platformContext: Boolean
get() = tracker.platformContextEnabled
set(platformContext) {
dirtyConfig.platformContext = platformContext
tracker.platformContextEnabled = platformContext
}

override var geoLocationContext: Boolean
get() = tracker.geoLocationContext
set(geoLocationContext) {
dirtyConfig.geoLocationContext = geoLocationContext
tracker.geoLocationContext = geoLocationContext
}

override var sessionContext: Boolean
get() = tracker.sessionContext
set(sessionContext) {
dirtyConfig.sessionContext = sessionContext
tracker.sessionContext = sessionContext
}

override var deepLinkContext: Boolean
get() = tracker.deepLinkContext
set(deepLinkContext) {
dirtyConfig.deepLinkContext = deepLinkContext
tracker.deepLinkContext = deepLinkContext
}

override var screenContext: Boolean
get() = tracker.screenContext
set(screenContext) {
dirtyConfig.screenContext = screenContext
tracker.screenContext = screenContext
}

override var screenViewAutotracking: Boolean
get() = tracker.screenViewAutotracking
set(screenViewAutotracking) {
dirtyConfig.screenViewAutotracking = screenViewAutotracking
tracker.screenViewAutotracking = screenViewAutotracking
}

override var lifecycleAutotracking: Boolean
get() = tracker.lifecycleAutotracking
set(lifecycleAutotracking) {
dirtyConfig.lifecycleAutotracking = lifecycleAutotracking
tracker.lifecycleAutotracking = lifecycleAutotracking
}

override var installAutotracking: Boolean
get() = tracker.installAutotracking
set(installAutotracking) {
dirtyConfig.installAutotracking = installAutotracking
tracker.installAutotracking = installAutotracking
}

override var exceptionAutotracking: Boolean
get() = tracker.exceptionAutotracking
set(exceptionAutotracking) {
dirtyConfig.exceptionAutotracking = exceptionAutotracking
tracker.exceptionAutotracking = exceptionAutotracking
}

override var diagnosticAutotracking: Boolean
get() = tracker.diagnosticAutotracking
set(diagnosticAutotracking) {
dirtyConfig.diagnosticAutotracking = diagnosticAutotracking
tracker.diagnosticAutotracking = diagnosticAutotracking
}

override var userAnonymisation: Boolean
get() = tracker.userAnonymisation
set(userAnonymisation) {
Expand Down
Loading

0 comments on commit f5f8690

Please sign in to comment.