opbeat 2.0.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (130) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -3
  3. data/.travis.yml +19 -28
  4. data/.yardopts +3 -0
  5. data/Gemfile +4 -2
  6. data/HISTORY.md +3 -0
  7. data/LICENSE +7 -196
  8. data/README.md +96 -177
  9. data/Rakefile +19 -13
  10. data/gemfiles/Gemfile.base +28 -0
  11. data/gemfiles/Gemfile.rails-3.2.x +3 -0
  12. data/gemfiles/Gemfile.rails-4.0.x +3 -0
  13. data/gemfiles/Gemfile.rails-4.1.x +3 -0
  14. data/gemfiles/Gemfile.rails-4.2.x +3 -0
  15. data/lib/opbeat.rb +113 -93
  16. data/lib/opbeat/capistrano.rb +3 -4
  17. data/lib/opbeat/client.rb +243 -82
  18. data/lib/opbeat/configuration.rb +51 -64
  19. data/lib/opbeat/data_builders.rb +16 -0
  20. data/lib/opbeat/data_builders/error.rb +27 -0
  21. data/lib/opbeat/data_builders/transactions.rb +85 -0
  22. data/lib/opbeat/error.rb +1 -2
  23. data/lib/opbeat/error_message.rb +71 -0
  24. data/lib/opbeat/error_message/exception.rb +12 -0
  25. data/lib/opbeat/error_message/http.rb +62 -0
  26. data/lib/opbeat/error_message/stacktrace.rb +75 -0
  27. data/lib/opbeat/error_message/user.rb +23 -0
  28. data/lib/opbeat/filter.rb +53 -43
  29. data/lib/opbeat/http_client.rb +141 -0
  30. data/lib/opbeat/injections.rb +83 -0
  31. data/lib/opbeat/injections/json.rb +19 -0
  32. data/lib/opbeat/injections/net_http.rb +43 -0
  33. data/lib/opbeat/injections/redis.rb +23 -0
  34. data/lib/opbeat/injections/sequel.rb +32 -0
  35. data/lib/opbeat/injections/sinatra.rb +56 -0
  36. data/lib/opbeat/{capistrano → integration}/capistrano2.rb +6 -6
  37. data/lib/opbeat/{capistrano → integration}/capistrano3.rb +3 -3
  38. data/lib/opbeat/{integrations → integration}/delayed_job.rb +6 -11
  39. data/lib/opbeat/integration/rails/inject_exceptions_catcher.rb +23 -0
  40. data/lib/opbeat/integration/railtie.rb +53 -0
  41. data/lib/opbeat/integration/resque.rb +16 -0
  42. data/lib/opbeat/integration/sidekiq.rb +38 -0
  43. data/lib/opbeat/line_cache.rb +21 -0
  44. data/lib/opbeat/logging.rb +37 -0
  45. data/lib/opbeat/middleware.rb +59 -0
  46. data/lib/opbeat/normalizers.rb +65 -0
  47. data/lib/opbeat/normalizers/action_controller.rb +21 -0
  48. data/lib/opbeat/normalizers/action_view.rb +71 -0
  49. data/lib/opbeat/normalizers/active_record.rb +41 -0
  50. data/lib/opbeat/sql_summarizer.rb +27 -0
  51. data/lib/opbeat/subscriber.rb +80 -0
  52. data/lib/opbeat/tasks.rb +20 -18
  53. data/lib/opbeat/trace.rb +47 -0
  54. data/lib/opbeat/trace_helpers.rb +29 -0
  55. data/lib/opbeat/transaction.rb +99 -0
  56. data/lib/opbeat/util.rb +26 -0
  57. data/lib/opbeat/util/constantize.rb +54 -0
  58. data/lib/opbeat/util/inspector.rb +75 -0
  59. data/lib/opbeat/version.rb +1 -1
  60. data/lib/opbeat/worker.rb +55 -0
  61. data/opbeat.gemspec +6 -14
  62. data/spec/opbeat/client_spec.rb +216 -29
  63. data/spec/opbeat/configuration_spec.rb +34 -38
  64. data/spec/opbeat/data_builders/error_spec.rb +43 -0
  65. data/spec/opbeat/data_builders/transactions_spec.rb +51 -0
  66. data/spec/opbeat/error_message/exception_spec.rb +22 -0
  67. data/spec/opbeat/error_message/http_spec.rb +65 -0
  68. data/spec/opbeat/error_message/stacktrace_spec.rb +56 -0
  69. data/spec/opbeat/error_message/user_spec.rb +28 -0
  70. data/spec/opbeat/error_message_spec.rb +78 -0
  71. data/spec/opbeat/filter_spec.rb +21 -99
  72. data/spec/opbeat/http_client_spec.rb +64 -0
  73. data/spec/opbeat/injections/net_http_spec.rb +37 -0
  74. data/spec/opbeat/injections/sequel_spec.rb +33 -0
  75. data/spec/opbeat/injections/sinatra_spec.rb +13 -0
  76. data/spec/opbeat/injections_spec.rb +49 -0
  77. data/spec/opbeat/integration/delayed_job_spec.rb +35 -0
  78. data/spec/opbeat/integration/json_spec.rb +41 -0
  79. data/spec/opbeat/integration/rails_spec.rb +88 -0
  80. data/spec/opbeat/integration/redis_spec.rb +20 -0
  81. data/spec/opbeat/integration/resque_spec.rb +42 -0
  82. data/spec/opbeat/integration/sidekiq_spec.rb +40 -0
  83. data/spec/opbeat/integration/sinatra_spec.rb +66 -0
  84. data/spec/opbeat/line_cache_spec.rb +38 -0
  85. data/spec/opbeat/logging_spec.rb +47 -0
  86. data/spec/opbeat/middleware_spec.rb +32 -0
  87. data/spec/opbeat/normalizers/action_controller_spec.rb +32 -0
  88. data/spec/opbeat/normalizers/action_view_spec.rb +77 -0
  89. data/spec/opbeat/normalizers/active_record_spec.rb +70 -0
  90. data/spec/opbeat/normalizers_spec.rb +16 -0
  91. data/spec/opbeat/sql_summarizer_spec.rb +6 -0
  92. data/spec/opbeat/subscriber_spec.rb +83 -0
  93. data/spec/opbeat/trace_spec.rb +43 -0
  94. data/spec/opbeat/transaction_spec.rb +98 -0
  95. data/spec/opbeat/util/inspector_spec.rb +40 -0
  96. data/spec/opbeat/util_spec.rb +20 -0
  97. data/spec/opbeat/worker_spec.rb +54 -0
  98. data/spec/opbeat_spec.rb +49 -0
  99. data/spec/spec_helper.rb +79 -6
  100. metadata +89 -149
  101. data/Makefile +0 -3
  102. data/gemfiles/rails30.gemfile +0 -9
  103. data/gemfiles/rails31.gemfile +0 -9
  104. data/gemfiles/rails32.gemfile +0 -9
  105. data/gemfiles/rails40.gemfile +0 -9
  106. data/gemfiles/rails41.gemfile +0 -9
  107. data/gemfiles/rails42.gemfile +0 -9
  108. data/gemfiles/ruby192_rails31.gemfile +0 -10
  109. data/gemfiles/ruby192_rails32.gemfile +0 -10
  110. data/gemfiles/sidekiq31.gemfile +0 -11
  111. data/lib/opbeat/better_attr_accessor.rb +0 -44
  112. data/lib/opbeat/event.rb +0 -223
  113. data/lib/opbeat/integrations/resque.rb +0 -22
  114. data/lib/opbeat/integrations/sidekiq.rb +0 -32
  115. data/lib/opbeat/interfaces.rb +0 -35
  116. data/lib/opbeat/interfaces/exception.rb +0 -16
  117. data/lib/opbeat/interfaces/http.rb +0 -57
  118. data/lib/opbeat/interfaces/message.rb +0 -19
  119. data/lib/opbeat/interfaces/stack_trace.rb +0 -50
  120. data/lib/opbeat/linecache.rb +0 -25
  121. data/lib/opbeat/logger.rb +0 -21
  122. data/lib/opbeat/rack.rb +0 -46
  123. data/lib/opbeat/rails/middleware/debug_exceptions_catcher.rb +0 -22
  124. data/lib/opbeat/railtie.rb +0 -26
  125. data/spec/opbeat/better_attr_accessor_spec.rb +0 -99
  126. data/spec/opbeat/event_spec.rb +0 -138
  127. data/spec/opbeat/integrations/delayed_job_spec.rb +0 -38
  128. data/spec/opbeat/logger_spec.rb +0 -55
  129. data/spec/opbeat/opbeat_spec.rb +0 -64
  130. data/spec/opbeat/rack_spec.rb +0 -117
