var {
  _optionalChain
} = require('@sentry/utils');

Object.defineProperty(exports, '__esModule', { value: true });

const core = require('@sentry/core');
const utils = require('@sentry/utils');
const debugBuild = require('../debug-build.js');
const nodeVersion = require('../nodeVersion.js');
const http = require('./utils/http.js');

const _httpIntegration = ((options = {}) => {
  const { breadcrumbs, tracing, shouldCreateSpanForRequest } = options;

  const convertedOptions = {
    breadcrumbs,
    tracing:
      tracing === false
        ? false
        : utils.dropUndefinedKeys({
            // If tracing is forced to `true`, we don't want to set `enableIfHasTracingEnabled`
            enableIfHasTracingEnabled: tracing === true ? undefined : true,
            shouldCreateSpanForRequest,
          }),
  };

  // eslint-disable-next-line deprecation/deprecation
  return new Http(convertedOptions) ;
}) ;

/**
 * The http module integration instruments Node's internal http module. It creates breadcrumbs, spans for outgoing
 * http requests, and attaches trace data when tracing is enabled via its `tracing` option.
 *
 * By default, this will always create breadcrumbs, and will create spans if tracing is enabled.
 */
const httpIntegration = core.defineIntegration(_httpIntegration);

/**
 * The http module integration instruments Node's internal http module. It creates breadcrumbs, transactions for outgoing
 * http requests and attaches trace data when tracing is enabled via its `tracing` option.
 *
 * @deprecated Use `httpIntegration()` instead.
 */
class Http  {
  /**
   * @inheritDoc
   */
   static __initStatic() {this.id = 'Http';}

  /**
   * @inheritDoc
   */
  // eslint-disable-next-line deprecation/deprecation
   __init() {this.name = Http.id;}

  /**
   * @inheritDoc
   */
   constructor(options = {}) {Http.prototype.__init.call(this);
    this._breadcrumbs = typeof options.breadcrumbs === 'undefined' ? true : options.breadcrumbs;
    this._tracing = !options.tracing ? undefined : options.tracing === true ? {} : options.tracing;
  }

  /**
   * @inheritDoc
   */
   setupOnce(
    _addGlobalEventProcessor,
    // eslint-disable-next-line deprecation/deprecation
    setupOnceGetCurrentHub,
  ) {
    // eslint-disable-next-line deprecation/deprecation
    const clientOptions = _optionalChain([setupOnceGetCurrentHub, 'call', _ => _(), 'access', _2 => _2.getClient, 'call', _3 => _3(), 'optionalAccess', _4 => _4.getOptions, 'call', _5 => _5()]);

    // If `tracing` is not explicitly set, we default this based on whether or not tracing is enabled.
    // But for compatibility, we only do that if `enableIfHasTracingEnabled` is set.
    const shouldCreateSpans = _shouldCreateSpans(this._tracing, clientOptions);

    // No need to instrument if we don't want to track anything
    if (!this._breadcrumbs && !shouldCreateSpans) {
      return;
    }

    // Do not auto-instrument for other instrumenter
    if (clientOptions && clientOptions.instrumenter !== 'sentry') {
      debugBuild.DEBUG_BUILD && utils.logger.log('HTTP Integration is skipped because of instrumenter configuration.');
      return;
    }

    const shouldCreateSpanForRequest = _getShouldCreateSpanForRequest(shouldCreateSpans, this._tracing, clientOptions);

    // eslint-disable-next-line deprecation/deprecation
    const tracePropagationTargets = _optionalChain([clientOptions, 'optionalAccess', _6 => _6.tracePropagationTargets]) || _optionalChain([this, 'access', _7 => _7._tracing, 'optionalAccess', _8 => _8.tracePropagationTargets]);

    // eslint-disable-next-line @typescript-eslint/no-var-requires
    const httpModule = require('http');
    const wrappedHttpHandlerMaker = _createWrappedRequestMethodFactory(
      httpModule,
      this._breadcrumbs,
      shouldCreateSpanForRequest,
      tracePropagationTargets,
    );
    utils.fill(httpModule, 'get', wrappedHttpHandlerMaker);
    utils.fill(httpModule, 'request', wrappedHttpHandlerMaker);

    // NOTE: Prior to Node 9, `https` used internals of `http` module, thus we don't patch it.
    // If we do, we'd get double breadcrumbs and double spans for `https` calls.
    // It has been changed in Node 9, so for all versions equal and above, we patch `https` separately.
    if (nodeVersion.NODE_VERSION.major > 8) {
      // eslint-disable-next-line @typescript-eslint/no-var-requires
      const httpsModule = require('https');
      const wrappedHttpsHandlerMaker = _createWrappedRequestMethodFactory(
        httpsModule,
        this._breadcrumbs,
        shouldCreateSpanForRequest,
        tracePropagationTargets,
      );
      utils.fill(httpsModule, 'get', wrappedHttpsHandlerMaker);
      utils.fill(httpsModule, 'request', wrappedHttpsHandlerMaker);
    }
  }
}Http.__initStatic();

