mirror of
https://github.com/Homebrew/brew.git
synced 2025-07-14 16:09:03 +08:00
364 lines
11 KiB
Ruby
364 lines
11 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "erb"
|
|
|
|
module Utils
|
|
# Helper module for fetching and reporting analytics data.
|
|
#
|
|
# @api private
|
|
module Analytics
|
|
class << self
|
|
include Context
|
|
|
|
def report(type, metadata = {})
|
|
return if not_this_run?
|
|
return if disabled?
|
|
|
|
args = []
|
|
|
|
# do not load .curlrc unless requested (must be the first argument)
|
|
args << "--disable" unless Homebrew::EnvConfig.curlrc?
|
|
|
|
args += %W[
|
|
--max-time 3
|
|
--user-agent #{HOMEBREW_USER_AGENT_CURL}
|
|
--data v=1
|
|
--data aip=1
|
|
--data t=#{type}
|
|
--data tid=#{ENV["HOMEBREW_ANALYTICS_ID"]}
|
|
--data cid=#{ENV["HOMEBREW_ANALYTICS_USER_UUID"]}
|
|
--data an=#{HOMEBREW_PRODUCT}
|
|
--data av=#{HOMEBREW_VERSION}
|
|
]
|
|
metadata.each do |key, value|
|
|
next unless key
|
|
next unless value
|
|
|
|
key = ERB::Util.url_encode key
|
|
value = ERB::Util.url_encode value
|
|
args << "--data" << "#{key}=#{value}"
|
|
end
|
|
|
|
# Send analytics. Don't send or store any personally identifiable information.
|
|
# https://docs.brew.sh/Analytics
|
|
# https://developers.google.com/analytics/devguides/collection/protocol/v1/devguide
|
|
# https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters
|
|
if ENV["HOMEBREW_ANALYTICS_DEBUG"]
|
|
url = "https://www.google-analytics.com/debug/collect"
|
|
puts "#{ENV["HOMEBREW_CURL"]} #{args.join(" ")} #{url}"
|
|
puts Utils.popen_read ENV["HOMEBREW_CURL"], *args, url
|
|
else
|
|
pid = fork do
|
|
exec ENV["HOMEBREW_CURL"],
|
|
*args,
|
|
"--silent", "--output", "/dev/null",
|
|
"https://www.google-analytics.com/collect"
|
|
end
|
|
Process.detach pid
|
|
end
|
|
end
|
|
|
|
def report_event(category, action, label = os_prefix_ci, value = nil)
|
|
report(:event,
|
|
ec: category,
|
|
ea: action,
|
|
el: label,
|
|
ev: value)
|
|
end
|
|
|
|
def report_build_error(exception)
|
|
return unless exception.formula.tap
|
|
return unless exception.formula.tap.installed?
|
|
return if exception.formula.tap.private?
|
|
|
|
action = exception.formula.full_name
|
|
if (options = exception.options.to_a.map(&:to_s).join(" ").presence)
|
|
action = "#{action} #{options}".strip
|
|
end
|
|
report_event("BuildError", action)
|
|
end
|
|
|
|
def messages_displayed?
|
|
config_true?(:analyticsmessage) && config_true?(:caskanalyticsmessage)
|
|
end
|
|
|
|
def disabled?
|
|
return true if Homebrew::EnvConfig.no_analytics?
|
|
|
|
config_true?(:analyticsdisabled)
|
|
end
|
|
|
|
def not_this_run?
|
|
ENV["HOMEBREW_NO_ANALYTICS_THIS_RUN"].present?
|
|
end
|
|
|
|
def no_message_output?
|
|
# Used by Homebrew/install
|
|
ENV["HOMEBREW_NO_ANALYTICS_MESSAGE_OUTPUT"].present?
|
|
end
|
|
|
|
def uuid
|
|
config_get(:analyticsuuid)
|
|
end
|
|
|
|
def messages_displayed!
|
|
config_set(:analyticsmessage, true)
|
|
config_set(:caskanalyticsmessage, true)
|
|
end
|
|
|
|
def enable!
|
|
config_set(:analyticsdisabled, false)
|
|
messages_displayed!
|
|
end
|
|
|
|
def disable!
|
|
config_set(:analyticsdisabled, true)
|
|
regenerate_uuid!
|
|
end
|
|
|
|
def regenerate_uuid!
|
|
# it will be regenerated in next run unless disabled.
|
|
config_delete(:analyticsuuid)
|
|
end
|
|
|
|
def output(filter: nil, args:)
|
|
days = args.days || "30"
|
|
category = args.category || "install"
|
|
json = formulae_brew_sh_json("analytics/#{category}/#{days}d.json")
|
|
return if json.blank? || json["items"].blank?
|
|
|
|
os_version = category == "os-version"
|
|
cask_install = category == "cask-install"
|
|
results = {}
|
|
json["items"].each do |item|
|
|
key = if os_version
|
|
item["os_version"]
|
|
elsif cask_install
|
|
item["cask"]
|
|
else
|
|
item["formula"]
|
|
end
|
|
if filter.present?
|
|
next if key != filter && !key.start_with?("#{filter} ")
|
|
end
|
|
results[key] = item["count"].tr(",", "").to_i
|
|
end
|
|
|
|
if filter.present? && results.blank?
|
|
onoe "No results matching `#{filter}` found!"
|
|
return
|
|
end
|
|
|
|
table_output(category, days, results, os_version: os_version, cask_install: cask_install)
|
|
end
|
|
|
|
def get_analytics(json, args:)
|
|
full_analytics = args.analytics? || verbose?
|
|
|
|
ohai "Analytics"
|
|
json["analytics"].each do |category, value|
|
|
category = category.tr("_", "-")
|
|
analytics = []
|
|
|
|
value.each do |days, results|
|
|
days = days.to_i
|
|
if full_analytics
|
|
if args.days.present?
|
|
next if args.days&.to_i != days
|
|
end
|
|
if args.category.present?
|
|
next if args.category != category
|
|
end
|
|
|
|
table_output(category, days, results)
|
|
else
|
|
total_count = results.values.inject("+")
|
|
analytics << "#{number_readable(total_count)} (#{days} days)"
|
|
end
|
|
end
|
|
|
|
puts "#{category}: #{analytics.join(", ")}" unless full_analytics
|
|
end
|
|
end
|
|
|
|
def formula_output(f, args:)
|
|
json = formulae_brew_sh_json("#{formula_path}/#{f}.json")
|
|
return if json.blank? || json["analytics"].blank?
|
|
|
|
get_analytics(json, args: args)
|
|
end
|
|
|
|
def cask_output(cask, args:)
|
|
json = formulae_brew_sh_json("#{cask_path}/#{cask}.json")
|
|
return if json.blank? || json["analytics"].blank?
|
|
|
|
get_analytics(json, args: args)
|
|
end
|
|
|
|
def custom_prefix_label
|
|
"custom-prefix"
|
|
end
|
|
|
|
def clear_os_prefix_ci
|
|
return unless instance_variable_defined?(:@os_prefix_ci)
|
|
|
|
remove_instance_variable(:@os_prefix_ci)
|
|
end
|
|
|
|
def os_prefix_ci
|
|
@os_prefix_ci ||= begin
|
|
os = OS_VERSION
|
|
prefix = ", #{custom_prefix_label}" unless Homebrew.default_prefix?
|
|
ci = ", CI" if ENV["CI"]
|
|
"#{os}#{prefix}#{ci}"
|
|
end
|
|
end
|
|
|
|
def table_output(category, days, results, os_version: false, cask_install: false)
|
|
oh1 "#{category} (#{days} days)"
|
|
total_count = results.values.inject("+")
|
|
formatted_total_count = format_count(total_count)
|
|
formatted_total_percent = format_percent(100)
|
|
|
|
index_header = "Index"
|
|
count_header = "Count"
|
|
percent_header = "Percent"
|
|
name_with_options_header = if os_version
|
|
"macOS Version"
|
|
elsif cask_install
|
|
"Token"
|
|
else
|
|
"Name (with options)"
|
|
end
|
|
|
|
total_index_footer = "Total"
|
|
max_index_width = results.length.to_s.length
|
|
index_width = [
|
|
index_header.length,
|
|
total_index_footer.length,
|
|
max_index_width,
|
|
].max
|
|
count_width = [
|
|
count_header.length,
|
|
formatted_total_count.length,
|
|
].max
|
|
percent_width = [
|
|
percent_header.length,
|
|
formatted_total_percent.length,
|
|
].max
|
|
name_with_options_width = Tty.width -
|
|
index_width -
|
|
count_width -
|
|
percent_width -
|
|
10 # spacing and lines
|
|
|
|
formatted_index_header =
|
|
format "%#{index_width}s", index_header
|
|
formatted_name_with_options_header =
|
|
format "%-#{name_with_options_width}s",
|
|
name_with_options_header[0..name_with_options_width-1]
|
|
formatted_count_header =
|
|
format "%#{count_width}s", count_header
|
|
formatted_percent_header =
|
|
format "%#{percent_width}s", percent_header
|
|
puts "#{formatted_index_header} | #{formatted_name_with_options_header} | "\
|
|
"#{formatted_count_header} | #{formatted_percent_header}"
|
|
|
|
columns_line = "#{"-"*index_width}:|-#{"-"*name_with_options_width}-|-"\
|
|
"#{"-"*count_width}:|-#{"-"*percent_width}:"
|
|
puts columns_line
|
|
|
|
index = 0
|
|
results.each do |name_with_options, count|
|
|
index += 1
|
|
formatted_index = format "%0#{max_index_width}d", index
|
|
formatted_index = format "%-#{index_width}s", formatted_index
|
|
formatted_name_with_options =
|
|
format "%-#{name_with_options_width}s",
|
|
name_with_options[0..name_with_options_width-1]
|
|
formatted_count = format "%#{count_width}s", format_count(count)
|
|
formatted_percent = if total_count.zero?
|
|
format "%#{percent_width}s", format_percent(0)
|
|
else
|
|
format "%#{percent_width}s",
|
|
format_percent((count.to_i * 100) / total_count.to_f)
|
|
end
|
|
puts "#{formatted_index} | #{formatted_name_with_options} | " \
|
|
"#{formatted_count} | #{formatted_percent}%"
|
|
next if index > 10
|
|
end
|
|
return unless results.length > 1
|
|
|
|
formatted_total_footer =
|
|
format "%-#{index_width}s", total_index_footer
|
|
formatted_blank_footer =
|
|
format "%-#{name_with_options_width}s", ""
|
|
formatted_total_count_footer =
|
|
format "%#{count_width}s", formatted_total_count
|
|
formatted_total_percent_footer =
|
|
format "%#{percent_width}s", formatted_total_percent
|
|
puts "#{formatted_total_footer} | #{formatted_blank_footer} | "\
|
|
"#{formatted_total_count_footer} | #{formatted_total_percent_footer}%"
|
|
end
|
|
|
|
def config_true?(key)
|
|
config_get(key) == "true"
|
|
end
|
|
|
|
def config_get(key)
|
|
HOMEBREW_REPOSITORY.cd do
|
|
Utils.popen_read("git", "config", "--get", "homebrew.#{key}").chomp
|
|
end
|
|
end
|
|
|
|
def config_set(key, value)
|
|
HOMEBREW_REPOSITORY.cd do
|
|
safe_system "git", "config", "--replace-all", "homebrew.#{key}", value.to_s
|
|
end
|
|
end
|
|
|
|
def config_delete(key)
|
|
HOMEBREW_REPOSITORY.cd do
|
|
system "git", "config", "--unset-all", "homebrew.#{key}"
|
|
end
|
|
end
|
|
|
|
def formulae_brew_sh_json(endpoint)
|
|
return if Homebrew::EnvConfig.no_analytics? || Homebrew::EnvConfig.no_github_api?
|
|
|
|
output, = curl_output("--max-time", "5",
|
|
"https://formulae.brew.sh/api/#{endpoint}")
|
|
return if output.blank?
|
|
|
|
JSON.parse(output)
|
|
rescue JSON::ParserError
|
|
nil
|
|
end
|
|
|
|
def format_count(count)
|
|
count.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
|
|
end
|
|
|
|
def format_percent(percent)
|
|
format("%<percent>.2f", percent: percent)
|
|
end
|
|
|
|
def formula_path
|
|
"formula"
|
|
end
|
|
alias generic_formula_path formula_path
|
|
|
|
def analytics_path
|
|
"analytics"
|
|
end
|
|
alias generic_analytics_path analytics_path
|
|
|
|
def cask_path
|
|
"cask"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
require "extend/os/utils/analytics"
|