Skip to content

Commit

Permalink
Add Stackdriver crash reporting (#9113)
Browse files Browse the repository at this point in the history
  • Loading branch information
ohayon committed May 11, 2017
1 parent cd304af commit 0b6bc6e
Show file tree
Hide file tree
Showing 12 changed files with 415 additions and 0 deletions.
6 changes: 6 additions & 0 deletions README.md
Expand Up @@ -154,6 +154,12 @@ We have recently changed the destination service to which these metrics are repo

You can easily opt-out of metrics collection by adding `opt_out_usage` at the top of your `Fastfile` or by setting the environment variable `FASTLANE_OPT_OUT_USAGE`. Participating helps us provide the best possible support for _fastlane_, so we hope you'll consider it a plus! :heavy_plus_sign:

## Crash Reporting

In order to continuously improve stability, _fastlane_ will record crash reports with sanitized stacktraces. Sanitization removes personal information from the stacktrace and error message (including home directories, _fastlane_ path, gem paths, environment variables, and parameters).

You can easily opt-out of crash reporting by adding `opt_out_crash_reporting` at the top of your `Fastfile` or by setting the environment variable `FASTLANE_OPT_OUT_CRASH_REPORTING`. Just like metrics mentioned above, participating helps us provide the best possible support for _fastlane_, so we hope you'll consider it a plus! :heavy_plus_sign:

## Need Help?

Please [submit an issue](https://github.com/fastlane/fastlane/issues) on GitHub and provide information about your setup.
Expand Down
41 changes: 41 additions & 0 deletions fastlane/lib/fastlane/actions/opt_out_crash_reporting.rb
@@ -0,0 +1,41 @@
module Fastlane
module Actions
class OptOutCrashReportingAction < Action
def self.run(params)
ENV['FASTLANE_OPT_OUT_CRASH_REPORTING'] = "YES"
UI.message("Disabled crash reporting")
end

def self.description
"This will prevent reports from being uploaded when _fastlane_ crashes"
end

def self.details
[
"By default, fastlane will send a report when it crashes",
"The stacktrace is sanitized so no personal information is sent.",
"Learn more at https://github.com/fastlane/fastlane#crash-reporting",
"Add `opt_out_crash_reporting` at the top of your Fastfile to disable crash reporting"
].join(' ')
end

def self.authors
['mpirri', 'ohayon']
end

def self.is_supported?(platform)
true
end

def self.example_code
[
'opt_out_crash_reporting # add this to the top of your Fastfile'
]
end

def self.category
:misc
end
end
end
end
4 changes: 4 additions & 0 deletions fastlane/spec/cli_tools_distributor_spec.rb
Expand Up @@ -21,6 +21,10 @@
end

describe "update checking" do
before do
allow(FastlaneCore::CrashReporter).to receive(:report_crash)
end

it "checks for updates when running a lane" do
FastlaneSpec::Env.with_ARGV(["sigh"])
require 'fastlane/commands_generator'
Expand Down
3 changes: 3 additions & 0 deletions fastlane_core/lib/fastlane_core.rb
Expand Up @@ -31,6 +31,9 @@
require 'fastlane_core/keychain_importer'
require 'fastlane_core/swag'
require 'fastlane_core/build_watcher'
require 'fastlane_core/crash_reporter/crash_reporter'
require 'fastlane_core/crash_reporter/crash_report_generator'
require 'fastlane_core/crash_reporter/backtrace_sanitizer'

# Third Party code
require 'colored'
Expand Down
@@ -0,0 +1,42 @@
module FastlaneCore
class BacktraceSanitizer
class << self
def sanitize(type: :unknown, backtrace: nil)
if type == :user_error || type == :crash
# If the crash is from `UI` we only want to include the stack trace
# up to the point where the crash was initiated.
# The two stack frames we are dropping are `method_missing` and
# the call to `crash!` or `user_error!`.
stack = backtrace.drop(2)
else
stack = backtrace
end

stack = remove_fastlane_gem_path(backtrace: stack)
stack = remove_gem_home_path(backtrace: stack)
remove_home_dir_mentions(backtrace: stack)
end

def remove_fastlane_gem_path(backtrace: nil)
fastlane_path = Gem.loaded_specs['fastlane'].full_gem_path
return backtrace unless fastlane_path
backtrace.map do |frame|
frame.gsub(fastlane_path, '[fastlane_path]')
end
end

def remove_gem_home_path(backtrace: nil)
return backtrace unless Gem.dir
backtrace.map do |frame|
frame.gsub(Gem.dir, '[gem_home]')
end
end

def remove_home_dir_mentions(backtrace: nil)
backtrace.map do |frame|
frame.gsub(Dir.home, '~')
end
end
end
end
end
@@ -0,0 +1,49 @@
module FastlaneCore
class CrashReportGenerator
class << self
def types
{
user_error: '[USER_ERROR]',
crash: '[FASTLANE_CRASH]',
exception: '[EXCEPTION]',
connection_failure: '[CONNECTION_FAILURE]',
system: '[SYSTEM_ERROR]',
option_parser: '[OPTION_PARSER]',
invalid_command: '[INVALID_COMMAND]',
unknown: '[UNKNOWN]'
}
end

def generate(type: :unknown, exception: nil)
message = crash_report_message(type: type, exception: exception)
crash_report_payload(message: message)
end

private

def crash_report_message(type: :unknown, exception: nil)
backtrace = FastlaneCore::BacktraceSanitizer.sanitize(type: type, backtrace: exception.backtrace).join("\n")
message = types[type]
if type == :user_error
message += ': '
else
message += ": #{exception.message}"
end
message = message[0..100]
message += "\n" unless type == :user_error
message + backtrace
end

def crash_report_payload(message: '')
{
'eventTime' => Time.now.utc.to_datetime.rfc3339,
'serviceContext' => {
'service' => 'fastlane',
'version' => Fastlane::VERSION
},
'message' => message
}.to_json
end
end
end
end
58 changes: 58 additions & 0 deletions fastlane_core/lib/fastlane_core/crash_reporter/crash_reporter.rb
@@ -0,0 +1,58 @@
require 'faraday'
require 'json'

module FastlaneCore
class CrashReporter
class << self
def crash_report_path
File.join(FastlaneCore.fastlane_user_dir, 'latest_crash.json')
end

def enabled?
!FastlaneCore::Env.truthy?("FASTLANE_OPT_OUT_CRASH_REPORTING")
end

def report_crash(type: :unknown, exception: nil)
return unless enabled?
payload = CrashReportGenerator.generate(type: type, exception: exception)
send_report(payload: payload)
save_file(payload: payload)
show_message unless did_show_message?
end

private

def show_message
UI.message("Sending crash report...")
UI.message("The stacktrace is sanitized so no personal information is sent.")
UI.message("To see what we are sending, look here: #{crash_report_path}")
UI.message("Learn more at https://github.com/fastlane/fastlane#crash-reporting")
UI.message("You can disable crash reporting by adding `opt_out_crash_reporting` at the top of your Fastfile")
end

def did_show_message?
file_name = ".did_show_opt_out_crash_info"

path = File.join(FastlaneCore.fastlane_user_dir, file_name)
did_show = File.exist?(path)

return did_show if did_show

File.write(path, '1')
false
end

def save_file(payload: "{}")
File.write(crash_report_path, payload)
end

def send_report(payload: "{}")
connection = Faraday.new(url: "https://clouderrorreporting.googleapis.com/v1beta1/projects/fastlane-166414/events:report?key=AIzaSyAMACPfuI-wi4grJWEZjcPvhfV2Rhmddwo")
connection.post do |request|
request.headers['Content-Type'] = 'application/json'
request.body = payload
end
end
end
end
end
7 changes: 7 additions & 0 deletions fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb
Expand Up @@ -48,6 +48,7 @@ def run!
if FastlaneCore::Helper.test?
raise e
else
FastlaneCore::CrashReporter.report_crash(type: :invalid_command, exception: e)
abort "#{e}. Use --help for more information"
end
rescue Interrupt => e
Expand All @@ -66,18 +67,21 @@ def run!
if FastlaneCore::Helper.test?
raise e
else
FastlaneCore::CrashReporter.report_crash(type: :option_parser, exception: e)
abort e.to_s
end
rescue FastlaneCore::Interface::FastlaneError => e # user_error!
collector.did_raise_error(@program[:name])
show_github_issues(e.message) if e.show_github_issues
FastlaneCore::CrashReporter.report_crash(type: :user_error, exception: e)
display_user_error!(e, e.message)
rescue Errno::ENOENT => e
# We're also printing the new-lines, as otherwise the message is not very visible in-between the error and the stacktrace
puts ""
FastlaneCore::UI.important("Error accessing file, this might be due to fastlane's directory handling")
FastlaneCore::UI.important("Check out https://docs.fastlane.tools/advanced/#directory-behavior for more details")
puts ""
FastlaneCore::CrashReporter.report_crash(type: :system, exception: e)
raise e
rescue FastlaneCore::Interface::FastlaneTestFailure => e # test_failure!
display_user_error!(e, e.to_s)
Expand All @@ -87,9 +91,12 @@ def run!
if e.message.include? 'Connection reset by peer - SSL_connect'
handle_tls_error!(e)
else
FastlaneCore::CrashReporter.report_crash(type: :connection_failure, exception: e)
handle_unknown_error!(e)
end
rescue => e # high chance this is actually FastlaneCore::Interface::FastlaneCrash, but can be anything else
type = e.kind_of?(FastlaneCore::Interface::FastlaneCrash) ? :crash : :exception
FastlaneCore::CrashReporter.report_crash(type: type, exception: e)
collector.did_crash(@program[:name])
handle_unknown_error!(e)
ensure
Expand Down
51 changes: 51 additions & 0 deletions fastlane_core/spec/crash_reporter/backtrace_sanitizer_spec.rb
@@ -0,0 +1,51 @@
describe FastlaneCore::BacktraceSanitizer do
context 'path sanitization' do
let(:fastlane_spec) { double('Gem::Specification') }

it 'returns a trace with fastlane path sanitized' do
expect(Gem).to receive(:loaded_specs).and_return({
'fastlane' => fastlane_spec
})
expect(fastlane_spec).to receive(:full_gem_path).at_least(1).and_return('/path/to/fastlane')
expect(FastlaneCore::BacktraceSanitizer.sanitize(
backtrace: ['/path/to/fastlane/source/file']
)).to eq(['[fastlane_path]/source/file'])
end

it 'returns a trace with gem home sanitized' do
expect(Gem).to receive(:dir).at_least(1).and_return('/path/to/gem')
expect(FastlaneCore::BacktraceSanitizer.sanitize(
backtrace: ['/path/to/gem/source/file']
)).to eq(['[gem_home]/source/file'])
end

it 'returns a trace with home directory sanitized' do
expect(Dir).to receive(:home).at_least(1).and_return('/path/to/home_dir')
expect(FastlaneCore::BacktraceSanitizer.sanitize(
backtrace: ['/path/to/home_dir/source/file']
)).to eq(['~/source/file'])
end
end

context 'stack frame dropping' do
it 'drops the first two frames from crashes' do
expect(FastlaneCore::BacktraceSanitizer.sanitize(
type: :crash,
backtrace: ['frame0', 'frame1', 'frame2']
)).to eq(['frame2'])
end

it 'drops the first two frames from user errors' do
expect(FastlaneCore::BacktraceSanitizer.sanitize(
type: :user_error,
backtrace: ['frame0', 'frame1', 'frame2']
)).to eq(['frame2'])
end

it 'drops no frames from other errors' do
expect(FastlaneCore::BacktraceSanitizer.sanitize(
backtrace: ['frame0', 'frame1', 'frame2']
)).to eq(['frame0', 'frame1', 'frame2'])
end
end
end
61 changes: 61 additions & 0 deletions fastlane_core/spec/crash_reporter/crash_report_generator_spec.rb
@@ -0,0 +1,61 @@
describe FastlaneCore::CrashReportGenerator do
context 'generate crash report' do
let(:exception) do
double(
'Exception',
backtrace: [
'path/to/fastlane/line/that/crashed',
'path/to/fastlane/line/that/called/the/crash'
],
message: 'message goes here'
)
end

let(:expected_body) do
{
'eventTime' => '0000-01-01T00:00:00+00:00',
'serviceContext' => {
'service' => 'fastlane',
'version' => '2.29.0'
},
'message' => ""
}
end

before do
allow(Time).to receive(:now).and_return(Time.utc(0))
end

it 'omits a message for type user_error' do
setup_sanitizer_expectation(type: :user_error)
setup_expected_body(type: :user_error, message_text: ": ")
expect(FastlaneCore::CrashReportGenerator.generate(type: :user_error, exception: exception)).to eq(expected_body.to_json)
end

it 'includes a message for other types' do
setup_sanitizer_expectation
setup_expected_body(message_text: ": #{exception.message}\n")
expect(FastlaneCore::CrashReportGenerator.generate(exception: exception)).to eq(expected_body.to_json)
end

it 'includes stack frames in message' do
setup_sanitizer_expectation
setup_expected_body(message_text: ": #{exception.message}\n")
report = JSON.parse(FastlaneCore::CrashReportGenerator.generate(exception: exception))
expect(report['message']).to include(exception.backtrace.join("\n"))
end
end
end

def setup_sanitizer_expectation(type: :unknown)
expect(FastlaneCore::BacktraceSanitizer).to receive(:sanitize).with(
type: type,
backtrace: exception.backtrace
) do |args|
args[:backtrace]
end
end

def setup_expected_body(type: :unknown, message_text: "")
expected_body['message'] = "#{FastlaneCore::CrashReportGenerator.types[type]}#{message_text}#{exception.backtrace.join("\n")}"
end

0 comments on commit 0b6bc6e

Please sign in to comment.