lib/playwright/channel_owners/tracing.rb



module Playwright
  define_channel_owner :Tracing do
    private def after_initialize
      @har_recorders = {}
      @har_id = nil
    end

    def start(name: nil, title: nil, screenshots: nil, snapshots: nil, sources: nil, live: nil)
      params = {
        name: name,
        screenshots: screenshots,
        snapshots: snapshots,
        sources: sources,
        live: live,
      }.compact
      @include_sources = params[:sources] || false
      @channel.send_message_to_server('tracingStart', params)
      trace_name = @channel.send_message_to_server('tracingStartChunk', { title: title, name: name }.compact)
      start_collecting_stacks(trace_name)
    end

    def start_chunk(title: nil, name: nil)
      trace_name = @channel.send_message_to_server('tracingStartChunk', { title: title, name: name }.compact)
      start_collecting_stacks(trace_name)
    end

    private def start_collecting_stacks(trace_name)
      unless @is_tracing
        @is_tracing = true
        @connection.set_in_tracing(true)
      end
      local_utils = @connection.local_utils
      @stacks_id = local_utils&.tracing_started(@traces_dir, trace_name)
    end

    def stop_chunk(path: nil)
      do_stop_chunk(file_path: path)
    end

    def stop(path: nil)
      do_stop_chunk(file_path: path)
      @channel.send_message_to_server('tracingStop')
    end

    private def do_stop_chunk(file_path:)
      reset_stack_counter
      local_utils = @connection.local_utils

      unless file_path
        # Not interested in any artifacts
        @channel.send_message_to_server('tracingStopChunk', mode: 'discard')
        if @stacks_id
          local_utils.trace_discarded(@stacks_id) if local_utils
        end

        return
      end

      is_local = !@connection.remote?
      if is_local
        unless local_utils
          raise 'Cannot save trace because localUtils is unavailable.'
        end

        result = @channel.send_message_to_server_result('tracingStopChunk', mode: 'entries')
        local_utils.zip(
          zipFile: file_path,
          entries: result['entries'],
          stacksId: @stacks_id,
          mode: 'write',
          includeSources: @include_sources,
        )

        return
      end


      result = @channel.send_message_to_server_result('tracingStopChunk', mode: 'archive')
      # The artifact may be missing if the browser closed while stopping tracing.
      unless result['artifact']
        if @stacks_id
          local_utils.trace_discarded(@stacks_id) if local_utils
        end

        return
      end

      # Save trace to the final local file.
      artifact = ChannelOwners::Artifact.from(result['artifact'])
      artifact.save_as(file_path)
      artifact.delete

      return unless local_utils

      local_utils.zip(
        zipFile: file_path,
        entries: [],
        stacksId: @stacks_id,
        mode: 'append',
        includeSources: @include_sources,
      )
    end

    def start_har(path, content: nil, mode: nil, urlFilter: nil, resourcesDir: nil)
      raise 'HAR recording has already been started' if @har_id
      if resourcesDir && path.end_with?('.zip')
        raise 'resourcesDir option is not compatible with a .zip har file'
      end

      default_content = path.end_with?('.zip') ? 'attach' : 'embed'
      @har_id = record_into_har(path, nil,
        url: urlFilter,
        update_content: content || default_content,
        update_mode: mode || 'full',
        resources_dir: resourcesDir,
      )
      DisposableStub.new { stop_har }
    end

    def stop_har
      har_id = @har_id
      raise 'HAR recording has not been started' unless har_id

      @har_id = nil
      export_har(har_id)
      nil
    end

    private def record_into_har(har, page, url:, update_content:, update_mode:, resources_dir: nil)
      options = {
        content: update_content || 'attach',
        mode: update_mode || 'minimal',
        harPath: har.end_with?('.zip') ? nil : har,
        resourcesDir: resources_dir,
      }.compact

      if url.is_a?(Regexp)
        regex = ::Playwright::JavaScript::Regex.new(url)
        options[:urlRegexSource] = regex.source
        options[:urlRegexFlags] = regex.flag
      elsif url.is_a?(String)
        options[:urlGlob] = url
      end

      params = { options: options }
      params[:page] = page.channel if page

      result = @channel.send_message_to_server_result('harStart', params)
      har_id = result['harId'] || result[:harId]
      @har_recorders[har_id] = { path: har, resources_dir: resources_dir }
      har_id
    end

    private def export_har(har_id)
      har_params = @har_recorders.delete(har_id)
      return unless har_params

      path = har_params[:path]
      is_zip = path.end_with?('.zip')
      local_utils = @connection.local_utils

      if !@connection.remote?
        result = @channel.send_message_to_server_result('harExport', harId: har_id, mode: 'entries')
        return unless is_zip
        raise 'Cannot save zipped HAR because localUtils is unavailable.' unless local_utils

        local_utils.zip(
          zipFile: path,
          entries: result['entries'],
          mode: 'write',
          includeSources: false,
        )
        return
      end

      result = @channel.send_message_to_server_result('harExport', harId: har_id, mode: 'archive')
      artifact = ChannelOwners::Artifact.from(result['artifact'])
      if is_zip
        artifact.save_as(path)
        artifact.delete
        return
      end

      raise 'Uncompressed har is not supported in thin clients' unless local_utils

      tmp_path = "#{path}.tmp"
      artifact.save_as(tmp_path)
      local_utils.har_unzip(tmp_path, path, resources_dir: har_params[:resources_dir])
      artifact.delete
    end

    private def export_all_hars
      @har_recorders.keys.each { |har_id| export_har(har_id) }
    end

    private def reset_stack_counter
      if @is_tracing
        @is_tracing = false
        @connection.set_in_tracing(false)
      end
    end

    private def update_traces_dir(traces_dir)
      @traces_dir = traces_dir
    end

    def group(name, location: nil)
      params = {
        name: name,
        location: location,
      }.compact
      @channel.send_message_to_server('tracingGroup', params)
    end

    def group_end
      @channel.send_message_to_server('tracingGroupEnd')
    end
  end
end