Skip to content

Commit

Permalink
chore(internal): Refactor view hierarchy to be more hybrid SDK friend…
Browse files Browse the repository at this point in the history
…ly (#2492)

Co-authored-by: Sentry Github Bot <bot+github-bot@sentry.io>
  • Loading branch information
krystofwoldrich and getsentry-bot authored Jan 25, 2023
1 parent 37e95ce commit 85fed9a
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 25 deletions.
1 change: 1 addition & 0 deletions sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ public final class io/sentry/android/core/UserInteractionIntegration : android/a
public final class io/sentry/android/core/ViewHierarchyEventProcessor : io/sentry/EventProcessor {
public fun <init> (Lio/sentry/android/core/SentryAndroidOptions;)V
public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent;
public static fun snapshotViewHierarchy (Landroid/app/Activity;Lio/sentry/ILogger;)Lio/sentry/protocol/ViewHierarchy;
public static fun snapshotViewHierarchy (Landroid/view/View;)Lio/sentry/protocol/ViewHierarchy;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import io.sentry.Attachment;
import io.sentry.EventProcessor;
import io.sentry.Hint;
import io.sentry.ILogger;
import io.sentry.SentryEvent;
import io.sentry.SentryLevel;
import io.sentry.android.core.internal.gestures.ViewUtils;
Expand Down Expand Up @@ -41,30 +42,43 @@ public ViewHierarchyEventProcessor(final @NotNull SentryAndroidOptions options)
}

final @Nullable Activity activity = CurrentActivityHolder.getInstance().getActivity();
final @Nullable ViewHierarchy viewHierarchy =
snapshotViewHierarchy(activity, options.getLogger());

if (viewHierarchy != null) {
hint.setViewHierarchy(Attachment.fromViewHierarchy(viewHierarchy));
}

return event;
}

@Nullable
public static ViewHierarchy snapshotViewHierarchy(
@Nullable Activity activity, @NotNull ILogger logger) {
if (activity == null) {
options.getLogger().log(SentryLevel.INFO, "Missing activity for view hierarchy snapshot.");
return event;
logger.log(SentryLevel.INFO, "Missing activity for view hierarchy snapshot.");
return null;
}

final @Nullable Window window = activity.getWindow();
if (window == null) {
options.getLogger().log(SentryLevel.INFO, "Missing window for view hierarchy snapshot.");
return event;
logger.log(SentryLevel.INFO, "Missing window for view hierarchy snapshot.");
return null;
}

final @Nullable View decorView = window.peekDecorView();
if (decorView == null) {
options.getLogger().log(SentryLevel.INFO, "Missing decor view for view hierarchy snapshot.");
return event;
logger.log(SentryLevel.INFO, "Missing decor view for view hierarchy snapshot.");
return null;
}

try {
final @NotNull ViewHierarchy viewHierarchy = snapshotViewHierarchy(decorView);
hint.setViewHierarchy(Attachment.fromViewHierarchy(viewHierarchy));
return viewHierarchy;
} catch (Throwable t) {
options.getLogger().log(SentryLevel.ERROR, "Failed to process view hierarchy.", t);
logger.log(SentryLevel.ERROR, "Failed to process view hierarchy.", t);
return null;
}
return event;
}

