stenotype 0.1.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.
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stenotype
4
+ #
5
+ # A namespace containing extensions of various frameworks.
6
+ # For example Rails components could be extended
7
+ #
8
+ module Frameworks
9
+ #
10
+ # An extension for a plain Ruby class in order to track invocation of
11
+ # instance methods.
12
+ #
13
+ module ObjectExt
14
+ #
15
+ # Class methods for `Object` to be extended by
16
+ #
17
+ ClassMethodsExtension = Class.new(Module)
18
+ #
19
+ # Instance methods to be included into `Object` ancestors chain
20
+ #
21
+ InstanceMethodsExtension = Class.new(Module)
22
+
23
+ attr_reader :instance_mod,
24
+ :class_mod
25
+
26
+ # @!visibility private
27
+ def self.included(klass)
28
+ @instance_mod = InstanceMethodsExtension.new
29
+ @class_mod = ClassMethodsExtension.new
30
+
31
+ build_instance_methods
32
+ build_class_methods
33
+
34
+ klass.const_set(:InstanceProxy, Module.new)
35
+ klass.const_set(:ClassProxy, Module.new)
36
+
37
+ klass.send(:include, instance_mod)
38
+ klass.extend(class_mod)
39
+
40
+ super
41
+ end
42
+
43
+ #
44
+ # @!method emit_event(data = {}, method: caller_locations.first.label, eval_context: nil)
45
+ # A method injected into all instances of Object
46
+ # @!scope instance
47
+ # @param data {Hash} Data to be sent to the targets
48
+ # @param method {String} An optional method name
49
+ # @param eval_context {Hash} A hash linking object to context handler
50
+ # @return {Stenotype::Event} An instance of emitted event
51
+ #
52
+
53
+ #
54
+ # @!method emit_event_before(*methods)
55
+ # A method injected into all instances of Object
56
+ # @!scope instance
57
+ # @param methods {Array<Symbol>} A list of method before which an event will be emitted
58
+ #
59
+
60
+ #
61
+ # @!method emit_klass_event_before(*class_methods)
62
+ # A class method injected into all subclasses of [Object]
63
+ # @!scope class
64
+ # @param class_method {Array<Symbol>} A list of class method before which
65
+ # an event will be emitted
66
+ #
67
+
68
+ #
69
+ # rubocop:disable Metrics/MethodLength
70
+ # Adds two methods: [#emit_event] and [#emit_event_before] to every object
71
+ # inherited from [Object]
72
+ #
73
+ def build_instance_methods
74
+ instance_mod.class_eval <<-RUBY, __FILE__, __LINE__ + 1
75
+ def emit_event(data = {}, method: caller_locations.first.label, eval_context: nil)
76
+ Stenotype::Event.emit!(
77
+ {
78
+ type: 'class_instance',
79
+ **data,
80
+ },
81
+ options: {
82
+ class: self.class.name,
83
+ method: method.to_sym
84
+ },
85
+ eval_context: (eval_context || { klass: self })
86
+ )
87
+ end
88
+
89
+ def emit_event_before(*methods)
90
+ proxy = const_get(:InstanceProxy)
91
+
92
+ methods.each do |method|
93
+ proxy.module_eval do
94
+ define_method(method) do |*args, **rest_args, &block|
95
+ Stenotype::Event.emit!(
96
+ { type: 'class_instance' },
97
+ options: {
98
+ class: self.class.name,
99
+ method: __method__
100
+ },
101
+ eval_context: { klass: self }
102
+ )
103
+ super(*args, **rest_args, &block)
104
+ end
105
+ end
106
+ end
107
+
108
+ send(:prepend, proxy)
109
+ end
110
+ RUBY
111
+ end
112
+
113
+ #
114
+ # Adds class method [#emit_klass_event_before] to every class
115
+ # inherited from [Object]
116
+ #
117
+ def build_class_methods
118
+ class_mod.class_eval <<-RUBY, __FILE__, __LINE__ + 1
119
+ def emit_klass_event_before(*class_methods)
120
+ proxy = const_get(:ClassProxy)
121
+
122
+ class_methods.each do |method|
123
+ proxy.module_eval do
124
+ define_method(method) do |*args, **rest_args, &block|
125
+ Stenotype::Event.emit!(
126
+ { type: 'class' },
127
+ options: {
128
+ class: self.name,
129
+ method: __method__
130
+ },
131
+ eval_context: { klass: self }
132
+ )
133
+ super(*args, **rest_args, &block)
134
+ end
135
+ end
136
+
137
+ singleton_class.send(:prepend, proxy)
138
+ end
139
+ end
140
+ RUBY
141
+ end
142
+ # rubocop:enable Metrics/MethodLength
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module Stenotype
6
+ module Frameworks
7
+ module Rails
8
+ #
9
+ # An ActionController extension to be injected into classes
10
+ # inheriting from [ActionController::Base]
11
+ #
12
+ module ActionControllerExtension
13
+ extend ActiveSupport::Concern
14
+
15
+ #
16
+ # Emits and event with given data
17
+ # @param data {Hash} Data to be sent to targets
18
+ #
19
+ def record_freshly_event(data)
20
+ Stenotype::Event.emit!(data, options: {}, eval_context: { controller: self })
21
+ end
22
+
23
+ #
24
+ # Class methods to be injected into classes
25
+ # inherited from [ActionController::Base]
26
+ #
27
+ module ClassMethods
28
+ # Adds a before_action to each action from the passed list. A before action
29
+ # is emitting a {Stenotype::Event}. Please note that in case track_view is
30
+ # used several times, it will keep track of the actions which emit events.
31
+ # Each time a new track_view is called it will find a symmetric difference
32
+ # of two sets: set of 'used' actions and a set passed to `track_view`.
33
+ #
34
+ # @example
35
+ # class MyController < ActionController::Base
36
+ # track_view :index, :show
37
+ #
38
+ # def index
39
+ # # do_something
40
+ # end
41
+ #
42
+ # def show
43
+ # # do something
44
+ # end
45
+ #
46
+ # # Not covered by track_view
47
+ # def update
48
+ # # do something
49
+ # end
50
+ # end
51
+ #
52
+ # @param actions {Array<Symbol>} a list of tracked controller actions
53
+ #
54
+ def track_view(*actions)
55
+ # A symmetric difference of two sets.
56
+ # This prevents accidental duplicating of events
57
+ delta = (_tracked_actions - Set[*actions]) | (Set[*actions] - _tracked_actions)
58
+
59
+ return if delta.empty?
60
+
61
+ before_action only: delta do
62
+ record_freshly_event(type: 'view')
63
+ end
64
+
65
+ _tracked_actions.merge(delta)
66
+ end
67
+
68
+ # :nodoc:
69
+ def _tracked_actions
70
+ @_tracked_actions ||= Set.new
71
+ end
72
+
73
+ # Note this action will only define a symmetric difference of
74
+ # the covered with events actions and the ones not used yet.
75
+ # @see #track_view
76
+ #
77
+ # @example
78
+ # class MyController < ActionController::Base
79
+ # track_all_views
80
+ #
81
+ # def index
82
+ # # do_something
83
+ # end
84
+ #
85
+ # def show
86
+ # # do something
87
+ # end
88
+ # end
89
+ #
90
+ def track_all_views
91
+ actions = self.action_methods
92
+
93
+ # A symmetric difference of two sets.
94
+ # This prevents accidental duplicating of events
95
+ delta = ((_tracked_actions - actions) | (actions - _tracked_actions))
96
+
97
+ return if delta.empty?
98
+
99
+ before_action only: delta.to_a do
100
+ record_freshly_event(type: "view")
101
+ end
102
+
103
+ # merge is a mutating op
104
+ _tracked_actions.merge(delta)
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module Stenotype
6
+ module Frameworks
7
+ #
8
+ # A namespace containing extensions for Ruby on Rails components
9
+ #
10
+ module Rails
11
+ #
12
+ # An extension for ActiveJob to enable adding a hook
13
+ # before performing an instance of [ActiveJob::Base] subclass
14
+ #
15
+ module ActiveJobExtension
16
+ # @!visibility private
17
+ def self.extended(base)
18
+ base.const_set(:JobExt, Module.new)
19
+ super
20
+ end
21
+
22
+ #
23
+ # @example
24
+ # class MyJob < ApplicationJob
25
+ # trackable_job! # => will prepend a perform action with event recorder
26
+ #
27
+ # def perform(data)
28
+ # # do_something
29
+ # end
30
+ # end
31
+ #
32
+ # rubocop:disable Metrics/MethodLength
33
+ #
34
+ def trackable_job!
35
+ proxy = const_get(:JobExt)
36
+ proxy.module_eval do
37
+ define_method(:perform) do |*args, **rest_args, &block|
38
+ Stenotype::Event.emit!(
39
+ { type: "active_job" },
40
+ options: {},
41
+ eval_context: { active_job: self }
42
+ )
43
+ super(*args, **rest_args, &block)
44
+ end
45
+ end
46
+
47
+ # Prepend an instance of module so that
48
+ # super() can be chained down the ancestors
49
+ # without changing existing ActiveJob interface
50
+ #
51
+ send(:prepend, proxy)
52
+ end
53
+ # rubocop:enable Metrics/MethodLength
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stenotype
4
+ # :nodoc:
5
+ VERSION = '0.1.0'
6
+ # :nodoc:
7
+ end
data/lib/stenotype.rb ADDED
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # A top level namespace for the freshly-events gem
5
+ #
6
+ module Stenotype
7
+ class << self
8
+ ##
9
+ # Configures the library.
10
+ # @yield {Stenotype::Configuration}
11
+ #
12
+ # @example
13
+ #
14
+ # Stenotype.configure do |config|
15
+ # config.targets = [
16
+ # Stenotype::Adapters::StdoutAdapter.new,
17
+ # Stenotype::Adapters::GoogleCloud.new
18
+ # ]
19
+ #
20
+ # config.dispatcher = Stenotype::Dispatcher.new
21
+ # config.gc_project_id = ENV['GC_PROJECT_ID']
22
+ # config.gc_credentials = ENV['GC_CREDENTIALS']
23
+ # config.gc_topic = ENV['GC_TOPIC']
24
+ # config.gc_mode = :async
25
+ # end
26
+ #
27
+ def configure(&block)
28
+ Stenotype::Configuration.configure(&block)
29
+ end
30
+
31
+ ##
32
+ # @return {Stenotype::Configuration}
33
+ #
34
+ def config
35
+ Stenotype::Configuration
36
+ end
37
+ end
38
+ end
39
+
40
+ require "stenotype/adapters"
41
+ require "stenotype/configuration"
42
+ require "stenotype/context_handlers"
43
+ require "stenotype/dispatcher"
44
+ require "stenotype/event"
45
+ require "stenotype/event_serializer"
46
+ require "stenotype/exceptions"
47
+ require "stenotype/version"
48
+ require "stenotype/frameworks/object_ext"
49
+
50
+ Stenotype.configure do |config|
51
+ config.uuid_generator = SecureRandom
52
+ config.dispatcher = Stenotype::Dispatcher.new
53
+
54
+ config.gc_project_id = ENV['GC_PROJECT_ID']
55
+ config.gc_credentials = ENV['GC_CREDENTIALS']
56
+ config.gc_topic = ENV['GC_TOPIC']
57
+ config.gc_mode = :async
58
+
59
+ config.targets = [
60
+ Stenotype::Adapters::StdoutAdapter.new,
61
+ Stenotype::Adapters::GoogleCloud.new
62
+ ]
63
+
64
+ Stenotype::ContextHandlers.module_eval do
65
+ register Stenotype::ContextHandlers::Klass
66
+ end
67
+
68
+ Object.send(:include, Stenotype::Frameworks::ObjectExt)
69
+ end
70
+
71
+ if defined?(Rails)
72
+ require "stenotype/frameworks/rails/action_controller"
73
+ require "stenotype/frameworks/rails/active_job"
74
+
75
+ module Stenotype
76
+ class Railtie < Rails::Railtie # :nodoc:
77
+ config.stenotype = Stenotype.config
78
+
79
+ #
80
+ # Register Rails handlers
81
+ #
82
+ Stenotype::ContextHandlers.module_eval do
83
+ register Stenotype::ContextHandlers::Rails::Controller
84
+ register Stenotype::ContextHandlers::Rails::ActiveJob
85
+ end
86
+
87
+ ActiveSupport.on_load(:action_controller) do
88
+ include Stenotype::Frameworks::Rails::ActionControllerExtension
89
+ end
90
+
91
+ ActiveSupport.on_load(:active_job) do
92
+ # @todo: consider using `::ActiveJob::Base.around_perform`
93
+ # or `::ActiveJob::Base.around_enqueue`
94
+ extend Stenotype::Frameworks::Rails::ActiveJobExtension
95
+ end
96
+ end
97
+ end
98
+ end
data/stenotype.gemspec ADDED
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path("lib", __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+
6
+ require "stenotype/version"
7
+
8
+ Gem::Specification.new do |spec|
9
+ spec.name = 'stenotype'
10
+ spec.version = Stenotype::VERSION
11
+ spec.authors = ["Roman Kapitonov"]
12
+ spec.email = ["roman.kapitonov@freshly.com"]
13
+
14
+ spec.summary = 'Gem for emitting events and sending them to an external system.'
15
+ spec.description = 'Pretty much it'
16
+ spec.homepage = 'https://github.com/Freshly/stenotype'
17
+ spec.license = 'MIT'
18
+
19
+ spec.metadata['homepage_uri'] = spec.homepage
20
+ spec.metadata['source_code_uri'] = 'https://github.com/Freshly/stenotype'
21
+ spec.metadata['changelog_uri'] = 'https://github.com/Freshly/stenotype/CHANGELOG.md'
22
+
23
+ # Specify which files should be added to the gem when it is released.
24
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
26
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
27
+ end
28
+ spec.bindir = 'exe'
29
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
30
+ spec.require_paths = ['lib']
31
+
32
+ spec.add_dependency 'activesupport', '>= 5.0.0'
33
+ spec.add_dependency 'google-cloud-pubsub', '~> 1.0.0'
34
+
35
+ spec.add_development_dependency 'bundler', '~> 2.0'
36
+ spec.add_development_dependency 'github-markup', '~> 3.0'
37
+ spec.add_development_dependency 'rake', '~> 10.0'
38
+ spec.add_development_dependency 'redcarpet', '~> 3.5'
39
+ spec.add_development_dependency 'yard', '~> 0.9'
40
+
41
+ spec.add_development_dependency 'pry', '~> 0.12'
42
+ spec.add_development_dependency 'rails', '~> 5.2.3'
43
+ spec.add_development_dependency 'rspec', '~> 3.0'
44
+ spec.add_development_dependency 'rubocop', '~> 0.76'
45
+ spec.add_development_dependency 'simplecov', '~> 0.17'
46
+ spec.add_development_dependency 'timecop', '~> 0.9'
47
+ end