code: /*
   * Copyright (c) 2022 Airbyte, Inc., all rights reserved.
   */
  
  package io.airbyte.scheduler.persistence.job_error_reporter;
  
  import edu.umd.cs.findbugs.annotations.Nullable;
  import io.airbyte.config.FailureReason;
  import io.airbyte.config.Metadata;
  import io.airbyte.config.StandardWorkspace;
  import io.sentry.Hub;
  import io.sentry.IHub;
  import io.sentry.NoOpHub;
  import io.sentry.SentryEvent;
  import io.sentry.SentryOptions;
  import io.sentry.protocol.Message;
  import io.sentry.protocol.SentryException;
  import io.sentry.protocol.User;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.Optional;
  
  public class SentryJobErrorReportingClient implements JobErrorReportingClient {
  
    static final String STACKTRACE_PARSE_ERROR_TAG_KEY = "stacktrace_parse_error";
    private final IHub sentryHub;
    private final SentryExceptionHelper exceptionHelper;
  
    SentryJobErrorReportingClient(final IHub sentryHub, final SentryExceptionHelper exceptionHelper) {
      this.sentryHub = sentryHub;
      this.exceptionHelper = exceptionHelper;
    }
  
    public SentryJobErrorReportingClient(final String sentryDSN, final SentryExceptionHelper exceptionHelper) {
      this(createSentryHubWithDSN(sentryDSN), exceptionHelper);
    }
  
    static IHub createSentryHubWithDSN(final String sentryDSN) {
      if (sentryDSN == null || sentryDSN.isEmpty()) {
        return NoOpHub.getInstance();
      }
  
      final SentryOptions options = new SentryOptions();
      options.setDsn(sentryDSN);
      options.setAttachStacktrace(false);
      options.setEnableUncaughtExceptionHandler(false);
      return new Hub(options);
    }
  
    /**
     * Reports a Connector Job FailureReason to Sentry
     *
     * @param workspace - Workspace where this failure occurred
     * @param failureReason - FailureReason to report
     * @param dockerImage - Tagged docker image that represents the release where this failure occurred
     * @param metadata - Extra metadata to set as tags on the event
     */
    @Override
    public void reportJobFailureReason(@Nullable final StandardWorkspace workspace,
                                       final FailureReason failureReason,
                                       final String dockerImage,
                                       final Map<String, String> metadata) {
      final SentryEvent event = new SentryEvent();
  
      // Remove invalid characters from the release name, use @ so sentry knows how to grab the tag
      // e.g. airbyte/source-xyz:1.2.0 -> airbyte-source-xyz@1.2.0
      // More info at https://docs.sentry.io/product/cli/releases/#creating-releases
      final String release = dockerImage.replace("/", "-").replace(":", "@");
      event.setRelease(release);
  
      // enhance event fingerprint to ensure separate grouping per connector
      final String[] releaseParts = release.split("@");
      if (releaseParts.length > 0) {
        event.setFingerprints(List.of("{{ default }}", releaseParts[0]));
      }
  
      // set workspace as the user in sentry to get impact and priority
      if (workspace != null) {
        final User sentryUser = new User();
        sentryUser.setId(String.valueOf(workspace.getWorkspaceId()));
        sentryUser.setUsername(workspace.getName());
        event.setUser(sentryUser);
      }
  
      // set metadata as tags
      event.setTags(metadata);
  
      // set failure reason's internalMessage as event message
      // Sentry will use this to fuzzy-group if no stacktrace information is available
      final Message message = new Message();
      message.setFormatted(failureReason.getInternalMessage());
      event.setMessage(message);
  
      // events can come from any platform
      event.setPlatform("other");
  
      // attach failure reason stack trace
      final String failureStackTrace = failureReason.getStacktrace();
      if (failureStackTrace != null && !failureStackTrace.isBlank()) {
        final Optional<List<SentryException>> parsedExceptions = exceptionHelper.buildSentryExceptions(failureStackTrace);
        if (parsedExceptions.isPresent()) {
          event.setExceptions(parsedExceptions.get());
        } else {
          event.setTag(STACKTRACE_PARSE_ERROR_TAG_KEY, "1");
  
          // We couldn't parse the stacktrace, but we can still give it to Sentry for (less accurate) grouping
          final String normalizedStacktrace = failureStackTrace
              .replace("\n", ", ")
              .replace(failureReason.getInternalMessage(), "");
  
          final SentryException sentryException = new SentryException();
          sentryException.setValue(normalizedStacktrace);
          event.setExceptions(List.of(sentryException));
        }
      }
  
      sentryHub.configureScope(scope -> {
        final Map<String, String> failureReasonContext = new HashMap<>();
        failureReasonContext.put("internalMessage", failureReason.getInternalMessage());
        failureReasonContext.put("externalMessage", failureReason.getExternalMessage());
        failureReasonContext.put("stacktrace", failureReason.getStacktrace());
        failureReasonContext.put("timestamp", failureReason.getTimestamp().toString());
  
        final Metadata failureReasonMeta = failureReason.getMetadata();
        if (failureReasonMeta != null) {
          failureReasonContext.put("metadata", failureReasonMeta.toString());
        }
  
        scope.setContexts("Failure Reason", failureReasonContext);
      });
  
      sentryHub.captureEvent(event);
    }
  
  }
  