// for ease of reading below

/**
 * Function which creates a function which creates wrapped versions of internal `request` and `get` calls within `http`
 * and `https` modules. (NB: Not a typo - this is a creator^2!)
 *
 * @param breadcrumbsEnabled Whether or not to record outgoing requests as breadcrumbs
 * @param tracingEnabled Whether or not to record outgoing requests as tracing spans
 *
 * @returns A function which accepts the exiting handler and returns a wrapped handler
 */
function _createWrappedRequestMethodFactory(
  httpModule,
  breadcrumbsEnabled,
  shouldCreateSpanForRequest,
  tracePropagationTargets,
) {
  // We're caching results so we don't have to recompute regexp every time we create a request.
  const createSpanUrlMap = new utils.LRUMap(100);
  const headersUrlMap = new utils.LRUMap(100);

  const shouldCreateSpan = (url) => {
    if (shouldCreateSpanForRequest === undefined) {
      return true;
    }

    const cachedDecision = createSpanUrlMap.get(url);
    if (cachedDecision !== undefined) {
      return cachedDecision;
    }

    const decision = shouldCreateSpanForRequest(url);
    createSpanUrlMap.set(url, decision);
    return decision;
  };

  const shouldAttachTraceData = (url) => {
    if (tracePropagationTargets === undefined) {
      return true;
    }

    const cachedDecision = headersUrlMap.get(url);
    if (cachedDecision !== undefined) {
      return cachedDecision;
    }

    const decision = utils.stringMatchesSomePattern(url, tracePropagationTargets);
    headersUrlMap.set(url, decision);
    return decision;
  };

  /**
   * Captures Breadcrumb based on provided request/response pair
   */
  function addRequestBreadcrumb(
    event,
    requestSpanData,
    req,
    res,
  ) {
    // eslint-disable-next-line deprecation/deprecation
    if (!core.getCurrentHub().getIntegration(Http)) {
      return;
    }

    core.addBreadcrumb(
      {
        category: 'http',
        data: {
          status_code: res && res.statusCode,
          ...requestSpanData,
        },
        type: 'http',
      },
      {
        event,
        request: req,
        response: res,
      },
    );
  }

  return function wrappedRequestMethodFactory(originalRequestMethod) {
    return function wrappedMethod( ...args) {
      const requestArgs = http.normalizeRequestArgs(httpModule, args);
      const requestOptions = requestArgs[0];
      // eslint-disable-next-line deprecation/deprecation
      const rawRequestUrl = http.extractRawUrl(requestOptions);
      const requestUrl = http.extractUrl(requestOptions);
      const client = core.getClient();

      // we don't want to record requests to Sentry as either breadcrumbs or spans, so just use the original method
      if (core.isSentryRequestUrl(requestUrl, client)) {
        return originalRequestMethod.apply(httpModule, requestArgs);
      }

      const scope = core.getCurrentScope();
      const isolationScope = core.getIsolationScope();
      const parentSpan = core.getActiveSpan();

      const data = getRequestSpanData(requestUrl, requestOptions);

      const requestSpan = shouldCreateSpan(rawRequestUrl)
        ? // eslint-disable-next-line deprecation/deprecation
          _optionalChain([parentSpan, 'optionalAccess', _9 => _9.startChild, 'call', _10 => _10({
            op: 'http.client',
            origin: 'auto.http.node.http',
            description: `${data['http.method']} ${data.url}`,
            data,
          })])
        : undefined;

      if (client && shouldAttachTraceData(rawRequestUrl)) {
        const { traceId, spanId, sampled, dsc } = {
          ...isolationScope.getPropagationContext(),
          ...scope.getPropagationContext(),
        };

        const sentryTraceHeader = requestSpan
          ? core.spanToTraceHeader(requestSpan)
          : utils.generateSentryTraceHeader(traceId, spanId, sampled);

        const sentryBaggageHeader = utils.dynamicSamplingContextToSentryBaggageHeader(
          dsc ||
            (requestSpan
              ? core.getDynamicSamplingContextFromSpan(requestSpan)
              : core.getDynamicSamplingContextFromClient(traceId, client, scope)),
        );

        addHeadersToRequestOptions(requestOptions, requestUrl, sentryTraceHeader, sentryBaggageHeader);
      } else {
        debugBuild.DEBUG_BUILD &&
          utils.logger.log(
            `[Tracing] Not adding sentry-trace header to outgoing request (${requestUrl}) due to mismatching tracePropagationTargets option.`,
          );
      }

      // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
      return originalRequestMethod
        .apply(httpModule, requestArgs)
        .once('response', function ( res) {
          // eslint-disable-next-line @typescript-eslint/no-this-alias
          const req = this;
          if (breadcrumbsEnabled) {
            addRequestBreadcrumb('response', data, req, res);
          }
          if (requestSpan) {
            if (res.statusCode) {
              core.setHttpStatus(requestSpan, res.statusCode);
            }
            requestSpan.updateName(
              http.cleanSpanDescription(core.spanToJSON(requestSpan).description || '', requestOptions, req) || '',
            );
            requestSpan.end();
          }
        })
        .once('error', function () {
          // eslint-disable-next-line @typescript-eslint/no-this-alias
          const req = this;

          if (breadcrumbsEnabled) {
            addRequestBreadcrumb('error', data, req);
          }
          if (requestSpan) {
            core.setHttpStatus(requestSpan, 500);
            requestSpan.updateName(
              http.cleanSpanDescription(core.spanToJSON(requestSpan).description || '', requestOptions, req) || '',
            );
            requestSpan.end();
          }
        });
    };
  };
}

