Skip to content

Commit

Permalink
Add session replay staging config to Sample App
Browse files Browse the repository at this point in the history
  • Loading branch information
mariusc83 committed Oct 3, 2022
1 parent f41ab89 commit 294ab95
Show file tree
Hide file tree
Showing 14 changed files with 319 additions and 133 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ fun configureFlavorForSampleApp(flavor: ApplicationProductFlavor, rootDir: File)
"DD_OVERRIDE_RUM_URL",
"\"${config.rumEndpoint}\""
)
flavor.buildConfigField(
"String",
"DD_OVERRIDE_SESSION_REPLAY_URL",
"\"${config.sessionReplayEndpoint}\""
)
flavor.buildConfigField(
"String",
"DD_RUM_APPLICATION_ID",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ data class SampleAppConfig(
val logsEndpoint: String = "",
val tracesEndpoint: String = "",
val rumEndpoint: String = "",
val sessionReplayEndpoint: String = "",
val token: String = "",
val rumApplicationId: String = "",
val apiKey: String = "",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,10 @@ internal abstract class DataOkHttpUploaderV2(
internal enum class TrackType(val trackName: String) {
LOGS("logs"),
RUM("rum"),
SPANS("spans"),
SESSION_REPLAY("replay")
SPANS("spans")
}

internal val uploaderName = javaClass.simpleName
private val uploaderName = javaClass.simpleName

internal val clientToken = if (isValidHeaderValue(rawClientToken)) rawClientToken else ""
internal val source: String = sanitizeHeaderValue(rawSource)
Expand Down Expand Up @@ -113,7 +112,7 @@ internal abstract class DataOkHttpUploaderV2(
return builder.build()
}

internal fun buildUrl(): String {
private fun buildUrl(): String {
val queryParams = buildQueryParameters()
return if (queryParams.isEmpty()) {
intakeUrl
Expand All @@ -122,7 +121,7 @@ internal abstract class DataOkHttpUploaderV2(
}
}

internal fun buildHeaders(builder: Request.Builder, requestId: String) {
private fun buildHeaders(builder: Request.Builder, requestId: String) {
builder.addHeader(HEADER_API_KEY, clientToken)
builder.addHeader(HEADER_EVP_ORIGIN, source)
builder.addHeader(HEADER_EVP_ORIGIN_VERSION, sdkVersion)
Expand All @@ -135,7 +134,7 @@ internal abstract class DataOkHttpUploaderV2(
return emptyMap()
}

internal fun responseCodeToUploadStatus(code: Int): UploadStatus {
private fun responseCodeToUploadStatus(code: Int): UploadStatus {
return when (code) {
HTTP_ACCEPTED -> UploadStatus.SUCCESS
HTTP_BAD_REQUEST -> UploadStatus.HTTP_CLIENT_ERROR
Expand Down Expand Up @@ -192,7 +191,6 @@ internal abstract class DataOkHttpUploaderV2(

internal const val CONTENT_TYPE_JSON = "application/json"
internal const val CONTENT_TYPE_TEXT_UTF8 = "text/plain;charset=UTF-8"
internal const val CONTENT_TYPE_MUTLIPART_FORM = "multipart/form-data"

private const val UPLOAD_URL = "%s/api/v2/%s"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,7 @@ internal class SessionReplayFeature(
return SessionReplayRequestFactory(
SessionReplayOkHttpUploader(
configuration.endpointUrl,
coreFeature.clientToken,
coreFeature.sourceName,
coreFeature.sdkVersion,
coreFeature.okHttpClient,
coreFeature.androidInfoProvider,
coreFeature
coreFeature.okHttpClient
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
package com.datadog.android.sessionreplay.internal.domain

import com.datadog.android.core.internal.net.DataOkHttpUploaderV2
import com.datadog.android.sessionreplay.internal.net.BatchesToSegmentsMapper
import com.datadog.android.sessionreplay.internal.net.SessionReplayOkHttpUploader
import com.datadog.android.sessionreplay.net.BatchesToSegmentsMapper
import com.datadog.android.v2.api.Request
import com.datadog.android.v2.api.RequestFactory
import com.datadog.android.v2.api.context.DatadogContext
Expand All @@ -27,17 +27,17 @@ internal class SessionReplayRequestFactory(
// Also add the necessary unit tests once this is done.
batchToSegmentsMapper.map(batchData).forEach {
sessionReplayOkHttpUploader.upload(
context,
it.first,
(it.second.toString() + "\n").toByteArray()
)
}
return Request(
sessionReplayOkHttpUploader.requestId,
"",
sessionReplayOkHttpUploader.buildUrl(),
sessionReplayOkHttpUploader.buildUrl(context),
mapOf(
DataOkHttpUploaderV2.HEADER_API_KEY
to sessionReplayOkHttpUploader.clientToken
DataOkHttpUploaderV2.HEADER_API_KEY to context.clientToken
),
ByteArray(0)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@

package com.datadog.android.sessionreplay.internal.net

import com.datadog.android.core.internal.CoreFeature
import com.datadog.android.core.internal.net.DataOkHttpUploaderV2
import com.datadog.android.core.internal.net.UploadStatus
import com.datadog.android.core.internal.system.AndroidInfoProvider
import com.datadog.android.core.internal.utils.devLogger
import com.datadog.android.core.internal.utils.sdkLogger
import com.datadog.android.log.Logger
import com.datadog.android.rum.RumAttributes
import com.datadog.android.sessionreplay.model.MobileSegment
import com.datadog.android.sessionreplay.net.BytesCompressor
import com.datadog.android.v2.api.context.DatadogContext
import java.util.Locale
import java.util.UUID
import okhttp3.Call
import okhttp3.MediaType
import okhttp3.MultipartBody
Expand All @@ -24,44 +26,64 @@ import okhttp3.RequestBody
// instead from SessionReplayRequestFactory
// This class is not test as it is meant for non - production usage. It will be dropped later.
internal class SessionReplayOkHttpUploader(
endpoint: String,
clientToken: String,
source: String,
sdkVersion: String,
callFactory: Call.Factory,
androidInfoProvider: AndroidInfoProvider,
private val coreFeature: CoreFeature,
private val endpoint: String,
internal val callFactory: Call.Factory,
internal val internalLogger: Logger = sdkLogger,
private val compressor: BytesCompressor = BytesCompressor()
) : DataOkHttpUploaderV2(
buildUrl(endpoint, TrackType.SESSION_REPLAY),
clientToken,
source,
sdkVersion,
callFactory,
CONTENT_TYPE_MUTLIPART_FORM,
androidInfoProvider,
sdkLogger
) {

private val tags: String
get() {
val elements = mutableListOf(
"${RumAttributes.SERVICE_NAME}:${coreFeature.serviceName}",
"${RumAttributes.APPLICATION_VERSION}:" +
coreFeature.packageVersionProvider.version,
"${RumAttributes.SDK_VERSION}:$sdkVersion",
"${RumAttributes.ENV}:${coreFeature.envName}"
)
if (coreFeature.variant.isNotEmpty()) {
elements.add("${RumAttributes.VARIANT}:${coreFeature.variant}")
private val uploaderName = javaClass.simpleName

internal val requestId: String by lazy {
UUID.randomUUID().toString()
}

private fun userAgent(datadogContext: DatadogContext): String {
return sanitizeHeaderValue(System.getProperty(SYSTEM_UA))
.ifBlank {
"Datadog/${sanitizeHeaderValue(datadogContext.sdkVersion)} " +
"(Linux; U; Android ${datadogContext.deviceInfo.osVersion}; " +
"${datadogContext.deviceInfo.deviceModel} " +
"Build/${datadogContext.deviceInfo.deviceBuildId})"
}
return elements.joinToString(",")
}

private val intakeUrl by lazy {
String.format(
Locale.US,
UPLOAD_URL,
endpoint,
"replay"
)
}

private fun tags(datadogContext: DatadogContext): String {
val elements = mutableListOf(
"${RumAttributes.SERVICE_NAME}:${datadogContext.service}",
"${RumAttributes.APPLICATION_VERSION}:" +
datadogContext.version,
"${RumAttributes.SDK_VERSION}:${datadogContext.sdkVersion}",
"${RumAttributes.ENV}:${datadogContext.env}"
)
if (datadogContext.variant.isNotEmpty()) {
elements.add("${RumAttributes.VARIANT}:${datadogContext.variant}")
}
return elements.joinToString(",")
}

@Suppress("TooGenericExceptionCaught")
fun upload(mobileSegment: MobileSegment, mobileSegmentAsBinary: ByteArray): UploadStatus {
fun upload(
datadogContext: DatadogContext,
mobileSegment: MobileSegment,
mobileSegmentAsBinary: ByteArray
): UploadStatus {
val uploadStatus = try {
executeUploadRequest(mobileSegment, mobileSegmentAsBinary, requestId)
executeUploadRequest(
datadogContext,
mobileSegment,
mobileSegmentAsBinary,
requestId
)
} catch (e: Throwable) {
internalLogger.e("Unable to upload batch data.", e)
UploadStatus.NETWORK_ERROR
Expand All @@ -87,16 +109,19 @@ internal class SessionReplayOkHttpUploader(
return uploadStatus
}

// region Internal

@Suppress("UnsafeThirdPartyFunctionCall") // Called within a try/catch block
private fun executeUploadRequest(
datadogContext: DatadogContext,
mobileSegment: MobileSegment,
mobileSegmentAsBinary: ByteArray,
requestId: String
): UploadStatus {
if (clientToken.isBlank()) {
if (datadogContext.clientToken.isBlank()) {
return UploadStatus.INVALID_TOKEN_ERROR
}
val request = buildRequest(mobileSegment, mobileSegmentAsBinary, requestId)
val request = buildRequest(datadogContext, mobileSegment, mobileSegmentAsBinary, requestId)
val call = callFactory.newCall(request)
val response = call.execute()
response.close()
Expand All @@ -105,15 +130,16 @@ internal class SessionReplayOkHttpUploader(

@Suppress("UnsafeThirdPartyFunctionCall") // Called within a try/catch block
private fun buildRequest(
datadogContext: DatadogContext,
segment: MobileSegment,
segmentAsBinary: ByteArray,
requestId: String
): Request {
val builder = Request.Builder()
.url(buildUrl())
.url(intakeUrl)
.post(buildRequestBody(segment, segmentAsBinary))

buildHeaders(builder, requestId)
buildHeaders(datadogContext, builder, requestId)

return builder.build()
}
Expand All @@ -131,6 +157,11 @@ internal class SessionReplayOkHttpUploader(
compressedData
)
)
.addFormDataPart(
SEGMENT_FORM_KEY,
segment.session.id

)
.addFormDataPart(
APPLICATION_ID_FORM_KEY,
segment.application.id
Expand Down Expand Up @@ -170,17 +201,91 @@ internal class SessionReplayOkHttpUploader(
.build()
}

override fun buildQueryParameters(): Map<String, Any> {
internal fun buildUrl(datadogContext: DatadogContext): String {
val queryParams = buildQueryParameters(datadogContext)
return if (queryParams.isEmpty()) {
intakeUrl
} else {
intakeUrl + queryParams.map { "${it.key}=${it.value}" }
.joinToString("&", prefix = "?")
}
}

private fun buildHeaders(
datadogContext: DatadogContext,
builder: Request.Builder,
requestId: String
) {
builder.addHeader(HEADER_API_KEY, datadogContext.clientToken)
builder.addHeader(HEADER_EVP_ORIGIN, datadogContext.source)
builder.addHeader(HEADER_EVP_ORIGIN_VERSION, datadogContext.sdkVersion)
builder.addHeader(HEADER_USER_AGENT, userAgent(datadogContext))
builder.addHeader(HEADER_CONTENT_TYPE, CONTENT_TYPE_MULTIPART_FORM)
builder.addHeader(HEADER_REQUEST_ID, requestId)
}

private fun responseCodeToUploadStatus(code: Int): UploadStatus {
return when (code) {
HTTP_ACCEPTED -> UploadStatus.SUCCESS
HTTP_BAD_REQUEST -> UploadStatus.HTTP_CLIENT_ERROR
HTTP_UNAUTHORIZED -> UploadStatus.INVALID_TOKEN_ERROR
HTTP_FORBIDDEN -> UploadStatus.INVALID_TOKEN_ERROR
HTTP_CLIENT_TIMEOUT -> UploadStatus.HTTP_CLIENT_RATE_LIMITING
HTTP_ENTITY_TOO_LARGE -> UploadStatus.HTTP_CLIENT_ERROR
HTTP_TOO_MANY_REQUESTS -> UploadStatus.HTTP_CLIENT_RATE_LIMITING
HTTP_INTERNAL_ERROR -> UploadStatus.HTTP_SERVER_ERROR
HTTP_UNAVAILABLE -> UploadStatus.HTTP_SERVER_ERROR
else -> UploadStatus.UNKNOWN_ERROR
}
}

private fun sanitizeHeaderValue(value: String?): String {
return value?.filter { isValidHeaderValueChar(it) }.orEmpty()
}

private fun isValidHeaderValueChar(c: Char): Boolean {
return c == '\t' || c in '\u0020' until '\u007F'
}

private fun buildQueryParameters(datadogContext: DatadogContext): Map<String, Any> {
return mapOf(
QUERY_PARAM_SOURCE to source,
QUERY_PARAM_TAGS to tags,
QUERY_PARAM_EVP_ORIGIN_KEY to source,
QUERY_PARAM_EVP_ORIGIN_VERSION_KEY to sdkVersion
QUERY_PARAM_SOURCE to datadogContext.source,
QUERY_PARAM_TAGS to tags(datadogContext),
QUERY_PARAM_EVP_ORIGIN_KEY to datadogContext.source,
QUERY_PARAM_EVP_ORIGIN_VERSION_KEY to datadogContext.sdkVersion
)
}

// endregion

companion object {

const val SYSTEM_UA = "http.agent"

const val HTTP_ACCEPTED = 202

const val HTTP_BAD_REQUEST = 400
const val HTTP_UNAUTHORIZED = 401
const val HTTP_FORBIDDEN = 403
const val HTTP_CLIENT_TIMEOUT = 408
const val HTTP_ENTITY_TOO_LARGE = 413
const val HTTP_TOO_MANY_REQUESTS = 429

const val HTTP_INTERNAL_ERROR = 500
const val HTTP_UNAVAILABLE = 503

internal const val HEADER_API_KEY = "DD-API-KEY"
internal const val HEADER_EVP_ORIGIN = "DD-EVP-ORIGIN"
internal const val HEADER_EVP_ORIGIN_VERSION = "DD-EVP-ORIGIN-VERSION"
internal const val HEADER_REQUEST_ID = "DD-REQUEST-ID"
internal const val HEADER_CONTENT_TYPE = "Content-Type"
internal const val HEADER_USER_AGENT = "User-Agent"

internal const val QUERY_PARAM_SOURCE = "ddsource"
internal const val QUERY_PARAM_TAGS = "ddtags"

private const val UPLOAD_URL = "%s/api/v2/%s"

internal const val QUERY_PARAM_EVP_ORIGIN_VERSION_KEY = "dd-evp-origin-version"
internal const val QUERY_PARAM_EVP_ORIGIN_KEY = "dd-evp-origin"
internal const val APPLICATION_ID_FORM_KEY = "application.id"
Expand All @@ -194,5 +299,8 @@ internal class SessionReplayOkHttpUploader(
internal const val SOURCE_FORM_KEY = "end"
internal const val SEGMENT_FORM_KEY = "segment"
internal const val CONTENT_TYPE_BINARY = "application/octet-stream"
internal const val CONTENT_TYPE_MULTIPART_FORM = "multipart/form-data"
private const val HEADER_ENCODING = "Content-Encoding"
private const val ENCODING_GZIP = "gzip"
}
}
Loading

0 comments on commit 294ab95

Please sign in to comment.