sentry-raven 0.12.2 → 0.12.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: aa170cbdab1ee92ed1a5b471c2edd146e9d3f3c5
4
- data.tar.gz: 4ca87cca729a05fbd1564a0e404b6bac57c47267
3
+ metadata.gz: f4d1ff44aab9878c7070e85ac6bac6a2573e5332
4
+ data.tar.gz: 7668ff7bdc645dc3d77305f0be63bd028696eee5
5
5
  SHA512:
6
- metadata.gz: e5f7bef20983567b7b622b119587524a3423f623683b39b2224203fa3df4335ec9aaf3bdde122220e86ca01aa96201d12b34ca62f77381a6c0d3fe923633a506
7
- data.tar.gz: ffb038d8c1a843c4458b0d7b21befacad00b14e43b32052db4054a4655e55fdde225482764b233adb8c93ae9d49f1b09b0a2f0d02b7bde654498204541847205
6
+ metadata.gz: ba3fb97a2c6a77adf1e8d5afcf00d7d94f14b3d5bf3851db165e0624afc1aa702efd61d2c3f24cf9195b8b9e2bedf72244219f7bd61a698e8d443732e227efec
7
+ data.tar.gz: e96788e536488bed998d179c9c752157044c5a703de5c479c9265966bde529f6d37dcba908478e9a292e4c149256f1ecb67ab748c2e89829ae73c8496c8d7e4a
data/README.md CHANGED
@@ -13,7 +13,7 @@ We test on Ruby MRI 1.8.7/REE, 1.9.3, 2.0 and 2.1. JRuby support is experimental
13
13
  ## Getting Started
14
14
  ### Install
15
15
  ```ruby
16
- gem "sentry-raven", :require => 'raven' #, :github => "getsentry/raven-ruby"
16
+ gem "sentry-raven" #, :github => "getsentry/raven-ruby"
17
17
  ```
18
18
  ### Set SENTRY_DSN
