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 3b27a0a commit 54ca7a8
Show file tree
Hide file tree
Showing 4 changed files with 270 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
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

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

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,29 @@
package com.snowplowanalytics.core.tracker

import com.snowplowanalytics.snowplow.controller.SessionController
import com.snowplowanalytics.snowplow.controller.TrackerController

/**
* The optional parameters to include in the query string added by [TrackerController.decorateLink]
*/
enum class CrossDeviceParameter {
/**
* Value of [SessionController.sessionId]
*/
SESSION_ID,

/**
* Value of [Tracker.appId]
*/
SOURCE_ID,

/**
* Value of [Tracker.platform]
*/
SOURCE_PLATFORM,

/**
* Value of [Subject.userId]
*/
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 @@ -70,6 +71,42 @@ 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(crossDeviceQueryParameterKey).isNullOrBlank()) {
builder.clearQuery()
uri.queryParameterNames.forEach {
if (it != crossDeviceQueryParameterKey) builder.appendQueryParameter(it, uri.getQueryParameter(it))
}
}

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

override val version: String
get() = BuildConfig.TRACKER_LABEL
override val isTracking: Boolean
Expand Down Expand Up @@ -219,6 +256,8 @@ class TrackerControllerImpl // Constructors
private val dirtyConfig: TrackerConfiguration
get() = serviceProvider.trackerConfiguration

private val crossDeviceQueryParameterKey = "_sp"

companion object {
private val TAG = TrackerControllerImpl::class.java.simpleName
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
*/
package com.snowplowanalytics.snowplow.controller

import android.net.Uri
import com.snowplowanalytics.core.session.Session
import com.snowplowanalytics.core.tracker.CrossDeviceParameter
import com.snowplowanalytics.core.tracker.TrackerConfigurationInterface
import com.snowplowanalytics.snowplow.configuration.TrackerConfiguration
import com.snowplowanalytics.snowplow.ecommerce.EcommerceController
import com.snowplowanalytics.snowplow.event.Event
import com.snowplowanalytics.snowplow.media.controller.MediaController
Expand Down Expand Up @@ -116,4 +120,23 @@ interface TrackerController : TrackerConfigurationInterface {
* The tracker will start tracking again.
*/
fun resume()

/**
* Adds user and session information to a URI.
*
* For example, calling decorateLink on `appSchema://path/to/page` will return:
*
* `appSchema://path/to/page?_sp=userId.timestamp.sessionId.appId.platform.domainUserId`
*
* @param uri The URI to add the query string to
* @param parameters The parameters to include in the query string (defaults to all)
*
* @return Optional Uri:
* - null if [Session.userId] is null from `sessionContext(false)` being passed in [TrackerConfiguration]
* - otherwise, decorated Uri
*/
fun decorateLink(
uri: Uri,
parameters: List<CrossDeviceParameter> = CrossDeviceParameter.values().toList()
): Uri?
}

0 comments on commit 54ca7a8

Please sign in to comment.