Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Android View Hierarchy support #2440

Merged
merged 29 commits into from
Jan 12, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
8682a9e
Add intial implemention for Android View Hierarchy support
markushi Dec 22, 2022
5e82c3a
Move vh attachment serialization to processor, add tests
markushi Dec 29, 2022
df56bd1
Merge branch 'main' into feat/view-hierarchy
markushi Dec 29, 2022
426d75a
Add changelog
markushi Dec 29, 2022
603c073
Switch sentry-compose-helper to kotlin mutliplatform
markushi Dec 29, 2022
ccc324b
Extract Screenshot activity callbacks to CurrentActivityIntegration
markushi Dec 30, 2022
5b6d756
Fix failing compose tests
markushi Dec 30, 2022
152cf78
Merge branch 'main' into feat/view-hierarchy
markushi Dec 30, 2022
0fd2993
Add missing integration tests for view hierarchy
markushi Jan 3, 2023
e5ab60b
Merge branch 'main' into feat/view-hierarchy
markushi Jan 3, 2023
95b543f
Fix Changelog
markushi Jan 3, 2023
ed189f8
Adapt modifiers / UTF-8 charset handling
markushi Jan 3, 2023
c67d132
Safeguard serialization of view hierarchy
markushi Jan 3, 2023
6f6a909
Update sentry/src/main/java/io/sentry/Hint.java
markushi Jan 4, 2023
1574530
Update sentry/src/main/java/io/sentry/protocol/ViewHierarchy.java
markushi Jan 4, 2023
b4463cf
Update CHANGELOG.md
markushi Jan 4, 2023
2084f33
Merge branch 'main' into feat/view-hierarchy
markushi Jan 4, 2023
cbffc5c
Improve structure based on PR comments
markushi Jan 4, 2023
9f65ca6
Add bytesFactory field to attachment
markushi Jan 5, 2023
040e1a0
Use visibility attribute on Android
markushi Jan 10, 2023
52af78f
Move ScreenshotEventProcessor init
markushi Jan 10, 2023
6077f28
Set proper attachment type for view hierarchy
markushi Jan 10, 2023
2c0efab
Re-use attachment.getBytes() result
markushi Jan 10, 2023
7ce3c98
Move View Hierarchy serialization from processor to SentryEnvelopeItem
markushi Jan 10, 2023
e219a6f
Merge branch 'main' into feat/view-hierarchy
markushi Jan 10, 2023
bbe99b4
Remove unused field
markushi Jan 11, 2023
3d22c9f
Fix don't lookup resource name when view id is not set
markushi Jan 11, 2023
331fb96
Fix typo
markushi Jan 12, 2023
c94745d
Fix do not look up resource names for generated view ids
markushi Jan 12, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import androidx.annotation.NonNull;
import io.sentry.Attachment;
import io.sentry.EventProcessor;
import io.sentry.Hint;
Expand All @@ -14,10 +13,6 @@
import io.sentry.protocol.ViewHierarchy;
import io.sentry.protocol.ViewHierarchyNode;
import io.sentry.util.Objects;
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.List;
Expand Down Expand Up @@ -62,39 +57,20 @@ public ViewHierarchyEventProcessor(final @NotNull SentryAndroidOptions options)
}

final @Nullable View decorView = window.peekDecorView();
options.getLogger().log(SentryLevel.INFO, "Missing decor view for view hierarchy snapshot.");
if (decorView == null) {
markushi marked this conversation as resolved.
Show resolved Hide resolved
options.getLogger().log(SentryLevel.INFO, "Missing decor view for view hierarchy snapshot.");
return event;
}

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

private void attachViewHierarchy(@NonNull ViewHierarchy viewHierarchy, @NonNull Hint hint) {
hint.setViewHierarchy(
Attachment.fromViewHierarchy(
() -> {
try {
try (final ByteArrayOutputStream stream = new ByteArrayOutputStream();
final Writer writer =
new BufferedWriter(new OutputStreamWriter(stream, UTF_8))) {

options.getSerializer().serialize(viewHierarchy, writer);
return stream.toByteArray();
}
} catch (Throwable t) {
options.getLogger().log(SentryLevel.ERROR, "Could not serialize ViewHierarchy", t);
throw t;
}
}));
}

