chromium/chromecast/cast_core/runtime/browser/cast_runtime_metrics_recorder.cc

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromecast/cast_core/runtime/browser/cast_runtime_metrics_recorder.h"

#include <cfloat>
#include <cmath>
#include <optional>

#include "base/json/json_string_value_serializer.h"
#include "base/logging.h"
#include "base/metrics/histogram_base.h"
#include "base/metrics/sparse_histogram.h"
#include "base/metrics/user_metrics.h"
#include "base/notreached.h"
#include "base/task/sequenced_task_runner.h"
#include "base/time/time.h"
#include "base/values.h"
#include "chromecast/base/metrics/cast_histograms.h"
#include "chromecast/base/metrics/cast_metrics_helper.h"
#include "third_party/metrics_proto/cast_logs.pb.h"

namespace chromecast {
namespace {

// The following are used by cast events created from a JSON format string.
constexpr char kEventName[] = "name";
constexpr char kEventAppId[] = "app_id";
constexpr char kEventSessionId[] = "session_id";
constexpr char kEventSdkVersion[] = "sdk_version";
constexpr char kEventTime[] = "time";
constexpr char kEventValue[] = "value";
constexpr char kEventEventsPair[] = "events";
constexpr char kEventAuditReport[] = "audit_report";

constexpr int64_t kInt64DoubleMax = 1LL << DBL_MANT_DIG;

constexpr size_t kCastEventLimit = 1000000;

void PopulateEventBuilder(CastEventBuilder* event_builder,
                          double event_time,
                          const std::string& app_id,
                          const std::string& sdk_version,
                          const std::string& session_id) {
  event_builder->SetTime(base::TimeTicks() + base::Microseconds(event_time));
  if (!app_id.empty())
    event_builder->SetAppId(app_id);
  if (!sdk_version.empty())
    event_builder->SetSdkVersion(sdk_version);
  if (!session_id.empty())
    event_builder->SetSessionId(session_id);
}

}  // namespace

CastRuntimeMetricsRecorder::EventBuilderFactory::~EventBuilderFactory() =
    default;

// NOTE: This is the same as MetricsRecorderImpl but it seems improper to mix
// MetricsRecorderImpl's static functions when this is the actual
// MetricsRecorder instance that is live.
// static
void CastRuntimeMetricsRecorder::RecordSimpleActionWithValue(
    const std::string& action,
    int64_t value) {
  MetricsRecorder* recorder = MetricsRecorder::GetInstance();
  std::unique_ptr<CastEventBuilder> event_builder(
      recorder->CreateEventBuilder(action));
  event_builder->SetExtraValue(value);
  recorder->RecordCastEvent(std::move(event_builder));
}

CastRuntimeMetricsRecorder::CastRuntimeMetricsRecorder(
    EventBuilderFactory* event_builder_factory)
    : task_runner_(base::SequencedTaskRunner::GetCurrentDefault()),
      event_builder_factory_(event_builder_factory) {
  DCHECK(event_builder_factory_);

  DCHECK(!MetricsRecorder::GetInstance());
  MetricsRecorder::SetInstance(this);
  metrics::CastMetricsHelper::GetInstance()->SetRecordActionCallback(
      base::BindRepeating(
          &CastRuntimeMetricsRecorder::CastMetricsHelperRecordActionCallback,
          weak_factory_.GetWeakPtr()));
}

CastRuntimeMetricsRecorder::~CastRuntimeMetricsRecorder() {
  DCHECK(MetricsRecorder::GetInstance() == this);
  metrics::CastMetricsHelper::GetInstance()->SetRecordActionCallback(
      base::BindRepeating(&base::RecordComputedAction));
  MetricsRecorder::SetInstance(nullptr);
}

std::vector<cast::metrics::Event> CastRuntimeMetricsRecorder::TakeEvents() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return std::move(events_);
}

std::unique_ptr<CastEventBuilder>
CastRuntimeMetricsRecorder::CreateEventBuilder(const std::string& name) {
  auto builder = event_builder_factory_->CreateEventBuilder();
  builder->SetName(name);
  return builder;
}