19
19
  ```bash
data/bin/raven CHANGED
@@ -4,8 +4,6 @@ require "raven"
4
4
  require "raven/cli"
5
5
  require "optparse"
6
6
 
7
- options = {}
8
-
9
7
  parser = OptionParser.new do |opt|
10
8
  opt.banner = "Usage: raven COMMAND [OPTIONS]"
11
9
  opt.separator ""
@@ -0,0 +1,104 @@
1
+ # :stopdoc:
2
+
3
+ # Stolen from ruby core's uri/common.rb, with modifications to support 1.8.x
4
+ #
5
+ # https://github.com/ruby/ruby/blob/trunk/lib/uri/common.rb
6
+ #
7
+ #
8
+
9
+ module URI
10
+ TBLENCWWWCOMP_ = {} # :nodoc:
11
+ 256.times do |i|
12
+ TBLENCWWWCOMP_[i.chr] = '%%%02X' % i
13
+ end
14
+ TBLENCWWWCOMP_[' '] = '+'
15
+ TBLENCWWWCOMP_.freeze
16
+ TBLDECWWWCOMP_ = {} # :nodoc:
17
+ 256.times do |i|
18
+ h, l = i>>4, i&15
19
+ TBLDECWWWCOMP_['%%%X%X' % [h, l]] = i.chr
20
+ TBLDECWWWCOMP_['%%%x%X' % [h, l]] = i.chr
21
+ TBLDECWWWCOMP_['%%%X%x' % [h, l]] = i.chr
22
+ TBLDECWWWCOMP_['%%%x%x' % [h, l]] = i.chr
23
+ end
24
+ TBLDECWWWCOMP_['+'] = ' '
25
+ TBLDECWWWCOMP_.freeze
26
+
27
+ # Encode given +s+ to URL-encoded form data.
28
+ #
29
+ # This method doesn't convert *, -, ., 0-9, A-Z, _, a-z, but does convert SP
30
+ # (ASCII space) to + and converts others to %XX.
31
+ #
32
+ # This is an implementation of
33
+ # http://www.w3.org/TR/html5/forms.html#url-encoded-form-data
34
+ #
35
+ # See URI.decode_www_form_component, URI.encode_www_form
36
+ def self.encode_www_form_component(s)
37
+ str = s.to_s
38
+ if RUBY_VERSION < "1.9" && $KCODE =~ /u/i
39
+ str.gsub(/([^ a-zA-Z0-9_.-]+)/) do
40
+ '%' + $1.unpack('H2' * Rack::Utils.bytesize($1)).join('%').upcase
41
+ end.tr(' ', '+')
42
+ else
43
+ str.gsub(/[^*\-.0-9A-Z_a-z]/) {|m| TBLENCWWWCOMP_[m]}
44
+ end
45
+ end
46
+
47
+ # Decode given +str+ of URL-encoded form data.
48
+ #
49
+ # This decodes + to SP.
50
+ #
51
+ # See URI.encode_www_form_component, URI.decode_www_form
52
+ def self.decode_www_form_component(str, enc=nil)
53
+ raise ArgumentError, "invalid %-encoding (#{str})" unless /\A(?:%[0-9a-fA-F]{2}|[^%])*\z/ =~ str
54
+ str.gsub(/\+|%[0-9a-fA-F]{2}/) {|m| TBLDECWWWCOMP_[m]}
55
+ end
56
+
57
+ # Generate URL-encoded form data from given +enum+.
58
+ #
59
+ # This generates application/x-www-form-urlencoded data defined in HTML5
60
+ # from given an Enumerable object.
61
+ #
62
+ # This internally uses URI.encode_www_form_component(str).
63
+ #
64
+ # This method doesn't convert the encoding of given items, so convert them
65
+ # before call this method if you want to send data as other than original
66
+ # encoding or mixed encoding data. (Strings which are encoded in an HTML5
67
+ # ASCII incompatible encoding are converted to UTF-8.)
68
+ #
69
+ # This method doesn't handle files. When you send a file, use
70
+ # multipart/form-data.
71
+ #
72
+ # This is an implementation of
73
+ # http://www.w3.org/TR/html5/forms.html#url-encoded-form-data
74
+ #
75
+ # URI.encode_www_form([["q", "ruby"], ["lang", "en"]])
76
+ # #=> "q=ruby&lang=en"
77
+ # URI.encode_www_form("q" => "ruby", "lang" => "en")
78
+ # #=> "q=ruby&lang=en"
79
+ # URI.encode_www_form("q" => ["ruby", "perl"], "lang" => "en")
80
+ # #=> "q=ruby&q=perl&lang=en"
81
+ # URI.encode_www_form([["q", "ruby"], ["q", "perl"], ["lang", "en"]])
82
+ # #=> "q=ruby&q=perl&lang=en"
83
+ #
84
+ # See URI.encode_www_form_component, URI.decode_www_form
85
+ def self.encode_www_form(enum)
86
+ enum.map do |k,v|
87
+ if v.nil?
88
+ encode_www_form_component(k)
89
+ elsif v.respond_to?(:to_ary)
90
+ v.to_ary.map do |w|
91
+ str = encode_www_form_component(k)
92
+ unless w.nil?
93
+ str << '='
94
+ str << encode_www_form_component(w)
95
+ end
96
+ end.join('&')
97
+ else
98
+ str = encode_www_form_component(k)
99
+ str << '='
100
+ str << encode_www_form_component(v)
101
+ end
102
+ end.join('&')
103
+ end
104
+ end
@@ -61,7 +61,7 @@ module Raven
61
61
  end
62
62
 
63
63
  def inspect
64
- "<Line:#{to_s}>"
64
+ "<Line:#{self}>"
65
65
  end
66
66
 
67
67
  private
@@ -77,8 +77,8 @@ module Raven
77
77
 
78
78
  filters = opts[:filters] || []
79
79
  filtered_lines = ruby_lines.to_a.map do |line|
80
- filters.reduce(line) do |line, proc|
81
- proc.call(line)
80
+ filters.reduce(line) do |nested_line, proc|
81
+ proc.call(nested_line)
82
82
  end
83
83
  end.compact
84
84
 
@@ -86,7 +86,7 @@ module Raven
86
86
  Line.parse(unparsed_line)
87
87
  end
88
88
 
89
- instance = new(lines)
89
+ new(lines)
90
90
  end
91
91
 
92
92
  def initialize(lines)
data/lib/raven/base.rb CHANGED
@@ -14,6 +14,11 @@ require 'raven/processor/sanitizedata'
14
14
  require 'raven/processor/removecircularreferences'
15
15
  require 'raven/processor/utf8conversion'
16
16
 
17
+ major, minor, patch = RUBY_VERSION.split('.').map(&:to_i)
18
+ if (major == 1 && minor < 9) || (major == 1 && minor == 9 && patch < 2)
19
+ require 'raven/backports/uri'
20
+ end
21
+
17
22
  module Raven
18
23
  class << self
19
24
  # The client object is responsible for delivering formatted data to the Sentry server.
@@ -101,7 +106,7 @@ module Raven
101
106
 
102
107
  def capture_exception(exception, options = {})
103
108
  send_or_skip(exception) do
104
- if evt = Event.from_exception(exception, options)
109
+ if (evt = Event.from_exception(exception, options))
105
110
  yield evt if block_given?
106
111
  if configuration.async?
107
112
  configuration.async.call(evt)
@@ -114,7 +119,7 @@ module Raven
114
119
 
115
120
  def capture_message(message, options = {})
116
121
  send_or_skip(message) do
117
- if evt = Event.from_message(message, options)
122
+ if (evt = Event.from_message(message, options))
118
123
  yield evt if block_given?
119
124
  if configuration.async?
120
125
  configuration.async.call(evt)
@@ -192,7 +197,7 @@ module Raven
192
197
  # Extra context shows up as Additional Data within Sentry, and is completely arbitrary.
193
198
  #
194
199
  # @example
195
- # Raven.tags_context('my_custom_data' => 'value')
200
+ # Raven.extra_context('my_custom_data' => 'value')
196
201
  def extra_context(options = {})
197
202
  self.context.extra.merge!(options)
198
203
  end
@@ -208,7 +213,17 @@ module Raven
208
213
  def inject
209
214
  available_integrations = %w[delayed_job rails sidekiq rack rake]
210
215
  integrations_to_load = available_integrations & Gem.loaded_specs.keys
211
- integrations_to_load.each { |integration| require "raven/integrations/#{integration}" }
216
+ # TODO(dcramer): integrations should have some additional checks baked-in
217
+ # or we should break them out into their own repos. Specifically both the
218
+ # rails and delayed_job checks are not always valid (i.e. Rails 2.3) and
219
+ # https://github.com/getsentry/raven-ruby/issues/180
220
+ integrations_to_load.each do |integration|
221
+ begin
222
+ require "raven/integrations/#{integration}"
223
+ rescue Exception => error
224
+ self.logger.warn "Unable to load raven/integrations/#{integration}: #{error}"
225
+ end
226
+ end
212
227
  end
213
228
 
214
229
  # For cross-language compat
data/lib/raven/cli.rb CHANGED
@@ -7,7 +7,7 @@ module Raven
7
7
 
8
8
  logger = ::Logger.new(STDOUT)
9
9
  logger.level = ::Logger::ERROR
10
- logger.formatter = proc do |severity, datetime, progname, msg|
10
+ logger.formatter = proc do |_severity, _datetime, _progname, msg|
11
11
  "-> #{msg}\n"
12
12
  end
13
13
 
data/lib/raven/client.rb CHANGED
@@ -19,6 +19,7 @@ module Raven
19
19
  def initialize(configuration)
20
20
  @configuration = configuration
21
21
  @processors = configuration.processors.map { |v| v.new(self) }
22
+ @state = ClientState.new
22
23
  end
23
24
 
24
25
  def send(event)
@@ -29,18 +30,25 @@ module Raven
29
30
 
30
31
  # Set the project ID correctly
31
32
  event.project = self.configuration.project_id
33
+
34
+ if !@state.should_try?
35
+ Raven.logger.error("Not sending event due to previous failure(s): #{get_log_message(event)}")
36
+ return
37
+ end
38
+
32
39
  Raven.logger.debug "Sending event #{event.id} to Sentry"
33
40
 
34
41
  content_type, encoded_data = encode(event)
35
42
  begin
36
- transport.send(generate_auth_header(encoded_data), encoded_data,
43
+ transport.send(generate_auth_header, encoded_data,
37
44
  :content_type => content_type)
38
45
  rescue => e
39
- Raven.logger.error "Unable to record event with remote Sentry server (#{e.class} - #{e.message})"
40
- e.backtrace[0..10].each { |line| Raven.logger.error(line) }
46
+ failed_send(e, event)
41
47
  return
42
48
  end
43
49
 
50
+ successful_send
51
+
44
52
  event
45
53
  end
46
54
 
@@ -66,6 +74,10 @@ module Raven
66
74
  end
67
75
  end
68
76
 
77
+ def get_log_message(event)
78
+ (event && event.message) || '<no message value>'
79
+ end
80
+
69
81
  def transport
70
82
  @transport ||=
71
83
  case self.configuration.scheme
@@ -78,7 +90,7 @@ module Raven
78
90
  end
79
91
  end
80
92
 
81
- def generate_auth_header(data)
93
+ def generate_auth_header
82
94
  now = Time.now.to_i.to_s
83
95
  fields = {
84
96
  'sentry_version' => PROTOCOL_VERSION,
@@ -100,5 +112,53 @@ module Raven
100
112
  end
101
113
  end
102
114
 
115
+ def successful_send
116
+ @state.success
117
+ end
118
+
119
+ def failed_send(e, event)
120
+ @state.failure
121
+ Raven.logger.error "Unable to record event with remote Sentry server (#{e.class} - #{e.message})"
122
+ e.backtrace[0..10].each { |line| Raven.logger.error(line) }
123
+ Raven.logger.error("Failed to submit event: #{get_log_message(event)}")
124
+ end
125
+
126
+ end
127
+
128
+ class ClientState
129
+ def initialize
130
+ reset
131
+ end
132
+
133
+ def should_try?
134
+ return true if @status == :online
135
+
136
+ interval = @retry_after || [@retry_number, 6].min ** 2
137
+ return true if Time.now - @last_check >= interval
138
+
139
+ false
140
+ end
141
+
142
+ def failure(retry_after = nil)
143
+ @status = :error
144
+ @retry_number += 1
145
+ @last_check = Time.now
146
+ @retry_after = retry_after
147
+ end
148
+
149
+ def success
150
+ reset
151
+ end
152
+
153
+ def reset
154
+ @status = :online
155
+ @retry_number = 0
156
+ @last_check = nil
157
+ @retry_after = nil
158
+ end
159
+
160
+ def failed?
161
+ @status == :error
162
+ end
103
163
  end
104
164
  end
@@ -55,9 +55,12 @@ module Raven
55
55
  # Should the SSL certificate of the server be verified?
56
56
  attr_accessor :ssl_verification
57
57
 
58
- # Ssl settings passed direactly to faraday's ssl option
58
+ # SSl settings passed direactly to faraday's ssl option
59
59
  attr_accessor :ssl
60
60
 
61
+ # Proxy information to pass to the HTTP adapter
62
+ attr_accessor :proxy
63
+
61
64
  attr_reader :current_environment
62
65
 
63
66
  # The Faraday adapter to be used. Will default to Net::HTTP when not set.
@@ -106,6 +109,7 @@ module Raven
106
109
  self.encoding = 'gzip'
107
110
  self.timeout = 1
108
111
  self.open_timeout = 1
112
+ self.proxy = nil
109
113
  self.tags = {}
110
114
  self.async = false
111
115
  self.catch_debugged_exceptions = true
@@ -160,11 +164,7 @@ module Raven
160
164
  end
161
165
 
162
166
  def send_in_current_environment?
163
- if environments
164
- environments.include?(current_environment)
165
- else
166
- true
167
- end
167
+ !!server && (!environments || environments.include?(current_environment))
168
168
  end
169
169
 
170
170
  def log_excluded_environment_message
data/lib/raven/event.rb CHANGED
@@ -59,11 +59,9 @@ module Raven
59
59
 
60
60
  block.call(self) if block
61
61
 
62
- if @configuration.send_in_current_environment?
63
- if !self[:http] && context.rack_env
64
- self.interface :http do |int|
65
- int.from_rack(context.rack_env)
66
- end
62
+ if !self[:http] && context.rack_env
63
+ self.interface :http do |int|
64
+ int.from_rack(context.rack_env)
67
65
  end
68
66
  end
69
67
 
@@ -76,7 +74,7 @@ module Raven
76
74
  def get_hostname
77
75
  # Try to resolve the hostname to an FQDN, but fall back to whatever the load name is
78
76
  hostname = Socket.gethostname
79
- hostname = Socket.gethostbyname(hostname).first rescue hostname
77
+ Socket.gethostbyname(hostname).first rescue hostname
80
78
  end
81
79
 
82
80
  def get_modules
@@ -140,7 +138,7 @@ module Raven
140
138
  context_lines = configuration[:context_lines]
141
139
 
142
140
  new(options) do |evt|
143
- evt.message = "#{exc.class.to_s}: #{exc.message}"
141
+ evt.message = "#{exc.class}: #{exc.message}"
144
142
  evt.level = options[:level] || :error
145
143
 
146
144
  evt.interface(:exception) do |int|
@@ -186,7 +184,7 @@ module Raven
186
184
  end
187
185
 
188
186
  # Because linecache can go to hell
189
- def self._source_lines(path, from, to)
187
+ def self._source_lines(_path, _from, _to)
190
188
  end
191
189
 
192
190
  def get_file_context(filename, lineno, context)
@@ -3,10 +3,10 @@ require 'sidekiq'
3
3
 
4
4
  module Raven
5
5
  class Sidekiq
6
- def call(worker, msg, queue)
6
+ def call(_worker, msg, _queue)
7
7
  started_at = Time.now
8
8
  yield
9
- rescue => ex
9
+ rescue Exception => ex
10
10
  Raven.capture_exception(ex, :extra => { :sidekiq => msg },
11
11
  :time_spent => Time.now-started_at)
12
12
  raise
@@ -4,7 +4,7 @@ require 'raven/cli'
4
4
 
5
5
  namespace :raven do
6
6
  desc "Send a test event to the remote Sentry server"
7
- task :test, [:dsn] do |t, args|
7
+ task :test, [:dsn] do |_t, args|
8
8
  Rake::Task["environment"].invoke if defined? Rails
9
9
 
10
10
  Raven::CLI.test(args.dsn)
data/lib/raven/okjson.rb CHANGED
@@ -81,7 +81,7 @@ module OkJson
81
81
  when false then "false"
82
82
  when nil then "null"
83
83
  else
84
- raise Error, "cannot encode #{x.class}: #{x.inspect}"
84
+ strenc((x.inspect rescue $!.to_s))
85
85
  end
86
86
  end
87
87
 
@@ -3,7 +3,7 @@ module Raven
3
3
  STRING_MASK = '********'
4
4
  INT_MASK = 0
5
5
  DEFAULT_FIELDS = %w(authorization password passwd secret ssn social(.*)?sec)
6
- VALUES_RE = /^\d{16}$/
6
+ CREDIT_CARD_RE = /^(?:\d[ -]*?){13,16}$/
7
7
 
8
8
  def process(value)
9
9
  value.inject(value) { |memo,(k,v)| memo[k] = sanitize(k,v); memo }
@@ -13,13 +13,15 @@ module Raven
13
13
  if v.is_a?(Hash)
14
14
  process(v)
15
15
  elsif v.is_a?(Array)
16
- v.map{|a| sanitize(nil, a)}
16
+ v.map{|a| sanitize(k, a)}
17
+ elsif k == 'query_string'
18
+ sanitize_query_string(v)
17
19
  elsif v.is_a?(String) && (json = parse_json_or_nil(v))
18
20
  #if this string is actually a json obj, convert and sanitize
19
21
  json.is_a?(Hash) ? process(json).to_json : v
20
- elsif v.is_a?(Integer) && (VALUES_RE.match(v.to_s) || fields_re.match(k.to_s))
22
+ elsif v.is_a?(Integer) && (CREDIT_CARD_RE.match(v.to_s) || fields_re.match(k.to_s))
21
23
  INT_MASK
22
- elsif v.is_a?(String) && (VALUES_RE.match(v.to_s) || fields_re.match(k.to_s))
24
+ elsif v.is_a?(String) && (CREDIT_CARD_RE.match(v.to_s) || fields_re.match(k.to_s))
23
25
  STRING_MASK
24
26
  else
25
27
  v
@@ -28,6 +30,12 @@ module Raven
28
30
 
29
31
  private
30
32
 
33
+ def sanitize_query_string(query_string)
34
+ query_hash = CGI::parse(query_string)
35
+ processed_query_hash = process(query_hash)
36
+ URI.encode_www_form(processed_query_hash)
37
+ end
38
+
31
39
  def fields_re
32
40
  @fields_re ||= /(#{(DEFAULT_FIELDS + @sanitize_fields).join("|")})/i
33
41
  end
@@ -3,9 +3,9 @@ module Raven
3
3
 
4
4
  def process(value)
5
5
  if value.is_a? Array
6
- value.map { |v_| process v_ }
6
+ value.map { |v| process v }
7
7
  elsif value.is_a? Hash
8
- value.merge(value) { |k, v_| process v_ }
8
+ value.merge(value) { |_, v| process v }
9
9
  else
10
10
  clean_invalid_utf8_bytes(value)
11
11
  end
@@ -10,8 +10,8 @@ module Raven
10
10
  @configuration = configuration
11
11
  end
12
12
 
13
- def send(auth_header, data, options = {})
14
- raise Error.new('Abstract method not implemented')
13
+ def send#(auth_header, data, options = {})
14
+ raise NotImplementedError.new('Abstract method not implemented')
15
15
  end
16
16
 
17
17
  protected
@@ -9,7 +9,7 @@ module Raven
9
9
 
10
10
  def send(auth_header, data, options = {})
11
11
  project_id = self.configuration[:project_id]
12
- path = self.configuration[:path].gsub('/sentry', '') + "/"
12
+ path = self.configuration[:path] + "/"
13
13
 
14
14
  response = conn.post "#{path}api/#{project_id}/store/" do |req|
15
15
  req.headers['Content-Type'] = options[:content_type]
@@ -37,6 +37,10 @@ module Raven
37
37
  builder.adapter(*adapter)
38
38
  end
39
39
 
40
+ if self.configuration.proxy
41
+ conn.options[:proxy] = self.configuration.proxy
42
+ end
43
+
40
44
  if self.configuration.timeout
41
45
  conn.options[:timeout] = self.configuration.timeout
42
46
  end
@@ -7,7 +7,7 @@ module Raven
7
7
  module Transports
8
8
  class UDP < Transport
9
9
 
10
- def send(auth_header, data, options = {})
10
+ def send(auth_header, data, _options = {})
11
11
  conn.send "#{auth_header}\n\n#{data}", 0
12
12
  end
13
13
 
data/lib/raven/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Raven
2
- VERSION = "0.12.2"
2
+ VERSION = "0.12.3"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sentry-raven
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.2
4
+ version: 0.12.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sentry Team
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-12-08 00:00:00.000000000 Z
11
+ date: 2015-01-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: timecop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
97
111
  description: A gem that provides a client interface for the Sentry error logger
98
112
  email: getsentry@googlegroups.com
99
113
  executables:
@@ -107,6 +121,7 @@ files:
107
121
  - README.md
108
122
  - bin/raven
109
123
  - lib/raven.rb
124
+ - lib/raven/backports/uri.rb
110
125
  - lib/raven/backtrace.rb
111
126
  - lib/raven/base.rb
112
127
  - lib/raven/better_attr_accessor.rb