Skip to content

Commit bdd9db5

Browse files
authored
[SR] Capture network requests
2 parents 33cd776 + 4f240d4 commit bdd9db5

File tree

14 files changed

+505
-65
lines changed

14 files changed

+505
-65
lines changed

sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BaseCaptureStrategy.kt

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.sentry.android.replay.capture
22

3+
import io.sentry.Breadcrumb
34
import io.sentry.DateUtils
45
import io.sentry.Hint
56
import io.sentry.IHub
@@ -8,6 +9,7 @@ import io.sentry.SentryOptions
89
import io.sentry.SentryReplayEvent
910
import io.sentry.SentryReplayEvent.ReplayType
1011
import io.sentry.SentryReplayEvent.ReplayType.SESSION
12+
import io.sentry.SpanDataConvention
1113
import io.sentry.android.replay.ReplayCache
1214
import io.sentry.android.replay.ScreenshotRecorderConfig
1315
import io.sentry.android.replay.util.gracefullyShutdown
@@ -16,6 +18,7 @@ import io.sentry.protocol.SentryId
1618
import io.sentry.rrweb.RRWebBreadcrumbEvent
1719
import io.sentry.rrweb.RRWebEvent
1820
import io.sentry.rrweb.RRWebMetaEvent
21+
import io.sentry.rrweb.RRWebSpanEvent
1922
import io.sentry.rrweb.RRWebVideoEvent
2023
import io.sentry.transport.ICurrentDateProvider
2124
import io.sentry.util.FileUtils
@@ -39,6 +42,15 @@ internal abstract class BaseCaptureStrategy(
3942

4043
internal companion object {
4144
private const val TAG = "CaptureStrategy"
45+
private val snakecasePattern = "_[a-z]".toRegex()
46+
private val supportedNetworkData = setOf(
47+
"status_code",
48+
"method",
49+
"response_content_length",
50+
"request_content_length",
51+
"http.response_content_length",
52+
"http.request_content_length"
53+
)
4254
}
4355

4456
protected var cache: ReplayCache? = null
@@ -74,7 +86,8 @@ internal abstract class BaseCaptureStrategy(
7486
}
7587
}
7688

77-
cache = replayCacheProvider?.invoke(replayId) ?: ReplayCache(options, replayId, recorderConfig)
89+
cache =
90+
replayCacheProvider?.invoke(replayId) ?: ReplayCache(options, replayId, recorderConfig)
7891