void CastRuntimeMetricsRecorder::AddActiveConnection(
    const std::string& transport_connection_id,
    const std::string& virtual_connection_id,
    const base::Value& sender_info,
    const net::IPAddressBytes& sender_ip) {
  NOTIMPLEMENTED();
}

void CastRuntimeMetricsRecorder::RemoveActiveConnection(
    const std::string& connection_id) {
  NOTIMPLEMENTED();
}

void CastRuntimeMetricsRecorder::RecordCastEvent(
    std::unique_ptr<CastEventBuilder> event_builder) {
  if (task_runner_->RunsTasksInCurrentSequence()) {
    DVLOG(1) << "RecordCastEvent direct";
    RecordCastEventOnSequence(std::move(event_builder));
  } else {
    DVLOG(1) << "RecordCastEvent bounce";
    task_runner_->PostTask(
        FROM_HERE,
        base::BindOnce(&CastRuntimeMetricsRecorder::RecordCastEventOnSequence,
                       weak_factory_.GetWeakPtr(), std::move(event_builder)));
  }
}

void CastRuntimeMetricsRecorder::RecordCastEventOnSequence(
    std::unique_ptr<CastEventBuilder> event_builder) {
  DVLOG(1) << "RecordCastEventOnSequence";
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (events_.size() >= kCastEventLimit) {
    static bool logged_once = false;
    if (!logged_once) {
      LOG(ERROR) << "Too many events queued, dropping...";
      logged_once = true;
    }
    return;
  }
  std::unique_ptr<::metrics::CastLogsProto_CastEventProto> event_proto(
      event_builder->Build());
  std::string str;
  if (!event_proto->SerializeToString(&str)) {
    LOG(ERROR) << "Failed to serialize CastEventProto";
    return;
  }

  cast::metrics::Event e;
  e.mutable_impl_defined_event()->set_name(event_builder->GetName());
  e.mutable_impl_defined_event()->set_data(str);
  events_.push_back(std::move(e));
}

void CastRuntimeMetricsRecorder::RecordHistogramTime(const std::string& name,
                                                     int sample,
                                                     int min,
                                                     int max,
                                                     int num_buckets) {
  UMA_HISTOGRAM_CUSTOM_TIMES_NO_CACHE(name, base::Milliseconds(sample),
                                      base::Milliseconds(min),
                                      base::Milliseconds(max), num_buckets);
}

void CastRuntimeMetricsRecorder::RecordHistogramCount(const std::string& name,
                                                      int sample,
                                                      int min,
                                                      int max,
                                                      int num_buckets) {
  UMA_HISTOGRAM_CUSTOM_COUNTS_NO_CACHE(name, sample, min, max, num_buckets,
                                       /* count */ 1);
}

void CastRuntimeMetricsRecorder::RecordHistogramCountRepeated(
    const std::string& name,
    int sample,
    int min,
    int max,
    int num_buckets,
    int count) {
  UMA_HISTOGRAM_CUSTOM_COUNTS_NO_CACHE(name, sample, min, max, num_buckets,
                                       count);
}

void CastRuntimeMetricsRecorder::RecordHistogramEnum(const std::string& name,
                                                     int sample,
                                                     int boundary) {
  UMA_HISTOGRAM_ENUMERATION_NO_CACHE(name, sample, boundary);
}

void CastRuntimeMetricsRecorder::RecordHistogramSparse(const std::string& name,
                                                       int sample) {
  base::HistogramBase* counter = base::SparseHistogram::FactoryGet(
      name, base::HistogramBase::kUmaTargetedHistogramFlag);
  counter->Add(sample);
}

