tapping_device 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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