@NotNull
Expand Down
1 change: 1 addition & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -3674,6 +3674,7 @@ public final class io/sentry/util/HttpUtils {
public final class io/sentry/util/JsonSerializationUtils {
public fun <init> ()V
public static fun atomicIntegerArrayToList (Ljava/util/concurrent/atomic/AtomicIntegerArray;)Ljava/util/List;
public static fun bytesFrom (Lio/sentry/ISerializer;Lio/sentry/ILogger;Lio/sentry/JsonSerializable;)[B
public static fun calendarToMap (Ljava/util/Calendar;)Ljava/util/Map;
}

Expand Down
23 changes: 8 additions & 15 deletions sentry/src/main/java/io/sentry/SentryEnvelopeItem.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import io.sentry.clientreport.ClientReport;
import io.sentry.exception.SentryEnvelopeException;
import io.sentry.protocol.SentryTransaction;
import io.sentry.util.JsonSerializationUtils;
import io.sentry.util.Objects;
import io.sentry.vendor.Base64;
import java.io.BufferedInputStream;
Expand Down Expand Up @@ -179,21 +180,13 @@ public static SentryEnvelopeItem fromAttachment(
return data;
} else if (attachment.getSerializable() != null) {
final JsonSerializable serializable = attachment.getSerializable();
try {
try (final ByteArrayOutputStream stream = new ByteArrayOutputStream();
final Writer writer =
new BufferedWriter(new OutputStreamWriter(stream, UTF_8))) {

serializer.serialize(serializable, writer);

final byte[] data = stream.toByteArray();
ensureAttachmentSizeLimit(
data.length, maxAttachmentSize, attachment.getFilename());
return data;
}
} catch (Throwable t) {
logger.log(SentryLevel.ERROR, "Could not serialize attachment serializable", t);
throw t;
final @Nullable byte[] data =
JsonSerializationUtils.bytesFrom(serializer, logger, serializable);

if (data != null) {
ensureAttachmentSizeLimit(
data.length, maxAttachmentSize, attachment.getFilename());
return data;
}
} else if (attachment.getPathname() != null) {
return readBytesFromFile(attachment.getPathname(), maxAttachmentSize);
Expand Down
31 changes: 31 additions & 0 deletions sentry/src/main/java/io/sentry/util/JsonSerializationUtils.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
package io.sentry.util;

import io.sentry.ILogger;
import io.sentry.ISerializer;
import io.sentry.JsonSerializable;
import io.sentry.SentryLevel;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
Expand All @@ -8,10 +17,14 @@
import java.util.concurrent.atomic.AtomicIntegerArray;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

@ApiStatus.Internal
public final class JsonSerializationUtils {

@SuppressWarnings("CharsetObjectCanBeUsed")
private static final Charset UTF_8 = Charset.forName("UTF-8");

public static @NotNull Map<String, Object> calendarToMap(final @NotNull Calendar calendar) {
final @NotNull Map<String, Object> map = new HashMap<>();

Expand All @@ -34,4 +47,22 @@ public final class JsonSerializationUtils {
}
return list;
}

public static @Nullable byte[] bytesFrom(
final @NotNull ISerializer serializer,
final @NotNull ILogger logger,
final @NotNull JsonSerializable serializable) {
try {
try (final ByteArrayOutputStream stream = new ByteArrayOutputStream();
final Writer writer = new BufferedWriter(new OutputStreamWriter(stream, UTF_8))) {

serializer.serialize(serializable, writer);

return stream.toByteArray();
}
} catch (Throwable t) {
logger.log(SentryLevel.ERROR, "Could not serialize serializable", t);
return null;
}
}
}
20 changes: 20 additions & 0 deletions sentry/src/test/java/io/sentry/SentryEnvelopeItemTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import io.sentry.protocol.ViewHierarchy
import io.sentry.test.injectForField
import io.sentry.vendor.Base64
import org.junit.Assert.assertArrayEquals
import org.mockito.kotlin.any
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
Expand All @@ -27,6 +28,11 @@ class SentryEnvelopeItemTest {
private class Fixture {
val options = SentryOptions()
val serializer = JsonSerializer(options)
val errorSerializer: JsonSerializer = mock {
on(it.serialize(any<JsonSerializable>(), any())).then {
throw Exception("Mocked exception.")
}
}
val pathname = "hello.txt"
val filename = pathname
val bytes = "hello".toByteArray()
Expand Down Expand Up @@ -250,6 +256,20 @@ class SentryEnvelopeItemTest {
)
}

@Test
fun `fromAttachment with bytesFrom serializable are null`() {
val attachment = Attachment(mock<JsonSerializable>(), "mock-file-name", null, null, false)

val item = SentryEnvelopeItem.fromAttachment(fixture.errorSerializer, fixture.options.logger, attachment, fixture.maxAttachmentSize)

assertFailsWith<SentryEnvelopeException>(
"Couldn't attach the attachment ${attachment.filename}.\n" +
"Please check that either bytes or a path is set."
) {
item.data
}
}

@Test
fun `fromProfilingTrace saves file as Base64`() {
val file = File(fixture.pathname)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
package io.sentry.util

import java.util.Calendar
import io.sentry.ILogger
import io.sentry.JsonSerializable
import io.sentry.JsonSerializer
import org.mockito.invocation.InvocationOnMock
import org.mockito.kotlin.any
import org.mockito.kotlin.mock
import java.io.Writer
import java.util.*
import java.util.concurrent.atomic.AtomicIntegerArray
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.assertNull

class JsonSerializationUtilsTest {

Expand All @@ -30,4 +39,34 @@ class JsonSerializationUtilsTest {
val actual = JsonSerializationUtils.atomicIntegerArrayToList(AtomicIntegerArray(arrayOf(1, 2, 3).toIntArray()))
assertEquals(listOf(1, 2, 3), actual)
}

@Test
fun `returns byte array of given serializable`() {
val mockSerializer: JsonSerializer = mock {
on(it.serialize(any<JsonSerializable>(), any())).then { invocationOnMock: InvocationOnMock ->
val writer: Writer = invocationOnMock.getArgument(1)
writer.write("mock-data")
writer.flush()
}
}
val logger: ILogger = mock()
val serializable: JsonSerializable = mock()
val actualBytes = JsonSerializationUtils.bytesFrom(mockSerializer, logger, serializable)

assertContentEquals("mock-data".toByteArray(), actualBytes, "Byte array should represent the mocked input data.")
}

@Test
fun `return null on serialization error`() {
val mockSerializer: JsonSerializer = mock {
on(it.serialize(any<JsonSerializable>(), any())).then {
throw Exception("Mocked exception.")
}
}
val logger: ILogger = mock()
val serializable: JsonSerializable = mock()
val actualBytes = JsonSerializationUtils.bytesFrom(mockSerializer, logger, serializable)

assertNull(actualBytes, "Mocker error should be captured and null returned.")
}
}

0 comments on commit 85fed9a

Please sign in to comment.