diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index 4b417ed5b4..600fe404fb 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -302,7 +302,6 @@ public final class io/sentry/android/core/NativeEventCollector$NativeEventData { public fun getEnvelope ()Lio/sentry/SentryEnvelope; public fun getEvent ()Lio/sentry/SentryEvent; public fun getFile ()Ljava/io/File; - public fun getTimestampMs ()J } public final class io/sentry/android/core/NdkHandlerStrategy : java/lang/Enum { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/NativeEventCollector.java b/sentry-android-core/src/main/java/io/sentry/android/core/NativeEventCollector.java index 44f493600f..de048f40b7 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/NativeEventCollector.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/NativeEventCollector.java @@ -3,21 +3,25 @@ import static io.sentry.cache.EnvelopeCache.PREFIX_CURRENT_SESSION_FILE; import static io.sentry.cache.EnvelopeCache.PREFIX_PREVIOUS_SESSION_FILE; import static io.sentry.cache.EnvelopeCache.STARTUP_CRASH_MARKER_FILE; +import static java.nio.charset.StandardCharsets.UTF_8; +import io.sentry.JsonObjectReader; import io.sentry.SentryEnvelope; import io.sentry.SentryEnvelopeItem; import io.sentry.SentryEvent; import io.sentry.SentryItemType; import io.sentry.SentryLevel; +import io.sentry.vendor.gson.stream.JsonToken; import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.ByteArrayInputStream; +import java.io.EOFException; import java.io.File; import java.io.FileInputStream; +import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -34,33 +38,52 @@ public final class NativeEventCollector { private static final String NATIVE_PLATFORM = "native"; - // TODO: will be replaced with the correlationId once the Native SDK supports it private static final long TIMESTAMP_TOLERANCE_MS = 5000; private final @NotNull SentryAndroidOptions options; - private final @NotNull List nativeEvents = new ArrayList<>(); + + /** Lightweight metadata collected during scan phase. */ + private final @NotNull List nativeEnvelopes = new ArrayList<>(); + private boolean collected = false; public NativeEventCollector(final @NotNull SentryAndroidOptions options) { this.options = options; } + /** Lightweight metadata for matching phase - only file reference and timestamp. */ + static final class NativeEnvelopeMetadata { + private final @NotNull File file; + private final long timestampMs; + + NativeEnvelopeMetadata(final @NotNull File file, final long timestampMs) { + this.file = file; + this.timestampMs = timestampMs; + } + + @NotNull + File getFile() { + return file; + } + + long getTimestampMs() { + return timestampMs; + } + } + /** Holds a native event along with its source file for later deletion. */ public static final class NativeEventData { private final @NotNull SentryEvent event; private final @NotNull File file; private final @NotNull SentryEnvelope envelope; - private final long timestampMs; NativeEventData( final @NotNull SentryEvent event, final @NotNull File file, - final @NotNull SentryEnvelope envelope, - final long timestampMs) { + final @NotNull SentryEnvelope envelope) { this.event = event; this.file = file; this.envelope = envelope; - this.timestampMs = timestampMs; } public @NotNull SentryEvent getEvent() { @@ -74,10 +97,6 @@ public static final class NativeEventData { public @NotNull SentryEnvelope getEnvelope() { return envelope; } - - public long getTimestampMs() { - return timestampMs; - } } /** @@ -119,22 +138,22 @@ public void collect() { continue; } - final @Nullable NativeEventData nativeEventData = extractNativeEventFromFile(file); - if (nativeEventData != null) { - nativeEvents.add(nativeEventData); + final @Nullable NativeEnvelopeMetadata metadata = extractNativeEnvelopeMetadata(file); + if (metadata != null) { + nativeEnvelopes.add(metadata); options .getLogger() .log( SentryLevel.DEBUG, "Found native event in outbox: %s (timestamp: %d)", file.getName(), - nativeEventData.getTimestampMs()); + metadata.getTimestampMs()); } } options .getLogger() - .log(SentryLevel.DEBUG, "Collected %d native events from outbox.", nativeEvents.size()); + .log(SentryLevel.DEBUG, "Collected %d native events from outbox.", nativeEnvelopes.size()); } /** @@ -152,14 +171,15 @@ public void collect() { // Lazily collect on first use (runs on executor thread, not main thread) collect(); - for (final NativeEventData nativeEvent : nativeEvents) { - final long timeDiff = Math.abs(tombstoneTimestampMs - nativeEvent.getTimestampMs()); + for (final NativeEnvelopeMetadata metadata : nativeEnvelopes) { + final long timeDiff = Math.abs(tombstoneTimestampMs - metadata.getTimestampMs()); if (timeDiff <= TIMESTAMP_TOLERANCE_MS) { options .getLogger() .log(SentryLevel.DEBUG, "Matched native event by timestamp (diff: %d ms)", timeDiff); - nativeEvents.remove(nativeEvent); - return nativeEvent; + nativeEnvelopes.remove(metadata); + // Only load full event data when we have a match + return loadFullNativeEventData(metadata.getFile()); } } @@ -198,7 +218,121 @@ public boolean deleteNativeEventFile(final @NotNull NativeEventData nativeEventD } } - private @Nullable NativeEventData extractNativeEventFromFile(final @NotNull File file) { + /** + * Extracts only lightweight metadata (timestamp) from an envelope file using streaming parsing. + * This avoids loading the entire envelope and deserializing the full event. + */ + private @Nullable NativeEnvelopeMetadata extractNativeEnvelopeMetadata(final @NotNull File file) { + // we use the backend envelope size limit as a bound for the read loop + final long maxEnvelopeSize = 200 * 1024 * 1024; + long bytesProcessed = 0; + + try (final InputStream stream = new BufferedInputStream(new FileInputStream(file))) { + // Skip envelope header line + final int headerBytes = skipLine(stream); + if (headerBytes < 0) { + return null; + } + bytesProcessed += headerBytes; + + while (bytesProcessed < maxEnvelopeSize) { + final @Nullable String itemHeaderLine = readLine(stream); + if (itemHeaderLine == null || itemHeaderLine.isEmpty()) { + // We reached the end of the envelope + break; + } + bytesProcessed += itemHeaderLine.length() + 1; // +1 for newline + + final @Nullable ItemHeaderInfo headerInfo = parseItemHeader(itemHeaderLine); + if (headerInfo == null) { + break; + } + + if ("event".equals(headerInfo.type)) { + final @Nullable NativeEnvelopeMetadata metadata = + extractMetadataFromEventPayload(stream, headerInfo.length, file); + if (metadata != null) { + return metadata; + } + } else { + skipBytes(stream, headerInfo.length); + } + bytesProcessed += headerInfo.length; + + // Skip the newline after payload (if present) + final int next = stream.read(); + if (next == -1) { + break; + } + bytesProcessed++; + if (next != '\n') { + // Not a newline, we're at the next item header. Can't unread easily, + // but this shouldn't happen with well-formed envelopes + break; + } + } + } catch (Throwable e) { + options + .getLogger() + .log( + SentryLevel.DEBUG, + e, + "Error extracting metadata from envelope file: %s", + file.getAbsolutePath()); + } + return null; + } + + /** + * Extracts platform and timestamp from an event payload using streaming JSON parsing. Only reads + * the fields we need and exits early once found. Uses a bounded stream to track position within + * the payload and skip any unread bytes on close, avoiding allocation of the full payload. + */ + private @Nullable NativeEnvelopeMetadata extractMetadataFromEventPayload( + final @NotNull InputStream stream, final int payloadLength, final @NotNull File file) { + + NativeEnvelopeMetadata result = null; + + try (final BoundedInputStream boundedStream = new BoundedInputStream(stream, payloadLength); + final Reader reader = new InputStreamReader(boundedStream, UTF_8)) { + final JsonObjectReader jsonReader = new JsonObjectReader(reader); + + String platform = null; + Date timestamp = null; + + jsonReader.beginObject(); + while (jsonReader.peek() == JsonToken.NAME) { + final String name = jsonReader.nextName(); + switch (name) { + case "platform": + platform = jsonReader.nextStringOrNull(); + break; + case "timestamp": + timestamp = jsonReader.nextDateOrNull(options.getLogger()); + break; + default: + jsonReader.skipValue(); + break; + } + if (platform != null && timestamp != null) { + break; + } + } + + if (NATIVE_PLATFORM.equals(platform) && timestamp != null) { + result = new NativeEnvelopeMetadata(file, timestamp.getTime()); + } + } catch (Throwable e) { + options + .getLogger() + .log(SentryLevel.DEBUG, e, "Error parsing event JSON from: %s", file.getName()); + } + + return result; + } + + /** Loads the full envelope and event data from a file. Used only when a match is found. */ + private @Nullable NativeEventData loadFullNativeEventData(final @NotNull File file) { try (final InputStream stream = new BufferedInputStream(new FileInputStream(file))) { final SentryEnvelope envelope = options.getEnvelopeReader().read(stream); if (envelope == null) { @@ -212,30 +346,115 @@ public boolean deleteNativeEventFile(final @NotNull NativeEventData nativeEventD try (final Reader eventReader = new BufferedReader( - new InputStreamReader( - new ByteArrayInputStream(item.getData()), StandardCharsets.UTF_8))) { + new InputStreamReader(new ByteArrayInputStream(item.getData()), UTF_8))) { final SentryEvent event = options.getSerializer().deserialize(eventReader, SentryEvent.class); if (event != null && NATIVE_PLATFORM.equals(event.getPlatform())) { - final long timestampMs = extractTimestampMs(event); - return new NativeEventData(event, file, envelope, timestampMs); + return new NativeEventData(event, file, envelope); } } } } catch (Throwable e) { options .getLogger() - .log(SentryLevel.DEBUG, e, "Error reading envelope file: %s", file.getAbsolutePath()); + .log(SentryLevel.DEBUG, e, "Error loading envelope file: %s", file.getAbsolutePath()); + } + return null; + } + + /** Minimal item header info needed for streaming. */ + private static final class ItemHeaderInfo { + final @Nullable String type; + final int length; + + ItemHeaderInfo(final @Nullable String type, final int length) { + this.type = type; + this.length = length; + } + } + + /** Parses item header JSON to extract only type and length fields. */ + private @Nullable ItemHeaderInfo parseItemHeader(final @NotNull String headerLine) { + try (final Reader reader = + new InputStreamReader(new ByteArrayInputStream(headerLine.getBytes(UTF_8)), UTF_8)) { + final JsonObjectReader jsonReader = new JsonObjectReader(reader); + + String type = null; + int length = -1; + + jsonReader.beginObject(); + while (jsonReader.peek() == JsonToken.NAME) { + final String name = jsonReader.nextName(); + switch (name) { + case "type": + type = jsonReader.nextStringOrNull(); + break; + case "length": + length = jsonReader.nextInt(); + break; + default: + jsonReader.skipValue(); + break; + } + // Early exit if we have both + if (type != null && length >= 0) { + break; + } + } + + if (length >= 0) { + return new ItemHeaderInfo(type, length); + } + } catch (Throwable e) { + options.getLogger().log(SentryLevel.DEBUG, e, "Error parsing item header"); } return null; } - private long extractTimestampMs(final @NotNull SentryEvent event) { - final @Nullable Date timestamp = event.getTimestamp(); - if (timestamp != null) { - return timestamp.getTime(); + /** Reads a line from the stream (up to and including newline). Returns null on EOF. */ + private @Nullable String readLine(final @NotNull InputStream stream) throws IOException { + final StringBuilder sb = new StringBuilder(); + int b; + while ((b = stream.read()) != -1) { + if (b == '\n') { + return sb.toString(); + } + sb.append((char) b); + } + return sb.length() > 0 ? sb.toString() : null; + } + + /** + * Skips a line in the stream (up to and including newline). Returns bytes skipped, or -1 on EOF. + */ + private int skipLine(final @NotNull InputStream stream) throws IOException { + int count = 0; + int b; + while ((b = stream.read()) != -1) { + count++; + if (b == '\n') { + return count; + } + } + return count > 0 ? count : -1; + } + + /** Skips exactly n bytes from the stream. */ + private static void skipBytes(final @NotNull InputStream stream, final long count) + throws IOException { + long remaining = count; + while (remaining > 0) { + final long skipped = stream.skip(remaining); + if (skipped == 0) { + // skip() returned 0, try reading instead + if (stream.read() == -1) { + throw new EOFException("Unexpected end of stream while skipping bytes"); + } + remaining--; + } else { + remaining -= skipped; + } } - return 0; } private boolean isRelevantFileName(final @Nullable String fileName) { @@ -244,4 +463,66 @@ private boolean isRelevantFileName(final @Nullable String fileName) { && !fileName.startsWith(PREFIX_PREVIOUS_SESSION_FILE) && !fileName.startsWith(STARTUP_CRASH_MARKER_FILE); } + + /** + * An InputStream wrapper that tracks reads within a bounded section of the stream. This allows + * callers to read/parse only what they need (e.g., extract a few JSON fields), then skip the + * remainder of the section on close to position the stream at the next envelope item. Does not + * close the underlying stream. + */ + private static final class BoundedInputStream extends InputStream { + private final @NotNull InputStream inner; + private long remaining; + + BoundedInputStream(final @NotNull InputStream inner, final int limit) { + this.inner = inner; + this.remaining = limit; + } + + @Override + public int read() throws IOException { + if (remaining <= 0) { + return -1; + } + final int result = inner.read(); + if (result != -1) { + remaining--; + } + return result; + } + + @Override + public int read(final byte[] b, final int off, final int len) throws IOException { + if (remaining <= 0) { + return -1; + } + final int toRead = Math.min(len, (int) remaining); + final int result = inner.read(b, off, toRead); + if (result > 0) { + remaining -= result; + } + return result; + } + + @Override + public long skip(final long n) throws IOException { + final long toSkip = Math.min(n, remaining); + final long skipped = inner.skip(toSkip); + remaining -= skipped; + return skipped; + } + + @Override + public int available() throws IOException { + return Math.min(inner.available(), (int) remaining); + } + + @Override + public void close() throws IOException { + // Skip any remaining bytes to advance the underlying stream position, + // but don't close the underlying stream, because we might have other + // envelope items to read. + skipBytes(inner, remaining); + } + } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/NativeEventCollectorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/NativeEventCollectorTest.kt new file mode 100644 index 0000000000..a652128060 --- /dev/null +++ b/sentry-android-core/src/test/java/io/sentry/android/core/NativeEventCollectorTest.kt @@ -0,0 +1,176 @@ +package io.sentry.android.core + +import io.sentry.DateUtils +import java.io.File +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import org.mockito.kotlin.mock + +class NativeEventCollectorTest { + + @get:Rule val tmpDir = TemporaryFolder() + + class Fixture { + lateinit var outboxDir: File + + val options = + SentryAndroidOptions().apply { + setLogger(mock()) + isDebug = true + } + + fun getSut(tmpDir: TemporaryFolder): NativeEventCollector { + outboxDir = File(tmpDir.root, "outbox") + outboxDir.mkdirs() + options.cacheDirPath = tmpDir.root.absolutePath + return NativeEventCollector(options) + } + } + + private val fixture = Fixture() + + @Test + fun `collects native event from outbox`() { + val sut = fixture.getSut(tmpDir) + copyEnvelopeToOutbox("native-event.txt") + + val timestamp = DateUtils.getDateTime("2023-07-15T10:30:00.000Z").time + val match = sut.findAndRemoveMatchingNativeEvent(timestamp) + assertNotNull(match) + } + + @Test + fun `does not collect java platform event`() { + val sut = fixture.getSut(tmpDir) + copyEnvelopeToOutbox("java-event.txt") + + val match = sut.findAndRemoveMatchingNativeEvent(0L) + assertNull(match) + } + + @Test + fun `does not collect session-only envelope`() { + val sut = fixture.getSut(tmpDir) + copyEnvelopeToOutbox("session-only.txt") + + val match = sut.findAndRemoveMatchingNativeEvent(0L) + assertNull(match) + } + + @Test + fun `collects native event after skipping attachment`() { + val sut = fixture.getSut(tmpDir) + copyEnvelopeToOutbox("native-with-attachment.txt") + + val timestamp = DateUtils.getDateTime("2023-07-15T11:45:30.500Z").time + val match = sut.findAndRemoveMatchingNativeEvent(timestamp) + assertNotNull(match) + } + + @Test + fun `handles empty file without throwing`() { + val sut = fixture.getSut(tmpDir) + File(fixture.outboxDir, "empty.envelope").writeText("") + + val match = sut.findAndRemoveMatchingNativeEvent(0L) + assertNull(match) + } + + @Test + fun `handles malformed envelope without throwing`() { + val sut = fixture.getSut(tmpDir) + File(fixture.outboxDir, "malformed.envelope").writeText("this is not a valid envelope") + + val match = sut.findAndRemoveMatchingNativeEvent(0L) + assertNull(match) + } + + @Test + fun `handles envelope with event and attachments without throwing`() { + val sut = fixture.getSut(tmpDir) + copyEnvelopeToOutbox("event-attachment.txt") + + val match = sut.findAndRemoveMatchingNativeEvent(0L) + assertNull(match) + } + + @Test + fun `handles transaction envelope without throwing`() { + val sut = fixture.getSut(tmpDir) + copyEnvelopeToOutbox("transaction.txt") + + val match = sut.findAndRemoveMatchingNativeEvent(0L) + assertNull(match) + } + + @Test + fun `handles session envelope without throwing`() { + val sut = fixture.getSut(tmpDir) + copyEnvelopeToOutbox("session.txt") + + val match = sut.findAndRemoveMatchingNativeEvent(0L) + assertNull(match) + } + + @Test + fun `handles feedback envelope without throwing`() { + val sut = fixture.getSut(tmpDir) + copyEnvelopeToOutbox("feedback.txt") + + val match = sut.findAndRemoveMatchingNativeEvent(0L) + assertNull(match) + } + + @Test + fun `handles attachment-only envelope without throwing`() { + val sut = fixture.getSut(tmpDir) + copyEnvelopeToOutbox("attachment.txt") + + val match = sut.findAndRemoveMatchingNativeEvent(0L) + assertNull(match) + } + + @Test + fun `collects multiple native events`() { + val sut = fixture.getSut(tmpDir) + copyEnvelopeToOutbox("native-event.txt") + copyEnvelopeToOutbox("native-with-attachment.txt") + + val timestamp1 = DateUtils.getDateTime("2023-07-15T10:30:00.000Z").time + val timestamp2 = DateUtils.getDateTime("2023-07-15T11:45:30.500Z").time + val match1 = sut.findAndRemoveMatchingNativeEvent(timestamp1) + val match2 = sut.findAndRemoveMatchingNativeEvent(timestamp2) + assertNotNull(match1) + assertNotNull(match2) + } + + @Test + fun `ignores non-native events when collecting multiple envelopes`() { + val sut = fixture.getSut(tmpDir) + copyEnvelopeToOutbox("native-event.txt") + copyEnvelopeToOutbox("java-event.txt") + copyEnvelopeToOutbox("transaction.txt") + copyEnvelopeToOutbox("session.txt") + + val timestamp = DateUtils.getDateTime("2023-07-15T10:30:00.000Z").time + val nativeMatch = sut.findAndRemoveMatchingNativeEvent(timestamp) + assertNotNull(nativeMatch) + + // No other matches (already removed) + val noMatch = sut.findAndRemoveMatchingNativeEvent(timestamp) + assertNull(noMatch) + } + + private fun copyEnvelopeToOutbox(name: String): File { + val resourcePath = "envelopes/$name" + val inputStream = + javaClass.classLoader?.getResourceAsStream(resourcePath) + ?: throw IllegalArgumentException("Resource not found: $resourcePath") + val outFile = File(fixture.outboxDir, name) + inputStream.use { input -> outFile.outputStream().use { output -> input.copyTo(output) } } + return outFile + } +} diff --git a/sentry-android-core/src/test/resources/envelopes/attachment.txt b/sentry-android-core/src/test/resources/envelopes/attachment.txt new file mode 100644 index 0000000000..04a6e32325 --- /dev/null +++ b/sentry-android-core/src/test/resources/envelopes/attachment.txt @@ -0,0 +1,3 @@ +{} +{"type":"attachment","length":61,"filename":"attachment.txt","content_type":"text/plain"} +some plain text attachment file which include two line breaks diff --git a/sentry-android-core/src/test/resources/envelopes/event-attachment.txt b/sentry-android-core/src/test/resources/envelopes/event-attachment.txt new file mode 100644 index 0000000000..4abe1bc18f --- /dev/null +++ b/sentry-android-core/src/test/resources/envelopes/event-attachment.txt @@ -0,0 +1,10 @@ +{"event_id":"9ec79c33ec9942ab8353589fcb2e04dc"} +{"type":"event","length":107,"content_type":"application/json"} +{"event_id":"9ec79c33ec9942ab8353589fcb2e04dc", "sdk": {"name":"sentry-android","version":"2.0.0-SNAPSHOT"} +{"type":"attachment","length":61,"filename":"attachment.txt","content_type":"text/plain","attachment_type":"event.minidump"} +some plain text attachment file which include two line breaks +{"type":"attachment","length":29,"filename":"log.txt","content_type":"text/plain"} +attachment +with +line breaks + diff --git a/sentry-android-core/src/test/resources/envelopes/feedback.txt b/sentry-android-core/src/test/resources/envelopes/feedback.txt new file mode 100644 index 0000000000..2120266986 --- /dev/null +++ b/sentry-android-core/src/test/resources/envelopes/feedback.txt @@ -0,0 +1,3 @@ +{"event_id":"bdd63725a2b84c1eabd761106e17d390","sdk":{"name":"sentry.dart.flutter","version":"6.0.0-beta.3","packages":[{"name":"pub:sentry","version":"6.0.0-beta.3"},{"name":"pub:sentry_flutter","version":"6.0.0-beta.3"}],"integrations":["isolateErrorIntegration","runZonedGuardedIntegration","widgetsFlutterBindingIntegration","flutterErrorIntegration","widgetsBindingIntegration","nativeSdkIntegration","loadAndroidImageListIntegration","loadReleaseIntegration"]}} +{"content_type":"application/json","type":"user_report","length":103} +{"event_id":"bdd63725a2b84c1eabd761106e17d390","name":"jonas","email":"a@b.com","comments":"bad stuff"} diff --git a/sentry-android-core/src/test/resources/envelopes/java-event.txt b/sentry-android-core/src/test/resources/envelopes/java-event.txt new file mode 100644 index 0000000000..9d16e5bf4e --- /dev/null +++ b/sentry-android-core/src/test/resources/envelopes/java-event.txt @@ -0,0 +1,3 @@ +{"event_id":"9ec79c33ec9942ab8353589fcb2e04dc"} +{"type":"event","length":121,"content_type":"application/json"} +{"event_id":"9ec79c33ec9942ab8353589fcb2e04dc","timestamp":"2023-07-15T10:30:00.000Z","platform":"java","level":"error"} diff --git a/sentry-android-core/src/test/resources/envelopes/native-event.txt b/sentry-android-core/src/test/resources/envelopes/native-event.txt new file mode 100644 index 0000000000..b826347a7f --- /dev/null +++ b/sentry-android-core/src/test/resources/envelopes/native-event.txt @@ -0,0 +1,3 @@ +{"event_id":"9ec79c33ec9942ab8353589fcb2e04dc"} +{"type":"event","length":123,"content_type":"application/json"} +{"event_id":"9ec79c33ec9942ab8353589fcb2e04dc","timestamp":"2023-07-15T10:30:00.000Z","platform":"native","level":"fatal"} diff --git a/sentry-android-core/src/test/resources/envelopes/native-with-attachment.txt b/sentry-android-core/src/test/resources/envelopes/native-with-attachment.txt new file mode 100644 index 0000000000..1b4ff75a11 --- /dev/null +++ b/sentry-android-core/src/test/resources/envelopes/native-with-attachment.txt @@ -0,0 +1,5 @@ +{"event_id":"9ec79c33ec9942ab8353589fcb2e04dc"} +{"type":"attachment","length":20,"filename":"log.txt","content_type":"text/plain"} +some attachment data +{"type":"event","length":123,"content_type":"application/json"} +{"event_id":"9ec79c33ec9942ab8353589fcb2e04dc","timestamp":"2023-07-15T11:45:30.500Z","platform":"native","level":"fatal"} diff --git a/sentry-android-core/src/test/resources/envelopes/session-only.txt b/sentry-android-core/src/test/resources/envelopes/session-only.txt new file mode 100644 index 0000000000..2b616d77e2 --- /dev/null +++ b/sentry-android-core/src/test/resources/envelopes/session-only.txt @@ -0,0 +1,3 @@ +{"event_id":"9ec79c33ec9942ab8353589fcb2e04dc"} +{"type":"session","length":85,"content_type":"application/json"} +{"sid":"12345678-1234-1234-1234-123456789012","status":"ok","timestamp":"2023-07-15T10:30:00.000Z"} diff --git a/sentry-android-core/src/test/resources/envelopes/session.txt b/sentry-android-core/src/test/resources/envelopes/session.txt new file mode 100644 index 0000000000..fe34ebf32e --- /dev/null +++ b/sentry-android-core/src/test/resources/envelopes/session.txt @@ -0,0 +1,3 @@ +{} +{"content_type":"application/json","type":"session","length":306} +{"sid":"c81d4e2e-bcf2-11e6-869b-7df92533d2db","did":"123","init":true,"started":"2020-02-07T14:16:00Z","status":"ok","seq":123456,"errors":2,"duration":6000.0,"timestamp":"2020-02-07T14:16:00Z","attrs":{"release":"io.sentry@1.0+123","environment":"debug","ip_address":"127.0.0.1","user_agent":"jamesBond"}} diff --git a/sentry-android-core/src/test/resources/envelopes/transaction.txt b/sentry-android-core/src/test/resources/envelopes/transaction.txt new file mode 100644 index 0000000000..a685facab6 --- /dev/null +++ b/sentry-android-core/src/test/resources/envelopes/transaction.txt @@ -0,0 +1,3 @@ +{"event_id":"3367f5196c494acaae85bbbd535379ac","trace":{"trace_id":"b156a475de54423d9c1571df97ec7eb6","public_key":"key"}} +{"type":"transaction","length":640,"content_type":"application/json"} +{"transaction":"a-transaction","type":"transaction","start_timestamp":"2020-10-23T10:24:01.791Z","timestamp":"2020-10-23T10:24:02.791Z","event_id":"3367f5196c494acaae85bbbd535379ac","contexts":{"trace":{"trace_id":"b156a475de54423d9c1571df97ec7eb6","span_id":"0a53026963414893","op":"http","status":"ok"},"custom":{"some-key":"some-value"}},"spans":[{"start_timestamp":"2021-03-05T08:51:12.838Z","timestamp":"2021-03-05T08:51:12.949Z","trace_id":"2b099185293344a5bfdd7ad89ebf9416","span_id":"5b95c29a5ded4281","parent_span_id":"a3b2d1d58b344b07","op":"PersonService.create","description":"desc","status":"aborted","tags":{"name":"value"}}]}