| #!/bin/bash | |
| # Copyright 2019 The Fuchsia Authors. All rights reserved. | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| # Common methods for metrics collection. | |
| # | |
| # Note: For non-shell programs, use metrics_custom_report.sh instead. | |
| # | |
| # Report events to the metrics collector from `fx`, the `fx metrics`, and the | |
| # rare subcommand that has special metrics needs. | |
| # | |
| # How to use it: This file is sourced in //scripts/fx for tracking command | |
| # execution and in //tools/devshell/metrics for managing the metrics collection | |
| # settings. Developers of shell-based subcommands can source this file in their | |
| # subcommand if they need custom event tracking, but only the method | |
| # track-subcommand-custom-event can be used in this context. | |
| # | |
| # This script assumes that vars.sh has already been sourced, since it | |
| # depends on FUCHSIA_DIR being defined correctly. | |
| _METRICS_GA_PROPERTY_ID="UA-127897021-6" | |
| _METRICS_TRACK_ALL_ARGS=( "emu" "set" "fidlcat" "run-test" "run-test-component" "run-host-tests" ) | |
| _METRICS_TRACK_RESULTS=( "set" "build" ) | |
| _METRICS_ALLOWS_CUSTOM_REPORTING=( "test" ) | |
| # These variables need to be global, but readonly (or declare -r) declares new | |
| # variables as local when they are source'd inside a function. | |
| # "declare -g -r" is the right way to handle it, but it is not supported in | |
| # old versions of Bash, particularly in the one in MacOS. The alternative is to | |
| # make them global first via the assignments above and marking they readonly | |
| # later. | |
| readonly _METRICS_GA_PROPERTY_ID _METRICS_TRACK_ALL_ARGS _METRICS_TRACK_RESULTS _METRICS_ALLOWS_CUSTOM_REPORTING | |
| # To properly enable unit testing, METRICS_CONFIG is not read-only | |
| METRICS_CONFIG="${FUCHSIA_DIR}/.fx-metrics-config" | |
| _METRICS_DEBUG=0 | |
| _METRICS_DEBUG_LOG_FILE="" | |
| _METRICS_USE_VALIDATION_SERVER=0 | |
| INIT_WARNING=$'Please opt in or out of fx metrics collection.\n' | |
| INIT_WARNING+=$'You will receive this warning until an option is selected.\n' | |
| INIT_WARNING+=$'To check what data we collect, run `fx metrics`\n' | |
| INIT_WARNING+=$'To opt in or out, run `fx metrics <enable|disable>\n' | |
| # Each Analytics batch call can send at most this many hits. | |
| declare -r BATCH_SIZE=20 | |
| # Keep track of how many hits have accumulated. | |
| hit_count=0 | |
| # Holds curl args for the current batch of hits. | |
| curl_args=() | |
| function __is_in { | |
| local v="$1" | |
| shift | |
| while [[ $# -gt 0 ]]; do | |
| if [[ "$1" == "$v" ]]; then | |
| return 0 | |
| fi | |
| shift | |
| done | |
| return 1 | |
| } | |
| function metrics-read-config { | |
| METRICS_UUID="" | |
| METRICS_ENABLED=0 | |
| _METRICS_DEBUG_LOG_FILE="" | |
| if [[ ! -f "${METRICS_CONFIG}" ]]; then | |
| return 1 | |
| fi | |
| source "${METRICS_CONFIG}" | |
| if [[ $METRICS_ENABLED == 1 && -z "$METRICS_UUID" ]]; then | |
| METRICS_ENABLED=0 | |
| return 1 | |
| fi | |
| return 0 | |
| } | |
| function metrics-write-config { | |
| local enabled=$1 | |
| if [[ "$enabled" -eq "1" ]]; then | |
| local uuid="$2" | |
| if [[ $# -gt 2 ]]; then | |
| local debug_logfile="$3" | |
| fi | |
| fi | |
| local -r tempfile="$(mktemp)" | |
| # Exit trap to clean up temp file | |
| trap "[[ -f \"${tempfile}\" ]] && rm -f \"${tempfile}\"" EXIT | |
| { | |
| echo "# Autogenerated config file for fx metrics. Run 'fx help metrics' for more information." | |
| echo "METRICS_ENABLED=${enabled}" | |
| echo "METRICS_UUID=\"${uuid}\"" | |
| if [[ -n "${debug_logfile}" ]]; then | |
| echo "_METRICS_DEBUG_LOG_FILE=${debug_logfile}" | |
| fi | |
| } >> "${tempfile}" | |
| # Only rewrite the config file if content has changed | |
| if ! cmp --silent "${tempfile}" "${METRICS_CONFIG}" ; then | |
| mv -f "${tempfile}" "${METRICS_CONFIG}" | |
| fi | |
| } | |
| function metrics-read-and-validate { | |
| local hide_init_warning=$1 | |
| if ! metrics-read-config; then | |
| if [[ $hide_init_warning -ne 1 ]]; then | |
| fx-warn "${INIT_WARNING}" | |
| fi | |
| return 1 | |
| fi | |
| return 0 | |
| } | |
| function metrics-set-debug-logfile { | |
| _METRICS_DEBUG_LOG_FILE="$1" | |
| return 0 | |
| } | |
| function metrics-get-debug-logfile { | |
| if [[ -n "$_METRICS_DEBUG_LOG_FILE" ]]; then | |
| echo "$_METRICS_DEBUG_LOG_FILE" | |
| fi | |
| } | |
| function metrics-maybe-log { | |
| local filename="$(metrics-get-debug-logfile)" | |
| if [[ -n "$filename" ]]; then | |
| if [[ ! -f "$filename" && -w $(dirname "$filename") ]]; then | |
| touch "$filename" | |
| fi | |
| if [[ -w "$filename" ]]; then | |
| TIMESTAMP="$(date +%Y%m%d_%H%M%S)" | |
| echo -n "${TIMESTAMP}:" >> "$filename" | |
| for i in "$@"; do | |
| if [[ "$i" =~ ^"--" ]]; then | |
| continue # Skip switches. | |
| fi | |
| # Space before $i is intentional. | |
| echo -n " $i" >> "$filename" | |
| done | |
| # Add a newline at the end. | |
| echo >> "$filename" | |
| fi | |
| fi | |
| } | |
| # Arguments: | |
| # - the name of the fx subcommand | |
| # - event action | |
| # - (optional) event label | |
| function track-subcommand-custom-event { | |
| local subcommand="$1" | |
| local event_action="$2" | |
| shift 2 | |
| local event_label="$*" | |
| # Only allow custom arguments to subcommands defined in # $_METRICS_ALLOWS_CUSTOM_REPORTING | |
| if ! __is_in "$subcommand" "${_METRICS_ALLOWS_CUSTOM_REPORTING[@]}"; then | |
| return 1 | |
| fi | |
| # Limit to the first 100 characters | |
| # The Analytics API supports up to 500 bytes, but it is likely that | |
| # anything larger than 100 characters is an invalid execution and/or not | |
| # what we want to track. | |
| event_label=${event_label:0:100} | |
| local hide_init_warning=1 | |
| metrics-read-and-validate $hide_init_warning | |
| if [[ $METRICS_ENABLED == 0 ]]; then | |
| return 0 | |
| fi | |
| analytics_args=( | |
| "t=event" \ | |
| "ec=fx_custom_${subcommand}" \ | |
| "ea=${event_action}" \ | |
| "el=${event_label}" \ | |
| ) | |
| _add-to-analytics-batch "${analytics_args[@]}" | |
| # Send any remaining hits. | |
| _send-analytics-batch | |
| return 0 | |
| } | |
| # Arguments: | |
| # - the name of the fx subcommand | |
| # - args of the subcommand | |
| function track-command-execution { | |
| local subcommand="$1" | |
| shift | |
| local args="$*" | |
| local hide_init_warning=0 | |
| if [[ "$subcommand" == "metrics" ]]; then | |
| hide_init_warning=1 | |
| fi | |
| metrics-read-and-validate $hide_init_warning | |
| if [[ $METRICS_ENABLED == 0 ]]; then | |
| return 0 | |
| fi | |
| if [[ "$subcommand" == "set" ]]; then | |
| # Add separate fx_set hits for packages | |
| _process-fx-set-command "$@" | |
| fi | |
| # Only track arguments to the subcommands in $_METRICS_TRACK_ALL_ARGS | |
| if ! __is_in "$subcommand" "${_METRICS_TRACK_ALL_ARGS[@]}"; then | |
| args="" | |
| else | |
| # Limit to the first 100 characters of arguments. | |
| # The Analytics API supports up to 500 bytes, but it is likely that | |
| # anything larger than 100 characters is an invalid execution and/or not | |
| # what we want to track. | |
| args=${args:0:100} | |
| fi | |
| analytics_args=( | |
| "t=event" \ | |
| "ec=fx" \ | |
| "ea=${subcommand}" \ | |
| "el=${args}" \ | |
| ) | |
| _add-to-analytics-batch "${analytics_args[@]}" | |
| # Send any remaining hits. | |
| _send-analytics-batch | |
| return 0 | |
| } | |
| # Arguments: | |
| # - args of `fx set` | |
| function _process-fx-set-command { | |
| local target="$1" | |
| shift | |
| while [[ $# -ne 0 ]]; do | |
| case $1 in | |
| --with) | |
| shift # remove "--with" | |
| _add-fx-set-hit "$target" "fx-with" "$1" | |
| ;; | |
| --with-base) | |
| shift # remove "--with-base" | |
| _add-fx-set-hit "$target" "fx-with-base" "$1" | |
| ;; | |
| *) | |
| ;; | |
| esac | |
| shift | |
| done | |
| } | |
| # Arguments: | |
| # - the product.board target for `fx set` | |
| # - category name, either "fx-with" or "fx-with-base" | |
| # - package(s) following "--with" or "--with-base" switch | |
| function _add-fx-set-hit { | |
| target="$1" | |
| category="$2" | |
| packages="$3" | |
| # Packages argument can be a comma-separated list. | |
| IFS=',' read -ra packages_parts <<< "$packages" | |
| for p in "${packages_parts[@]}"; do | |
| analytics_args=( | |
| "t=event" \ | |
| "ec=${category}" \ | |
| "ea=${p}" \ | |
| "el=${target}" \ | |
| ) | |
| _add-to-analytics-batch "${analytics_args[@]}" | |
| done | |
| } | |
| # Arguments: | |
| # - time taken to complete (milliseconds) | |
| # - exit status | |
| # - the name of the fx subcommand | |
| # - args of the subcommand | |
| function track-command-finished { | |
| timing=$1 | |
| exit_status=$2 | |
| subcommand=$3 | |
| shift 3 | |
| args="$*" | |
| metrics-read-config | |
| if [[ $METRICS_ENABLED == 0 ]] || ! __is_in "$subcommand" "${_METRICS_TRACK_RESULTS[@]}"; then | |
| return 0 | |
| fi | |
| # Only track arguments to the subcommands in $_METRICS_TRACK_ALL_ARGS | |
| if ! __is_in "$subcommand" "${_METRICS_TRACK_ALL_ARGS[@]}"; then | |
| args="" | |
| else | |
| # Limit to the first 100 characters of arguments. | |
| # The Analytics API supports up to 500 bytes, but it is likely that | |
| # anything larger than 100 characters is an invalid execution and/or not | |
| # what we want to track. | |
| args=${args:0:100} | |
| fi | |
| if [[ $exit_status == 0 ]]; then | |
| # Successes are logged as timing hits | |
| hit_type="timing" | |
| analytics_args=( | |
| "t=timing" \ | |
| "utc=fx" \ | |
| "utv=${subcommand}" \ | |
| "utt=${timing}" \ | |
| "utl=${args}" \ | |
| ) | |
| else | |
| # Failures are logged as event hits with a separate category | |
| # exit status is stored as Custom Dimension 1 | |
| hit_type="event" | |
| analytics_args=( | |
| "t=event" \ | |
| "ec=fx_exception" \ | |
| "ea=${subcommand}" \ | |
| "el=${args}" \ | |
| "cd1=${exit_status}" \ | |
| ) | |
| fi | |
| _add-to-analytics-batch "${analytics_args[@]}" | |
| # Send any remaining hits. | |
| _send-analytics-batch | |
| return 0 | |
| } | |
| # Add an analytics hit with the given args to the batch of hits. This will trigger | |
| # sending a batch when the batch size limit is hit. | |
| # | |
| # Arguments: | |
| # - analytics arguments, e.g. "t=event" "ec=fx" etc. | |
| function _add-to-analytics-batch { | |
| if [[ $# -eq 0 ]]; then | |
| return 0 | |
| fi | |
| if (( hit_count > 0 )); then | |
| # Each hit in a batch must be on its own line. The below will append a newline | |
| # without url-encoding it. Note that this does add a '&' to the end of each hit, | |
| # but those are ignored by Google Analytics. | |
| curl_args+=(--data-binary) | |
| curl_args+=($'\n') | |
| fi | |
| # All hits send some common parameters | |
| local app_name="$(_app_name)" | |
| local app_version="$(_app_version)" | |
| params=( | |
| "v=1" \ | |
| "tid=${_METRICS_GA_PROPERTY_ID}" \ | |
| "cid=${METRICS_UUID}" \ | |
| "an=${app_name}" \ | |
| "av=${app_version}" \ | |
| "$@" \ | |
| ) | |
| for p in "${params[@]}"; do | |
| curl_args+=(--data-urlencode) | |
| curl_args+=("$p") | |
| done | |
| : $(( hit_count += 1 )) | |
| if ((hit_count == BATCH_SIZE)); then | |
| _send-analytics-batch | |
| fi | |
| } | |
| # Sends the current batch of hits to the Analytics server. As a side effect, clears | |
| # the hit count and batch data. | |
| function _send-analytics-batch { | |
| if [[ $hit_count -eq 0 ]]; then | |
| return 0 | |
| fi | |
| local user_agent="Fuchsia-fx $(_os_data)" | |
| local url_path="/batch" | |
| local result="" | |
| if [[ $_METRICS_DEBUG == 1 && $_METRICS_USE_VALIDATION_SERVER == 1 ]]; then | |
| url_path="/debug/collect" | |
| # Validation server does not accept batches. Send just the first hit instead. | |
| local limit=0 | |
| for i in "${curl_args[@]}"; do | |
| if [[ "$i" == "--data-binary" ]]; then | |
| curl_args=("${curl_args[@]:0:$limit}") | |
| break | |
| fi | |
| : $(( limit += 1 )) | |
| done | |
| fi | |
| if [[ $_METRICS_DEBUG == 1 && $_METRICS_USE_VALIDATION_SERVER == 0 ]]; then | |
| # if testing and not using the validation server, always return 202 | |
| result="202" | |
| elif [[ $_METRICS_DEBUG == 0 || $_METRICS_USE_VALIDATION_SERVER == 1 ]]; then | |
| result=$(curl -s -o /dev/null -w "%{http_code}" "${curl_args[@]}" \ | |
| -H "User-Agent: $user_agent" \ | |
| "https://www.google-analytics.com/${url_path}") | |
| fi | |
| metrics-maybe-log "${curl_args[@]}" "RESULT=${result}" | |
| # Clear batch. | |
| hit_count=0 | |
| curl_args=() | |
| } | |
| function _os_data { | |
| if command -v uname >/dev/null 2>&1 ; then | |
| uname -rs | |
| else | |
| echo "Unknown" | |
| fi | |
| } | |
| function _app_name { | |
| if [[ -n "${BASH_VERSION}" ]]; then | |
| echo "bash" | |
| elif [[ -n "${ZSH_VERSION}" ]]; then | |
| echo "zsh" | |
| else | |
| echo "Unknown" | |
| fi | |
| } | |
| function _app_version { | |
| if [[ -n "${BASH_VERSION}" ]]; then | |
| echo "${BASH_VERSION}" | |
| elif [[ -n "${ZSH_VERSION}" ]]; then | |
| echo "${ZSH_VERSION}" | |
| else | |
| echo "Unknown" | |
| fi | |
| } | |
| # Args: | |
| # debug_log_file: string with a filename to save logs | |
| # use_validation_hit_server: | |
| # 0 do not hit any Analytics server (for local tests) | |
| # 1 use the Analytics validation Hit server (for integration tests) | |
| # config_file: string with a filename to save the config file. Defaults to | |
| # METRICS_CONFIG | |
| function _enable_testing { | |
| _METRICS_DEBUG_LOG_FILE="$1" | |
| _METRICS_USE_VALIDATION_SERVER=$2 | |
| if [[ $# -gt 2 ]]; then | |
| METRICS_CONFIG="$3" | |
| fi | |
| _METRICS_DEBUG=1 | |
| METRICS_UUID="TEST" | |
| } |