Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });

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

const CHUNK_INTERVAL_MS = 60000; // 1 minute
// Maximum length for trace lifecycle profiling per root span (e.g. if spanEnd never fires)
const MAX_ROOT_SPAN_PROFILE_MS = 300000; // 5 minutes

/**
 * Browser trace-lifecycle profiler (UI Profiling / Profiling V2):
 * - Starts when the first sampled root span starts
 * - Stops when the last sampled root span ends
 * - While running, periodically stops and restarts the JS self-profiling API to collect chunks
 *
 * Profiles are emitted as standalone `profile_chunk` envelopes either when:
 * - there are no more sampled root spans, or
 * - the 60s chunk timer elapses while profiling is running.
 */
class BrowserTraceLifecycleProfiler {

  // For keeping track of active root spans

  // ID for Profiler session

   constructor() {
    this._client = undefined;
    this._profiler = undefined;
    this._chunkTimer = undefined;
    this._activeRootSpanIds = new Set();
    this._rootSpanTimeouts = new Map();
    this._profilerId = undefined;
    this._isRunning = false;
    this._sessionSampled = false;
  }

  /**
   * Initialize the profiler with client and session sampling decision computed by the integration.
   */
   initialize(client, sessionSampled) {
    // One Profiler ID per profiling session (user session)
    this._profilerId = core.uuid4();

    debugBuild.DEBUG_BUILD && core.debug.log("[Profiling] Initializing profiler (lifecycle='trace').");

    this._client = client;
    this._sessionSampled = sessionSampled;

    client.on('spanStart', span => {
      if (!this._sessionSampled) {
        debugBuild.DEBUG_BUILD && core.debug.log('[Profiling] Session not sampled because of negative sampling decision.');
        return;
      }
      if (span !== core.getRootSpan(span)) {
        return;
      }
      // Only count sampled root spans
      if (!span.isRecording()) {
        debugBuild.DEBUG_BUILD && core.debug.log('[Profiling] Discarding profile because root span was not sampled.');
        return;
      }

      // Matching root spans with profiles
      core.getGlobalScope().setContext('profile', {
        profiler_id: this._profilerId,
      });

      const spanId = span.spanContext().spanId;
      if (!spanId) {
        return;
      }
      if (this._activeRootSpanIds.has(spanId)) {
        return;
      }

      this._activeRootSpanIds.add(spanId);
      const rootSpanCount = this._activeRootSpanIds.size;

      const timeout = setTimeout(() => {
        this._onRootSpanTimeout(spanId);
      }, MAX_ROOT_SPAN_PROFILE_MS);
      this._rootSpanTimeouts.set(spanId, timeout);

      if (rootSpanCount === 1) {
        debugBuild.DEBUG_BUILD &&
          core.debug.log(
            `[Profiling] Root span with ID ${spanId} started. Will continue profiling for as long as there are active root spans (currently: ${rootSpanCount}).`,
          );

        this.start();
      }
    });

    client.on('spanEnd', span => {
      if (!this._sessionSampled) {
        return;
      }

      const spanId = span.spanContext().spanId;
      if (!spanId || !this._activeRootSpanIds.has(spanId)) {
        return;
      }

      this._activeRootSpanIds.delete(spanId);
      const rootSpanCount = this._activeRootSpanIds.size;

      debugBuild.DEBUG_BUILD &&
        core.debug.log(
          `[Profiling] Root span with ID ${spanId} ended. Will continue profiling for as long as there are active root spans (currently: ${rootSpanCount}).`,
        );
      if (rootSpanCount === 0) {
        this._collectCurrentChunk().catch(e => {
          debugBuild.DEBUG_BUILD && core.debug.error('[Profiling] Failed to collect current profile chunk on `spanEnd`:', e);
        });

        this.stop();
      }
    });
  }

  /**
   * Handle an already-active root span at integration setup time.
   */
   notifyRootSpanActive(rootSpan) {
    if (!this._sessionSampled) {
      return;
    }

    const spanId = rootSpan.spanContext().spanId;
    if (!spanId || this._activeRootSpanIds.has(spanId)) {
      return;
    }

    this._activeRootSpanIds.add(spanId);

    const rootSpanCount = this._activeRootSpanIds.size;

    if (rootSpanCount === 1) {
      debugBuild.DEBUG_BUILD &&
        core.debug.log('[Profiling] Detected already active root span during setup. Active root spans now:', rootSpanCount);

      this.start();
    }
  }

  /**
   * Start profiling if not already running.
   */
   start() {
    if (this._isRunning) {
      return;
    }
    this._isRunning = true;

    debugBuild.DEBUG_BUILD && core.debug.log('[Profiling] Started profiling with profile ID:', this._profilerId);

    this._startProfilerInstance();

    if (!this._profiler) {
      debugBuild.DEBUG_BUILD && core.debug.log('[Profiling] Stopping trace lifecycle profiling.');
      this._resetProfilerInfo();
      return;
    }

    this._startPeriodicChunking();
  }