@@ -1,90 +1,77 @@
1
+ require 'logger'
2
+
1
3
  module Opbeat
2
4
  class Configuration
5
+ DEFAULTS = {
6
+ server: "https://intake.opbeat.com".freeze,
7
+ logger: Logger.new(nil),
8
+ context_lines: 3,
9
+ enabled_environments: %w{production},
10
+ excluded_exceptions: [],
11
+ filter_parameters: [/(authorization|password|passwd|secret)/i],
12
+ timeout: 100,
13
+ open_timeout: 100,
14
+ backoff_multiplier: 2,
15
+ use_ssl: true,
16
+ current_user_method: :current_user,
17
+ environment: ENV['RACK_ENV'] || ENV['RAILS_ENV'] || 'default',
18
+ transaction_post_interval: 60,
19
+
20
+ disable_performance: false,
21
+ disable_errors: false,
22
+
23
+ debug_traces: false,
24
+
25
+ # for tests
26
+ disable_worker: false
27
+ }.freeze
3
28
 
4
- # Base URL of the Opbeat server
5
- attr_accessor :server
6
-
7
- # Secret access token for authentication with Opbeat
8
29
  attr_accessor :secret_token
9
-
10
- # Organization ID to use with Opbeat
11
30
  attr_accessor :organization_id
