Skip to content

Commit f970a1a

Browse files
authored
Merge cba3c72 into 870fdd8
2 parents 870fdd8 + cba3c72 commit f970a1a

7 files changed

Lines changed: 335 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
### Internal
1010

1111
- Session Replay: Allow overriding `SdkVersion` for replay events ([#4014](https://github.com/getsentry/sentry-java/pull/4014))
12+
- Session Replay: Send replay options as tags ([#4015](https://github.com/getsentry/sentry-java/pull/4015))
1213

1314
## 7.19.1
1415

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import io.sentry.protocol.SentryId
1616
import io.sentry.rrweb.RRWebBreadcrumbEvent
1717
import io.sentry.rrweb.RRWebEvent
1818
import io.sentry.rrweb.RRWebMetaEvent
19+
import io.sentry.rrweb.RRWebOptionsEvent
1920
import io.sentry.rrweb.RRWebVideoEvent
2021
import java.io.File
2122
import java.util.Date
@@ -195,6 +196,10 @@ internal interface CaptureStrategy {
195196
}
196197
}
197198

199+
if (segmentId == 0) {
200+
recordingPayload += RRWebOptionsEvent(options)
201+
}
202+
198203
val recording = ReplayRecording().apply {
199204
this.segmentId = segmentId
200205
this.payload = recordingPayload.sortedBy { it.timestamp }

sentry-android-replay/src/test/java/io/sentry/android/replay/capture/SessionCaptureStrategyTest.kt

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import io.sentry.ScopeCallback
99
import io.sentry.SentryOptions
1010
import io.sentry.SentryReplayEvent
1111
import io.sentry.SentryReplayEvent.ReplayType
12+
import io.sentry.SentryReplayOptions.SentryReplayQuality.HIGH
13+
import io.sentry.android.replay.BuildConfig
1214
import io.sentry.android.replay.DefaultReplayBreadcrumbConverter
1315
import io.sentry.android.replay.GeneratedVideo
1416
import io.sentry.android.replay.ReplayCache
@@ -22,9 +24,11 @@ import io.sentry.android.replay.ReplayCache.Companion.SEGMENT_KEY_TIMESTAMP
2224
import io.sentry.android.replay.ReplayCache.Companion.SEGMENT_KEY_WIDTH
2325
import io.sentry.android.replay.ReplayFrame
2426
import io.sentry.android.replay.ScreenshotRecorderConfig
27+
import io.sentry.android.replay.maskAllImages
2528
import io.sentry.protocol.SentryId
2629
import io.sentry.rrweb.RRWebBreadcrumbEvent
2730
import io.sentry.rrweb.RRWebMetaEvent
31+
import io.sentry.rrweb.RRWebOptionsEvent
2832
import io.sentry.transport.CurrentDateProvider
2933
import io.sentry.transport.ICurrentDateProvider
3034
import org.junit.Rule
@@ -367,4 +371,39 @@ class SessionCaptureStrategyTest {
367371
"the current replay cache folder is not being deleted."
368372
)
369373
}
374+
375+
@Test
376+
fun `records replay options event for segment 0`() {
377+
fixture.options.experimental.sessionReplay.sessionSampleRate = 1.0
378+
fixture.options.experimental.sessionReplay.maskAllImages = false
379+
fixture.options.experimental.sessionReplay.quality = HIGH
380+
fixture.options.experimental.sessionReplay.addMaskViewClass("my.custom.View")
381+
382+
val now =
383+
System.currentTimeMillis() + (fixture.options.experimental.sessionReplay.sessionSegmentDuration * 5)
384+
val strategy = fixture.getSut(dateProvider = { now })
385+
strategy.start(fixture.recorderConfig)
386+
387+
strategy.onScreenshotRecorded(mock<Bitmap>()) {}
388+
389+
verify(fixture.hub).captureReplay(
390+
argThat { event ->
391+
event is SentryReplayEvent && event.segmentId == 0
392+
},
393+
check {
394+
val optionsEvent =
395+
it.replayRecording?.payload?.filterIsInstance<RRWebOptionsEvent>()!!
396+
assertEquals("sentry.java", optionsEvent[0].optionsPayload["nativeSdkName"])
397+
assertEquals(BuildConfig.VERSION_NAME, optionsEvent[0].optionsPayload["nativeSdkVersion"])
398+
399+
assertEquals(null, optionsEvent[0].optionsPayload["errorSampleRate"])
400+
assertEquals(1.0, optionsEvent[0].optionsPayload["sessionSampleRate"])
401+
assertEquals(true, optionsEvent[0].optionsPayload["maskAllText"])
402+
assertEquals(false, optionsEvent[0].optionsPayload["maskAllImages"])
403+
assertEquals("high", optionsEvent[0].optionsPayload["quality"])
404+
assertEquals("android.widget.TextView,android.webkit.WebView,android.widget.VideoView,androidx.media3.ui.PlayerView,com.google.android.exoplayer2.ui.PlayerView,com.google.android.exoplayer2.ui.StyledPlayerView,my.custom.View", optionsEvent[0].optionsPayload["maskedViewClasses"])
405+
assertEquals("android.widget.ImageView", optionsEvent[0].optionsPayload["unmaskedViewClasses"])
406+
}
407+
)
408+
}
370409
}

sentry/api/sentry.api

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2760,6 +2760,7 @@ public final class io/sentry/SentryReplayOptions$SentryReplayQuality : java/lang
27602760
public static final field MEDIUM Lio/sentry/SentryReplayOptions$SentryReplayQuality;
27612761
public final field bitRate I
27622762
public final field sizeScale F
2763+
public fun serializedName ()Ljava/lang/String;
27632764
public static fun valueOf (Ljava/lang/String;)Lio/sentry/SentryReplayOptions$SentryReplayQuality;
27642765
public static fun values ()[Lio/sentry/SentryReplayOptions$SentryReplayQuality;
27652766
}
@@ -5415,6 +5416,33 @@ public final class io/sentry/rrweb/RRWebMetaEvent$JsonKeys {
54155416
public fun <init> ()V
54165417
}
54175418

5419+
public final class io/sentry/rrweb/RRWebOptionsEvent : io/sentry/rrweb/RRWebEvent, io/sentry/JsonSerializable, io/sentry/JsonUnknown {
5420+
public static final field EVENT_TAG Ljava/lang/String;
5421+
public fun <init> ()V
5422+
public fun <init> (Lio/sentry/SentryOptions;)V
5423+
public fun getDataUnknown ()Ljava/util/Map;
5424+
public fun getOptionsPayload ()Ljava/util/Map;
5425+
public fun getTag ()Ljava/lang/String;
5426+
public fun getUnknown ()Ljava/util/Map;
5427+
public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V
5428+
public fun setDataUnknown (Ljava/util/Map;)V
5429+
public fun setOptionsPayload (Ljava/util/Map;)V
5430+
public fun setTag (Ljava/lang/String;)V
5431+
public fun setUnknown (Ljava/util/Map;)V
5432+
}
5433+
5434+
public final class io/sentry/rrweb/RRWebOptionsEvent$Deserializer : io/sentry/JsonDeserializer {
5435+
public fun <init> ()V
5436+
public fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Lio/sentry/rrweb/RRWebOptionsEvent;
5437+
public synthetic fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Ljava/lang/Object;
5438+
}
5439+
5440+
public final class io/sentry/rrweb/RRWebOptionsEvent$JsonKeys {
5441+
public static final field DATA Ljava/lang/String;
5442+
public static final field PAYLOAD Ljava/lang/String;
5443+
public fun <init> ()V
5444+
}
5445+
54185446
public final class io/sentry/rrweb/RRWebSpanEvent : io/sentry/rrweb/RRWebEvent, io/sentry/JsonSerializable, io/sentry/JsonUnknown {
54195447
public static final field EVENT_TAG Ljava/lang/String;
54205448
public fun <init> ()V
@@ -5637,6 +5665,7 @@ public final class io/sentry/util/CollectionUtils {
56375665
public static fun contains ([Ljava/lang/Object;Ljava/lang/Object;)Z
56385666
public static fun filterListEntries (Ljava/util/List;Lio/sentry/util/CollectionUtils$Predicate;)Ljava/util/List;
56395667
public static fun filterMapEntries (Ljava/util/Map;Lio/sentry/util/CollectionUtils$Predicate;)Ljava/util/Map;
5668+
public static fun joinToString (Ljava/util/Set;Ljava/lang/String;)Ljava/lang/String;
56405669
public static fun map (Ljava/util/List;Lio/sentry/util/CollectionUtils$Mapper;)Ljava/util/List;
56415670
public static fun newArrayList (Ljava/util/List;)Ljava/util/List;
56425671
public static fun newConcurrentHashMap (Ljava/util/Map;)Ljava/util/Map;

sentry/src/main/java/io/sentry/SentryReplayOptions.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import io.sentry.protocol.SdkVersion;
44
import io.sentry.util.SampleRateUtils;
5+
import java.util.Locale;
56
import java.util.Set;
67
import java.util.concurrent.CopyOnWriteArraySet;
78
import org.jetbrains.annotations.ApiStatus;
@@ -42,6 +43,10 @@ public enum SentryReplayQuality {
4243
this.sizeScale = sizeScale;
4344
this.bitRate = bitRate;
4445
}
46+
47+
public @NotNull String serializedName() {
48+
return name().toLowerCase(Locale.ROOT);
49+
}
4550
}
4651

4752
/**
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
package io.sentry.rrweb;
2+
3+
import io.sentry.ILogger;
4+
import io.sentry.JsonDeserializer;
5+
import io.sentry.JsonSerializable;
6+
import io.sentry.JsonUnknown;
7+
import io.sentry.ObjectReader;
8+
import io.sentry.ObjectWriter;
9+
import io.sentry.SentryOptions;
10+
import io.sentry.SentryReplayOptions;
11+
import io.sentry.protocol.SdkVersion;
12+
import io.sentry.util.CollectionUtils;
13+
import io.sentry.vendor.gson.stream.JsonToken;
14+
import java.io.IOException;
15+
import java.util.HashMap;
16+
import java.util.Map;
17+
import java.util.concurrent.ConcurrentHashMap;
18+
import org.jetbrains.annotations.NotNull;
19+
import org.jetbrains.annotations.Nullable;
20+
21+
public final class RRWebOptionsEvent extends RRWebEvent implements JsonSerializable, JsonUnknown {
22+
public static final String EVENT_TAG = "options";
23+
24+
private @NotNull String tag;
25+
// keeping this untyped so hybrids can easily set what they want
26+
private @NotNull Map<String, Object> optionsPayload = new HashMap<>();
27+
// to support unknown json attributes with nesting, we have to have unknown map for each of the
28+
// nested object in json: { ..., "data": { ..., "payload": { ... } } }
29+
private @Nullable Map<String, Object> unknown;
30+
private @Nullable Map<String, Object> dataUnknown;
31+
32+
public RRWebOptionsEvent() {
33+
super(RRWebEventType.Custom);
34+
tag = EVENT_TAG;
35+
}
36+
37+
public RRWebOptionsEvent(final @NotNull SentryOptions options) {
38+
this();
39+
final SdkVersion sdkVersion = options.getSdkVersion();
40+
if (sdkVersion != null) {
41+
optionsPayload.put("nativeSdkName", sdkVersion.getName());
42+
optionsPayload.put("nativeSdkVersion", sdkVersion.getVersion());
43+
}
44+
final @NotNull SentryReplayOptions replayOptions = options.getExperimental().getSessionReplay();
45+
optionsPayload.put("errorSampleRate", replayOptions.getOnErrorSampleRate());
46+
optionsPayload.put("sessionSampleRate", replayOptions.getSessionSampleRate());
47+
optionsPayload.put(
48+
"maskAllImages",
49+
replayOptions.getMaskViewClasses().contains(SentryReplayOptions.IMAGE_VIEW_CLASS_NAME));
50+
optionsPayload.put(
51+
"maskAllText",
52+
replayOptions.getMaskViewClasses().contains(SentryReplayOptions.TEXT_VIEW_CLASS_NAME));
53+
optionsPayload.put("quality", replayOptions.getQuality().serializedName());
54+
optionsPayload.put(
55+
"maskedViewClasses", CollectionUtils.joinToString(replayOptions.getMaskViewClasses(), ","));
56+
optionsPayload.put(
57+
"unmaskedViewClasses",
58+
CollectionUtils.joinToString(replayOptions.getUnmaskViewClasses(), ","));
59+
}
60+
61+
@NotNull
62+
public String getTag() {
63+
return tag;
64+
}
65+
66+
public void setTag(final @NotNull String tag) {
67+
this.tag = tag;
68+
}
69+
70+
public @NotNull Map<String, Object> getOptionsPayload() {
71+
return optionsPayload;
72+
}
73+
74+
public void setOptionsPayload(final @NotNull Map<String, Object> optionsPayload) {
75+
this.optionsPayload = optionsPayload;
76+
}
77+
78+
public @Nullable Map<String, Object> getDataUnknown() {
79+
return dataUnknown;
80+
}
81+
82+
public void setDataUnknown(final @Nullable Map<String, Object> dataUnknown) {
83+
this.dataUnknown = dataUnknown;
84+
}
85+
86+
@Override
87+
public @Nullable Map<String, Object> getUnknown() {
88+
return unknown;
89+
}
90+
91+
@Override
92+
public void setUnknown(final @Nullable Map<String, Object> unknown) {
93+
this.unknown = unknown;
94+
}
95+
96+
// region json
97+
public static final class JsonKeys {
98+
public static final String DATA = "data";
99+
public static final String PAYLOAD = "payload";
100+
}
101+
102+
@Override
103+
public void serialize(@NotNull ObjectWriter writer, @NotNull ILogger logger) throws IOException {
104+
writer.beginObject();
105+
new RRWebEvent.Serializer().serialize(this, writer, logger);
106+
writer.name(JsonKeys.DATA);
107+
serializeData(writer, logger);
108+
if (unknown != null) {
109+
for (final String key : unknown.keySet()) {
110+
final Object value = unknown.get(key);
111+
writer.name(key);
112+
writer.value(logger, value);
113+
}
114+
}
115+
writer.endObject();
116+
}
117+
118+
private void serializeData(final @NotNull ObjectWriter writer, final @NotNull ILogger logger)
119+
throws IOException {
120+
writer.beginObject();
121+
writer.name(RRWebEvent.JsonKeys.TAG).value(tag);
122+
writer.name(JsonKeys.PAYLOAD);
123+
serializePayload(writer, logger);
124+
if (dataUnknown != null) {
125+
for (String key : dataUnknown.keySet()) {
126+
Object value = dataUnknown.get(key);
127+
writer.name(key);
128+
writer.value(logger, value);
129+
}
130+
}
131+
writer.endObject();
132+
}
133+
134+
private void serializePayload(final @NotNull ObjectWriter writer, final @NotNull ILogger logger)
135+
throws IOException {
136+
writer.beginObject();
137+
if (optionsPayload != null) {
138+
for (final String key : optionsPayload.keySet()) {
139+
final Object value = optionsPayload.get(key);
140+
writer.name(key);
141+
writer.value(logger, value);
142+
}
143+
}
144+
writer.endObject();
145+
}
146+
147+
public static final class Deserializer implements JsonDeserializer<RRWebOptionsEvent> {
148+
149+
@Override
150+
public @NotNull RRWebOptionsEvent deserialize(
151+
@NotNull ObjectReader reader, @NotNull ILogger logger) throws Exception {
152+
reader.beginObject();
153+
@Nullable Map<String, Object> unknown = null;
154+
155+
final RRWebOptionsEvent event = new RRWebOptionsEvent();
156+
final RRWebEvent.Deserializer baseEventDeserializer = new RRWebEvent.Deserializer();
157+
158+
while (reader.peek() == JsonToken.NAME) {
159+
final String nextName = reader.nextName();
160+
switch (nextName) {
161+
case JsonKeys.DATA:
162+
deserializeData(event, reader, logger);
163+
break;
164+
default:
165+
if (!baseEventDeserializer.deserializeValue(event, nextName, reader, logger)) {
166+
if (unknown == null) {
167+
unknown = new HashMap<>();
168+
}
169+
reader.nextUnknown(logger, unknown, nextName);
170+
}
171+
break;
172+
}
173+
}
174+
175+
event.setUnknown(unknown);
176+
reader.endObject();
177+
return event;
178+
}
179+
180+
private void deserializeData(
181+
final @NotNull RRWebOptionsEvent event,
182+
final @NotNull ObjectReader reader,
183+
final @NotNull ILogger logger)
184+
throws Exception {
185+
@Nullable Map<String, Object> dataUnknown = null;
186+
187+
reader.beginObject();
188+
while (reader.peek() == JsonToken.NAME) {
189+
final String nextName = reader.nextName();
190+
switch (nextName) {
191+
case RRWebEvent.JsonKeys.TAG:
192+
final String tag = reader.nextStringOrNull();
193+
event.tag = tag == null ? "" : tag;
194+
break;
195+
case JsonKeys.PAYLOAD:
196+
deserializePayload(event, reader, logger);
197+
break;
198+
default:
199+
if (dataUnknown == null) {
200+
dataUnknown = new ConcurrentHashMap<>();
201+
}
202+
reader.nextUnknown(logger, dataUnknown, nextName);
203+
}
204+
}
205+
event.setDataUnknown(dataUnknown);
206+
reader.endObject();
207+
}
208+
209+
@SuppressWarnings("unchecked")
210+
private void deserializePayload(
211+
final @NotNull RRWebOptionsEvent event,
212+
final @NotNull ObjectReader reader,
213+
final @NotNull ILogger logger)
214+
throws Exception {
215+
@Nullable Map<String, Object> optionsPayload = null;
216+
217+
reader.beginObject();
218+
while (reader.peek() == JsonToken.NAME) {
219+
final String nextName = reader.nextName();
220+
if (optionsPayload == null) {
221+
optionsPayload = new HashMap<>();
222+
}
223+
reader.nextUnknown(logger, optionsPayload, nextName);
224+
}
225+
if (optionsPayload != null) {
226+
event.setOptionsPayload(optionsPayload);
227+
}
228+
reader.endObject();
229+
}
230+
}
231+
// endregion json
232+
}

0 commit comments

Comments
 (0)