function processForProto(lhr) {
  /** @type {LH.Result} */
  const reportJson = JSON.parse(JSON.stringify(lhr));

  // Clean up the configSettings
  // Note: This is not strictly required for conversion if protobuf parsing is set to
  // 'ignore unknown fields' in the language of conversion.
  if (reportJson.configSettings) {
    // The settings that are in both proto and LHR
    const {
      formFactor,
      locale,
      onlyCategories,
      channel,
      throttling,
      screenEmulation,
      throttlingMethod} = reportJson.configSettings;

    // @ts-expect-error - intentionally only a subset of settings.
    reportJson.configSettings = {
      formFactor,
      locale,
      onlyCategories,
      channel,
      throttling,
      screenEmulation,
      throttlingMethod};
  }

  // Remove runtimeError if it is NO_ERROR
  if (reportJson.runtimeError && reportJson.runtimeError.code === 'NO_ERROR') {
    delete reportJson.runtimeError;
  }

  // Clean up actions that require 'audits' to exist
  if (reportJson.audits) {
    Object.keys(reportJson.audits).forEach(auditName => {
      const audit = reportJson.audits[auditName];

      // Rewrite 'not-applicable' and 'not_applicable' scoreDisplayMode to 'notApplicable'. #6201, #6783.
      if (audit.scoreDisplayMode) {
        // @ts-expect-error ts properly flags this as invalid as it should not happen,
        // but remains in preprocessor to protect from proto translation errors from
        // old LHRs.
        // eslint-disable-next-line max-len
        if (audit.scoreDisplayMode === 'not-applicable' || audit.scoreDisplayMode === 'not_applicable') {
          audit.scoreDisplayMode = 'notApplicable';
        }
      }

      // Normalize displayValue to always be a string, not an array. #6200
      if (Array.isArray(audit.displayValue)) {
        /** @type {Array<any>}*/
        const values = [];
        audit.displayValue.forEach(item => {
          values.push(item);
        });
        audit.displayValue = values.join(' | ');
      }
    });
  }

  /**
   * Execute `cb(obj, key)` on every object property where obj[key] is a string, recursively.
   * @param {any} obj
   * @param {(obj: Record<string, string>, key: string) => void} cb
   */
  function iterateStrings(obj, cb) {
    if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
      Object.keys(obj).forEach(key => {
        if (typeof obj[key] === 'string') {
          cb(obj, key);
        } else {
          iterateStrings(obj[key], cb);
        }
      });
    } else if (Array.isArray(obj)) {
      obj.forEach(item => {
        if (typeof item === 'object' || Array.isArray(item)) {
          iterateStrings(item, cb);
        }
      });
    }
  }

  iterateStrings(reportJson, (obj, key) => {
    const value = obj[key];

    // Remove empty strings, as they are dropped after round-tripping anyway.
    if (value === '') {
      delete obj[key];
      return;
    }

    // Sanitize lone surrogates.
    // @ts-expect-error node 20
    if (String.prototype.isWellFormed && !value.isWellFormed()) {
      // @ts-expect-error node 20
      obj[key] = value.toWellFormed();
    }
  });

  return reportJson;
}
Transform an LHR into a proto-friendly, mostly-compatible LHR.
@param {LH.Result} lhr
@return {LH.Result}