All Downloads are FREE. Search and download functionalities are using the official Maven repository.

package.build.npm.cjs.tracing.browserTracingIntegration.js Maven / Gradle / Ivy

There is a newer version: 8.40.0
Show newest version
Object.defineProperty(exports, '__esModule', { value: true });

const browserUtils = require('@sentry-internal/browser-utils');
const core = require('@sentry/core');
const utils = require('@sentry/utils');
const debugBuild = require('../debug-build.js');
const helpers = require('../helpers.js');
const backgroundtab = require('./backgroundtab.js');
const request = require('./request.js');

/* eslint-disable max-lines */

const BROWSER_TRACING_INTEGRATION_ID = 'BrowserTracing';

const DEFAULT_BROWSER_TRACING_OPTIONS = {
  ...core.TRACING_DEFAULTS,
  instrumentNavigation: true,
  instrumentPageLoad: true,
  markBackgroundSpan: true,
  enableLongTask: true,
  enableInp: true,
  _experiments: {},
  ...request.defaultRequestInstrumentationOptions,
};

/**
 * The Browser Tracing integration automatically instruments browser pageload/navigation
 * actions as transactions, and captures requests, metrics and errors as spans.
 *
 * The integration can be configured with a variety of options, and can be extended to use
 * any routing library.
 *
 * We explicitly export the proper type here, as this has to be extended in some cases.
 */
