diff --git a/.gitlab/ci/test.gitlab-ci.yml b/.gitlab/ci/test.gitlab-ci.yml index 0608f1504a9eba2b4b23157e949622d62a6b0f5b..bf7d10592c9b5872b73d5bfc2a95ba33c1545bb5 100644 --- a/.gitlab/ci/test.gitlab-ci.yml +++ b/.gitlab/ci/test.gitlab-ci.yml @@ -4,6 +4,15 @@ stage: test needs: [] +.ruby-job: + image: "ruby:${RUBY_VERSION}" + before_script: + - gem install gitlab-sdk + - gem install sentry-ruby + parallel: + matrix: + - RUBY_VERSION: ["3.0", "3.1", "3.2"] + docs-lint: extends: - .test-job @@ -16,26 +25,19 @@ rubocop: extends: - .test-job - .rules:code-changes - image: "ruby:${RUBY_VERSION}" + - .ruby-job script: - make rubocop - parallel: - matrix: - - RUBY_VERSION: ['3.0', '3.1', '3.2'] rspec: extends: - .test-job - .rules:code-changes - image: "ruby:${RUBY_VERSION}" + - .ruby-job variables: RSPEC_ARGS: "--format doc --format RspecJunitFormatter --out rspec.xml" script: - make rspec - cache: - key: "ruby-${RUBY_VERSION}-bundle" - paths: - - $BUNDLE_PATH artifacts: paths: - rspec.xml @@ -44,27 +46,24 @@ rspec: coverage_report: coverage_format: cobertura path: coverage/coverage.xml - parallel: - matrix: - - RUBY_VERSION: ['3.0', '3.1', '3.2'] shellcheck: extends: - .test-job - .rules:code-changes - image: "ruby:${RUBY_VERSION}" + - .ruby-job script: - apt-get update - make shellcheck - parallel: - matrix: - - RUBY_VERSION: ['3.0', '3.1', '3.2'] checkmake: extends: - .test-job - .rules:code-changes image: registry.gitlab.com/gitlab-org/gitlab-development-kit/asdf-bootstrapped-verify:main + before_script: + - gem install gitlab-sdk + - gem install sentry-ruby script: - make checkmake @@ -72,18 +71,15 @@ gdk-example-yml: extends: - .test-job - .rules:code-changes - image: "ruby:${RUBY_VERSION}" + - .ruby-job script: - make verify-gdk-example-yml - parallel: - matrix: - - RUBY_VERSION: ['3.0', '3.1', '3.2'] asdf-combine: extends: - .test-job - .rules:code-changes - image: "ruby:${RUBY_VERSION}" + - .ruby-job artifacts: when: on_failure paths: @@ -92,33 +88,24 @@ asdf-combine: - make verify-asdf-combine rules: - if: '$CI_PIPELINE_SOURCE == "schedule"' - - if: '$CI_MERGE_REQUEST_IID' + - if: "$CI_MERGE_REQUEST_IID" changes: - ".tool-versions*" - "support/asdf-combine" - "support/ci/verify-asdf-combine" - parallel: - matrix: - - RUBY_VERSION: ['3.0', '3.1', '3.2'] makefile-config: extends: - .test-job - .rules:code-changes - image: "ruby:${RUBY_VERSION}" + - .ruby-job script: - support/ci/verify-makefile-config - parallel: - matrix: - - RUBY_VERSION: ['3.0', '3.1', '3.2'] ruby-version: extends: - .test-job - .rules:ruby-version-changes - image: "ruby:${RUBY_VERSION}" + - .ruby-job script: - support/ruby-check-versions - parallel: - matrix: - - RUBY_VERSION: ['3.0', '3.1', '3.2'] diff --git a/Gemfile b/Gemfile index e5b92b598cd972a02363442d9b579ccc071cdbe7..52f1849cda38d6377480d4c69580f46767f8c2ad 100644 --- a/Gemfile +++ b/Gemfile @@ -23,3 +23,6 @@ group :development, :test, :danger do gem 'gitlab-dangerfiles', '~> 3.10.0', require: false gem 'resolv', '~> 0.2.2', require: false end + +gem 'gitlab-sdk', '~> 0.2.2' +gem 'sentry-ruby', '~> 5.11' diff --git a/Gemfile.lock b/Gemfile.lock index ab310f477a64934e272f39f2c5dde7e9f501fa20..55d8186800b9e29b06093a8a6de2514a16186c41 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -54,6 +54,10 @@ GEM danger (>= 8.4.5) danger-gitlab (>= 8.0.0) rake + gitlab-sdk (0.2.2) + activesupport (>= 5.2.0) + rake (~> 13.0) + snowplow-tracker (~> 0.8.0) gitlab-styles (10.0.0) rubocop (~> 1.43.0) rubocop-graphql (~> 0.18) @@ -151,6 +155,8 @@ GEM sawyer (0.9.2) addressable (>= 2.3.5) faraday (>= 0.17.3, < 3) + sentry-ruby (5.11.0) + concurrent-ruby (~> 1.0, >= 1.0.2) simplecov (0.21.2) docile (~> 1.1) simplecov-html (~> 0.11) @@ -160,6 +166,7 @@ GEM simplecov (~> 0.19) simplecov-html (0.12.3) simplecov_json_formatter (0.1.4) + snowplow-tracker (0.8.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) tzinfo (2.0.6) @@ -172,6 +179,7 @@ PLATFORMS DEPENDENCIES gitlab-dangerfiles (~> 3.10.0) + gitlab-sdk (~> 0.2.2) gitlab-styles (~> 10.0.0) irb (~> 1.7.0) lefthook (~> 1.4.1) @@ -182,6 +190,7 @@ DEPENDENCIES rspec_junit_formatter (~> 0.6.0) rubocop rubocop-rake (~> 0.6.0) + sentry-ruby (~> 5.11) simplecov-cobertura (~> 2.1.0) yard (~> 0.9.34) diff --git a/HELP b/HELP index 74a026582ab86dc10a498674f6e10fa2cc28086f..1518c99587a1bfa6a581c76a312880263dc74cfc 100644 --- a/HELP +++ b/HELP @@ -40,6 +40,8 @@ Manage GDK: gdk diff-config # Print difference between current # and new configuration values + gdk telemetry # Opt in or out of error tracking and analytic data collection + gdk reset-data # Back up and create fresh git repository, PostgreSQL # data and Rails upload directory gdk reset-praefect-data # Back up and create fresh Praefect PostgreSQL data diff --git a/data/announcements/0007_telemetry.yml b/data/announcements/0007_telemetry.yml new file mode 100644 index 0000000000000000000000000000000000000000..e3316a74c65b34a26d5132d921f6d612d25ecd20 --- /dev/null +++ b/data/announcements/0007_telemetry.yml @@ -0,0 +1,10 @@ +--- +header: GDK now supports error tracking and analytic data +body: | + To improve GDK, GitLab would like to collect basic error and usage + data to see how GDK is used and how it is performing. + + You can opt in to send error and analytic data to GitLab by running `gdk telemetry`. + + For more details, see + https://gitlab.com/groups/gitlab-org/-/epics/9255. diff --git a/doc/gdk_commands.md b/doc/gdk_commands.md index 40c7489c57ec4b45ec4f90d4c08827c97bccd6c8..0dfa6891f9e289ca81f51bc38fee52fc82089b0e 100644 --- a/doc/gdk_commands.md +++ b/doc/gdk_commands.md @@ -292,3 +292,15 @@ basic workflow in the repository. The reports are stored in `<gdk-root>/sitespeed-result` as `<branch>_YYYY-MM-DD-HH-MM-SS`. This requires Docker installed and running. + +## Toggle Telemetry + +```shell +gdk telemetry +``` + +Use the `gdk telemetry` command to enable and disable GDK telemetry. GDK telemetry can be: + +- Enabled, and associated with a GitLab username. +- Enabled anonymously. +- Disabled. diff --git a/gdk.example.yml b/gdk.example.yml index 9baeb86c067b4b4085573a0b32fd0963119f03d1..aee8ea969feb451065a8ecfca79ec3d3b3dbc642 100644 --- a/gdk.example.yml +++ b/gdk.example.yml @@ -428,6 +428,9 @@ sshd: use_gitlab_sshd: true user: git web_listen: 127.0.0.1:9122 +telemetry: + enabled: false + username: '' tracer: build_tags: tracer_static tracer_static_jaeger jaeger: diff --git a/gem/gitlab-development-kit.gemspec b/gem/gitlab-development-kit.gemspec index c1874d2a3734290d7c3e9342f75ad71672f19e9f..1441ee74002672e0316e9cf67b851fedc85f8410 100644 --- a/gem/gitlab-development-kit.gemspec +++ b/gem/gitlab-development-kit.gemspec @@ -17,6 +17,8 @@ Gem::Specification.new do |spec| spec.executables = ['gdk'] spec.required_ruby_version = '>= 3.0.5' + spec.add_dependency 'gitlab-sdk', '~> 0.2.2' spec.add_dependency 'rake', '~> 13.0' + spec.add_dependency 'sentry-ruby', '~> 5.11' spec.metadata['rubygems_mfa_required'] = 'true' end diff --git a/lib/gdk.rb b/lib/gdk.rb index 8e79ee3052fdbd359743637fc139c277d11cb757..2573ccef37b452f1acefa7ac77e48216a6b9004f 100644 --- a/lib/gdk.rb +++ b/lib/gdk.rb @@ -13,6 +13,7 @@ autoload :Asdf, 'asdf' autoload :Shellout, 'shellout' +autoload :Telemetry, 'telemetry' # GitLab Development Kit module GDK @@ -104,7 +105,7 @@ def self.main validate_yaml! unless SUBCOMMANDS_NOT_REQUIRING_YAML_VALIDATION.include?(subcommand) if ::GDK::Command::COMMANDS.key?(subcommand) - exit(::GDK::Command::COMMANDS[subcommand].call.new.run(ARGV)) + exit(run(subcommand)) else suggestions = DidYouMean::SpellChecker.new(dictionary: ::GDK::Command::COMMANDS.keys).correct(subcommand) message = ["#{subcommand} is not a GDK command"] @@ -151,15 +152,19 @@ def self.template_root def self.make(*targets, env: {}) sh = Shellout.new(MAKE, targets, chdir: GDK.root, env: env) sh.stream - sh.success? + sh end def self.validate_yaml! config.validate! nil rescue StandardError => e - GDK::Output.error("Your gdk.yml is invalid.\n\n") + GDK::Output.error("Your gdk.yml is invalid.\n\n", e) GDK::Output.puts(e.message, stderr: true) abort('') end + + def self.run(subcommand) + Telemetry.with_telemetry(subcommand) { ::GDK::Command::COMMANDS[subcommand].call.new.run(ARGV) } + end end diff --git a/lib/gdk/command.rb b/lib/gdk/command.rb index 8df3cd012cdcca8648b0670a3969432d20af6733..1c21fa494cac16ae2371028f71466d9d3026094a 100644 --- a/lib/gdk/command.rb +++ b/lib/gdk/command.rb @@ -18,6 +18,7 @@ module Command autoload :MeasureUrl, 'gdk/command/measure_url' autoload :MeasureWorkflow, 'gdk/command/measure_workflow' autoload :Open, 'gdk/command/open' + autoload :Telemetry, 'gdk/command/telemetry' autoload :Pristine, 'gdk/command/pristine' autoload :Psql, 'gdk/command/psql' autoload :PsqlGeo, 'gdk/command/psql_geo' @@ -57,6 +58,7 @@ module Command 'measure' => -> { GDK::Command::MeasureUrl }, 'measure-workflow' => -> { GDK::Command::MeasureWorkflow }, 'open' => -> { GDK::Command::Open }, + 'telemetry' => -> { GDK::Command::Telemetry }, 'psql' => -> { GDK::Command::Psql }, 'psql-geo' => -> { GDK::Command::PsqlGeo }, 'pristine' => -> { GDK::Command::Pristine }, diff --git a/lib/gdk/command/config.rb b/lib/gdk/command/config.rb index 0218cf902db471bbea3d8f3eef1103717fe88a95..ebddc855ecc8877f214fcf33fcb9c804197b8806 100644 --- a/lib/gdk/command/config.rb +++ b/lib/gdk/command/config.rb @@ -42,7 +42,7 @@ def config_get(*name) rescue GDK::ConfigSettings::SettingUndefined GDK::Output.abort("Cannot get config for #{name.join('.')}") rescue GDK::ConfigSettings::UnsupportedConfiguration => e - GDK::Output.abort("#{e.message}.") + GDK::Output.abort("#{e.message}.", e) end def config_set(slug, value) @@ -65,12 +65,12 @@ def config_set(slug, value) GDK::Output.info("Don't forget to run 'gdk reconfigure'.") true - rescue GDK::ConfigSettings::SettingUndefined - GDK::Output.abort("Cannot get config for '#{slug}'.") + rescue GDK::ConfigSettings::SettingUndefined => e + GDK::Output.abort("Cannot get config for '#{slug}'.", e) rescue TypeError => e - GDK::Output.abort(e.message) + GDK::Output.abort(e.message, e) rescue StandardError => e - GDK::Output.error(e.message) + GDK::Output.error(e.message, e) abort end end diff --git a/lib/gdk/command/install.rb b/lib/gdk/command/install.rb index 8e870fb4553c86a23c4ab0e02290b3a936b48e43..356bfb2c030df1c7f4ba5a54560f80a61ba50421 100644 --- a/lib/gdk/command/install.rb +++ b/lib/gdk/command/install.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require_relative '../../telemetry' + module GDK module Command # Handles `gdk install` command execution @@ -8,14 +10,23 @@ module Command # - gitlab_repo=<url to repository> (defaults to: "https://gitlab.com/gitlab-org/gitlab") class Install < BaseCommand def run(args = []) + args.each do |arg| + next unless arg.start_with?('telemetry_user=') + + username = arg.split('=').last + ::Telemetry.update_settings(username) + + break + end + result = GDK.make('install', *args) - unless result - GDK::Output.error('Failed to install.') + unless result.success? + GDK::Output.error('Failed to install.', result.stderr_str) display_help_message end - result + result.success? end end end diff --git a/lib/gdk/command/pristine.rb b/lib/gdk/command/pristine.rb index 6a83f9ae22b7dddd2ced41ac364796b0f92092bf..f960c63f0c0d81a4f14bfaacfa16043b489480b1 100644 --- a/lib/gdk/command/pristine.rb +++ b/lib/gdk/command/pristine.rb @@ -30,7 +30,7 @@ def run(_args = []) true rescue StandardError => e - GDK::Output.error("Failed to run 'gdk pristine' - #{e.message}.") + GDK::Output.error("Failed to run 'gdk pristine' - #{e.message}.", e) display_help_message false diff --git a/lib/gdk/command/reconfigure.rb b/lib/gdk/command/reconfigure.rb index cf3bf2ba51b1e20db849bd2785bb8b0edcd43113..c2875b37e125125e6b0e3b986faa4bdd61be7145 100644 --- a/lib/gdk/command/reconfigure.rb +++ b/lib/gdk/command/reconfigure.rb @@ -7,12 +7,12 @@ class Reconfigure < BaseCommand def run(_args = []) result = GDK.make('reconfigure') - unless result - GDK::Output.error('Failed to reconfigure.') + unless result.success? + GDK::Output.error('Failed to reconfigure.', result.stderr_str) display_help_message end - result + result.success? end end end diff --git a/lib/gdk/command/reset_data.rb b/lib/gdk/command/reset_data.rb index 45194970e6338f479108b63ad073758f13c90bd7..fc632264c7b6552c2cc490b33fd814f1a6423035 100644 --- a/lib/gdk/command/reset_data.rb +++ b/lib/gdk/command/reset_data.rb @@ -27,11 +27,13 @@ def stop_and_backup! end def reset_data! - if GDK.make('ensure-databases-setup', 'reconfigure') + result = GDK.make('ensure-databases-setup', 'reconfigure') + + if result.success? GDK::Output.notice('Successfully reset data!') GDK::Command::Start.new.run else - GDK::Output.error('Failed to reset data.') + GDK::Output.error('Failed to reset data.', result.stderr_str) display_help_message false @@ -61,7 +63,7 @@ def create_directory(directory) true rescue Errno::ENOENT => e - GDK::Output.error("Failed to create directory '#{directory}' - #{e}") + GDK::Output.error("Failed to create directory '#{directory}' - #{e}", e) false end @@ -78,7 +80,7 @@ def backup_path(message, *path) true rescue SystemCallError => e - GDK::Output.error("Failed to rename path '#{path}' to '#{path_to_backup}/' - #{e}") + GDK::Output.error("Failed to rename path '#{path}' to '#{path_to_backup}/' - #{e}", e) false end diff --git a/lib/gdk/command/telemetry.rb b/lib/gdk/command/telemetry.rb new file mode 100644 index 0000000000000000000000000000000000000000..24941f5d1cc42d2b4cd538561bdd0bd754253bea --- /dev/null +++ b/lib/gdk/command/telemetry.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require_relative '../../telemetry' + +module GDK + module Command + class Telemetry < BaseCommand + def run(_ = []) + puts <<~TEXT + To improve GDK, GitLab would like to collect basic error and usage data. Please choose one of the following options: + + - To send data to GitLab, enter your GitLab username. + - To send data to GitLab anonymously, leave blank. + - To avoid sending data to GitLab, enter a period ('.'). + TEXT + + username = $stdin.gets&.chomp + ::Telemetry.update_settings(username) + + puts \ + case username + when '.' + 'Error tracking and analytic data will not be collected.' + when '', NilClass + 'Error tracking and analytic data will now be collected anonymously.' + else + "Error tracking and analytic data will now be collected as '#{username}'." + end + + true + end + end + end +end diff --git a/lib/gdk/command/update.rb b/lib/gdk/command/update.rb index ce675d0de923628138f903f75742d5c9e38a5dc3..1c1f74c995fb3b4e431cf8b10f2f85afed69890d 100644 --- a/lib/gdk/command/update.rb +++ b/lib/gdk/command/update.rb @@ -25,15 +25,15 @@ def update! GDK::Hooks.with_hooks(config.gdk.update_hooks, 'gdk update') do if self_update? GDK.make('self-update') - GDK.make('self-update', 'update', env: update_env) + GDK.make('self-update', 'update', env: update_env).success? else - GDK.make('update', env: update_env) + GDK.make('update', env: update_env).success? end end end def reconfigure! - GDK.make('reconfigure-tasks') + GDK.make('reconfigure-tasks').success? end def self_update? diff --git a/lib/gdk/config.rb b/lib/gdk/config.rb index ac1764f3bcbb1d29e6748c4784bdb60e99cfa7c8..e37a5b908644f2761a405d7cb72dc28062f48d79 100644 --- a/lib/gdk/config.rb +++ b/lib/gdk/config.rb @@ -43,6 +43,11 @@ class Config < ConfigSettings string(:ca_path) { '' } end + settings :telemetry do + string(:username) { '' } + bool(:enabled) { false } + end + settings :repositories do string(:charts_gitlab) { 'https://gitlab.com/gitlab-org/charts/gitlab.git' } string(:gitaly) { 'https://gitlab.com/gitlab-org/gitaly.git' } diff --git a/lib/gdk/diagnostic/ruby_gems.rb b/lib/gdk/diagnostic/ruby_gems.rb index 0c96c1ad8dd2b0c6351ef34bc831273369bc6650..5350f7468678ec2c354b22e348138f45ab873edd 100644 --- a/lib/gdk/diagnostic/ruby_gems.rb +++ b/lib/gdk/diagnostic/ruby_gems.rb @@ -6,7 +6,7 @@ module GDK module Diagnostic class RubyGems < Base TITLE = 'Ruby Gems' - GITLAB_GEMS_TO_CHECK = %w[charlock_holmes ffi gpgme pg oj].freeze + GITLAB_GEMS_WITH_C_CODE_TO_CHECK = %w[charlock_holmes ffi gpgme pg oj].freeze def initialize(allow_gem_not_installed: false) @allow_gem_not_installed = allow_gem_not_installed @@ -31,7 +31,7 @@ def allow_gem_not_installed? end def failed_to_load_gitlab_gems - @failed_to_load_gitlab_gems ||= GITLAB_GEMS_TO_CHECK.reject { |name| gem_ok?(name) } + @failed_to_load_gitlab_gems ||= GITLAB_GEMS_WITH_C_CODE_TO_CHECK.reject { |name| gem_ok?(name) } end def gem_ok?(name) diff --git a/lib/gdk/erb_renderer.rb b/lib/gdk/erb_renderer.rb index a591e78b8daaf7d6eb1c14483df3fac197642f2b..c59e138862aaf1dfde1e586811c5cc04e84ca59e 100644 --- a/lib/gdk/erb_renderer.rb +++ b/lib/gdk/erb_renderer.rb @@ -34,7 +34,7 @@ def safe_render! FileUtils.mkdir_p(File.dirname(target)) # Ensure target's directory exists FileUtils.mv(temp_file.path, target) rescue GDK::ConfigSettings::UnsupportedConfiguration => e - GDK::Output.abort("#{e.message}.") + GDK::Output.abort("#{e.message}.", e) false ensure temp_file&.close @@ -49,7 +49,7 @@ def render!(target = @target) File.write(target, result) rescue GDK::ConfigSettings::UnsupportedConfiguration => e - GDK::Output.abort("#{e.message}.") + GDK::Output.abort("#{e.message}.", e) false end diff --git a/lib/gdk/hooks.rb b/lib/gdk/hooks.rb index 9dc78430602aecef7fe34b0fc604bcd74f329e6d..68aeb0a57dbe4b776ac6a16ff720d3cbbdb0dc63 100644 --- a/lib/gdk/hooks.rb +++ b/lib/gdk/hooks.rb @@ -29,7 +29,7 @@ def self.execute_hook_cmd(cmd, description) true rescue HookCommandError, Shellout::StreamCommandFailedError => e - GDK::Output.abort(e.message) + GDK::Output.abort(e.message, e) end end end diff --git a/lib/gdk/output.rb b/lib/gdk/output.rb index aabba8d469618621dcebf2a359c173485c3e8fae..5297f67e67077e683c748bb7ec4e92695cad16b3 100644 --- a/lib/gdk/output.rb +++ b/lib/gdk/output.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require_relative '../telemetry' + module GDK module Output COLOR_CODE_RED = '31' @@ -100,11 +102,15 @@ def format_error(message) icon(:error) + wrap_in_color('ERROR', COLOR_CODE_RED) + ": #{message}" end - def error(message) + def error(message, exception = nil) + Telemetry.capture_exception(exception || message) + puts(format_error(message), stderr: true) end - def abort(message) + def abort(message, exception = nil) + Telemetry.capture_exception(exception || message) + Kernel.abort(format_error(message)) end diff --git a/lib/gdk/postgresql_upgrader.rb b/lib/gdk/postgresql_upgrader.rb index a70981149c2fe38a41f2494414482acab05377f6..1b21561bef0fea610e8b23097306fadd4b044872 100644 --- a/lib/gdk/postgresql_upgrader.rb +++ b/lib/gdk/postgresql_upgrader.rb @@ -41,7 +41,7 @@ def upgrade! pg_replica_upgrade('replica_2') rescue StandardError => e success = false - GDK::Output.error "An error occurred: #{e}" + GDK::Output.error("An error occurred: #{e}", e) GDK::Output.warn 'Rolling back..' rename_current_data_dir_back end diff --git a/lib/gdk/project/git_worktree.rb b/lib/gdk/project/git_worktree.rb index 680d6de7d8923bac7a179fe7d56c5eb4d5b8982d..82075329a89d713758ccfdebb0a8e482e264eef4 100644 --- a/lib/gdk/project/git_worktree.rb +++ b/lib/gdk/project/git_worktree.rb @@ -18,7 +18,7 @@ def update sh = execute_command(fetch_cmd) unless sh.success? GDK::Output.puts(sh.read_stderr, stderr: true) - GDK::Output.error("Failed to fetch for '#{short_worktree_path}'") + GDK::Output.error("Failed to fetch for '#{short_worktree_path}'", sh.read_stderr) return false end @@ -58,7 +58,7 @@ def checkout_revision true else GDK::Output.puts(sh.read_stderr, stderr: true) - GDK::Output.error("Failed to fetch and check out '#{revision}' for '#{short_worktree_path}'") + GDK::Output.error("Failed to fetch and check out '#{revision}' for '#{short_worktree_path}'", sh.read_stderr) false end end @@ -72,7 +72,7 @@ def pull_ff_only true else GDK::Output.puts(sh.read_stderr, stderr: true) - GDK::Output.error("Failed to pull (--ff-only) for for '#{short_worktree_path}'") + GDK::Output.error("Failed to pull (--ff-only) for for '#{short_worktree_path}'", sh.read_stderr) false end end @@ -109,7 +109,7 @@ def rebase true else GDK::Output.puts(sh.read_stderr, stderr: true) - GDK::Output.error("Failed to rebase '#{default_branch}' on '#{current_branch_name}' for '#{short_worktree_path}'") + GDK::Output.error("Failed to rebase '#{default_branch}' on '#{current_branch_name}' for '#{short_worktree_path}'", sh.read_stderr) execute_command('git rebase --abort', display_output: false) false # Always send false as the initial 'git rebase' failed. end diff --git a/lib/shellout.rb b/lib/shellout.rb index eb1ce22173049af4b4d20cd2a1c304972825200f..47654e4f3ac4f60ad297b78bf2c530002288bbe5 100644 --- a/lib/shellout.rb +++ b/lib/shellout.rb @@ -5,7 +5,7 @@ # Controls execution of commands delegated to the running shell class Shellout - attr_reader :args, :env, :opts + attr_reader :args, :env, :opts, :stderr_str DEFAULT_EXECUTE_DISPLAY_OUTPUT = true DEFAULT_EXECUTE_RETRY_ATTEMPTS = 0 @@ -41,17 +41,17 @@ def execute(display_output: true, display_error: true, retry_attempts: DEFAULT_E end self - rescue StreamCommandFailedError, ExecuteCommandFailedError + rescue StreamCommandFailedError, ExecuteCommandFailedError => e error_message = "'#{command}' failed." if (retry_attempts -= 1).negative? - GDK::Output.error(error_message) if display_error + GDK::Output.error(error_message, e) if display_error self else retried = true error_message += " Retrying in #{retry_delay_secs} secs.." - GDK::Output.error(error_message) if display_error + GDK::Output.error(error_message, e) if display_error sleep(retry_delay_secs) retry diff --git a/lib/telemetry.rb b/lib/telemetry.rb new file mode 100644 index 0000000000000000000000000000000000000000..ee237aee4efe7967059e8ab760db0c15f0fc8afd --- /dev/null +++ b/lib/telemetry.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +autoload :FileUtils, 'fileutils' +autoload :GitlabSDK, 'gitlab-sdk' +autoload :Logger, 'logger' +autoload :Sentry, 'sentry-ruby' +autoload :SnowplowTracker, 'snowplow-tracker' + +module Telemetry + ANALYTICS_APP_ID = '35SLpKmD0ZB-K34dBAz9Tg' + ANALYTICS_BASE_URL = 'https://collector.prod-1.gl-product-analytics.com' + SENTRY_DSN = 'https://glet_1a56990d202783685f3708b129fde6c0@observe.gitlab.com:443/errortracking/api/v1/projects/48924931' + + def self.with_telemetry(command) + return yield unless telemetry_enabled? + + start = Process.clock_gettime(Process::CLOCK_MONOTONIC) + client.identify(GDK.config.telemetry.username) + client.track("Start #{command} #{ARGV}", {}) + + result = yield + + # This is tightly coupled to GDK commands which return false to indicate failure? + message = result ? 'Finish' : 'Failed' + duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start + client.track("#{message} #{command} #{ARGV}", { duration: duration }) + + result + end + + def self.client + return @client if @client + + app_id = ENV.fetch('GITLAB_SDK_APP_ID', ANALYTICS_APP_ID) + host = ENV.fetch('GITLAB_SDK_HOST', ANALYTICS_BASE_URL) + + SnowplowTracker::LOGGER.level = Logger::WARN + @client = GitlabSDK::Client.new(app_id: app_id, host: host) + end + + def self.init_sentry + Sentry.init do |config| + config.dsn = SENTRY_DSN + config.breadcrumbs_logger = [:sentry_logger] + config.traces_sample_rate = 1.0 + config.logger.level = Logger::WARN + end + end + + def self.capture_exception(message) + return unless telemetry_enabled? + + if message.is_a?(Exception) + exception = message + else + exception = StandardError.new(message) + exception.set_backtrace(caller) + end + + init_sentry + Sentry.capture_exception(exception) + end + + def self.telemetry_enabled? + GDK.config.telemetry.enabled + end + + def self.update_settings(username) + enabled = true + + if username == '.' + username = '' + enabled = false + end + + FileUtils.touch(GDK::Config::FILE) + GDK.config.bury!('telemetry.enabled', enabled) + GDK.config.bury!('telemetry.username', username) + GDK.config.save_yaml! + end +end diff --git a/spec/lib/gdk/command/install_spec.rb b/spec/lib/gdk/command/install_spec.rb index e5b0e64cb7c6e62e651390009bd70e7b1c3edd05..8a1b0dac61fb50f2a125fc034d81b0c16b735c98 100644 --- a/spec/lib/gdk/command/install_spec.rb +++ b/spec/lib/gdk/command/install_spec.rb @@ -4,16 +4,20 @@ let(:args) { [] } context 'when install fails' do + let(:sh) { instance_double(Shellout, success?: false, stderr_str: nil) } + it 'returns an error message' do - allow(GDK).to receive(:make).with('install') + allow(GDK).to receive(:make).with('install').and_return(sh) expect { subject.run(args) }.to output(/Failed to install/).to_stderr.and output(/You can try the following that may be of assistance/).to_stdout end end context 'when install succeeds' do + let(:sh) { instance_double(Shellout, success?: true) } + it 'finishes without problem' do - allow(GDK).to receive(:make).with('install').and_return('Some output') + allow(GDK).to receive(:make).with('install').and_return(sh) expect { subject.run(args) }.not_to raise_error end diff --git a/spec/lib/gdk/command/pristine_spec.rb b/spec/lib/gdk/command/pristine_spec.rb index e3e1e791265fd71fbc0f65eff8ac5b89f8cdd8b0..bae8eeca9d68eb0fcea7880a72ccbd49356874d7 100644 --- a/spec/lib/gdk/command/pristine_spec.rb +++ b/spec/lib/gdk/command/pristine_spec.rb @@ -14,7 +14,7 @@ it 'displays an error and returns false', :hide_stdout do expect(Runit).to receive(:stop).with(quiet: true).and_return(false) - expect(GDK::Output).to receive(:error).with("Failed to run 'gdk pristine' - Had an issue with 'gdk_stop'.") + expect(GDK::Output).to receive(:error).with("Failed to run 'gdk pristine' - Had an issue with 'gdk_stop'.", RuntimeError) expect(subject.run).to be(false) end diff --git a/spec/lib/gdk/command/reconfigure_spec.rb b/spec/lib/gdk/command/reconfigure_spec.rb index 7a38a24470557e50b9449b9a5154296ef0584365..ccca60199b9eaeb532f151cd26b08ec1787c6364 100644 --- a/spec/lib/gdk/command/reconfigure_spec.rb +++ b/spec/lib/gdk/command/reconfigure_spec.rb @@ -18,6 +18,7 @@ end def stub_make_reconfigure(success:) - expect(GDK).to receive(:make).with('reconfigure').and_return(success) + sh = instance_double(Shellout, success?: success, stderr_str: nil) + expect(GDK).to receive(:make).with('reconfigure').and_return(sh) end end diff --git a/spec/lib/gdk/command/reset_data_spec.rb b/spec/lib/gdk/command/reset_data_spec.rb index 0e90719f178aa4b3fcd6a94f309d7fe8237da8cf..04c7cecd3d4452ca1e4b75e8ed2f1f562ee5998c 100644 --- a/spec/lib/gdk/command/reset_data_spec.rb +++ b/spec/lib/gdk/command/reset_data_spec.rb @@ -57,7 +57,7 @@ stub_postgres_data_move allow(FileUtils).to receive(:mv).with(postgresql_data_directory, backup_postgresql_data_directory).and_raise(Errno::ENOENT) - expect(GDK::Output).to receive(:error).with("Failed to rename path '#{postgresql_data_directory}' to '#{backup_postgresql_data_directory}/' - No such file or directory") + expect(GDK::Output).to receive(:error).with("Failed to rename path '#{postgresql_data_directory}' to '#{backup_postgresql_data_directory}/' - No such file or directory", Errno::ENOENT) expect(GDK::Output).to receive(:error).with('Failed to backup data.') expect(subject).to receive(:display_help_message) expect(GDK).not_to receive(:make) @@ -80,9 +80,10 @@ travel_to(now) do stub_data_moves - expect(GDK).to receive(:make).with('ensure-databases-setup', 'reconfigure').and_return(false) + sh = instance_double(Shellout, success?: false, stderr_str: 'Error') + expect(GDK).to receive(:make).with('ensure-databases-setup', 'reconfigure').and_return(sh) - expect(GDK::Output).to receive(:error).with('Failed to reset data.') + expect(GDK::Output).to receive(:error).with('Failed to reset data.', 'Error') expect(GDK::Command::Start).not_to receive(:new) expect(subject).to receive(:display_help_message) @@ -96,7 +97,8 @@ travel_to(now) do stub_data_moves - expect(GDK).to receive(:make).with('ensure-databases-setup', 'reconfigure').and_return(true) + sh = instance_double(Shellout, success?: true) + expect(GDK).to receive(:make).with('ensure-databases-setup', 'reconfigure').and_return(sh) expect(GDK::Output).to receive(:notice).with("Moving PostgreSQL data from '#{postgresql_data_directory}' to '#{backup_postgresql_data_directory}/'") expect(GDK::Output).to receive(:notice).with("Moving redis dump.rdb from '#{redis_dump_rdb_path}' to '#{backup_redis_dump_rdb_path}/'") diff --git a/spec/lib/gdk/command/update_spec.rb b/spec/lib/gdk/command/update_spec.rb index e096fdbe2795c07352baa73a4370a69f9bd89cbf..3d33b4e47b0cddb2d1e1e83e6433f1520908a2cf 100644 --- a/spec/lib/gdk/command/update_spec.rb +++ b/spec/lib/gdk/command/update_spec.rb @@ -1,9 +1,11 @@ # frozen_string_literal: true RSpec.describe GDK::Command::Update do + let(:sh) { instance_double(Shellout, success?: true) } + before do allow(GDK::Hooks).to receive(:execute_hooks) - allow(GDK).to receive(:make) + allow(GDK).to receive(:make).and_return(sh) end describe '#run' do diff --git a/spec/lib/gdk/diagnostic/ruby_gems_spec.rb b/spec/lib/gdk/diagnostic/ruby_gems_spec.rb index 31fc9607e692e7b6f2e15d4d4fe24270d5c6dd13..e3a17b32c859c005c115d8e425dcd041f69e021f 100644 --- a/spec/lib/gdk/diagnostic/ruby_gems_spec.rb +++ b/spec/lib/gdk/diagnostic/ruby_gems_spec.rb @@ -6,7 +6,7 @@ subject(:diagnostic) { described_class.new(allow_gem_not_installed: allow_gem_not_installed) } before do - stub_const('GDK::Diagnostic::RubyGems::GITLAB_GEMS_TO_CHECK', %w[bad_gem]) + stub_const('GDK::Diagnostic::RubyGems::GITLAB_GEMS_WITH_C_CODE_TO_CHECK', %w[bad_gem]) end describe '#success?' do diff --git a/spec/lib/gdk/project/git_worktree_spec.rb b/spec/lib/gdk/project/git_worktree_spec.rb index 693742287098eea3d74931e8d30135420f51c489..5c0f4b32f09cd7cb50fc5b4f27e1e2aff446a760 100644 --- a/spec/lib/gdk/project/git_worktree_spec.rb +++ b/spec/lib/gdk/project/git_worktree_spec.rb @@ -32,7 +32,7 @@ it 'fetch fails, but stash pops' do expect_update(stash_result: stash_saved_something, fetch_success: false, shallow_clone: shallow_clone) expect(GDK::Output).to receive(:puts).with("fetch_success: false", stderr: true) - expect(GDK::Output).to receive(:error).with("Failed to fetch for '#{short_worktree_path}'") + expect(GDK::Output).to receive(:error).with("Failed to fetch for '#{short_worktree_path}'", 'fetch_success: false') expect_shellout('git stash pop') expect(subject.update).to be_falsey end @@ -71,7 +71,7 @@ it 'fetch fails, but stash pops' do expect_update(stash_result: stash_saved_something, fetch_success: false, shallow_clone: shallow_clone) expect(GDK::Output).to receive(:puts).with("fetch_success: false", stderr: true) - expect(GDK::Output).to receive(:error).with("Failed to fetch for '#{short_worktree_path}'") + expect(GDK::Output).to receive(:error).with("Failed to fetch for '#{short_worktree_path}'", "fetch_success: false") expect_shellout('git stash pop') expect(subject.update).to be_falsey end @@ -144,7 +144,7 @@ def expect_auto_rebase(rebase_success = true) expect(GDK::Output).to receive(:success).with("Successfully fetched and rebased '#{default_branch}' on '#{current_branch_name}' for '#{short_worktree_path}'") else expect(GDK::Output).to receive(:puts).with(stderr, stderr: true) - expect(GDK::Output).to receive(:error).with("Failed to rebase '#{default_branch}' on '#{current_branch_name}' for '#{short_worktree_path}'") + expect(GDK::Output).to receive(:error).with("Failed to rebase '#{default_branch}' on '#{current_branch_name}' for '#{short_worktree_path}'", stderr) end end @@ -163,12 +163,12 @@ def expect_checkout_and_pull(checkout_success: true, pull_success: true) expect(GDK::Output).to receive(:success).with("Successfully pulled (--ff-only) for '#{short_worktree_path}'") else expect(GDK::Output).to receive(:puts).with(pull_stderr, stderr: true) - expect(GDK::Output).to receive(:error).with("Failed to pull (--ff-only) for for '#{short_worktree_path}'") + expect(GDK::Output).to receive(:error).with("Failed to pull (--ff-only) for for '#{short_worktree_path}'", pull_stderr) end end else expect(GDK::Output).to receive(:puts).with(checkout_stderr, stderr: true) - expect(GDK::Output).to receive(:error).with("Failed to fetch and check out '#{revision}' for '#{short_worktree_path}'") + expect(GDK::Output).to receive(:error).with("Failed to fetch and check out '#{revision}' for '#{short_worktree_path}'", checkout_stderr) end end @@ -181,7 +181,7 @@ def expect_just_checkout(checkout_success = true) expect(GDK::Output).to receive(:success).with("Successfully fetched and checked out '#{revision}' for '#{short_worktree_path}'") else expect(GDK::Output).to receive(:puts).with(checkout_stderr, stderr: true) - expect(GDK::Output).to receive(:error).with("Failed to fetch and check out '#{revision}' for '#{short_worktree_path}'") + expect(GDK::Output).to receive(:error).with("Failed to fetch and check out '#{revision}' for '#{short_worktree_path}'", checkout_stderr) end end diff --git a/spec/lib/gdk/telemetry_spec.rb b/spec/lib/gdk/telemetry_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..c618bce0dffe8fbb8c7dec25b692e92268667691 --- /dev/null +++ b/spec/lib/gdk/telemetry_spec.rb @@ -0,0 +1,184 @@ +# frozen_string_literal: true + +require 'fileutils' +require 'gitlab-sdk' +require 'sentry-ruby' +require 'snowplow-tracker' + +# rubocop:disable RSpec/ExpectInHook +RSpec.describe Telemetry do + describe '.with_telemetry' do + let(:command) { 'test_command' } + let(:args) { %w[arg1 arg2] } + let(:telemetry_enabled) { true } + + before do + expect(described_class).to receive(:telemetry_enabled?).and_return(telemetry_enabled) + expect(described_class).to receive(:with_telemetry).and_call_original + + allow(GDK).to receive_message_chain(:config, :telemetry, :username).and_return('testuser') + allow(described_class).to receive(:client) + + stub_const('ARGV', args) + end + + context 'when telemetry is not enabled' do + let(:telemetry_enabled) { false } + + it 'does not track telemetry and directly yields the block' do + expect { |b| described_class.with_telemetry(command, &b) }.to yield_control + end + end + + it 'tracks the start and finish of the command' do + expect(described_class).to receive_message_chain(:client, :identify).with('testuser') + expect(described_class).to receive_message_chain(:client, :track).with("Start #{command} #{args.inspect}", {}) + expect(described_class).to receive_message_chain(:client, :track).with(a_string_starting_with('Finish'), hash_including(:duration)) + + described_class.with_telemetry(command) { true } + end + + context 'when the block returns false' do + it 'tracks the start and failure of the command' do + expect(described_class).to receive_message_chain(:client, :identify).with('testuser') + expect(described_class).to receive_message_chain(:client, :track).with("Start #{command} #{args.inspect}", {}) + expect(described_class).to receive_message_chain(:client, :track).with(a_string_starting_with('Failed'), hash_including(:duration)) + + described_class.with_telemetry(command) { false } + end + end + end + + describe '.client' do + before do + described_class.instance_variable_set(:@client, nil) + + stub_env('GITLAB_SDK_APP_ID', 'app_id') + stub_env('GITLAB_SDK_HOST', 'https://collector') + + allow(GitlabSDK::Client).to receive(:new).and_return(mocked_client) + end + + let(:mocked_client) { instance_double(GitlabSDK::Client) } + + it 'initializes the gitlab sdk client with the correct configuration' do + expect(SnowplowTracker::LOGGER).to receive(:level=).with(Logger::WARN) + expect(GitlabSDK::Client).to receive(:new).with(app_id: 'app_id', host: 'https://collector').and_return(mocked_client) + + described_class.client + end + + context 'when client is already initialized' do + before do + described_class.instance_variable_set(:@client, mocked_client) + end + + it 'returns the existing client without reinitializing' do + expect(GitlabSDK::Client).not_to receive(:new) + expect(described_class.client).to eq(mocked_client) + end + end + end + + describe '.init_sentry' do + let(:config) { instance_double(Sentry::Configuration) } + + it 'initializes the sentry client with expected values' do + allow(Sentry).to receive(:init).and_yield(config) + + expect(config).to receive(:dsn=).with('https://glet_1a56990d202783685f3708b129fde6c0@observe.gitlab.com:443/errortracking/api/v1/projects/48924931') + expect(config).to receive(:breadcrumbs_logger=).with([:sentry_logger]) + expect(config).to receive(:traces_sample_rate=).with(1.0) + expect(config).to receive_message_chain(:logger, :level=).with(Logger::WARN) + + described_class.init_sentry + end + end + + describe '.telemetry_enabled?' do + [true, false].each do |value| + context "when #{value}" do + it "returns #{value}" do + expect(GDK).to receive_message_chain(:config, :telemetry, :enabled).and_return(value) + + expect(described_class.telemetry_enabled?).to eq(value) + end + end + end + end + + describe '.update_settings' do + before do + expect(FileUtils).to receive(:touch) + expect(GDK.config).to receive(:save_yaml!) + end + + context 'when username is not .' do + let(:username) { 'testuser' } + + it 'updates the settings with the provided username and enables telemetry' do + expect(GDK.config).to receive(:bury!).with('telemetry.enabled', true) + expect(GDK.config).to receive(:bury!).with('telemetry.username', username) + + described_class.update_settings(username) + end + end + + context 'when username is .' do + let(:username) { '.' } + + it 'updates the settings with an empty username and disables telemetry' do + expect(GDK.config).to receive(:bury!).with('telemetry.enabled', false) + expect(GDK.config).to receive(:bury!).with('telemetry.username', '') + + described_class.update_settings(username) + end + end + end + + describe '.capture_exception' do + let(:telemetry_enabled) { true } + + before do + expect(described_class).to receive(:telemetry_enabled?).and_return(telemetry_enabled) + + allow(described_class).to receive(:capture_exception).and_call_original + allow(described_class).to receive(:init_sentry) + end + + context 'when telemetry is not enabled' do + let(:telemetry_enabled) { false } + + it 'does not capture the exception' do + expect(Sentry).not_to receive(:capture_exception) + + described_class.capture_exception('Test error') + end + end + + context 'when given an exception' do + let(:exception) { StandardError.new('Test error') } + + it 'captures the given exception' do + expect(Sentry).to receive(:capture_exception).with(exception) + + described_class.capture_exception(exception) + end + end + + context 'when given a string' do + let(:message) { 'Test error message' } + + it 'captures a new exception with the given message' do + expect(Sentry).to receive(:capture_exception) do |exception| + expect(exception).to be_a(StandardError) + expect(exception.message).to eq(message) + expect(exception.backtrace).not_to be_empty + end + + described_class.capture_exception(message) + end + end + end +end +# rubocop:enable RSpec/ExpectInHook diff --git a/spec/lib/gdk_spec.rb b/spec/lib/gdk_spec.rb index b5ebdbfd01bf4314c1597e635e193f49272fd47a..448302020cf36c65dd755b1ccff29b3bc3bba584 100644 --- a/spec/lib/gdk_spec.rb +++ b/spec/lib/gdk_spec.rb @@ -68,7 +68,7 @@ def expect_output(level, message: nil) shared_examples 'invalid YAML' do |error_message| it 'prints an error' do - expect(GDK::Output).to receive(:error).with("Your gdk.yml is invalid.\n\n") + expect(GDK::Output).to receive(:error).with("Your gdk.yml is invalid.\n\n", StandardError) expect(GDK::Output).to receive(:puts).with(error_message, stderr: true) expect { described_class.validate_yaml! }.to raise_error(SystemExit).and output("\n").to_stderr diff --git a/spec/lib/shellout_spec.rb b/spec/lib/shellout_spec.rb index 505f272203d8160da6f530278aab9e55557c060c..0a9183818531ff70238100b86238375f87a88973 100644 --- a/spec/lib/shellout_spec.rb +++ b/spec/lib/shellout_spec.rb @@ -112,7 +112,7 @@ it 'displays output and errors' do expect(GDK::Output).to receive(:print).with(expected_command_stderr_puts, stderr: true) - expect(GDK::Output).to receive(:error).with(expected_command_error) + expect(GDK::Output).to receive(:error).with(expected_command_error, Shellout::ShelloutBaseError) subject.execute end @@ -132,8 +132,8 @@ expect(subject).to receive(expected_execute_method).exactly(3).times # 1 for the first run + 2 retries expect(subject).to receive(:success?).exactly(6).times.and_return(false) - expect(GDK::Output).to receive(:error).with("'#{command}' failed. Retrying in 2 secs..").twice - expect(GDK::Output).to receive(:error).with("'#{command}' failed.") + expect(GDK::Output).to receive(:error).with("'#{command}' failed. Retrying in 2 secs..", Shellout::ExecuteCommandFailedError).twice + expect(GDK::Output).to receive(:error).with("'#{command}' failed.", Shellout::ExecuteCommandFailedError) subject.execute(display_output: display_output, retry_attempts: 2) end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 956bd5954ae6a307e58774dd8bcf701ceebb183d..442b4aaf3807fc6d5928044acd359720d52584e2 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -14,6 +14,7 @@ require_relative '../lib/gdk' require_relative '../lib/gdk/task_helpers' +require_relative '../lib/telemetry' RSpec.configure do |config| config.before do |example| @@ -45,6 +46,13 @@ allow(GDK).to receive(:root).and_return(gdk_root_tmp_path) end + + unless example.metadata[:with_telemetry] + allow(Telemetry).to receive(:with_telemetry).and_wrap_original do |_method, *_args, &block| + block.call + end + allow(Telemetry).to receive(:capture_exception) + end end config.disable_monkey_patching diff --git a/support/install b/support/install index 6ae1c8705008a4be3b58cd9d54c034d41ba826a8..a98bb6e04d3f9284ed0fe897ec67aa3084f4e162 100755 --- a/support/install +++ b/support/install @@ -66,7 +66,7 @@ bootstrap() { gdk_install() { # shellcheck disable=SC1090 source "${ASDF_SH_PATH}" - gdk install gitlab_repo="$GITLAB_REPO_URL" + gdk install gitlab_repo="$GITLAB_REPO_URL" telemetry_user="$GITLAB_USERNAME" } echo @@ -80,13 +80,20 @@ echo if [ $# -eq 1 ]; then echo "Where would you like to install the GDK? [./${DEFAULT_GDK_INSTALL_DIR}]" read -r GDK_INSTALL_DIR </dev/tty - + echo echo "Which GitLab repo URL would you like to clone? [${DEFAULT_GITLAB_REPO_URL}]" echo echo "ATTENTION: For members of the wider community, it is recommended to use the community fork (https://gitlab.com/gitlab-community/gitlab.git)." echo "See https://gitlab.com/gitlab-community/meta for instructions on how to join." echo "If you'd prefer to use your own repository, please ensure that its visibility is set to public." read -r GITLAB_REPO_URL </dev/tty + echo + echo "To improve GDK, GitLab would like to collect basic error and usage data. Please choose one of the following options:" + echo + echo "- To send data to GitLab, enter your GitLab username." + echo "- To send data to GitLab anonymously, leave blank." + echo "- To avoid sending data to GitLab, enter a period ('.')." + read -r GITLAB_USERNAME </dev/tty else GDK_INSTALL_DIR="${2-gitlab-development-kit}" GDK_CLONE_BRANCH="${3-main}" diff --git a/support/test_url b/support/test_url index eed23cbc81b75c3d42c1aac491615708862f60e0..fb2d315c3f154503bf111b09c0fe541c3842a7b8 100755 --- a/support/test_url +++ b/support/test_url @@ -9,7 +9,7 @@ verbose = ENV['QUIET'] == 'false' begin exit(GDK::TestURL.new(url).wait(verbose: verbose)) -rescue GDK::TestURL::UrlAppearsInvalid - GDK::Output.error("'#{url}' does not appear to be a valid URL?") +rescue GDK::TestURL::UrlAppearsInvalid => e + GDK::Output.error("'#{url}' does not appear to be a valid URL?", e) exit(1) end