Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b1ced85
Add sentry replay envelope and event
romtsn Feb 1, 2024
f8419d1
Merge branch 'rz/feat/session-replay-sources' into rz/feat/session-re…
romtsn Feb 13, 2024
a63cac1
WIP
romtsn Feb 15, 2024
fa72057
Add replay envelopes
romtsn Feb 19, 2024
6cfb511
Remove jsonValue
romtsn Feb 19, 2024
0d031d7
Remove
romtsn Feb 19, 2024
07e6b26
Fix json
romtsn Feb 19, 2024
18af924
Finalize replay envelopes
romtsn Feb 20, 2024
64cedfa
Introduce MapObjectReader
romtsn Feb 20, 2024
b8cb924
Add missing test
romtsn Feb 20, 2024
28d341f
Merge branch 'rz/feat/session-replay-envelopes' into rz/feat/session-…
romtsn Feb 20, 2024
1e76fc7
Add test for MapObjectReader
romtsn Feb 22, 2024
13c1971
Add MapObjectWriter change
romtsn Feb 22, 2024
a14e090
Merge branch 'rz/feat/session-replay-envelopes' into rz/feat/session-…
romtsn Feb 22, 2024
86baf7f
Add finals
romtsn Feb 22, 2024
f1ca9f6
Fix test
romtsn Feb 22, 2024
fbbe0d9
Fix test
romtsn Feb 22, 2024
688233f
Merge branch 'rz/feat/session-replay-envelopes' into rz/feat/session-…
romtsn Feb 22, 2024
fd63960
Address review
romtsn Feb 28, 2024
93785cc
Add finals and annotations
romtsn Feb 28, 2024
4db19e0
Merge pull request #3215 from getsentry/rz/feat/session-replay-map-ob…
romtsn Feb 28, 2024
62477b4
Remove public captureReplay method
romtsn Mar 1, 2024
af42fb3
Fix test
romtsn Mar 1, 2024
cd09739
Merge branch 'rz/feat/session-replay-sources' into rz/feat/session-re…
romtsn Mar 1, 2024
4e54c77
api dump
romtsn Mar 1, 2024
fb14ecb
Merge branch 'rz/feat/session-replay' into rz/feat/session-replay-env…
romtsn Mar 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Finalize replay envelopes
  • Loading branch information
romtsn committed Feb 20, 2024
commit 18af924efa5d49b0922703d3cf23b92699e5951f
1 change: 1 addition & 0 deletions buildSrc/src/main/java/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ object Config {
val jsonUnit = "net.javacrumbs.json-unit:json-unit:2.32.0"
val hsqldb = "org.hsqldb:hsqldb:2.6.1"
val javaFaker = "com.github.javafaker:javafaker:1.0.2"
val msgpack = "org.msgpack:msgpack-core:0.9.8"
}