12
-
13
- # App ID to use with Opbeat
14
31
  attr_accessor :app_id
15
32
 
16
- # Logger to use internally
33
+ attr_accessor :server
17
34
  attr_accessor :logger
18
-
19
- # Number of lines of code context to capture, or nil for none
20
35
  attr_accessor :context_lines
21
-
22
- # Whitelist of environments that will send notifications to Opbeat
23
- attr_accessor :environments
24
-
25
- # Which exceptions should never be sent
36
+ attr_accessor :enabled_environments
26
37
  attr_accessor :excluded_exceptions
27
-
28
- # An array of parameters whould should be filtered from the log
29
38
  attr_accessor :filter_parameters
30
-
31
- # Timeout when waiting for the server to return data in seconds
32
39
  attr_accessor :timeout
33
-
34
- # Timout when opening connection to the server
35
40
  attr_accessor :open_timeout
36
-
37
- # Backoff multipler
38
41
  attr_accessor :backoff_multiplier
42
+ attr_accessor :use_ssl
43
+ attr_accessor :current_user_method
44
+ attr_accessor :environment
45
+ attr_accessor :transaction_post_interval
39
46
 
40
- # Should the SSL certificate of the server be verified?
41
- attr_accessor :ssl_verification
47
+ attr_accessor :disable_performance
48
+ attr_accessor :disable_errors
42
49
 
43
- attr_reader :current_environment
50
+ attr_accessor :debug_traces
44
51
 
45
- attr_accessor :user_controller_method
52
+ attr_accessor :disable_worker
46
53
 
47
- # Optional Proc to be used to send events asynchronously
48
- attr_reader :async
54
+ attr_accessor :view_paths
49
55
 
50
- def initialize
51
- self.server = ENV['OPBEAT_SERVER'] || "https://intake.opbeat.com"
52
- self.secret_token = ENV['OPBEAT_SECRET_TOKEN'] if ENV['OPBEAT_SECRET_TOKEN']
53
- self.organization_id = ENV['OPBEAT_ORGANIZATION_ID'] if ENV['OPBEAT_ORGANIZATION_ID']
54
- self.app_id = ENV['OPBEAT_APP_ID'] if ENV['OPBEAT_APP_ID']
55
- @context_lines = 3
56
- self.environments = %w[ development production default ]
57
- self.current_environment = (defined?(::Rails) && ::Rails.env) || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'default'
58
- self.excluded_exceptions = []
59
- self.timeout = 1
60
- self.open_timeout = 1
61
- self.backoff_multiplier = 2
62
- self.ssl_verification = true
63
- self.user_controller_method = 'current_user'
64
- self.async = false
65
- end
56
+ def initialize opts = {}
57
+ DEFAULTS.merge(opts).each do |k, v|
58
+ self.send("#{k}=", v)
59
+ end
66
60
 