function addHeadersToRequestOptions(
  requestOptions,
  requestUrl,
  sentryTraceHeader,
  sentryBaggageHeader,
) {
  // Don't overwrite sentry-trace and baggage header if it's already set.
  const headers = requestOptions.headers || {};
  if (headers['sentry-trace']) {
    return;
  }

  debugBuild.DEBUG_BUILD &&
    utils.logger.log(`[Tracing] Adding sentry-trace header ${sentryTraceHeader} to outgoing request to "${requestUrl}": `);

  requestOptions.headers = {
    ...requestOptions.headers,
    'sentry-trace': sentryTraceHeader,
    // Setting a header to `undefined` will crash in node so we only set the baggage header when it's defined
    ...(sentryBaggageHeader &&
      sentryBaggageHeader.length > 0 && { baggage: normalizeBaggageHeader(requestOptions, sentryBaggageHeader) }),
  };
}

function getRequestSpanData(requestUrl, requestOptions) {
  const method = requestOptions.method || 'GET';
  const data = {
    url: requestUrl,
    'http.method': method,
  };
  if (requestOptions.hash) {
    // strip leading "#"
    data['http.fragment'] = requestOptions.hash.substring(1);
  }
  if (requestOptions.search) {
    // strip leading "?"
    data['http.query'] = requestOptions.search.substring(1);
  }
  return data;
}

function normalizeBaggageHeader(
  requestOptions,
  sentryBaggageHeader,
) {
  if (!requestOptions.headers || !requestOptions.headers.baggage) {
    return sentryBaggageHeader;
  } else if (!sentryBaggageHeader) {
    return requestOptions.headers.baggage ;
  } else if (Array.isArray(requestOptions.headers.baggage)) {
    return [...requestOptions.headers.baggage, sentryBaggageHeader];
  }
  // Type-cast explanation:
  // Technically this the following could be of type `(number | string)[]` but for the sake of simplicity
  // we say this is undefined behaviour, since it would not be baggage spec conform if the user did this.
  return [requestOptions.headers.baggage, sentryBaggageHeader] ;
}

/** Exported for tests only. */
function _shouldCreateSpans(
  tracingOptions,
  clientOptions,
) {
  return tracingOptions === undefined
    ? false
    : tracingOptions.enableIfHasTracingEnabled
      ? core.hasTracingEnabled(clientOptions)
      : true;
}

/** Exported for tests only. */
function _getShouldCreateSpanForRequest(
  shouldCreateSpans,
  tracingOptions,
  clientOptions,
) {
  const handler = shouldCreateSpans
    ? // eslint-disable-next-line deprecation/deprecation
      _optionalChain([tracingOptions, 'optionalAccess', _11 => _11.shouldCreateSpanForRequest]) || _optionalChain([clientOptions, 'optionalAccess', _12 => _12.shouldCreateSpanForRequest])
    : () => false;

  return handler;
}

exports.Http = Http;
exports._getShouldCreateSpanForRequest = _getShouldCreateSpanForRequest;
exports._shouldCreateSpans = _shouldCreateSpans;
exports.httpIntegration = httpIntegration;
//# sourceMappingURL=http.js.map
