diff --git a/CHANGELOG.md b/CHANGELOG.md index 3509b35f6ec..a9c04ac5c22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Allow setting SDK info (name & version) in manifest ([#2016](https://github.com/getsentry/sentry-java/pull/2016)) - Allow setting native Android SDK name during build ([#2035](https://github.com/getsentry/sentry-java/pull/2035)) - Hints are now used via a Hints object and passed into beforeSend and EventProcessor as @NotNull Hints object ([#2045](https://github.com/getsentry/sentry-java/pull/2045)) +- Attachments can be manipulated via hints ([#2046](https://github.com/getsentry/sentry-java/pull/2046)) ## 6.0.0-beta.3 diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ScreenshotEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/ScreenshotEventProcessor.java index 6fed7fcf3b8..4630e6a7bde 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ScreenshotEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ScreenshotEventProcessor.java @@ -1,7 +1,6 @@ package io.sentry.android.core; import static io.sentry.TypeCheckHint.ANDROID_ACTIVITY; -import static io.sentry.TypeCheckHint.SENTRY_SCREENSHOT; import android.annotation.SuppressLint; import android.app.Activity; @@ -93,9 +92,7 @@ public ScreenshotEventProcessor( if (byteArrayOutputStream.size() > 0) { // screenshot png is around ~100-150 kb - hints.set( - SENTRY_SCREENSHOT, - Attachment.fromScreenshot(byteArrayOutputStream.toByteArray())); + hints.setScreenshot(Attachment.fromScreenshot(byteArrayOutputStream.toByteArray())); hints.set(ANDROID_ACTIVITY, activity); } else { this.options diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ScreenshotEventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ScreenshotEventProcessorTest.kt index 2e8ef405e79..0db193574a1 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ScreenshotEventProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ScreenshotEventProcessorTest.kt @@ -14,7 +14,6 @@ import io.sentry.Attachment import io.sentry.MainEventProcessor import io.sentry.SentryEvent import io.sentry.TypeCheckHint.ANDROID_ACTIVITY -import io.sentry.TypeCheckHint.SENTRY_SCREENSHOT import io.sentry.hints.Hints import org.junit.runner.RunWith import kotlin.test.BeforeTest @@ -103,7 +102,7 @@ class ScreenshotEventProcessorTest { val event = fixture.mainProcessor.process(getEvent(), hints) sut.process(event, hints) - assertNull(hints[SENTRY_SCREENSHOT]) + assertNull(hints.screenshot) } @Test @@ -116,7 +115,7 @@ class ScreenshotEventProcessorTest { val event = fixture.mainProcessor.process(SentryEvent(), hints) sut.process(event, hints) - assertNull(hints[SENTRY_SCREENSHOT]) + assertNull(hints.screenshot) } @Test @@ -127,7 +126,7 @@ class ScreenshotEventProcessorTest { val event = fixture.mainProcessor.process(getEvent(), hints) sut.process(event, hints) - assertNull(hints[SENTRY_SCREENSHOT]) + assertNull(hints.screenshot) } @Test @@ -141,7 +140,7 @@ class ScreenshotEventProcessorTest { val event = fixture.mainProcessor.process(getEvent(), hints) sut.process(event, hints) - assertNull(hints[SENTRY_SCREENSHOT]) + assertNull(hints.screenshot) } @Test @@ -156,7 +155,7 @@ class ScreenshotEventProcessorTest { val event = fixture.mainProcessor.process(getEvent(), hints) sut.process(event, hints) - assertNull(hints[SENTRY_SCREENSHOT]) + assertNull(hints.screenshot) } @Test @@ -169,7 +168,7 @@ class ScreenshotEventProcessorTest { val event = fixture.mainProcessor.process(getEvent(), hints) sut.process(event, hints) - val screenshot = hints[SENTRY_SCREENSHOT] + val screenshot = hints.screenshot assertTrue(screenshot is Attachment) assertEquals("screenshot.png", screenshot.filename) assertEquals("image/png", screenshot.contentType) @@ -188,7 +187,7 @@ class ScreenshotEventProcessorTest { val event = fixture.mainProcessor.process(getEvent(), hints) sut.process(event, hints) - assertNull(hints[SENTRY_SCREENSHOT]) + assertNull(hints.screenshot) } private fun getEvent(): SentryEvent = SentryEvent(Throwable("Throwable")) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 2489f2d3bb0..b2195ad59b4 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -1638,7 +1638,6 @@ public final class io/sentry/TypeCheckHint { public static final field OKHTTP_RESPONSE Ljava/lang/String; public static final field OPEN_FEIGN_REQUEST Ljava/lang/String; public static final field OPEN_FEIGN_RESPONSE Ljava/lang/String; - public static final field SENTRY_SCREENSHOT Ljava/lang/String; public static final field SENTRY_SYNTHETIC_EXCEPTION Ljava/lang/String; public static final field SENTRY_TYPE_CHECK_HINT Ljava/lang/String; public static final field SERVLET_REQUEST Ljava/lang/String; @@ -1848,9 +1847,19 @@ public abstract interface class io/sentry/hints/Flushable { public final class io/sentry/hints/Hints { public fun ()V + public fun addAttachment (Lio/sentry/Attachment;)V + public fun addAttachments (Ljava/util/List;)V + public fun clearAttachments ()V public fun get (Ljava/lang/String;)Ljava/lang/Object; + public fun getAs (Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Object; + public fun getAttachments ()Ljava/util/List; + public fun getScreenshot ()Lio/sentry/Attachment; public fun remove (Ljava/lang/String;)V + public fun replaceAttachments (Ljava/util/List;)V public fun set (Ljava/lang/String;Ljava/lang/Object;)V + public fun setScreenshot (Lio/sentry/Attachment;)V + public static fun withAttachment (Lio/sentry/Attachment;)Lio/sentry/hints/Hints; + public static fun withAttachments (Ljava/util/List;)Lio/sentry/hints/Hints; } public abstract interface class io/sentry/hints/Resettable { diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index 790434e6c96..5fd62f9b27d 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -1,7 +1,5 @@ package io.sentry; -import static io.sentry.TypeCheckHint.SENTRY_SCREENSHOT; - import io.sentry.clientreport.DiscardReason; import io.sentry.exception.SentryEnvelopeException; import io.sentry.hints.DiskFlushNotification; @@ -80,6 +78,11 @@ private boolean shouldApplyScopeData( hints = new Hints(); } + // TODO what does cached etc mean for devs manipulating hints in beforeSend and eventProcessor? + if (shouldApplyScopeData(event, hints)) { + addScopeAttachmentsToHints(scope, hints); + } + options.getLogger().log(SentryLevel.DEBUG, "Capturing event: %s", event.getEventId()); if (event != null) { @@ -173,7 +176,7 @@ private boolean shouldApplyScopeData( ? scope.getTransaction().traceState() : null; final boolean shouldSendAttachments = event != null; - List attachments = shouldSendAttachments ? getAttachments(scope, hints) : null; + List attachments = shouldSendAttachments ? getAttachments(hints) : null; final SentryEnvelope envelope = buildEnvelope(event, attachments, session, traceState, null); if (envelope != null) { @@ -189,6 +192,12 @@ private boolean shouldApplyScopeData( return sentryId; } + private void addScopeAttachmentsToHints(@Nullable Scope scope, @NotNull Hints hints) { + if (scope != null) { + hints.addAttachments(scope.getAttachments()); + } + } + private boolean shouldSendSessionUpdateForDroppedEvent( @Nullable Session sessionBeforeUpdate, @Nullable Session sessionAfterUpdate) { if (sessionAfterUpdate == null) { @@ -215,21 +224,12 @@ private boolean shouldSendSessionUpdateForDroppedEvent( return false; } - private @Nullable List getAttachments( - final @Nullable Scope scope, final @NotNull Hints hints) { - List attachments = null; - if (scope != null) { - attachments = scope.getAttachments(); - } - - final Object screenshotAttachment = hints.get(SENTRY_SCREENSHOT); - if (screenshotAttachment instanceof Attachment) { - - if (attachments == null) { - attachments = new ArrayList<>(); - } + private @Nullable List getAttachments(final @NotNull Hints hints) { + @NotNull final List attachments = hints.getAttachments(); - attachments.add((Attachment) screenshotAttachment); + @Nullable final Attachment screenshot = hints.getScreenshot(); + if (screenshot != null) { + attachments.add(screenshot); } return attachments; @@ -506,6 +506,10 @@ public void captureSession(final @NotNull Session session, final @Nullable Hints hints = new Hints(); } + if (shouldApplyScopeData(transaction, hints)) { + addScopeAttachmentsToHints(scope, hints); + } + options .getLogger() .log(SentryLevel.DEBUG, "Capturing transaction: %s", transaction.getEventId()); @@ -540,7 +544,7 @@ public void captureSession(final @NotNull Session session, final @Nullable Hints final SentryEnvelope envelope = buildEnvelope( transaction, - filterForTransaction(getAttachments(scope, hints)), + filterForTransaction(getAttachments(hints)), null, traceState, profilingTraceData); diff --git a/sentry/src/main/java/io/sentry/TypeCheckHint.java b/sentry/src/main/java/io/sentry/TypeCheckHint.java index 9bf5a79f14f..cc50b40c378 100644 --- a/sentry/src/main/java/io/sentry/TypeCheckHint.java +++ b/sentry/src/main/java/io/sentry/TypeCheckHint.java @@ -25,8 +25,6 @@ public final class TypeCheckHint { public static final String ANDROID_VIEW = "android:view"; /** Used for Fragment breadcrumbs. */ public static final String ANDROID_FRAGMENT = "android:fragment"; - /** Used for screenshots. */ - public static final String SENTRY_SCREENSHOT = "sentry:screenshot"; /** Used for OkHttp response breadcrumbs. */ public static final String OKHTTP_RESPONSE = "okHttp:response"; diff --git a/sentry/src/main/java/io/sentry/hints/Hints.java b/sentry/src/main/java/io/sentry/hints/Hints.java index 51ea2b57ca6..0475df60cf7 100644 --- a/sentry/src/main/java/io/sentry/hints/Hints.java +++ b/sentry/src/main/java/io/sentry/hints/Hints.java @@ -1,29 +1,108 @@ package io.sentry.hints; +import io.sentry.Attachment; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; public final class Hints { + private static final @NotNull Map> PRIMITIVE_MAPPINGS; + + static { + PRIMITIVE_MAPPINGS = new HashMap<>(); + PRIMITIVE_MAPPINGS.put("boolean", Boolean.class); + PRIMITIVE_MAPPINGS.put("char", Character.class); + PRIMITIVE_MAPPINGS.put("byte", Byte.class); + PRIMITIVE_MAPPINGS.put("short", Short.class); + PRIMITIVE_MAPPINGS.put("int", Integer.class); + PRIMITIVE_MAPPINGS.put("long", Long.class); + PRIMITIVE_MAPPINGS.put("float", Float.class); + PRIMITIVE_MAPPINGS.put("double", Double.class); + } + private final @NotNull Map internalStorage = new HashMap(); + private final @NotNull List attachments = new ArrayList<>(); + private @Nullable Attachment screenshot = null; - public void set(@NotNull String hintType, @Nullable Object hint) { - internalStorage.put(hintType, hint); + public static @NotNull Hints withAttachment(@Nullable Attachment attachment) { + @NotNull final Hints hints = new Hints(); + hints.addAttachment(attachment); + return hints; } - public @Nullable Object get(@NotNull String hintType) { - return internalStorage.get(hintType); + public static @NotNull Hints withAttachments(@Nullable List attachments) { + @NotNull final Hints hints = new Hints(); + hints.addAttachments(attachments); + return hints; } - // TODO maybe not public - public void remove(@NotNull String hintType) { - internalStorage.remove(hintType); + public void set(@NotNull String name, @Nullable Object hint) { + internalStorage.put(name, hint); } - // TODO addAttachment(one) - // TODO getAttachments(): List - // TODO setAttachments(list) - // TODO clearAttachments() + public @Nullable Object get(@NotNull String name) { + return internalStorage.get(name); + } + + @SuppressWarnings("unchecked") + public @Nullable T getAs(@NotNull String name, @NotNull Class clazz) { + Object hintValue = internalStorage.get(name); + + if (clazz.isInstance(hintValue)) { + return (T) hintValue; + } else if (isCastablePrimitive(hintValue, clazz)) { + return (T) hintValue; + } else { + return null; + } + } + + public void remove(@NotNull String name) { + internalStorage.remove(name); + } + + public void addAttachment(@Nullable Attachment attachment) { + if (attachment != null) { + attachments.add(attachment); + } + } + + public void addAttachments(@Nullable List attachments) { + if (attachments != null) { + this.attachments.addAll(attachments); + } + } + + public @NotNull List getAttachments() { + return new ArrayList<>(attachments); + } + + public void replaceAttachments(@Nullable List attachments) { + clearAttachments(); + addAttachments(attachments); + } + + public void clearAttachments() { + attachments.clear(); + } + + public void setScreenshot(@Nullable Attachment screenshot) { + this.screenshot = screenshot; + } + + public @Nullable Attachment getScreenshot() { + return screenshot; + } + + private boolean isCastablePrimitive(@Nullable Object hintValue, @NotNull Class clazz) { + Class nonPrimitiveClass = PRIMITIVE_MAPPINGS.get(clazz.getCanonicalName()); + return hintValue != null + && clazz.isPrimitive() + && nonPrimitiveClass != null + && nonPrimitiveClass.isInstance(hintValue); + } } diff --git a/sentry/src/main/java/io/sentry/util/HintUtils.java b/sentry/src/main/java/io/sentry/util/HintUtils.java index cf7bafb8fcb..78e802076a4 100644 --- a/sentry/src/main/java/io/sentry/util/HintUtils.java +++ b/sentry/src/main/java/io/sentry/util/HintUtils.java @@ -10,7 +10,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -/** Util class for Applying or not scope's data to an event */ +/** Util class dealing with Hints as not to pollute the Hints API with internal functionality */ @ApiStatus.Internal public final class HintUtils { diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index 0429f583c7e..b3672b04c14 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -14,7 +14,6 @@ import com.nhaarman.mockitokotlin2.times import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions import com.nhaarman.mockitokotlin2.whenever -import io.sentry.TypeCheckHint.SENTRY_SCREENSHOT import io.sentry.clientreport.ClientReportTestHelper.Companion.assertClientReport import io.sentry.clientreport.DiscardReason import io.sentry.clientreport.DiscardedEvent @@ -84,6 +83,8 @@ class SentryClientTest { } var attachment = Attachment("hello".toByteArray(), "hello.txt", "text/plain", true) + var attachment2 = Attachment("hello2".toByteArray(), "hello2.txt", "text/plain", true) + var attachment3 = Attachment("hello3".toByteArray(), "hello3.txt", "text/plain", true) val profilingTraceFile = Files.createTempFile("trace", ".trace").toFile() var profilingTraceData = ProfilingTraceData(profilingTraceFile, sentryTracer) var profilingNonExistingTraceData = ProfilingTraceData(File("non_existent.trace"), sentryTracer) @@ -1282,7 +1283,7 @@ class SentryClientTest { fun `screenshot is added to the envelope from the hint`() { val sut = fixture.getSut() val attachment = Attachment.fromScreenshot(byteArrayOf()) - val hints = Hints().also { it.set(SENTRY_SCREENSHOT, attachment) } + val hints = Hints().also { it.screenshot = attachment } sut.captureEvent(SentryEvent(), hints) @@ -1302,7 +1303,7 @@ class SentryClientTest { fixture.sentryOptions.beforeSend = CustomBeforeSendCallback() val sut = fixture.getSut() val attachment = Attachment.fromScreenshot(byteArrayOf()) - val hints = Hints().also { it.set(SENTRY_SCREENSHOT, attachment) } + val hints = Hints().also { it.screenshot = attachment } sut.captureEvent(SentryEvent(), hints) @@ -1592,6 +1593,234 @@ class SentryClientTest { ) } + @Test + fun `can pass an attachment via hints`() { + val sut = fixture.getSut() + + sut.captureException(IllegalStateException(), Hints.withAttachment(fixture.attachment)) + + thenEnvelopeIsSentWith(eventCount = 1, sessionCount = 0, attachmentCount = 1) + } + + @Test + fun `an attachment passed via hint is used with scope attachments`() { + val sut = fixture.getSut() + + val scope = givenScopeWithStartedSession() + scope.addAttachment(fixture.attachment2) + sut.captureException(IllegalStateException(), scope, Hints.withAttachment(fixture.attachment)) + + thenEnvelopeIsSentWith(eventCount = 1, sessionCount = 1, attachmentCount = 2) + } + + @Test + fun `can add to attachments in beforeSend`() { + val sut = fixture.getSut { options -> + options.setBeforeSend { event, hints -> + assertEquals(listOf(fixture.attachment, fixture.attachment2), hints.attachments) + hints.addAttachment(fixture.attachment3) + event + } + } + + val scope = givenScopeWithStartedSession() + scope.addAttachment(fixture.attachment2) + sut.captureException(IllegalStateException(), scope, Hints.withAttachment(fixture.attachment)) + + thenEnvelopeIsSentWith(eventCount = 1, sessionCount = 1, attachmentCount = 3) + } + + @Test + fun `can replace attachments in beforeSend`() { + val sut = fixture.getSut { options -> + options.setBeforeSend { event, hints -> + hints.replaceAttachments(listOf(fixture.attachment)) + event + } + } + + val scope = givenScopeWithStartedSession() + scope.addAttachment(fixture.attachment2) + sut.captureException(IllegalStateException(), scope, Hints.withAttachment(fixture.attachment)) + + thenEnvelopeIsSentWith(eventCount = 1, sessionCount = 1, attachmentCount = 1) + } + + @Test + fun `can add to attachments in eventProcessor`() { + val sut = fixture.getSut { options -> + options.addEventProcessor(object : EventProcessor { + override fun process(event: SentryEvent, hints: Hints): SentryEvent? { + assertEquals(listOf(fixture.attachment, fixture.attachment2), hints.attachments) + hints.addAttachment(fixture.attachment3) + return event + } + + override fun process( + transaction: SentryTransaction, + hints: Hints + ): SentryTransaction? { + return transaction + } + }) + } + + val scope = givenScopeWithStartedSession() + scope.addAttachment(fixture.attachment2) + sut.captureException(IllegalStateException(), scope, Hints.withAttachment(fixture.attachment)) + + thenEnvelopeIsSentWith(eventCount = 1, sessionCount = 1, attachmentCount = 3) + } + + @Test + fun `can replace attachments in eventProcessor`() { + val sut = fixture.getSut { options -> + options.addEventProcessor(object : EventProcessor { + override fun process(event: SentryEvent, hints: Hints): SentryEvent? { + hints.replaceAttachments(listOf(fixture.attachment)) + return event + } + + override fun process( + transaction: SentryTransaction, + hints: Hints + ): SentryTransaction? { + return transaction + } + }) + } + + val scope = givenScopeWithStartedSession() + scope.addAttachment(fixture.attachment2) + sut.captureException(IllegalStateException(), scope, Hints.withAttachment(fixture.attachment)) + + thenEnvelopeIsSentWith(eventCount = 1, sessionCount = 1, attachmentCount = 1) + } + + @Test + fun `can pass an attachment via hints for transactions`() { + val sut = fixture.getSut() + val scope = createScope() + + sut.captureTransaction( + SentryTransaction(fixture.sentryTracer), + scope, + Hints.withAttachment(fixture.attachment) + ) + + thenEnvelopeIsSentWith(eventCount = 0, sessionCount = 0, attachmentCount = 1, transactionCount = 1) + } + + @Test + fun `an attachment passed via hint is used with scope attachments for transactions`() { + val sut = fixture.getSut() + + val scope = givenScopeWithStartedSession() + scope.addAttachment(fixture.attachment2) + + sut.captureTransaction( + SentryTransaction(fixture.sentryTracer), + scope, + Hints.withAttachment(fixture.attachment) + ) + + thenEnvelopeIsSentWith(eventCount = 0, sessionCount = 0, attachmentCount = 2, transactionCount = 1) + } + + @Test + fun `can add to attachments in eventProcessor for transactions`() { + val sut = fixture.getSut { options -> + options.addEventProcessor(object : EventProcessor { + override fun process(event: SentryEvent, hints: Hints): SentryEvent? { + return event + } + + override fun process( + transaction: SentryTransaction, + hints: Hints + ): SentryTransaction? { + assertEquals(listOf(fixture.attachment, fixture.attachment2), hints.attachments) + hints.addAttachment(fixture.attachment3) + return transaction + } + }) + } + + val scope = givenScopeWithStartedSession() + scope.addAttachment(fixture.attachment2) + + sut.captureTransaction( + SentryTransaction(fixture.sentryTracer), + scope, + Hints.withAttachment(fixture.attachment) + ) + + thenEnvelopeIsSentWith(eventCount = 0, sessionCount = 0, attachmentCount = 3, transactionCount = 1) + } + + @Test + fun `can replace attachments in eventProcessor for transactions`() { + val sut = fixture.getSut { options -> + options.addEventProcessor(object : EventProcessor { + override fun process(event: SentryEvent, hints: Hints): SentryEvent? { + return event + } + + override fun process( + transaction: SentryTransaction, + hints: Hints + ): SentryTransaction? { + hints.replaceAttachments(listOf(fixture.attachment)) + return transaction + } + }) + } + + val scope = givenScopeWithStartedSession() + scope.addAttachment(fixture.attachment2) + + sut.captureTransaction( + SentryTransaction(fixture.sentryTracer), + scope, + Hints.withAttachment(fixture.attachment) + ) + + thenEnvelopeIsSentWith(eventCount = 0, sessionCount = 0, attachmentCount = 1, transactionCount = 1) + } + + @Test + fun `passing attachments via hint into breadcrumb ignores them`() { + val sut = fixture.getSut { options -> + options.setBeforeBreadcrumb { breadcrumb, hints -> + breadcrumb + } + } + + val scope = givenScopeWithStartedSession() + scope.addBreadcrumb(Breadcrumb.info("hello from breadcrumb"), Hints.withAttachment(fixture.attachment)) + + sut.captureException(IllegalStateException(), scope) + + thenEnvelopeIsSentWith(eventCount = 1, sessionCount = 1, attachmentCount = 0) + } + + @Test + fun `adding attachments in beforeBreadcrumb ignores them`() { + val sut = fixture.getSut { options -> + options.setBeforeBreadcrumb { breadcrumb, hints -> + hints.addAttachment(fixture.attachment) + breadcrumb + } + } + + val scope = givenScopeWithStartedSession() + scope.addBreadcrumb(Breadcrumb.info("hello from breadcrumb")) + + sut.captureException(IllegalStateException(), scope) + + thenEnvelopeIsSentWith(eventCount = 1, sessionCount = 1, attachmentCount = 0) + } + private fun givenScopeWithStartedSession(errored: Boolean = false, crashed: Boolean = false): Scope { val scope = createScope(fixture.sentryOptions) scope.startSession() @@ -1611,7 +1840,7 @@ class SentryClientTest { verify(fixture.transport, never()).send(anyOrNull(), anyOrNull()) } - private fun thenEnvelopeIsSentWith(eventCount: Int, sessionCount: Int) { + private fun thenEnvelopeIsSentWith(eventCount: Int, sessionCount: Int, attachmentCount: Int = 0, transactionCount: Int = 0) { val argumentCaptor = argumentCaptor() verify(fixture.transport, times(1)).send(argumentCaptor.capture(), anyOrNull()) @@ -1619,6 +1848,8 @@ class SentryClientTest { val envelopeItemTypes = envelope.items.map { it.header.type } assertEquals(eventCount, envelopeItemTypes.count { it == SentryItemType.Event }) assertEquals(sessionCount, envelopeItemTypes.count { it == SentryItemType.Session }) + assertEquals(attachmentCount, envelopeItemTypes.count { it == SentryItemType.Attachment }) + assertEquals(transactionCount, envelopeItemTypes.count { it == SentryItemType.Transaction }) } private fun thenSessionIsStillOK(scope: Scope) { @@ -1641,7 +1872,7 @@ class SentryClientTest { class CustomBeforeSendCallback : SentryOptions.BeforeSendCallback { override fun execute(event: SentryEvent, hints: Hints): SentryEvent? { - hints.remove(SENTRY_SCREENSHOT) + hints.screenshot = null return event } diff --git a/sentry/src/test/java/io/sentry/hints/HintsTest.kt b/sentry/src/test/java/io/sentry/hints/HintsTest.kt new file mode 100644 index 00000000000..92e6d302cd9 --- /dev/null +++ b/sentry/src/test/java/io/sentry/hints/HintsTest.kt @@ -0,0 +1,202 @@ +package io.sentry.hints + +import io.sentry.Attachment +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class HintsTest { + @Test + fun `getting as wrong class returns null`() { + val hints = Hints() + hints.set("hint1", "not a number") + + assertNull(hints.getAs("hint1", Int::class.java)) + } + + @Test + fun `getting as correct class returns it`() { + val hints = Hints() + hints.set("hint1", "some string") + + assertEquals("some string", hints.getAs("hint1", String::class.java)) + } + + @Test + fun `getting casted returns null if not contained`() { + val hints = Hints() + assertNull(hints.getAs("hint-does-not-exist", Int::class.java)) + } + + @Test + fun `getting returns null if not contained`() { + val hints = Hints() + assertNull(hints.get("hint-does-not-exist")) + } + + @Test + fun `kotlin java interop for primitives works for float`() { + val hints = Hints() + hints.set("hint1", 1.3f) + assertEquals(1.3f, hints.getAs("hint1", Float::class.java)) + } + + @Test + fun `kotlin java interop for primitives works for double`() { + val hints = Hints() + hints.set("hint1", 1.4) + assertEquals(1.4, hints.getAs("hint1", Double::class.java)) + } + + @Test + fun `kotlin java interop for primitives works for long`() { + val hints = Hints() + hints.set("hint1", 1718L) + assertEquals(1718L, hints.getAs("hint1", Long::class.java)) + } + + @Test + fun `kotlin java interop for primitives works for int`() { + val hints = Hints() + hints.set("hint1", 123) + assertEquals(123, hints.getAs("hint1", Int::class.java)) + } + + @Test + fun `kotlin java interop for primitives works for short`() { + val hints = Hints() + val s: Short = 123 + hints.set("hint1", s) + assertEquals(s, hints.getAs("hint1", Short::class.java)) + } + + @Test + fun `kotlin java interop for primitives works for byte`() { + val hints = Hints() + val b: Byte = 1 + hints.set("hint1", b) + assertEquals(b, hints.getAs("hint1", Byte::class.java)) + } + + @Test + fun `kotlin java interop for primitives works for char`() { + val hints = Hints() + hints.set("hint1", 'a') + assertEquals('a', hints.getAs("hint1", Char::class.java)) + } + + @Test + fun `kotlin java interop for primitives works for boolean`() { + val hints = Hints() + hints.set("hint1", true) + assertEquals(true, hints.getAs("hint1", Boolean::class.java)) + } + + @Test + fun `setting twice only keeps second value`() { + val hints = Hints() + + hints.set("hint1", "some string") + hints.set("hint1", "a different string") + + assertEquals("a different string", hints.getAs("hint1", String::class.java)) + } + + @Test + fun `after removing the value is gone`() { + val hints = Hints() + + hints.set("hint1", "some string") + assertEquals("some string", hints.getAs("hint1", String::class.java)) + + hints.remove("hint1") + assertNull(hints.get("hint1")) + } + + @Test + fun `removing leaves other values`() { + val hints = Hints() + + hints.set("hint1", "some string") + assertEquals("some string", hints.getAs("hint1", String::class.java)) + hints.set("hint2", "another string") + + hints.remove("hint1") + assertNull(hints.get("hint1")) + assertEquals("another string", hints.getAs("hint2", String::class.java)) + } + + @Test + fun `can retrieve Attachments`() { + val hints = Hints() + assertNotNull(hints.attachments) + } + + @Test + fun `can create hints with attachment`() { + val attachment = newAttachment("test1") + val hints = Hints.withAttachment(attachment) + assertEquals(listOf(attachment), hints.attachments) + } + + @Test + fun `can create hints with attachments`() { + val attachment1 = newAttachment("test1") + val attachment2 = newAttachment("test1") + val hints = Hints.withAttachments(listOf(attachment1, attachment2)) + assertEquals(listOf(attachment1, attachment2), hints.attachments) + } + + @Test + fun `can add an attachment`() { + val hints = Hints() + val attachment = newAttachment("test1") + hints.addAttachment(attachment) + + assertEquals(listOf(attachment), hints.attachments) + } + + @Test + fun `can add multiple attachments`() { + val hints = Hints() + val attachment1 = newAttachment("test1") + val attachment2 = newAttachment("test2") + hints.addAttachment(attachment1) + hints.addAttachment(attachment2) + + assertEquals(listOf(attachment1, attachment2), hints.attachments) + } + + @Test + fun `after reset list is empty`() { + val hints = Hints() + val attachment1 = newAttachment("test1") + val attachment2 = newAttachment("test2") + hints.addAttachment(attachment1) + hints.addAttachment(attachment2) + + hints.clearAttachments() + + assertEquals(emptyList(), hints.attachments) + } + + @Test + fun `after replace list contains only new item`() { + val hints = Hints() + val attachment1 = newAttachment("test1") + val attachment2 = newAttachment("test2") + val attachment3 = newAttachment("test2") + val attachment4 = newAttachment("test2") + hints.addAttachment(attachment1) + hints.addAttachment(attachment2) + + hints.replaceAttachments(listOf(attachment3, attachment4)) + + assertEquals(listOf(attachment3, attachment4), hints.attachments) + } + + companion object { + fun newAttachment(content: String) = Attachment(content.toByteArray(), "$content.txt") + } +}