67
- # Allows config options to be read like a hash
68
- #
69
- # @param [Symbol] option Key for a given attribute
70
- def [](option)
71
- send(option)
61
+ if block_given?
62
+ yield self
63
+ end
72
64
  end
73
65
 
74
- def current_environment=(environment)
75
- @current_environment = environment.to_s
76
- end
66
+ def validate!
67
+ %w{app_id secret_token organization_id}.each do |key|
68
+ raise Error.new("Configuration missing `#{key}'") unless self.send(key)
69
+ end
77
70
 
78
- def send_in_current_environment?
79
- environments.include? current_environment
71
+ true
72
+ rescue Error => e
73
+ logger.error e.message
74
+ false
80
75
  end
81
-
82
- def async=(value)
83
- raise ArgumentError.new("async must be callable (or false to disable)") unless (value == false || value.respond_to?(:call))
84
- @async = value
85
- end
86
-
87
- alias_method :async?, :async
88
-
89
76
  end
90
77
  end
@@ -0,0 +1,16 @@
1
+ module Opbeat
2
+ # @api private
3
+ module DataBuilders
4
+ class DataBuilder
5
+ def initialize config
6
+ @config = config
7
+ end
8
+
9
+ attr_reader :config
10
+ end
11
+
12
+ %w{transactions error}.each do |f|
13
+ require "opbeat/data_builders/#{f}"
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,27 @@
1
+ require 'opbeat/filter'
2
+
3
+ module Opbeat
4
+ module DataBuilders
5
+ class Error < DataBuilder
6
+ def build error_message
7
+ h = {
8
+ message: error_message.message,
9
+ timestamp: error_message.timestamp,
10
+ level: error_message.level,
11
+ logger: error_message.logger,
12
+ culprit: error_message.culprit,
13
+ machine: error_message.machine,
14
+ extra: error_message.extra,
15
+ param_message: error_message.param_message
16
+ }
17
+
18
+ h[:exception] = error_message.exception.to_h if error_message.exception
19
+ h[:stacktrace] = error_message.stacktrace.to_h if error_message.stacktrace
20
+ h[:http] = error_message.http.to_h if error_message.http
21
+ h[:user] = error_message.user.to_h if error_message.user
22
+
23
+ h
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,85 @@
1
+ module Opbeat
2
+ module DataBuilders
3
+ class Transactions < DataBuilder
4
+ def build transactions
5
+ reduced = transactions.reduce({ transactions: {}, traces: {} }) do |data, transaction|
6
+ key = [transaction.endpoint, transaction.result, transaction.timestamp]
7
+
8
+ if data[:transactions][key].nil?
9
+ data[:transactions][key] = build_transaction(transaction)
10
+ else
11
+ data[:transactions][key][:durations] << ms(transaction.duration)
12
+ end
13
+
14
+ combine_traces transaction.traces, data[:traces]
15
+
16
+ data
17
+ end.reduce({}) do |data, kv|
18
+ key, collection = kv
19
+ data[key] = collection.values
20
+ data
21
+ end
22
+
23
+ reduced[:traces].each do |trace|
24
+ # traces' start time is average across collected
25
+ trace[:start_time] = trace[:start_time].reduce(0, :+) / trace[:start_time].length
26
+ end
27
+
28
+ # preserve root
29
+ root = reduced[:traces].shift
30
+ # re-add root
31
+ reduced[:traces].unshift root
32
+
33
+ reduced
34
+ end
35
+
36
+ private
37
+
38
+ def combine_traces traces, into
39
+ traces.each do |trace|
40
+ key = [trace.transaction.endpoint, trace.signature, trace.timestamp]
41
+
42
+ if into[key].nil?
43
+ into[key] = build_trace(trace)
44
+ else
45
+ into[key][:durations] << [
46
+ ms(trace.duration),
47
+ ms(trace.transaction.duration)
48
+ ]
49
+ into[key][:start_time] << ms(trace.relative_start)
50
+ end
51
+ end
52
+ end
53
+
54
+ def build_transaction transaction
55
+ {
56
+ transaction: transaction.endpoint,
57
+ result: transaction.result,
58
+ kind: transaction.kind,
59
+ timestamp: transaction.timestamp,
60
+ durations: [ms(transaction.duration)]
61
+ }
62
+ end
63
+
64
+ def build_trace trace
65
+ {
66
+ transaction: trace.transaction.endpoint,
67
+ signature: trace.signature,
68
+ durations: [[
69
+ ms(trace.duration),
70
+ ms(trace.transaction.duration)
71
+ ]],
72
+ start_time: [ms(trace.relative_start)],
73
+ kind: trace.kind,
74
+ timestamp: trace.timestamp,
75
+ parents: trace.parents && trace.parents.map(&:signature) || [],
76
+ extra: trace.extra
77
+ }
78
+ end
79
+
80
+ def ms nanos
81
+ nanos.to_f / 1_000_000
82
+ end
83
+ end
84
+ end
85
+ end
data/lib/opbeat/error.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  module Opbeat
2
-
2
+ # @api private
3
3
  class Error < StandardError
