diff --git a/Library/Homebrew/.rubocop.yml b/Library/Homebrew/.rubocop.yml index 70a3193f3b..d63d82e1e0 100644 --- a/Library/Homebrew/.rubocop.yml +++ b/Library/Homebrew/.rubocop.yml @@ -41,7 +41,7 @@ Metrics/PerceivedComplexity: Metrics/MethodLength: Max: 232 Metrics/ModuleLength: - Max: 475 + Max: 480 Exclude: # TODO: extract more of the bottling logic - "dev-cmd/bottle.rb" diff --git a/Library/Homebrew/brew.sh b/Library/Homebrew/brew.sh index 123fc7fab4..601ced299c 100644 --- a/Library/Homebrew/brew.sh +++ b/Library/Homebrew/brew.sh @@ -651,6 +651,7 @@ export HOMEBREW_GIT export HOMEBREW_GIT_WARNING export HOMEBREW_MINIMUM_GIT_VERSION export HOMEBREW_LINUX_MINIMUM_GLIBC_VERSION +export HOMEBREW_PHYSICAL_PROCESSOR export HOMEBREW_PROCESSOR export HOMEBREW_PRODUCT export HOMEBREW_OS_VERSION diff --git a/Library/Homebrew/cask/installer.rb b/Library/Homebrew/cask/installer.rb index 30dac0afc6..ceb2c528e5 100644 --- a/Library/Homebrew/cask/installer.rb +++ b/Library/Homebrew/cask/installer.rb @@ -111,7 +111,7 @@ module Cask install_artifacts if @cask.tap&.should_report_analytics? - ::Utils::Analytics.report_event("cask_install", @cask.token, on_request: true) + ::Utils::Analytics.report_event(:cask_install, @cask.token, on_request: true) end purge_backed_up_versioned_files diff --git a/Library/Homebrew/env_config.rb b/Library/Homebrew/env_config.rb index 7b33ed4ac3..490352bc32 100644 --- a/Library/Homebrew/env_config.rb +++ b/Library/Homebrew/env_config.rb @@ -297,6 +297,11 @@ module Homebrew description: "If set, do not print any hints about changing Homebrew's behaviour with environment variables.", boolean: true, }, + HOMEBREW_NO_GOOGLE_ANALYTICS: { + description: "If set, do not send analytics to Google Analytics but allow sending to Homebrew's InfluxDB " \ + "analytics server. For more information, see: ", + boolean: true, + }, HOMEBREW_NO_GITHUB_API: { description: "If set, do not use the GitHub API, e.g. for searches or fetching relevant issues " \ "after a failed install.", diff --git a/Library/Homebrew/extend/os/mac/utils/analytics.rb b/Library/Homebrew/extend/os/mac/utils/analytics.rb index 6c96066994..4d104e6dce 100644 --- a/Library/Homebrew/extend/os/mac/utils/analytics.rb +++ b/Library/Homebrew/extend/os/mac/utils/analytics.rb @@ -6,22 +6,18 @@ module Utils class << self extend T::Sig - sig { params(verbose: T::Boolean).returns(String) } - def custom_prefix_label(verbose: false) - return generic_custom_prefix_label if Hardware::CPU.arm? + sig { returns(String) } + def custom_prefix_label_google + return generic_custom_prefix_label_google if Hardware::CPU.arm? "non-/usr/local" end - sig { params(verbose: T::Boolean).returns(String) } - def arch_label(verbose: false) - if Hardware::CPU.arm? - "ARM" - elsif Hardware::CPU.in_rosetta2? - "Rosetta" - else - "" - end + sig { returns(String) } + def arch_label_google + return "Rosetta" if Hardware::CPU.in_rosetta2? + + generic_arch_label_google end end end diff --git a/Library/Homebrew/formula_installer.rb b/Library/Homebrew/formula_installer.rb index 8965f08f7c..6b5bd8a471 100644 --- a/Library/Homebrew/formula_installer.rb +++ b/Library/Homebrew/formula_installer.rb @@ -416,7 +416,7 @@ class FormulaInstaller if formula.tap&.should_report_analytics? action = "#{formula.full_name} #{options}".strip - Utils::Analytics.report_event("install", action, on_request: installed_on_request?) + Utils::Analytics.report_event(:formula_install, action, on_request: installed_on_request?) end self.class.attempted << formula diff --git a/Library/Homebrew/global.rb b/Library/Homebrew/global.rb index 011ec9def1..75583b6254 100644 --- a/Library/Homebrew/global.rb +++ b/Library/Homebrew/global.rb @@ -48,6 +48,7 @@ HOMEBREW_WWW = "https://brew.sh" HOMEBREW_DOCS_WWW = "https://docs.brew.sh" HOMEBREW_SYSTEM = ENV.fetch("HOMEBREW_SYSTEM").freeze HOMEBREW_PROCESSOR = ENV.fetch("HOMEBREW_PROCESSOR").freeze +HOMEBREW_PHYSICAL_PROCESSOR = ENV.fetch("HOMEBREW_PHYSICAL_PROCESSOR").freeze HOMEBREW_BREWED_CURL_PATH = Pathname(ENV.fetch("HOMEBREW_BREWED_CURL_PATH")).freeze HOMEBREW_USER_AGENT_CURL = ENV.fetch("HOMEBREW_USER_AGENT_CURL").freeze diff --git a/Library/Homebrew/test/utils/analytics_spec.rb b/Library/Homebrew/test/utils/analytics_spec.rb index 3f1731d3e9..6fca5195b8 100644 --- a/Library/Homebrew/test/utils/analytics_spec.rb +++ b/Library/Homebrew/test/utils/analytics_spec.rb @@ -5,49 +5,93 @@ require "utils/analytics" require "formula_installer" describe Utils::Analytics do - describe "::os_arch_prefix_ci" do - context "when os_arch_prefix_ci is not set" do - before do - described_class.clear_os_arch_prefix_ci - end + describe "::label_google" do + before do + described_class.clear_additional_tags_cache + end - let(:ci) { ", CI" if ENV["CI"] } + let(:ci) { ", CI" if ENV["CI"] } - it "returns OS_VERSION and prefix when HOMEBREW_PREFIX is a custom prefix on intel" do - allow(Hardware::CPU).to receive(:type).and_return(:intel) - allow(Hardware::CPU).to receive(:in_rosetta2?).and_return(false) - allow(Homebrew).to receive(:default_prefix?).and_return(false) - expect(described_class.os_arch_prefix_ci).to have_key(:prefix) - expect(described_class.os_arch_prefix_ci[:prefix]).to eq described_class.custom_prefix_label - end + it "returns OS_VERSION and prefix when HOMEBREW_PREFIX is a custom prefix on intel" do + allow(Hardware::CPU).to receive(:type).and_return(:intel) + allow(Hardware::CPU).to receive(:in_rosetta2?).and_return(false) + allow(Homebrew).to receive(:default_prefix?).and_return(false) + expect(described_class.label_google).to include described_class.custom_prefix_label_google + end - it "returns OS_VERSION, ARM and prefix when HOMEBREW_PREFIX is a custom prefix on arm" do - allow(Hardware::CPU).to receive(:type).and_return(:arm) - allow(Hardware::CPU).to receive(:in_rosetta2?).and_return(false) - allow(Homebrew).to receive(:default_prefix?).and_return(false) - expect(described_class.os_arch_prefix_ci).to have_key(:arch) - expect(described_class.os_arch_prefix_ci[:arch]).to eq described_class.arch_label - expect(described_class.os_arch_prefix_ci).to have_key(:prefix) - expect(described_class.os_arch_prefix_ci[:prefix]).to eq described_class.custom_prefix_label - end + it "returns OS_VERSION, ARM and prefix when HOMEBREW_PREFIX is a custom prefix on arm" do + allow(Hardware::CPU).to receive(:type).and_return(:arm) + allow(Hardware::CPU).to receive(:in_rosetta2?).and_return(false) + allow(Homebrew).to receive(:default_prefix?).and_return(false) + expect(described_class.label_google).to include described_class.arch_label_google + expect(described_class.label_google).to include described_class.custom_prefix_label_google + end - it "returns OS_VERSION, Rosetta and prefix when HOMEBREW_PREFIX is a custom prefix on Rosetta", :needs_macos do - allow(Hardware::CPU).to receive(:type).and_return(:intel) - allow(Hardware::CPU).to receive(:in_rosetta2?).and_return(true) - allow(Homebrew).to receive(:default_prefix?).and_return(false) - expect(described_class.os_arch_prefix_ci).to have_key(:prefix) - expect(described_class.os_arch_prefix_ci[:prefix]).to eq described_class.custom_prefix_label - end + it "returns OS_VERSION, Rosetta and prefix when HOMEBREW_PREFIX is a custom prefix on Rosetta", :needs_macos do + allow(Hardware::CPU).to receive(:type).and_return(:intel) + allow(Hardware::CPU).to receive(:in_rosetta2?).and_return(true) + allow(Homebrew).to receive(:default_prefix?).and_return(false) + expect(described_class.label_google).to include described_class.custom_prefix_label_google + end - it "does not include prefix when HOMEBREW_PREFIX is the default prefix" do - allow(Homebrew).to receive(:default_prefix?).and_return(true) - expect(described_class.os_arch_prefix_ci).not_to have_key(:prefix) - end + it "does not include prefix when HOMEBREW_PREFIX is the default prefix" do + allow(Homebrew).to receive(:default_prefix?).and_return(true) + expect(described_class.label_google).not_to include HOMEBREW_PREFIX.to_s + end - it "includes CI when ENV['CI'] is set" do - ENV["CI"] = "true" - expect(described_class.os_arch_prefix_ci).to have_key(:ci) - end + it "includes CI when ENV['CI'] is set" do + ENV["CI"] = "1" + expect(described_class.label_google).to include "CI" + end + end + + describe "::additional_tags_influx" do + before do + described_class.clear_additional_tags_cache + end + + let(:ci) { ", CI" if ENV["CI"] } + + it "returns OS_VERSION and prefix when HOMEBREW_PREFIX is a custom prefix on intel" do + allow(Hardware::CPU).to receive(:type).and_return(:intel) + allow(Hardware::CPU).to receive(:in_rosetta2?).and_return(false) + allow(Homebrew).to receive(:default_prefix?).and_return(false) + expect(described_class.additional_tags_influx).to have_key(:prefix) + expect(described_class.additional_tags_influx[:prefix]).to eq "custom-prefix" + end + + it "returns OS_VERSION, ARM and prefix when HOMEBREW_PREFIX is a custom prefix on arm" do + allow(Hardware::CPU).to receive(:type).and_return(:arm) + allow(Hardware::CPU).to receive(:in_rosetta2?).and_return(false) + allow(Homebrew).to receive(:default_prefix?).and_return(false) + expect(described_class.additional_tags_influx).to have_key(:arch) + expect(described_class.additional_tags_influx[:arch]).to eq HOMEBREW_PHYSICAL_PROCESSOR + expect(described_class.additional_tags_influx).to have_key(:prefix) + expect(described_class.additional_tags_influx[:prefix]).to eq "custom-prefix" + end + + it "returns OS_VERSION, Rosetta and prefix when HOMEBREW_PREFIX is a custom prefix on Rosetta", :needs_macos do + allow(Hardware::CPU).to receive(:type).and_return(:intel) + allow(Hardware::CPU).to receive(:in_rosetta2?).and_return(true) + allow(Homebrew).to receive(:default_prefix?).and_return(false) + expect(described_class.additional_tags_influx).to have_key(:prefix) + expect(described_class.additional_tags_influx[:prefix]).to eq "custom-prefix" + end + + it "does not include prefix when HOMEBREW_PREFIX is the default prefix" do + allow(Homebrew).to receive(:default_prefix?).and_return(true) + expect(described_class.additional_tags_influx).to have_key(:prefix) + expect(described_class.additional_tags_influx[:prefix]).to eq HOMEBREW_PREFIX.to_s + end + + it "includes CI when ENV['CI'] is set" do + ENV["CI"] = "1" + expect(described_class.additional_tags_influx).to have_key(:ci) + end + + it "includes developer when ENV['HOMEBREW_DEVELOPER'] is set" do + ENV["HOMEBREW_DEVELOPER"] = "1" + expect(described_class.additional_tags_influx).to have_key(:developer) end end @@ -61,14 +105,14 @@ describe Utils::Analytics do ENV["HOMEBREW_NO_ANALYTICS"] = "true" expect(described_class).not_to receive(:report_google) expect(described_class).not_to receive(:report_influx) - described_class.report_event("install", action) + described_class.report_event(:install, action) end it "returns nil when HOMEBREW_NO_ANALYTICS_THIS_RUN is true" do ENV["HOMEBREW_NO_ANALYTICS_THIS_RUN"] = "true" expect(described_class).not_to receive(:report_google) expect(described_class).not_to receive(:report_influx) - described_class.report_event("install", action) + described_class.report_event(:install, action) end it "returns nil when HOMEBREW_ANALYTICS_DEBUG is true" do @@ -78,13 +122,14 @@ describe Utils::Analytics do expect(described_class).to receive(:report_google) expect(described_class).to receive(:report_influx) - described_class.report_event("install", action) + described_class.report_event(:install, action) end end it "passes to the influxdb and google methods" do ENV.delete("HOMEBREW_NO_ANALYTICS_THIS_RUN") ENV.delete("HOMEBREW_NO_ANALYTICS") + ENV.delete("HOMEBREW_DEVELOPER") ENV["HOMEBREW_ANALYTICS_DEBUG"] = "true" expect(described_class).to receive(:report_google).with(:event, hash_including(ea: action)).once expect(described_class).to receive(:report_influx).with(:install, "formula_name --head", false, @@ -95,6 +140,7 @@ describe Utils::Analytics do it "sends to google twice on request" do ENV.delete("HOMEBREW_NO_ANALYTICS_THIS_RUN") ENV.delete("HOMEBREW_NO_ANALYTICS") + ENV.delete("HOMEBREW_DEVELOPER") ENV["HOMEBREW_ANALYTICS_DEBUG"] = "true" expect(described_class).to receive(:report_google).with(:event, hash_including(ea: action, ec: :install)).once expect(described_class).to receive(:report_google).with(:event, @@ -117,6 +163,7 @@ describe Utils::Analytics do ENV.delete("HOMEBREW_NO_ANALYTICS") ENV["HOMEBREW_ANALYTICS_DEBUG"] = "true" ENV["HOMEBREW_ANALYTICS_ENABLE_INFLUX"] = "true" + ENV["HOMEBREW_DEVELOPER"] = "1" expect(described_class).to receive(:deferred_curl).once described_class.report_influx(:install, action, true, developer: true, CI: true) end diff --git a/Library/Homebrew/utils/analytics.rb b/Library/Homebrew/utils/analytics.rb index 3b0c395cc3..c429f8ecbe 100644 --- a/Library/Homebrew/utils/analytics.rb +++ b/Library/Homebrew/utils/analytics.rb @@ -20,15 +20,8 @@ module Utils INFLUX_TOKEN = "y2JZsgE7glWT9V-S-nElETLp8oyH9PGh9JVa-kCdOdp7mEHIOws4BtdjsIe3HHpCBty7IQHLnmh0prqK2vBj9A==" INFLUX_HOST = "europe-west1-1.gcp.cloud2.influxdata.com" - sig { params(type: T.any(String, Symbol), metadata: T::Hash[Symbol, T.untyped]).void } + sig { params(type: Symbol, metadata: T::Hash[Symbol, T.untyped]).void } def report_google(type, metadata = {}) - os = metadata[:el][:os] - arch = ", #{metadata[:el][:arch]}" if metadata[:el][:arch].present? - prefix = ", #{metadata[:el][:prefix]}" if metadata[:el][:prefix].present? - ci = ", CI" if metadata[:el][:CI] == true - - metadata[:el] = "#{os}#{arch}#{prefix}#{ci}" - analytics_ids = ENV.fetch("HOMEBREW_ANALYTICS_IDS", "").split(",") analytics_ids.each do |analytics_id| args = [] @@ -79,14 +72,14 @@ module Utils end sig { - params(category: T.any(String, Symbol), action: T.any(String, Symbol), on_request: T::Boolean, + params(measurement: Symbol, package_and_options: String, on_request: T::Boolean, additional_tags: T::Hash[Symbol, T.untyped]).void } - def report_influx(category, action, on_request, additional_tags = {}) + def report_influx(measurement, package_and_options, on_request, additional_tags = {}) return unless ENV["HOMEBREW_ANALYTICS_ENABLE_INFLUX"] # Append general information to device information - tags = additional_tags.merge(action: action, on_request: !on_request.nil?) + tags = additional_tags.merge(package_and_options: package_and_options, on_request: !on_request.nil?) .compact_blank .map { |k, v| "#{k}=#{v.to_s.sub(" ", "\\ ")}" } # convert to key/value parameters .join(",") @@ -96,7 +89,7 @@ module Utils "--header", "Content-Type: text/plain; charset=utf-8", "--header", "Accept: application/json", "--header", "Authorization: Token #{INFLUX_TOKEN}", - "--data-raw", "#{category},#{tags} count=1i #{Time.now.to_i}" + "--data-raw", "#{measurement},#{tags} count=1i #{Time.now.to_i}" ] url = "https://#{INFLUX_HOST}/api/v2/write?bucket=#{INFLUX_BUCKET}&precision=s" @@ -117,40 +110,76 @@ module Utils end end - sig { params(category: T.any(String, Symbol), action: String, on_request: T::Boolean).void } - def report_event(category, action, on_request: false) - return if not_this_run? - return if disabled? + sig { params(measurement: Symbol, package_and_options: String, on_request: T::Boolean).void } + def report_event(measurement, package_and_options, on_request: false) + report_google_event(measurement, package_and_options, on_request: on_request) + report_influx_event(measurement, package_and_options, on_request: on_request) + end - google_label = os_arch_prefix_ci(verbose: false) + sig { params(category: Symbol, action: String, on_request: T::Boolean).void } + def report_google_event(category, action, on_request: false) + return if not_this_run? || disabled? || Homebrew::EnvConfig.no_google_analytics? + + category = "install" if category == :formula_install report_google(:event, ec: category, ea: action, - el: google_label, + el: label_google, ev: nil) - if on_request - report_google(:event, - ec: :install_on_request, - ea: action, - el: google_label, - ev: nil) - end + return unless on_request - influx_additional_data = os_arch_prefix_ci(verbose: true) - report_influx(category, action, on_request, influx_additional_data) + report_google(:event, + ec: :install_on_request, + ea: action, + el: label_google, + ev: nil) end + sig { params(measurement: Symbol, package_and_options: String, on_request: T::Boolean).void } + def report_influx_event(measurement, package_and_options, on_request: false) + return if not_this_run? || disabled? + + report_influx(measurement, package_and_options, on_request, additional_tags_influx) + end + + sig { params(exception: Exception).void } def report_build_error(exception) + report_google_build_error(exception) + report_influx_error(exception) + end + + sig { params(exception: Exception).void } + def report_google_build_error(exception) + return if not_this_run? || disabled? + return unless exception.formula.tap return unless exception.formula.tap.should_report_analytics? - action = exception.formula.full_name - if (options = exception.options.to_a.map(&:to_s).join(" ").presence) - action = "#{action} #{options}".strip + formula_full_name = exception.formula.full_name + package_and_options = if (options = exception.options.to_a.map(&:to_s).join(" ").presence) + "#{formula_full_name} #{options}".strip + else + formula_full_name end - report_event("BuildError", action) + report_event("BuildError", package_and_options) + end + + sig { params(exception: Exception).void } + def report_influx_error(exception) + return if not_this_run? || disabled? + + return unless exception.formula.tap + return unless exception.formula.tap.should_report_analytics? + + formula_full_name = exception.formula.full_name + package_and_options = if (options = exception.options.to_a.map(&:to_s).join(" ").presence) + "#{formula_full_name} #{options}".strip + else + formula_full_name + end + report_event(:build_error, package_and_options) end def messages_displayed? @@ -280,45 +309,55 @@ module Utils nil end - sig { params(verbose: T::Boolean).returns(String) } - def custom_prefix_label(verbose: false) + sig { returns(String) } + def custom_prefix_label_google "custom-prefix" end - alias generic_custom_prefix_label custom_prefix_label + alias generic_custom_prefix_label_google custom_prefix_label_google - sig { params(verbose: T::Boolean).returns(String) } - def arch_label(verbose: false) + sig { returns(String) } + def arch_label_google if Hardware::CPU.arm? "ARM" else "" end end + alias generic_arch_label_google arch_label_google - def clear_os_arch_prefix_ci - return unless instance_variable_defined?(:@os_arch_prefix_ci) - - remove_instance_variable(:@os_arch_prefix_ci) + def clear_additional_tags_cache + remove_instance_variable(:@label_google) if instance_variable_defined?(:@label_google) + remove_instance_variable(:@additional_tags_influx) if instance_variable_defined?(:@additional_tags_influx) end - sig { params(verbose: T::Boolean).returns(T::Hash[Symbol, String]) } - def os_arch_prefix_ci(verbose: false) - @os_arch_prefix_ci ||= begin - data = { - os: OS_VERSION, - developer: Homebrew::EnvConfig.developer?, - version: HOMEBREW_VERSION, - system: HOMEBREW_SYSTEM, - ci: ENV["CI"].present?, - arch: arch_label(verbose: verbose), - prefix: custom_prefix_label(verbose: verbose), - } - unless verbose - data.delete(:arch) if data[:arch].blank? - data.delete(:prefix) if Homebrew.default_prefix? - end + sig { returns(String) } + def label_google + @label_google ||= begin + os = OS_VERSION + arch = ", #{arch_label_google}" if arch_label_google.present? + prefix = ", #{custom_prefix_label_google}" unless Homebrew.default_prefix? + ci = ", CI" if ENV["CI"] + "#{os}#{arch}#{prefix}#{ci}" + end + end - data + sig { returns(T::Hash[Symbol, String]) } + def additional_tags_influx + @additional_tags_influx ||= begin + version = HOMEBREW_VERSION.match(/^[\d.]+/)[0] + version = "#{version}-dev" if HOMEBREW_VERSION.include?("-") + prefix = Homebrew.default_prefix? ? HOMEBREW_PREFIX.to_s : "custom-prefix" + + { + version: version, + prefix: prefix, + default_prefix: Homebrew.default_prefix?, + ci: ENV["CI"].present?, + developer: Homebrew::EnvConfig.developer?, + arch: HOMEBREW_PHYSICAL_PROCESSOR, + os: HOMEBREW_SYSTEM, + os_name_and_version: OS_VERSION, + } end end diff --git a/docs/Analytics.md b/docs/Analytics.md index 558da53b59..de18d272bd 100644 --- a/docs/Analytics.md +++ b/docs/Analytics.md @@ -1,6 +1,6 @@ # Anonymous Aggregate User Behaviour Analytics -Homebrew gathers anonymous aggregate user behaviour analytics using Google Analytics. You will be notified the first time you run `brew update` or install Homebrew. Analytics are not enabled until after this notice is shown, to ensure that you can [opt out](Analytics.md#opting-out) without ever sending analytics data. +Homebrew gathers anonymous aggregate user behaviour analytics using Google Analytics (until our in-progress migration to our own InfluxDB). You will be notified the first time you run `brew update` or install Homebrew. Analytics are not enabled until after this notice is shown, to ensure that you can [opt out](Analytics.md#opting-out) without ever sending analytics data. ## Why? @@ -61,6 +61,12 @@ Homebrew analytics helps us maintainers and leaving it on is appreciated. Howeve export HOMEBREW_NO_ANALYTICS=1 ``` +If you are fine with analytics being sent to Homebrew's InfluxDB but not to Google Analytics, you can set: + +```sh +export HOMEBREW_NO_GOOGLE_ANALYTICS=1 +``` + Alternatively, this will prevent analytics from ever being sent: ```sh