object QualityPlugins {
Expand Down
1 change: 1 addition & 0 deletions sentry/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ dependencies {
testImplementation(Config.TestLibs.mockitoInline)
testImplementation(Config.TestLibs.awaitility)
testImplementation(Config.TestLibs.javaFaker)
testImplementation(Config.TestLibs.msgpack)
testImplementation(projects.sentryTestSupport)
}

Expand Down
11 changes: 11 additions & 0 deletions sentry/src/main/java/io/sentry/JsonObjectWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ public JsonObjectWriter value(final @Nullable String value) throws IOException {
return this;
}

@Override
public ObjectWriter jsonValue(@Nullable String value) throws IOException {
jsonWriter.jsonValue(value);
return this;
}

@Override
public JsonObjectWriter nullValue() throws IOException {
jsonWriter.nullValue();
Expand Down Expand Up @@ -103,6 +109,11 @@ public JsonObjectWriter value(final @NotNull ILogger logger, final @Nullable Obj
return this;
}

@Override
public void setLenient(final boolean lenient) {
jsonWriter.setLenient(lenient);
}

public void setIndent(final @NotNull String indent) {
jsonWriter.setIndent(indent);
}
Expand Down
2 changes: 2 additions & 0 deletions sentry/src/main/java/io/sentry/JsonSerializer.java
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ public JsonSerializer(@NotNull SentryOptions options) {
deserializersByClass.put(
ProfileMeasurementValue.class, new ProfileMeasurementValue.Deserializer());
deserializersByClass.put(Request.class, new Request.Deserializer());
deserializersByClass.put(ReplayRecording.class, new ReplayRecording.Deserializer());
deserializersByClass.put(RRWebEventType.class, new RRWebEventType.Deserializer());
deserializersByClass.put(RRWebMetaEvent.class, new RRWebMetaEvent.Deserializer());
deserializersByClass.put(RRWebVideoEvent.class, new RRWebVideoEvent.Deserializer());
Expand All @@ -107,6 +108,7 @@ public JsonSerializer(@NotNull SentryOptions options) {
deserializersByClass.put(SentryLockReason.class, new SentryLockReason.Deserializer());
deserializersByClass.put(SentryPackage.class, new SentryPackage.Deserializer());
deserializersByClass.put(SentryRuntime.class, new SentryRuntime.Deserializer());
deserializersByClass.put(SentryReplayEvent.class, new SentryReplayEvent.Deserializer());
deserializersByClass.put(SentrySpan.class, new SentrySpan.Deserializer());
deserializersByClass.put(SentryStackFrame.class, new SentryStackFrame.Deserializer());
deserializersByClass.put(SentryStackTrace.class, new SentryStackTrace.Deserializer());
Expand Down
4 changes: 4 additions & 0 deletions sentry/src/main/java/io/sentry/ObjectWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public interface ObjectWriter {

ObjectWriter value(final @Nullable String value) throws IOException;

ObjectWriter jsonValue(final @Nullable String value) throws IOException;

ObjectWriter nullValue() throws IOException;

ObjectWriter value(final boolean value) throws IOException;
Expand All @@ -31,4 +33,6 @@ public interface ObjectWriter {

ObjectWriter value(final @NotNull ILogger logger, final @Nullable Object object)
throws IOException;

void setLenient(boolean lenient);
}
84 changes: 83 additions & 1 deletion sentry/src/main/java/io/sentry/ReplayRecording.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
package io.sentry;

import io.sentry.rrweb.RRWebEvent;
import io.sentry.rrweb.RRWebEventType;
import io.sentry.rrweb.RRWebMetaEvent;
import io.sentry.rrweb.RRWebVideoEvent;
import io.sentry.util.MapObjectReader;
import io.sentry.util.Objects;
import io.sentry.vendor.gson.stream.JsonToken;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -37,6 +44,19 @@ public void setPayload(@Nullable List<? extends RRWebEvent> payload) {
this.payload = payload;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ReplayRecording that = (ReplayRecording) o;
return Objects.equals(segmentId, that.segmentId) && Objects.equals(payload, that.payload);
}

@Override
public int hashCode() {
return Objects.hash(segmentId, payload);
}

@Override
public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger logger)
throws IOException {
Expand All @@ -52,6 +72,15 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger
}
}
writer.endObject();

// {"segment_id":0}\n{json-serialized-rrweb-protocol}

writer.setLenient(true);
writer.jsonValue("\n");
if (payload != null) {
writer.value(logger, payload);
}
writer.setLenient(false);
}

@Override
Expand All @@ -66,14 +95,16 @@ public void setUnknown(@Nullable Map<String, Object> unknown) {

public static final class Deserializer implements JsonDeserializer<ReplayRecording> {

@SuppressWarnings("unchecked")
@Override
public @NotNull ReplayRecording deserialize(
@NotNull JsonObjectReader reader, @NotNull ILogger logger) throws Exception {
@NotNull ObjectReader reader, @NotNull ILogger logger) throws Exception {

final ReplayRecording replay = new ReplayRecording();

@Nullable Map<String, Object> unknown = null;
@Nullable Integer segmentId = null;
@Nullable List<RRWebEvent> payload = null;

reader.beginObject();
while (reader.peek() == JsonToken.NAME) {
Expand All @@ -92,7 +123,58 @@ public static final class Deserializer implements JsonDeserializer<ReplayRecordi
}
reader.endObject();

// {"segment_id":0}\n{json-serialized-rrweb-protocol}

reader.setLenient(true);
List<Object> events = (List<Object>) reader.nextObjectOrNull();
reader.setLenient(false);

// since we lose the type of an rrweb event at runtime, we have to recover it from a map
if (events != null) {
payload = new ArrayList<>(events.size());
for (Object event : events) {
if (event instanceof Map) {
final Map<String, Object> eventMap = (Map<String, Object>) event;
final ObjectReader mapReader = new MapObjectReader(eventMap);
for (Map.Entry<String, Object> entry : eventMap.entrySet()) {
final String key = entry.getKey();
final Object value = entry.getValue();
if (key.equals("type")) {
RRWebEventType type = RRWebEventType.values()[(int) value];
switch (type) {
case Meta:
final RRWebEvent metaEvent =
new RRWebMetaEvent.Deserializer().deserialize(mapReader, logger);
payload.add(metaEvent);
break;
case Custom:
final Map<String, Object> data =
(Map<String, Object>) eventMap.getOrDefault("data", Collections.emptyMap());
final String tag =
(String) data.getOrDefault(RRWebEvent.JsonKeys.TAG, "default");
switch (tag) {
case RRWebVideoEvent.EVENT_TAG:
final RRWebEvent videoEvent =
new RRWebVideoEvent.Deserializer().deserialize(mapReader, logger);
payload.add(videoEvent);
break;
default:
logger.log(SentryLevel.DEBUG, "Unsupported rrweb event type %s", type);
break;
}
break;
default:
logger.log(SentryLevel.DEBUG, "Unsupported rrweb event type %s", type);
break;
}
}
}
}
}
}

replay.setSegmentId(segmentId);
replay.setPayload(payload);
replay.setUnknown(unknown);
return replay;
}
Expand Down
36 changes: 13 additions & 23 deletions sentry/src/main/java/io/sentry/SentryEnvelopeItem.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.io.Reader;
import java.io.Writer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
Expand Down Expand Up @@ -365,22 +366,16 @@ public static SentryEnvelopeItem fromReplay(
replayPayload.put(SentryItemType.ReplayEvent.getItemType(), stream.toByteArray());
stream.reset();

// next serialize replay recording in the following format:
// {"segment_id":0}\n{json-serialized-rrweb-protocol}
// next serialize replay recording
if (replayRecording != null) {
serializer.serialize(replayRecording, writer);
writer.write("\n");
writer.flush();
if (replayRecording.getPayload() != null) {
serializer.serialize(replayRecording.getPayload(), writer);
}
replayPayload.put(
SentryItemType.ReplayRecording.getItemType(), stream.toByteArray());
stream.reset();
}

// next serialize replay video bytes from given file
if (replayVideo.exists()) {
if (replayVideo != null && replayVideo.exists()) {
final byte[] videoBytes =
readBytesFromFile(
replayVideo.getPath(), SentryReplayEvent.REPLAY_VIDEO_MAX_SIZE);
Expand All @@ -395,7 +390,9 @@ public static SentryEnvelopeItem fromReplay(
logger.log(SentryLevel.ERROR, "Could not serialize replay recording", t);
return null;
} finally {
replayVideo.delete();
if (replayVideo != null) {
replayVideo.delete();
}
}
});

Expand Down Expand Up @@ -428,7 +425,7 @@ public CachedItem(final @Nullable Callable<byte[]> dataFactory) {
}
}

@SuppressWarnings("CharsetObjectCanBeUsed")
@SuppressWarnings({"CharsetObjectCanBeUsed", "UnnecessaryParentheses"})
private static byte[] serializeToMsgpack(Map<String, byte[]> map) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();

Expand All @@ -440,24 +437,17 @@ private static byte[] serializeToMsgpack(Map<String, byte[]> map) throws IOExcep
// Pack the key as a string
byte[] keyBytes = entry.getKey().getBytes(Charset.forName("UTF-8"));
int keyLength = keyBytes.length;
if (keyLength <= 31) {
baos.write((byte) (0xA0 | keyLength));
} else {
baos.write((byte) (0xD9));
baos.write((byte) (keyLength));
}
// string up to 255 chars
baos.write((byte) (0xd9));
baos.write((byte) (keyLength));
baos.write(keyBytes);

// Pack the value as a binary string
byte[] valueBytes = entry.getValue();
int valueLength = valueBytes.length;
if (valueLength <= 255) {
baos.write((byte) (0xC4));
baos.write((byte) (valueLength));
} else {
baos.write((byte) (0xC5));
baos.write(ByteBuffer.allocate(4).putInt(valueLength).array());
}
// We will always use the 4 bytes data length for simplicity.
baos.write((byte) (0xc6));
baos.write(ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(valueLength).array());
baos.write(valueBytes);
}

Expand Down
32 changes: 26 additions & 6 deletions sentry/src/main/java/io/sentry/SentryReplayEvent.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.sentry;

import io.sentry.protocol.SentryId;
import io.sentry.util.Objects;
import io.sentry.vendor.gson.stream.JsonToken;
import java.io.File;
import java.io.IOException;
Expand Down Expand Up @@ -29,8 +30,8 @@ public void serialize(@NotNull ObjectWriter writer, @NotNull ILogger logger)

public static final class Deserializer implements JsonDeserializer<ReplayType> {
@Override
public @NotNull ReplayType deserialize(
@NotNull JsonObjectReader reader, @NotNull ILogger logger) throws Exception {
public @NotNull ReplayType deserialize(@NotNull ObjectReader reader, @NotNull ILogger logger)
throws Exception {
return ReplayType.valueOf(reader.nextString().toUpperCase(Locale.ROOT));
}
}
Expand All @@ -39,7 +40,7 @@ public static final class Deserializer implements JsonDeserializer<ReplayType> {
public static final long REPLAY_VIDEO_MAX_SIZE = 10 * 1024 * 1024;
public static final String REPLAY_EVENT_TYPE = "replay_event";

private @NotNull File videoFile;
private @Nullable File videoFile;
private @NotNull String type;
private @NotNull ReplayType replayType;
private @Nullable SentryId replayId;
Expand All @@ -62,12 +63,12 @@ public SentryReplayEvent() {
timestamp = DateUtils.getCurrentDateTime();
}

@NotNull
@Nullable
public File getVideoFile() {
return videoFile;
}

public void setVideoFile(final @NotNull File videoFile) {
public void setVideoFile(final @Nullable File videoFile) {
this.videoFile = videoFile;
}

Expand Down Expand Up @@ -151,6 +152,25 @@ public void setReplayType(final @NotNull ReplayType replayType) {
this.replayType = replayType;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SentryReplayEvent that = (SentryReplayEvent) o;
return segmentId == that.segmentId
&& Objects.equals(type, that.type)
&& replayType == that.replayType
&& Objects.equals(replayId, that.replayId)
&& Objects.equals(urls, that.urls)
&& Objects.equals(errorIds, that.errorIds)
&& Objects.equals(traceIds, that.traceIds);
}

@Override
public int hashCode() {
return Objects.hash(type, replayType, replayId, segmentId, urls, errorIds, traceIds);
}

// region json
public static final class JsonKeys {
public static final String TYPE = "type";
Expand Down Expand Up @@ -219,7 +239,7 @@ public static final class Deserializer implements JsonDeserializer<SentryReplayE
@SuppressWarnings("unchecked")
@Override
public @NotNull SentryReplayEvent deserialize(
final @NotNull JsonObjectReader reader, final @NotNull ILogger logger) throws Exception {
final @NotNull ObjectReader reader, final @NotNull ILogger logger) throws Exception {

SentryBaseEvent.Deserializer baseEventDeserializer = new SentryBaseEvent.Deserializer();

Expand Down
Loading