4
4
  end
5
-
6
5
  end
@@ -0,0 +1,71 @@
1
+ require 'opbeat/line_cache'
2
+ require 'opbeat/error_message/exception'
3
+ require 'opbeat/error_message/stacktrace'
4
+ require 'opbeat/error_message/http'
5
+ require 'opbeat/error_message/user'
6
+
7
+ module Opbeat
8
+ class ErrorMessage
9
+ extend Logging
10
+
11
+ DEFAULTS = {
12
+ level: :error,
13
+ logger: 'root'.freeze
14
+ }.freeze
15
+
16
+ def initialize config, message, attrs = {}
17
+ @config = config
18
+
19
+ @message = message
20
+ @timestamp = Time.now.utc.to_i
21
+ DEFAULTS.merge(attrs).each do |k,v|
22
+ send(:"#{k}=", v)
23
+ end
24
+ @filter = Filter.new config
25
+
26
+ yield self if block_given?
27
+ end
28
+
29
+ attr_reader :config
30
+ attr_accessor :message
31
+ attr_reader :timestamp
32
+ attr_accessor :level
33
+ attr_accessor :logger
34
+ attr_accessor :culprit
35
+ attr_accessor :machine
36
+ attr_accessor :extra
37
+ attr_accessor :param_message
38
+ attr_accessor :exception
39
+ attr_accessor :stacktrace
40
+ attr_accessor :http
41
+ attr_accessor :user
42
+
43
+ def self.from_exception config, exception, opts = {}
44
+ message = "#{exception.class}: #{exception.message}"
45
+
46
+ if config.excluded_exceptions.include? exception.class.to_s
47
+ info "Skipping excluded exception #{exception.class}"
48
+ return nil
49
+ end
50
+
51
+ error_message = new(config, message) do |msg|
52
+ msg.level = :error
53
+ msg.exception = Exception.from(exception)
54
+ msg.stacktrace = Stacktrace.from(config, exception)
55
+ end
56
+
57
+ if frames = error_message.stacktrace && error_message.stacktrace.frames
58
+ if first_frame = frames[0]
59
+ error_message.culprit = "#{first_frame.filename}:#{first_frame.lineno}:in `#{first_frame.function}'"
60
+ end
61
+ end
62
+
63
+ if env = opts[:rack_env]
64
+ error_message.http = HTTP.from_rack_env env, filter: @filter
65
+ error_message.user = User.from_rack_env config, env
66
+ end
67
+
68
+ error_message
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,12 @@
1
+ module Opbeat
2
+ class ErrorMessage
3
+ class Exception < Struct.new(:type, :value, :module)
4
+ SPLIT = '::'.freeze
5
+
6
+ def self.from exception
7
+ new exception.class.to_s, exception.message,
8
+ exception.class.to_s.split(SPLIT)[0...-1].join(SPLIT)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,62 @@
1
+ module Opbeat
2
+ class ErrorMessage
3
+ class HTTP < Struct.new(:url, :method, :data, :query_string, :cookies,
4
+ :headers, :remote_host, :http_host, :user_agent,
5
+ :secure, :env)
6
+
7
+ HTTP_ENV_KEY = /^HTTP_/.freeze
8
+ UNDERSCORE = "_".freeze
9
+ DASH = "-".freeze
10
+ QUESTION = "?".freeze
11
+
12
+ def self.from_rack_env env, opts = {}
13
+ req = Rack::Request.new env
14
+
15
+ http = new(
16
+ req.url.split(QUESTION).first, # url
17
+ req.request_method, # method
18
+ nil, # data
19
+ req.query_string, # query string
20
+ env['HTTP_COOKIE'], # cookies
21
+ {}, # headers
22
+ req.ip, # remote host
23
+ req.host_with_port, # http host
24
+ req.user_agent, # user agent
25
+ req.scheme == 'https'.freeze ? true : false, # secure
26
+ {} # env
27
+ )
28
+
29
+ env.each do |k, v|
30
+ next unless k.upcase == k # lower case stuff isn't relevant
31
+
32
+ if k.match(HTTP_ENV_KEY)
33
+ header = k.gsub(HTTP_ENV_KEY, '')
34
+ .split(UNDERSCORE).map(&:capitalize).join(DASH)
35
+ http.headers[header] = v.to_s
36
+ else
37
+ http.env[k] = v.to_s
38
+ end
39
+ end
40
+
41
+ if req.form_data?
42
+ http.data = req.POST
43
+ elsif req.body
44
+ http.data = req.body.read
45
+ req.body.rewind
46
+ end
47
+
48
+ if filter = opts[:filter]
49
+ http.apply_filter filter
50
+ end
51
+
52
+ http
53
+ end
54
+
55
+ def apply_filter filter
56
+ self.data = filter.apply data
57
+ self.query_string = filter.apply query_string
58
+ self.cookies = filter.apply cookies
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,75 @@
1
+ module Opbeat
2
+ class ErrorMessage
3
+ class Stacktrace
4
+
5
+ def initialize config, frames
6
+ @config, @frames = config, frames
7
+ end
8
+
9
+ attr_reader :frames
10
+
11
+ def self.from config, exception
12
+ return unless exception.backtrace
13
+
14
+ new(config, exception.backtrace.map do |line|
15
+ Frame.from_line config, line
16
+ end)
17
+ end
18
+
19
+ def to_h
20
+ { frames: frames.map(&:to_h) }
21
+ end
22
+
23
+ private
24
+
25
+ class Frame < Struct.new(:filename, :lineno, :abs_path, :function, :vars,
26
+ :pre_context, :context_line, :post_context)
27
+
28
+ BACKTRACE_REGEX = /^(.+?):(\d+)(?::in `(.+?)')?$/.freeze
29
+
30
+ class << self
31
+ def from_line config, line
32
+ _, abs_path, lineno, function = line.match(BACKTRACE_REGEX).to_a
33
+ lineno = lineno.to_i
34
+ filename = strip_load_path(abs_path)
35
+
36
+ if lines = config.context_lines
37
+ pre_context, context_line, post_context =
38
+ get_contextlines(abs_path, lineno, lines)
39
+ end
40
+
41
+ new filename, lineno, abs_path, function, nil,
42
+ pre_context, context_line, post_context
43
+ end
44
+
45
+ private
46
+
47
+ def strip_load_path path
48
+ prefix = $:
49
+ .map(&:to_s)
50
+ .select { |s| path.start_with?(s) }
51
+ .sort_by { |s| s.length }
52
+ .last
53
+
54
+ return path unless prefix
55
+
56
+ path[prefix.chomp(File::SEPARATOR).length + 1..-1]
57
+ end
58
+
59
+ def get_contextlines path, line, context
60
+ lines = (2 * context + 1).times.map do |i|
61
+ LineCache.find(path, line - context + i)
62
+ end
63
+
64
+ pre = lines[0..(context-1)]
65
+ line = lines[context]
66
+ post = lines[(context+1)..-1]
67
+
68
+ [pre, line, post]
69
+ end
70
+ end
71
+ end
72
+
73
+ end
74
+ end
75
+ end