| 
									
										
										
										
											2023-03-07 17:24:20 -08:00
										 |  |  | # typed: true | 
					
						
							| 
									
										
										
										
											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 | 
					
						
							| 
									
										
										
										
											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 | 
					
						
							|  |  |  |           pid = fork do | 
					
						
							|  |  |  |             exec curl, *args, "--silent", "--output", "/dev/null", url | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  |           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| | 
					
						
							|  |  |  |           url.sub("/orgs/Homebrew/packages/", "/Homebrew/homebrew-core/pkgs/") | 
					
						
							|  |  |  |         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? | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           last_thirty_days_downloads = last_thirty_days_match.captures.first.tr(",", "") | 
					
						
							|  |  |  |           thirty_day_download_count += if (millions_match = last_thirty_days_downloads.match(/(\d+\.\d+)M/).presence) | 
					
						
							|  |  |  |             millions_match.captures.first.to_i * 1_000_000
 | 
					
						
							|  |  |  |           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 |