journaled 4.2.0 → 4.3.0

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: bfdabb8b24bdfdf23c4f77ec819f03e89a4a0239ed997368eb1f07ac0bc2cd5b
4
- data.tar.gz: 97eeec1dfa22f7b683f23e322eb663e6fae4b47e0c2786690c4f44166b8cca51
3
+ metadata.gz: dc6d5adec8f9ee32ba34d1ba8dae8cb3da062e39dd1b67d5fc3e219be116d675
4
+ data.tar.gz: ae77057a26203e90fbb8770e1b41864edcff69058cd80b65011eb414ddf2365a
5
5
  SHA512:
6
- metadata.gz: 78c8b888f9c00a084e8d60c2f271d56a9a188f60287cb30edfbadc7a4f9fc1c3fc3210c763e757658a0040fb57f6f94f1a713fc1f95942db801e2e114dd50ec3
7
- data.tar.gz: f0e56609680ecfc3e1f3f3a9d2ba801515762311307f0a208757a70a82ff7c37ba6af040f8ee5a64ebbf7a08a910579c27d65e940680b2406383b8eca022f323
6
+ metadata.gz: 05a99d3d5d530bca784fb44fa290d0ff661430263fbde8fa882f8c98a9dda2eabe76c82df142b6477e68439066cba52d66d875c035a7af6e9d6a2938c8985b82
7
+ data.tar.gz: 7726aa5152e545cdba1a50ef475f8df0ce37fb97edc2d4fa146c74f73b41c7096434948afeecbe61bc042138cae9c3b94dda475adbf9c74d42892b1d6286e069
data/README.md CHANGED
@@ -179,52 +179,6 @@ journaling. Note that the less-frequently-used methods `toggle`,
179
179
  `increment*`, `decrement*`, and `update_counters` are not intercepted at
180
180
  this time.
181
181
 
182
- ### Tagged Events
183
-
184
- Events may be optionally marked as "tagged." This will add a `tags` field, intended for tracing and
185
- auditing purposes.
186
-
187
- ```ruby
188
- class MyEvent
189
- include Journaled::Event
190
-
191
- journal_attributes :attr_1, :attr_2, tagged: true
192
- end
193
- ```
194
-
195
- You may then use `Journaled.tag!` and `Journaled.tagged` inside of your
196
- `ApplicationController` and `ApplicationJob` classes (or anywhere else!) to tag
197
- all events with request and job metadata:
198
-
199
- ```ruby
200
- class ApplicationController < ActionController::Base
201
- before_action do
202
- Journaled.tag!(request_id: request.request_id, current_user_id: current_user&.id)
203
- end
204
- end
205
-
206
- class ApplicationJob < ActiveJob::Base
207
- around_perform do |job, perform|
208
- Journaled.tagged(job_id: job.id) { perform.call }
209
- end
210
- end
211
- ```
212
-
213
- This feature relies on `ActiveSupport::CurrentAttributes` under the hood, so these tags are local to
214
- the current thread, and will be cleared at the end of each request request/job.
215
-
216
- #### Testing
217
-
218
- If you use RSpec (and have required `journaled/rspec` in your
219
- `spec/rails_helper.rb`), you can regression-protect important journaling
220
- config with the `journal_changes_to` matcher:
221
-
222
- ```ruby
223
- it "journals exactly these things or there will be heck to pay" do
224
- expect(User).to journal_changes_to(:email, :first_name, :last_name, as: :identity_change)
225
- end
226
- ```
227
-
228
182
  ### Custom Journaling
229
183
 
230
184
  For every custom implementation of journaling in your application, define the JSON schema for the attributes in your event.
@@ -309,6 +263,40 @@ An event like the following will be journaled to kinesis:
309
263
  }
