diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 42446f8e58..b201a47d0c 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -517,72 +517,6 @@ public final class io/sentry/HttpStatusCodeRange { public fun isInRange (I)Z } -public final class io/sentry/Hub : io/sentry/IHub, io/sentry/metrics/MetricsApi$IMetricsInterface { - public fun (Lio/sentry/SentryOptions;)V - public fun addBreadcrumb (Lio/sentry/Breadcrumb;)V - public fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V - public fun bindClient (Lio/sentry/ISentryClient;)V - public fun captureCheckIn (Lio/sentry/CheckIn;)Lio/sentry/protocol/SentryId; - public fun captureEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; - public fun captureEvent (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; - public fun captureEvent (Lio/sentry/SentryEvent;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; - public fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; - public fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; - public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; - public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; - public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;Lio/sentry/Hint;Lio/sentry/ProfilingTraceData;)Lio/sentry/protocol/SentryId; - public fun captureUserFeedback (Lio/sentry/UserFeedback;)V - public fun clearBreadcrumbs ()V - public fun clone ()Lio/sentry/IHub; - public synthetic fun clone ()Ljava/lang/Object; - public fun close ()V - public fun close (Z)V - public fun configureScope (Lio/sentry/ScopeType;Lio/sentry/ScopeCallback;)V - public fun continueTrace (Ljava/lang/String;Ljava/util/List;)Lio/sentry/TransactionContext; - public fun endSession ()V - public fun flush (J)V - public fun forkedCurrentScope (Ljava/lang/String;)Lio/sentry/IScopes; - public fun forkedRootScopes (Ljava/lang/String;)Lio/sentry/IScopes; - public fun forkedScopes (Ljava/lang/String;)Lio/sentry/IScopes; - public fun getBaggage ()Lio/sentry/BaggageHeader; - public fun getDefaultTagsForMetrics ()Ljava/util/Map; - public fun getGlobalScope ()Lio/sentry/IScope; - public fun getIsolationScope ()Lio/sentry/IScope; - public fun getLastEventId ()Lio/sentry/protocol/SentryId; - public fun getLocalMetricsAggregator ()Lio/sentry/metrics/LocalMetricsAggregator; - public fun getMetricsAggregator ()Lio/sentry/IMetricsAggregator; - public fun getOptions ()Lio/sentry/SentryOptions; - public fun getRateLimiter ()Lio/sentry/transport/RateLimiter; - public fun getScope ()Lio/sentry/IScope; - public fun getSpan ()Lio/sentry/ISpan; - public fun getTraceparent ()Lio/sentry/SentryTraceHeader; - public fun getTransaction ()Lio/sentry/ITransaction; - public fun isCrashedLastRun ()Ljava/lang/Boolean; - public fun isEnabled ()Z - public fun isHealthy ()Z - public fun makeCurrent ()Lio/sentry/ISentryLifecycleToken; - public fun metrics ()Lio/sentry/metrics/MetricsApi; - public fun popScope ()V - public fun pushIsolationScope ()Lio/sentry/ISentryLifecycleToken; - public fun pushScope ()Lio/sentry/ISentryLifecycleToken; - public fun removeExtra (Ljava/lang/String;)V - public fun removeTag (Ljava/lang/String;)V - public fun reportFullyDisplayed ()V - public fun setExtra (Ljava/lang/String;Ljava/lang/String;)V - public fun setFingerprint (Ljava/util/List;)V - public fun setLevel (Lio/sentry/SentryLevel;)V - public fun setSpanContext (Ljava/lang/Throwable;Lio/sentry/ISpan;Ljava/lang/String;)V - public fun setTag (Ljava/lang/String;Ljava/lang/String;)V - public fun setTransaction (Ljava/lang/String;)V - public fun setUser (Lio/sentry/protocol/User;)V - public fun startSession ()V - public fun startSpanForMetric (Ljava/lang/String;Ljava/lang/String;)Lio/sentry/ISpan; - public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; - public fun traceHeaders ()Lio/sentry/SentryTraceHeader; - public fun withIsolationScope (Lio/sentry/ScopeCallback;)V - public fun withScope (Lio/sentry/ScopeCallback;)V -} - public final class io/sentry/HubAdapter : io/sentry/IHub { public fun addBreadcrumb (Lio/sentry/Breadcrumb;)V public fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V diff --git a/sentry/src/main/java/io/sentry/Hub.java b/sentry/src/main/java/io/sentry/Hub.java deleted file mode 100644 index 63081e6cd2..0000000000 --- a/sentry/src/main/java/io/sentry/Hub.java +++ /dev/null @@ -1,1069 +0,0 @@ -package io.sentry; - -import io.sentry.Stack.StackItem; -import io.sentry.clientreport.DiscardReason; -import io.sentry.hints.SessionEndHint; -import io.sentry.hints.SessionStartHint; -import io.sentry.metrics.LocalMetricsAggregator; -import io.sentry.metrics.MetricsApi; -import io.sentry.protocol.SentryId; -import io.sentry.protocol.SentryTransaction; -import io.sentry.protocol.User; -import io.sentry.transport.RateLimiter; -import io.sentry.util.ExceptionUtils; -import io.sentry.util.HintUtils; -import io.sentry.util.Objects; -import io.sentry.util.Pair; -import io.sentry.util.TracingUtils; -import java.io.Closeable; -import java.io.IOException; -import java.lang.ref.WeakReference; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.WeakHashMap; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -// TODO [HSM] remove Hub class -@Deprecated -public final class Hub implements IHub, MetricsApi.IMetricsInterface { - - private volatile @NotNull SentryId lastEventId; - private final @NotNull SentryOptions options; - private volatile boolean isEnabled; - private final @NotNull Stack stack; - private final @NotNull TracesSampler tracesSampler; - private final @NotNull Map, String>> throwableToSpan = - Collections.synchronizedMap(new WeakHashMap<>()); - private final @NotNull TransactionPerformanceCollector transactionPerformanceCollector; - private final @NotNull MetricsApi metricsApi; - - public Hub(final @NotNull SentryOptions options) { - this(options, createRootStackItem(options)); - // Integrations are no longer registered on Hub ctor, but on Sentry.init - } - - private Hub(final @NotNull SentryOptions options, final @NotNull Stack stack) { - validateOptions(options); - - this.options = options; - this.tracesSampler = new TracesSampler(options); - this.stack = stack; - this.lastEventId = SentryId.EMPTY_ID; - this.transactionPerformanceCollector = options.getTransactionPerformanceCollector(); - - // Integrations will use this Hub instance once registered. - // Make sure Hub ready to be used then. - this.isEnabled = true; - - this.metricsApi = new MetricsApi(this); - } - - private Hub(final @NotNull SentryOptions options, final @NotNull StackItem rootStackItem) { - this(options, new Stack(options.getLogger(), rootStackItem)); - } - - private static void validateOptions(final @NotNull SentryOptions options) { - Objects.requireNonNull(options, "SentryOptions is required."); - if (options.getDsn() == null || options.getDsn().isEmpty()) { - throw new IllegalArgumentException( - "Hub requires a DSN to be instantiated. Considering using the NoOpHub if no DSN is available."); - } - } - - private static StackItem createRootStackItem(final @NotNull SentryOptions options) { - validateOptions(options); - final IScope scope = new Scope(options); - final ISentryClient client = new SentryClient(options); - return new StackItem(options, client, scope); - } - - @Override - public boolean isEnabled() { - return isEnabled; - } - - @Override - public @NotNull SentryId captureEvent( - final @NotNull SentryEvent event, final @Nullable Hint hint) { - return captureEventInternal(event, hint, null); - } - - @Override - public @NotNull SentryId captureEvent( - final @NotNull SentryEvent event, - final @Nullable Hint hint, - final @NotNull ScopeCallback callback) { - return captureEventInternal(event, hint, callback); - } - - private @NotNull SentryId captureEventInternal( - final @NotNull SentryEvent event, - final @Nullable Hint hint, - final @Nullable ScopeCallback scopeCallback) { - SentryId sentryId = SentryId.EMPTY_ID; - if (!isEnabled()) { - options - .getLogger() - .log( - SentryLevel.WARNING, "Instance is disabled and this 'captureEvent' call is a no-op."); - } else if (event == null) { - options.getLogger().log(SentryLevel.WARNING, "captureEvent called with null parameter."); - } else { - try { - assignTraceContext(event); - final StackItem item = stack.peek(); - - final IScope scope = buildLocalScope(item.getScope(), scopeCallback); - - sentryId = item.getClient().captureEvent(event, scope, hint); - this.lastEventId = sentryId; - } catch (Throwable e) { - options - .getLogger() - .log( - SentryLevel.ERROR, "Error while capturing event with id: " + event.getEventId(), e); - } - } - return sentryId; - } - - @Override - public @NotNull SentryId captureMessage( - final @NotNull String message, final @NotNull SentryLevel level) { - return captureMessageInternal(message, level, null); - } - - @Override - public @NotNull SentryId captureMessage( - final @NotNull String message, - final @NotNull SentryLevel level, - final @NotNull ScopeCallback callback) { - return captureMessageInternal(message, level, callback); - } - - private @NotNull SentryId captureMessageInternal( - final @NotNull String message, - final @NotNull SentryLevel level, - final @Nullable ScopeCallback scopeCallback) { - SentryId sentryId = SentryId.EMPTY_ID; - if (!isEnabled()) { - options - .getLogger() - .log( - SentryLevel.WARNING, - "Instance is disabled and this 'captureMessage' call is a no-op."); - } else if (message == null) { - options.getLogger().log(SentryLevel.WARNING, "captureMessage called with null parameter."); - } else { - try { - final StackItem item = stack.peek(); - - final IScope scope = buildLocalScope(item.getScope(), scopeCallback); - - sentryId = item.getClient().captureMessage(message, level, scope); - } catch (Throwable e) { - options.getLogger().log(SentryLevel.ERROR, "Error while capturing message: " + message, e); - } - } - this.lastEventId = sentryId; - return sentryId; - } - - @ApiStatus.Internal - @Override - public @NotNull SentryId captureEnvelope( - final @NotNull SentryEnvelope envelope, final @Nullable Hint hint) { - Objects.requireNonNull(envelope, "SentryEnvelope is required."); - - SentryId sentryId = SentryId.EMPTY_ID; - if (!isEnabled()) { - options - .getLogger() - .log( - SentryLevel.WARNING, - "Instance is disabled and this 'captureEnvelope' call is a no-op."); - } else { - try { - final SentryId capturedEnvelopeId = - stack.peek().getClient().captureEnvelope(envelope, hint); - if (capturedEnvelopeId != null) { - sentryId = capturedEnvelopeId; - } - } catch (Throwable e) { - options.getLogger().log(SentryLevel.ERROR, "Error while capturing envelope.", e); - } - } - return sentryId; - } - - @Override - public @NotNull SentryId captureException( - final @NotNull Throwable throwable, final @Nullable Hint hint) { - return captureExceptionInternal(throwable, hint, null); - } - - @Override - public @NotNull SentryId captureException( - final @NotNull Throwable throwable, - final @Nullable Hint hint, - final @NotNull ScopeCallback callback) { - - return captureExceptionInternal(throwable, hint, callback); - } - - private @NotNull SentryId captureExceptionInternal( - final @NotNull Throwable throwable, - final @Nullable Hint hint, - final @Nullable ScopeCallback scopeCallback) { - SentryId sentryId = SentryId.EMPTY_ID; - if (!isEnabled()) { - options - .getLogger() - .log( - SentryLevel.WARNING, - "Instance is disabled and this 'captureException' call is a no-op."); - } else if (throwable == null) { - options.getLogger().log(SentryLevel.WARNING, "captureException called with null parameter."); - } else { - try { - final StackItem item = stack.peek(); - final SentryEvent event = new SentryEvent(throwable); - assignTraceContext(event); - - final IScope scope = buildLocalScope(item.getScope(), scopeCallback); - - sentryId = item.getClient().captureEvent(event, scope, hint); - } catch (Throwable e) { - options - .getLogger() - .log( - SentryLevel.ERROR, "Error while capturing exception: " + throwable.getMessage(), e); - } - } - this.lastEventId = sentryId; - return sentryId; - } - - private void assignTraceContext(final @NotNull SentryEvent event) { - if (options.isTracingEnabled() && event.getThrowable() != null) { - final Pair, String> pair = - throwableToSpan.get(ExceptionUtils.findRootCause(event.getThrowable())); - if (pair != null) { - final WeakReference spanWeakRef = pair.getFirst(); - if (event.getContexts().getTrace() == null && spanWeakRef != null) { - final ISpan span = spanWeakRef.get(); - if (span != null) { - event.getContexts().setTrace(span.getSpanContext()); - } - } - final String transactionName = pair.getSecond(); - if (event.getTransaction() == null && transactionName != null) { - event.setTransaction(transactionName); - } - } - } - } - - @Override - public void captureUserFeedback(final @NotNull UserFeedback userFeedback) { - if (!isEnabled()) { - options - .getLogger() - .log( - SentryLevel.WARNING, - "Instance is disabled and this 'captureUserFeedback' call is a no-op."); - } else { - try { - final StackItem item = stack.peek(); - item.getClient().captureUserFeedback(userFeedback); - } catch (Throwable e) { - options - .getLogger() - .log( - SentryLevel.ERROR, - "Error while capturing captureUserFeedback: " + userFeedback.toString(), - e); - } - } - } - - @Override - public void startSession() { - if (!isEnabled()) { - options - .getLogger() - .log( - SentryLevel.WARNING, "Instance is disabled and this 'startSession' call is a no-op."); - } else { - final StackItem item = this.stack.peek(); - final Scope.SessionPair pair = item.getScope().startSession(); - if (pair != null) { - // TODO: add helper overload `captureSessions` to pass a list of sessions and submit a - // single envelope - // Or create the envelope here with both items and call `captureEnvelope` - if (pair.getPrevious() != null) { - final Hint hint = HintUtils.createWithTypeCheckHint(new SessionEndHint()); - - item.getClient().captureSession(pair.getPrevious(), hint); - } - - final Hint hint = HintUtils.createWithTypeCheckHint(new SessionStartHint()); - - item.getClient().captureSession(pair.getCurrent(), hint); - } else { - options.getLogger().log(SentryLevel.WARNING, "Session could not be started."); - } - } - } - - @Override - public void endSession() { - if (!isEnabled()) { - options - .getLogger() - .log(SentryLevel.WARNING, "Instance is disabled and this 'endSession' call is a no-op."); - } else { - final StackItem item = this.stack.peek(); - final Session previousSession = item.getScope().endSession(); - if (previousSession != null) { - final Hint hint = HintUtils.createWithTypeCheckHint(new SessionEndHint()); - - item.getClient().captureSession(previousSession, hint); - } - } - } - - @Override - public void close() { - close(false); - } - - @Override - @SuppressWarnings("FutureReturnValueIgnored") - public void close(final boolean isRestarting) { - if (!isEnabled()) { - options - .getLogger() - .log(SentryLevel.WARNING, "Instance is disabled and this 'close' call is a no-op."); - } else { - try { - for (Integration integration : options.getIntegrations()) { - if (integration instanceof Closeable) { - try { - ((Closeable) integration).close(); - } catch (IOException e) { - options - .getLogger() - .log(SentryLevel.WARNING, "Failed to close the integration {}.", integration, e); - } - } - } - - configureScope(scope -> scope.clear()); - options.getTransactionProfiler().close(); - options.getTransactionPerformanceCollector().close(); - final @NotNull ISentryExecutorService executorService = options.getExecutorService(); - if (isRestarting) { - executorService.submit(() -> executorService.close(options.getShutdownTimeoutMillis())); - } else { - executorService.close(options.getShutdownTimeoutMillis()); - } - - // Close the top-most client - final StackItem item = stack.peek(); - // TODO: should we end session before closing client? - item.getClient().close(isRestarting); - } catch (Throwable e) { - options.getLogger().log(SentryLevel.ERROR, "Error while closing the Hub.", e); - } - isEnabled = false; - } - } - - @Override - public void addBreadcrumb(final @NotNull Breadcrumb breadcrumb, final @Nullable Hint hint) { - if (!isEnabled()) { - options - .getLogger() - .log( - SentryLevel.WARNING, - "Instance is disabled and this 'addBreadcrumb' call is a no-op."); - } else if (breadcrumb == null) { - options.getLogger().log(SentryLevel.WARNING, "addBreadcrumb called with null parameter."); - } else { - stack.peek().getScope().addBreadcrumb(breadcrumb, hint); - } - } - - @Override - public void addBreadcrumb(final @NotNull Breadcrumb breadcrumb) { - addBreadcrumb(breadcrumb, new Hint()); - } - - @Override - public void setLevel(final @Nullable SentryLevel level) { - if (!isEnabled()) { - options - .getLogger() - .log(SentryLevel.WARNING, "Instance is disabled and this 'setLevel' call is a no-op."); - } else { - stack.peek().getScope().setLevel(level); - } - } - - @Override - public void setTransaction(final @Nullable String transaction) { - if (!isEnabled()) { - options - .getLogger() - .log( - SentryLevel.WARNING, - "Instance is disabled and this 'setTransaction' call is a no-op."); - } else if (transaction != null) { - stack.peek().getScope().setTransaction(transaction); - } else { - options.getLogger().log(SentryLevel.WARNING, "Transaction cannot be null"); - } - } - - @Override - public void setUser(final @Nullable User user) { - if (!isEnabled()) { - options - .getLogger() - .log(SentryLevel.WARNING, "Instance is disabled and this 'setUser' call is a no-op."); - } else { - stack.peek().getScope().setUser(user); - } - } - - @Override - public void setFingerprint(final @NotNull List fingerprint) { - if (!isEnabled()) { - options - .getLogger() - .log( - SentryLevel.WARNING, - "Instance is disabled and this 'setFingerprint' call is a no-op."); - } else if (fingerprint == null) { - options.getLogger().log(SentryLevel.WARNING, "setFingerprint called with null parameter."); - } else { - stack.peek().getScope().setFingerprint(fingerprint); - } - } - - @Override - public void clearBreadcrumbs() { - if (!isEnabled()) { - options - .getLogger() - .log( - SentryLevel.WARNING, - "Instance is disabled and this 'clearBreadcrumbs' call is a no-op."); - } else { - stack.peek().getScope().clearBreadcrumbs(); - } - } - - @Override - public void setTag(final @NotNull String key, final @NotNull String value) { - if (!isEnabled()) { - options - .getLogger() - .log(SentryLevel.WARNING, "Instance is disabled and this 'setTag' call is a no-op."); - } else if (key == null || value == null) { - options.getLogger().log(SentryLevel.WARNING, "setTag called with null parameter."); - } else { - stack.peek().getScope().setTag(key, value); - } - } - - @Override - public void removeTag(final @NotNull String key) { - if (!isEnabled()) { - options - .getLogger() - .log(SentryLevel.WARNING, "Instance is disabled and this 'removeTag' call is a no-op."); - } else if (key == null) { - options.getLogger().log(SentryLevel.WARNING, "removeTag called with null parameter."); - } else { - stack.peek().getScope().removeTag(key); - } - } - - @Override - public void setExtra(final @NotNull String key, final @NotNull String value) { - if (!isEnabled()) { - options - .getLogger() - .log(SentryLevel.WARNING, "Instance is disabled and this 'setExtra' call is a no-op."); - } else if (key == null || value == null) { - options.getLogger().log(SentryLevel.WARNING, "setExtra called with null parameter."); - } else { - stack.peek().getScope().setExtra(key, value); - } - } - - @Override - public void removeExtra(final @NotNull String key) { - if (!isEnabled()) { - options - .getLogger() - .log(SentryLevel.WARNING, "Instance is disabled and this 'removeExtra' call is a no-op."); - } else if (key == null) { - options.getLogger().log(SentryLevel.WARNING, "removeExtra called with null parameter."); - } else { - stack.peek().getScope().removeExtra(key); - } - } - - @Override - public @NotNull SentryId getLastEventId() { - return lastEventId; - } - - @Override - public @NotNull ISentryLifecycleToken pushScope() { - if (!isEnabled()) { - options - .getLogger() - .log(SentryLevel.WARNING, "Instance is disabled and this 'pushScope' call is a no-op."); - } else { - final StackItem item = stack.peek(); - final StackItem newItem = new StackItem(options, item.getClient(), item.getScope().clone()); - stack.push(newItem); - } - return NoOpScopesStorage.NoOpScopesLifecycleToken.getInstance(); - } - - @Override - public @NotNull ISentryLifecycleToken pushIsolationScope() { - return NoOpScopesStorage.NoOpScopesLifecycleToken.getInstance(); - } - - @Override - public @NotNull SentryOptions getOptions() { - return this.stack.peek().getOptions(); - } - - @Override - public @Nullable Boolean isCrashedLastRun() { - return SentryCrashLastRunState.getInstance() - .isCrashedLastRun(options.getCacheDirPath(), !options.isEnableAutoSessionTracking()); - } - - @Override - public void reportFullyDisplayed() { - if (options.isEnableTimeToFullDisplayTracing()) { - options.getFullyDisplayedReporter().reportFullyDrawn(); - } - } - - @Override - @Deprecated - public void popScope() { - if (!isEnabled()) { - options - .getLogger() - .log(SentryLevel.WARNING, "Instance is disabled and this 'popScope' call is a no-op."); - } else { - stack.pop(); - } - } - - @Override - public void withScope(final @NotNull ScopeCallback callback) { - if (!isEnabled()) { - try { - callback.run(NoOpScope.getInstance()); - } catch (Throwable e) { - options.getLogger().log(SentryLevel.ERROR, "Error in the 'withScope' callback.", e); - } - - } else { - pushScope(); - try { - callback.run(stack.peek().getScope()); - } catch (Throwable e) { - options.getLogger().log(SentryLevel.ERROR, "Error in the 'withScope' callback.", e); - } - popScope(); - } - } - - @Override - public void withIsolationScope(final @NotNull ScopeCallback callback) { - if (!isEnabled()) { - try { - callback.run(NoOpScope.getInstance()); - } catch (Throwable e) { - options.getLogger().log(SentryLevel.ERROR, "Error in the 'withScope' callback.", e); - } - - } else { - pushScope(); - try { - callback.run(stack.peek().getScope()); - } catch (Throwable e) { - options.getLogger().log(SentryLevel.ERROR, "Error in the 'withScope' callback.", e); - } - popScope(); - } - } - - @Override - public void configureScope( - final @Nullable ScopeType scopeType, final @NotNull ScopeCallback callback) { - if (!isEnabled()) { - options - .getLogger() - .log( - SentryLevel.WARNING, - "Instance is disabled and this 'configureScope' call is a no-op."); - } else { - try { - callback.run(stack.peek().getScope()); - } catch (Throwable e) { - options.getLogger().log(SentryLevel.ERROR, "Error in the 'configureScope' callback.", e); - } - } - } - - @Override - public void bindClient(final @NotNull ISentryClient client) { - if (!isEnabled()) { - options - .getLogger() - .log(SentryLevel.WARNING, "Instance is disabled and this 'bindClient' call is a no-op."); - } else { - final StackItem item = stack.peek(); - if (client != null) { - options.getLogger().log(SentryLevel.DEBUG, "New client bound to scope."); - item.setClient(client); - } else { - options.getLogger().log(SentryLevel.DEBUG, "NoOp client bound to scope."); - item.setClient(NoOpSentryClient.getInstance()); - } - } - } - - @Override - public boolean isHealthy() { - return stack.peek().getClient().isHealthy(); - } - - @Override - public void flush(long timeoutMillis) { - if (!isEnabled()) { - options - .getLogger() - .log(SentryLevel.WARNING, "Instance is disabled and this 'flush' call is a no-op."); - } else { - try { - stack.peek().getClient().flush(timeoutMillis); - } catch (Throwable e) { - options.getLogger().log(SentryLevel.ERROR, "Error in the 'client.flush'.", e); - } - } - } - - @Override - public @NotNull IHub clone() { - if (!isEnabled()) { - options.getLogger().log(SentryLevel.WARNING, "Disabled Hub cloned."); - } - // Clone will be invoked in parallel - return new Hub(this.options, new Stack(this.stack)); - } - - @Override - public @NotNull IScopes forkedScopes(@NotNull String creator) { - return Sentry.forkedScopes(creator); - } - - @Override - public @NotNull IScopes forkedCurrentScope(@NotNull String creator) { - return Sentry.forkedCurrentScope(creator); - } - - @Override - public @NotNull IScopes forkedRootScopes(final @NotNull String creator) { - return Sentry.forkedRootScopes(creator); - } - - @Override - public @NotNull ISentryLifecycleToken makeCurrent() { - return NoOpScopesStorage.NoOpScopesLifecycleToken.getInstance(); - } - - @Override - @ApiStatus.Internal - public @NotNull IScope getScope() { - return Sentry.getCurrentScopes().getScope(); - } - - @Override - @ApiStatus.Internal - public @NotNull IScope getIsolationScope() { - return Sentry.getCurrentScopes().getIsolationScope(); - } - - @Override - @ApiStatus.Internal - public @NotNull IScope getGlobalScope() { - return Sentry.getGlobalScope(); - } - - @ApiStatus.Internal - @Override - public @NotNull SentryId captureTransaction( - final @NotNull SentryTransaction transaction, - final @Nullable TraceContext traceContext, - final @Nullable Hint hint, - final @Nullable ProfilingTraceData profilingTraceData) { - Objects.requireNonNull(transaction, "transaction is required"); - - SentryId sentryId = SentryId.EMPTY_ID; - if (!isEnabled()) { - options - .getLogger() - .log( - SentryLevel.WARNING, - "Instance is disabled and this 'captureTransaction' call is a no-op."); - } else { - if (!transaction.isFinished()) { - options - .getLogger() - .log( - SentryLevel.WARNING, - "Transaction: %s is not finished and this 'captureTransaction' call is a no-op.", - transaction.getEventId()); - } else { - if (!Boolean.TRUE.equals(transaction.isSampled())) { - options - .getLogger() - .log( - SentryLevel.DEBUG, - "Transaction %s was dropped due to sampling decision.", - transaction.getEventId()); - if (options.getBackpressureMonitor().getDownsampleFactor() > 0) { - options - .getClientReportRecorder() - .recordLostEvent(DiscardReason.BACKPRESSURE, DataCategory.Transaction); - } else { - options - .getClientReportRecorder() - .recordLostEvent(DiscardReason.SAMPLE_RATE, DataCategory.Transaction); - } - } else { - StackItem item = null; - try { - item = stack.peek(); - sentryId = - item.getClient() - .captureTransaction( - transaction, traceContext, item.getScope(), hint, profilingTraceData); - } catch (Throwable e) { - options - .getLogger() - .log( - SentryLevel.ERROR, - "Error while capturing transaction with id: " + transaction.getEventId(), - e); - } - } - } - } - return sentryId; - } - - @Override - public @NotNull ITransaction startTransaction( - final @NotNull TransactionContext transactionContext, - final @NotNull TransactionOptions transactionOptions) { - return createTransaction(transactionContext, transactionOptions); - } - - private @NotNull ITransaction createTransaction( - final @NotNull TransactionContext transactionContext, - final @NotNull TransactionOptions transactionOptions) { - Objects.requireNonNull(transactionContext, "transactionContext is required"); - - ITransaction transaction; - if (!isEnabled()) { - options - .getLogger() - .log( - SentryLevel.WARNING, - "Instance is disabled and this 'startTransaction' returns a no-op."); - transaction = NoOpTransaction.getInstance(); - } else if (!options.getInstrumenter().equals(transactionContext.getInstrumenter())) { - options - .getLogger() - .log( - SentryLevel.DEBUG, - "Returning no-op for instrumenter %s as the SDK has been configured to use instrumenter %s", - transactionContext.getInstrumenter(), - options.getInstrumenter()); - transaction = NoOpTransaction.getInstance(); - } else if (!options.isTracingEnabled()) { - options - .getLogger() - .log( - SentryLevel.INFO, "Tracing is disabled and this 'startTransaction' returns a no-op."); - transaction = NoOpTransaction.getInstance(); - } else { - final SamplingContext samplingContext = - new SamplingContext(transactionContext, transactionOptions.getCustomSamplingContext()); - @NotNull TracesSamplingDecision samplingDecision = tracesSampler.sample(samplingContext); - transactionContext.setSamplingDecision(samplingDecision); - - transaction = - new SentryTracer( - transactionContext, this, transactionOptions, transactionPerformanceCollector); - - // The listener is called only if the transaction exists, as the transaction is needed to - // stop it - if (samplingDecision.getSampled() && samplingDecision.getProfileSampled()) { - final ITransactionProfiler transactionProfiler = options.getTransactionProfiler(); - // If the profiler is not running, we start and bind it here. - if (!transactionProfiler.isRunning()) { - transactionProfiler.start(); - transactionProfiler.bindTransaction(transaction); - } else if (transactionOptions.isAppStartTransaction()) { - // If the profiler is running and the current transaction is the app start, we bind it. - transactionProfiler.bindTransaction(transaction); - } - } - } - if (transactionOptions.isBindToScope()) { - configureScope(scope -> scope.setTransaction(transaction)); - } - return transaction; - } - - @Deprecated - @SuppressWarnings("InlineMeSuggester") - @Override - public @Nullable SentryTraceHeader traceHeaders() { - return getTraceparent(); - } - - @Override - public @Nullable ISpan getSpan() { - ISpan span = null; - if (!isEnabled()) { - options - .getLogger() - .log(SentryLevel.WARNING, "Instance is disabled and this 'getSpan' call is a no-op."); - } else { - span = stack.peek().getScope().getSpan(); - } - return span; - } - - @Override - @ApiStatus.Internal - public @Nullable ITransaction getTransaction() { - ITransaction span = null; - if (!isEnabled()) { - options - .getLogger() - .log( - SentryLevel.WARNING, - "Instance is disabled and this 'getTransaction' call is a no-op."); - } else { - span = stack.peek().getScope().getTransaction(); - } - return span; - } - - @Override - @ApiStatus.Internal - public void setSpanContext( - final @NotNull Throwable throwable, - final @NotNull ISpan span, - final @NotNull String transactionName) { - Objects.requireNonNull(throwable, "throwable is required"); - Objects.requireNonNull(span, "span is required"); - Objects.requireNonNull(transactionName, "transactionName is required"); - // to match any cause, span context is always attached to the root cause of the exception - final Throwable rootCause = ExceptionUtils.findRootCause(throwable); - // the most inner span should be assigned to a throwable - if (!throwableToSpan.containsKey(rootCause)) { - throwableToSpan.put(rootCause, new Pair<>(new WeakReference<>(span), transactionName)); - } - } - - @Nullable - SpanContext getSpanContext(final @NotNull Throwable throwable) { - Objects.requireNonNull(throwable, "throwable is required"); - final Throwable rootCause = ExceptionUtils.findRootCause(throwable); - final Pair, String> pair = this.throwableToSpan.get(rootCause); - if (pair != null) { - final WeakReference spanWeakRef = pair.getFirst(); - if (spanWeakRef != null) { - final ISpan span = spanWeakRef.get(); - if (span != null) { - return span.getSpanContext(); - } - } - } - return null; - } - - private IScope buildLocalScope( - final @NotNull IScope scope, final @Nullable ScopeCallback callback) { - if (callback != null) { - try { - final IScope localScope = scope.clone(); - callback.run(localScope); - return localScope; - } catch (Throwable t) { - options.getLogger().log(SentryLevel.ERROR, "Error in the 'ScopeCallback' callback.", t); - } - } - return scope; - } - - @Override - public @Nullable TransactionContext continueTrace( - final @Nullable String sentryTrace, final @Nullable List baggageHeaders) { - @NotNull - PropagationContext propagationContext = - PropagationContext.fromHeaders(getOptions().getLogger(), sentryTrace, baggageHeaders); - configureScope( - (scope) -> { - scope.setPropagationContext(propagationContext); - }); - if (options.isTracingEnabled()) { - return TransactionContext.fromPropagationContext(propagationContext); - } else { - return null; - } - } - - @Override - public @Nullable SentryTraceHeader getTraceparent() { - if (!isEnabled()) { - options - .getLogger() - .log( - SentryLevel.WARNING, - "Instance is disabled and this 'getTraceparent' call is a no-op."); - } else { - final @Nullable TracingUtils.TracingHeaders headers = - TracingUtils.trace(this, null, getSpan()); - if (headers != null) { - return headers.getSentryTraceHeader(); - } - } - - return null; - } - - @Override - public @Nullable BaggageHeader getBaggage() { - if (!isEnabled()) { - options - .getLogger() - .log(SentryLevel.WARNING, "Instance is disabled and this 'getBaggage' call is a no-op."); - } else { - final @Nullable TracingUtils.TracingHeaders headers = - TracingUtils.trace(this, null, getSpan()); - if (headers != null) { - return headers.getBaggageHeader(); - } - } - - return null; - } - - @Override - @ApiStatus.Experimental - public @NotNull SentryId captureCheckIn(final @NotNull CheckIn checkIn) { - SentryId sentryId = SentryId.EMPTY_ID; - if (!isEnabled()) { - options - .getLogger() - .log( - SentryLevel.WARNING, - "Instance is disabled and this 'captureCheckIn' call is a no-op."); - } else { - try { - StackItem item = stack.peek(); - sentryId = item.getClient().captureCheckIn(checkIn, item.getScope(), null); - } catch (Throwable e) { - options.getLogger().log(SentryLevel.ERROR, "Error while capturing check-in for slug", e); - } - } - this.lastEventId = sentryId; - return sentryId; - } - - @ApiStatus.Internal - @Override - public @Nullable RateLimiter getRateLimiter() { - final StackItem item = stack.peek(); - return item.getClient().getRateLimiter(); - } - - @Override - public @NotNull MetricsApi metrics() { - return metricsApi; - } - - @Override - public @NotNull IMetricsAggregator getMetricsAggregator() { - return stack.peek().getClient().getMetricsAggregator(); - } - - @Override - public @NotNull Map getDefaultTagsForMetrics() { - if (!options.isEnableDefaultTagsForMetrics()) { - return Collections.emptyMap(); - } - - final @NotNull Map tags = new HashMap<>(); - final @Nullable String release = options.getRelease(); - if (release != null) { - tags.put("release", release); - } - - final @Nullable String environment = options.getEnvironment(); - if (environment != null) { - tags.put("environment", environment); - } - - final @Nullable String txnName = stack.peek().getScope().getTransactionName(); - if (txnName != null) { - tags.put("transaction", txnName); - } - return Collections.unmodifiableMap(tags); - } - - @Override - public @Nullable ISpan startSpanForMetric(@NotNull String op, @NotNull String description) { - final @Nullable ISpan span = getSpan(); - if (span != null) { - return span.startChild(op, description); - } - return null; - } - - @Override - public @Nullable LocalMetricsAggregator getLocalMetricsAggregator() { - if (!options.isEnableSpanLocalMetricAggregation()) { - return null; - } - final @Nullable ISpan span = getSpan(); - if (span != null) { - return span.getLocalMetricsAggregator(); - } - return null; - } -} diff --git a/sentry/src/main/java/io/sentry/Scope.java b/sentry/src/main/java/io/sentry/Scope.java index 3df40a2b01..0d42326e13 100644 --- a/sentry/src/main/java/io/sentry/Scope.java +++ b/sentry/src/main/java/io/sentry/Scope.java @@ -116,7 +116,6 @@ private Scope(final @NotNull Scope scope) { this.options = scope.options; this.level = scope.level; this.client = scope.client; - // TODO [HSM] should we do this? didn't do it for Hub this.lastEventId = scope.getLastEventId(); final User userRef = scope.user; @@ -838,7 +837,7 @@ public SessionPair startSession() { SessionPair pair = null; synchronized (sessionLock) { if (session != null) { - // Assumes session will NOT flush itself (Not passing any hub to it) + // Assumes session will NOT flush itself (Not passing any scopes to it) session.end(); } previousSession = session; diff --git a/sentry/src/test/java/io/sentry/HubTest.kt b/sentry/src/test/java/io/sentry/HubTest.kt deleted file mode 100644 index f30fd0a966..0000000000 --- a/sentry/src/test/java/io/sentry/HubTest.kt +++ /dev/null @@ -1,2149 +0,0 @@ -package io.sentry - -import io.sentry.backpressure.IBackpressureMonitor -import io.sentry.cache.EnvelopeCache -import io.sentry.clientreport.ClientReportTestHelper.Companion.assertClientReport -import io.sentry.clientreport.DiscardReason -import io.sentry.clientreport.DiscardedEvent -import io.sentry.hints.SessionEndHint -import io.sentry.hints.SessionStartHint -import io.sentry.protocol.SentryId -import io.sentry.protocol.SentryTransaction -import io.sentry.protocol.User -import io.sentry.test.DeferredExecutorService -import io.sentry.test.callMethod -import io.sentry.util.HintUtils -import io.sentry.util.StringUtils -import org.mockito.kotlin.any -import org.mockito.kotlin.anyOrNull -import org.mockito.kotlin.argWhere -import org.mockito.kotlin.argumentCaptor -import org.mockito.kotlin.check -import org.mockito.kotlin.doAnswer -import org.mockito.kotlin.doThrow -import org.mockito.kotlin.eq -import org.mockito.kotlin.mock -import org.mockito.kotlin.never -import org.mockito.kotlin.spy -import org.mockito.kotlin.times -import org.mockito.kotlin.verify -import org.mockito.kotlin.verifyNoMoreInteractions -import org.mockito.kotlin.whenever -import java.io.File -import java.nio.file.Files -import java.util.Queue -import java.util.UUID -import java.util.concurrent.atomic.AtomicReference -import kotlin.test.AfterTest -import kotlin.test.BeforeTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertFalse -import kotlin.test.assertNotEquals -import kotlin.test.assertNotNull -import kotlin.test.assertNull -import kotlin.test.assertTrue -import kotlin.test.fail - -class HubTest { - - private lateinit var file: File - private lateinit var profilingTraceFile: File - - @BeforeTest - fun `set up`() { - file = Files.createTempDirectory("sentry-disk-cache-test").toAbsolutePath().toFile() - profilingTraceFile = Files.createTempFile("trace", ".trace").toFile() - profilingTraceFile.writeText("sampledProfile") - } - - @AfterTest - fun shutdown() { - file.deleteRecursively() - profilingTraceFile.delete() - Sentry.close() - } - - @Test - fun `when no dsn available, ctor throws illegal arg`() { - val ex = assertFailsWith { Hub(SentryOptions()) } - assertEquals("Hub requires a DSN to be instantiated. Considering using the NoOpHub if no DSN is available.", ex.message) - } - - @Test - fun `when scopes is cloned, integrations are not registered`() { - val integrationMock = mock() - val options = SentryOptions() - options.cacheDirPath = file.absolutePath - options.dsn = "https://key@sentry.io/proj" - options.setSerializer(mock()) - options.addIntegration(integrationMock) -// val expected = HubAdapter.getInstance() - val scopes = Hub(options) -// verify(integrationMock).register(expected, options) - scopes.clone() - verifyNoMoreInteractions(integrationMock) - } - - @Test - fun `when scopes is cloned, scope changes are isolated`() { - val options = SentryOptions() - options.cacheDirPath = file.absolutePath - options.dsn = "https://key@sentry.io/proj" - options.setSerializer(mock()) - val scopes = Hub(options) - var firstScope: IScope? = null - scopes.configureScope { - firstScope = it - it.setTag("scopes", "a") - } - var cloneScope: IScope? = null - val clone = scopes.clone() - clone.configureScope { - cloneScope = it - it.setTag("scopes", "b") - } - assertEquals("a", firstScope!!.tags["scopes"]) - assertEquals("b", cloneScope!!.tags["scopes"]) - } - - @Test - fun `when scopes is initialized, breadcrumbs are capped as per options`() { - val options = SentryOptions() - options.cacheDirPath = file.absolutePath - options.maxBreadcrumbs = 5 - options.dsn = "https://key@sentry.io/proj" - options.setSerializer(mock()) - val sut = Hub(options) - (1..10).forEach { _ -> sut.addBreadcrumb(Breadcrumb(), null) } - var actual = 0 - sut.configureScope { - actual = it.breadcrumbs.size - } - assertEquals(options.maxBreadcrumbs, actual) - } - - @Test - fun `when beforeBreadcrumb returns null, crumb is dropped`() { - val options = SentryOptions() - options.cacheDirPath = file.absolutePath - options.beforeBreadcrumb = SentryOptions.BeforeBreadcrumbCallback { _: Breadcrumb, _: Any? -> null } - options.dsn = "https://key@sentry.io/proj" - options.setSerializer(mock()) - val sut = Hub(options) - sut.addBreadcrumb(Breadcrumb(), null) - var breadcrumbs: Queue? = null - sut.configureScope { breadcrumbs = it.breadcrumbs } - assertEquals(0, breadcrumbs!!.size) - } - - @Test - fun `when beforeBreadcrumb modifies crumb, crumb is stored modified`() { - val options = SentryOptions() - options.cacheDirPath = file.absolutePath - val expected = "expected" - options.beforeBreadcrumb = SentryOptions.BeforeBreadcrumbCallback { breadcrumb: Breadcrumb, _: Any? -> breadcrumb.message = expected; breadcrumb; } - options.dsn = "https://key@sentry.io/proj" - options.setSerializer(mock()) - val sut = Hub(options) - val crumb = Breadcrumb() - crumb.message = "original" - sut.addBreadcrumb(crumb) - var breadcrumbs: Queue? = null - sut.configureScope { breadcrumbs = it.breadcrumbs } - assertEquals(expected, breadcrumbs!!.first().message) - } - - @Test - fun `when beforeBreadcrumb is null, crumb is stored`() { - val options = SentryOptions() - options.cacheDirPath = file.absolutePath - options.beforeBreadcrumb = null - options.dsn = "https://key@sentry.io/proj" - options.setSerializer(mock()) - val sut = Hub(options) - val expected = Breadcrumb() - sut.addBreadcrumb(expected) - var breadcrumbs: Queue? = null - sut.configureScope { breadcrumbs = it.breadcrumbs } - assertEquals(expected, breadcrumbs!!.single()) - } - - @Test - fun `when beforeSend throws an exception, breadcrumb adds an entry to the data field with exception message`() { - val exception = Exception("test") - - val options = SentryOptions() - options.cacheDirPath = file.absolutePath - options.beforeBreadcrumb = SentryOptions.BeforeBreadcrumbCallback { _: Breadcrumb, _: Any? -> throw exception } - options.dsn = "https://key@sentry.io/proj" - options.setSerializer(mock()) - val sut = Hub(options) - - val actual = Breadcrumb() - sut.addBreadcrumb(actual) - - assertEquals("test", actual.data["sentry:message"]) - } - - @Test - fun `when initialized, lastEventId is empty`() { - val options = SentryOptions() - options.cacheDirPath = file.absolutePath - options.dsn = "https://key@sentry.io/proj" - options.setSerializer(mock()) - val sut = Hub(options) - assertEquals(SentryId.EMPTY_ID, sut.lastEventId) - } - - @Test - fun `when addBreadcrumb is called on disabled client, no-op`() { - val options = SentryOptions() - options.cacheDirPath = file.absolutePath - options.dsn = "https://key@sentry.io/proj" - options.setSerializer(mock()) - val sut = Hub(options) - var breadcrumbs: Queue? = null - sut.configureScope { breadcrumbs = it.breadcrumbs } - sut.close() - sut.addBreadcrumb(Breadcrumb()) - assertTrue(breadcrumbs!!.isEmpty()) - } - - @Test - fun `when addBreadcrumb is called with message and category, breadcrumb object has values`() { - val options = SentryOptions() - options.cacheDirPath = file.absolutePath - options.dsn = "https://key@sentry.io/proj" - options.setSerializer(mock()) - val sut = Hub(options) - var breadcrumbs: Queue? = null - sut.configureScope { breadcrumbs = it.breadcrumbs } - sut.addBreadcrumb("message", "category") - assertEquals("message", breadcrumbs!!.single().message) - assertEquals("category", breadcrumbs!!.single().category) - } - - @Test - fun `when addBreadcrumb is called with message, breadcrumb object has value`() { - val options = SentryOptions() - options.cacheDirPath = file.absolutePath - options.dsn = "https://key@sentry.io/proj" - options.setSerializer(mock()) - val sut = Hub(options) - var breadcrumbs: Queue? = null - sut.configureScope { breadcrumbs = it.breadcrumbs } - sut.addBreadcrumb("message", "category") - assertEquals("message", breadcrumbs!!.single().message) - assertEquals("category", breadcrumbs!!.single().category) - } - - @Test - fun `when flush is called on disabled client, no-op`() { - val (sut, mockClient) = getEnabledHub() - sut.close() - - sut.flush(1000) - verify(mockClient, never()).flush(1000) - } - - @Test - fun `when flush is called, client flush gets called`() { - val (sut, mockClient) = getEnabledHub() - - sut.flush(1000) - verify(mockClient).flush(1000) - } - - //region captureEvent tests - @Test - fun `when captureEvent is called and event is null, lastEventId is empty`() { - val options = SentryOptions() - options.cacheDirPath = file.absolutePath - options.dsn = "https://key@sentry.io/proj" - options.setSerializer(mock()) - val sut = Hub(options) - sut.callMethod("captureEvent", SentryEvent::class.java, null) - assertEquals(SentryId.EMPTY_ID, sut.lastEventId) - } - - @Test - fun `when captureEvent is called on disabled client, do nothing`() { - val (sut, mockClient) = getEnabledHub() - sut.close() - - sut.captureEvent(SentryEvent()) - verify(mockClient, never()).captureEvent(any(), any()) - } - - @Test - fun `when captureEvent is called with a valid argument, captureEvent on the client should be called`() { - val (sut, mockClient) = getEnabledHub() - - val event = SentryEvent() - val hints = HintUtils.createWithTypeCheckHint({}) - sut.captureEvent(event, hints) - verify(mockClient).captureEvent(eq(event), any(), eq(hints)) - } - - @Test - fun `when captureEvent is called on disabled scopes, lastEventId does not get overwritten`() { - val (sut, mockClient) = getEnabledHub() - whenever(mockClient.captureEvent(any(), any(), anyOrNull())).thenReturn(SentryId(UUID.randomUUID())) - val event = SentryEvent() - val hints = HintUtils.createWithTypeCheckHint({}) - sut.captureEvent(event, hints) - val lastEventId = sut.lastEventId - sut.close() - sut.captureEvent(event, hints) - assertEquals(lastEventId, sut.lastEventId) - } - - @Test - fun `when captureEvent is called and session tracking is disabled, it should not capture a session`() { - val (sut, mockClient) = getEnabledHub() - - val event = SentryEvent() - val hints = HintUtils.createWithTypeCheckHint({}) - sut.captureEvent(event, hints) - verify(mockClient).captureEvent(eq(event), any(), eq(hints)) - verify(mockClient, never()).captureSession(any(), any()) - } - - @Test - fun `when captureEvent is called but no session started, it should not capture a session`() { - val (sut, mockClient) = getEnabledHub() - - val event = SentryEvent() - val hints = HintUtils.createWithTypeCheckHint({}) - sut.captureEvent(event, hints) - verify(mockClient).captureEvent(eq(event), any(), eq(hints)) - verify(mockClient, never()).captureSession(any(), any()) - } - - @Test - fun `when captureEvent is called and event has exception which has been previously attached with span context, sets span context to the event`() { - val (sut, mockClient) = getEnabledHub() - val exception = RuntimeException() - val span = mock() - whenever(span.spanContext).thenReturn(SpanContext("op")) - sut.setSpanContext(exception, span, "tx-name") - - val event = SentryEvent(exception) - - val hints = HintUtils.createWithTypeCheckHint({}) - sut.captureEvent(event, hints) - assertEquals(span.spanContext, event.contexts.trace) - verify(mockClient).captureEvent(eq(event), any(), eq(hints)) - } - - @Test - fun `when captureEvent is called and event has exception which root cause has been previously attached with span context, sets span context to the event`() { - val (sut, mockClient) = getEnabledHub() - val rootCause = RuntimeException() - val span = mock() - whenever(span.spanContext).thenReturn(SpanContext("op")) - sut.setSpanContext(rootCause, span, "tx-name") - - val event = SentryEvent(RuntimeException(rootCause)) - - val hints = HintUtils.createWithTypeCheckHint({}) - sut.captureEvent(event, hints) - assertEquals(span.spanContext, event.contexts.trace) - verify(mockClient).captureEvent(eq(event), any(), eq(hints)) - } - - @Test - fun `when captureEvent is called and event has exception which non-root cause has been previously attached with span context, sets span context to the event`() { - val (sut, mockClient) = getEnabledHub() - val rootCause = RuntimeException() - val exceptionAssignedToSpan = RuntimeException(rootCause) - val span = mock() - whenever(span.spanContext).thenReturn(SpanContext("op")) - sut.setSpanContext(exceptionAssignedToSpan, span, "tx-name") - - val event = SentryEvent(RuntimeException(exceptionAssignedToSpan)) - - val hints = HintUtils.createWithTypeCheckHint({}) - sut.captureEvent(event, hints) - assertEquals(span.spanContext, event.contexts.trace) - verify(mockClient).captureEvent(eq(event), any(), eq(hints)) - } - - @Test - fun `when captureEvent is called and event has exception which has been previously attached with span context and trace context already set, does not set new span context to the event`() { - val (sut, mockClient) = getEnabledHub() - val exception = RuntimeException() - val span = mock() - whenever(span.spanContext).thenReturn(SpanContext("op")) - sut.setSpanContext(exception, span, "tx-name") - - val event = SentryEvent(exception) - val originalSpanContext = SpanContext("op") - event.contexts.trace = originalSpanContext - - val hints = HintUtils.createWithTypeCheckHint({}) - sut.captureEvent(event, hints) - assertEquals(originalSpanContext, event.contexts.trace) - verify(mockClient).captureEvent(eq(event), any(), eq(hints)) - } - - @Test - fun `when captureEvent is called and event has exception which has not been previously attached with span context, does not set new span context to the event`() { - val (sut, mockClient) = getEnabledHub() - - val event = SentryEvent(RuntimeException()) - - val hints = HintUtils.createWithTypeCheckHint({}) - sut.captureEvent(event, hints) - assertNull(event.contexts.trace) - verify(mockClient).captureEvent(eq(event), any(), eq(hints)) - } - - @Test - fun `when captureEvent is called with a ScopeCallback then the modified scope is sent to the client`() { - val (sut, mockClient) = getEnabledHub() - - sut.captureEvent(SentryEvent(), null) { - it.setTag("test", "testValue") - } - - verify(mockClient).captureEvent( - any(), - check { - assertEquals("testValue", it.tags["test"]) - }, - anyOrNull() - ) - } - - @Test - fun `when captureEvent is called with a ScopeCallback then subsequent calls to captureEvent send the unmodified Scope to the client`() { - val (sut, mockClient) = getEnabledHub() - val argumentCaptor = argumentCaptor() - - sut.captureEvent(SentryEvent(), null) { - it.setTag("test", "testValue") - } - - sut.captureEvent(SentryEvent()) - - verify(mockClient, times(2)).captureEvent( - any(), - argumentCaptor.capture(), - anyOrNull() - ) - - assertEquals("testValue", argumentCaptor.allValues[0].tags["test"]) - assertNull(argumentCaptor.allValues[1].tags["test"]) - } - - @Test - fun `when captureEvent is called with a ScopeCallback that crashes then the event should still be captured`() { - val (sut, mockClient, logger) = getEnabledHub() - - val exception = Exception("scope callback exception") - sut.captureEvent(SentryEvent(), null) { - throw exception - } - - verify(mockClient).captureEvent( - any(), - anyOrNull(), - anyOrNull() - ) - - verify(logger).log(eq(SentryLevel.ERROR), any(), eq(exception)) - } - //endregion - - //region captureMessage tests - @Test - fun `when captureMessage is called and event is null, lastEventId is empty`() { - val options = SentryOptions() - options.cacheDirPath = file.absolutePath - options.dsn = "https://key@sentry.io/proj" - options.setSerializer(mock()) - val sut = Hub(options) - sut.callMethod("captureMessage", String::class.java, null) - assertEquals(SentryId.EMPTY_ID, sut.lastEventId) - } - - @Test - fun `when captureMessage is called on disabled client, do nothing`() { - val (sut, mockClient) = getEnabledHub() - sut.close() - - sut.captureMessage("test") - verify(mockClient, never()).captureMessage(any(), any()) - } - - @Test - fun `when captureMessage is called with a valid message, captureMessage on the client should be called`() { - val (sut, mockClient) = getEnabledHub() - - sut.captureMessage("test") - verify(mockClient).captureMessage(any(), any(), any()) - } - - @Test - fun `when captureMessage is called, level is INFO by default`() { - val (sut, mockClient) = getEnabledHub() - sut.captureMessage("test") - verify(mockClient).captureMessage(eq("test"), eq(SentryLevel.INFO), any()) - } - - @Test - fun `when captureMessage is called with a ScopeCallback then the modified scope is sent to the client`() { - val (sut, mockClient) = getEnabledHub() - - sut.captureMessage("test") { - it.setTag("test", "testValue") - } - - verify(mockClient).captureMessage( - any(), - any(), - check { - assertEquals("testValue", it.tags["test"]) - } - ) - } - - @Test - fun `when captureMessage is called with a ScopeCallback then subsequent calls to captureMessage send the unmodified Scope to the client`() { - val (sut, mockClient) = getEnabledHub() - val argumentCaptor = argumentCaptor() - - sut.captureMessage("testMessage") { - it.setTag("test", "testValue") - } - - sut.captureMessage("test", SentryLevel.INFO) - - verify(mockClient, times(2)).captureMessage( - any(), - any(), - argumentCaptor.capture() - ) - - assertEquals("testValue", argumentCaptor.allValues[0].tags["test"]) - assertNull(argumentCaptor.allValues[1].tags["test"]) - } - - @Test - fun `when captureMessage is called with a ScopeCallback that crashes then the message should still be captured`() { - val (sut, mockClient, logger) = getEnabledHub() - - val exception = Exception("scope callback exception") - sut.captureMessage("Hello World") { - throw exception - } - - verify(mockClient).captureMessage( - any(), - anyOrNull(), - anyOrNull() - ) - - verify(logger).log(eq(SentryLevel.ERROR), any(), eq(exception)) - } - - //endregion - - //region captureException tests - @Test - fun `when captureException is called and exception is null, lastEventId is empty`() { - val options = SentryOptions() - options.cacheDirPath = file.absolutePath - options.dsn = "https://key@sentry.io/proj" - options.setSerializer(mock()) - val sut = Hub(options) - sut.callMethod("captureException", Throwable::class.java, null) - assertEquals(SentryId.EMPTY_ID, sut.lastEventId) - } - - @Test - fun `when captureException is called on disabled client, do nothing`() { - val (sut, mockClient) = getEnabledHub() - sut.close() - - sut.captureException(Throwable()) - verify(mockClient, never()).captureEvent(any(), any(), any()) - } - - @Test - fun `when captureException is called with a valid argument and hint, captureEvent on the client should be called`() { - val (sut, mockClient) = getEnabledHub() - - val hints = HintUtils.createWithTypeCheckHint({}) - sut.captureException(Throwable(), hints) - verify(mockClient).captureEvent(any(), any(), any()) - } - - @Test - fun `when captureException is called with a valid argument but no hint, captureEvent on the client should be called`() { - val (sut, mockClient) = getEnabledHub() - - sut.captureException(Throwable()) - verify(mockClient).captureEvent(any(), any(), any()) - } - - @Test - fun `when captureException is called with an exception which has been previously attached with span context, span context should be set on the event before capturing`() { - val (sut, mockClient) = getEnabledHub() - val throwable = Throwable() - val span = mock() - whenever(span.spanContext).thenReturn(SpanContext("op")) - sut.setSpanContext(throwable, span, "tx-name") - - sut.captureException(throwable) - verify(mockClient).captureEvent( - check { - assertEquals(span.spanContext, it.contexts.trace) - assertEquals("tx-name", it.transaction) - }, - any(), - anyOrNull() - ) - } - - @Test - fun `when captureException is called with an exception which has not been previously attached with span context, span context should not be set on the event before capturing`() { - val (sut, mockClient) = getEnabledHub() - val span = mock() - whenever(span.spanContext).thenReturn(SpanContext("op")) - sut.setSpanContext(Throwable(), span, "tx-name") - - sut.captureException(Throwable()) - verify(mockClient).captureEvent( - check { - assertNull(it.contexts.trace) - }, - any(), - anyOrNull() - ) - } - - @Test - fun `when captureException is called with a ScopeCallback then the modified scope is sent to the client`() { - val (sut, mockClient) = getEnabledHub() - - sut.captureException(Throwable(), null) { - it.setTag("test", "testValue") - } - - verify(mockClient).captureEvent( - any(), - check { - assertEquals("testValue", it.tags["test"]) - }, - anyOrNull() - ) - } - - @Test - fun `when captureException is called with a ScopeCallback then subsequent calls to captureException send the unmodified Scope to the client`() { - val (sut, mockClient) = getEnabledHub() - val argumentCaptor = argumentCaptor() - - sut.captureException(Throwable(), null) { - it.setTag("test", "testValue") - } - - sut.captureException(Throwable()) - - verify(mockClient, times(2)).captureEvent( - any(), - argumentCaptor.capture(), - anyOrNull() - ) - - assertEquals("testValue", argumentCaptor.allValues[0].tags["test"]) - assertNull(argumentCaptor.allValues[1].tags["test"]) - } - - @Test - fun `when captureException is called with a ScopeCallback that crashes then the exception should still be captured`() { - val (sut, mockClient, logger) = getEnabledHub() - - val exception = Exception("scope callback exception") - sut.captureException(Throwable()) { - throw exception - } - - verify(mockClient).captureEvent( - any(), - anyOrNull(), - anyOrNull() - ) - - verify(logger).log(eq(SentryLevel.ERROR), any(), eq(exception)) - } - - //endregion - - //region captureUserFeedback tests - - @Test - fun `when captureUserFeedback is called it is forwarded to the client`() { - val (sut, mockClient) = getEnabledHub() - sut.captureUserFeedback(userFeedback) - - verify(mockClient).captureUserFeedback( - check { - assertEquals(userFeedback.eventId, it.eventId) - assertEquals(userFeedback.email, it.email) - assertEquals(userFeedback.name, it.name) - assertEquals(userFeedback.comments, it.comments) - } - ) - } - - @Test - fun `when captureUserFeedback is called on disabled client, do nothing`() { - val (sut, mockClient) = getEnabledHub() - sut.close() - - sut.captureUserFeedback(userFeedback) - verify(mockClient, never()).captureUserFeedback(any()) - } - - @Test - fun `when captureUserFeedback is called and client throws, don't crash`() { - val (sut, mockClient) = getEnabledHub() - - whenever(mockClient.captureUserFeedback(any())).doThrow(IllegalArgumentException("")) - - sut.captureUserFeedback(userFeedback) - } - - private val userFeedback: UserFeedback get() { - val eventId = SentryId("c2fb8fee2e2b49758bcb67cda0f713c7") - return UserFeedback(eventId).apply { - name = "John" - email = "john@me.com" - comments = "comment" - } - } - - //region captureCheckIn tests - - @Test - fun `when captureCheckIn is called it is forwarded to the client`() { - val (sut, mockClient) = getEnabledHub() - sut.captureCheckIn(checkIn) - - verify(mockClient).captureCheckIn( - check { - assertEquals(checkIn.checkInId, it.checkInId) - assertEquals(checkIn.monitorSlug, it.monitorSlug) - assertEquals(checkIn.status, it.status) - }, - any(), - anyOrNull() - ) - } - - @Test - fun `when captureCheckIn is called on disabled client, do nothing`() { - val (sut, mockClient) = getEnabledHub() - sut.close() - - sut.captureCheckIn(checkIn) - verify(mockClient, never()).captureCheckIn(any(), any(), anyOrNull()) - } - - @Test - fun `when captureCheckIn is called and client throws, don't crash`() { - val (sut, mockClient) = getEnabledHub() - - whenever(mockClient.captureCheckIn(any(), any(), anyOrNull())).doThrow(IllegalArgumentException("")) - - sut.captureCheckIn(checkIn) - } - - private val checkIn: CheckIn = CheckIn("some_slug", CheckInStatus.OK) - - //endregion - - //region close tests - @Test - fun `when close is called on disabled client, do nothing`() { - val (sut, mockClient) = getEnabledHub() - sut.close() - - sut.close() - verify(mockClient).close(eq(false)) // 1 to close, but next one wont be recorded - } - - @Test - fun `when close is called and client is alive, close on the client should be called`() { - val (sut, mockClient) = getEnabledHub() - - sut.close() - verify(mockClient).close(eq(false)) - } - - @Test - fun `when close is called with isRestarting false and client is alive, close on the client should be called with isRestarting false`() { - val (sut, mockClient) = getEnabledHub() - - sut.close(false) - verify(mockClient).close(eq(false)) - } - - @Test - fun `when close is called with isRestarting true and client is alive, close on the client should be called with isRestarting true`() { - val (sut, mockClient) = getEnabledHub() - - sut.close(true) - verify(mockClient).close(eq(true)) - } - //endregion - - //region withScope tests - @Test - fun `when withScope is called on disabled client, execute on NoOpScope`() { - val (sut) = getEnabledHub() - - val scopeCallback = mock() - sut.close() - - sut.withScope(scopeCallback) - verify(scopeCallback).run(NoOpScope.getInstance()) - } - - @Test - fun `when withScope is called with alive client, run should be called`() { - val (sut) = getEnabledHub() - - val scopeCallback = mock() - - sut.withScope(scopeCallback) - verify(scopeCallback).run(any()) - } - - @Test - fun `when withScope throws an exception then it should be caught`() { - val (scopes, _, logger) = getEnabledHub() - - val exception = Exception("scope callback exception") - val scopeCallback = ScopeCallback { - throw exception - } - - scopes.withScope(scopeCallback) - - verify(logger).log(eq(SentryLevel.ERROR), any(), eq(exception)) - } - //endregion - - //region configureScope tests - @Test - fun `when configureScope is called on disabled client, do nothing`() { - val (sut) = getEnabledHub() - - val scopeCallback = mock() - sut.close() - - sut.configureScope(scopeCallback) - verify(scopeCallback, never()).run(any()) - } - - @Test - fun `when configureScope is called with alive client, run should be called`() { - val (sut) = getEnabledHub() - - val scopeCallback = mock() - - sut.configureScope(scopeCallback) - verify(scopeCallback).run(any()) - } - - @Test - fun `when configureScope throws an exception then it should be caught`() { - val (scopes, _, logger) = getEnabledHub() - - val exception = Exception("scope callback exception") - val scopeCallback = ScopeCallback { - throw exception - } - - scopes.configureScope(scopeCallback) - - verify(logger).log(eq(SentryLevel.ERROR), any(), eq(exception)) - } - //endregion - - @Test - fun `when integration is registered, scopes is enabled`() { - val mock = mock() - - var options: SentryOptions? = null - // init main scopes and make it enabled - Sentry.init { - it.addIntegration(mock) - it.dsn = "https://key@sentry.io/proj" - it.cacheDirPath = file.absolutePath - it.setSerializer(mock()) - options = it - } - - doAnswer { - val scopes = it.arguments[0] as IScopes - assertTrue(scopes.isEnabled) - }.whenever(mock).register(any(), eq(options!!)) - - verify(mock).register(any(), eq(options!!)) - } - - //region setLevel tests - @Test - fun `when setLevel is called on disabled client, do nothing`() { - val scopes = generateHub() - var scope: IScope? = null - scopes.configureScope { - scope = it - } - scopes.close() - - scopes.setLevel(SentryLevel.INFO) - assertNull(scope?.level) - } - - @Test - fun `when setLevel is called, level is set`() { - val scopes = generateHub() - var scope: IScope? = null - scopes.configureScope { - scope = it - } - - scopes.setLevel(SentryLevel.INFO) - assertEquals(SentryLevel.INFO, scope?.level) - } - //endregion - - //region setTransaction tests - @Test - fun `when setTransaction is called on disabled client, do nothing`() { - val scopes = generateHub() - var scope: IScope? = null - scopes.configureScope { - scope = it - } - scopes.close() - - scopes.setTransaction("test") - assertNull(scope?.transactionName) - } - - @Test - fun `when setTransaction is called, and transaction is not set, transaction name is changed`() { - val scopes = generateHub() - var scope: IScope? = null - scopes.configureScope { - scope = it - } - - scopes.setTransaction("test") - assertEquals("test", scope?.transactionName) - } - - @Test - fun `when setTransaction is called, and transaction is set, transaction name is changed`() { - val scopes = generateHub() - var scope: IScope? = null - scopes.configureScope { - scope = it - } - - val tx = scopes.startTransaction("test", "op") - scopes.configureScope { it.setTransaction(tx) } - - assertEquals("test", scope?.transactionName) - } - - @Test - fun `when startTransaction is called with different instrumenter, no-op is returned`() { - val scopes = generateHub() - - val transactionContext = TransactionContext("test", "op").also { it.instrumenter = Instrumenter.OTEL } - val transactionOptions = TransactionOptions() - val tx = scopes.startTransaction(transactionContext, transactionOptions) - - assertTrue(tx is NoOpTransaction) - } - - @Test - fun `when startTransaction is called with different instrumenter, no-op is returned 2`() { - val scopes = generateHub() { - it.instrumenter = Instrumenter.OTEL - } - - val tx = scopes.startTransaction("test", "op") - - assertTrue(tx is NoOpTransaction) - } - - @Test - fun `when startTransaction is called with configured instrumenter, it works`() { - val scopes = generateHub() { - it.instrumenter = Instrumenter.OTEL - } - - val transactionContext = TransactionContext("test", "op").also { it.instrumenter = Instrumenter.OTEL } - val transactionOptions = TransactionOptions() - val tx = scopes.startTransaction(transactionContext, transactionOptions) - - assertFalse(tx is NoOpTransaction) - } - //endregion - - //region setUser tests - @Test - fun `when setUser is called on disabled client, do nothing`() { - val scopes = generateHub() - var scope: IScope? = null - scopes.configureScope { - scope = it - } - scopes.close() - - scopes.setUser(User()) - assertNull(scope?.user) - } - - @Test - fun `when setUser is called, user is set`() { - val scopes = generateHub() - var scope: IScope? = null - scopes.configureScope { - scope = it - } - - val user = User() - scopes.setUser(user) - assertEquals(user, scope?.user) - } - //endregion - - //region setFingerprint tests - @Test - fun `when setFingerprint is called on disabled client, do nothing`() { - val scopes = generateHub() - var scope: IScope? = null - scopes.configureScope { - scope = it - } - scopes.close() - - val fingerprint = listOf("abc") - scopes.setFingerprint(fingerprint) - assertEquals(0, scope?.fingerprint?.count()) - } - - @Test - fun `when setFingerprint is called with null parameter, do nothing`() { - val scopes = generateHub() - var scope: IScope? = null - scopes.configureScope { - scope = it - } - - scopes.callMethod("setFingerprint", List::class.java, null) - assertEquals(0, scope?.fingerprint?.count()) - } - - @Test - fun `when setFingerprint is called, fingerprint is set`() { - val scopes = generateHub() - var scope: IScope? = null - scopes.configureScope { - scope = it - } - - val fingerprint = listOf("abc") - scopes.setFingerprint(fingerprint) - assertEquals(1, scope?.fingerprint?.count()) - } - //endregion - - //region clearBreadcrumbs tests - @Test - fun `when clearBreadcrumbs is called on disabled client, do nothing`() { - val scopes = generateHub() - var scope: IScope? = null - scopes.configureScope { - scope = it - } - scopes.addBreadcrumb(Breadcrumb()) - assertEquals(1, scope?.breadcrumbs?.count()) - - scopes.close() - - assertEquals(0, scope?.breadcrumbs?.count()) - } - - @Test - fun `when clearBreadcrumbs is called, clear breadcrumbs`() { - val scopes = generateHub() - var scope: IScope? = null - scopes.configureScope { - scope = it - } - - scopes.addBreadcrumb(Breadcrumb()) - assertEquals(1, scope?.breadcrumbs?.count()) - scopes.clearBreadcrumbs() - assertEquals(0, scope?.breadcrumbs?.count()) - } - //endregion - - //region setTag tests - @Test - fun `when setTag is called on disabled client, do nothing`() { - val scopes = generateHub() - var scope: IScope? = null - scopes.configureScope { - scope = it - } - scopes.close() - - scopes.setTag("test", "test") - assertEquals(0, scope?.tags?.count()) - } - - @Test - fun `when setTag is called with null parameters, do nothing`() { - val scopes = generateHub() - var scope: IScope? = null - scopes.configureScope { - scope = it - } - - scopes.callMethod("setTag", parameterTypes = arrayOf(String::class.java, String::class.java), null, null) - assertEquals(0, scope?.tags?.count()) - } - - @Test - fun `when setTag is called, tag is set`() { - val scopes = generateHub() - var scope: IScope? = null - scopes.configureScope { - scope = it - } - - scopes.setTag("test", "test") - assertEquals(1, scope?.tags?.count()) - } - //endregion - - //region setExtra tests - @Test - fun `when setExtra is called on disabled client, do nothing`() { - val scopes = generateHub() - var scope: IScope? = null - scopes.configureScope { - scope = it - } - scopes.close() - - scopes.setExtra("test", "test") - assertEquals(0, scope?.extras?.count()) - } - - @Test - fun `when setExtra is called with null parameters, do nothing`() { - val scopes = generateHub() - var scope: IScope? = null - scopes.configureScope { - scope = it - } - - scopes.callMethod("setExtra", parameterTypes = arrayOf(String::class.java, String::class.java), null, null) - assertEquals(0, scope?.extras?.count()) - } - - @Test - fun `when setExtra is called, extra is set`() { - val scopes = generateHub() - var scope: IScope? = null - scopes.configureScope { - scope = it - } - - scopes.setExtra("test", "test") - assertEquals(1, scope?.extras?.count()) - } - //endregion - - //region captureEnvelope tests - @Test - fun `when captureEnvelope is called and envelope is null, throws IllegalArgumentException`() { - val options = SentryOptions() - options.cacheDirPath = file.absolutePath - options.dsn = "https://key@sentry.io/proj" - options.setSerializer(mock()) - val sut = Hub(options) - try { - sut.callMethod("captureEnvelope", SentryEnvelope::class.java, null) - fail() - } catch (e: Exception) { - assertTrue(e.cause is java.lang.IllegalArgumentException) - } - } - - @Test - fun `when captureEnvelope is called on disabled client, do nothing`() { - val options = SentryOptions() - options.cacheDirPath = file.absolutePath - options.dsn = "https://key@sentry.io/proj" - options.setSerializer(mock()) - val sut = Hub(options) - val mockClient = mock() - sut.bindClient(mockClient) - sut.close() - - sut.captureEnvelope(SentryEnvelope(SentryId(UUID.randomUUID()), null, setOf())) - verify(mockClient, never()).captureEnvelope(any(), any()) - } - - @Test - fun `when captureEnvelope is called with a valid envelope, captureEnvelope on the client should be called`() { - val options = SentryOptions() - options.cacheDirPath = file.absolutePath - options.dsn = "https://key@sentry.io/proj" - options.setSerializer(mock()) - val sut = Hub(options) - val mockClient = mock() - sut.bindClient(mockClient) - - val envelope = SentryEnvelope(SentryId(UUID.randomUUID()), null, setOf()) - sut.captureEnvelope(envelope) - verify(mockClient).captureEnvelope(any(), anyOrNull()) - } - - @Test - fun `when captureEnvelope is called, lastEventId is not set`() { - val options = SentryOptions().apply { - dsn = "https://key@sentry.io/proj" - setSerializer(mock()) - } - val sut = Hub(options) - val mockClient = mock() - sut.bindClient(mockClient) - whenever(mockClient.captureEnvelope(any(), anyOrNull())).thenReturn(SentryId()) - val envelope = SentryEnvelope(SentryId(UUID.randomUUID()), null, setOf()) - sut.captureEnvelope(envelope) - assertEquals(SentryId.EMPTY_ID, sut.lastEventId) - } - //endregion - - //region startSession tests - @Test - fun `when startSession is called on disabled client, do nothing`() { - val options = SentryOptions() - options.cacheDirPath = file.absolutePath - options.dsn = "https://key@sentry.io/proj" - options.release = "0.0.1" - options.setSerializer(mock()) - val sut = Hub(options) - val mockClient = mock() - sut.bindClient(mockClient) - sut.close() - - sut.startSession() - verify(mockClient, never()).captureSession(any(), any()) - } - - @Test - fun `when startSession is called, starts a session`() { - val options = SentryOptions() - options.cacheDirPath = file.absolutePath - options.dsn = "https://key@sentry.io/proj" - options.release = "0.0.1" - options.setSerializer(mock()) - val sut = Hub(options) - val mockClient = mock() - sut.bindClient(mockClient) - - sut.startSession() - verify(mockClient).captureSession(any(), argWhere { HintUtils.hasType(it, SessionStartHint::class.java) }) - } - - @Test - fun `when startSession is called and there's a session, stops it and starts a new one`() { - val options = SentryOptions() - options.cacheDirPath = file.absolutePath - options.dsn = "https://key@sentry.io/proj" - options.release = "0.0.1" - options.setSerializer(mock()) - val sut = Hub(options) - val mockClient = mock() - sut.bindClient(mockClient) - - sut.startSession() - sut.startSession() - verify(mockClient).captureSession(any(), argWhere { HintUtils.hasType(it, SessionEndHint::class.java) }) - verify(mockClient, times(2)).captureSession(any(), argWhere { HintUtils.hasType(it, SessionStartHint::class.java) }) - } - //endregion - - //region endSession tests - @Test - fun `when endSession is called on disabled client, do nothing`() { - val options = SentryOptions() - options.cacheDirPath = file.absolutePath - options.dsn = "https://key@sentry.io/proj" - options.release = "0.0.1" - options.setSerializer(mock()) - val sut = Hub(options) - val mockClient = mock() - sut.bindClient(mockClient) - sut.close() - - sut.endSession() - verify(mockClient, never()).captureSession(any(), any()) - } - - @Test - fun `when endSession is called and session tracking is disabled, do nothing`() { - val options = SentryOptions() - options.cacheDirPath = file.absolutePath - options.dsn = "https://key@sentry.io/proj" - options.release = "0.0.1" - options.setSerializer(mock()) - val sut = Hub(options) - val mockClient = mock() - sut.bindClient(mockClient) - - sut.endSession() - verify(mockClient, never()).captureSession(any(), any()) - } - - @Test - fun `when endSession is called, end a session`() { - val options = SentryOptions() - options.cacheDirPath = file.absolutePath - options.dsn = "https://key@sentry.io/proj" - options.release = "0.0.1" - options.setSerializer(mock()) - val sut = Hub(options) - val mockClient = mock() - sut.bindClient(mockClient) - - sut.startSession() - sut.endSession() - verify(mockClient).captureSession(any(), argWhere { HintUtils.hasType(it, SessionStartHint::class.java) }) - verify(mockClient).captureSession(any(), argWhere { HintUtils.hasType(it, SessionEndHint::class.java) }) - } - - @Test - fun `when endSession is called and there's no session, do nothing`() { - val options = SentryOptions() - options.cacheDirPath = file.absolutePath - options.dsn = "https://key@sentry.io/proj" - options.release = "0.0.1" - options.setSerializer(mock()) - val sut = Hub(options) - val mockClient = mock() - sut.bindClient(mockClient) - - sut.endSession() - verify(mockClient, never()).captureSession(any(), any()) - } - //endregion - - //region captureTransaction tests - @Test - fun `when captureTransaction is called on disabled client, do nothing`() { - val options = SentryOptions() - options.cacheDirPath = file.absolutePath - options.dsn = "https://key@sentry.io/proj" - options.setSerializer(mock()) - val sut = Hub(options) - val mockClient = mock() - sut.bindClient(mockClient) - sut.close() - - val sentryTracer = SentryTracer(TransactionContext("name", "op"), sut) - sentryTracer.finish() - sut.captureTransaction(SentryTransaction(sentryTracer), null as TraceContext?) - verify(mockClient, never()).captureTransaction(any(), any(), any()) - verify(mockClient, never()).captureTransaction(any(), any(), any(), anyOrNull(), anyOrNull()) - } - - @Test - fun `when captureTransaction and transaction is sampled, captureTransaction on the client should be called`() { - val options = SentryOptions() - options.cacheDirPath = file.absolutePath - options.dsn = "https://key@sentry.io/proj" - options.setSerializer(mock()) - val sut = Hub(options) - val mockClient = mock() - sut.bindClient(mockClient) - - val sentryTracer = SentryTracer(TransactionContext("name", "op", TracesSamplingDecision(true)), sut) - sentryTracer.finish() - val traceContext = sentryTracer.traceContext() - verify(mockClient).captureTransaction(any(), equalTraceContext(traceContext), any(), eq(null), eq(null)) - } - - @Test - fun `when captureTransaction is called, lastEventId is not set`() { - val options = SentryOptions().apply { - dsn = "https://key@sentry.io/proj" - setSerializer(mock()) - } - val sut = Hub(options) - val mockClient = mock() - sut.bindClient(mockClient) - whenever(mockClient.captureTransaction(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())).thenReturn(SentryId()) - - val sentryTracer = SentryTracer(TransactionContext("name", "op", TracesSamplingDecision(true)), sut) - sentryTracer.finish() - assertEquals(SentryId.EMPTY_ID, sut.lastEventId) - } - - @Test - fun `when captureTransaction and transaction is not finished, captureTransaction on the client should not be called`() { - val options = SentryOptions() - options.cacheDirPath = file.absolutePath - options.dsn = "https://key@sentry.io/proj" - options.setSerializer(mock()) - val sut = Hub(options) - val mockClient = mock() - sut.bindClient(mockClient) - - val sentryTracer = SentryTracer(TransactionContext("name", "op", TracesSamplingDecision(true)), sut) - sut.captureTransaction(SentryTransaction(sentryTracer), null as TraceContext?) - verify(mockClient, never()).captureTransaction(any(), any(), any(), eq(null), anyOrNull()) - } - - @Test - fun `when captureTransaction and transaction is not sampled, captureTransaction on the client should not be called`() { - val options = SentryOptions() - options.cacheDirPath = file.absolutePath - options.dsn = "https://key@sentry.io/proj" - options.setSerializer(mock()) - val sut = Hub(options) - val mockClient = mock() - sut.bindClient(mockClient) - - val sentryTracer = SentryTracer(TransactionContext("name", "op", TracesSamplingDecision(false)), sut) - sentryTracer.finish() - val traceContext = sentryTracer.traceContext() - verify(mockClient, never()).captureTransaction(any(), equalTraceContext(traceContext), any(), eq(null), anyOrNull()) - } - - @Test - fun `transactions lost due to sampling are recorded as lost`() { - val options = SentryOptions() - options.cacheDirPath = file.absolutePath - options.dsn = "https://key@sentry.io/proj" - options.setSerializer(mock()) - val sut = Hub(options) - val mockClient = mock() - sut.bindClient(mockClient) - - val sentryTracer = SentryTracer(TransactionContext("name", "op", TracesSamplingDecision(false)), sut) - sentryTracer.finish() - - assertClientReport( - options.clientReportRecorder, - listOf(DiscardedEvent(DiscardReason.SAMPLE_RATE.reason, DataCategory.Transaction.category, 1)) - ) - } - - @Test - fun `transactions lost due to sampling caused by backpressure are recorded as lost`() { - val options = SentryOptions() - options.cacheDirPath = file.absolutePath - options.dsn = "https://key@sentry.io/proj" - options.setSerializer(mock()) - val sut = Hub(options) - val mockClient = mock() - sut.bindClient(mockClient) - val mockBackpressureMonitor = mock() - options.backpressureMonitor = mockBackpressureMonitor - whenever(mockBackpressureMonitor.downsampleFactor).thenReturn(1) - - val sentryTracer = SentryTracer(TransactionContext("name", "op", TracesSamplingDecision(false)), sut) - sentryTracer.finish() - - assertClientReport( - options.clientReportRecorder, - listOf(DiscardedEvent(DiscardReason.BACKPRESSURE.reason, DataCategory.Transaction.category, 1)) - ) - } - //endregion - - //region profiling tests - - @Test - fun `when startTransaction and profiling is enabled, transaction is profiled only if sampled`() { - val mockTransactionProfiler = mock() - val mockClient = mock() - whenever(mockTransactionProfiler.onTransactionFinish(any(), anyOrNull(), anyOrNull())).thenAnswer { mockClient.captureEnvelope(mock()) } - val scopes = generateHub { - it.setTransactionProfiler(mockTransactionProfiler) - } - scopes.bindClient(mockClient) - // Transaction is not sampled, so it should not be profiled - val contexts = TransactionContext("name", "op", TracesSamplingDecision(false, null, true, null)) - val transaction = scopes.startTransaction(contexts) - transaction.finish() - verify(mockClient, never()).captureEnvelope(any()) - - // Transaction is sampled, so it should be profiled - val sampledContexts = TransactionContext("name", "op", TracesSamplingDecision(true, null, true, null)) - val sampledTransaction = scopes.startTransaction(sampledContexts) - sampledTransaction.finish() - verify(mockClient).captureEnvelope(any()) - } - - @Test - fun `when startTransaction and is sampled but profiling is disabled, transaction is not profiled`() { - val mockTransactionProfiler = mock() - val mockClient = mock() - whenever(mockTransactionProfiler.onTransactionFinish(any(), anyOrNull(), anyOrNull())).thenAnswer { mockClient.captureEnvelope(mock()) } - val scopes = generateHub { - it.profilesSampleRate = 0.0 - it.setTransactionProfiler(mockTransactionProfiler) - } - scopes.bindClient(mockClient) - val contexts = TransactionContext("name", "op") - val transaction = scopes.startTransaction(contexts) - transaction.finish() - verify(mockClient, never()).captureEnvelope(any()) - } - - @Test - fun `when profiler is running and isAppStartTransaction is false, startTransaction does not interact with profiler`() { - val mockTransactionProfiler = mock() - whenever(mockTransactionProfiler.isRunning).thenReturn(true) - val scopes = generateHub { - it.profilesSampleRate = 1.0 - it.setTransactionProfiler(mockTransactionProfiler) - } - val context = TransactionContext("name", "op") - scopes.startTransaction(context, TransactionOptions().apply { isAppStartTransaction = false }) - verify(mockTransactionProfiler, never()).start() - verify(mockTransactionProfiler, never()).bindTransaction(any()) - } - - @Test - fun `when profiler is running and isAppStartTransaction is true, startTransaction binds current profile`() { - val mockTransactionProfiler = mock() - whenever(mockTransactionProfiler.isRunning).thenReturn(true) - val scopes = generateHub { - it.profilesSampleRate = 1.0 - it.setTransactionProfiler(mockTransactionProfiler) - } - val context = TransactionContext("name", "op") - val transaction = scopes.startTransaction(context, TransactionOptions().apply { isAppStartTransaction = true }) - verify(mockTransactionProfiler, never()).start() - verify(mockTransactionProfiler).bindTransaction(eq(transaction)) - } - - @Test - fun `when profiler is not running, startTransaction starts and binds current profile`() { - val mockTransactionProfiler = mock() - whenever(mockTransactionProfiler.isRunning).thenReturn(false) - val scopes = generateHub { - it.profilesSampleRate = 1.0 - it.setTransactionProfiler(mockTransactionProfiler) - } - val context = TransactionContext("name", "op") - val transaction = scopes.startTransaction(context, TransactionOptions().apply { isAppStartTransaction = false }) - verify(mockTransactionProfiler).start() - verify(mockTransactionProfiler).bindTransaction(eq(transaction)) - } - //endregion - - //region startTransaction tests - @Test - fun `when startTransaction, creates transaction`() { - val scopes = generateHub() - val contexts = TransactionContext("name", "op") - - val transaction = scopes.startTransaction(contexts) - assertTrue(transaction is SentryTracer) - assertEquals(contexts, transaction.root.spanContext) - } - - @Test - fun `when startTransaction with bindToScope set to false, transaction is not attached to the scope`() { - val scopes = generateHub() - - scopes.startTransaction("name", "op", TransactionOptions()) - - scopes.configureScope { - assertNull(it.span) - } - } - - @Test - fun `when startTransaction without bindToScope set, transaction is not attached to the scope`() { - val scopes = generateHub() - - scopes.startTransaction("name", "op") - - scopes.configureScope { - assertNull(it.span) - } - } - - @Test - fun `when startTransaction with bindToScope set to true, transaction is attached to the scope`() { - val scopes = generateHub() - - val transaction = scopes.startTransaction("name", "op", TransactionOptions().also { it.isBindToScope = true }) - - scopes.configureScope { - assertEquals(transaction, it.span) - } - } - - @Test - fun `when startTransaction and no tracing sampling is configured, event is not sampled`() { - val scopes = generateHub { - it.tracesSampleRate = 0.0 - } - - val transaction = scopes.startTransaction("name", "op") - assertFalse(transaction.isSampled!!) - } - - @Test - fun `when startTransaction and no profile sampling is configured, profile is not sampled`() { - val scopes = generateHub { - it.tracesSampleRate = 1.0 - it.profilesSampleRate = 0.0 - } - - val transaction = scopes.startTransaction("name", "op") - assertTrue(transaction.isSampled!!) - assertFalse(transaction.isProfileSampled!!) - } - - @Test - fun `when startTransaction with parent sampled and no traces sampler provided, transaction inherits sampling decision`() { - val scopes = generateHub() - val transactionContext = TransactionContext("name", "op") - transactionContext.parentSampled = true - val transaction = scopes.startTransaction(transactionContext) - assertNotNull(transaction) - assertNotNull(transaction.isSampled) - assertTrue(transaction.isSampled!!) - } - - @Test - fun `when startTransaction with parent profile sampled and no profile sampler provided, transaction inherits profile sampling decision`() { - val scopes = generateHub() - val transactionContext = TransactionContext("name", "op") - transactionContext.setParentSampled(true, true) - val transaction = scopes.startTransaction(transactionContext) - assertTrue(transaction.isProfileSampled!!) - } - - @Test - fun `Hub should close the sentry executor processor, profiler and performance collector on close call`() { - val executor = mock() - val profiler = mock() - val performanceCollector = mock() - val options = SentryOptions().apply { - dsn = "https://key@sentry.io/proj" - cacheDirPath = file.absolutePath - executorService = executor - setTransactionProfiler(profiler) - transactionPerformanceCollector = performanceCollector - } - val sut = Hub(options) - sut.close() - verify(executor).close(any()) - verify(profiler).close() - verify(performanceCollector).close() - } - - @Test - fun `Hub with isRestarting true should close the sentry executor in the background`() { - val executor = spy(DeferredExecutorService()) - val options = SentryOptions().apply { - dsn = "https://key@sentry.io/proj" - executorService = executor - } - val sut = Hub(options) - sut.close(true) - verify(executor, never()).close(any()) - executor.runAll() - verify(executor).close(any()) - } - - @Test - fun `Hub with isRestarting false should close the sentry executor in the background`() { - val executor = mock() - val options = SentryOptions().apply { - dsn = "https://key@sentry.io/proj" - executorService = executor - } - val sut = Hub(options) - sut.close(false) - verify(executor).close(any()) - } - - @Test - fun `Hub close should clear the scope`() { - val options = SentryOptions().apply { - dsn = "https://key@sentry.io/proj" - } - - val sut = Hub(options) - sut.addBreadcrumb("Test") - sut.startTransaction("test", "test.op", TransactionOptions().also { it.isBindToScope = true }) - sut.close() - - // we have to clone the scope, so its isEnabled returns true, but it's still built up from - // the old scope preserving its data - val clone = sut.clone() - var oldScope: IScope? = null - clone.configureScope { scope -> oldScope = scope } - assertNull(oldScope!!.transaction) - assertTrue(oldScope!!.breadcrumbs.isEmpty()) - } - - @Test - fun `when tracesSampleRate and tracesSampler are not set on SentryOptions, startTransaction returns NoOp`() { - val scopes = generateHub { - it.tracesSampleRate = null - it.tracesSampler = null - } - val transaction = scopes.startTransaction(TransactionContext("name", "op", TracesSamplingDecision(true))) - assertTrue(transaction is NoOpTransaction) - } - //endregion - - //region startTransaction tests - @Test - fun `when traceHeaders and no transaction is active, traceHeaders are generated from scope`() { - val scopes = generateHub() - - var spanId: SpanId? = null - scopes.configureScope { spanId = it.propagationContext.spanId } - - val traceHeader = scopes.traceHeaders() - assertNotNull(traceHeader) - assertEquals(spanId, traceHeader.spanId) - } - - @Test - fun `when traceHeaders and there is an active transaction, traceHeaders are not null`() { - val scopes = generateHub() - val tx = scopes.startTransaction("aTransaction", "op") - scopes.configureScope { it.setTransaction(tx) } - - assertNotNull(scopes.traceHeaders()) - } - //endregion - - //region getSpan tests - @Test - fun `when there is no active transaction, getSpan returns null`() { - val scopes = generateHub() - assertNull(scopes.span) - } - - @Test - fun `when there is no active transaction, getTransaction returns null`() { - val scopes = generateHub() - assertNull(scopes.transaction) - } - - @Test - fun `when there is active transaction bound to the scope, getTransaction and getSpan return active transaction`() { - val scopes = generateHub() - val tx = scopes.startTransaction("aTransaction", "op") - scopes.configureScope { it.transaction = tx } - - assertEquals(tx, scopes.transaction) - assertEquals(tx, scopes.span) - } - - @Test - fun `when there is a transaction but the scopes is closed, getTransaction returns null`() { - val scopes = generateHub() - scopes.startTransaction("name", "op") - scopes.close() - - assertNull(scopes.transaction) - } - - @Test - fun `when there is active span within a transaction bound to the scope, getSpan returns active span`() { - val scopes = generateHub() - val tx = scopes.startTransaction("aTransaction", "op") - scopes.configureScope { it.setTransaction(tx) } - scopes.configureScope { it.setTransaction(tx) } - val span = tx.startChild("op") - - assertEquals(tx, scopes.transaction) - assertEquals(span, scopes.span) - } - // endregion - - //region setSpanContext - @Test - fun `associates span context with throwable`() { - val (scopes, mockClient) = getEnabledHub() - val transaction = scopes.startTransaction("aTransaction", "op") - val span = transaction.startChild("op") - val exception = RuntimeException() - - scopes.setSpanContext(exception, span, "tx-name") - scopes.captureEvent(SentryEvent(exception)) - - verify(mockClient).captureEvent( - check { - assertEquals(span.spanContext, it.contexts.trace) - }, - anyOrNull(), - anyOrNull() - ) - } - - @Test - fun `returns null when no span context associated with throwable`() { - val scopes = generateHub() as Hub - assertNull(scopes.getSpanContext(RuntimeException())) - } - // endregion - - @Test - fun `isCrashedLastRun does not delete native marker if auto session is enabled`() { - val nativeMarker = File(hashedFolder(), EnvelopeCache.NATIVE_CRASH_MARKER_FILE) - nativeMarker.mkdirs() - nativeMarker.createNewFile() - val scopes = generateHub() as Hub - - assertTrue(scopes.isCrashedLastRun!!) - assertTrue(nativeMarker.exists()) - } - - @Test - fun `isCrashedLastRun deletes the native marker if auto session is disabled`() { - val nativeMarker = File(hashedFolder(), EnvelopeCache.NATIVE_CRASH_MARKER_FILE) - nativeMarker.mkdirs() - nativeMarker.createNewFile() - val scopes = generateHub { - it.isEnableAutoSessionTracking = false - } - - assertTrue(scopes.isCrashedLastRun!!) - assertFalse(nativeMarker.exists()) - } - - @Test - fun `reportFullyDisplayed is ignored if TimeToFullDisplayTracing is disabled`() { - var called = false - val scopes = generateHub { - it.fullyDisplayedReporter.registerFullyDrawnListener { - called = !called - } - } - scopes.reportFullyDisplayed() - assertFalse(called) - } - - @Test - fun `reportFullyDisplayed calls FullyDisplayedReporter if TimeToFullDisplayTracing is enabled`() { - var called = false - val scopes = generateHub { - it.isEnableTimeToFullDisplayTracing = true - it.fullyDisplayedReporter.registerFullyDrawnListener { - called = !called - } - } - scopes.reportFullyDisplayed() - assertTrue(called) - } - - @Test - fun `reportFullyDisplayed calls FullyDisplayedReporter only once`() { - var called = false - val scopes = generateHub { - it.isEnableTimeToFullDisplayTracing = true - it.fullyDisplayedReporter.registerFullyDrawnListener { - called = !called - } - } - scopes.reportFullyDisplayed() - assertTrue(called) - scopes.reportFullyDisplayed() - assertTrue(called) - } - - @Test - fun `reportFullDisplayed calls reportFullyDisplayed`() { - val scopes = spy(generateHub()) - scopes.reportFullDisplayed() - verify(scopes).reportFullyDisplayed() - } - - @Test - fun `continueTrace creates propagation context from headers and returns transaction context if performance enabled`() { - val scopes = generateHub() - val traceId = SentryId() - val parentSpanId = SpanId() - val transactionContext = scopes.continueTrace("$traceId-$parentSpanId-1", listOf("sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=1,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET")) - - scopes.configureScope { scope -> - assertEquals(traceId, scope.propagationContext.traceId) - assertEquals(parentSpanId, scope.propagationContext.parentSpanId) - } - - assertEquals(traceId, transactionContext!!.traceId) - assertEquals(parentSpanId, transactionContext!!.parentSpanId) - } - - @Test - fun `continueTrace creates new propagation context if header invalid and returns transaction context if performance enabled`() { - val scopes = generateHub() - val traceId = SentryId() - var propagationContextHolder = AtomicReference() - - scopes.configureScope { propagationContextHolder.set(it.propagationContext) } - val propagationContextAtStart = propagationContextHolder.get()!! - - val transactionContext = scopes.continueTrace("invalid", listOf("sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=1,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET")) - - scopes.configureScope { scope -> - assertNotEquals(propagationContextAtStart.traceId, scope.propagationContext.traceId) - assertNotEquals(propagationContextAtStart.parentSpanId, scope.propagationContext.parentSpanId) - assertNotEquals(propagationContextAtStart.spanId, scope.propagationContext.spanId) - - assertEquals(scope.propagationContext.traceId, transactionContext!!.traceId) - assertEquals(scope.propagationContext.parentSpanId, transactionContext!!.parentSpanId) - assertEquals(scope.propagationContext.spanId, transactionContext!!.spanId) - } - } - - @Test - fun `continueTrace creates propagation context from headers and returns null if performance disabled`() { - val scopes = generateHub { it.enableTracing = false } - val traceId = SentryId() - val parentSpanId = SpanId() - val transactionContext = scopes.continueTrace("$traceId-$parentSpanId-1", listOf("sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=1,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET")) - - scopes.configureScope { scope -> - assertEquals(traceId, scope.propagationContext.traceId) - assertEquals(parentSpanId, scope.propagationContext.parentSpanId) - } - - assertNull(transactionContext) - } - - @Test - fun `continueTrace creates new propagation context if header invalid and returns null if performance disabled`() { - val scopes = generateHub { it.enableTracing = false } - val traceId = SentryId() - var propagationContextHolder = AtomicReference() - - scopes.configureScope { propagationContextHolder.set(it.propagationContext) } - val propagationContextAtStart = propagationContextHolder.get()!! - - val transactionContext = scopes.continueTrace("invalid", listOf("sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=1,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET")) - - scopes.configureScope { scope -> - assertNotEquals(propagationContextAtStart.traceId, scope.propagationContext.traceId) - assertNotEquals(propagationContextAtStart.parentSpanId, scope.propagationContext.parentSpanId) - assertNotEquals(propagationContextAtStart.spanId, scope.propagationContext.spanId) - } - - assertNull(transactionContext) - } - - @Test - fun `scopes provides no tags for metrics, if metric option is disabled`() { - val scopes = generateHub { - it.isEnableMetrics = false - it.isEnableDefaultTagsForMetrics = true - } as Hub - - assertTrue( - scopes.defaultTagsForMetrics.isEmpty() - ) - } - - @Test - fun `scopes provides no tags for metrics, if default tags option is disabled`() { - val scopes = generateHub { - it.isEnableMetrics = true - it.isEnableDefaultTagsForMetrics = false - } as Hub - - assertTrue( - scopes.defaultTagsForMetrics.isEmpty() - ) - } - - @Test - fun `scopes provides minimum default tags for metrics, if nothing is set up`() { - val scopes = generateHub { - it.isEnableMetrics = true - it.isEnableDefaultTagsForMetrics = true - } as Hub - - assertEquals( - mapOf( - "environment" to "production" - ), - scopes.defaultTagsForMetrics - ) - } - - @Test - fun `scopes provides default tags for metrics, based on options and running transaction`() { - val scopes = generateHub { - it.isEnableMetrics = true - it.isEnableDefaultTagsForMetrics = true - it.environment = "test" - it.release = "1.0" - } as Hub - scopes.startTransaction( - "name", - "op", - TransactionOptions().apply { isBindToScope = true } - ) - - assertEquals( - mapOf( - "environment" to "test", - "release" to "1.0", - "transaction" to "name" - ), - scopes.defaultTagsForMetrics - ) - } - - @Test - fun `scopes provides no local metric aggregator if metrics feature is disabled`() { - val scopes = generateHub { - it.isEnableMetrics = false - it.isEnableSpanLocalMetricAggregation = true - } as Hub - - scopes.startTransaction( - "name", - "op", - TransactionOptions().apply { isBindToScope = true } - ) - - assertNull(scopes.localMetricsAggregator) - } - - @Test - fun `scopes provides no local metric aggregator if local aggregation feature is disabled`() { - val scopes = generateHub { - it.isEnableMetrics = true - it.isEnableSpanLocalMetricAggregation = false - } as Hub - - scopes.startTransaction( - "name", - "op", - TransactionOptions().apply { isBindToScope = true } - ) - - assertNull(scopes.localMetricsAggregator) - } - - @Test - fun `scopes provides local metric aggregator if feature is enabled`() { - val scopes = generateHub { - it.isEnableMetrics = true - it.isEnableSpanLocalMetricAggregation = true - } as Hub - - scopes.startTransaction( - "name", - "op", - TransactionOptions().apply { isBindToScope = true } - ) - assertNotNull(scopes.localMetricsAggregator) - } - - @Test - fun `scopes startSpanForMetric starts a child span`() { - val scopes = generateHub { - it.isEnableMetrics = true - it.isEnableSpanLocalMetricAggregation = true - it.sampleRate = 1.0 - } as Hub - - val txn = scopes.startTransaction( - "name.txn", - "op.txn", - TransactionOptions().apply { isBindToScope = true } - ) - - val span = scopes.startSpanForMetric("op", "key")!! - - assertEquals("op", span.spanContext.op) - assertEquals("key", span.spanContext.description) - assertEquals(span.spanContext.parentSpanId, txn.spanContext.spanId) - } - - private val dsnTest = "https://key@sentry.io/proj" - - private fun generateHub(optionsConfiguration: Sentry.OptionsConfiguration? = null): IScopes { - val options = SentryOptions().apply { - dsn = dsnTest - cacheDirPath = file.absolutePath - setSerializer(mock()) - tracesSampleRate = 1.0 - } - optionsConfiguration?.configure(options) - return Hub(options) - } - - private fun getEnabledHub(): Triple { - val logger = mock() - - val options = SentryOptions() - options.cacheDirPath = file.absolutePath - options.dsn = "https://key@sentry.io/proj" - options.setSerializer(mock()) - options.tracesSampleRate = 1.0 - options.isDebug = true - options.setLogger(logger) - - val sut = Hub(options) - val mockClient = mock() - sut.bindClient(mockClient) - return Triple(sut, mockClient, logger) - } - - private fun hashedFolder(): String { - val hash = StringUtils.calculateStringHash(dsnTest, mock()) - val fileHashFolder = File(file.absolutePath, hash!!) - return fileHashFolder.absolutePath - } - - private fun equalTraceContext(expectedContext: TraceContext?): TraceContext? { - expectedContext ?: return eq(null) - - return argWhere { actual -> - expectedContext.traceId == actual.traceId && - expectedContext.transaction == actual.transaction && - expectedContext.environment == actual.environment && - expectedContext.release == actual.release && - expectedContext.publicKey == actual.publicKey && - expectedContext.sampleRate == actual.sampleRate && - expectedContext.userId == actual.userId && - expectedContext.userSegment == actual.userSegment - } - } -}