tapping_device 0.4.1 → 0.4.2

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
  SHA256:
3
- metadata.gz: 65b116ba192b195aca0df0f0400e8d2b0617b221c620dcaaf8eab0f16e2d85fb
4
- data.tar.gz: 0cb2b12d7329f5f770bd8d1522b8ab0d5f7c1787d142d06116cf83908e136f63
3
+ metadata.gz: fcaaf227b8114ef1cbfeabf472ac00bc86512e6426f98fc88bdf841f1ca71cee
4
+ data.tar.gz: e1a87da54066d42958915ef19e4c933edad4f9a2907e4df95893c68d4b5dcf95
5
5
  SHA512:
6
- metadata.gz: 7192f5957a150267f71b4fe88a9eabb45f0966d5fda933e2a050f8a52e823ae338e6a3e35f24ff6f3ccef9f3b96c8fca9df300b8e3b3c4e69d29964f5ebdaa91
7
- data.tar.gz: 264dd2bd5c83c1f716d272179c296a03bdf7612d2415602174432817769fa8f069612af45c0f58da722a0e499fe06bc440d3f1810bcc1a810e8c238e27fd97c5
6
+ metadata.gz: 437e25448ba4e10f984596d1c7502101f2afddf49400a0a72e8c753df8a2498981ef0812975d4b551c83caf3ab603352741830b6aa16019e323687ea487ee3fe
7
+ data.tar.gz: 1cf0b05b79cf2ea64a0862d051a493833d3bcec039d1f400b6184a0901c9c46122d5f2bf2a90588a61cedadc422c87e2d52aaee9d7df6ec247feb2def2defeb3
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- tapping_device (0.4.1)
4
+ tapping_device (0.4.2)
5
5
  activerecord (>= 5.2)
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -126,7 +126,7 @@ All tapping methods (start with `tap_`) takes a block and yield a `Payload` obje
126
126
  {
127
127
  :receiver=>#<Student:0x00007fabed02aeb8 @name="Stan", @age=18, @tapping_device=[#<TracePoint:return `age'@/PROJECT_PATH/tapping_device/spec/trackable_spec.rb:17>]>,
128
128
  :method_name=>:age,
129
- :arguments=>[],
129
+ :arguments=>{},
130
130
  :return_value=>18,
131
131
  :filepath=>"/PROJECT_PATH/tapping_device/spec/trackable_spec.rb",
132
132
  :line_number=>"171",
@@ -142,7 +142,7 @@ The hash contains
142
142
  - `method_name` - method’s name (symbol)
143
143
  - e.g. `:name`
144
144
  - `arguments` - arguments of the method call
145
- - e.g. `[[:name, “Stan”], [:age, 25]]`
145
+ - e.g. `{name: “Stan”, age: 25}`
146
146
  - `return_value` - return value of the method call
147
147
  - `filepath` - path to the file that performs the method call
148
148
  - `line_number`
@@ -152,7 +152,7 @@ The hash contains
152
152
 
153
153
  #### Some useful helpers
154
154
  - `method_name_and_location` - `"Method: :initialize, line: /PROJECT_PATH/tapping_device/spec/payload_spec.rb:7"`
155
- - `method_name_and_arguments` - `"Method: :initialize, argments: [[:name, \"Stan\"], [:age, 25]]"`
155
+ - `method_name_and_arguments` - `"Method: :initialize, argments: {:name=>\"Stan\", :age=>25}"`
156
156
 
157
157
 
158
158
  ### Options
@@ -193,7 +193,7 @@ end
193
193
  Student.new("Stan", 18)
194
194
  Student.new("Jane", 23)
195
195
 
196
- puts(calls.to_s) #=> [[:initialize, [[:name, "Stan"], [:age, 18]]], [:initialize, [[:name, "Jane"], [:age, 23]]]]
196
+ puts(calls.to_s) #=> [[:initialize, {:name=>"Stan", :age=>18}], [:initialize, {:name=>"Jane", :age=>23}]]
197
197
  ```
198
198
 
199
199
  ### `tap_on!`
@@ -1,6 +1,6 @@
1
1
  class TappingDevice
2
2
  class Payload < Hash
3
- ATTRS = [:receiver, :method_name, :arguments, :return_value, :filepath, :line_number, :defined_class, :trace, :tp]
3
+ ATTRS = [:receiver, :method_name, :arguments, :return_value, :filepath, :line_number, :defined_class, :trace, :tp, :sql]
4
4
 
5
5
  ATTRS.each do |attr|
6
6
  define_method attr do
@@ -1,67 +1,84 @@
1
- require "tapping_device/sql_listener"
2
-
3
1
  class TappingDevice
4
2
  module SqlTappingMethods
5
- @@sql_listeners = []
3
+ CALL_STACK_SKIPPABLE_METHODS = [:transaction, :tap]
6
4
 
7
- ActiveSupport::Notifications.subscribe('sql.active_record') do |_1, _2, _3, _4, payload|
8
- if !["SCHEMA", "TRANSACTION"].include? payload[:name]
9
- @@sql_listeners.each do |listener|
10
- listener.payload[:sql] = payload[:sql]
11
- listener.payload[:binds] = payload[:binds]
12
- listener.device.record_call!(listener.payload)
13
- end
14
- end
5
+ # SQLListener acts like an interface for us to intercept activerecord query instrumentations
6
+ # this means we only need to register one subscriber no matter how many objects we want to tap on
7
+ class SQLListener
8
+ def call(name, start, finish, message_id, values);end
15
9
  end
10
+ @@sql_listener = SQLListener.new
11
+
12
+ ActiveSupport::Notifications.subscribe("sql.active_record", @@sql_listener)
16
13
 
17
14
  def tap_sql!(object)
18
- @trace_point = TracePoint.new(:call, :b_call, :c_call) do |start_tp|
15
+ @call_stack = []
16
+ @trace_point = with_trace_point_on_target(object, event: [:call, :c_call]) do |start_tp|
17
+ ########## Check if the call is worth recording ##########
18
+ filepath, line_number = get_call_location(start_tp, padding: 1) # we need extra padding because of `with_trace_point_on_target`
19
19
  method = start_tp.callee_id
20
+ next if should_be_skipped_by_paths?(filepath) || already_recording?(method)
20
21
 
21
- if is_from_target?(object, start_tp)
22
- filepath, line_number = get_call_location(start_tp)
22
+ ########## Start the recording ##########
23
+ # 1. Mark recording state by pushing method into @call_stack
24
+ # 2. Subscribe sql instrumentations generated by activerecord
25
+ # 3. Record those sqls and run device callbacks
26
+ # 4. Start waiting for current call's return callback
27
+ @call_stack.push(method)
28
+ payload = build_payload(tp: start_tp, filepath: filepath, line_number: line_number)
29
+ device = tap_on_sql_instrumentation!(payload)
23
30
 
24
- next if should_be_skip_by_paths?(filepath)
31
+ with_trace_point_on_target(object, event: :return) do |return_tp|
32
+ next unless return_tp.callee_id == method
25
33
 
26
- yield_parameters = build_yield_parameters(tp: start_tp, filepath: filepath, line_number: line_number)
34
+ ########## End recording ##########
35
+ # 1. Close itself
36
+ # 2. Stop our subscription on SQLListener
37
+ # 3. Remove current method from @call_stack
38
+ # 4. Stop the device if stop condition is fulfilled
39
+ return_tp.disable
40
+ device.stop!
41
+ @call_stack.pop
42
+ stop_if_condition_fulfilled(payload)
27
43
 
28
- # usually, AR's query methods (like `first`) will end up calling `find_by_sql`
29
- # then to TappingDevice, both `first` and `find_by_sql` generates the sql
30
- # but the results are duplicated, we should only consider the `first` call
31
- # so @in_call is used to determine if we're already in a middle of a call
32
- # it's not an optimal solution and should be updated
33
- next if @in_call
44
+ ########## Track descendant objects ##########
45
+ # if the method creates another Relation object
46
+ if return_tp.defined_class == ActiveRecord::QueryMethods
47
+ create_child_device.tap_sql!(return_tp.return_value)
48
+ end
49
+ end.enable
50
+ end
34
51
 
35
- @in_call = true
52
+ @trace_point.enable unless self.class.suspend_new
36
53
 
37
- sql_listener = SqlListenser.new(method, yield_parameters, self)
54
+ self
55
+ end
56
+ end
38
57
 
39
- @@sql_listeners << sql_listener
58
+ private
40
59
 
41
- # return of the method call
42
- TracePoint.trace(:return) do |return_tp|
43
- if is_from_target?(object, return_tp)
44
- # if it's a query method, end the sql tapping
45
- if return_tp.callee_id == method
46
- # if the method creates another Relation object
47
- if return_tp.defined_class == ActiveRecord::QueryMethods
48
- create_child_device.tap_sql!(return_tp.return_value)
49
- end
60
+ def tap_on_sql_instrumentation!(payload)
61
+ device = TappingDevice.new do |sql_listener_payload|
62
+ values = sql_listener_payload.arguments[:values]
63
+ next if ["SCHEMA", "TRANSACTION", nil].include? values[:name]
64
+ payload[:sql] = values[:sql]
65
+ record_call!(payload)
66
+ end
67
+ device.tap_on!(@@sql_listener)
68
+ end
50
69
 
51
- @@sql_listeners.delete(sql_listener)
52
- return_tp.disable
53
- @in_call = false
70
+ # usually, AR's query methods (like `first`) will end up calling `find_by_sql`
71
+ # then to TappingDevice, both `first` and `find_by_sql` generates the sql
72
+ # but the results are duplicated, we should only consider the `first` call
73
+ def already_recording?(method)
74
+ !@call_stack.empty? || CALL_STACK_SKIPPABLE_METHODS.include?(method)
75
+ end
54
76
 
55
- stop_if_condition_fulfilled(yield_parameters)
56
- end
57
- end
58
- end
59
- end
77
+ def with_trace_point_on_target(object, event:)
78
+ TracePoint.new(*event) do |tp|
79
+ if is_from_target?(object, tp)
80
+ yield(tp)
60
81
  end
61
-
62
- @trace_point.enable unless self.class.suspend_new
63
-
64
- self
65
82
  end
66
83
  end
67
84
  end
@@ -1,3 +1,3 @@
1
1
  class TappingDevice
2
- VERSION = "0.4.1"
2
+ VERSION = "0.4.2"
3
3
  end
@@ -99,13 +99,13 @@ class TappingDevice
99
99
  options[:descendants]
100
100
  end
101
101
 
102
- def record_call!(yield_parameters)
102
+ def record_call!(payload)
103
103
  return if @disabled
104
104
 
105
105
  if @block
106
- root_device.calls << @block.call(yield_parameters)
106
+ root_device.calls << @block.call(payload)
107
107
  else
108
- root_device.calls << yield_parameters
108
+ root_device.calls << payload
109
109
  end
110
110
  end
111
111
 
@@ -121,13 +121,13 @@ class TappingDevice
121
121
  if send(condition, object, validation_params)
122
122
  filepath, line_number = get_call_location(tp)
123
123
 
124
- next if should_be_skip_by_paths?(filepath)
124
+ next if should_be_skipped_by_paths?(filepath)
125
125
 
126
- yield_parameters = build_yield_parameters(tp: tp, filepath: filepath, line_number: line_number)
126
+ payload = build_payload(tp: tp, filepath: filepath, line_number: line_number)
127
127
 
128
- record_call!(yield_parameters)
128
+ record_call!(payload)
129
129
 
130
- stop_if_condition_fulfilled(yield_parameters)
130
+ stop_if_condition_fulfilled(payload)
131
131
  end
132
132
  end
133
133
 
@@ -136,22 +136,23 @@ class TappingDevice
136
136
  self
137
137
  end
138
138
 
139
- def get_call_location(tp)
139
+ def get_call_location(tp, padding: 0)
140
140
  if tp.event == :c_call
141
- caller(C_CALLER_START_POINT)
141
+ caller(C_CALLER_START_POINT + padding)
142
142
  else
143
- caller(CALLER_START_POINT)
143
+ caller(CALLER_START_POINT + padding)
144
144
  end.first.split(":")[0..1]
145
145
  end
146
146
 
147
147
  # this needs to be placed upfront so we can exclude noise before doing more work
148
- def should_be_skip_by_paths?(filepath)
148
+ def should_be_skipped_by_paths?(filepath)
149
149
  options[:exclude_by_paths].any? { |pattern| pattern.match?(filepath) } ||
150
150
  (options[:filter_by_paths].present? && !options[:filter_by_paths].any? { |pattern| pattern.match?(filepath) })
151
151
  end
152
152
 
153
- def build_yield_parameters(tp:, filepath:, line_number:)
154
- arguments = tp.binding.local_variables.map { |n| [n, tp.binding.local_variable_get(n)] }
153
+ def build_payload(tp:, filepath:, line_number:)
154
+ arguments = {}
155
+ tp.binding.local_variables.each { |name| arguments[name] = tp.binding.local_variable_get(name) }
155
156
 
156
157
  Payload.init({
157
158
  receiver: tp.self,
@@ -198,8 +199,8 @@ class TappingDevice
198
199
  options
199
200
  end
200
201
 
201
- def stop_if_condition_fulfilled(yield_parameters)
202
- if @stop_when&.call(yield_parameters)
202
+ def stop_if_condition_fulfilled(payload)
203
+ if @stop_when&.call(payload)
203
204
  stop!
204
205
  root_device.stop!
205
206
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tapping_device
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - st0012
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-12-06 00:00:00.000000000 Z
11
+ date: 2019-12-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -131,7 +131,6 @@ files:
131
131
  - lib/tapping_device.rb
132
132
  - lib/tapping_device/exceptions.rb
133
133
  - lib/tapping_device/payload.rb
134
- - lib/tapping_device/sql_listener.rb
135
134
  - lib/tapping_device/sql_tapping_methods.rb
136
135
  - lib/tapping_device/trackable.rb
137
136
  - lib/tapping_device/version.rb
@@ -1,10 +0,0 @@
1
- class TappingDevice
2
- class SqlListenser
3
- attr_reader :method, :payload, :device
4
- def initialize(method, payload, device)
5
- @method = method
6
- @payload = payload
7
- @device = device
8
- end
9
- end
10
- end