itly-sdk 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f956ac42d9761509feed18e020c4c5afee5cfa5928f099e1895b982e59dd5d10
4
+ data.tar.gz: e980e7b80fab1c12ce52ec928afbd264221a1d0ba9d1ba9570c69c849e9a42a9
5
+ SHA512:
6
+ metadata.gz: 85ab4bfde5fc16accc2629d83417ca697dfbdd6511ad26b95bfdde8cdff39346f76034897ae028916d452ed1a37afe8a37e4a2a406cf19187dbfbfd9352cd20e
7
+ data.tar.gz: 5b90b6d14135f194aa63c1b3d0cd09119a11d1b84cae7b3f93d84455220a97c7236f51606250c7aadd2309aef085df334c6e1cfbd72e8d7dd9ac476b148a1777
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in itly-sdk.gemspec
6
+ gemspec
7
+
8
+ gem 'rake', '~> 13.0'
9
+
10
+ gem 'rbs', '~> 1.0'
11
+ gem 'rspec'
12
+ gem 'steep', '~> 0.41'
data/Steepfile ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ target :lib do
4
+ signature 'sig'
5
+
6
+ check 'lib'
7
+
8
+ library 'logger', 'set', 'itly-sdk'
9
+ end
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'itly-sdk'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start(__FILE__)
data/bin/rspec ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Configure and load RBS
5
+ unless ENV['DISABLE_TYPE_CHECKING']
6
+ ENV['RBS_TEST_TARGET'] = 'Itly::*, AcceptancePlugin, AcceptancePluginCallOptions, FakeCallOptions'
7
+ ENV['RBS_TEST_LOGLEVEL'] = 'warn'
8
+ ENV['RBS_TEST_DOUBLE_SUITE'] = 'rspec'
9
+ ENV['RBS_TEST_OPT'] = '-I./sig -I./spec/sig'
10
+
11
+ require 'rbs/test/setup'
12
+ end
13
+
14
+ # Start RSpec
15
+ require 'rspec/core'
16
+
17
+ ENV['RSPEC_RUN_FROM_SCRIPT'] = 'true'
18
+ RSpec::Core::Runner.invoke
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/itly-sdk.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/itly/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'itly-sdk'
7
+ spec.version = Itly::VERSION
8
+ spec.authors = ['Iteratively', 'Benjamin Bouchet', 'Justin Fiedler', 'Andrey Sokolov']
9
+ spec.email = ['support@iterative.ly']
10
+
11
+ spec.summary = 'Iteratively SDK for Ruby'
12
+ spec.description = 'Track and validate analytics with a unified, extensible interface ' \
13
+ 'that works with all your 3rd party analytics providers.'
14
+ spec.homepage = 'https://github.com/iterativelyhq/itly-sdk-ruby'
15
+ spec.license = 'MIT'
16
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.6.0')
17
+
18
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org/'
19
+
20
+ spec.metadata['homepage_uri'] = spec.homepage
21
+ spec.metadata['source_code_uri'] = 'https://github.com/iterativelyhq/itly-sdk-ruby/sdk'
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{\A(?:test|spec|features)/}) }
27
+ end
28
+ spec.require_paths = ['lib']
29
+ end
data/lib/itly-sdk.rb ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'itly/version'
4
+ require_relative 'itly/options/environment'
5
+ require_relative 'itly/options/validation'
6
+ require_relative 'itly/exceptions'
7
+ require_relative 'itly/validation_response'
8
+ require_relative 'itly/plugin_call_options'
9
+ require_relative 'itly/plugin_options'
10
+ require_relative 'itly/plugins'
11
+ require_relative 'itly/plugin'
12
+ require_relative 'itly/options'
13
+ require_relative 'itly/event'
14
+ require_relative 'itly/itly'
15
+ require_relative 'itly/loggers'
data/lib/itly/event.rb ADDED
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Itly
4
+ ##
5
+ # Event object used to communicate data between Itly core SDK and its plugins
6
+ #
7
+ # Properties:
8
+ # +name+: The event's name.
9
+ # +properties+: The event's properties.
10
+ # +id+: The event's unique ID in Iteratively.
11
+ # +version+: The event's version, e.g. 2.0.1.
12
+ # +plugins+: Granular Event Destinations: to control to which plugin to forward this event to
13
+ #
14
+ class Event
15
+ attr_reader :name, :properties, :id, :version, :plugins
16
+
17
+ ##
18
+ # Create a new Event object
19
+ #
20
+ # @param [String] name: The event's name.
21
+ # @param [Hash] properties: The event's properties.
22
+ # @param [String] id: The event's unique ID in Iteratively.
23
+ # @param [String] version: The event's version, e.g. 2.0.1.
24
+ # @param [Hash] plugins: Granular Event Destinations: to control to which plugin to forward this event to
25
+ #
26
+ def initialize(name:, properties: {}, id: nil, version: nil, plugins: {})
27
+ @name = name
28
+ @properties = properties
29
+ @id = id
30
+ @version = version
31
+ @plugins = plugins.transform_keys(&:to_s)
32
+ end
33
+
34
+ ##
35
+ # Describe the object
36
+ #
37
+ # @return [String] the object description
38
+ #
39
+ def to_s
40
+ str = "#<#{self.class.name}: name: #{name}, "
41
+ str += "id: #{id}, " unless id.nil?
42
+ str += "version: #{version}, " unless version.nil?
43
+ str + "properties: #{properties}>"
44
+ end
45
+
46
+ ##
47
+ # Compare the object to another
48
+ #
49
+ # @param [Object] other: the object to compare to
50
+ #
51
+ # @return [True/False] are the objects similar
52
+ #
53
+ def ==(other)
54
+ other.class == self.class && [name, properties, id, version] ==
55
+ [other.name, other.properties, other.id, other.version]
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Itly main class
4
+ class Itly
5
+ class InitializationError < StandardError; end
6
+
7
+ class ValidationError < StandardError; end
8
+
9
+ class RemoteError < StandardError; end
10
+ end
data/lib/itly/itly.rb ADDED
@@ -0,0 +1,404 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # Itly main class
5
+ #
6
+ class Itly
7
+ include Itly::Plugins
8
+
9
+ ##
10
+ # Create a new Itly object.
11
+ #
12
+ # The +is_initialized+ instance variable is a True/False flag indicating
13
+ # if the +load+ method was called on the object.
14
+ #
15
+ def initialize
16
+ @is_initialized = false
17
+ end
18
+
19
+ ##
20
+ # Load options ans the plugins. It must be called only once on an object.
21
+ #
22
+ # Accept an optional block to define the options. The variable yielded in
23
+ # the block is of type `Itly::Options`.
24
+ #
25
+ # Calls the +load+ method of each plugin passing the +options+ object as an argument.
26
+ #
27
+ # @param [Hash, nil] context: to assign to the "context" Event object. Default to nil
28
+ #
29
+ def load(context: nil)
30
+ # Ensure #load was not already called on this object
31
+ raise InitializationError, 'Itly is already initialized.' if @is_initialized
32
+
33
+ # Create a new Options object and yield it is a block is provided
34
+ @options = Itly::Options.new
35
+ yield @options if block_given?
36
+
37
+ # Create the context event
38
+ @context = context.nil? ? nil : Itly::Event.new(name: 'context', properties: context)
39
+
40
+ # Log
41
+ logger&.info 'load()'
42
+ logger&.info 'Itly is disabled!' unless enabled?
43
+ logger&.warn 'No plugin enabled!' if options.plugins.empty?
44
+
45
+ # pass options to plugins
46
+ run_on_plugins { |plugin| plugin.load options: options.for_plugin } if enabled?
47
+
48
+ # Mark that the #load method was called on this object
49
+ @is_initialized = true
50
+ end
51
+
52
+ ##
53
+ # Identify a user in your application and associate all future events with
54
+ # their identity, or to set their traits.
55
+ #
56
+ # Validates the +properties+ with all registered plugins first.
57
+ # Raises a Itly::ValidationError if one of the validations failed and
58
+ # if your set the +options.validation+ value to +ERROR_ON_INVALID+.
59
+ #
60
+ # Call +identify+ on all plugins and call +post_identify+ on all plugins.
61
+ #
62
+ # Example:
63
+ #
64
+ # itly.identify user_id: 'MyUser123', role: 'admin'
65
+ #
66
+ # @param [String] user_id: the id of the user in your application
67
+ # @param [Hash] properties: the user's traits to pass to your application
68
+ # @param [Hash] options: plugin specific option. The keys must correspond
69
+ # to a plugin id, and the values will be passed only to the plugin identified by the key.
70
+ #
71
+ def identify(user_id:, properties: {}, options: {})
72
+ # Run only if the object is enabled and was initialized
73
+ return unless was_initialized? && enabled?
74
+
75
+ # Log
76
+ log = Itly::Loggers.vars_to_log user_id: user_id, properties: properties, options: options
77
+ logger&.info "identify(#{log})"
78
+
79
+ # Validate and run on all plugins
80
+ event = Event.new name: 'identify', properties: properties
81
+
82
+ action = ->(plugin, combined_event) {
83
+ plugin.identify(
84
+ user_id: user_id, properties: combined_event.properties, options: options[plugin.id]
85
+ )
86
+ }
87
+
88
+ post_action = ->(plugin, combined_event, validation_results) {
89
+ plugin.post_identify(
90
+ user_id: user_id, properties: combined_event.properties, validation_results: validation_results
91
+ )
92
+ }
93
+
94
+ validate_and_send_to_plugins event: event, action: action, post_action: post_action
95
+ end
96
+
97
+ ##
98
+ # Associate a user with their group (for example, their department or company),
99
+ # or to set the group's traits.
100
+ #
101
+ # Validates the +properties+ with all registered plugins first.
102
+ # Raises a Itly::ValidationError if one of the validations failed and
103
+ # if your set the +options.validation+ value to +ERROR_ON_INVALID+.
104
+ #
105
+ # Call +group+ on all plugins and call +post_group+ on all plugins.
106
+ #
107
+ # Example:
108
+ #
109
+ # itly.group user_id: 'MyUser123', group_id: 'MyGroup456', name: 'Iteratively, Inc.'
110
+ #
111
+ # @param [String] user_id: the id of the user in your application
112
+ # @param [String] group_id: the id of the group in your application
113
+ # @param [Hash] properties: The list of properties to pass to your application
114
+ # @param [Hash] options: plugin specific option. The keys must correspond
115
+ # to a plugin id, and the values will be passed only to the plugin identified by the key.
116
+ #
117
+ def group(user_id:, group_id:, properties: {}, options: {})
118
+ # Run only if the object is enabled and was initialized
119
+ return unless was_initialized? && enabled?
120
+
121
+ # Log
122
+ log = Itly::Loggers.vars_to_log user_id: user_id, group_id: group_id, properties: properties, options: options
123
+ logger&.info "group(#{log})"
124
+
125
+ # Validate and run on all plugins
126
+ event = Event.new name: 'group', properties: properties
127
+
128
+ action = ->(plugin, combined_event) {
129
+ plugin.group(
130
+ user_id: user_id, group_id: group_id, properties: combined_event.properties,
131
+ options: options[plugin.id]
132
+ )
133
+ }
134
+
135
+ post_action = ->(plugin, combined_event, validation_results) {
136
+ plugin.post_group(
137
+ user_id: user_id, group_id: group_id, properties: combined_event.properties,
138
+ validation_results: validation_results
139
+ )
140
+ }
141
+
142
+ validate_and_send_to_plugins event: event, action: action, post_action: post_action
143
+ end
144
+
145
+ ##
146
+ # The Page method lets you record page views, along with optional extra information about
147
+ # the page viewed by the user.
148
+ #
149
+ # Validates the +properties+ with all registered plugins first.
150
+ # Raises a Itly::ValidationError if one of the validations failed and
151
+ # if your set the +options.validation+ value to +ERROR_ON_INVALID+.
152
+ #
153
+ # Call +page+ on all plugins and call +post_page+ on all plugins.
154
+ #
155
+ # Example:
156
+ #
157
+ # itly.page user_id: 'MyUser123', category: 'Products', name: 'MyPage456', name: 'Iteratively, Inc.'
158
+ #
159
+ # @param [String] user_id: the id of the user in your application
160
+ # @param [String] category: the category of the page
161
+ # @param [String] name: the name of the page.
162
+ # @param [Hash] properties: The list of properties to pass to your application
163
+ # @param [Hash] options: plugin specific option. The keys must correspond
164
+ # to a plugin id, and the values will be passed only to the plugin identified by the key.
165
+ #
166
+ def page(user_id:, category: nil, name: nil, properties: {}, options: {})
167
+ # Run only if the object is enabled and was initialized
168
+ return unless was_initialized? && enabled?
169
+
170
+ # Log
171
+ log = Itly::Loggers.vars_to_log(
172
+ user_id: user_id, category: category, name: name, properties: properties, options: options
173
+ )
174
+ logger&.info "page(#{log})"
175
+
176
+ # Validate and run on all plugins
177
+ event = Event.new name: 'page', properties: properties
178
+
179
+ action = ->(plugin, combined_event) {
180
+ plugin.page(
181
+ user_id: user_id, category: category, name: name, properties: combined_event.properties,
182
+ options: options[plugin.id]
183
+ )
184
+ }
185
+
186
+ post_action = ->(plugin, combined_event, validation_results) {
187
+ plugin.post_page(
188
+ user_id: user_id, category: category, name: name, properties: combined_event.properties,
189
+ validation_results: validation_results
190
+ )
191
+ }
192
+
193
+ validate_and_send_to_plugins event: event, action: action, post_action: post_action
194
+ end
195
+
196
+ ##
197
+ # Track an event, call the event's corresponding function on plugins.
198
+ #
199
+ # Validates the +properties+ of the +Event+ object passed as parameter
200
+ # with all registered plugins first.
201
+ # Raises a Itly::ValidationError if one of the validations failed and
202
+ # if your set the +options.validation+ value to +ERROR_ON_INVALID+.
203
+ #
204
+ # The properties of the +context+ instance attribute passed when called #load
205
+ # are merged with the +event+ parameter before validation and calling the event
206
+ # on your application.
207
+ #
208
+ # Call +track+ on all plugins and call +post_track+ on all plugins.
209
+ #
210
+ # Example:
211
+ #
212
+ # event = Itly::Event.new name: 'watched_video', properties: {'video_id' => 'MyVider123', watch_time: '123456'}
213
+ # itly.track user_id: 'MyUser123', event: event
214
+ #
215
+ # @param [String] user_id: the id of the user in your application
216
+ # @param [Event] event: the Event object to pass to your application
217
+ # @param [Hash] options: plugin specific option. The keys must correspond
218
+ # to a plugin id, and the values will be passed only to the plugin identified by the key.
219
+ #
220
+ def track(user_id:, event:, options: {})
221
+ # Run only if the object is enabled and was initialized
222
+ return unless was_initialized? && enabled?
223
+
224
+ # Log
225
+ log = Itly::Loggers.vars_to_log(
226
+ user_id: user_id, event: event&.name, properties: event&.properties, options: options
227
+ )
228
+ logger&.info "track(#{log})"
229
+
230
+ # Validate and run on all plugins
231
+ action = ->(plugin, combined_event) {
232
+ plugin.track user_id: user_id, event: combined_event, options: options[plugin.id]
233
+ }
234
+
235
+ post_action = ->(plugin, combined_event, validation_results) {
236
+ plugin.post_track user_id: user_id, event: combined_event, validation_results: validation_results
237
+ }
238
+
239
+ validate_and_send_to_plugins event: event, context: @context, action: action, post_action: post_action
240
+ end
241
+
242
+ ##
243
+ # Associate one user ID with another (typically a known user ID with an anonymous one).
244
+ #
245
+ # Call +alias+ on all plugins and call +post_alias+ on all plugins.
246
+ #
247
+ # @param [String] user_id: The ID that the user will be identified by going forward. This is
248
+ # typically the user's database ID (as opposed to an anonymous ID), or their updated ID
249
+ # (for example, if the ID is an email address which the user just updated).
250
+ # @param [String] previous_id: The ID the user has been identified by so far.
251
+ # @param [Hash] options: plugin specific option. The keys must correspond
252
+ # to a plugin id, and the values will be passed only to the plugin identified by the key.
253
+ #
254
+ def alias(user_id:, previous_id:, options: {})
255
+ # Run only if the object is enabled and was initialized
256
+ return unless was_initialized? && enabled?
257
+
258
+ # Log
259
+ log = Itly::Loggers.vars_to_log user_id: user_id, previous_id: previous_id, options: options
260
+ logger&.info "alias(#{log})"
261
+
262
+ # Run on all plugins
263
+ run_on_plugins do |plugin|
264
+ plugin.alias user_id: user_id, previous_id: previous_id, options: options[plugin.id]
265
+ end
266
+ run_on_plugins do |plugin|
267
+ plugin.post_alias user_id: user_id, previous_id: previous_id
268
+ end
269
+ end
270
+
271
+ ##
272
+ # Send +flush+ to your plugins.
273
+ #
274
+ # Call +flush+ on all plugins.
275
+ #
276
+ def flush
277
+ # Run only if the object is enabled and was initialized
278
+ return unless was_initialized? && enabled?
279
+
280
+ # Log
281
+ logger&.info 'flush()'
282
+
283
+ # Run on all plugins
284
+ run_on_plugins(&:flush)
285
+ end
286
+
287
+ ##
288
+ # Send +shutdown+ to your plugins.
289
+ #
290
+ # Call +shutdown+ on all plugins.
291
+ #
292
+ def shutdown
293
+ # Run only if the object is enabled and was initialized
294
+ return unless was_initialized? && enabled?
295
+
296
+ # Log
297
+ logger&.info 'shutdown()'
298
+
299
+ # Run on all plugins
300
+ run_on_plugins(&:shutdown)
301
+ end
302
+
303
+ ##
304
+ # Reset the SDK's (and all plugins') state. This method is usually called when a user logs out.
305
+ #
306
+ # Call +reset+ on all plugins.
307
+ #
308
+ def reset
309
+ # Run only if the object is enabled and was initialized
310
+ return unless was_initialized? && enabled?
311
+
312
+ # Log
313
+ logger&.info 'reset()'
314
+
315
+ # Run on all plugins
316
+ run_on_plugins(&:reset)
317
+ end
318
+
319
+ ##
320
+ # Validate an Event
321
+ #
322
+ # Call +event+ on all plugins and collect their return values.
323
+ #
324
+ # @param [Event] event: the event to validate
325
+ #
326
+ # @return [Array] array of Itly::ValidationResponse objects that were generated by the plugins
327
+ #
328
+ def validate(event:)
329
+ return unless was_initialized? && validation_enabled?
330
+
331
+ # Log
332
+ log = Itly::Loggers.vars_to_log event: event
333
+ logger&.info "validate(#{log})"
334
+
335
+ # Run on all plugins
336
+ run_on_plugins { |plugin| plugin.validate event: event }
337
+ end
338
+
339
+ def is_loaded?
340
+ !!@is_initialized
341
+ end
342
+
343
+ private
344
+
345
+ def was_initialized?
346
+ @is_initialized ? true : raise(InitializationError, 'Itly is not initialized. Call #load { |options| ... }')
347
+ end
348
+
349
+ def validate_and_send_to_plugins(action:, post_action:, event:, context: nil)
350
+ # Perform validation on the context and the event
351
+ context_validations, event_validations, is_valid = validate_context_and_event context, event
352
+ validations = context_validations + event_validations
353
+
354
+ # Call the action on all plugins
355
+ event.properties.merge! context.properties if context
356
+
357
+ if is_valid || @options.validation == Itly::Options::Validation::TRACK_INVALID
358
+ run_on_plugins do |plugin|
359
+ action.call(plugin, event) unless event.plugins[plugin.id].is_a?(FalseClass)
360
+ end
361
+ end
362
+
363
+ # Log all errors
364
+ log_validation_errors validations, event
365
+
366
+ # Call the post_action on all plugins
367
+ run_on_plugins do |plugin|
368
+ post_action.call(plugin, event, validations) unless event.plugins[plugin.id].is_a?(FalseClass)
369
+ end
370
+
371
+ # Throw an exception if requested
372
+ raise_validation_errors is_valid, validations, event
373
+ end
374
+
375
+ def validate_context_and_event(context, event)
376
+ # Validate the context
377
+ context_validations = (validate event: context if context) || []
378
+
379
+ # Validate the event
380
+ event_validations = validate(event: event) || []
381
+
382
+ # Check if all validation succeeded
383
+ is_valid = (context_validations + event_validations).all?(&:valid)
384
+
385
+ [context_validations, event_validations, is_valid]
386
+ end
387
+
388
+ def log_validation_errors(validations, event)
389
+ validations.reject(&:valid).each do |response|
390
+ @options.logger&.error %(Validation error for "#{event.name}" )\
391
+ "in #{response.plugin_id}. Message: #{response.message}"
392
+ end
393
+ end
394
+
395
+ def raise_validation_errors(is_valid, validations, event)
396
+ return unless !is_valid && @options.validation == Itly::Options::Validation::ERROR_ON_INVALID
397
+
398
+ messages = validations.reject(&:valid).collect(&:message)
399
+ messages = messages.select { |m| !m.nil? && m.length.positive? }
400
+ messages << "Unknown error validating #{event.name}" if messages.empty?
401
+
402
+ raise ValidationError, messages.join('. ')
403
+ end
404
+ end