  /**
   * Stop profiling; final chunk will be collected and sent.
   */
   stop() {
    if (!this._isRunning) {
      return;
    }

    this._isRunning = false;
    if (this._chunkTimer) {
      clearTimeout(this._chunkTimer);
      this._chunkTimer = undefined;
    }

    this._clearAllRootSpanTimeouts();

    // Collect whatever was currently recording
    this._collectCurrentChunk().catch(e => {
      debugBuild.DEBUG_BUILD && core.debug.error('[Profiling] Failed to collect current profile chunk on `stop()`:', e);
    });
  }

  /**
   * Resets profiling information from scope and resets running state
   */
   _resetProfilerInfo() {
    this._isRunning = false;
    core.getGlobalScope().setContext('profile', {});
  }

  /**
   * Clear and reset all per-root-span timeouts.
   */
   _clearAllRootSpanTimeouts() {
    this._rootSpanTimeouts.forEach(timeout => clearTimeout(timeout));
    this._rootSpanTimeouts.clear();
  }

  /**
   * Start a profiler instance if needed.
   */
   _startProfilerInstance() {
    if (this._profiler?.stopped === false) {
      return;
    }
    const profiler = utils.startJSSelfProfile();
    if (!profiler) {
      debugBuild.DEBUG_BUILD && core.debug.log('[Profiling] Failed to start JS Profiler in trace lifecycle.');
      return;
    }
    this._profiler = profiler;
  }

  /**
   * Schedule the next 60s chunk while running.
   * Each tick collects a chunk and restarts the profiler.
   * A chunk should be closed when there are no active root spans anymore OR when the maximum chunk interval is reached.
   */
   _startPeriodicChunking() {
    if (!this._isRunning) {
      return;
    }

    this._chunkTimer = setTimeout(() => {
      this._collectCurrentChunk().catch(e => {
        debugBuild.DEBUG_BUILD && core.debug.error('[Profiling] Failed to collect current profile chunk during periodic chunking:', e);
      });

      if (this._isRunning) {
        this._startProfilerInstance();

        if (!this._profiler) {
          // If restart failed, stop scheduling further chunks and reset context.
          this._resetProfilerInfo();
          return;
        }

        this._startPeriodicChunking();
      }
    }, CHUNK_INTERVAL_MS);
  }

  /**
   * Handle timeout for a specific root span ID to avoid indefinitely running profiler if `spanEnd` never fires.
   * If this was the last active root span, collect the current chunk and stop profiling.
   */
   _onRootSpanTimeout(rootSpanId) {
    // If span already ended, ignore
    if (!this._rootSpanTimeouts.has(rootSpanId)) {
      return;
    }
    this._rootSpanTimeouts.delete(rootSpanId);

    if (!this._activeRootSpanIds.has(rootSpanId)) {
      return;
    }

    debugBuild.DEBUG_BUILD &&
      core.debug.log(
        `[Profiling] Reached 5-minute timeout for root span ${rootSpanId}. You likely started a manual root span that never called \`.end()\`.`,
      );

    this._activeRootSpanIds.delete(rootSpanId);

    const rootSpanCount = this._activeRootSpanIds.size;
    if (rootSpanCount === 0) {
      this.stop();
    }
  }

  /**
   * Stop the current profiler, convert and send a profile chunk.
   */
   async _collectCurrentChunk() {
    const prevProfiler = this._profiler;
    this._profiler = undefined;

    if (!prevProfiler) {
      return;
    }

    try {
      const profile = await prevProfiler.stop();

      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
      const chunk = utils.createProfileChunkPayload(profile, this._client, this._profilerId);

      // Validate chunk before sending
      const validationReturn = utils.validateProfileChunk(chunk);
      if ('reason' in validationReturn) {
        debugBuild.DEBUG_BUILD &&
          core.debug.log(
            '[Profiling] Discarding invalid profile chunk (this is probably a bug in the SDK):',
            validationReturn.reason,
          );
        return;
      }

      this._sendProfileChunk(chunk);

      debugBuild.DEBUG_BUILD && core.debug.log('[Profiling] Collected browser profile chunk.');
    } catch (e) {
      debugBuild.DEBUG_BUILD && core.debug.log('[Profiling] Error while stopping JS Profiler for chunk:', e);
    }
  }

  /**
   * Send a profile chunk as a standalone envelope.
   */
   _sendProfileChunk(chunk) {
    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
    const client = this._client;

    const sdkInfo = core.getSdkMetadataForEnvelopeHeader(client.getSdkMetadata?.());
    const dsn = client.getDsn();
    const tunnel = client.getOptions().tunnel;

    const envelope = core.createEnvelope(
      {
        event_id: core.uuid4(),
        sent_at: new Date().toISOString(),
        ...(sdkInfo && { sdk: sdkInfo }),
        ...(!!tunnel && dsn && { dsn: core.dsnToString(dsn) }),
      },
      [[{ type: 'profile_chunk' }, chunk]],
    );

    client.sendEnvelope(envelope).then(null, reason => {
      debugBuild.DEBUG_BUILD && core.debug.error('Error while sending profile chunk envelope:', reason);
    });
  }
}

exports.BrowserTraceLifecycleProfiler = BrowserTraceLifecycleProfiler;//# sourceMappingURL=http://go/sourcemap/sourcemaps/cd1c87ff9b66021918fb9731605f8d1d5fd2f0b0/node_modules/@sentry/browser/build/npm/cjs/prod/profiling/lifecycleMode/traceLifecycleProfiler.js.map
