itly-sdk 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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