void CastRuntimeMetricsRecorder::CastMetricsHelperRecordActionCallback(
    const std::string& action) {
  DVLOG(1) << "Record action via CastMetricsHelper: " << action;
  std::string action_name;
  std::string app_id;
  std::string session_id;
  std::string sdk_version;
  if (metrics::CastMetricsHelper::DecodeAppInfoFromMetricsName(
          action, &action_name, &app_id, &session_id, &sdk_version)) {
    if (app_id.empty() && session_id.empty()) {
      LOG(ERROR) << "Missing app and session ID";
      return;
    }
    std::unique_ptr<CastEventBuilder> event_builder(
        CreateEventBuilder(action_name));
    event_builder->SetAppId(app_id)
        .SetSessionId(session_id)
        .SetSdkVersion(sdk_version);
    RecordCastEvent(std::move(event_builder));
    return;
  }

  // Tries to parse and record event string as JSON event.
  if (RecordJsonCastEvent(action)) {
    return;
  }

  // This is normal non-JSON user action event.
  RecordCastEvent(CreateEventBuilder(action));
}

bool CastRuntimeMetricsRecorder::RecordJsonCastEvent(const std::string& event) {
  std::unique_ptr<const base::Value> value(
      JSONStringValueDeserializer(event).Deserialize(nullptr, nullptr));
  if (!value) {
    LOG(ERROR) << "This is not a JSON format event: " << event;
    return false;
  }

  if (!value->is_dict()) {
    LOG(ERROR) << "This is not a dictionary JSON format event: " << event;
    return false;
  }

  const base::Value::Dict& value_dict = value->GetDict();
  const std::string* name = value_dict.FindString(kEventName);
  if (!name) {
    LOG(ERROR) << "Missing field:" << kEventName;
    return false;
  }

  // Gets event creation time. If unavailable use now.
  std::optional<double> maybe_event_time = value_dict.FindDouble(kEventTime);
  double event_time = 0;
  if (maybe_event_time && maybe_event_time.value() > 0) {
    event_time = maybe_event_time.value();
  } else {
    event_time = (base::TimeTicks::Now() - base::TimeTicks()).InMicroseconds();
  }
  // Gets App Id.
  const std::string* maybe_app_id = value_dict.FindString(kEventAppId);
  std::string app_id;
  if (maybe_app_id) {
    app_id = *maybe_app_id;
  }
  // Gets session Id.
  const std::string* maybe_session_id = value_dict.FindString(kEventSessionId);
  std::string session_id;
  if (maybe_session_id) {
    session_id = *maybe_session_id;
  }
  // Gets SDK version.
  const std::string* maybe_sdk_version =
      value_dict.FindString(kEventSdkVersion);
  std::string sdk_version;
  if (maybe_sdk_version) {
    sdk_version = *maybe_sdk_version;
  }

  const base::Value::Dict* multiple_events =
      value_dict.FindDict(kEventEventsPair);
  if (!multiple_events) {
    std::unique_ptr<CastEventBuilder> event_builder(CreateEventBuilder(*name));
    PopulateEventBuilder(event_builder.get(), event_time, app_id, sdk_version,
                         session_id);
    std::optional<double> maybe_event_value =
        value_dict.FindDouble(kEventValue);
    if (maybe_event_value) {
      double event_value = maybe_event_value.value();
      DCHECK_EQ(event_value, std::nearbyint(event_value));
      DCHECK_LE(std::abs(event_value), kInt64DoubleMax);
      event_builder->SetExtraValue(static_cast<int64_t>(event_value));
    }

    const std::string* audit_report = value_dict.FindString(kEventAuditReport);
    if (audit_report) {
      event_builder->SetAuditReport(*audit_report);
    }

    RecordCastEvent(std::move(event_builder));
    return true;
  }

  for (auto kv : *multiple_events) {
    std::optional<double> maybe_event_value = kv.second.GetIfDouble();
    if (!maybe_event_value) {
      continue;
    }

    double event_value = maybe_event_value.value();
    DCHECK_EQ(event_value, std::nearbyint(event_value));
    DCHECK_LE(std::abs(event_value), kInt64DoubleMax);

    std::unique_ptr<CastEventBuilder> event_builder(
        CreateEventBuilder(*name + "." + kv.first));
    event_builder->SetExtraValue(static_cast<int64_t>(event_value));
    PopulateEventBuilder(event_builder.get(), event_time, app_id, sdk_version,
                         session_id);
    RecordCastEvent(std::move(event_builder));
  }
  return true;
}

}  // namespace chromecast