@NotNull
public static ViewHierarchy snapshotViewHierarchy(@NotNull final View view) {
final List<ViewHierarchyNode> windows = new ArrayList<>(1);
Expand Down
8 changes: 4 additions & 4 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,25 @@ public final class io/sentry/AsyncHttpTransportFactory : io/sentry/ITransportFac
}

public final class io/sentry/Attachment {
public fun <init> (Lio/sentry/JsonSerializable;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V
public fun <init> (Ljava/lang/String;)V
public fun <init> (Ljava/lang/String;Ljava/lang/String;)V
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;)V
public fun <init> (Ljava/util/concurrent/Callable;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V
public fun <init> ([BLjava/lang/String;)V
public fun <init> ([BLjava/lang/String;Ljava/lang/String;)V
public fun <init> ([BLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V
public fun <init> ([BLjava/lang/String;Ljava/lang/String;Z)V
public static fun fromScreenshot ([B)Lio/sentry/Attachment;
public static fun fromViewHierarchy (Ljava/util/concurrent/Callable;)Lio/sentry/Attachment;
public static fun fromViewHierarchy (Lio/sentry/protocol/ViewHierarchy;)Lio/sentry/Attachment;
public fun getAttachmentType ()Ljava/lang/String;
public fun getBytes ()[B
public fun getBytesFactory ()Ljava/util/concurrent/Callable;
public fun getContentType ()Ljava/lang/String;
public fun getFilename ()Ljava/lang/String;
public fun getPathname ()Ljava/lang/String;
public fun getSerializable ()Lio/sentry/JsonSerializable;
}

public final class io/sentry/Baggage {
Expand Down Expand Up @@ -1278,7 +1278,7 @@ public final class io/sentry/SentryEnvelopeHeader$JsonKeys {
}

public final class io/sentry/SentryEnvelopeItem {
public static fun fromAttachment (Lio/sentry/Attachment;J)Lio/sentry/SentryEnvelopeItem;
public static fun fromAttachment (Lio/sentry/ISerializer;Lio/sentry/ILogger;Lio/sentry/Attachment;J)Lio/sentry/SentryEnvelopeItem;
public static fun fromClientReport (Lio/sentry/ISerializer;Lio/sentry/clientreport/ClientReport;)Lio/sentry/SentryEnvelopeItem;
public static fun fromEvent (Lio/sentry/ISerializer;Lio/sentry/SentryBaseEvent;)Lio/sentry/SentryEnvelopeItem;
public static fun fromProfilingTrace (Lio/sentry/ProfilingTraceData;JLio/sentry/ISerializer;)Lio/sentry/SentryEnvelopeItem;
Expand Down
28 changes: 14 additions & 14 deletions sentry/src/main/java/io/sentry/Attachment.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
package io.sentry;

import io.sentry.protocol.ViewHierarchy;
import java.io.File;
import java.util.concurrent.Callable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/** You can use an attachment to store additional files alongside an event or transaction. */
public final class Attachment {

private @Nullable byte[] bytes;
private final @Nullable Callable<byte[]> bytesFactory;
private final @Nullable JsonSerializable serializable;
private @Nullable String pathname;
private final @NotNull String filename;
private final @Nullable String contentType;
Expand Down Expand Up @@ -83,7 +83,7 @@ public Attachment(
final @Nullable String attachmentType,
final boolean addToTransactions) {
this.bytes = bytes;
this.bytesFactory = null;
this.serializable = null;
this.filename = filename;
this.contentType = contentType;
this.attachmentType = attachmentType;
Expand All @@ -94,21 +94,21 @@ public Attachment(
* Initializes an Attachment with bytes factory, a filename, a content type, and
* addToTransactions.
*
* @param bytesFactory The bytes factory providing the data when being called
* @param serializable A json serializable holding the attachment payload
* @param filename The name of the attachment to display in Sentry.
* @param contentType The content type of the attachment.
* @param attachmentType the attachment type.
* @param addToTransactions <code>true</code> if the SDK should add this attachment to every
* {@link ITransaction} or set to <code>false</code> if it shouldn't.
*/
public Attachment(
final @NotNull Callable<byte[]> bytesFactory,
final @NotNull JsonSerializable serializable,
final @NotNull String filename,
final @Nullable String contentType,
final @Nullable String attachmentType,
final boolean addToTransactions) {
this.bytes = null;
this.bytesFactory = bytesFactory;
this.serializable = serializable;
this.filename = filename;
this.contentType = contentType;
this.attachmentType = attachmentType;
Expand Down Expand Up @@ -185,7 +185,7 @@ public Attachment(
final boolean addToTransactions) {
this.pathname = pathname;
this.filename = filename;
this.bytesFactory = null;
this.serializable = null;
this.contentType = contentType;
this.attachmentType = attachmentType;
this.addToTransactions = addToTransactions;
Expand All @@ -211,7 +211,7 @@ public Attachment(
final boolean addToTransactions) {
this.pathname = pathname;
this.filename = filename;
this.bytesFactory = null;
this.serializable = null;
this.contentType = contentType;
this.addToTransactions = addToTransactions;
}
Expand Down Expand Up @@ -239,7 +239,7 @@ public Attachment(
final @Nullable String attachmentType) {
this.pathname = pathname;
this.filename = filename;
this.bytesFactory = null;
this.serializable = null;
this.contentType = contentType;
this.addToTransactions = addToTransactions;
this.attachmentType = attachmentType;
Expand All @@ -259,8 +259,8 @@ public Attachment(
*
* @return the bytes factory responsible for providing the bytes.
*/
public @Nullable Callable<byte[]> getBytesFactory() {
return bytesFactory;
public @Nullable JsonSerializable getSerializable() {
return serializable;
}

/**
Expand Down Expand Up @@ -323,12 +323,12 @@ boolean isAddToTransactions() {
/**
* Creates a new View Hierarchy Attachment
*
* @param bytesFactory the serialized View Hierarchy
* @param viewHierarchy the View Hierarchy
* @return the Attachment
*/
public static @NotNull Attachment fromViewHierarchy(final Callable<byte[]> bytesFactory) {
public static @NotNull Attachment fromViewHierarchy(final ViewHierarchy viewHierarchy) {
return new Attachment(
bytesFactory,
viewHierarchy,
"view-hierarchy.json",
"application/json",
VIEW_HIERARCHY_ATTACHMENT_TYPE,
Expand Down
6 changes: 5 additions & 1 deletion sentry/src/main/java/io/sentry/SentryClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,11 @@ private boolean shouldSendSessionUpdateForDroppedEvent(
if (attachments != null) {
for (final Attachment attachment : attachments) {
final SentryEnvelopeItem attachmentItem =
SentryEnvelopeItem.fromAttachment(attachment, options.getMaxAttachmentSize());
SentryEnvelopeItem.fromAttachment(
romtsn marked this conversation as resolved.
Show resolved Hide resolved
options.getSerializer(),
options.getLogger(),
attachment,
options.getMaxAttachmentSize());
envelopeItems.add(attachmentItem);
}
}
Expand Down
27 changes: 22 additions & 5 deletions sentry/src/main/java/io/sentry/SentryEnvelopeItem.java
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,10 @@ public static SentryEnvelopeItem fromUserFeedback(
}

public static SentryEnvelopeItem fromAttachment(
final @NotNull Attachment attachment, final long maxAttachmentSize) {
final @NotNull ISerializer serializer,
final @NotNull ILogger logger,
final @NotNull Attachment attachment,
final long maxAttachmentSize) {

final CachedItem cachedItem =
new CachedItem(
Expand All @@ -174,10 +177,24 @@ public static SentryEnvelopeItem fromAttachment(
final byte[] data = attachment.getBytes();
ensureAttachmentSizeLimit(data.length, maxAttachmentSize, attachment.getFilename());
return data;
} else if (attachment.getBytesFactory() != null) {
final byte[] data = attachment.getBytesFactory().call();
ensureAttachmentSizeLimit(data.length, maxAttachmentSize, attachment.getFilename());
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;
}
} else if (attachment.getPathname() != null) {
return readBytesFromFile(attachment.getPathname(), maxAttachmentSize);
}
Expand Down
7 changes: 4 additions & 3 deletions sentry/src/test/java/io/sentry/AttachmentTest.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.sentry

import io.sentry.protocol.ViewHierarchy
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
Expand Down Expand Up @@ -115,14 +116,14 @@ class AttachmentTest {

@Test
fun `creates attachment from view hierarchy`() {
val bytes = byteArrayOf(1, 2, 3)
val attachment = Attachment.fromViewHierarchy { bytes }
val hierarchy = ViewHierarchy("android", emptyList())
val attachment = Attachment.fromViewHierarchy(hierarchy)

assertEquals("view-hierarchy.json", attachment.filename)
assertEquals("application/json", attachment.contentType)
assertEquals(false, attachment.isAddToTransactions)
assertEquals("event.view_hierarchy", attachment.attachmentType)
assertNull(attachment.bytes)
assertEquals(bytes, attachment.bytesFactory!!.call())
assertEquals(hierarchy, attachment.serializable)
}
}
9 changes: 5 additions & 4 deletions sentry/src/test/java/io/sentry/JsonSerializerTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,15 @@ class JsonSerializerTest {
val serializer: ISerializer
val hub = mock<IHub>()
val traceFile = Files.createTempFile("test", "here").toFile()
val options = SentryOptions()

init {
val options = SentryOptions()
options.dsn = "https://key@sentry.io/proj"
options.setLogger(logger)
options.setDebug(true)
options.isDebug = true
whenever(hub.options).thenReturn(options)
serializer = JsonSerializer(options)
options.setSerializer(serializer)
options.setEnvelopeReader(EnvelopeReader(serializer))
}
}
Expand Down Expand Up @@ -946,9 +947,9 @@ class JsonSerializerTest {

val message = "hello"
val attachment = Attachment(message.toByteArray(), "bytes.txt")
val validAttachmentItem = SentryEnvelopeItem.fromAttachment(attachment, 5)
val validAttachmentItem = SentryEnvelopeItem.fromAttachment(fixture.serializer, fixture.options.logger, attachment, 5)

val invalidAttachmentItem = SentryEnvelopeItem.fromAttachment(Attachment("no"), 5)
val invalidAttachmentItem = SentryEnvelopeItem.fromAttachment(fixture.serializer, fixture.options.logger, Attachment("no"), 5)
val envelope = SentryEnvelope(header, listOf(invalidAttachmentItem, validAttachmentItem))

val actualJson = serializeToString(envelope)
Expand Down
5 changes: 3 additions & 2 deletions sentry/src/test/java/io/sentry/SentryClientTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import io.sentry.protocol.SentryException
import io.sentry.protocol.SentryId
import io.sentry.protocol.SentryTransaction
import io.sentry.protocol.User
import io.sentry.protocol.ViewHierarchy
import io.sentry.test.callMethod
import io.sentry.transport.ITransport
import io.sentry.transport.ITransportGate
Expand Down Expand Up @@ -1423,7 +1424,7 @@ class SentryClientTest {
@Test
fun `view hierarchy is added to the envelope from the hint`() {
val sut = fixture.getSut()
val attachment = Attachment.fromViewHierarchy { byteArrayOf() }
val attachment = Attachment.fromViewHierarchy(ViewHierarchy("android_view_system", emptyList()))
val hint = Hint().also { it.viewHierarchy = attachment }

sut.captureEvent(SentryEvent(), hint)
Expand All @@ -1443,7 +1444,7 @@ class SentryClientTest {
fun `view hierarchy is dropped from hint via before send`() {
fixture.sentryOptions.beforeSend = CustomBeforeSendCallback()
val sut = fixture.getSut()
val attachment = Attachment.fromViewHierarchy { byteArrayOf() }
val attachment = Attachment.fromViewHierarchy(ViewHierarchy("android_view_system", emptyList()))
val hint = Hint().also { it.viewHierarchy = attachment }

sut.captureEvent(SentryEvent(), hint)
Expand Down
Loading