diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java index 7454566ce..d58393b40 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java @@ -7,9 +7,9 @@ import android.content.pm.PackageInfo; import android.os.Build; import io.sentry.core.ILogger; -import io.sentry.core.SendCachedEventFireAndForgetIntegration; +import io.sentry.core.SendCachedEnvelopeFireAndForgetIntegration; import io.sentry.core.SendFireAndForgetEnvelopeSender; -import io.sentry.core.SendFireAndForgetEventSender; +import io.sentry.core.SendFireAndForgetOutboxSender; import io.sentry.core.SentryLevel; import io.sentry.core.SentryOptions; import io.sentry.core.util.Objects; @@ -116,12 +116,8 @@ private static void installDefaultIntegrations( final @NotNull ILoadClass loadClass) { options.addIntegration( - new SendCachedEventFireAndForgetIntegration( - new SendFireAndForgetEventSender(() -> options.getCacheDirPath()))); - - options.addIntegration( - new SendCachedEventFireAndForgetIntegration( - new SendFireAndForgetEnvelopeSender(() -> options.getSessionsPath()))); + new SendCachedEnvelopeFireAndForgetIntegration( + new SendFireAndForgetEnvelopeSender(() -> options.getCacheDirPath()))); // Integrations are registered in the same order. NDK before adding Watch outbox, // because sentry-native move files around and we don't want to watch that. @@ -136,8 +132,8 @@ private static void installDefaultIntegrations( // this should be executed after NdkIntegration because sentry-native move files on init. // and we'd like to send them right away options.addIntegration( - new SendCachedEventFireAndForgetIntegration( - new SendFireAndForgetEnvelopeSender(() -> options.getOutboxPath()))); + new SendCachedEnvelopeFireAndForgetIntegration( + new SendFireAndForgetOutboxSender(() -> options.getOutboxPath()))); options.addIntegration(new AnrIntegration(context)); options.addIntegration(new AppLifecycleIntegration()); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/EnvelopeFileObserverIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/EnvelopeFileObserverIntegration.java index dcfdcc6ed..340fd9e92 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/EnvelopeFileObserverIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/EnvelopeFileObserverIntegration.java @@ -1,9 +1,9 @@ package io.sentry.android.core; -import io.sentry.core.EnvelopeSender; import io.sentry.core.IHub; import io.sentry.core.ILogger; import io.sentry.core.Integration; +import io.sentry.core.OutboxSender; import io.sentry.core.SentryLevel; import io.sentry.core.SentryOptions; import io.sentry.core.util.Objects; @@ -37,8 +37,8 @@ public final void register(final @NotNull IHub hub, final @NotNull SentryOptions logger.log( SentryLevel.DEBUG, "Registering EnvelopeFileObserverIntegration for path: %s", path); - final EnvelopeSender envelopeSender = - new EnvelopeSender( + final OutboxSender outboxSender = + new OutboxSender( hub, options.getEnvelopeReader(), options.getSerializer(), @@ -46,7 +46,7 @@ public final void register(final @NotNull IHub hub, final @NotNull SentryOptions options.getFlushTimeoutMillis()); observer = - new EnvelopeFileObserver(path, envelopeSender, logger, options.getFlushTimeoutMillis()); + new EnvelopeFileObserver(path, outboxSender, logger, options.getFlushTimeoutMillis()); observer.startWatching(); logger.log(SentryLevel.DEBUG, "EnvelopeFileObserverIntegration installed."); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java index 11fad962a..6077b528d 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java @@ -84,6 +84,7 @@ static void applyMetadata( options.setEnableSessionTracking(sessionTrackingEnabled); if (options.getSampleRate() == null) { + // TODO: it needs to read a Float I guess Double sampleRate = metadata.getDouble(SAMPLE_RATE, -1); options.getLogger().log(SentryLevel.DEBUG, "sampleRate read: %s", sampleRate); if (sampleRate != -1) { diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt index e0c04602b..658935874 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt @@ -13,7 +13,7 @@ import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever import io.sentry.core.ILogger import io.sentry.core.MainEventProcessor -import io.sentry.core.SendCachedEventFireAndForgetIntegration +import io.sentry.core.SendCachedEnvelopeFireAndForgetIntegration import io.sentry.core.SentryLevel import io.sentry.core.SentryOptions import java.io.File @@ -92,16 +92,6 @@ class AndroidOptionsInitializerTest { assertTrue(sentryOptions.outboxPath?.endsWith("${File.separator}cache${File.separator}sentry${File.separator}outbox")!!) } - @Test - fun `sessionDir should be set at initialization`() { - val sentryOptions = SentryAndroidOptions() - val mockContext = createMockContext() - - AndroidOptionsInitializer.init(sentryOptions, mockContext) - - assertTrue(sentryOptions.sessionsPath?.endsWith("${File.separator}cache${File.separator}sentry${File.separator}sessions")!!) - } - @Test fun `init should set context package name as appInclude`() { val sentryOptions = SentryAndroidOptions() @@ -271,12 +261,12 @@ class AndroidOptionsInitializerTest { } @Test - fun `SendCachedEventFireAndForgetIntegration added to integration list`() { + fun `SendCachedEnvelopeFireAndForgetIntegration added to integration list`() { val sentryOptions = SentryAndroidOptions() val mockContext = createMockContext() AndroidOptionsInitializer.init(sentryOptions, mockContext) - val actual = sentryOptions.integrations.firstOrNull { it is SendCachedEventFireAndForgetIntegration } + val actual = sentryOptions.integrations.firstOrNull { it is SendCachedEnvelopeFireAndForgetIntegration } assertNotNull(actual) } diff --git a/sentry-core/src/main/java/io/sentry/core/AsyncConnectionFactory.java b/sentry-core/src/main/java/io/sentry/core/AsyncConnectionFactory.java index e7c2cf483..6d51298e5 100644 --- a/sentry-core/src/main/java/io/sentry/core/AsyncConnectionFactory.java +++ b/sentry-core/src/main/java/io/sentry/core/AsyncConnectionFactory.java @@ -1,22 +1,19 @@ package io.sentry.core; import io.sentry.core.cache.IEnvelopeCache; -import io.sentry.core.cache.IEventCache; import io.sentry.core.transport.AsyncConnection; final class AsyncConnectionFactory { private AsyncConnectionFactory() {} - public static AsyncConnection create( - SentryOptions options, IEventCache eventCache, IEnvelopeCache sessionCache) { + public static AsyncConnection create(SentryOptions options, IEnvelopeCache envelopeCache) { // the connection doesn't do any retries of failed sends and can hold at most the same number // of pending events as there are being cached. The rest is dropped. return new AsyncConnection( options.getTransport(), options.getTransportGate(), - eventCache, - sessionCache, + envelopeCache, options.getMaxQueueSize(), options); } diff --git a/sentry-core/src/main/java/io/sentry/core/DirectoryProcessor.java b/sentry-core/src/main/java/io/sentry/core/DirectoryProcessor.java index 578d1aef9..b5edc27c7 100644 --- a/sentry-core/src/main/java/io/sentry/core/DirectoryProcessor.java +++ b/sentry-core/src/main/java/io/sentry/core/DirectoryProcessor.java @@ -22,7 +22,7 @@ abstract class DirectoryProcessor { this.flushTimeoutMillis = flushTimeoutMillis; } - public void processDirectory(@NotNull File directory) { + public void processDirectory(final @NotNull File directory) { try { logger.log(SentryLevel.DEBUG, "Processing dir. %s", directory.getAbsolutePath()); @@ -39,13 +39,13 @@ public void processDirectory(@NotNull File directory) { return; } - File[] listFiles = directory.listFiles(); + final File[] listFiles = directory.listFiles(); if (listFiles == null) { logger.log(SentryLevel.ERROR, "Cache dir %s is null.", directory.getAbsolutePath()); return; } - File[] filteredListFiles = directory.listFiles((d, name) -> isRelevantFileName(name)); + final File[] filteredListFiles = directory.listFiles((d, name) -> isRelevantFileName(name)); logger.log( SentryLevel.DEBUG, @@ -62,7 +62,7 @@ public void processDirectory(@NotNull File directory) { logger.log(SentryLevel.DEBUG, "Processing file: %s", file.getAbsolutePath()); - final SendCachedEventHint hint = new SendCachedEventHint(flushTimeoutMillis, logger); + final SendCachedEnvelopeHint hint = new SendCachedEnvelopeHint(flushTimeoutMillis, logger); processFile(file, hint); } } catch (Exception e) { @@ -70,11 +70,11 @@ public void processDirectory(@NotNull File directory) { } } - protected abstract void processFile(File file, @Nullable Object hint); + protected abstract void processFile(final @NotNull File file, final @Nullable Object hint); protected abstract boolean isRelevantFileName(String fileName); - private static final class SendCachedEventHint + private static final class SendCachedEnvelopeHint implements Cached, Retryable, SubmissionResult, Flushable { boolean retry = false; boolean succeeded = false; @@ -83,7 +83,7 @@ private static final class SendCachedEventHint private final long flushTimeoutMillis; private final @NotNull ILogger logger; - public SendCachedEventHint(final long flushTimeoutMillis, final @NotNull ILogger logger) { + public SendCachedEnvelopeHint(final long flushTimeoutMillis, final @NotNull ILogger logger) { this.flushTimeoutMillis = flushTimeoutMillis; this.latch = new CountDownLatch(1); this.logger = logger; diff --git a/sentry-core/src/main/java/io/sentry/core/EnvelopeSender.java b/sentry-core/src/main/java/io/sentry/core/EnvelopeSender.java index 676301e95..2c10965ce 100644 --- a/sentry-core/src/main/java/io/sentry/core/EnvelopeSender.java +++ b/sentry-core/src/main/java/io/sentry/core/EnvelopeSender.java @@ -1,24 +1,16 @@ package io.sentry.core; -import static io.sentry.core.SentryLevel.ERROR; -import static io.sentry.core.cache.SessionCache.PREFIX_CURRENT_SESSION_FILE; - +import io.sentry.core.cache.EnvelopeCache; import io.sentry.core.hints.Flushable; import io.sentry.core.hints.Retryable; -import io.sentry.core.hints.SubmissionResult; -import io.sentry.core.util.CollectionUtils; import io.sentry.core.util.LogUtils; import io.sentry.core.util.Objects; import java.io.BufferedInputStream; -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.nio.charset.Charset; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -26,59 +18,77 @@ @ApiStatus.Internal public final class EnvelopeSender extends DirectoryProcessor implements IEnvelopeSender { - @SuppressWarnings("CharsetObjectCanBeUsed") - private static final Charset UTF_8 = Charset.forName("UTF-8"); - private final @NotNull IHub hub; - private final @NotNull IEnvelopeReader envelopeReader; private final @NotNull ISerializer serializer; private final @NotNull ILogger logger; public EnvelopeSender( final @NotNull IHub hub, - final @NotNull IEnvelopeReader envelopeReader, final @NotNull ISerializer serializer, final @NotNull ILogger logger, final long flushTimeoutMillis) { super(logger, flushTimeoutMillis); this.hub = Objects.requireNonNull(hub, "Hub is required."); - this.envelopeReader = Objects.requireNonNull(envelopeReader, "Envelope reader is required."); this.serializer = Objects.requireNonNull(serializer, "Serializer is required."); this.logger = Objects.requireNonNull(logger, "Logger is required."); } @Override - protected void processFile(final @NotNull File file, @Nullable Object hint) { - Objects.requireNonNull(file, "File is required."); + protected void processFile(@NotNull File file, @Nullable Object hint) { + if (!file.isFile()) { + logger.log(SentryLevel.DEBUG, "'%s' is not a file.", file.getAbsolutePath()); + return; + } if (!isRelevantFileName(file.getName())) { - logger.log(SentryLevel.DEBUG, "File '%s' should be ignored.", file.getAbsolutePath()); + logger.log( + SentryLevel.DEBUG, "File '%s' doesn't match extension expected.", file.getAbsolutePath()); return; } - try (final InputStream stream = new BufferedInputStream(new FileInputStream(file))) { - final SentryEnvelope envelope = envelopeReader.read(stream); - if (envelope == null) { - logger.log( - SentryLevel.ERROR, - "Stream from path %s resulted in a null envelope.", - file.getAbsolutePath()); + if (!file.getParentFile().canWrite()) { + logger.log( + SentryLevel.WARNING, + "File '%s' cannot be deleted so it will not be processed.", + file.getAbsolutePath()); + return; + } + + try (final InputStream is = new BufferedInputStream(new FileInputStream(file))) { + SentryEnvelope envelope = serializer.deserializeEnvelope(is); + hub.captureEnvelope(envelope, hint); + + if (hint instanceof Flushable) { + if (!((Flushable) hint).waitFlush()) { + logger.log(SentryLevel.WARNING, "Timed out waiting for envelope submission."); + } } else { - processEnvelope(envelope, hint); - logger.log(SentryLevel.DEBUG, "File '%s' is done.", file.getAbsolutePath()); + LogUtils.logIfNotFlushable(logger, hint); } + } catch (FileNotFoundException e) { + logger.log(SentryLevel.ERROR, e, "File '%s' cannot be found.", file.getAbsolutePath()); } catch (IOException e) { - logger.log(SentryLevel.ERROR, "Error processing envelope.", e); + logger.log(SentryLevel.ERROR, e, "I/O on file '%s' failed.", file.getAbsolutePath()); + } catch (Exception e) { + logger.log( + SentryLevel.ERROR, e, "Failed to capture cached envelope %s", file.getAbsolutePath()); + if (hint instanceof Retryable) { + ((Retryable) hint).setRetry(false); + logger.log(SentryLevel.INFO, e, "File '%s' won't retry.", file.getAbsolutePath()); + } else { + LogUtils.logIfNotRetryable(logger, hint); + } } finally { + // Unless the transport marked this to be retried, it'll be deleted. if (hint instanceof Retryable) { if (!((Retryable) hint).isRetry()) { - try { - if (!file.delete()) { - logger.log(SentryLevel.ERROR, "Failed to delete: %s", file.getAbsolutePath()); - } - } catch (RuntimeException e) { - logger.log(SentryLevel.ERROR, e, "Failed to delete: %s", file.getAbsolutePath()); - } + safeDelete(file, "after trying to capture it"); + logger.log(SentryLevel.DEBUG, "Deleted file %s.", file.getAbsolutePath()); + } else { + logger.log( + SentryLevel.INFO, + "File not deleted since retry was marked. %s.", + file.getAbsolutePath()); } } else { LogUtils.logIfNotRetryable(logger, hint); @@ -87,10 +97,8 @@ protected void processFile(final @NotNull File file, @Nullable Object hint) { } @Override - protected boolean isRelevantFileName(final @Nullable String fileName) { - // ignore current.envelope - return fileName != null && !fileName.startsWith(PREFIX_CURRENT_SESSION_FILE); - // TODO: Use an extension to filter out relevant files + protected boolean isRelevantFileName(String fileName) { + return fileName.endsWith(EnvelopeCache.SUFFIX_ENVELOPE_FILE); } @Override @@ -100,124 +108,22 @@ public void processEnvelopeFile(@NotNull String path, @Nullable Object hint) { processFile(new File(path), hint); } - private void processEnvelope(final @NotNull SentryEnvelope envelope, final @Nullable Object hint) - throws IOException { - logger.log( - SentryLevel.DEBUG, - "Processing Envelope with %d item(s)", - CollectionUtils.size(envelope.getItems())); - int items = 0; - for (final SentryEnvelopeItem item : envelope.getItems()) { - items++; - - if (item.getHeader() == null) { - logger.log(SentryLevel.ERROR, "Item %d has no header", items); - continue; - } - if (SentryItemType.Event.equals(item.getHeader().getType())) { - try (final Reader eventReader = - new BufferedReader( - new InputStreamReader(new ByteArrayInputStream(item.getData()), UTF_8))) { - SentryEvent event = serializer.deserializeEvent(eventReader); - if (event == null) { - logger.log( - SentryLevel.ERROR, - "Item %d of type %s returned null by the parser.", - items, - item.getHeader().getType()); - } else { - if (envelope.getHeader().getEventId() != null - && !envelope.getHeader().getEventId().equals(event.getEventId())) { - logger.log( - SentryLevel.ERROR, - "Item %d of has a different event id (%s) to the envelope header (%s)", - items, - envelope.getHeader().getEventId(), - event.getEventId()); - continue; - } - hub.captureEvent(event, hint); - logger.log(SentryLevel.DEBUG, "Item %d is being captured.", items); - if (hint instanceof Flushable) { - if (!((Flushable) hint).waitFlush()) { - logger.log( - SentryLevel.WARNING, - "Timed out waiting for event submission: %s", - event.getEventId()); - - // TODO: find out about the time out - // if (hint instanceof Retryable) { - // ((Retryable) hint).setRetry(true); - // } - - break; - } - } else { - LogUtils.logIfNotFlushable(logger, hint); - } - } - } catch (Exception e) { - logger.log(ERROR, "Item failed to process.", e); - } - } else if (SentryItemType.Session.equals(item.getHeader().getType())) { - try (final Reader reader = - new BufferedReader( - new InputStreamReader(new ByteArrayInputStream(item.getData()), UTF_8))) { - final Session session = serializer.deserializeSession(reader); - if (session == null) { - logger.log( - SentryLevel.ERROR, - "Item %d of type %s returned null by the parser.", - items, - item.getHeader().getType()); - } else { - // TODO: Bundle all session in a single envelope - hub.captureEnvelope( - SentryEnvelope.fromSession( - serializer, session, envelope.getHeader().getSdkVersion()), - hint); - logger.log(SentryLevel.DEBUG, "Item %d is being captured.", items); - - if (hint instanceof Flushable) { - logger.log(SentryLevel.DEBUG, "Going to wait flush %d item.", items); - if (!((Flushable) hint).waitFlush()) { - logger.log( - SentryLevel.WARNING, - "Timed out waiting for item submission: %s", - session.getSessionId()); - - // TODO: find out about the time out - // if (hint instanceof Retryable) { - // ((Retryable) hint).setRetry(true); - // } - - break; - } - logger.log(SentryLevel.DEBUG, "Flushed %d item.", items); - } else { - LogUtils.logIfNotFlushable(logger, hint); - } - } - } catch (Exception e) { - logger.log(ERROR, "Item failed to process.", e); - } - } else { - // TODO: Handle attachments and other types + private void safeDelete(File file, String errorMessageSuffix) { + try { + if (!file.delete()) { logger.log( - SentryLevel.WARNING, "Item %d of type: %s ignored.", items, item.getHeader().getType()); - } - - if (hint instanceof SubmissionResult) { - if (!((SubmissionResult) hint).isSuccess()) { - // Failed to send an item of the envelope: Stop attempting to send the rest (an attachment - // without the event that created it isn't useful) - logger.log( - SentryLevel.WARNING, - "Envelope had a failed capture at item %d. No more items will be sent.", - items); - break; - } + SentryLevel.ERROR, + "Failed to delete '%s' %s", + file.getAbsolutePath(), + errorMessageSuffix); } + } catch (Exception e) { + logger.log( + SentryLevel.ERROR, + e, + "Failed to delete '%s' %s", + file.getAbsolutePath(), + errorMessageSuffix); } } } diff --git a/sentry-core/src/main/java/io/sentry/core/OutboxSender.java b/sentry-core/src/main/java/io/sentry/core/OutboxSender.java new file mode 100644 index 000000000..3d466e409 --- /dev/null +++ b/sentry-core/src/main/java/io/sentry/core/OutboxSender.java @@ -0,0 +1,176 @@ +package io.sentry.core; + +import static io.sentry.core.SentryLevel.ERROR; +import static io.sentry.core.cache.EnvelopeCache.PREFIX_CURRENT_SESSION_FILE; + +import io.sentry.core.hints.Flushable; +import io.sentry.core.hints.Retryable; +import io.sentry.core.hints.SubmissionResult; +import io.sentry.core.util.CollectionUtils; +import io.sentry.core.util.LogUtils; +import io.sentry.core.util.Objects; +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +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.Charset; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public final class OutboxSender extends DirectoryProcessor implements IEnvelopeSender { + + @SuppressWarnings("CharsetObjectCanBeUsed") + private static final Charset UTF_8 = Charset.forName("UTF-8"); + + private final @NotNull IHub hub; + private final @NotNull IEnvelopeReader envelopeReader; + private final @NotNull ISerializer serializer; + private final @NotNull ILogger logger; + + public OutboxSender( + final @NotNull IHub hub, + final @NotNull IEnvelopeReader envelopeReader, + final @NotNull ISerializer serializer, + final @NotNull ILogger logger, + final long flushTimeoutMillis) { + super(logger, flushTimeoutMillis); + this.hub = Objects.requireNonNull(hub, "Hub is required."); + this.envelopeReader = Objects.requireNonNull(envelopeReader, "Envelope reader is required."); + this.serializer = Objects.requireNonNull(serializer, "Serializer is required."); + this.logger = Objects.requireNonNull(logger, "Logger is required."); + } + + @Override + protected void processFile(final @NotNull File file, @Nullable Object hint) { + Objects.requireNonNull(file, "File is required."); + + if (!isRelevantFileName(file.getName())) { + logger.log(SentryLevel.DEBUG, "File '%s' should be ignored.", file.getAbsolutePath()); + return; + } + + try (final InputStream stream = new BufferedInputStream(new FileInputStream(file))) { + final SentryEnvelope envelope = envelopeReader.read(stream); + if (envelope == null) { + logger.log( + SentryLevel.ERROR, + "Stream from path %s resulted in a null envelope.", + file.getAbsolutePath()); + } else { + processEnvelope(envelope, hint); + logger.log(SentryLevel.DEBUG, "File '%s' is done.", file.getAbsolutePath()); + } + } catch (IOException e) { + logger.log(SentryLevel.ERROR, "Error processing envelope.", e); + } finally { + if (hint instanceof Retryable) { + if (!((Retryable) hint).isRetry()) { + try { + if (!file.delete()) { + logger.log(SentryLevel.ERROR, "Failed to delete: %s", file.getAbsolutePath()); + } + } catch (RuntimeException e) { + logger.log(SentryLevel.ERROR, e, "Failed to delete: %s", file.getAbsolutePath()); + } + } + } else { + LogUtils.logIfNotRetryable(logger, hint); + } + } + } + + @Override + protected boolean isRelevantFileName(final @Nullable String fileName) { + // ignore current.envelope + return fileName != null && !fileName.startsWith(PREFIX_CURRENT_SESSION_FILE); + // TODO: Use an extension to filter out relevant files + } + + @Override + public void processEnvelopeFile(@NotNull String path, @Nullable Object hint) { + Objects.requireNonNull(path, "Path is required."); + + processFile(new File(path), hint); + } + + private void processEnvelope(final @NotNull SentryEnvelope envelope, final @Nullable Object hint) + throws IOException { + logger.log( + SentryLevel.DEBUG, + "Processing Envelope with %d item(s)", + CollectionUtils.size(envelope.getItems())); + int items = 0; + for (final SentryEnvelopeItem item : envelope.getItems()) { + items++; + + if (item.getHeader() == null) { + logger.log(SentryLevel.ERROR, "Item %d has no header", items); + continue; + } + if (SentryItemType.Event.equals(item.getHeader().getType())) { + try (final Reader eventReader = + new BufferedReader( + new InputStreamReader(new ByteArrayInputStream(item.getData()), UTF_8))) { + SentryEvent event = serializer.deserializeEvent(eventReader); + if (event == null) { + logger.log( + SentryLevel.ERROR, + "Item %d of type %s returned null by the parser.", + items, + item.getHeader().getType()); + } else { + if (envelope.getHeader().getEventId() != null + && !envelope.getHeader().getEventId().equals(event.getEventId())) { + logger.log( + SentryLevel.ERROR, + "Item %d of has a different event id (%s) to the envelope header (%s)", + items, + envelope.getHeader().getEventId(), + event.getEventId()); + continue; + } + hub.captureEvent(event, hint); + logger.log(SentryLevel.DEBUG, "Item %d is being captured.", items); + if (hint instanceof Flushable) { + if (!((Flushable) hint).waitFlush()) { + logger.log( + SentryLevel.WARNING, + "Timed out waiting for event submission: %s", + event.getEventId()); + + break; + } + } else { + LogUtils.logIfNotFlushable(logger, hint); + } + } + } catch (Exception e) { + logger.log(ERROR, "Item failed to process.", e); + } + } else { + // TODO: Handle attachments and other types + logger.log( + SentryLevel.WARNING, "Item %d of type: %s ignored.", items, item.getHeader().getType()); + } + + if (hint instanceof SubmissionResult) { + if (!((SubmissionResult) hint).isSuccess()) { + // Failed to send an item of the envelope: Stop attempting to send the rest (an attachment + // without the event that created it isn't useful) + logger.log( + SentryLevel.WARNING, + "Envelope had a failed capture at item %d. No more items will be sent.", + items); + break; + } + } + } + } +} diff --git a/sentry-core/src/main/java/io/sentry/core/Scope.java b/sentry-core/src/main/java/io/sentry/core/Scope.java index 8860de7f1..ec54ab860 100644 --- a/sentry-core/src/main/java/io/sentry/core/Scope.java +++ b/sentry-core/src/main/java/io/sentry/core/Scope.java @@ -392,11 +392,19 @@ public void addEventProcessor(@NotNull EventProcessor eventProcessor) { * Callback to do atomic operations on session * * @param sessionCallback the IWithSession callback + * @return a clone of the Session after executing the callback and mutating the session */ - void withSession(@NotNull IWithSession sessionCallback) { + @Nullable + Session withSession(@NotNull IWithSession sessionCallback) { + Session cloneSession = null; synchronized (sessionLock) { sessionCallback.accept(session); + + if (session != null) { + cloneSession = session.clone(); + } } + return cloneSession; } /** the IWithSession callback */ diff --git a/sentry-core/src/main/java/io/sentry/core/SendCachedEventFireAndForgetIntegration.java b/sentry-core/src/main/java/io/sentry/core/SendCachedEnvelopeFireAndForgetIntegration.java similarity index 93% rename from sentry-core/src/main/java/io/sentry/core/SendCachedEventFireAndForgetIntegration.java rename to sentry-core/src/main/java/io/sentry/core/SendCachedEnvelopeFireAndForgetIntegration.java index 9dabe1bad..8200be48d 100644 --- a/sentry-core/src/main/java/io/sentry/core/SendCachedEventFireAndForgetIntegration.java +++ b/sentry-core/src/main/java/io/sentry/core/SendCachedEnvelopeFireAndForgetIntegration.java @@ -6,7 +6,7 @@ import org.jetbrains.annotations.Nullable; /** Sends cached events over when your App. is starting. */ -public final class SendCachedEventFireAndForgetIntegration implements Integration { +public final class SendCachedEnvelopeFireAndForgetIntegration implements Integration { private final SendFireAndForgetFactory factory; @@ -46,7 +46,8 @@ default boolean hasValidPath(final @Nullable String dirPath, final @NotNull ILog } } - public SendCachedEventFireAndForgetIntegration(final @NotNull SendFireAndForgetFactory factory) { + public SendCachedEnvelopeFireAndForgetIntegration( + final @NotNull SendFireAndForgetFactory factory) { this.factory = Objects.requireNonNull(factory, "SendFireAndForgetFactory is required"); } diff --git a/sentry-core/src/main/java/io/sentry/core/SendCachedEvent.java b/sentry-core/src/main/java/io/sentry/core/SendCachedEvent.java deleted file mode 100644 index 13f8480d4..000000000 --- a/sentry-core/src/main/java/io/sentry/core/SendCachedEvent.java +++ /dev/null @@ -1,130 +0,0 @@ -package io.sentry.core; - -import io.sentry.core.cache.DiskCache; -import io.sentry.core.hints.Flushable; -import io.sentry.core.hints.Retryable; -import io.sentry.core.util.LogUtils; -import io.sentry.core.util.Objects; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.Reader; -import java.nio.charset.Charset; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -final class SendCachedEvent extends DirectoryProcessor { - private static final Charset UTF_8 = Charset.forName("UTF-8"); - private final ISerializer serializer; - private final IHub hub; - private final @NotNull ILogger logger; - - SendCachedEvent( - @NotNull ISerializer serializer, - @NotNull IHub hub, - final @NotNull ILogger logger, - final long flushTimeoutMillis) { - super(logger, flushTimeoutMillis); - this.serializer = Objects.requireNonNull(serializer, "Serializer is required."); - this.hub = Objects.requireNonNull(hub, "Hub is required."); - this.logger = Objects.requireNonNull(logger, "Logger is required."); - } - - @Override - protected void processFile(@NotNull File file, @Nullable Object hint) { - if (!file.isFile()) { - logger.log(SentryLevel.DEBUG, "'%s' is not a file.", file.getAbsolutePath()); - return; - } - - if (!isRelevantFileName(file.getName())) { - logger.log( - SentryLevel.DEBUG, "File '%s' doesn't match extension expected.", file.getAbsolutePath()); - return; - } - - if (!file.getParentFile().canWrite()) { - logger.log( - SentryLevel.WARNING, - "File '%s' cannot be delete so it will not be processed.", - file.getAbsolutePath()); - return; - } - - try (final Reader reader = - new BufferedReader(new InputStreamReader(new FileInputStream(file), UTF_8))) { - SentryEvent event = serializer.deserializeEvent(reader); - hub.captureEvent(event, hint); - - if (hint instanceof Flushable) { - if (!((Flushable) hint).waitFlush()) { - logger.log( - SentryLevel.WARNING, - "Timed out waiting for event submission: %s", - event.getEventId()); - - // TODO: find out about the time out - // if (hint instanceof Retryable) { - // ((Retryable) hint).setRetry(true); - // } - } - } else { - LogUtils.logIfNotFlushable(logger, hint); - } - } catch (FileNotFoundException e) { - logger.log(SentryLevel.ERROR, e, "File '%s' cannot be found.", file.getAbsolutePath()); - } catch (IOException e) { - logger.log(SentryLevel.ERROR, e, "I/O on file '%s' failed.", file.getAbsolutePath()); - } catch (Exception e) { - logger.log(SentryLevel.ERROR, e, "Failed to capture cached event %s", file.getAbsolutePath()); - if (hint instanceof Retryable) { - ((Retryable) hint).setRetry(false); - logger.log(SentryLevel.INFO, e, "File '%s' won't retry.", file.getAbsolutePath()); - } else { - LogUtils.logIfNotRetryable(logger, hint); - } - } finally { - // Unless the transport marked this to be retried, it'll be deleted. - if (hint instanceof Retryable) { - if (!((Retryable) hint).isRetry()) { - safeDelete(file, "after trying to capture it"); - logger.log(SentryLevel.DEBUG, "Deleted file %s.", file.getAbsolutePath()); - } else { - logger.log( - SentryLevel.INFO, - "File not deleted since retry was marked. %s.", - file.getAbsolutePath()); - } - } else { - LogUtils.logIfNotRetryable(logger, hint); - } - } - } - - @Override - protected boolean isRelevantFileName(String fileName) { - return fileName.endsWith(DiskCache.FILE_SUFFIX); - } - - private void safeDelete(File file, String errorMessageSuffix) { - try { - if (!file.delete()) { - logger.log( - SentryLevel.ERROR, - "Failed to delete '%s' %s", - file.getAbsolutePath(), - errorMessageSuffix); - } - } catch (Exception e) { - logger.log( - SentryLevel.ERROR, - e, - "Failed to delete '%s' %s", - file.getAbsolutePath(), - errorMessageSuffix); - } - } -} diff --git a/sentry-core/src/main/java/io/sentry/core/SendFireAndForgetEnvelopeSender.java b/sentry-core/src/main/java/io/sentry/core/SendFireAndForgetEnvelopeSender.java index 2f05ad305..172fe51f2 100644 --- a/sentry-core/src/main/java/io/sentry/core/SendFireAndForgetEnvelopeSender.java +++ b/sentry-core/src/main/java/io/sentry/core/SendFireAndForgetEnvelopeSender.java @@ -7,20 +7,20 @@ @ApiStatus.Internal public final class SendFireAndForgetEnvelopeSender - implements SendCachedEventFireAndForgetIntegration.SendFireAndForgetFactory { + implements SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForgetFactory { - private final @NotNull SendCachedEventFireAndForgetIntegration.SendFireAndForgetDirPath + private final @NotNull SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForgetDirPath sendFireAndForgetDirPath; public SendFireAndForgetEnvelopeSender( - final @NotNull SendCachedEventFireAndForgetIntegration.SendFireAndForgetDirPath + final @NotNull SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForgetDirPath sendFireAndForgetDirPath) { this.sendFireAndForgetDirPath = Objects.requireNonNull(sendFireAndForgetDirPath, "SendFireAndForgetDirPath is required"); } @Override - public @Nullable SendCachedEventFireAndForgetIntegration.SendFireAndForget create( + public @Nullable SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForget create( final @NotNull IHub hub, final @NotNull SentryOptions options) { Objects.requireNonNull(hub, "Hub is required"); Objects.requireNonNull(options, "SentryOptions is required"); @@ -33,11 +33,7 @@ public SendFireAndForgetEnvelopeSender( final EnvelopeSender envelopeSender = new EnvelopeSender( - hub, - options.getEnvelopeReader(), - options.getSerializer(), - options.getLogger(), - options.getFlushTimeoutMillis()); + hub, options.getSerializer(), options.getLogger(), options.getFlushTimeoutMillis()); return processDir(envelopeSender, dirPath, options.getLogger()); } diff --git a/sentry-core/src/main/java/io/sentry/core/SendFireAndForgetEventSender.java b/sentry-core/src/main/java/io/sentry/core/SendFireAndForgetEventSender.java deleted file mode 100644 index 8bdfc314c..000000000 --- a/sentry-core/src/main/java/io/sentry/core/SendFireAndForgetEventSender.java +++ /dev/null @@ -1,34 +0,0 @@ -package io.sentry.core; - -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; - -@ApiStatus.Internal -public final class SendFireAndForgetEventSender - implements SendCachedEventFireAndForgetIntegration.SendFireAndForgetFactory { - - private final @NotNull SendCachedEventFireAndForgetIntegration.SendFireAndForgetDirPath - sendFireAndForgetDirPath; - - public SendFireAndForgetEventSender( - final @NotNull SendCachedEventFireAndForgetIntegration.SendFireAndForgetDirPath - sendFireAndForgetDirPath) { - this.sendFireAndForgetDirPath = sendFireAndForgetDirPath; - } - - @Override - public SendCachedEventFireAndForgetIntegration.SendFireAndForget create( - final @NotNull IHub hub, final @NotNull SentryOptions options) { - final String dirPath = sendFireAndForgetDirPath.getDirPath(); - if (!hasValidPath(dirPath, options.getLogger())) { - options.getLogger().log(SentryLevel.ERROR, "No cache dir path is defined in options."); - return null; - } - - final SendCachedEvent sender = - new SendCachedEvent( - options.getSerializer(), hub, options.getLogger(), options.getFlushTimeoutMillis()); - - return processDir(sender, dirPath, options.getLogger()); - } -} diff --git a/sentry-core/src/main/java/io/sentry/core/SendFireAndForgetOutboxSender.java b/sentry-core/src/main/java/io/sentry/core/SendFireAndForgetOutboxSender.java new file mode 100644 index 000000000..a56b1a7da --- /dev/null +++ b/sentry-core/src/main/java/io/sentry/core/SendFireAndForgetOutboxSender.java @@ -0,0 +1,44 @@ +package io.sentry.core; + +import io.sentry.core.util.Objects; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public final class SendFireAndForgetOutboxSender + implements SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForgetFactory { + + private final @NotNull SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForgetDirPath + sendFireAndForgetDirPath; + + public SendFireAndForgetOutboxSender( + final @NotNull SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForgetDirPath + sendFireAndForgetDirPath) { + this.sendFireAndForgetDirPath = + Objects.requireNonNull(sendFireAndForgetDirPath, "SendFireAndForgetDirPath is required"); + } + + @Override + public @Nullable SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForget create( + final @NotNull IHub hub, final @NotNull SentryOptions options) { + Objects.requireNonNull(hub, "Hub is required"); + Objects.requireNonNull(options, "SentryOptions is required"); + + final String dirPath = sendFireAndForgetDirPath.getDirPath(); + if (!hasValidPath(dirPath, options.getLogger())) { + options.getLogger().log(SentryLevel.ERROR, "No outbox dir path is defined in options."); + return null; + } + + final OutboxSender outboxSender = + new OutboxSender( + hub, + options.getEnvelopeReader(), + options.getSerializer(), + options.getLogger(), + options.getFlushTimeoutMillis()); + + return processDir(outboxSender, dirPath, options.getLogger()); + } +} diff --git a/sentry-core/src/main/java/io/sentry/core/Sentry.java b/sentry-core/src/main/java/io/sentry/core/Sentry.java index 90adbb4c0..6404668cd 100644 --- a/sentry-core/src/main/java/io/sentry/core/Sentry.java +++ b/sentry-core/src/main/java/io/sentry/core/Sentry.java @@ -1,7 +1,6 @@ package io.sentry.core; -import io.sentry.core.cache.DiskCache; -import io.sentry.core.cache.SessionCache; +import io.sentry.core.cache.EnvelopeCache; import io.sentry.core.protocol.SentryId; import io.sentry.core.protocol.User; import java.io.File; @@ -197,11 +196,7 @@ private static boolean initConfigurations(final @NotNull SentryOptions options) final File outboxDir = new File(options.getOutboxPath()); outboxDir.mkdirs(); - final File sessionsDir = new File(options.getSessionsPath()); - sessionsDir.mkdirs(); - - options.setEventDiskCache(new DiskCache(options)); - options.setEnvelopeDiskCache(new SessionCache(options)); + options.setEnvelopeDiskCache(new EnvelopeCache(options)); } else { logger.log(SentryLevel.INFO, "No outbox dir path is defined in options."); } diff --git a/sentry-core/src/main/java/io/sentry/core/SentryClient.java b/sentry-core/src/main/java/io/sentry/core/SentryClient.java index e65aac25b..7b5086226 100644 --- a/sentry-core/src/main/java/io/sentry/core/SentryClient.java +++ b/sentry-core/src/main/java/io/sentry/core/SentryClient.java @@ -1,8 +1,6 @@ package io.sentry.core; import io.sentry.core.hints.DiskFlushNotification; -import io.sentry.core.hints.SessionEndHint; -import io.sentry.core.hints.SessionUpdateHint; import io.sentry.core.protocol.SentryId; import io.sentry.core.transport.Connection; import io.sentry.core.transport.ITransport; @@ -12,6 +10,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Random; import org.jetbrains.annotations.ApiStatus; @@ -48,9 +47,7 @@ public SentryClient(final @NotNull SentryOptions options, @Nullable Connection c } if (connection == null) { - connection = - AsyncConnectionFactory.create( - options, options.getEventDiskCache(), options.getEnvelopeDiskCache()); + connection = AsyncConnectionFactory.create(options, options.getEnvelopeDiskCache()); } this.connection = connection; random = options.getSampleRate() == null ? null : new Random(); @@ -70,7 +67,7 @@ public SentryClient(final @NotNull SentryOptions options, @Nullable Connection c event = applyScope(event, scope, hint); if (event == null) { - return SentryId.EMPTY_ID; + options.getLogger().log(SentryLevel.DEBUG, "Event was dropped by applyScope"); } } else { options @@ -92,39 +89,79 @@ public SentryClient(final @NotNull SentryOptions options, @Nullable Connection c } } - if (event == null) { - return SentryId.EMPTY_ID; + Session session = null; + + if (event != null) { + session = updateSessionData(event, hint, scope); + + if (!sample()) { + options + .getLogger() + .log( + SentryLevel.DEBUG, + "Event %s was dropped due to sampling decision.", + event.getEventId()); + // setting event as null to not be sent as its been discarded by sample rate + event = null; + } } - // TODO: should it be done on the HUB? - updateSessionData(event, hint, scope); + if (event != null) { + event = executeBeforeSend(event, hint); - if (!sample()) { - options - .getLogger() - .log( - SentryLevel.DEBUG, - "Event %s was dropped due to sampling decision.", - event.getEventId()); - return SentryId.EMPTY_ID; + if (event == null) { + options.getLogger().log(SentryLevel.DEBUG, "Event was dropped by beforeSend"); + } } - event = executeBeforeSend(event, hint); + SentryId sentryId = SentryId.EMPTY_ID; - if (event == null) { - options.getLogger().log(SentryLevel.DEBUG, "Event was dropped by beforeSend"); - return SentryId.EMPTY_ID; + if (event != null) { + sentryId = event.getEventId(); } try { - connection.send(event, hint); + final SentryEnvelope envelope = buildEnvelope(event, session); + + if (envelope != null) { + connection.send(envelope, hint); + } } catch (IOException e) { - options - .getLogger() - .log(SentryLevel.WARNING, "Capturing event " + event.getEventId() + " failed.", e); + options.getLogger().log(SentryLevel.WARNING, e, "Capturing event %s failed.", sentryId); + + // if there was an error capturing the event, we return an emptyId + sentryId = SentryId.EMPTY_ID; } - return event.getEventId(); + return sentryId; + } + + private @Nullable SentryEnvelope buildEnvelope( + final @Nullable SentryEvent event, final @Nullable Session session) throws IOException { + SentryId sentryId = null; + + final List envelopeItems = new ArrayList<>(); + + if (event != null) { + final SentryEnvelopeItem eventItem = + SentryEnvelopeItem.fromEvent(options.getSerializer(), event); + envelopeItems.add(eventItem); + sentryId = event.getEventId(); + } + + if (session != null) { + final SentryEnvelopeItem sessionItem = + SentryEnvelopeItem.fromSession(options.getSerializer(), session); + envelopeItems.add(sessionItem); + } + + if (!envelopeItems.isEmpty()) { + final SentryEnvelopeHeader envelopeHeader = + new SentryEnvelopeHeader(sentryId, options.getSdkVersion()); + return new SentryEnvelope(envelopeHeader, envelopeItems); + } + + return null; } /** @@ -135,53 +172,52 @@ public SentryClient(final @NotNull SentryOptions options, @Nullable Connection c * @param scope the Scope or null */ @TestOnly - void updateSessionData( + @Nullable + Session updateSessionData( final @NotNull SentryEvent event, final @Nullable Object hint, final @Nullable Scope scope) { + Session clonedSession = null; + if (ApplyScopeUtils.shouldApplyScopeData(hint)) { if (scope != null) { - scope.withSession( - session -> { - if (session != null) { - Session.State status = null; - if (event.isCrashed()) { - status = Session.State.Crashed; - } - - boolean crashedOrErrored = false; - if (Session.State.Crashed == status || event.isErrored()) { - crashedOrErrored = true; - } - - String userAgent = null; - if (event.getRequest() != null && event.getRequest().getHeaders() != null) { - if (event.getRequest().getHeaders().containsKey("user-agent")) { - userAgent = event.getRequest().getHeaders().get("user-agent"); - } - } - - if (session.update(status, userAgent, crashedOrErrored)) { - - Object sessionHint; - - // if hint is DiskFlushNotification, it means we have an uncaughtException - // and we can end the session. - if (hint instanceof DiskFlushNotification) { - sessionHint = new SessionEndHint(); - session.end(); + clonedSession = + scope.withSession( + session -> { + if (session != null) { + Session.State status = null; + if (event.isCrashed()) { + status = Session.State.Crashed; + } + + boolean crashedOrErrored = false; + if (Session.State.Crashed == status || event.isErrored()) { + crashedOrErrored = true; + } + + String userAgent = null; + if (event.getRequest() != null && event.getRequest().getHeaders() != null) { + if (event.getRequest().getHeaders().containsKey("user-agent")) { + userAgent = event.getRequest().getHeaders().get("user-agent"); + } + } + + if (session.update(status, userAgent, crashedOrErrored)) { + // if hint is DiskFlushNotification, it means we have an uncaughtException + // and we can end the session. + if (hint instanceof DiskFlushNotification) { + session.end(); + } + } } else { - // otherwise we just cache in the disk but do not flush to the network. - sessionHint = new SessionUpdateHint(); + options + .getLogger() + .log(SentryLevel.INFO, "Session is null on scope.withSession"); } - captureSession(session, sessionHint); - } - } else { - options.getLogger().log(SentryLevel.INFO, "Session is null on scope.withSession"); - } - }); + }); } else { options.getLogger().log(SentryLevel.INFO, "Scope is null on client.captureEvent"); } } + return clonedSession; } @ApiStatus.Internal diff --git a/sentry-core/src/main/java/io/sentry/core/SentryOptions.java b/sentry-core/src/main/java/io/sentry/core/SentryOptions.java index f3310859e..01e3b819e 100644 --- a/sentry-core/src/main/java/io/sentry/core/SentryOptions.java +++ b/sentry-core/src/main/java/io/sentry/core/SentryOptions.java @@ -2,12 +2,10 @@ import com.jakewharton.nopen.annotation.Open; import io.sentry.core.cache.IEnvelopeCache; -import io.sentry.core.cache.IEventCache; import io.sentry.core.protocol.SdkVersion; import io.sentry.core.transport.ITransport; import io.sentry.core.transport.ITransportGate; import io.sentry.core.transport.NoOpEnvelopeCache; -import io.sentry.core.transport.NoOpEventCache; import io.sentry.core.transport.NoOpTransport; import io.sentry.core.transport.NoOpTransportGate; import java.io.File; @@ -101,11 +99,8 @@ public class SentryOptions { /** The cache dir. size for capping the number of events Default is 10 */ private int cacheDirSize = 10; - /** The sessions dir. size for capping the number of envelopes Default is 100 */ - private int sessionsDirSize = 100; - /** Max. queue size before flushing events/envelopes to the disk */ - private int maxQueueSize = cacheDirSize + sessionsDirSize; + private int maxQueueSize = cacheDirSize; /** * This variable controls the total amount of breadcrumbs that should be captured Default is 100 @@ -201,9 +196,6 @@ public class SentryOptions { /** whether to ignore TLS errors */ private boolean bypassSecurity = false; - /** Reads and caches event json files in the disk */ - private @NotNull IEventCache eventDiskCache = NoOpEventCache.getInstance(); - /** Reads and caches envelope files in the disk */ private @NotNull IEnvelopeCache envelopeDiskCache = NoOpEnvelopeCache.getInstance(); @@ -456,18 +448,6 @@ public void setBeforeBreadcrumb(@Nullable BeforeBreadcrumbCallback beforeBreadcr return cacheDirPath + File.separator + "outbox"; } - /** - * Returns the sessions path if cacheDirPath is set - * - * @return the sessions path or null if not set - */ - public @Nullable String getSessionsPath() { - if (cacheDirPath == null || cacheDirPath.isEmpty()) { - return null; - } - return cacheDirPath + File.separator + "sessions"; - } - /** * Sets the cache dir. path * @@ -753,24 +733,6 @@ public void setServerName(@Nullable String serverName) { this.serverName = serverName; } - /** - * Returns the sessions dir size - * - * @return the dir size - */ - public int getSessionsDirSize() { - return sessionsDirSize; - } - - /** - * Sets the sessions dir size - * - * @param sessionsDirSize the sessions dir size - */ - public void setSessionsDirSize(int sessionsDirSize) { - this.sessionsDirSize = sessionsDirSize; - } - /** * Returns the session tracking interval in millis * @@ -920,24 +882,6 @@ public void setBypassSecurity(boolean bypassSecurity) { this.bypassSecurity = bypassSecurity; } - /** - * Returns the EventCache interface - * - * @return the EventCache object - */ - public @NotNull IEventCache getEventDiskCache() { - return eventDiskCache; - } - - /** - * Sets the EventCache interface - * - * @param eventDiskCache the EventCache object - */ - public void setEventDiskCache(final @Nullable IEventCache eventDiskCache) { - this.eventDiskCache = eventDiskCache != null ? eventDiskCache : NoOpEventCache.getInstance(); - } - /** * Returns the EnvelopeCache interface * diff --git a/sentry-core/src/main/java/io/sentry/core/UncaughtExceptionHandlerIntegration.java b/sentry-core/src/main/java/io/sentry/core/UncaughtExceptionHandlerIntegration.java index dc6ca5822..10ea15646 100644 --- a/sentry-core/src/main/java/io/sentry/core/UncaughtExceptionHandlerIntegration.java +++ b/sentry-core/src/main/java/io/sentry/core/UncaughtExceptionHandlerIntegration.java @@ -5,6 +5,7 @@ import io.sentry.core.exception.ExceptionMechanismException; import io.sentry.core.hints.DiskFlushNotification; import io.sentry.core.hints.Flushable; +import io.sentry.core.hints.SessionEnd; import io.sentry.core.protocol.Mechanism; import io.sentry.core.util.Objects; import java.io.Closeable; @@ -137,7 +138,8 @@ public void close() { } } - private static final class UncaughtExceptionHint implements DiskFlushNotification, Flushable { + private static final class UncaughtExceptionHint + implements DiskFlushNotification, Flushable, SessionEnd { private final CountDownLatch latch; private final long flushTimeoutMillis; diff --git a/sentry-core/src/main/java/io/sentry/core/cache/DiskCache.java b/sentry-core/src/main/java/io/sentry/core/cache/DiskCache.java deleted file mode 100644 index fd91f3085..000000000 --- a/sentry-core/src/main/java/io/sentry/core/cache/DiskCache.java +++ /dev/null @@ -1,130 +0,0 @@ -package io.sentry.core.cache; - -import static io.sentry.core.SentryLevel.DEBUG; -import static io.sentry.core.SentryLevel.ERROR; -import static io.sentry.core.SentryLevel.WARNING; -import static java.lang.String.format; - -import io.sentry.core.SentryEvent; -import io.sentry.core.SentryOptions; -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.Reader; -import java.io.Writer; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; - -/** - * A simple cache implementation storing the events to a disk, each event in a separater file in a - * configured directory. - */ -@ApiStatus.Internal -public final class DiskCache extends CacheStrategy implements IEventCache { - /** File suffix added to all serialized event files. */ - public static final String FILE_SUFFIX = ".sentry-event"; - - public DiskCache(final @NotNull SentryOptions options) { - super(options, options.getCacheDirPath(), options.getCacheDirSize()); - } - - @Override - public void store(final @NotNull SentryEvent event) { - rotateCacheIfNeeded(allEventFiles()); - - final File eventFile = getEventFile(event); - if (eventFile.exists()) { - options - .getLogger() - .log( - WARNING, - "Not adding Event to offline storage because it already exists: %s", - eventFile.getAbsolutePath()); - return; - } else { - options - .getLogger() - .log(DEBUG, "Adding Event to offline storage: %s", eventFile.getAbsolutePath()); - } - - try (final OutputStream outputStream = new FileOutputStream(eventFile); - final Writer writer = new BufferedWriter(new OutputStreamWriter(outputStream, UTF_8))) { - serializer.serialize(event, writer); - } catch (Exception e) { - options - .getLogger() - .log(ERROR, e, "Error writing Event to offline storage: %s", event.getEventId()); - } - } - - @Override - public void discard(final @NotNull SentryEvent event) { - final File eventFile = getEventFile(event); - if (eventFile.exists()) { - options - .getLogger() - .log(DEBUG, "Discarding event from cache: %s", eventFile.getAbsolutePath()); - - if (!eventFile.delete()) { - options.getLogger().log(ERROR, "Failed to delete Event: %s", eventFile.getAbsolutePath()); - } - } else { - options.getLogger().log(DEBUG, "Event was not cached: %s", eventFile.getAbsolutePath()); - } - } - - private @NotNull File getEventFile(final @NotNull SentryEvent event) { - return new File(directory.getAbsolutePath(), event.getEventId().toString() + FILE_SUFFIX); - } - - @Override - public @NotNull Iterator iterator() { - final File[] allCachedEvents = allEventFiles(); - - final List ret = new ArrayList<>(allCachedEvents.length); - - for (final File f : allCachedEvents) { - try (final Reader reader = - new BufferedReader(new InputStreamReader(new FileInputStream(f), UTF_8))) { - - ret.add(serializer.deserializeEvent(reader)); - } catch (FileNotFoundException e) { - options - .getLogger() - .log( - DEBUG, - "Event file '%s' disappeared while converting all cached files to events.", - f.getAbsolutePath()); - } catch (IOException e) { - options - .getLogger() - .log( - ERROR, - format("Error while reading cached event from file %s", f.getAbsolutePath()), - e); - } - } - - return ret.iterator(); - } - - private @NotNull File[] allEventFiles() { - if (isDirectoryValid()) { - final File[] files = directory.listFiles((__, fileName) -> fileName.endsWith(FILE_SUFFIX)); - if (files != null) { - return files; - } - } - return new File[] {}; - } -} diff --git a/sentry-core/src/main/java/io/sentry/core/cache/SessionCache.java b/sentry-core/src/main/java/io/sentry/core/cache/EnvelopeCache.java similarity index 96% rename from sentry-core/src/main/java/io/sentry/core/cache/SessionCache.java rename to sentry-core/src/main/java/io/sentry/core/cache/EnvelopeCache.java index d7b25702f..b4a88db56 100644 --- a/sentry-core/src/main/java/io/sentry/core/cache/SessionCache.java +++ b/sentry-core/src/main/java/io/sentry/core/cache/EnvelopeCache.java @@ -15,7 +15,6 @@ import io.sentry.core.Session; import io.sentry.core.hints.SessionEnd; import io.sentry.core.hints.SessionStart; -import io.sentry.core.hints.SessionUpdate; import io.sentry.core.util.Objects; import java.io.BufferedInputStream; import java.io.BufferedReader; @@ -44,10 +43,10 @@ import org.jetbrains.annotations.Nullable; @ApiStatus.Internal -public final class SessionCache extends CacheStrategy implements IEnvelopeCache { +public final class EnvelopeCache extends CacheStrategy implements IEnvelopeCache { /** File suffix added to all serialized envelopes files. */ - static final String SUFFIX_ENVELOPE_FILE = ".envelope"; + public static final String SUFFIX_ENVELOPE_FILE = ".envelope"; public static final String PREFIX_CURRENT_SESSION_FILE = "session"; static final String SUFFIX_CURRENT_SESSION_FILE = ".json"; @@ -55,8 +54,8 @@ public final class SessionCache extends CacheStrategy implements IEnvelopeCache private final @NotNull Map fileNameMap = new WeakHashMap<>(); - public SessionCache(final @NotNull SentryOptions options) { - super(options, options.getSessionsPath(), options.getSessionsDirSize()); + public EnvelopeCache(final @NotNull SentryOptions options) { + super(options, options.getCacheDirPath(), options.getCacheDirSize()); } @Override @@ -133,12 +132,7 @@ public void store(final @NotNull SentryEnvelope envelope, final @Nullable Object } updateCurrentSession(currentSessionFile, envelope); } - - if (hint instanceof SessionUpdate) { - updateCurrentSession(currentSessionFile, envelope); - return; - } - + // TODO: problem we need to update the current session file final File envelopeFile = getEnvelopeFile(envelope); if (envelopeFile.exists()) { options diff --git a/sentry-core/src/main/java/io/sentry/core/cache/IEventCache.java b/sentry-core/src/main/java/io/sentry/core/cache/IEventCache.java deleted file mode 100644 index 6a29ebe0a..000000000 --- a/sentry-core/src/main/java/io/sentry/core/cache/IEventCache.java +++ /dev/null @@ -1,30 +0,0 @@ -package io.sentry.core.cache; - -import io.sentry.core.SentryEvent; - -/** - * Implementations of this interface are used as a kind of persistent storage for events that wait - * to be sent to the Sentry server. - * - *