const browserTracingIntegration = ((_options = {}) => {
  core.registerSpanErrorInstrumentation();

  const {
    enableInp,
    enableLongTask,
    _experiments: { enableInteractions },
    beforeStartSpan,
    idleTimeout,
    finalTimeout,
    childSpanTimeout,
    markBackgroundSpan,
    traceFetch,
    traceXHR,
    shouldCreateSpanForRequest,
    enableHTTPTimings,
    instrumentPageLoad,
    instrumentNavigation,
  } = {
    ...DEFAULT_BROWSER_TRACING_OPTIONS,
    ..._options,
  };

  const _collectWebVitals = browserUtils.startTrackingWebVitals();

  if (enableInp) {
    browserUtils.startTrackingINP();
  }

  if (enableLongTask) {
    browserUtils.startTrackingLongTasks();
  }
  if (enableInteractions) {
    browserUtils.startTrackingInteractions();
  }

  const latestRoute = {
    name: undefined,
    source: undefined,
  };

  /** Create routing idle transaction. */
  function _createRouteSpan(client, startSpanOptions) {
    const isPageloadTransaction = startSpanOptions.op === 'pageload';

    const finalStartSpanOptions = beforeStartSpan
      ? beforeStartSpan(startSpanOptions)
      : startSpanOptions;

    const attributes = finalStartSpanOptions.attributes || {};

    // If `finalStartSpanOptions.name` is different than `startSpanOptions.name`
    // it is because `beforeStartSpan` set a custom name. Therefore we set the source to 'custom'.
    if (startSpanOptions.name !== finalStartSpanOptions.name) {
      attributes[core.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] = 'custom';
      finalStartSpanOptions.attributes = attributes;
    }

    latestRoute.name = finalStartSpanOptions.name;
    latestRoute.source = attributes[core.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE];

    const idleSpan = core.startIdleSpan(finalStartSpanOptions, {
      idleTimeout,
      finalTimeout,
      childSpanTimeout,
      // should wait for finish signal if it's a pageload transaction
      disableAutoFinish: isPageloadTransaction,
      beforeSpanEnd: span => {
        _collectWebVitals();
        browserUtils.addPerformanceEntries(span);
      },
    });

    function emitFinish() {
      if (['interactive', 'complete'].includes(helpers.WINDOW.document.readyState)) {
        client.emit('idleSpanEnableAutoFinish', idleSpan);
      }
    }

    if (isPageloadTransaction && helpers.WINDOW.document) {
      helpers.WINDOW.document.addEventListener('readystatechange', () => {
        emitFinish();
      });

      emitFinish();
    }

    return idleSpan;
  }

  return {
    name: BROWSER_TRACING_INTEGRATION_ID,
    afterAllSetup(client) {
      let activeSpan;
      let startingUrl = helpers.WINDOW.location && helpers.WINDOW.location.href;

      client.on('startNavigationSpan', startSpanOptions => {
        if (core.getClient() !== client) {
          return;
        }

        if (activeSpan) {
          debugBuild.DEBUG_BUILD && utils.logger.log(`[Tracing] Finishing current root span with op: ${core.spanToJSON(activeSpan).op}`);
          // If there's an open transaction on the scope, we need to finish it before creating an new one.
          activeSpan.end();
        }
        activeSpan = _createRouteSpan(client, {
          op: 'navigation',
          ...startSpanOptions,
        });
      });

      client.on('startPageLoadSpan', (startSpanOptions, traceOptions = {}) => {
        if (core.getClient() !== client) {
          return;
        }

        if (activeSpan) {
          debugBuild.DEBUG_BUILD && utils.logger.log(`[Tracing] Finishing current root span with op: ${core.spanToJSON(activeSpan).op}`);
          // If there's an open transaction on the scope, we need to finish it before creating an new one.
          activeSpan.end();
        }

        const sentryTrace = traceOptions.sentryTrace || getMetaContent('sentry-trace');
        const baggage = traceOptions.baggage || getMetaContent('baggage');

        const propagationContext = utils.propagationContextFromHeaders(sentryTrace, baggage);
        core.getCurrentScope().setPropagationContext(propagationContext);

        activeSpan = _createRouteSpan(client, {
          op: 'pageload',
          ...startSpanOptions,
        });
      });

      // A trace should to stay the consistent over the entire time span of one route.
      // Therefore, when the initial pageload or navigation root span ends, we update the
      // scope's propagation context to keep span-specific attributes like the `sampled` decision and
      // the dynamic sampling context valid, even after the root span has ended.
      // This ensures that the trace data is consistent for the entire duration of the route.
      client.on('spanEnd', span => {
        const op = core.spanToJSON(span).op;
        if (span !== core.getRootSpan(span) || (op !== 'navigation' && op !== 'pageload')) {
          return;
        }

        const scope = core.getCurrentScope();
        const oldPropagationContext = scope.getPropagationContext();

        scope.setPropagationContext({
          ...oldPropagationContext,
          sampled: oldPropagationContext.sampled !== undefined ? oldPropagationContext.sampled : core.spanIsSampled(span),
          dsc: oldPropagationContext.dsc || core.getDynamicSamplingContextFromSpan(span),
        });
      });

      if (helpers.WINDOW.location) {
        if (instrumentPageLoad) {
          startBrowserTracingPageLoadSpan(client, {
            name: helpers.WINDOW.location.pathname,
            // pageload should always start at timeOrigin (and needs to be in s, not ms)
            startTime: utils.browserPerformanceTimeOrigin ? utils.browserPerformanceTimeOrigin / 1000 : undefined,
            attributes: {
              [core.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
              [core.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser',
            },
          });
        }

        if (instrumentNavigation) {
          browserUtils.addHistoryInstrumentationHandler(({ to, from }) => {
            /**
             * This early return is there to account for some cases where a navigation transaction starts right after
             * long-running pageload. We make sure that if `from` is undefined and a valid `startingURL` exists, we don't
             * create an uneccessary navigation transaction.
             *
             * This was hard to duplicate, but this behavior stopped as soon as this fix was applied. This issue might also
             * only be caused in certain development environments where the usage of a hot module reloader is causing
             * errors.
             */
            if (from === undefined && startingUrl && startingUrl.indexOf(to) !== -1) {
              startingUrl = undefined;
              return;
            }

            if (from !== to) {
              startingUrl = undefined;
              startBrowserTracingNavigationSpan(client, {
                name: helpers.WINDOW.location.pathname,
                attributes: {
                  [core.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
                  [core.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser',
                },
              });
            }
          });
        }
      }

      if (markBackgroundSpan) {
        backgroundtab.registerBackgroundTabDetection();
      }

      if (enableInteractions) {
        registerInteractionListener(idleTimeout, finalTimeout, childSpanTimeout, latestRoute);
      }

      if (enableInp) {
        browserUtils.registerInpInteractionListener(latestRoute);
      }

      request.instrumentOutgoingRequests({
        traceFetch,
        traceXHR,
        tracePropagationTargets: client.getOptions().tracePropagationTargets,
        shouldCreateSpanForRequest,
        enableHTTPTimings,
      });
    },
  };
}) ;

/**
 * Manually start a page load span.
 * This will only do something if a browser tracing integration integration has been setup.
 *
 * If you provide a custom `traceOptions` object, it will be used to continue the trace
 * instead of the default behavior, which is to look it up on the  tags.
 */
function startBrowserTracingPageLoadSpan(
  client,
  spanOptions,
  traceOptions,
) {
  client.emit('startPageLoadSpan', spanOptions, traceOptions);

  core.getCurrentScope().setTransactionName(spanOptions.name);

  const span = core.getActiveSpan();
  const op = span && core.spanToJSON(span).op;
  return op === 'pageload' ? span : undefined;
}

/**
 * Manually start a navigation span.
 * This will only do something if a browser tracing integration has been setup.
 */
function startBrowserTracingNavigationSpan(client, spanOptions) {
  core.getIsolationScope().setPropagationContext(utils.generatePropagationContext());
  core.getCurrentScope().setPropagationContext(utils.generatePropagationContext());

  client.emit('startNavigationSpan', spanOptions);

  core.getCurrentScope().setTransactionName(spanOptions.name);

  const span = core.getActiveSpan();
  const op = span && core.spanToJSON(span).op;
  return op === 'navigation' ? span : undefined;
}

/** Returns the value of a meta tag */
function getMetaContent(metaName) {
  // Can't specify generic to `getDomElement` because tracing can be used
  // in a variety of environments, have to disable `no-unsafe-member-access`
  // as a result.
  const metaTag = utils.getDomElement(`meta[name=${metaName}]`);
  // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
  return metaTag ? metaTag.getAttribute('content') : undefined;
}

/** Start listener for interaction transactions */
function registerInteractionListener(
  idleTimeout,
  finalTimeout,
  childSpanTimeout,
  latestRoute,
) {
  let inflightInteractionSpan;
  const registerInteractionTransaction = () => {
    const op = 'ui.action.click';

    const activeSpan = core.getActiveSpan();
    const rootSpan = activeSpan && core.getRootSpan(activeSpan);
    if (rootSpan) {
      const currentRootSpanOp = core.spanToJSON(rootSpan).op;
      if (['navigation', 'pageload'].includes(currentRootSpanOp )) {
        debugBuild.DEBUG_BUILD &&
          utils.logger.warn(`[Tracing] Did not create ${op} span because a pageload or navigation span is in progress.`);
        return undefined;
      }
    }

    if (inflightInteractionSpan) {
      inflightInteractionSpan.setAttribute(core.SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON, 'interactionInterrupted');
      inflightInteractionSpan.end();
      inflightInteractionSpan = undefined;
    }

    if (!latestRoute.name) {
      debugBuild.DEBUG_BUILD && utils.logger.warn(`[Tracing] Did not create ${op} transaction because _latestRouteName is missing.`);
      return undefined;
    }

    inflightInteractionSpan = core.startIdleSpan(
      {
        name: latestRoute.name,
        op,
        attributes: {
          [core.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: latestRoute.source || 'url',
        },
      },
      {
        idleTimeout,
        finalTimeout,
        childSpanTimeout,
      },
    );
  };

  if (helpers.WINDOW.document) {
    addEventListener('click', registerInteractionTransaction, { once: false, capture: true });
  }
}

exports.BROWSER_TRACING_INTEGRATION_ID = BROWSER_TRACING_INTEGRATION_ID;
exports.browserTracingIntegration = browserTracingIntegration;
exports.getMetaContent = getMetaContent;
exports.startBrowserTracingNavigationSpan = startBrowserTracingNavigationSpan;
exports.startBrowserTracingPageLoadSpan = startBrowserTracingPageLoadSpan;
//# sourceMappingURL=browserTracingIntegration.js.map




© 2015 - 2024 Weber Informatics LLC | Privacy Policy