2024-08-12 10:30:59 +01:00
|
|
|
# typed: true # rubocop:todo Sorbet/StrictSigil
|
2019-04-19 15:38:03 +09:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2020-11-23 10:14:18 +01:00
|
|
|
require "context"
|
2016-08-09 19:18:43 +01:00
|
|
|
require "erb"
|
2021-01-12 16:27:25 -05:00
|
|
|
require "settings"
|
2024-07-14 08:49:39 -04:00
|
|
|
require "extend/cachable"
|
2016-08-09 19:18:43 +01:00
|
|
|
|
2016-05-03 14:21:08 +01:00
|
|
|
module Utils
|
2020-08-19 07:43:40 +02:00
|
|
|
# Helper module for fetching and reporting analytics data.
|
2016-05-03 14:21:08 +01:00
|
|
|
module Analytics
|
2023-02-21 17:07:01 +00:00
|
|
|
INFLUX_BUCKET = "analytics"
|
2023-07-18 16:22:23 +01:00
|
|
|
INFLUX_TOKEN = "iVdsgJ_OjvTYGAA79gOfWlA_fX0QCuj4eYUNdb-qVUTrC3tp3JTWCADVNE9HxV0kp2ZjIK9tuthy_teX4szr9A=="
|
2023-07-10 20:47:19 +02:00
|
|
|
INFLUX_HOST = "https://eu-central-1-1.aws.cloud2.influxdata.com"
|
|
|
|
INFLUX_ORG = "d81a3e6d582d485f"
|
2023-02-21 17:07:01 +00:00
|
|
|
|
2024-03-31 18:38:03 -07:00
|
|
|
extend Cachable
|
|
|
|
|
2016-05-03 14:21:08 +01:00
|
|
|
class << self
|
2020-08-02 14:32:31 +02:00
|
|
|
include Context
|
|
|
|
|
2022-05-31 18:03:48 +02:00
|
|
|
sig {
|
2024-03-07 15:19:04 +00:00
|
|
|
params(measurement: Symbol,
|
|
|
|
tags: T::Hash[Symbol, T.any(T::Boolean, String)],
|
|
|
|
fields: T::Hash[Symbol, T.any(T::Boolean, String)]).void
|
2022-05-31 18:03:48 +02:00
|
|
|
}
|
2024-03-07 15:19:04 +00:00
|
|
|
def report_influx(measurement, tags, fields)
|
|
|
|
return if not_this_run? || disabled?
|
2023-03-20 15:26:47 +00:00
|
|
|
|
|
|
|
# Tags are always implicitly strings and must have low cardinality.
|
2024-03-07 15:19:04 +00:00
|
|
|
tags_string = tags.map { |k, v| "#{k}=#{v}" }
|
|
|
|
.join(",")
|
2023-03-20 15:26:47 +00:00
|
|
|
|
|
|
|
# Fields need explicitly wrapped with quotes and can have high cardinality.
|
2024-03-07 15:19:04 +00:00
|
|
|
fields_string = fields.compact
|
|
|
|
.map { |k, v| %Q(#{k}="#{v}") }
|
|
|
|
.join(",")
|
2022-05-31 18:03:48 +02:00
|
|
|
|
|
|
|
args = [
|
|
|
|
"--max-time", "3",
|
2023-03-20 15:26:47 +00:00
|
|
|
"--header", "Authorization: Token #{INFLUX_TOKEN}",
|
2022-05-31 18:03:48 +02:00
|
|
|
"--header", "Content-Type: text/plain; charset=utf-8",
|
|
|
|
"--header", "Accept: application/json",
|
2024-03-07 15:19:04 +00:00
|
|
|
"--data-binary", "#{measurement},#{tags_string} #{fields_string} #{Time.now.to_i}"
|
2022-05-31 18:03:48 +02:00
|
|
|
]
|
|
|
|
|
2023-03-20 15:26:47 +00:00
|
|
|
# Second precision is highest we can do and has the lowest performance cost.
|
2023-02-21 17:07:01 +00:00
|
|
|
url = "#{INFLUX_HOST}/api/v2/write?bucket=#{INFLUX_BUCKET}&precision=s"
|
2023-02-06 16:56:25 +01:00
|
|
|
deferred_curl(url, args)
|
2023-02-06 16:28:34 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
sig { params(url: String, args: T::Array[String]).void }
|
2023-02-06 16:56:25 +01:00
|
|
|
def deferred_curl(url, args)
|
2024-07-14 08:49:39 -04:00
|
|
|
require "utils/curl"
|
|
|
|
|
2023-02-06 16:28:34 +01:00
|
|
|
curl = Utils::Curl.curl_executable
|
2024-11-26 11:50:45 -08:00
|
|
|
args = Utils::Curl.curl_args(*args, "--silent", "--output", File::NULL, show_error: false)
|
2022-05-31 18:03:48 +02:00
|
|
|
if ENV["HOMEBREW_ANALYTICS_DEBUG"]
|
|
|
|
puts "#{curl} #{args.join(" ")} \"#{url}\""
|
|
|
|
puts Utils.popen_read(curl, *args, url)
|
|
|
|
else
|
2024-08-09 13:45:47 -07:00
|
|
|
pid = spawn curl, *args, url
|
2022-05-31 18:03:48 +02:00
|
|
|
Process.detach T.must(pid)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-03-20 15:26:47 +00:00
|
|
|
sig {
|
|
|
|
params(measurement: Symbol, package_name: String, tap_name: String,
|
|
|
|
on_request: T::Boolean, options: String).void
|
|
|
|
}
|
2024-03-07 15:19:04 +00:00
|
|
|
def report_package_event(measurement, package_name:, tap_name:, on_request: false, options: "")
|
2023-02-15 16:34:50 +00:00
|
|
|
return if not_this_run? || disabled?
|
|
|
|
|
2024-03-07 15:19:04 +00:00
|
|
|
# ensure on_request is a boolean
|
|
|
|
on_request = on_request ? true : false
|
2023-02-15 16:34:50 +00:00
|
|
|
|
2024-03-07 15:19:04 +00:00
|
|
|
# ensure options are removed (by `.compact` below) if empty
|
|
|
|
options = nil if options.blank?
|
|
|
|
|
|
|
|
# Tags must have low cardinality.
|
2024-03-07 16:20:20 +00:00
|
|
|
tags = default_package_tags.merge(on_request:)
|
2024-03-07 15:19:04 +00:00
|
|
|
|
|
|
|
# Fields can have high cardinality.
|
2024-03-07 16:20:20 +00:00
|
|
|
fields = default_package_fields.merge(package: package_name, tap_name:, options:)
|
2024-03-07 15:19:04 +00:00
|
|
|
.compact
|
|
|
|
|
|
|
|
report_influx(measurement, tags, fields)
|
2023-02-15 16:34:50 +00:00
|
|
|
end
|
|
|
|
|
2023-03-07 17:24:20 -08:00
|
|
|
sig { params(exception: BuildError).void }
|
2024-03-07 15:19:04 +00:00
|
|
|
def report_build_error(exception)
|
2023-02-15 16:34:50 +00:00
|
|
|
return if not_this_run? || disabled?
|
|
|
|
|
2023-03-20 15:26:47 +00:00
|
|
|
formula = exception.formula
|
|
|
|
return unless formula
|
2023-02-15 16:34:50 +00:00
|
|
|
|
2023-03-20 15:26:47 +00:00
|
|
|
tap = formula.tap
|
|
|
|
return unless tap
|
|
|
|
return unless tap.should_report_analytics?
|
|
|
|
|
2024-04-25 17:38:04 +01:00
|
|
|
options = exception.options.to_a.compact.map(&:to_s).sort.uniq.join(" ")
|
2024-03-07 16:20:20 +00:00
|
|
|
report_package_event(:build_error, package_name: formula.name, tap_name: tap.name, options:)
|
2016-05-03 14:21:08 +01:00
|
|
|
end
|
2019-11-22 09:08:31 +00:00
|
|
|
|
2024-04-25 17:38:04 +01:00
|
|
|
sig { params(command_instance: Homebrew::AbstractCommand).void }
|
|
|
|
def report_command_run(command_instance)
|
|
|
|
return if not_this_run? || disabled?
|
|
|
|
|
|
|
|
command = command_instance.class.command_name
|
|
|
|
|
|
|
|
options_array = command_instance.args.options_only.to_a.compact
|
|
|
|
|
|
|
|
# Strip out any flag values to reduce cardinality and preserve privacy.
|
2024-07-16 17:06:38 +01:00
|
|
|
options_array.map! { |option| option.sub(/=.*/m, "=") }
|
2024-07-14 13:15:09 -04:00
|
|
|
|
|
|
|
# Strip out --with-* and --without-* options
|
|
|
|
options_array.reject! { |option| option.match(/^--with(out)?-/) }
|
|
|
|
|
2024-04-25 17:38:04 +01:00
|
|
|
options = options_array.sort.uniq.join(" ")
|
|
|
|
|
|
|
|
# Tags must have low cardinality.
|
|
|
|
tags = {
|
|
|
|
command:,
|
|
|
|
ci: ENV["CI"].present?,
|
2024-07-14 11:50:57 -04:00
|
|
|
devcmdrun: Homebrew::EnvConfig.devcmdrun?,
|
2024-04-25 17:38:04 +01:00
|
|
|
developer: Homebrew::EnvConfig.developer?,
|
|
|
|
}
|
|
|
|
|
|
|
|
# Fields can have high cardinality.
|
|
|
|
fields = { options: }
|
|
|
|
|
|
|
|
report_influx(:command_run, tags, fields)
|
|
|
|
end
|
|
|
|
|
|
|
|
sig { params(step_command_short: String, passed: T::Boolean).void }
|
|
|
|
def report_test_bot_test(step_command_short, passed)
|
|
|
|
return if not_this_run? || disabled?
|
|
|
|
return if ENV["HOMEBREW_TEST_BOT_ANALYTICS"].blank?
|
|
|
|
|
|
|
|
# ensure passed is a boolean
|
|
|
|
passed = passed ? true : false
|
|
|
|
|
|
|
|
# Tags must have low cardinality.
|
|
|
|
tags = {
|
|
|
|
passed:,
|
|
|
|
arch: HOMEBREW_PHYSICAL_PROCESSOR,
|
|
|
|
os: HOMEBREW_SYSTEM,
|
|
|
|
}
|
|
|
|
|
2024-04-26 15:24:12 +01:00
|
|
|
# Strip out any flag values to reduce cardinality and preserve privacy.
|
|
|
|
# Sort options to ensure consistent ordering and improve readability.
|
2024-04-26 09:37:48 +01:00
|
|
|
command_and_package, options =
|
|
|
|
step_command_short.split
|
2024-04-26 15:24:12 +01:00
|
|
|
.map { |arg| arg.sub(/=.*/, "=") }
|
2024-04-26 09:37:48 +01:00
|
|
|
.partition { |arg| !arg.start_with?("-") }
|
2024-04-26 15:24:12 +01:00
|
|
|
command = (command_and_package + options.sort).join(" ")
|
2024-04-25 17:38:04 +01:00
|
|
|
|
|
|
|
# Fields can have high cardinality.
|
|
|
|
fields = { command: }
|
|
|
|
|
|
|
|
report_influx(:test_bot_test, tags, fields)
|
|
|
|
end
|
|
|
|
|
2023-07-18 08:03:57 +01:00
|
|
|
def influx_message_displayed?
|
|
|
|
config_true?(:influxanalyticsmessage)
|
|
|
|
end
|
|
|
|
|
2019-11-22 09:08:31 +00:00
|
|
|
def messages_displayed?
|
2023-07-17 19:26:10 +01:00
|
|
|
config_true?(:analyticsmessage) &&
|
|
|
|
config_true?(:caskanalyticsmessage) &&
|
2023-07-18 08:03:57 +01:00
|
|
|
influx_message_displayed?
|
2019-11-22 09:08:31 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def disabled?
|
2020-04-05 15:44:50 +01:00
|
|
|
return true if Homebrew::EnvConfig.no_analytics?
|
2019-11-22 09:08:31 +00:00
|
|
|
|
|
|
|
config_true?(:analyticsdisabled)
|
|
|
|
end
|
|
|
|
|
2020-02-22 17:13:51 +00:00
|
|
|
def not_this_run?
|
|
|
|
ENV["HOMEBREW_NO_ANALYTICS_THIS_RUN"].present?
|
|
|
|
end
|
|
|
|
|
2019-11-22 09:08:31 +00:00
|
|
|
def no_message_output?
|
|
|
|
# Used by Homebrew/install
|
|
|
|
ENV["HOMEBREW_NO_ANALYTICS_MESSAGE_OUTPUT"].present?
|
|
|
|
end
|
|
|
|
|
|
|
|
def messages_displayed!
|
2021-01-13 11:16:09 -05:00
|
|
|
Homebrew::Settings.write :analyticsmessage, true
|
|
|
|
Homebrew::Settings.write :caskanalyticsmessage, true
|
2023-07-17 19:26:10 +01:00
|
|
|
Homebrew::Settings.write :influxanalyticsmessage, true
|
2019-11-22 09:08:31 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def enable!
|
2021-01-13 11:16:09 -05:00
|
|
|
Homebrew::Settings.write :analyticsdisabled, false
|
2023-02-20 09:05:15 +00:00
|
|
|
delete_uuid!
|
2019-11-22 09:08:31 +00:00
|
|
|
messages_displayed!
|
|
|
|
end
|
|
|
|
|
|
|
|
def disable!
|
2021-01-13 11:16:09 -05:00
|
|
|
Homebrew::Settings.write :analyticsdisabled, true
|
2023-02-20 09:05:15 +00:00
|
|
|
delete_uuid!
|
2019-11-22 09:08:31 +00:00
|
|
|
end
|
|
|
|
|
2023-02-20 09:05:15 +00:00
|
|
|
def delete_uuid!
|
2021-01-13 11:16:09 -05:00
|
|
|
Homebrew::Settings.delete :analyticsuuid
|
2019-11-22 09:08:31 +00:00
|
|
|
end
|
|
|
|
|
2020-09-01 11:21:45 +01:00
|
|
|
def output(args:, filter: nil)
|
2024-07-14 08:49:39 -04:00
|
|
|
require "api"
|
|
|
|
|
2020-07-24 23:16:30 +02:00
|
|
|
days = args.days || "30"
|
|
|
|
category = args.category || "install"
|
2021-08-06 02:30:44 -04:00
|
|
|
begin
|
|
|
|
json = Homebrew::API::Analytics.fetch category, days
|
|
|
|
rescue ArgumentError
|
|
|
|
# Ignore failed API requests
|
|
|
|
return
|
|
|
|
end
|
2019-11-22 09:08:31 +00:00
|
|
|
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
|
2020-09-01 14:05:52 +01:00
|
|
|
next if filter.present? && key != filter && !key.start_with?("#{filter} ")
|
|
|
|
|
2019-11-22 09:08:31 +00:00
|
|
|
results[key] = item["count"].tr(",", "").to_i
|
|
|
|
end
|
|
|
|
|
|
|
|
if filter.present? && results.blank?
|
|
|
|
onoe "No results matching `#{filter}` found!"
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
2024-03-07 16:20:20 +00:00
|
|
|
table_output(category, days, results, os_version:, cask_install:)
|
2019-11-22 09:08:31 +00:00
|
|
|
end
|
|
|
|
|
2023-06-16 15:39:49 +01:00
|
|
|
def output_analytics(json, args:)
|
2020-08-02 14:32:31 +02:00
|
|
|
full_analytics = args.analytics? || verbose?
|
2019-11-22 09:08:31 +00:00
|
|
|
|
|
|
|
ohai "Analytics"
|
|
|
|
json["analytics"].each do |category, value|
|
|
|
|
category = category.tr("_", "-")
|
|
|
|
analytics = []
|
|
|
|
|
|
|
|
value.each do |days, results|
|
|
|
|
days = days.to_i
|
|
|
|
if full_analytics
|
2020-09-01 14:05:52 +01:00
|
|
|
next if args.days.present? && args.days&.to_i != days
|
|
|
|
next if args.category.present? && args.category != category
|
2019-11-22 09:08:31 +00:00
|
|
|
|
2019-11-22 10:39:16 +00:00
|
|
|
table_output(category, days, results)
|
2019-11-22 09:08:31 +00:00
|
|
|
else
|
|
|
|
total_count = results.values.inject("+")
|
|
|
|
analytics << "#{number_readable(total_count)} (#{days} days)"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
puts "#{category}: #{analytics.join(", ")}" unless full_analytics
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-06-16 15:39:49 +01:00
|
|
|
# This method is undocumented because it is not intended for general use.
|
|
|
|
# It relies on screen scraping some GitHub HTML that's not available as an API.
|
|
|
|
# This seems very likely to break in the future.
|
|
|
|
# That said, it's the only way to get the data we want right now.
|
|
|
|
def output_github_packages_downloads(formula, args:)
|
|
|
|
return unless args.github_packages_downloads?
|
|
|
|
return unless formula.core_formula?
|
|
|
|
|
2024-07-14 08:49:39 -04:00
|
|
|
require "utils/curl"
|
|
|
|
|
2023-06-16 15:39:49 +01:00
|
|
|
escaped_formula_name = GitHubPackages.image_formula_name(formula.name)
|
2023-07-26 16:46:23 +01:00
|
|
|
.gsub("/", "%2F")
|
2023-06-16 15:39:49 +01:00
|
|
|
formula_url_suffix = "container/core%2F#{escaped_formula_name}/"
|
|
|
|
formula_url = "https://github.com/Homebrew/homebrew-core/pkgs/#{formula_url_suffix}"
|
|
|
|
output = Utils::Curl.curl_output("--fail", formula_url)
|
|
|
|
return unless output.success?
|
|
|
|
|
|
|
|
formula_version_urls = output.stdout
|
|
|
|
.scan(%r{/orgs/Homebrew/packages/#{formula_url_suffix}\d+\?tag=[^"]+})
|
|
|
|
.map do |url|
|
Curl: use `typed: strict`
This upgrades `utils/curl.rb` to `typed: strict`, which requires
a number of changes to pass `brew typecheck`. The most
straightforward are adding type signatures to methods, adding type
annotations (e.g., `T.let`) to variables that need them, and ensuring
that methods always use the expected return type.
I had to refactor areas where we call a `Utils::Curl` method and use
array destructuring on a `SystemCommand::Result` return value
(e.g., `output, errors, status = curl_output(...)`), as Sorbet
doesn't understand implicit array conversion. As suggested by Markus,
I've switched these areas to use `#stdout`, `#stderr`, and `#status`.
This requires the use of an intermediate variable (`result`) in some
cases but this was a fairly straightforward substitution.
I also had to refactor how `Cask::URL::BlockDSL::PageWithURL` works.
It currently uses `page.extend PageWithURL` to add a `url` attribute
but this reworks it to subclass `SimpleDelegator` and use an
`initialize` method instead. This achieves the same goal but in a way
that Sorbet can understand.
2025-01-10 21:37:20 -05:00
|
|
|
T.cast(url, String).sub("/orgs/Homebrew/packages/", "/Homebrew/homebrew-core/pkgs/")
|
2023-06-16 15:39:49 +01:00
|
|
|
end
|
|
|
|
return if formula_version_urls.empty?
|
|
|
|
|
|
|
|
thirty_day_download_count = 0
|
|
|
|
formula_version_urls.each do |formula_version_url_suffix|
|
|
|
|
formula_version_url = "https://github.com#{formula_version_url_suffix}"
|
|
|
|
output = Utils::Curl.curl_output("--fail", formula_version_url)
|
|
|
|
next unless output.success?
|
|
|
|
|
|
|
|
last_thirty_days_match = output.stdout.match(
|
|
|
|
%r{<span class="[\s\-a-z]*">Last 30 days</span>\s*<span class="[\s\-a-z]*">([\d.M,]+)</span>}m,
|
|
|
|
)
|
|
|
|
next if last_thirty_days_match.blank?
|
|
|
|
|
Curl: use `typed: strict`
This upgrades `utils/curl.rb` to `typed: strict`, which requires
a number of changes to pass `brew typecheck`. The most
straightforward are adding type signatures to methods, adding type
annotations (e.g., `T.let`) to variables that need them, and ensuring
that methods always use the expected return type.
I had to refactor areas where we call a `Utils::Curl` method and use
array destructuring on a `SystemCommand::Result` return value
(e.g., `output, errors, status = curl_output(...)`), as Sorbet
doesn't understand implicit array conversion. As suggested by Markus,
I've switched these areas to use `#stdout`, `#stderr`, and `#status`.
This requires the use of an intermediate variable (`result`) in some
cases but this was a fairly straightforward substitution.
I also had to refactor how `Cask::URL::BlockDSL::PageWithURL` works.
It currently uses `page.extend PageWithURL` to add a `url` attribute
but this reworks it to subclass `SimpleDelegator` and use an
`initialize` method instead. This achieves the same goal but in a way
that Sorbet can understand.
2025-01-10 21:37:20 -05:00
|
|
|
last_thirty_days_downloads = T.must(last_thirty_days_match.captures.first).tr(",", "")
|
2023-06-16 15:39:49 +01:00
|
|
|
thirty_day_download_count += if (millions_match = last_thirty_days_downloads.match(/(\d+\.\d+)M/).presence)
|
Curl: use `typed: strict`
This upgrades `utils/curl.rb` to `typed: strict`, which requires
a number of changes to pass `brew typecheck`. The most
straightforward are adding type signatures to methods, adding type
annotations (e.g., `T.let`) to variables that need them, and ensuring
that methods always use the expected return type.
I had to refactor areas where we call a `Utils::Curl` method and use
array destructuring on a `SystemCommand::Result` return value
(e.g., `output, errors, status = curl_output(...)`), as Sorbet
doesn't understand implicit array conversion. As suggested by Markus,
I've switched these areas to use `#stdout`, `#stderr`, and `#status`.
This requires the use of an intermediate variable (`result`) in some
cases but this was a fairly straightforward substitution.
I also had to refactor how `Cask::URL::BlockDSL::PageWithURL` works.
It currently uses `page.extend PageWithURL` to add a `url` attribute
but this reworks it to subclass `SimpleDelegator` and use an
`initialize` method instead. This achieves the same goal but in a way
that Sorbet can understand.
2025-01-10 21:37:20 -05:00
|
|
|
(millions_match.captures.first.to_f * 1_000_000).to_i
|
2023-06-16 15:39:49 +01:00
|
|
|
else
|
|
|
|
last_thirty_days_downloads.to_i
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
ohai "GitHub Packages Downloads"
|
|
|
|
puts "#{number_readable(thirty_day_download_count)} (30 days)"
|
|
|
|
end
|
|
|
|
|
2023-03-10 23:46:07 +00:00
|
|
|
def formula_output(formula, args:)
|
2021-08-06 02:30:44 -04:00
|
|
|
return if Homebrew::EnvConfig.no_analytics? || Homebrew::EnvConfig.no_github_api?
|
|
|
|
|
2024-07-14 08:49:39 -04:00
|
|
|
require "api"
|
|
|
|
|
2023-03-10 23:46:07 +00:00
|
|
|
json = Homebrew::API::Formula.fetch formula.name
|
2020-05-17 01:38:11 +05:30
|
|
|
return if json.blank? || json["analytics"].blank?
|
|
|
|
|
2024-03-07 16:20:20 +00:00
|
|
|
output_analytics(json, args:)
|
|
|
|
output_github_packages_downloads(formula, args:)
|
2021-08-06 02:30:44 -04:00
|
|
|
rescue ArgumentError
|
|
|
|
# Ignore failed API requests
|
|
|
|
nil
|
2020-05-17 01:38:11 +05:30
|
|
|
end
|
|
|
|
|
2020-07-24 23:16:30 +02:00
|
|
|
def cask_output(cask, args:)
|
2021-08-06 02:30:44 -04:00
|
|
|
return if Homebrew::EnvConfig.no_analytics? || Homebrew::EnvConfig.no_github_api?
|
|
|
|
|
2024-07-14 08:49:39 -04:00
|
|
|
require "api"
|
|
|
|
|
2021-08-09 18:39:49 -04:00
|
|
|
json = Homebrew::API::Cask.fetch cask.token
|
2020-05-17 01:38:11 +05:30
|
|
|
return if json.blank? || json["analytics"].blank?
|
|
|
|
|
2024-03-07 16:20:20 +00:00
|
|
|
output_analytics(json, args:)
|
2021-08-06 02:30:44 -04:00
|
|
|
rescue ArgumentError
|
|
|
|
# Ignore failed API requests
|
|
|
|
nil
|
2020-05-17 01:38:11 +05:30
|
|
|
end
|
2019-11-22 09:08:31 +00:00
|
|
|
|
2023-02-15 16:34:50 +00:00
|
|
|
sig { returns(T::Hash[Symbol, String]) }
|
2024-03-07 15:19:04 +00:00
|
|
|
def default_package_tags
|
2024-03-31 18:38:03 -07:00
|
|
|
cache[:default_package_tags] ||= begin
|
2023-03-20 15:26:47 +00:00
|
|
|
# Only display default prefixes to reduce cardinality and improve privacy
|
|
|
|
prefix = Homebrew.default_prefix? ? HOMEBREW_PREFIX.to_s : "custom-prefix"
|
|
|
|
|
|
|
|
# Tags are always strings and must have low cardinality.
|
|
|
|
{
|
|
|
|
ci: ENV["CI"].present?,
|
2024-03-07 16:20:20 +00:00
|
|
|
prefix:,
|
2023-03-20 15:26:47 +00:00
|
|
|
default_prefix: Homebrew.default_prefix?,
|
|
|
|
developer: Homebrew::EnvConfig.developer?,
|
2024-07-14 11:50:57 -04:00
|
|
|
devcmdrun: Homebrew::EnvConfig.devcmdrun?,
|
2023-03-20 15:26:47 +00:00
|
|
|
arch: HOMEBREW_PHYSICAL_PROCESSOR,
|
|
|
|
os: HOMEBREW_SYSTEM,
|
|
|
|
}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# remove os_version starting with " or number
|
|
|
|
# remove macOS patch release
|
|
|
|
sig { returns(T::Hash[Symbol, String]) }
|
2024-03-07 15:19:04 +00:00
|
|
|
def default_package_fields
|
2024-03-31 18:38:03 -07:00
|
|
|
cache[:default_package_fields] ||= begin
|
2023-11-26 02:48:18 +00:00
|
|
|
version = if (match_data = HOMEBREW_VERSION.match(/^[\d.]+/))
|
|
|
|
suffix = "-dev" if HOMEBREW_VERSION.include?("-")
|
|
|
|
match_data[0] + suffix.to_s
|
|
|
|
else
|
|
|
|
">=4.1.22"
|
|
|
|
end
|
2023-02-15 16:34:50 +00:00
|
|
|
|
2023-03-20 15:26:47 +00:00
|
|
|
# Only include OS versions with an actual name.
|
|
|
|
os_name_and_version = if (os_version = OS_VERSION.presence) && os_version.downcase.match?(/^[a-z]/)
|
|
|
|
os_version
|
|
|
|
end
|
2023-02-16 13:15:04 +00:00
|
|
|
|
2023-02-15 16:34:50 +00:00
|
|
|
{
|
2024-03-07 16:20:20 +00:00
|
|
|
version:,
|
|
|
|
os_name_and_version:,
|
2022-05-31 18:03:48 +02:00
|
|
|
}
|
2019-11-22 09:08:31 +00:00
|
|
|
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
|
2022-06-28 10:09:59 +01:00
|
|
|
puts "#{formatted_index_header} | #{formatted_name_with_options_header} | " \
|
2021-07-06 23:44:09 +05:30
|
|
|
"#{formatted_count_header} | #{formatted_percent_header}"
|
2019-11-22 09:08:31 +00:00
|
|
|
|
2022-06-28 10:09:59 +01:00
|
|
|
columns_line = "#{"-"*index_width}:|-#{"-"*name_with_options_width}-|-" \
|
2021-07-06 23:44:09 +05:30
|
|
|
"#{"-"*count_width}:|-#{"-"*percent_width}:"
|
2019-11-22 09:08:31 +00:00
|
|
|
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} | " \
|
2021-07-06 23:44:09 +05:30
|
|
|
"#{formatted_count} | #{formatted_percent}%"
|
2019-11-22 09:08:31 +00:00
|
|
|
next if index > 10
|
|
|
|
end
|
2023-04-18 15:06:50 -07:00
|
|
|
return if results.length <= 1
|
2019-11-22 09:08:31 +00:00
|
|
|
|
|
|
|
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
|
2022-06-28 10:09:59 +01:00
|
|
|
puts "#{formatted_total_footer} | #{formatted_blank_footer} | " \
|
2021-07-06 23:44:09 +05:30
|
|
|
"#{formatted_total_count_footer} | #{formatted_total_percent_footer}%"
|
2019-11-22 09:08:31 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def config_true?(key)
|
2021-01-13 11:16:09 -05:00
|
|
|
Homebrew::Settings.read(key) == "true"
|
2019-11-22 09:08:31 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def format_count(count)
|
|
|
|
count.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
|
|
|
|
end
|
|
|
|
|
|
|
|
def format_percent(percent)
|
2024-03-07 16:20:20 +00:00
|
|
|
format("%<percent>.2f", percent:)
|
2019-11-22 09:08:31 +00:00
|
|
|
end
|
2016-05-03 14:21:08 +01:00
|
|
|
end
|
|
|
|
end
|
2016-03-28 09:16:40 +01:00
|
|
|
end
|