Note that this interface doesn't handle the situation of resending the stored events after a - * crash. While that is surely one of the main usecases for the persistent storage of events, the - * re-initialization is out of scope of the event transport logic. - */ -public interface IEventCache extends Iterable { - - /** - * Stores the event so that it can be sent later. - * - * @param event the event to store - */ - void store(SentryEvent event); - - /** - * Discards the event from the storage. This means that the event has been successfully sent. Note - * that this MUST NOT fail on events that haven't been stored before (i.e. this method is called - * even for events that has been sent on the first attempt). - * - * @param event the event to discard from storage - */ - void discard(SentryEvent event); -} diff --git a/sentry-core/src/main/java/io/sentry/core/hints/SessionUpdate.java b/sentry-core/src/main/java/io/sentry/core/hints/SessionUpdate.java deleted file mode 100644 index 85d843ed1..000000000 --- a/sentry-core/src/main/java/io/sentry/core/hints/SessionUpdate.java +++ /dev/null @@ -1,4 +0,0 @@ -package io.sentry.core.hints; - -/** Hint that shows this is a session update envelope */ -public interface SessionUpdate {} diff --git a/sentry-core/src/main/java/io/sentry/core/hints/SessionUpdateHint.java b/sentry-core/src/main/java/io/sentry/core/hints/SessionUpdateHint.java deleted file mode 100644 index fca36f278..000000000 --- a/sentry-core/src/main/java/io/sentry/core/hints/SessionUpdateHint.java +++ /dev/null @@ -1,3 +0,0 @@ -package io.sentry.core.hints; - -public final class SessionUpdateHint implements SessionUpdate {} diff --git a/sentry-core/src/main/java/io/sentry/core/transport/AsyncConnection.java b/sentry-core/src/main/java/io/sentry/core/transport/AsyncConnection.java index a351597d8..5e7b484b9 100644 --- a/sentry-core/src/main/java/io/sentry/core/transport/AsyncConnection.java +++ b/sentry-core/src/main/java/io/sentry/core/transport/AsyncConnection.java @@ -3,16 +3,12 @@ import io.sentry.core.ILogger; import io.sentry.core.SentryEnvelope; import io.sentry.core.SentryEnvelopeItem; -import io.sentry.core.SentryEvent; -import io.sentry.core.SentryItemType; import io.sentry.core.SentryLevel; import io.sentry.core.SentryOptions; import io.sentry.core.cache.IEnvelopeCache; -import io.sentry.core.cache.IEventCache; import io.sentry.core.hints.Cached; import io.sentry.core.hints.DiskFlushNotification; import io.sentry.core.hints.Retryable; -import io.sentry.core.hints.SessionUpdate; import io.sentry.core.hints.SubmissionResult; import io.sentry.core.util.LogUtils; import io.sentry.core.util.Objects; @@ -35,23 +31,20 @@ public final class AsyncConnection implements Closeable, Connection { private final @NotNull ITransport transport; private final @NotNull ITransportGate transportGate; private final @NotNull ExecutorService executor; - private final @NotNull IEventCache eventCache; - private final @NotNull IEnvelopeCache sessionCache; + private final @NotNull IEnvelopeCache envelopeCache; private final @NotNull SentryOptions options; public AsyncConnection( final ITransport transport, final ITransportGate transportGate, - final IEventCache eventCache, - final IEnvelopeCache sessionCache, + final IEnvelopeCache envelopeCache, final int maxQueueSize, final SentryOptions options) { this( transport, transportGate, - eventCache, - sessionCache, - initExecutor(maxQueueSize, eventCache, sessionCache, options.getLogger()), + envelopeCache, + initExecutor(maxQueueSize, envelopeCache, options.getLogger()), options); } @@ -59,44 +52,31 @@ public AsyncConnection( AsyncConnection( final @NotNull ITransport transport, final @NotNull ITransportGate transportGate, - final @NotNull IEventCache eventCache, - final @NotNull IEnvelopeCache sessionCache, + final @NotNull IEnvelopeCache envelopeCache, final @NotNull ExecutorService executorService, final @NotNull SentryOptions options) { this.transport = transport; this.transportGate = transportGate; - this.eventCache = eventCache; - this.sessionCache = sessionCache; + this.envelopeCache = envelopeCache; this.options = options; this.executor = executorService; } private static QueuedThreadPoolExecutor initExecutor( final int maxQueueSize, - final @NotNull IEventCache eventCache, - final @NotNull IEnvelopeCache sessionCache, + final @NotNull IEnvelopeCache envelopeCache, final @NotNull ILogger logger) { final RejectedExecutionHandler storeEvents = (r, executor) -> { - if (r instanceof EventSender) { - final EventSender eventSender = (EventSender) r; + if (r instanceof EnvelopeSender) { + final EnvelopeSender envelopeSender = (EnvelopeSender) r; - if (!(eventSender.hint instanceof Cached)) { - eventCache.store(eventSender.event); + if (!(envelopeSender.hint instanceof Cached)) { + envelopeCache.store(envelopeSender.envelope, envelopeSender.hint); } - markHintWhenSendingFailed(eventSender.hint, true); - logger.log(SentryLevel.WARNING, "Event rejected: %s", eventSender.event.getEventId()); - } - if (r instanceof SessionSender) { - final SessionSender sessionSender = (SessionSender) r; - - if (!(sessionSender.hint instanceof Cached)) { - sessionCache.store(sessionSender.envelope, sessionSender.hint); - } - - markHintWhenSendingFailed(sessionSender.hint, true); + markHintWhenSendingFailed(envelopeSender.hint, true); logger.log(SentryLevel.WARNING, "Envelope rejected"); } }; @@ -120,42 +100,12 @@ private static void markHintWhenSendingFailed(final @Nullable Object hint, final } } - /** - * Tries to send the event to the Sentry server. - * - * @param event the event to send - * @throws IOException on error - */ - @SuppressWarnings("FutureReturnValueIgnored") - @Override - public void send(final @NotNull SentryEvent event, final @Nullable Object hint) - throws IOException { - IEventCache currentEventCache = eventCache; - boolean cached = false; - if (hint instanceof Cached) { - currentEventCache = NoOpEventCache.getInstance(); - cached = true; - options.getLogger().log(SentryLevel.DEBUG, "Captured SentryEvent is already cached"); - } - - // no reason to continue - if (transport.isRetryAfter(SentryItemType.Event.getItemType())) { - if (cached) { - eventCache.discard(event); - } - markHintWhenSendingFailed(hint, false); - return; - } - - executor.submit(new EventSender(event, hint, currentEventCache)); - } - @SuppressWarnings("FutureReturnValueIgnored") @Override public void send(@NotNull SentryEnvelope envelope, final @Nullable Object hint) throws IOException { // For now no caching on envelopes - IEnvelopeCache currentEnvelopeCache = sessionCache; + IEnvelopeCache currentEnvelopeCache = envelopeCache; boolean cached = false; if (hint instanceof Cached) { currentEnvelopeCache = NoOpEnvelopeCache.getInstance(); @@ -192,7 +142,7 @@ public void send(@NotNull SentryEnvelope envelope, final @Nullable Object hint) // no reason to continue if (toSend.isEmpty()) { if (cached) { - sessionCache.discard(envelope); + envelopeCache.discard(envelope); } options.getLogger().log(SentryLevel.INFO, "Envelope discarded due all items rate limited."); @@ -203,7 +153,7 @@ public void send(@NotNull SentryEnvelope envelope, final @Nullable Object hint) envelope = new SentryEnvelope(envelope.getHeader(), toSend); } - executor.submit(new SessionSender(envelope, hint, currentEnvelopeCache)); + executor.submit(new EnvelopeSender(envelope, hint, currentEnvelopeCache)); } @Override @@ -240,103 +190,19 @@ private static final class AsyncConnectionThreadFactory implements ThreadFactory } } - private final class EventSender implements Runnable { - private final SentryEvent event; - private final Object hint; - private final IEventCache eventCache; - private final TransportResult failedResult = TransportResult.error(); - - EventSender( - final @NotNull SentryEvent event, - final @Nullable Object hint, - final @NotNull IEventCache eventCache) { - this.event = event; - this.hint = hint; - this.eventCache = eventCache; - } - - @Override - public void run() { - TransportResult result = this.failedResult; - try { - result = flush(); - } catch (Exception e) { - options - .getLogger() - .log(SentryLevel.ERROR, e, "Event submission failed: %s", event.getEventId()); - throw e; - } finally { - if (hint instanceof SubmissionResult) { - options - .getLogger() - .log(SentryLevel.DEBUG, "Marking event submission result: %s", result.isSuccess()); - ((SubmissionResult) hint).setResult(result.isSuccess()); - } - } - } - - private @NotNull TransportResult flush() { - TransportResult result = this.failedResult; - eventCache.store(event); - - if (hint instanceof DiskFlushNotification) { - ((DiskFlushNotification) hint).markFlushed(); - options - .getLogger() - .log(SentryLevel.DEBUG, "Disk flush event fired: %s", event.getEventId()); - } - - if (transportGate.isConnected()) { - try { - result = - transport.send( - SentryEnvelope.fromEvent( - options.getSerializer(), event, options.getSdkVersion())); - if (result.isSuccess()) { - eventCache.discard(event); - } else { - final String message = - "The transport failed to send the event with response code " - + result.getResponseCode(); - - options.getLogger().log(SentryLevel.ERROR, message); - - throw new IllegalStateException(message); - } - } catch (IOException e) { - // Failure due to IO is allowed to retry the event - if (hint instanceof Retryable) { - ((Retryable) hint).setRetry(true); - } else { - LogUtils.logIfNotRetryable(options.getLogger(), hint); - } - throw new IllegalStateException("Sending the event failed.", e); - } - } else { - // If transportGate is blocking from sending, allowed to retry - if (hint instanceof Retryable) { - ((Retryable) hint).setRetry(true); - } else { - LogUtils.logIfNotRetryable(options.getLogger(), hint); - } - } - return result; - } - } - - private final class SessionSender implements Runnable { + private final class EnvelopeSender implements Runnable { private final @NotNull SentryEnvelope envelope; private final @Nullable Object hint; - private final @NotNull IEnvelopeCache sessionCache; + private final @NotNull IEnvelopeCache envelopeCache; private final TransportResult failedResult = TransportResult.error(); - SessionSender( + EnvelopeSender( final @NotNull SentryEnvelope envelope, final @Nullable Object hint, - final @NotNull IEnvelopeCache sessionCache) { + final @NotNull IEnvelopeCache envelopeCache) { this.envelope = Objects.requireNonNull(envelope, "Envelope is required."); this.hint = hint; - this.sessionCache = Objects.requireNonNull(sessionCache, "SessionCache is required."); + this.envelopeCache = Objects.requireNonNull(envelopeCache, "EnvelopeCache is required."); } @Override @@ -361,21 +227,18 @@ public void run() { private @NotNull TransportResult flush() { TransportResult result = this.failedResult; - sessionCache.store(envelope, hint); + envelopeCache.store(envelope, hint); - // we only flush a session update to the disk, but not to the network - if (hint instanceof SessionUpdate) { - options - .getLogger() - .log(SentryLevel.DEBUG, "SessionUpdate event, leaving after event being cached."); - return TransportResult.success(); + if (hint instanceof DiskFlushNotification) { + ((DiskFlushNotification) hint).markFlushed(); + options.getLogger().log(SentryLevel.DEBUG, "Disk flush envelope fired"); } if (transportGate.isConnected()) { try { result = transport.send(envelope); if (result.isSuccess()) { - sessionCache.discard(envelope); + envelopeCache.discard(envelope); } else { final String message = "The transport failed to send the envelope with response code " diff --git a/sentry-core/src/main/java/io/sentry/core/transport/Connection.java b/sentry-core/src/main/java/io/sentry/core/transport/Connection.java index 02a5da2ae..d925c1b4c 100644 --- a/sentry-core/src/main/java/io/sentry/core/transport/Connection.java +++ b/sentry-core/src/main/java/io/sentry/core/transport/Connection.java @@ -1,17 +1,10 @@ package io.sentry.core.transport; import io.sentry.core.SentryEnvelope; -import io.sentry.core.SentryEvent; import java.io.IOException; import org.jetbrains.annotations.Nullable; public interface Connection { - void send(SentryEvent event, @Nullable Object hint) throws IOException; - - default void send(SentryEvent event) throws IOException { - send(event, null); - } - void send(SentryEnvelope event, @Nullable Object hint) throws IOException; default void send(SentryEnvelope envelope) throws IOException { diff --git a/sentry-core/src/main/java/io/sentry/core/transport/NoOpEventCache.java b/sentry-core/src/main/java/io/sentry/core/transport/NoOpEventCache.java deleted file mode 100644 index 83a5cd265..000000000 --- a/sentry-core/src/main/java/io/sentry/core/transport/NoOpEventCache.java +++ /dev/null @@ -1,28 +0,0 @@ -package io.sentry.core.transport; - -import io.sentry.core.SentryEvent; -import io.sentry.core.cache.IEventCache; -import java.util.ArrayList; -import java.util.Iterator; -import org.jetbrains.annotations.NotNull; - -public final class NoOpEventCache implements IEventCache { - private static final NoOpEventCache instance = new NoOpEventCache(); - - public static NoOpEventCache getInstance() { - return instance; - } - - private NoOpEventCache() {} - - @Override - public void store(SentryEvent event) {} - - @Override - public void discard(SentryEvent event) {} - - @Override - public @NotNull Iterator iterator() { - return new ArrayList(0).iterator(); - } -} diff --git a/sentry-core/src/test/java/io/sentry/core/DirectoryProcessorTest.kt b/sentry-core/src/test/java/io/sentry/core/DirectoryProcessorTest.kt index 231180698..8039ca6ae 100644 --- a/sentry-core/src/test/java/io/sentry/core/DirectoryProcessorTest.kt +++ b/sentry-core/src/test/java/io/sentry/core/DirectoryProcessorTest.kt @@ -32,8 +32,8 @@ class DirectoryProcessorTest { options.setLogger(logger) } - fun getSut(): EnvelopeSender { - return EnvelopeSender(hub, envelopeReader, serializer, logger, 15000) + fun getSut(): OutboxSender { + return OutboxSender(hub, envelopeReader, serializer, logger, 15000) } } @@ -55,11 +55,17 @@ class DirectoryProcessorTest { fun `process directory folder has a non ApplyScopeData hint`() { val path = getTempEnvelope("envelope-event-attachment.txt") assertTrue(File(path).exists()) // sanity check - val session = createSession() - whenever(fixture.envelopeReader.read(any())).thenReturn(SentryEnvelope.fromSession(fixture.serializer, session, null)) - whenever(fixture.serializer.deserializeSession(any())).thenReturn(session) +// val session = createSession() +// whenever(fixture.envelopeReader.read(any())).thenReturn(SentryEnvelope.fromSession(fixture.serializer, session, null)) +// whenever(fixture.serializer.deserializeSession(any())).thenReturn(session) + val event = SentryEvent() + val envelope = SentryEnvelope.fromEvent(fixture.serializer, event, null) + + whenever(fixture.envelopeReader.read(any())).thenReturn(envelope) + whenever(fixture.serializer.deserializeEvent(any())).thenReturn(event) + fixture.getSut().processDirectory(file) - verify(fixture.hub).captureEnvelope(any(), argWhere { it !is ApplyScopeData }) + verify(fixture.hub).captureEvent(any(), argWhere { it !is ApplyScopeData }) } @Test diff --git a/sentry-core/src/test/java/io/sentry/core/EnvelopeSenderTest.kt b/sentry-core/src/test/java/io/sentry/core/EnvelopeSenderTest.kt index 7f2817375..7fb998994 100644 --- a/sentry-core/src/test/java/io/sentry/core/EnvelopeSenderTest.kt +++ b/sentry-core/src/test/java/io/sentry/core/EnvelopeSenderTest.kt @@ -1,187 +1,115 @@ package io.sentry.core import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.argWhere +import com.nhaarman.mockitokotlin2.doThrow import com.nhaarman.mockitokotlin2.eq import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.never import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions import com.nhaarman.mockitokotlin2.whenever -import io.sentry.core.cache.SessionCache +import io.sentry.core.cache.EnvelopeCache import io.sentry.core.hints.Retryable -import io.sentry.core.protocol.SentryId -import io.sentry.core.protocol.User +import io.sentry.core.util.NoFlushTimeout import java.io.File -import java.io.FileNotFoundException import java.nio.file.Files -import java.nio.file.Paths -import java.util.Date -import java.util.UUID +import java.nio.file.Path +import kotlin.test.AfterTest +import kotlin.test.BeforeTest import kotlin.test.Test -import kotlin.test.assertFailsWith import kotlin.test.assertFalse -import kotlin.test.assertTrue class EnvelopeSenderTest { private class Fixture { - - var hub: IHub = mock() - var envelopeReader: IEnvelopeReader = mock() - var serializer: ISerializer = mock() - var logger: ILogger = mock() - var options: SentryOptions + var hub: IHub? = mock() + var logger: ILogger? = mock() + var serializer: ISerializer? = mock() + var options = SentryOptions().NoFlushTimeout() init { - options = SentryOptions() options.isDebug = true options.setLogger(logger) } fun getSut(): EnvelopeSender { - return EnvelopeSender(hub, envelopeReader, serializer, logger, 15000) + return EnvelopeSender(hub!!, serializer!!, logger!!, options.flushTimeoutMillis) } } + private lateinit var tempDirectory: Path private val fixture = Fixture() - private fun getTempEnvelope(fileName: String): String { - val testFile = this::class.java.classLoader.getResource(fileName) - val testFileBytes = testFile!!.readBytes() - val targetFile = File.createTempFile("temp-envelope", ".tmp") - Files.write(Paths.get(targetFile.toURI()), testFileBytes) - return targetFile.absolutePath + @BeforeTest + fun `before send`() { + tempDirectory = Files.createTempDirectory("send-cached-event-test") } - @Test - fun `when envelopeReader returns null, file is deleted `() { - whenever(fixture.envelopeReader.read(any())).thenReturn(null) - val sut = fixture.getSut() - val path = getTempEnvelope("envelope-event-attachment.txt") - assertTrue(File(path).exists()) // sanity check - sut.processEnvelopeFile(path, mock()) - assertFalse(File(path).exists()) - // Additionally make sure we have a error logged - verify(fixture.logger).log(eq(SentryLevel.ERROR), any(), any()) + @AfterTest + fun `after send`() { + File(tempDirectory.toUri()).delete() } @Test - fun `when parser is EnvelopeReader and serializer returns SentryEvent, event captured, file is deleted `() { - fixture.envelopeReader = EnvelopeReader() - val expected = SentryEvent(SentryId(UUID.fromString("9ec79c33-ec99-42ab-8353-589fcb2e04dc")), Date()) - whenever(fixture.serializer.deserializeEvent(any())).thenReturn(expected) + fun `when directory doesn't exist, processDirectory logs and returns`() { val sut = fixture.getSut() - val path = getTempEnvelope("envelope-event-attachment.txt") - assertTrue(File(path).exists()) // sanity check - sut.processEnvelopeFile(path, mock()) - - verify(fixture.hub).captureEvent(eq(expected), any()) - assertFalse(File(path).exists()) - // Additionally make sure we have no errors logged - verify(fixture.logger, never()).log(eq(SentryLevel.ERROR), any(), any()) - verify(fixture.logger, never()).log(eq(SentryLevel.ERROR), any(), any()) + sut.processDirectory(File("i don't exist")) + verify(fixture.logger)!!.log(eq(SentryLevel.WARNING), eq("Directory '%s' doesn't exist. No cached events to send."), any()) + verifyNoMoreInteractions(fixture.hub) } @Test - fun `when parser is EnvelopeReader and serializer returns SentryEnvelope, event captured, file is deleted `() { - fixture.envelopeReader = EnvelopeReader() - val session = Session("123", User(), "env", "release") - val expected = SentryEnvelope(SentryId("3067d54967f84f20a2adfab5119156ce"), null, setOf()) - whenever(fixture.serializer.deserializeEnvelope(any())).thenReturn(expected) - whenever(fixture.serializer.deserializeSession(any())).thenReturn(session) + fun `when directory is actually a file, processDirectory logs and returns`() { val sut = fixture.getSut() - val path = getTempEnvelope("envelope-session-start.txt") - assertTrue(File(path).exists()) // sanity check - sut.processEnvelopeFile(path, mock()) - - verify(fixture.hub).captureEnvelope(any(), any()) - assertFalse(File(path).exists()) - // Additionally make sure we have no errors logged - verify(fixture.logger, never()).log(eq(SentryLevel.ERROR), any(), any()) - verify(fixture.logger, never()).log(eq(SentryLevel.ERROR), any(), any()) + val testFile = File(Files.createTempFile("send-cached-event-test", EnvelopeCache.SUFFIX_ENVELOPE_FILE).toUri()) + testFile.deleteOnExit() + sut.processDirectory(testFile) + verify(fixture.logger)!!.log(eq(SentryLevel.ERROR), eq("Cache dir %s is not a directory."), any()) + verifyNoMoreInteractions(fixture.hub) } @Test - fun `when parser is EnvelopeReader and serializer returns a null event, file error logged, no event captured `() { - fixture.envelopeReader = EnvelopeReader() - whenever(fixture.serializer.deserializeEvent(any())).thenReturn(null) + fun `when directory has non event files, processDirectory logs that`() { val sut = fixture.getSut() - val path = getTempEnvelope("envelope-event-attachment.txt") - assertTrue(File(path).exists()) // sanity check - sut.processEnvelopeFile(path, mock()) - - // Additionally make sure we have no errors logged - verify(fixture.logger).log(eq(SentryLevel.ERROR), any(), any()) - verify(fixture.hub, never()).captureEvent(any()) - assertFalse(File(path).exists()) + val testFile = File(Files.createTempFile(tempDirectory, "send-cached-event-test", ".not-right-suffix").toUri()) + sut.processDirectory(File(tempDirectory.toUri())) + testFile.deleteOnExit() + verify(fixture.logger)!!.log(eq(SentryLevel.DEBUG), eq("File '%s' doesn't match extension expected."), any()) + verifyNoMoreInteractions(fixture.hub) } @Test - fun `when parser is EnvelopeReader and serializer returns a null envelope, file error logged, no event captured `() { - fixture.envelopeReader = EnvelopeReader() - whenever(fixture.serializer.deserializeEnvelope(any())).thenReturn(null) - whenever(fixture.serializer.deserializeSession(any())).thenReturn(null) + fun `when directory has event files, processDirectory captures with hub`() { + val event = SentryEvent() + val envelope = SentryEnvelope.fromEvent(fixture.serializer!!, event, null) + whenever(fixture.serializer!!.deserializeEnvelope(any())).thenReturn(envelope) val sut = fixture.getSut() - val path = getTempEnvelope("envelope-session-start.txt") - assertTrue(File(path).exists()) // sanity check - sut.processEnvelopeFile(path, mock()) - - // Additionally make sure we have no errors logged - verify(fixture.logger).log(eq(SentryLevel.ERROR), any(), any()) - verify(fixture.hub, never()).captureEvent(any()) - assertFalse(File(path).exists()) + val testFile = File(Files.createTempFile(tempDirectory, "send-cached-event-test", EnvelopeCache.SUFFIX_ENVELOPE_FILE).toUri()) + testFile.deleteOnExit() + sut.processDirectory(File(tempDirectory.toUri())) + verify(fixture.hub)!!.captureEnvelope(eq(envelope), any()) } @Test - fun `when processEnvelopeFile is called with a invalid path, logs error`() { + fun `when serializer throws, error is logged, file deleted`() { + val expected = RuntimeException() + whenever(fixture.serializer!!.deserializeEnvelope(any())).doThrow(expected) val sut = fixture.getSut() - sut.processEnvelopeFile(File.separator + "i-hope-it-doesnt-exist" + File.separator + "file.txt", mock()) - verify(fixture.logger).log(eq(SentryLevel.ERROR), any(), argWhere { it is FileNotFoundException }) - } - - @Test - fun `when hub is null, ctor throws`() { - val clazz = Class.forName("io.sentry.core.EnvelopeSender") - val ctor = clazz.getConstructor(IHub::class.java, IEnvelopeReader::class.java, ISerializer::class.java, ILogger::class.java, Long::class.java) - val params = arrayOf(null, mock(), mock(), mock(), null) - assertFailsWith { ctor.newInstance(params) } - } - - @Test - fun `when envelopeReader is null, ctor throws`() { - val clazz = Class.forName("io.sentry.core.EnvelopeSender") - val ctor = clazz.getConstructor(IHub::class.java, IEnvelopeReader::class.java, ISerializer::class.java, ILogger::class.java, Long::class.java) - val params = arrayOf(mock(), null, mock(), mock(), 15000) - assertFailsWith { ctor.newInstance(params) } + val testFile = File(Files.createTempFile(tempDirectory, "send-cached-event-test", EnvelopeCache.SUFFIX_ENVELOPE_FILE).toUri()) + testFile.deleteOnExit() + sut.processFile(testFile, mock()) + verify(fixture.logger)!!.log(eq(SentryLevel.ERROR), eq(expected), eq("Failed to capture cached envelope %s"), eq(testFile.absolutePath)) + verifyNoMoreInteractions(fixture.hub) + assertFalse(testFile.exists()) } @Test - fun `when serializer is null, ctor throws`() { - val clazz = Class.forName("io.sentry.core.EnvelopeSender") - val ctor = clazz.getConstructor(IHub::class.java, IEnvelopeReader::class.java, ISerializer::class.java, ILogger::class.java, Long::class.java) - val params = arrayOf(mock(), mock(), null, mock(), 15000) - assertFailsWith { ctor.newInstance(params) } - } - - @Test - fun `when logger is null, ctor throws`() { - val clazz = Class.forName("io.sentry.core.EnvelopeSender") - val ctor = clazz.getConstructor(IHub::class.java, IEnvelopeReader::class.java, ISerializer::class.java, ILogger::class.java, Long::class.java) - val params = arrayOf(mock(), mock(), mock(), null, 15000) - assertFailsWith { ctor.newInstance(params) } - } - - @Test - fun `when file name is null, should not be relevant`() { - assertFalse(fixture.getSut().isRelevantFileName(null)) - } - - @Test - fun `when file name is current prefix, should be ignored`() { - assertFalse(fixture.getSut().isRelevantFileName(SessionCache.PREFIX_CURRENT_SESSION_FILE)) - } - - @Test - fun `when file name is relevant, should return true`() { - assertTrue(fixture.getSut().isRelevantFileName("123.envelope")) + fun `when hub throws, file gets deleted`() { + val expected = RuntimeException() + whenever(fixture.serializer!!.deserializeEnvelope(any())).doThrow(expected) + val sut = fixture.getSut() + val testFile = File(Files.createTempFile(tempDirectory, "send-cached-event-test", EnvelopeCache.SUFFIX_ENVELOPE_FILE).toUri()) + testFile.deleteOnExit() + sut.processFile(testFile, any()) + verify(fixture.logger)!!.log(eq(SentryLevel.ERROR), eq(expected), eq("Failed to capture cached envelope %s"), eq(testFile.absolutePath)) + verifyNoMoreInteractions(fixture.hub) } } diff --git a/sentry-core/src/test/java/io/sentry/core/OutboxSenderTest.kt b/sentry-core/src/test/java/io/sentry/core/OutboxSenderTest.kt new file mode 100644 index 000000000..56e91f914 --- /dev/null +++ b/sentry-core/src/test/java/io/sentry/core/OutboxSenderTest.kt @@ -0,0 +1,187 @@ +package io.sentry.core + +import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.argWhere +import com.nhaarman.mockitokotlin2.eq +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.never +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.whenever +import io.sentry.core.cache.EnvelopeCache +import io.sentry.core.hints.Retryable +import io.sentry.core.protocol.SentryId +import java.io.File +import java.io.FileNotFoundException +import java.nio.file.Files +import java.nio.file.Paths +import java.util.Date +import java.util.UUID +import kotlin.test.Test +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class OutboxSenderTest { + private class Fixture { + + var hub: IHub = mock() + var envelopeReader: IEnvelopeReader = mock() + var serializer: ISerializer = mock() + var logger: ILogger = mock() + var options: SentryOptions + + init { + options = SentryOptions() + options.isDebug = true + options.setLogger(logger) + } + + fun getSut(): OutboxSender { + return OutboxSender(hub, envelopeReader, serializer, logger, 15000) + } + } + + private val fixture = Fixture() + + private fun getTempEnvelope(fileName: String): String { + val testFile = this::class.java.classLoader.getResource(fileName) + val testFileBytes = testFile!!.readBytes() + val targetFile = File.createTempFile("temp-envelope", ".tmp") + Files.write(Paths.get(targetFile.toURI()), testFileBytes) + return targetFile.absolutePath + } + + @Test + fun `when envelopeReader returns null, file is deleted `() { + whenever(fixture.envelopeReader.read(any())).thenReturn(null) + val sut = fixture.getSut() + val path = getTempEnvelope("envelope-event-attachment.txt") + assertTrue(File(path).exists()) // sanity check + sut.processEnvelopeFile(path, mock()) + assertFalse(File(path).exists()) + // Additionally make sure we have a error logged + verify(fixture.logger).log(eq(SentryLevel.ERROR), any(), any()) + } + + @Test + fun `when parser is EnvelopeReader and serializer returns SentryEvent, event captured, file is deleted `() { + fixture.envelopeReader = EnvelopeReader() + val expected = SentryEvent(SentryId(UUID.fromString("9ec79c33-ec99-42ab-8353-589fcb2e04dc")), Date()) + whenever(fixture.serializer.deserializeEvent(any())).thenReturn(expected) + val sut = fixture.getSut() + val path = getTempEnvelope("envelope-event-attachment.txt") + assertTrue(File(path).exists()) // sanity check + sut.processEnvelopeFile(path, mock()) + + verify(fixture.hub).captureEvent(eq(expected), any()) + assertFalse(File(path).exists()) + // Additionally make sure we have no errors logged + verify(fixture.logger, never()).log(eq(SentryLevel.ERROR), any(), any()) + verify(fixture.logger, never()).log(eq(SentryLevel.ERROR), any(), any()) + } + + @Test + fun `when parser is EnvelopeReader and serializer returns SentryEnvelope, event captured, file is deleted `() { + fixture.envelopeReader = EnvelopeReader() + + val event = SentryEvent(SentryId("9ec79c33ec9942ab8353589fcb2e04dc"), Date()) + val expected = SentryEnvelope(SentryId("9ec79c33ec9942ab8353589fcb2e04dc"), null, setOf()) + whenever(fixture.serializer.deserializeEnvelope(any())).thenReturn(expected) + whenever(fixture.serializer.deserializeEvent(any())).thenReturn(event) + val sut = fixture.getSut() + val path = getTempEnvelope("envelope-event-attachment.txt") + assertTrue(File(path).exists()) // sanity check + sut.processEnvelopeFile(path, mock()) + + verify(fixture.hub).captureEvent(any(), any()) + assertFalse(File(path).exists()) + // Additionally make sure we have no errors logged + verify(fixture.logger, never()).log(eq(SentryLevel.ERROR), any(), any()) + verify(fixture.logger, never()).log(eq(SentryLevel.ERROR), any(), any()) + } + + @Test + fun `when parser is EnvelopeReader and serializer returns a null event, file error logged, no event captured `() { + fixture.envelopeReader = EnvelopeReader() + whenever(fixture.serializer.deserializeEvent(any())).thenReturn(null) + val sut = fixture.getSut() + val path = getTempEnvelope("envelope-event-attachment.txt") + assertTrue(File(path).exists()) // sanity check + sut.processEnvelopeFile(path, mock()) + + // Additionally make sure we have no errors logged + verify(fixture.logger).log(eq(SentryLevel.ERROR), any(), any()) + verify(fixture.hub, never()).captureEvent(any()) + assertFalse(File(path).exists()) + } + + @Test + fun `when parser is EnvelopeReader and serializer returns a null envelope, file error logged, no event captured `() { + fixture.envelopeReader = EnvelopeReader() + whenever(fixture.serializer.deserializeEnvelope(any())).thenReturn(null) + whenever(fixture.serializer.deserializeEvent(any())).thenReturn(null) + val sut = fixture.getSut() + val path = getTempEnvelope("envelope-event-attachment.txt") + assertTrue(File(path).exists()) // sanity check + sut.processEnvelopeFile(path, mock()) + + // Additionally make sure we have no errors logged + verify(fixture.logger).log(eq(SentryLevel.ERROR), any(), any()) + verify(fixture.hub, never()).captureEvent(any()) + assertFalse(File(path).exists()) + } + + @Test + fun `when processEnvelopeFile is called with a invalid path, logs error`() { + val sut = fixture.getSut() + sut.processEnvelopeFile(File.separator + "i-hope-it-doesnt-exist" + File.separator + "file.txt", mock()) + verify(fixture.logger).log(eq(SentryLevel.ERROR), any(), argWhere { it is FileNotFoundException }) + } + + @Test + fun `when hub is null, ctor throws`() { + val clazz = Class.forName("io.sentry.core.OutboxSender") + val ctor = clazz.getConstructor(IHub::class.java, IEnvelopeReader::class.java, ISerializer::class.java, ILogger::class.java, Long::class.java) + val params = arrayOf(null, mock(), mock(), mock(), null) + assertFailsWith { ctor.newInstance(params) } + } + + @Test + fun `when envelopeReader is null, ctor throws`() { + val clazz = Class.forName("io.sentry.core.OutboxSender") + val ctor = clazz.getConstructor(IHub::class.java, IEnvelopeReader::class.java, ISerializer::class.java, ILogger::class.java, Long::class.java) + val params = arrayOf(mock(), null, mock(), mock(), 15000) + assertFailsWith { ctor.newInstance(params) } + } + + @Test + fun `when serializer is null, ctor throws`() { + val clazz = Class.forName("io.sentry.core.OutboxSender") + val ctor = clazz.getConstructor(IHub::class.java, IEnvelopeReader::class.java, ISerializer::class.java, ILogger::class.java, Long::class.java) + val params = arrayOf(mock(), mock(), null, mock(), 15000) + assertFailsWith { ctor.newInstance(params) } + } + + @Test + fun `when logger is null, ctor throws`() { + val clazz = Class.forName("io.sentry.core.OutboxSender") + val ctor = clazz.getConstructor(IHub::class.java, IEnvelopeReader::class.java, ISerializer::class.java, ILogger::class.java, Long::class.java) + val params = arrayOf(mock(), mock(), mock(), null, 15000) + assertFailsWith { ctor.newInstance(params) } + } + + @Test + fun `when file name is null, should not be relevant`() { + assertFalse(fixture.getSut().isRelevantFileName(null)) + } + + @Test + fun `when file name is current prefix, should be ignored`() { + assertFalse(fixture.getSut().isRelevantFileName(EnvelopeCache.PREFIX_CURRENT_SESSION_FILE)) + } + + @Test + fun `when file name is relevant, should return true`() { + assertTrue(fixture.getSut().isRelevantFileName("123.envelope")) + } +} diff --git a/sentry-core/src/test/java/io/sentry/core/SendCachedEventFireAndForgetIntegrationTest.kt b/sentry-core/src/test/java/io/sentry/core/SendCachedEnvelopeFireAndForgetIntegrationTest.kt similarity index 77% rename from sentry-core/src/test/java/io/sentry/core/SendCachedEventFireAndForgetIntegrationTest.kt rename to sentry-core/src/test/java/io/sentry/core/SendCachedEnvelopeFireAndForgetIntegrationTest.kt index 9c242ff75..fb32a8e08 100644 --- a/sentry-core/src/test/java/io/sentry/core/SendCachedEventFireAndForgetIntegrationTest.kt +++ b/sentry-core/src/test/java/io/sentry/core/SendCachedEnvelopeFireAndForgetIntegrationTest.kt @@ -7,20 +7,20 @@ import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions import kotlin.test.Test import kotlin.test.assertFalse -class SendCachedEventFireAndForgetIntegrationTest { +class SendCachedEnvelopeFireAndForgetIntegrationTest { private class Fixture { var hub: IHub = mock() var logger: ILogger = mock() var options = SentryOptions() - var callback = mock() + var callback = mock() init { options.isDebug = true options.setLogger(logger) } - fun getSut(): SendCachedEventFireAndForgetIntegration { - return SendCachedEventFireAndForgetIntegration(callback) + fun getSut(): SendCachedEnvelopeFireAndForgetIntegration { + return SendCachedEnvelopeFireAndForgetIntegration(callback) } } @@ -55,15 +55,15 @@ class SendCachedEventFireAndForgetIntegrationTest { @Test fun `when Factory returns null, register logs and exit`() { - val sut = SendCachedEventFireAndForgetIntegration(CustomFactory()) + val sut = SendCachedEnvelopeFireAndForgetIntegration(CustomFactory()) fixture.options.cacheDirPath = "abc" sut.register(fixture.hub, fixture.options) verify(fixture.logger).log(eq(SentryLevel.ERROR), eq("SendFireAndForget factory is null.")) verifyNoMoreInteractions(fixture.hub) } - private class CustomFactory : SendCachedEventFireAndForgetIntegration.SendFireAndForgetFactory { - override fun create(hub: IHub?, options: SentryOptions?): SendCachedEventFireAndForgetIntegration.SendFireAndForget? { + private class CustomFactory : SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForgetFactory { + override fun create(hub: IHub?, options: SentryOptions?): SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForget? { return null } } diff --git a/sentry-core/src/test/java/io/sentry/core/SendCachedEventTest.kt b/sentry-core/src/test/java/io/sentry/core/SendCachedEventTest.kt deleted file mode 100644 index 0d8453dd4..000000000 --- a/sentry-core/src/test/java/io/sentry/core/SendCachedEventTest.kt +++ /dev/null @@ -1,115 +0,0 @@ -package io.sentry.core - -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.doThrow -import com.nhaarman.mockitokotlin2.eq -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions -import com.nhaarman.mockitokotlin2.whenever -import io.sentry.core.cache.DiskCache -import io.sentry.core.hints.Retryable -import io.sentry.core.util.NoFlushTimeout -import java.io.File -import java.nio.file.Files -import java.nio.file.Path -import kotlin.test.AfterTest -import kotlin.test.BeforeTest -import kotlin.test.Test -import kotlin.test.assertFalse - -class SendCachedEventTest { - private class Fixture { - var hub: IHub? = mock() - var logger: ILogger? = mock() - var serializer: ISerializer? = mock() - var options = SentryOptions().NoFlushTimeout() - - init { - options.isDebug = true - options.setLogger(logger) - } - - fun getSut(): SendCachedEvent { - return SendCachedEvent(serializer!!, hub!!, logger!!, options.flushTimeoutMillis) - } - } - - private lateinit var tempDirectory: Path - private val fixture = Fixture() - - @BeforeTest - fun `before send`() { - tempDirectory = Files.createTempDirectory("send-cached-event-test") - } - - @AfterTest - fun `after send`() { - File(tempDirectory.toUri()).delete() - } - - @Test - fun `when directory doesn't exist, processDirectory logs and returns`() { - val sut = fixture.getSut() - sut.processDirectory(File("i don't exist")) - verify(fixture.logger)!!.log(eq(SentryLevel.WARNING), eq("Directory '%s' doesn't exist. No cached events to send."), any()) - verifyNoMoreInteractions(fixture.hub) - } - - @Test - fun `when directory is actually a file, processDirectory logs and returns`() { - val sut = fixture.getSut() - val testFile = File(Files.createTempFile("send-cached-event-test", DiskCache.FILE_SUFFIX).toUri()) - testFile.deleteOnExit() - sut.processDirectory(testFile) - verify(fixture.logger)!!.log(eq(SentryLevel.ERROR), eq("Cache dir %s is not a directory."), any()) - verifyNoMoreInteractions(fixture.hub) - } - - @Test - fun `when directory has non event files, processDirectory logs that`() { - val sut = fixture.getSut() - val testFile = File(Files.createTempFile(tempDirectory, "send-cached-event-test", ".not-right-suffix").toUri()) - sut.processDirectory(File(tempDirectory.toUri())) - testFile.deleteOnExit() - verify(fixture.logger)!!.log(eq(SentryLevel.DEBUG), eq("File '%s' doesn't match extension expected."), any()) - verifyNoMoreInteractions(fixture.hub) - } - - @Test - fun `when directory has event files, processDirectory captures with hub`() { - val expected = SentryEvent() - whenever(fixture.serializer!!.deserializeEvent(any())).thenReturn(expected) - val sut = fixture.getSut() - val testFile = File(Files.createTempFile(tempDirectory, "send-cached-event-test", DiskCache.FILE_SUFFIX).toUri()) - testFile.deleteOnExit() - sut.processDirectory(File(tempDirectory.toUri())) - verify(fixture.hub)!!.captureEvent(eq(expected), any()) - } - - @Test - fun `when serializer throws, error is logged, file deleted`() { - val expected = RuntimeException() - whenever(fixture.serializer!!.deserializeEvent(any())).doThrow(expected) - val sut = fixture.getSut() - val testFile = File(Files.createTempFile(tempDirectory, "send-cached-event-test", DiskCache.FILE_SUFFIX).toUri()) - testFile.deleteOnExit() - sut.processFile(testFile, mock()) - verify(fixture.logger)!!.log(eq(SentryLevel.ERROR), eq(expected), eq("Failed to capture cached event %s"), eq(testFile.absolutePath)) - verifyNoMoreInteractions(fixture.hub) - assertFalse(testFile.exists()) - } - - @Test - fun `when hub throws, file gets deleted`() { - whenever(fixture.serializer!!.deserializeEvent(any())).thenReturn(SentryEvent()) - val expected = RuntimeException() - whenever(fixture.serializer!!.deserializeEvent(any())).doThrow(expected) - val sut = fixture.getSut() - val testFile = File(Files.createTempFile(tempDirectory, "send-cached-event-test", DiskCache.FILE_SUFFIX).toUri()) - testFile.deleteOnExit() - sut.processFile(testFile, any()) - verify(fixture.logger)!!.log(eq(SentryLevel.ERROR), eq(expected), eq("Failed to capture cached event %s"), eq(testFile.absolutePath)) - verifyNoMoreInteractions(fixture.hub) - } -} diff --git a/sentry-core/src/test/java/io/sentry/core/SentryClientTest.kt b/sentry-core/src/test/java/io/sentry/core/SentryClientTest.kt index 471e22246..227bfb1a8 100644 --- a/sentry-core/src/test/java/io/sentry/core/SentryClientTest.kt +++ b/sentry-core/src/test/java/io/sentry/core/SentryClientTest.kt @@ -2,10 +2,8 @@ package io.sentry.core import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.anyOrNull -import com.nhaarman.mockitokotlin2.argWhere import com.nhaarman.mockitokotlin2.check import com.nhaarman.mockitokotlin2.eq -import com.nhaarman.mockitokotlin2.isNull import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.mockingDetails import com.nhaarman.mockitokotlin2.never @@ -15,8 +13,6 @@ import com.nhaarman.mockitokotlin2.whenever import io.sentry.core.hints.ApplyScopeData import io.sentry.core.hints.Cached import io.sentry.core.hints.DiskFlushNotification -import io.sentry.core.hints.SessionEndHint -import io.sentry.core.hints.SessionUpdateHint import io.sentry.core.protocol.Mechanism import io.sentry.core.protocol.Request import io.sentry.core.protocol.SdkVersion @@ -26,7 +22,9 @@ import io.sentry.core.protocol.User import io.sentry.core.transport.AsyncConnection import io.sentry.core.transport.HttpTransport import io.sentry.core.transport.ITransportGate +import java.io.ByteArrayInputStream import java.io.IOException +import java.io.InputStreamReader import java.net.URL import java.util.UUID import kotlin.test.Ignore @@ -48,8 +46,10 @@ class SentryClientTest { name = "test" version = "1.2.3" } + setSerializer(GsonSerializer(mock(), envelopeReader)) } var connection: AsyncConnection = mock() + fun getSut() = SentryClient(sentryOptions, connection) } @@ -120,17 +120,22 @@ class SentryClientTest { val sut = fixture.getSut() val event = SentryEvent() sut.captureEvent(event) - verify(fixture.connection, never()).send(event) + verify(fixture.connection, never()).send(any()) } @Test fun `when beforeSend is returns new instance, new instance is sent`() { - val expected = SentryEvent() + val expected = SentryEvent().apply { + setTag("test", "test") + } fixture.sentryOptions.setBeforeSend { _, _ -> expected } val sut = fixture.getSut() val actual = SentryEvent() sut.captureEvent(actual) - verify(fixture.connection).send(eq(expected), isNull()) + verify(fixture.connection).send(check { + val event = getEventFromData(it.items.first().data) + assertEquals("test", event.tags["test"]) + }, anyOrNull()) verifyNoMoreInteractions(fixture.connection) } @@ -157,7 +162,7 @@ class SentryClientTest { val sut = fixture.getSut() val expectedHint = Object() sut.captureEvent(event, expectedHint) - verify(fixture.connection).send(event, expectedHint) + verify(fixture.connection).send(any(), eq(expectedHint)) } @Test @@ -422,6 +427,7 @@ class SentryClientTest { val event = SentryEvent() val scope = createScope() val processor = mock() + whenever(processor.process(any(), anyOrNull())).thenReturn(event) scope.addEventProcessor(processor) val sut = fixture.getSut() @@ -577,39 +583,6 @@ class SentryClientTest { } } - @Test - fun `When event comes from uncaughtException, captureSession should use SessionEndHint`() { - fixture.sentryOptions.release = "a@1+1" - val sut = fixture.getSut() - - val event = SentryEvent().apply { - exceptions = createNonHandledException() - } - val scope = Scope(fixture.sentryOptions) - scope.startSession() - val hint = mock() - sut.captureEvent(event, scope, hint) - verify(fixture.connection).send(any(), argWhere { - it is SessionEndHint - }) - } - - @Test - fun `When event is not from uncaughtException, captureSession should use SessionUpdateHint`() { - fixture.sentryOptions.release = "a@1+1" - val sut = fixture.getSut() - - val event = SentryEvent().apply { - exceptions = createNonHandledException() - } - val scope = Scope(fixture.sentryOptions) - scope.startSession() - sut.captureEvent(event, scope) - verify(fixture.connection).send(any(), argWhere { - it is SessionUpdateHint - }) - } - @Test fun `when captureEvent with sampling, session is still updated`() { fixture.sentryOptions.sampleRate = 1.0 @@ -631,13 +604,13 @@ class SentryClientTest { fun `when context property is missing on the event, property from scope contexts is applied`() { val sut = fixture.getSut() - val event = SentryEvent() val scope = Scope(fixture.sentryOptions) scope.setContexts("key", "value") scope.startSession().current - sut.captureEvent(event, scope, null) - verify(fixture.connection).send(check() { - assertEquals("value", it.contexts["key"]) + sut.captureEvent(SentryEvent(), scope, null) + verify(fixture.connection).send(check { + val event = getEventFromData(it.items.first().data) + assertEquals("value", event.contexts["key"]) }, anyOrNull()) } @@ -651,8 +624,9 @@ class SentryClientTest { scope.setContexts("key", "scope value") scope.startSession().current sut.captureEvent(event, scope, null) - verify(fixture.connection).send(check() { - assertEquals("event value", it.contexts["key"]) + verify(fixture.connection).send(check { + val eventFromData = getEventFromData(it.items.first().data) + assertEquals("event value", eventFromData.contexts["key"]) }, anyOrNull()) } @@ -705,6 +679,11 @@ class SentryClientTest { return listOf(exception) } + private fun getEventFromData(data: ByteArray): SentryEvent { + val inputStream = InputStreamReader(ByteArrayInputStream(data)) + return fixture.sentryOptions.serializer.deserializeEvent(inputStream) + } + internal class CustomCachedApplyScopeDataHint : Cached, ApplyScopeData internal class DiskFlushNotificationHint : DiskFlushNotification { diff --git a/sentry-core/src/test/java/io/sentry/core/SentryOptionsTest.kt b/sentry-core/src/test/java/io/sentry/core/SentryOptionsTest.kt index 5068179e0..89732b673 100644 --- a/sentry-core/src/test/java/io/sentry/core/SentryOptionsTest.kt +++ b/sentry-core/src/test/java/io/sentry/core/SentryOptionsTest.kt @@ -106,19 +106,6 @@ class SentryOptionsTest { assertEquals("${File.separator}test${File.separator}outbox", options.outboxPath) } - @Test - fun `when there's no cacheDirPath, sessionPath returns null`() { - val options = SentryOptions() - assertNull(options.sessionsPath) - } - - @Test - fun `when cacheDirPath is set, sessionPath concatenate sessions path`() { - val options = SentryOptions() - options.cacheDirPath = "${File.separator}test" - assertEquals("${File.separator}test${File.separator}sessions", options.sessionsPath) - } - @Test fun `SentryOptions creates SentryExecutorService on ctor`() { val options = SentryOptions() diff --git a/sentry-core/src/test/java/io/sentry/core/SentryTest.kt b/sentry-core/src/test/java/io/sentry/core/SentryTest.kt index 348946fa9..61b579c6f 100644 --- a/sentry-core/src/test/java/io/sentry/core/SentryTest.kt +++ b/sentry-core/src/test/java/io/sentry/core/SentryTest.kt @@ -46,20 +46,6 @@ class SentryTest { file.deleteOnExit() } - @Test - fun `sessionDir should be created at initialization`() { - var sentryOptions: SentryOptions? = null - Sentry.init { - it.dsn = "http://key@localhost/proj" - it.cacheDirPath = getTempPath() - sentryOptions = it - } - - val file = File(sentryOptions!!.sessionsPath!!) - assertTrue(file.exists()) - file.deleteOnExit() - } - @Test fun `Init sets SystemOutLogger if logger is NoOp and debug is enabled`() { var sentryOptions: SentryOptions? = null diff --git a/sentry-core/src/test/java/io/sentry/core/cache/DiskCacheTest.kt b/sentry-core/src/test/java/io/sentry/core/cache/DiskCacheTest.kt deleted file mode 100644 index d79e47b2a..000000000 --- a/sentry-core/src/test/java/io/sentry/core/cache/DiskCacheTest.kt +++ /dev/null @@ -1,114 +0,0 @@ -package io.sentry.core.cache - -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.doAnswer -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.whenever -import io.sentry.core.ISerializer -import io.sentry.core.SentryEvent -import io.sentry.core.SentryOptions -import io.sentry.core.protocol.SentryId -import java.io.Reader -import java.io.Writer -import java.nio.file.Files -import java.nio.file.Path -import kotlin.test.AfterTest -import kotlin.test.Test -import kotlin.test.assertEquals - -class DiskCacheTest { - private class Fixture { - val maxSize = 5 - val dir: Path = Files.createTempDirectory("sentry-disk-cache-test") - - fun getSUT(): DiskCache { - val options = SentryOptions() - options.cacheDirSize = maxSize - options.cacheDirPath = dir.toAbsolutePath().toFile().absolutePath - - val serializer = mock() - doAnswer { - val event = it.arguments[0] as SentryEvent - val writer = it.arguments[1] as Writer - - writer.write(event.eventId.toString()) - }.whenever(serializer).serialize(any(), any()) - - whenever(serializer.deserializeEvent(any())).thenAnswer { - val reader = it.arguments[0] as Reader - - val ret = SentryEvent() - val text = reader.readText() - ret.eventId = SentryId(text) - ret - } - - options.setSerializer(serializer) - - return DiskCache(options) - } - } - - private val fixture = Fixture() - - @AfterTest - fun cleanUp() { - fixture.dir.toFile().listFiles()?.forEach { it.delete() } - Files.delete(fixture.dir) - } - - @Test - fun `stores events`() { - val cache = fixture.getSUT() - - val nofFiles = { fixture.dir.toFile().list()?.size } - - assertEquals(0, nofFiles()) - - cache.store(SentryEvent()) - - assertEquals(1, nofFiles()) - } - - @Test - fun `limits the number of stored events`() { - val cache = fixture.getSUT() - - val nofFiles = { fixture.dir.toFile().list()?.size } - - assertEquals(0, nofFiles()) - - (1..fixture.maxSize + 1).forEach { _ -> - cache.store(SentryEvent(Exception())) - } - - assertEquals(fixture.maxSize, nofFiles()) - } - - @Test - fun `tolerates discarding unknown event`() { - val cache = fixture.getSUT() - - cache.discard(SentryEvent()) - - // no exception thrown - } - - @Test - fun `reads the event back`() { - - val cache = fixture.getSUT() - - val event = SentryEvent() - - cache.store(event) - - val readEvents = cache.toList() - - assertEquals(1, readEvents.size) - - val readEvent = readEvents[0] - - assertEquals(event.eventId.toString(), readEvent.eventId.toString()) - } -} diff --git a/sentry-core/src/test/java/io/sentry/core/cache/SessionCacheTest.kt b/sentry-core/src/test/java/io/sentry/core/cache/EnvelopeCacheTest.kt similarity index 69% rename from sentry-core/src/test/java/io/sentry/core/cache/SessionCacheTest.kt rename to sentry-core/src/test/java/io/sentry/core/cache/EnvelopeCacheTest.kt index b4d2e1586..fdfc1cf86 100644 --- a/sentry-core/src/test/java/io/sentry/core/cache/SessionCacheTest.kt +++ b/sentry-core/src/test/java/io/sentry/core/cache/EnvelopeCacheTest.kt @@ -11,12 +11,10 @@ import io.sentry.core.SentryEnvelope import io.sentry.core.SentryLevel import io.sentry.core.SentryOptions import io.sentry.core.Session -import io.sentry.core.cache.SessionCache.PREFIX_CURRENT_SESSION_FILE -import io.sentry.core.cache.SessionCache.SUFFIX_CURRENT_SESSION_FILE -import io.sentry.core.cache.SessionCache.SUFFIX_ENVELOPE_FILE +import io.sentry.core.cache.EnvelopeCache.PREFIX_CURRENT_SESSION_FILE +import io.sentry.core.cache.EnvelopeCache.SUFFIX_CURRENT_SESSION_FILE import io.sentry.core.hints.SessionEndHint import io.sentry.core.hints.SessionStartHint -import io.sentry.core.hints.SessionUpdateHint import io.sentry.core.protocol.User import java.io.File import java.nio.file.Files @@ -27,7 +25,7 @@ import kotlin.test.assertFalse import kotlin.test.assertNotNull import kotlin.test.assertTrue -class SessionCacheTest { +class EnvelopeCacheTest { private class Fixture { val maxSize = 5 val dir: Path = Files.createTempDirectory("sentry-session-cache-test") @@ -36,9 +34,7 @@ class SessionCacheTest { val logger = mock() fun getSUT(): IEnvelopeCache { - options.sessionsDirSize = maxSize options.cacheDirPath = dir.toAbsolutePath().toFile().absolutePath - File(options.sessionsPath!!).mkdirs() whenever(serializer.deserializeSession(any())).thenAnswer { Session("dis", User(), "env", "rel") @@ -48,7 +44,7 @@ class SessionCacheTest { options.setSerializer(serializer) options.isDebug = true - return SessionCache(options) + return EnvelopeCache(options) } } @@ -58,7 +54,7 @@ class SessionCacheTest { fun `stores envelopes`() { val cache = fixture.getSUT() - val file = File(fixture.options.sessionsPath!!) + val file = File(fixture.options.cacheDirPath!!) val nofFiles = { file.list()?.size } assertEquals(0, nofFiles()) @@ -70,24 +66,6 @@ class SessionCacheTest { file.deleteRecursively() } - @Test - fun `limits the number of stored envelopes`() { - val cache = fixture.getSUT() - - val file = File(fixture.options.sessionsPath!!) - val nofFiles = { file.list()?.size } - - assertEquals(0, nofFiles()) - - (1..fixture.maxSize + 1).forEach { _ -> - cache.store(SentryEnvelope.fromSession(fixture.serializer, createSession(), null)) - } - - assertEquals(fixture.maxSize, nofFiles()) - - file.deleteRecursively() - } - @Test fun `tolerates discarding unknown envelope`() { val cache = fixture.getSUT() @@ -101,12 +79,12 @@ class SessionCacheTest { fun `creates current file on session start`() { val cache = fixture.getSUT() - val file = File(fixture.options.sessionsPath!!) + val file = File(fixture.options.cacheDirPath!!) val envelope = SentryEnvelope.fromSession(fixture.serializer, createSession(), null) cache.store(envelope, SessionStartHint()) - val currentFile = File(fixture.options.sessionsPath!!, "$PREFIX_CURRENT_SESSION_FILE$SUFFIX_CURRENT_SESSION_FILE") + val currentFile = File(fixture.options.cacheDirPath!!, "$PREFIX_CURRENT_SESSION_FILE$SUFFIX_CURRENT_SESSION_FILE") assertTrue(currentFile.exists()) file.deleteRecursively() @@ -116,12 +94,12 @@ class SessionCacheTest { fun `deletes current file on session end`() { val cache = fixture.getSUT() - val file = File(fixture.options.sessionsPath!!) + val file = File(fixture.options.cacheDirPath!!) val envelope = SentryEnvelope.fromSession(fixture.serializer, createSession(), null) cache.store(envelope, SessionStartHint()) - val currentFile = File(fixture.options.sessionsPath!!, "$PREFIX_CURRENT_SESSION_FILE$SUFFIX_CURRENT_SESSION_FILE") + val currentFile = File(fixture.options.cacheDirPath!!, "$PREFIX_CURRENT_SESSION_FILE$SUFFIX_CURRENT_SESSION_FILE") assertTrue(currentFile.exists()) cache.store(envelope, SessionEndHint()) @@ -130,39 +108,16 @@ class SessionCacheTest { file.deleteRecursively() } - @Test - fun `updates current file on session update, but do not create a new envelope`() { - val cache = fixture.getSUT() - - val file = File(fixture.options.sessionsPath!!) - - val envelope = SentryEnvelope.fromSession(fixture.serializer, createSession(), null) - cache.store(envelope, SessionStartHint()) - - val currentFile = File(fixture.options.sessionsPath!!, "$PREFIX_CURRENT_SESSION_FILE$SUFFIX_CURRENT_SESSION_FILE") - assertTrue(currentFile.exists()) - - val newEnvelope = SentryEnvelope.fromSession(fixture.serializer, createSession(), null) - - cache.store(newEnvelope, SessionUpdateHint()) - assertTrue(currentFile.exists()) - - val newFile = File(file.absolutePath, "${newEnvelope.header.eventId}$SUFFIX_ENVELOPE_FILE") - assertFalse(newFile.exists()) - - file.deleteRecursively() - } - @Test fun `updates current file on session update and read it back`() { val cache = fixture.getSUT() - val file = File(fixture.options.sessionsPath!!) + val file = File(fixture.options.cacheDirPath!!) val envelope = SentryEnvelope.fromSession(fixture.serializer, createSession(), null) cache.store(envelope, SessionStartHint()) - val currentFile = File(fixture.options.sessionsPath!!, "$PREFIX_CURRENT_SESSION_FILE$SUFFIX_CURRENT_SESSION_FILE") + val currentFile = File(fixture.options.cacheDirPath!!, "$PREFIX_CURRENT_SESSION_FILE$SUFFIX_CURRENT_SESSION_FILE") assertTrue(currentFile.exists()) val session = fixture.serializer.deserializeSession(currentFile.bufferedReader(Charsets.UTF_8)) @@ -190,8 +145,8 @@ class SessionCacheTest { fun `when session start, current file already exist and crash marker file exist, end session and delete marker file`() { val cache = fixture.getSUT() - val file = File(fixture.options.sessionsPath!!) - val markerFile = File(fixture.options.cacheDirPath!!, SessionCache.CRASH_MARKER_FILE) + val file = File(fixture.options.cacheDirPath!!) + val markerFile = File(fixture.options.cacheDirPath!!, EnvelopeCache.CRASH_MARKER_FILE) markerFile.mkdirs() assertTrue(markerFile.exists()) @@ -209,8 +164,8 @@ class SessionCacheTest { @Test fun `when session start, current file already exist and crash marker file exist, end session with given timestamp`() { val cache = fixture.getSUT() - val file = File(fixture.options.sessionsPath!!) - val markerFile = File(fixture.options.cacheDirPath!!, SessionCache.CRASH_MARKER_FILE) + val file = File(fixture.options.cacheDirPath!!) + val markerFile = File(fixture.options.cacheDirPath!!, EnvelopeCache.CRASH_MARKER_FILE) File(fixture.options.cacheDirPath!!, ".sentry-native").mkdirs() markerFile.createNewFile() val date = "2020-02-07T14:16:00.000Z" diff --git a/sentry-core/src/test/java/io/sentry/core/transport/AsyncConnectionTest.kt b/sentry-core/src/test/java/io/sentry/core/transport/AsyncConnectionTest.kt index 2b7b8dc05..0e54785eb 100644 --- a/sentry-core/src/test/java/io/sentry/core/transport/AsyncConnectionTest.kt +++ b/sentry-core/src/test/java/io/sentry/core/transport/AsyncConnectionTest.kt @@ -17,7 +17,6 @@ import io.sentry.core.SentryEvent import io.sentry.core.SentryOptions import io.sentry.core.Session import io.sentry.core.cache.IEnvelopeCache -import io.sentry.core.cache.IEventCache import io.sentry.core.dsnString import io.sentry.core.protocol.User import java.io.IOException @@ -30,8 +29,7 @@ class AsyncConnectionTest { private class Fixture { var transport = mock() var transportGate = mock() - var eventCache = mock() - var sessionCache = mock() + var envelopeCache = mock() var executor = mock() var sentryOptions: SentryOptions = SentryOptions().apply { dsn = dsnString @@ -46,70 +44,34 @@ class AsyncConnectionTest { } fun getSUT(): AsyncConnection { - return AsyncConnection(transport, transportGate, eventCache, sessionCache, executor, sentryOptions) + return AsyncConnection(transport, transportGate, envelopeCache, executor, sentryOptions) } } private val fixture = Fixture() @Test - fun `successful send discards the event from cache`() { - // given - val ev = mock() - whenever(fixture.transportGate.isConnected).thenReturn(true) - whenever(fixture.transport.send(any())).thenReturn(TransportResult.success()) - - // when - fixture.getSUT().send(ev) - - // then - val order = inOrder(fixture.transport, fixture.eventCache) - - // because storeBeforeSend is enabled by default - order.verify(fixture.eventCache).store(eq(ev)) - - order.verify(fixture.transport).send(check { - assertEquals(ev.eventId, it.header.eventId) - }) - order.verify(fixture.eventCache).discard(eq(ev)) - } - - @Test - fun `successful send discards the session from cache`() { + fun `successful send discards the envelope from cache`() { // given val envelope = SentryEnvelope.fromSession(fixture.sentryOptions.serializer, createSession(), null) whenever(fixture.transportGate.isConnected).thenReturn(true) - whenever(fixture.transport.send(any())).thenReturn(TransportResult.success()) + whenever(fixture.transport.send(any())).thenReturn(TransportResult.success()) // when fixture.getSUT().send(envelope) // then - val order = inOrder(fixture.transport, fixture.sessionCache) + val order = inOrder(fixture.transport, fixture.envelopeCache) // because storeBeforeSend is enabled by default - order.verify(fixture.sessionCache).store(eq(envelope), anyOrNull()) + order.verify(fixture.envelopeCache).store(eq(envelope), anyOrNull()) order.verify(fixture.transport).send(eq(envelope)) - order.verify(fixture.sessionCache).discard(eq(envelope)) + order.verify(fixture.envelopeCache).discard(eq(envelope)) } @Test - fun `stores event in cache if sending is not allowed`() { - // given - val ev = mock() - whenever(fixture.transportGate.isConnected).thenReturn(false) - - // when - fixture.getSUT().send(ev) - - // then - verify(fixture.eventCache).store(eq(ev)) - verify(fixture.transport).isRetryAfter(any()) - } - - @Test - fun `stores session in cache if sending is not allowed`() { + fun `stores envelope in cache if sending is not allowed`() { // given val envelope = SentryEnvelope.fromSession(fixture.sentryOptions.serializer, createSession(), null) whenever(fixture.transportGate.isConnected).thenReturn(false) @@ -118,42 +80,16 @@ class AsyncConnectionTest { fixture.getSUT().send(envelope) // then - verify(fixture.sessionCache).store(eq(envelope), anyOrNull()) + verify(fixture.envelopeCache).store(eq(envelope), anyOrNull()) verify(fixture.transport).isRetryAfter(any()) } @Test - fun `stores event after unsuccessful send`() { - // given - val ev = mock() - whenever(fixture.transportGate.isConnected).thenReturn(true) - whenever(fixture.transport.send(any())).thenReturn(TransportResult.error(500)) - - // when - try { - fixture.getSUT().send(ev) - } catch (e: IllegalStateException) { - // expected - this is how the AsyncConnection signals failure to the executor for it to retry - } - - // then - val order = inOrder(fixture.transport, fixture.eventCache) - - // because storeBeforeSend is enabled by default - order.verify(fixture.eventCache).store(eq(ev)) - - order.verify(fixture.transport).send(check { - assertEquals(ev.eventId, it.header.eventId) - }) - verify(fixture.eventCache, never()).discard(any()) - } - - @Test - fun `stores session after unsuccessful send`() { + fun `stores envelope after unsuccessful send`() { // given val envelope = SentryEnvelope.fromSession(fixture.sentryOptions.serializer, createSession(), null) whenever(fixture.transportGate.isConnected).thenReturn(true) - whenever(fixture.transport.send(any())).thenReturn(TransportResult.error(500)) + whenever(fixture.transport.send(any())).thenReturn(TransportResult.error(500)) // when try { @@ -163,43 +99,21 @@ class AsyncConnectionTest { } // then - val order = inOrder(fixture.transport, fixture.sessionCache) + val order = inOrder(fixture.transport, fixture.envelopeCache) // because storeBeforeSend is enabled by default - order.verify(fixture.sessionCache).store(eq(envelope), anyOrNull()) + order.verify(fixture.envelopeCache).store(eq(envelope), anyOrNull()) order.verify(fixture.transport).send(eq(envelope)) - verify(fixture.eventCache, never()).discard(any()) - } - - @Test - fun `stores event after send failure`() { - // given - val ev = mock() - whenever(fixture.transportGate.isConnected).thenReturn(true) - whenever(fixture.transport.send(any())).thenThrow(IOException()) - - // when - try { - fixture.getSUT().send(ev) - } catch (e: IllegalStateException) { - // expected - this is how the AsyncConnection signals failure to the executor for it to retry - } - - // then - val order = inOrder(fixture.transport, fixture.eventCache) - order.verify(fixture.transport).send(check { - assertEquals(ev.eventId, it.header.eventId) - }) - verify(fixture.eventCache, never()).discard(any()) + verify(fixture.envelopeCache, never()).discard(any()) } @Test - fun `stores session after send failure`() { + fun `stores envelope after send failure`() { // given val envelope = SentryEnvelope.fromSession(fixture.sentryOptions.serializer, createSession(), null) whenever(fixture.transportGate.isConnected).thenReturn(true) - whenever(fixture.transport.send(any())).thenThrow(IOException()) + whenever(fixture.transport.send(any())).thenThrow(IOException()) // when try { @@ -209,9 +123,9 @@ class AsyncConnectionTest { } // then - val order = inOrder(fixture.transport, fixture.sessionCache) + val order = inOrder(fixture.transport, fixture.envelopeCache) order.verify(fixture.transport).send(eq(envelope)) - verify(fixture.sessionCache, never()).discard(any()) + verify(fixture.envelopeCache, never()).discard(any()) } @Test @@ -219,9 +133,10 @@ class AsyncConnectionTest { // given val ev = mock() whenever(fixture.transport.isRetryAfter(any())).thenReturn(true) + val envelope = SentryEnvelope.fromEvent(fixture.sentryOptions.serializer, ev, null) // when - fixture.getSUT().send(ev) + fixture.getSUT().send(envelope) // then verify(fixture.executor, never()).submit(any()) @@ -232,9 +147,10 @@ class AsyncConnectionTest { // given val ev = mock() whenever(fixture.transport.isRetryAfter(any())).thenReturn(false) + val envelope = SentryEnvelope.fromEvent(fixture.sentryOptions.serializer, ev, null) // when - fixture.getSUT().send(ev) + fixture.getSUT().send(envelope) // then verify(fixture.executor).submit(any()) @@ -263,7 +179,7 @@ class AsyncConnectionTest { fixture.getSUT().send(envelope, CachedEvent()) // then - verify(fixture.sessionCache).discard(any()) + verify(fixture.envelopeCache).discard(any()) } @Test @@ -276,7 +192,7 @@ class AsyncConnectionTest { fixture.getSUT().send(envelope) // then - verify(fixture.sessionCache, never()).discard(any()) + verify(fixture.envelopeCache, never()).discard(any()) } @Test @@ -313,12 +229,13 @@ class AsyncConnectionTest { // given val ev = mock() whenever(fixture.transport.isRetryAfter(any())).thenReturn(true) + val envelope = SentryEnvelope.fromEvent(fixture.sentryOptions.serializer, ev, null) // when - fixture.getSUT().send(ev, CachedEvent()) + fixture.getSUT().send(envelope, CachedEvent()) // then - verify(fixture.eventCache).discard(any()) + verify(fixture.envelopeCache).discard(any()) } @Test @@ -326,12 +243,13 @@ class AsyncConnectionTest { // given val ev = mock() whenever(fixture.transport.isRetryAfter(any())).thenReturn(true) + val envelope = SentryEnvelope.fromEvent(fixture.sentryOptions.serializer, ev, null) // when - fixture.getSUT().send(ev) + fixture.getSUT().send(envelope) // then - verify(fixture.eventCache, never()).discard(any()) + verify(fixture.envelopeCache, never()).discard(any()) } private fun createSession(): Session {