7992
// TODO: replace it with dateProvider.currentTimeMillis to also test it
8093
segmentTimestamp.set(DateUtils.getCurrentDateTime())
@@ -180,7 +193,12 @@ internal abstract class BaseCaptureStrategy(
180193
val breadcrumbCategory: String?
181194
val breadcrumbData = mutableMapOf<String, Any?>()
182195
when {
183-
breadcrumb.category == "http" -> return@forEach
196+
breadcrumb.category == "http" -> {
197+
if (breadcrumb.isValidForRRWebSpan()) {
198+
recordingPayload += breadcrumb.toRRWebSpanEvent()
199+
}
200+
return@forEach
201+
}
184202

185203
breadcrumb.category == "device.orientation" -> {
186204
breadcrumbCategory = breadcrumb.category!!
@@ -208,7 +226,8 @@ internal abstract class BaseCaptureStrategy(
208226

209227
breadcrumb.type == "system" -> {
210228
breadcrumbCategory = breadcrumb.type!!
211-
breadcrumbMessage = breadcrumb.data.entries.joinToString() as? String ?: ""
229+
breadcrumbMessage =
230+
breadcrumb.data.entries.joinToString() as? String ?: ""
212231
}
213232

214233
else -> {
@@ -280,4 +299,40 @@ internal abstract class BaseCaptureStrategy(
280299
}
281300
}
282301
}
302+
303+
private fun Breadcrumb.isValidForRRWebSpan(): Boolean {
304+
return !(data["url"] as? String).isNullOrEmpty() &&
305+
SpanDataConvention.HTTP_START_TIMESTAMP in data &&
306+
SpanDataConvention.HTTP_END_TIMESTAMP in data
307+
}
308+
309+
private fun String.snakeToCamelCase(): String {
310+
return replace(snakecasePattern) { it.value.last().uppercase() }
311+
}
312+
313+
private fun Breadcrumb.toRRWebSpanEvent(): RRWebSpanEvent {
314+
val breadcrumb = this
315+
return RRWebSpanEvent().apply {
316+
timestamp = breadcrumb.timestamp.time
317+
op = "resource.http"
318+
description = breadcrumb.data["url"] as String
319+
startTimestamp =
320+
(breadcrumb.data[SpanDataConvention.HTTP_START_TIMESTAMP] as Long) / 1000.0
321+
endTimestamp =
322+
(breadcrumb.data[SpanDataConvention.HTTP_END_TIMESTAMP] as Long) / 1000.0
323+
324+
val breadcrumbData = mutableMapOf<String, Any?>()
325+
for ((key, value) in breadcrumb.data) {
326+
if (key in supportedNetworkData) {
327+
breadcrumbData[
328+
key
329+
.replace("content_length", "body_size")
330+
.substringAfter(".")
331+
.snakeToCamelCase()
332+
] = value
333+
}
334+
}
335+
data = breadcrumbData
336+
}
337+
}
283338
}

sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEvent.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import io.sentry.okhttp.SentryOkHttpEventListener.Companion.REQUEST_HEADERS_EVEN
1515
import io.sentry.okhttp.SentryOkHttpEventListener.Companion.RESPONSE_BODY_EVENT
1616
import io.sentry.okhttp.SentryOkHttpEventListener.Companion.RESPONSE_HEADERS_EVENT
1717
import io.sentry.okhttp.SentryOkHttpEventListener.Companion.SECURE_CONNECT_EVENT
18+
import io.sentry.transport.CurrentDateProvider
1819
import io.sentry.util.Platform
1920
import io.sentry.util.UrlUtils
2021
import okhttp3.Request
@@ -58,6 +59,8 @@ internal class SentryOkHttpEvent(private val hub: IHub, private val request: Req
5859
breadcrumb = Breadcrumb.http(url, method)
5960
breadcrumb.setData("host", host)
6061
breadcrumb.setData("path", encodedPath)
62+
// needs this as unix timestamp for rrweb
63+
breadcrumb.setData(SpanDataConvention.HTTP_START_TIMESTAMP, CurrentDateProvider.getInstance().currentTimeMillis)
6164

6265
// We add the same data to the root call span
6366
callRootSpan?.setData("url", url)
@@ -150,6 +153,8 @@ internal class SentryOkHttpEvent(private val hub: IHub, private val request: Req
150153
hint.set(TypeCheckHint.OKHTTP_REQUEST, request)
151154
response?.let { hint.set(TypeCheckHint.OKHTTP_RESPONSE, it) }
152155

156+
// needs this as unix timestamp for rrweb
157+
breadcrumb.setData(SpanDataConvention.HTTP_END_TIMESTAMP, CurrentDateProvider.getInstance().currentTimeMillis)
153158
// We send the breadcrumb even without spans.
154159
hub.addBreadcrumb(breadcrumb, hint)
155160

sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpInterceptor.kt

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import io.sentry.SpanStatus
1414
import io.sentry.TypeCheckHint.OKHTTP_REQUEST
1515
import io.sentry.TypeCheckHint.OKHTTP_RESPONSE
1616
import io.sentry.okhttp.SentryOkHttpInterceptor.BeforeSpanCallback
17+
import io.sentry.transport.CurrentDateProvider
1718
import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion
1819
import io.sentry.util.Platform
1920
import io.sentry.util.PropagationTargetsUtils
@@ -79,6 +80,7 @@ public open class SentryOkHttpInterceptor(
7980
val parentSpan = if (Platform.isAndroid()) hub.transaction else hub.span
8081
span = parentSpan?.startChild("http.client", "$method $url")
8182
}
83+
val startTimestamp = CurrentDateProvider.getInstance().currentTimeMillis
8284

8385
span?.spanContext?.origin = TRACE_ORIGIN
8486

@@ -137,12 +139,17 @@ public open class SentryOkHttpInterceptor(
137139

138140
// The SentryOkHttpEventListener will send the breadcrumb itself if used for this call
139141
if (!isFromEventListener) {
140-
sendBreadcrumb(request, code, response)
142+
sendBreadcrumb(request, code, response, startTimestamp)
141143
}
142144
}
143145
}
144146

145-
private fun sendBreadcrumb(request: Request, code: Int?, response: Response?) {
147+
private fun sendBreadcrumb(
148+
request: Request,
149+
code: Int?,
150+
response: Response?,
151+
startTimestamp: Long
152+
) {
146153
val breadcrumb = Breadcrumb.http(request.url.toString(), request.method, code)
147154
request.body?.contentLength().ifHasValidLength {
148155
breadcrumb.setData("http.request_content_length", it)
@@ -156,6 +163,9 @@ public open class SentryOkHttpInterceptor(
156163

157164
hint[OKHTTP_RESPONSE] = it
158165
}
166+
// needs this as unix timestamp for rrweb
167+
breadcrumb.setData(SpanDataConvention.HTTP_START_TIMESTAMP, startTimestamp)
168+
breadcrumb.setData(SpanDataConvention.HTTP_END_TIMESTAMP, CurrentDateProvider.getInstance().currentTimeMillis)
159169

160170
hub.addBreadcrumb(breadcrumb, hint)
161171
}

sentry/api/sentry.api

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2961,10 +2961,12 @@ public abstract interface class io/sentry/SpanDataConvention {
29612961
public static final field FRAMES_FROZEN Ljava/lang/String;
29622962
public static final field FRAMES_SLOW Ljava/lang/String;
29632963
public static final field FRAMES_TOTAL Ljava/lang/String;
2964+
public static final field HTTP_END_TIMESTAMP Ljava/lang/String;
29642965
public static final field HTTP_FRAGMENT_KEY Ljava/lang/String;
29652966
public static final field HTTP_METHOD_KEY Ljava/lang/String;
29662967
public static final field HTTP_QUERY_KEY Ljava/lang/String;
29672968
public static final field HTTP_RESPONSE_CONTENT_LENGTH_KEY Ljava/lang/String;
2969+
public static final field HTTP_START_TIMESTAMP Ljava/lang/String;
29682970
public static final field HTTP_STATUS_CODE_KEY Ljava/lang/String;
29692971
public static final field THREAD_ID Ljava/lang/String;
29702972
public static final field THREAD_NAME Ljava/lang/String;
@@ -5155,6 +5157,46 @@ public final class io/sentry/rrweb/RRWebMetaEvent$JsonKeys {
51555157
public fun <init> ()V
51565158
}
51575159

5160+
public final class io/sentry/rrweb/RRWebSpanEvent : io/sentry/rrweb/RRWebEvent, io/sentry/JsonSerializable, io/sentry/JsonUnknown {
5161+
public static final field EVENT_TAG Ljava/lang/String;
5162+
public fun <init> ()V
5163+
public fun getData ()Ljava/util/Map;
5164+
public fun getDataUnknown ()Ljava/util/Map;
5165+
public fun getDescription ()Ljava/lang/String;
5166+
public fun getEndTimestamp ()D
5167+
public fun getOp ()Ljava/lang/String;
5168+
public fun getPayloadUnknown ()Ljava/util/Map;
5169+
public fun getStartTimestamp ()D
5170+
public fun getTag ()Ljava/lang/String;
5171+
public fun getUnknown ()Ljava/util/Map;
5172+
public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V
5173+
public fun setData (Ljava/util/Map;)V
5174+
public fun setDataUnknown (Ljava/util/Map;)V
5175+
public fun setDescription (Ljava/lang/String;)V
5176+
public fun setEndTimestamp (D)V
5177+
public fun setOp (Ljava/lang/String;)V
5178+
public fun setPayloadUnknown (Ljava/util/Map;)V
5179+
public fun setStartTimestamp (D)V
5180+
public fun setTag (Ljava/lang/String;)V
5181+
public fun setUnknown (Ljava/util/Map;)V
5182+
}
5183+
5184+
public final class io/sentry/rrweb/RRWebSpanEvent$Deserializer : io/sentry/JsonDeserializer {
5185+
public fun <init> ()V
5186+
public fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Lio/sentry/rrweb/RRWebSpanEvent;
5187+
public synthetic fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Ljava/lang/Object;
5188+
}
5189+
5190+
public final class io/sentry/rrweb/RRWebSpanEvent$JsonKeys {
5191+
public static final field DATA Ljava/lang/String;
5192+
public static final field DESCRIPTION Ljava/lang/String;
5193+
public static final field END_TIMESTAMP Ljava/lang/String;
5194+
public static final field OP Ljava/lang/String;
5195+
public static final field PAYLOAD Ljava/lang/String;
5196+
public static final field START_TIMESTAMP Ljava/lang/String;
5197+
public fun <init> ()V
5198+
}
5199+
51585200
public final class io/sentry/rrweb/RRWebVideoEvent : io/sentry/rrweb/RRWebEvent, io/sentry/JsonSerializable, io/sentry/JsonUnknown {
51595201
public static final field EVENT_TAG Ljava/lang/String;
51605202
public static final field REPLAY_CONTAINER Ljava/lang/String;

sentry/src/main/java/io/sentry/ReplayRecording.java

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package io.sentry;
22

3+
import io.sentry.rrweb.RRWebBreadcrumbEvent;
34
import io.sentry.rrweb.RRWebEvent;
45
import io.sentry.rrweb.RRWebEventType;
56
import io.sentry.rrweb.RRWebMetaEvent;
7+
import io.sentry.rrweb.RRWebSpanEvent;
68
import io.sentry.rrweb.RRWebVideoEvent;
79
import io.sentry.util.MapObjectReader;
810
import io.sentry.util.Objects;
@@ -148,19 +150,33 @@ public static final class Deserializer implements JsonDeserializer<ReplayRecordi
148150
payload.add(metaEvent);
149151
break;
150152
case Custom:
151-
final Map<String, Object> data =
152-
(Map<String, Object>) eventMap.getOrDefault("data", Collections.emptyMap());
153-
final String tag =
154-
(String) data.getOrDefault(RRWebEvent.JsonKeys.TAG, "default");
155-
switch (tag) {
156-
case RRWebVideoEvent.EVENT_TAG:
157-
final RRWebEvent videoEvent =
158-
new RRWebVideoEvent.Deserializer().deserialize(mapReader, logger);
159-
payload.add(videoEvent);
160-
break;
161-
default:
162-
logger.log(SentryLevel.DEBUG, "Unsupported rrweb event type %s", type);
163-
break;
153+
Map<String, Object> data = (Map<String, Object>) eventMap.get("data");
154+
if (data == null) {
155+
data = Collections.emptyMap();
156+
}
157+
final String tag = (String) data.get(RRWebEvent.JsonKeys.TAG);
158+
if (tag != null) {
159+
switch (tag) {
160+
case RRWebVideoEvent.EVENT_TAG:
161+
final RRWebEvent videoEvent =
162+
new RRWebVideoEvent.Deserializer().deserialize(mapReader, logger);
163+
payload.add(videoEvent);
164+
break;
165+
case RRWebBreadcrumbEvent.EVENT_TAG:
166+
final RRWebEvent breadcrumbEvent =
167+
new RRWebBreadcrumbEvent.Deserializer()
168+
.deserialize(mapReader, logger);
169+
payload.add(breadcrumbEvent);
170+
break;
171+
case RRWebSpanEvent.EVENT_TAG:
172+
final RRWebEvent spanEvent =
173+
new RRWebSpanEvent.Deserializer().deserialize(mapReader, logger);
174+
payload.add(spanEvent);
175+
break;
176+
default:
177+
logger.log(SentryLevel.DEBUG, "Unsupported rrweb event type %s", type);
178+
break;
179+
}
164180
}
165181
break;
166182
default:

sentry/src/main/java/io/sentry/SpanDataConvention.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,6 @@ public interface SpanDataConvention {
2121
String FRAMES_SLOW = "frames.slow";
2222
String FRAMES_FROZEN = "frames.frozen";
2323
String FRAMES_DELAY = "frames.delay";
24+
String HTTP_START_TIMESTAMP = "http.start_timestamp";
25+
String HTTP_END_TIMESTAMP = "http.end_timestamp";
2426
}

0 commit comments

Comments
 (0)