310
264
  ```
311
265
 
266
+ ### Tagged Events
267
+
268
+ Events may be optionally marked as "tagged." This will add a `tags` field, intended for tracing and
269
+ auditing purposes.
270
+
271
+ ```ruby
272
+ class MyEvent
273
+ include Journaled::Event
274
+
275
+ journal_attributes :attr_1, :attr_2, tagged: true
276
+ end
277
+ ```
278
+
279
+ You may then use `Journaled.tag!` and `Journaled.tagged` inside of your
280
+ `ApplicationController` and `ApplicationJob` classes (or anywhere else!) to tag
281
+ all events with request and job metadata:
282
+
283
+ ```ruby
284
+ class ApplicationController < ActionController::Base
285
+ before_action do
286
+ Journaled.tag!(request_id: request.request_id, current_user_id: current_user&.id)
287
+ end
288
+ end
289
+
290
+ class ApplicationJob < ActiveJob::Base
291
+ around_perform do |job, perform|
292
+ Journaled.tagged(job_id: job.id) { perform.call }
293
+ end
294
+ end
295
+ ```
296
+
297
+ This feature relies on `ActiveSupport::CurrentAttributes` under the hood, so these tags are local to
298
+ the current thread, and will be cleared at the end of each request request/job.
299
+
312
300
  ### Helper methods for custom events
313
301
 
314
302
  Journaled provides a couple helper methods that may be useful in your
@@ -352,6 +340,102 @@ Returns one of the following in order of preference:
352
340
  In order for this to be most useful, you must configure your controller
353
341
  as described in [Change Journaling](#change-journaling) above.
354
342
 
343
+ ### Testing
344
+
345
+ If you use RSpec, you can test for journaling behaviors with the
346
+ `journal_event(s)_including` and `journal_changes_to` matchers. First, make
347
+ sure to require `journaled/rspec` in your spec setup (e.g.
348
+ `spec/rails_helper.rb`):
349
+
350
+ ```ruby
351
+ require 'journaled/rspec'
352
+ ```
353
+
354
+ #### Checking for specific events
355
+
356
+ The `journal_event_including` and `journal_events_including` matchers allow you
357
+ to check for one or more matching event being journaled:
358
+
359
+ ```ruby
360
+ expect { my_code }
361
+ .to journal_event_including(name: 'foo')
362
+ expect { my_code }
363
+ .to journal_events_including({ name: 'foo', value: 1 }, { name: 'foo', value: 2 })
364
+ ```
365
+
366
+ This will only perform matches on the specified fields (and will not match one
367
+ way or the other against unspecified fields). These matchers will also ignore
368
+ any extraneous events that are not positively matched (as they may be unrelated
369
+ to behavior under test).
370
+
371
+ When writing tests, pairing every positive assertion with a negative assertion
372
+ is a good practice, and so negative matching is also supported (via both
373
+ `.not_to` and `.to not_`):
374
+
375
+ ```ruby
376
+ expect { my_code }
377
+ .not_to journal_events_including({ name: 'foo' }, { name: 'bar' })
378
+ expect { my_code }
379
+ .to raise_error(SomeError)
380
+ .and not_journal_event_including(name: 'foo') # the `not_` variant can chain off of `.and`
381
+ ```
382
+
383
+ Several chainable modifiers are also available:
384
+
385
+ ```ruby
386
+ expect { my_code }.to journal_event_including(name: 'foo')
387
+ .with_schema_name('my_event_schema')
388
+ .with_partition_key(user.id)
389
+ .with_stream_name('my_stream_name')
390
+ .with_enqueue_opts(run_at: future_time)
391
+ .with_priority(999)
392
+ ```
393
+
394
+ All of this can be chained together to test for multiple sets of events with
395
+ multiple sets of options:
396
+
397
+ ```ruby
398
+ expect { subject.journal! }
399
+ .to journal_events_including({ name: 'event1', value: 300 }, { name: 'event2', value: 200 })
400
+ .with_priority(10)
401
+ .and journal_event_including(name: 'event3', value: 100)
402
+ .with_priority(20)
403
+ .and not_journal_event_including(name: 'other_event')
404
+ ```
405
+
406
+ #### Checking for `Journaled::Changes` declarations
407
+
408
+ The `journal_changes_to` matcher checks against the list of attributes specified
409
+ on the model. It does not actually test that an event is emitted within a given
410
+ codepath, and is instead intended to guard against accidental regressions that
411
+ may impact external consumers of these events:
412
+
413
+ ```ruby
414
+ it "journals exactly these things or there will be heck to pay" do
415
+ expect(User).to journal_changes_to(:email, :first_name, :last_name, as: :identity_change)
416
+ end
417
+ ```
418
+
419
+ ### Instrumentation
420
+
421
+ When an event is enqueued, an `ActiveSupport::Notification` titled
422
+ `journaled.event.enqueue` is emitted. Its payload will include the `:event` and
423
+ its background job `:priority`.
424
+
425
+ This can be forwarded along to your preferred monitoring solution via a Rails
426
+ initializer:
427
+
428
+ ```ruby
429
+ ActiveSupport::Notifications.subscribe('journaled.event.enqueue') do |*args|
430
+ payload = ActiveSupport::Notifications::Event.new(*args).payload
431
+ journaled_event = payload[:event]
432
+
433
+ tags = { priority: payload[:priority], event_type: journaled_event.journaled_attributes[:event_type] }
434
+
435
+ Statsd.increment('journaled.event.enqueue', tags: tags.map { |k,v| "#{k.to_s[0..64]}:#{v.to_s[0..255]}" })
436
+ end
437
+ ```
438
+
355
439
  ## Upgrades
356
440
 
357
441
  Since this gem relies on background jobs (which can remain in the queue across
@@ -63,7 +63,7 @@ module Journaled::Event
63
63
  end
64
64
 
65
65
  included do
66
- cattr_accessor(:journaled_enqueue_opts, instance_writer: false) { {} }
66
+ class_attribute :journaled_enqueue_opts, default: {}
67
67
 
68
68
  journal_attributes :id, :event_type, :created_at
69
69
  end
@@ -26,9 +26,9 @@ class Journaled::Writer
26
26
 
27
27
  def journal!
28
28
  validate!
29
- Journaled::DeliveryJob
30
- .set(journaled_enqueue_opts.reverse_merge(priority: Journaled.job_priority))
31
- .perform_later(**delivery_perform_args)
29
+ ActiveSupport::Notifications.instrument('journaled.event.enqueue', event: journaled_event, priority: job_opts[:priority]) do
30
+ Journaled::DeliveryJob.set(job_opts).perform_later(**delivery_perform_args)
31
+ end
32
32
  end
33
33
 
34
34
  private
@@ -43,6 +43,10 @@ class Journaled::Writer
43
43
  schema_validator(journaled_schema_name).validate! serialized_event
44
44
  end
45
45
 
46
+ def job_opts
47
+ journaled_enqueue_opts.reverse_merge(priority: Journaled.job_priority)
48
+ end
49
+
46
50
  def delivery_perform_args
47
51
  {
48
52
  serialized_event: serialized_event,
@@ -16,3 +16,89 @@ RSpec::Matchers.define :journal_changes_to do |*attribute_names, as:|
16
16
  "expected #{model_class} not to journal changes to #{attribute_names.map(&:inspect).join(', ')} as #{as.inspect}"
17
17
  end
18
18
  end
19
+
20
+ RSpec::Matchers.define_negated_matcher :not_journal_changes_to, :journal_changes_to
21
+
22
+ RSpec::Matchers.define :journal_events_including do |*expected_events|
23
+ raise "Please specify at least one expected event. RSpec argument matchers are supported." if expected_events.empty?
24
+
25
+ attr_accessor :expected, :actual, :matches, :nonmatches
26
+
27
+ chain :with_schema_name, :expected_schema_name
28
+ chain :with_partition_key, :expected_partition_key
29
+ chain :with_stream_name, :expected_stream_name
30
+ chain :with_enqueue_opts, :expected_enqueue_opts
31
+ chain :with_priority, :expected_priority
32
+
33
+ def supports_block_expectations?
34
+ true
35
+ end
36
+
37
+ def hash_including_recursive(hash)
38
+ hash_including(
39
+ hash.transform_values { |v| v.is_a?(Hash) ? hash_including_recursive(v) : v },
40
+ )
41
+ end
42
+
43
+ match do |block|
44
+ expected_events = [expected_events.first].flatten(1) unless expected_events.length > 1
45
+
46
+ self.expected = expected_events.map { |e| { journaled_attributes: e } }
47
+ expected.each { |e| e.merge!(journaled_schema_name: expected_schema_name) } if expected_schema_name
48
+ expected.each { |e| e.merge!(journaled_partition_key: expected_partition_key) } if expected_partition_key
49
+ expected.each { |e| e.merge!(journaled_stream_name: expected_stream_name) } if expected_stream_name
50
+ expected.each { |e| e.merge!(journaled_enqueue_opts: expected_enqueue_opts) } if expected_enqueue_opts
51
+ expected.each { |e| e.merge!(priority: expected_priority) } if expected_priority
52
+ self.actual = []
53
+
54
+ callback = ->(_name, _started, _finished, _unique_id, payload) do
55
+ event = payload[:event]
56
+ a = { journaled_attributes: event.journaled_attributes }
57
+ a[:journaled_schema_name] = event.journaled_schema_name if expected_schema_name
58
+ a[:journaled_partition_key] = event.journaled_partition_key if expected_partition_key
59
+ a[:journaled_stream_name] = event.journaled_stream_name if expected_stream_name
60
+ a[:journaled_enqueue_opts] = event.journaled_enqueue_opts if expected_enqueue_opts
61
+ a[:priority] = payload[:priority] if expected_priority
62
+ actual << a
63
+ end
64
+
65
+ ActiveSupport::Notifications.subscribed(callback, 'journaled.event.enqueue', &block)
66
+
67
+ self.matches = actual.select do |a|
68
+ expected.any? { |e| values_match?(hash_including_recursive(e), a) }
69
+ end
70
+
71
+ self.nonmatches = actual - matches
72
+
73
+ exact_matches = matches.dup
74
+ matches.count == expected.count && expected.all? do |e|
75
+ match, index = exact_matches.each_with_index.find { |a, _| values_match?(hash_including_recursive(e), a) }
76
+ exact_matches.delete_at(index) if match
77
+ end && exact_matches.empty?
78
+ end
79
+
80
+ failure_message do
81
+ <<~MSG
82
+ Expected the code block to journal exactly one matching event per expected event.
83
+
84
+ Expected Events (#{expected.count}):
85
+ ===============================================================================
86
+ #{expected.map(&:to_json).join("\n ")}
87
+ ===============================================================================
88
+
89
+ Matching Events (#{matches.count}):
90
+ ===============================================================================
91
+ #{matches.map(&:to_json).join("\n ")}
92
+ ===============================================================================
93
+
94
+ Non-Matching Events (#{nonmatches.count}):
95
+ ===============================================================================
96
+ #{nonmatches.map(&:to_json).join("\n ")}
97
+ ===============================================================================
98
+ MSG
99
+ end
100
+ end
101
+
102
+ RSpec::Matchers.alias_matcher :journal_event_including, :journal_events_including
103
+ RSpec::Matchers.define_negated_matcher :not_journal_events_including, :journal_events_including
104
+ RSpec::Matchers.define_negated_matcher :not_journal_event_including, :journal_event_including
@@ -1,3 +1,3 @@
1
1
  module Journaled
2
- VERSION = "4.2.0".freeze
2
+ VERSION = "4.3.0".freeze
3
3
  end
@@ -44,7 +44,7 @@ RSpec.describe Journaled::Changes do
44
44
 
45
45
  subject { klass.new }
46
46
 
47
- let(:change_writer) { double(Journaled::ChangeWriter, create: true, update: true, delete: true) }
47
+ let(:change_writer) { instance_double(Journaled::ChangeWriter, create: true, update: true, delete: true) }
48
48
 
49
49
  before do
50
50
  allow(Journaled::ChangeWriter).to receive(:new) do |opts|
@@ -1,11 +1,12 @@
1
1
  require 'rails_helper'
2
2
 
3
3
  RSpec.describe Journaled::Writer do
4
+ let(:event_class) { Class.new { include Journaled::Event } }
4
5
  subject { described_class.new journaled_event: journaled_event }
5
6
 
6
7
  describe '#initialize' do
7
8
  context 'when the Journaled Event does not implement all the necessary methods' do
8
- let(:journaled_event) { double }
9
+ let(:journaled_event) { instance_double(event_class) }
9
10
 
10
11
  it 'raises on initialization' do
11
12
  expect { subject }.to raise_error RuntimeError, /An enqueued event must respond to/
@@ -14,7 +15,8 @@ RSpec.describe Journaled::Writer do
14
15
 
15
16
  context 'when the Journaled Event returns non-present values for some of the required methods' do
16
17
  let(:journaled_event) do
17
- double(
18
+ instance_double(
19
+ event_class,
18
20
  journaled_schema_name: nil,
19
21
  journaled_attributes: {},
20
22
  journaled_partition_key: '',
@@ -30,7 +32,8 @@ RSpec.describe Journaled::Writer do
30
32
 
31
33
  context 'when the Journaled Event complies with the API' do
32
34
  let(:journaled_event) do
33
- double(
35
+ instance_double(
36
+ event_class,
34
37
  journaled_schema_name: :fake_schema_name,
35
38
  journaled_attributes: { foo: :bar },
36
39
  journaled_partition_key: 'fake_partition_key',
@@ -71,8 +74,9 @@ RSpec.describe Journaled::Writer do
71
74
 
72
75
  let(:journaled_enqueue_opts) { {} }
73
76
  let(:journaled_event) do
74
- double(
75
- journaled_schema_name: :fake_schema_name,
77
+ instance_double(
78
+ event_class,
79
+ journaled_schema_name: 'fake_schema_name',
76
80
  journaled_attributes: journaled_event_attributes,
77
81
  journaled_partition_key: 'fake_partition_key',
78
82
  journaled_stream_name: 'my_app_events',
@@ -85,7 +89,9 @@ RSpec.describe Journaled::Writer do
85
89
  let(:journaled_event_attributes) { { foo: 1 } }
86
90
 
87
91
  it 'raises an error and does not enqueue anything' do
88
- expect { subject.journal! }.to raise_error JSON::Schema::ValidationError
92
+ expect { subject.journal! }
93
+ .to raise_error(JSON::Schema::ValidationError)
94
+ .and not_journal_event_including(anything)
89
95
  expect(enqueued_jobs.count).to eq 0
90
96
  end
91
97
  end
@@ -95,7 +101,9 @@ RSpec.describe Journaled::Writer do
95
101
  let(:journaled_event_attributes) { { id: 'FAKE_UUID', event_type: 'fake_event', created_at: Time.zone.now, foo: 1 } }
96
102
 
97
103
  it 'raises an error and does not enqueue anything' do
98
- expect { subject.journal! }.to raise_error JSON::Schema::ValidationError
104
+ expect { subject.journal! }
105
+ .to raise_error(JSON::Schema::ValidationError)
106
+ .and not_journal_event_including(anything)
99
107
  expect(enqueued_jobs.count).to eq 0
100
108
  end
101
109
  end
@@ -104,7 +112,13 @@ RSpec.describe Journaled::Writer do
104
112
  let(:journaled_event_attributes) { { id: 'FAKE_UUID', event_type: 'fake_event', created_at: Time.zone.now, foo: :bar } }
105
113
 
106
114
  it 'creates a delivery with the app name passed through' do
107
- expect { subject.journal! }.to change { enqueued_jobs.count }.from(0).to(1)
115
+ expect { subject.journal! }
116
+ .to change { enqueued_jobs.count }.from(0).to(1)
117
+ .and journal_event_including(journaled_event_attributes)
118
+ .with_schema_name('fake_schema_name')
119
+ .with_partition_key('fake_partition_key')
120
+ .with_stream_name('my_app_events')
121
+ .with_enqueue_opts({})
108
122
  expect(enqueued_jobs.first[:args].first).to include('stream_name' => 'my_app_events')
109
123
  end
110
124
 
@@ -124,6 +138,13 @@ RSpec.describe Journaled::Writer do
124
138
  enqueued_jobs.count { |j| j['job_class'] == 'Journaled::DeliveryJob' && j['priority'] == 999 }
125
139
  end
126
140
  }.from(0).to(1)
141
+ .and journal_event_including(journaled_event_attributes)
142
+ .with_schema_name('fake_schema_name')
143
+ .with_partition_key('fake_partition_key')
144
+ .with_stream_name('my_app_events')
145
+ .with_priority(999)
146
+ .and not_journal_event_including(anything)
147
+ .with_enqueue_opts(priority: 999) # with_enqueue_opts looks at event itself
127
148
  end
128
149
  end
129
150
 
@@ -138,6 +159,12 @@ RSpec.describe Journaled::Writer do
138
159
  enqueued_jobs.count { |j| j['job_class'] == 'Journaled::DeliveryJob' && j['priority'] == 13 }
139
160
  end
140
161
  }.from(0).to(1)
162
+ .and journal_event_including(journaled_event_attributes)
163
+ .with_schema_name('fake_schema_name')
164
+ .with_partition_key('fake_partition_key')
165
+ .with_stream_name('my_app_events')
166
+ .with_priority(13)
167
+ .with_enqueue_opts(priority: 13)
141
168
  end
142
169
  end
143
170
  end
@@ -154,7 +181,9 @@ RSpec.describe Journaled::Writer do
154
181
  end
155
182
 
156
183
  it 'raises an error and does not enqueue anything' do
157
- expect { subject.journal! }.to raise_error JSON::Schema::ValidationError
184
+ expect { subject.journal! }
185
+ .to not_journal_events_including(anything)
186
+ .and raise_error JSON::Schema::ValidationError
158
187
  expect(enqueued_jobs.count).to eq 0
159
188
  end
160
189
  end
@@ -165,7 +194,16 @@ RSpec.describe Journaled::Writer do
165
194
  end
166
195
 
167
196
  it 'creates a delivery with the app name passed through' do
168
- expect { subject.journal! }.to change { enqueued_jobs.count }.from(0).to(1)
197
+ expect { subject.journal! }
198
+ .to change { enqueued_jobs.count }.from(0).to(1)
199
+ .and journal_event_including(journaled_event_attributes)
200
+ .with_schema_name('fake_schema_name')
201
+ .with_partition_key('fake_partition_key')
202
+ .with_stream_name('my_app_events')
203
+ .with_enqueue_opts({})
204
+ .with_priority(Journaled.job_priority)
205
+ .and not_journal_event_including(anything)
206
+ .with_enqueue_opts(priority: Journaled.job_priority)
169
207
  expect(enqueued_jobs.first[:args].first).to include('stream_name' => 'my_app_events')
170
208
  end
171
209
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: journaled
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.2.0
4
+ version: 4.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jake Lipson
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2022-03-17 00:00:00.000000000 Z
14
+ date: 2022-04-07 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: activejob