segmentor 0.0.1
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 +7 -0
 - data/.editorconfig +10 -0
 - data/.gitignore +6 -0
 - data/.rubocop.yml +10 -0
 - data/.ruby-gemset +1 -0
 - data/.ruby-version +1 -0
 - data/Gemfile +6 -0
 - data/Gemfile.lock +219 -0
 - data/LICENSE +201 -0
 - data/README.md +0 -0
 - data/Rakefile +4 -0
 - data/bin/console +14 -0
 - data/bin/setup +8 -0
 - data/config.ru +9 -0
 - data/lib/generators/segmentor/segmentor.rb +27 -0
 - data/lib/generators/segmentor/templates/create_segmentor_tables.rb +59 -0
 - data/lib/segmentor/email_notifier_context.rb +8 -0
 - data/lib/segmentor/errors/evaluation_error.rb +9 -0
 - data/lib/segmentor/errors/notification_error.rb +16 -0
 - data/lib/segmentor/errors/session_error.rb +12 -0
 - data/lib/segmentor/notifier_context.rb +14 -0
 - data/lib/segmentor/receipt.rb +35 -0
 - data/lib/segmentor/segment.rb +151 -0
 - data/lib/segmentor/session.rb +97 -0
 - data/lib/segmentor/source.rb +19 -0
 - data/lib/segmentor/sources/ruby_source.rb +26 -0
 - data/lib/segmentor/target.rb +66 -0
 - data/lib/segmentor/version.rb +5 -0
 - data/lib/segmentor.rb +11 -0
 - data/segmentor.gemspec +45 -0
 - metadata +310 -0
 
| 
         @@ -0,0 +1,35 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Segmentor
         
     | 
| 
      
 4 
     | 
    
         
            +
              # Receipt is generated when a notification is sent to a target.
         
     | 
| 
      
 5 
     | 
    
         
            +
              class Receipt < ActiveRecord::Base
         
     | 
| 
      
 6 
     | 
    
         
            +
                self.table_name = :segmentor_receipts
         
     | 
| 
      
 7 
     | 
    
         
            +
                store :metadata, accessors: %i[target_id session_id], coder: JSON
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                belongs_to :segment, class_name: '::Segmentor::Segment'
         
     | 
| 
      
 10 
     | 
    
         
            +
                belongs_to :user
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                validates :sent_at, presence: true
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                def self.record_receipt(target, rendered_value = nil)
         
     | 
| 
      
 15 
     | 
    
         
            +
                  receipt = ::Segmentor::Receipt.new
         
     | 
| 
      
 16 
     | 
    
         
            +
                  receipt.segment = target.segment
         
     | 
| 
      
 17 
     | 
    
         
            +
                  receipt.user_id = target.user_id
         
     | 
| 
      
 18 
     | 
    
         
            +
                  receipt.target_id = target.id
         
     | 
| 
      
 19 
     | 
    
         
            +
                  receipt.session_id = target.session_id
         
     | 
| 
      
 20 
     | 
    
         
            +
                  receipt.rendered_value = rendered_value
         
     | 
| 
      
 21 
     | 
    
         
            +
                  receipt.sent_at = Time.now.utc
         
     | 
| 
      
 22 
     | 
    
         
            +
                  receipt.save!
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                  receipt
         
     | 
| 
      
 25 
     | 
    
         
            +
                end
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                def next
         
     | 
| 
      
 28 
     | 
    
         
            +
                  segment.receipts.where('id > ?', id).first
         
     | 
| 
      
 29 
     | 
    
         
            +
                end
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                def prev
         
     | 
| 
      
 32 
     | 
    
         
            +
                  segment.receipts.where('id < ?', id).last
         
     | 
| 
      
 33 
     | 
    
         
            +
                end
         
     | 
| 
      
 34 
     | 
    
         
            +
              end
         
     | 
| 
      
 35 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,151 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require 'active_record'
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            module Segmentor
         
     | 
| 
      
 6 
     | 
    
         
            +
              # Segment is a slice of data, based on a condition.
         
     | 
| 
      
 7 
     | 
    
         
            +
              class Segment < ActiveRecord::Base
         
     | 
| 
      
 8 
     | 
    
         
            +
                self.table_name = :segmentor_segments
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
                NO_REPEAT = 0
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                has_many :receipts, class_name: '::Segmentor::Receipt', dependent: :destroy
         
     | 
| 
      
 13 
     | 
    
         
            +
                has_many :sessions, class_name: '::Segmentor::Session', dependent: :destroy
         
     | 
| 
      
 14 
     | 
    
         
            +
                has_one :source, class_name: '::Segmentor::Source', inverse_of: :segment, dependent: :destroy
         
     | 
| 
      
 15 
     | 
    
         
            +
                has_one :notifier_context, class_name: '::Segmentor::NotifierContext', inverse_of: :segment, dependent: :destroy
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                accepts_nested_attributes_for :source
         
     | 
| 
      
 18 
     | 
    
         
            +
                accepts_nested_attributes_for :notifier_context
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
                class_attribute :_after_session_change
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                def self.after_session_change(method_name)
         
     | 
| 
      
 23 
     | 
    
         
            +
                  self._after_session_change = method_name
         
     | 
| 
      
 24 
     | 
    
         
            +
                end
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                # repeat_frequency is in days, should be between 0 and 365. 0 means no repeat
         
     | 
| 
      
 27 
     | 
    
         
            +
                validates :repeat_frequency, numericality: { only_integer: true,
         
     | 
| 
      
 28 
     | 
    
         
            +
                                                             greater_than_or_equal_to: 0,
         
     | 
| 
      
 29 
     | 
    
         
            +
                                                             less_than_or_equal_to: 365 }
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                validates :name, length: { minimum: 3, maximum: 255 }, presence: true
         
     | 
| 
      
 32 
     | 
    
         
            +
                validates :description, length: { maximum: 255 }
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                # generate all the targets for this segment
         
     | 
| 
      
 35 
     | 
    
         
            +
                # it then returns the session
         
     | 
| 
      
 36 
     | 
    
         
            +
                def generate!
         
     | 
| 
      
 37 
     | 
    
         
            +
                  # start a new session
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                  session = start_session
         
     | 
| 
      
 40 
     | 
    
         
            +
                  begin
         
     | 
| 
      
 41 
     | 
    
         
            +
                    # get the data from the source
         
     | 
| 
      
 42 
     | 
    
         
            +
                    returned_targets = source.execute
         
     | 
| 
      
 43 
     | 
    
         
            +
                    # replace the data in the segment in a transaction
         
     | 
| 
      
 44 
     | 
    
         
            +
                    returned_targets.each do |target|
         
     | 
| 
      
 45 
     | 
    
         
            +
                      target.session = session
         
     | 
| 
      
 46 
     | 
    
         
            +
                      target.save!
         
     | 
| 
      
 47 
     | 
    
         
            +
                    end
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
                    # mark the session as draft
         
     | 
| 
      
 50 
     | 
    
         
            +
                    end_session(session)
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
                    activate(session) if auto_activate
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
                    session
         
     | 
| 
      
 55 
     | 
    
         
            +
                  rescue ::Segmentor::Errors::EvaluationError => e
         
     | 
| 
      
 56 
     | 
    
         
            +
                    # mark the session as failed with a reason. don't raise
         
     | 
| 
      
 57 
     | 
    
         
            +
                    session.mark_as_failed!(e.message)
         
     | 
| 
      
 58 
     | 
    
         
            +
                    session
         
     | 
| 
      
 59 
     | 
    
         
            +
                  rescue StandardError
         
     | 
| 
      
 60 
     | 
    
         
            +
                    # delete the session and raise
         
     | 
| 
      
 61 
     | 
    
         
            +
                    session.destroy
         
     | 
| 
      
 62 
     | 
    
         
            +
                    raise
         
     | 
| 
      
 63 
     | 
    
         
            +
                  end
         
     | 
| 
      
 64 
     | 
    
         
            +
                end
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
                # notify! calls the given block for each target that can be notified
         
     | 
| 
      
 67 
     | 
    
         
            +
                # if the block raises a Segmentor::Errors::NotificationError, the target is skipped
         
     | 
| 
      
 68 
     | 
    
         
            +
                # and no receipt is issued but the rest of the targets are still notified
         
     | 
| 
      
 69 
     | 
    
         
            +
                # as most notification systems do not support transactions, it is important
         
     | 
| 
      
 70 
     | 
    
         
            +
                # to ensure only ::Segmentor::Errors::NotificationError is raised unless you are
         
     | 
| 
      
 71 
     | 
    
         
            +
                # sure you want to rollback all notification receipts
         
     | 
| 
      
 72 
     | 
    
         
            +
                def notify!(&block)
         
     | 
| 
      
 73 
     | 
    
         
            +
                  # get the active targets from the segment
         
     | 
| 
      
 74 
     | 
    
         
            +
                  # for each target, check if its receipt is within segment repeat frequency
         
     | 
| 
      
 75 
     | 
    
         
            +
                  # if not, send the notification and generate a receipt
         
     | 
| 
      
 76 
     | 
    
         
            +
                  # if so, do nothing
         
     | 
| 
      
 77 
     | 
    
         
            +
                  raise ::Segmentor::Errors::NoSessionError, 'no active session' unless active_session
         
     | 
| 
      
 78 
     | 
    
         
            +
             
     | 
| 
      
 79 
     | 
    
         
            +
                  ::ActiveRecord::Base.transaction do
         
     | 
| 
      
 80 
     | 
    
         
            +
                    active_session.targets.each do |target|
         
     | 
| 
      
 81 
     | 
    
         
            +
                      next unless target.can_be_notified?
         
     | 
| 
      
 82 
     | 
    
         
            +
             
     | 
| 
      
 83 
     | 
    
         
            +
                      # send the notification
         
     | 
| 
      
 84 
     | 
    
         
            +
                      # generate a receipt
         
     | 
| 
      
 85 
     | 
    
         
            +
                      result = block.call(target, notifier_context) if block_given?
         
     | 
| 
      
 86 
     | 
    
         
            +
                      target.issue_receipt!(result)
         
     | 
| 
      
 87 
     | 
    
         
            +
                    # catch ::Segmentor::Errors::NotificationError and do nothing
         
     | 
| 
      
 88 
     | 
    
         
            +
                    rescue ::Segmentor::Errors::NotificationError
         
     | 
| 
      
 89 
     | 
    
         
            +
                      # do nothing. so we only skip issue_receipt! but continue to the rest of the targets
         
     | 
| 
      
 90 
     | 
    
         
            +
                      # any other error will be raised and rollback the transaction
         
     | 
| 
      
 91 
     | 
    
         
            +
                    end
         
     | 
| 
      
 92 
     | 
    
         
            +
                    active_session.touch
         
     | 
| 
      
 93 
     | 
    
         
            +
                  end
         
     | 
| 
      
 94 
     | 
    
         
            +
                end
         
     | 
| 
      
 95 
     | 
    
         
            +
             
     | 
| 
      
 96 
     | 
    
         
            +
                # resets the last session of this segment to be in progress again
         
     | 
| 
      
 97 
     | 
    
         
            +
                def reset!
         
     | 
| 
      
 98 
     | 
    
         
            +
                  latest_session.mark_as_draft!
         
     | 
| 
      
 99 
     | 
    
         
            +
                end
         
     | 
| 
      
 100 
     | 
    
         
            +
             
     | 
| 
      
 101 
     | 
    
         
            +
                # turns a session from draft into active and marks
         
     | 
| 
      
 102 
     | 
    
         
            +
                # the current active session as archived.
         
     | 
| 
      
 103 
     | 
    
         
            +
                # if no current active session is found. the latest session is marked as active
         
     | 
| 
      
 104 
     | 
    
         
            +
                def activate(session)
         
     | 
| 
      
 105 
     | 
    
         
            +
                  ::ActiveRecord::Base.transaction do
         
     | 
| 
      
 106 
     | 
    
         
            +
                    # mark the current active session as archived
         
     | 
| 
      
 107 
     | 
    
         
            +
                    as = active_session
         
     | 
| 
      
 108 
     | 
    
         
            +
                    if as
         
     | 
| 
      
 109 
     | 
    
         
            +
                      as.status = ::Segmentor::Session::ARCHIVED
         
     | 
| 
      
 110 
     | 
    
         
            +
                      as.save!
         
     | 
| 
      
 111 
     | 
    
         
            +
                    end
         
     | 
| 
      
 112 
     | 
    
         
            +
             
     | 
| 
      
 113 
     | 
    
         
            +
                    # mark the new session as active
         
     | 
| 
      
 114 
     | 
    
         
            +
                    session.status = ::Segmentor::Session::ACTIVE
         
     | 
| 
      
 115 
     | 
    
         
            +
                    session.save!
         
     | 
| 
      
 116 
     | 
    
         
            +
                  end
         
     | 
| 
      
 117 
     | 
    
         
            +
                end
         
     | 
| 
      
 118 
     | 
    
         
            +
             
     | 
| 
      
 119 
     | 
    
         
            +
                def active_session
         
     | 
| 
      
 120 
     | 
    
         
            +
                  sessions.active.first
         
     | 
| 
      
 121 
     | 
    
         
            +
                end
         
     | 
| 
      
 122 
     | 
    
         
            +
             
     | 
| 
      
 123 
     | 
    
         
            +
                def active_session!
         
     | 
| 
      
 124 
     | 
    
         
            +
                  as = active_session
         
     | 
| 
      
 125 
     | 
    
         
            +
                  raise ::Segmentor::Errors::NoSessionError, 'no active session' unless as
         
     | 
| 
      
 126 
     | 
    
         
            +
             
     | 
| 
      
 127 
     | 
    
         
            +
                  as
         
     | 
| 
      
 128 
     | 
    
         
            +
                end
         
     | 
| 
      
 129 
     | 
    
         
            +
             
     | 
| 
      
 130 
     | 
    
         
            +
                def latest_session
         
     | 
| 
      
 131 
     | 
    
         
            +
                  # pick the latest session that's not in progress or archived
         
     | 
| 
      
 132 
     | 
    
         
            +
                  sessions.where
         
     | 
| 
      
 133 
     | 
    
         
            +
                          .not(status: [::Segmentor::Session::ARCHIVED, ::Segmentor::Session::IN_PROGRESS])
         
     | 
| 
      
 134 
     | 
    
         
            +
                          .order(created_at: :desc)
         
     | 
| 
      
 135 
     | 
    
         
            +
                          .first
         
     | 
| 
      
 136 
     | 
    
         
            +
                end
         
     | 
| 
      
 137 
     | 
    
         
            +
             
     | 
| 
      
 138 
     | 
    
         
            +
                private
         
     | 
| 
      
 139 
     | 
    
         
            +
             
     | 
| 
      
 140 
     | 
    
         
            +
                def start_session
         
     | 
| 
      
 141 
     | 
    
         
            +
                  session = ::Segmentor::Session.new(segment: self)
         
     | 
| 
      
 142 
     | 
    
         
            +
                  session.save!
         
     | 
| 
      
 143 
     | 
    
         
            +
             
     | 
| 
      
 144 
     | 
    
         
            +
                  session
         
     | 
| 
      
 145 
     | 
    
         
            +
                end
         
     | 
| 
      
 146 
     | 
    
         
            +
             
     | 
| 
      
 147 
     | 
    
         
            +
                def end_session(session)
         
     | 
| 
      
 148 
     | 
    
         
            +
                  session.mark_as_draft!
         
     | 
| 
      
 149 
     | 
    
         
            +
                end
         
     | 
| 
      
 150 
     | 
    
         
            +
              end
         
     | 
| 
      
 151 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,97 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Segmentor
         
     | 
| 
      
 4 
     | 
    
         
            +
              # Session is a single segmentation session. It can be
         
     | 
| 
      
 5 
     | 
    
         
            +
              # draft, in progress, or active.
         
     | 
| 
      
 6 
     | 
    
         
            +
              # at any point, there is only 1 active session per segment is allowed
         
     | 
| 
      
 7 
     | 
    
         
            +
              # while draft and in progress, there can be multiple sessions
         
     | 
| 
      
 8 
     | 
    
         
            +
              # in progress is when a source is still generating targets and they
         
     | 
| 
      
 9 
     | 
    
         
            +
              # are being saved.
         
     | 
| 
      
 10 
     | 
    
         
            +
              # draft is when a session has the targets, but they are not the ones
         
     | 
| 
      
 11 
     | 
    
         
            +
              # used for notification. This can be used to test a new source code for example.
         
     | 
| 
      
 12 
     | 
    
         
            +
              class Session < ActiveRecord::Base
         
     | 
| 
      
 13 
     | 
    
         
            +
                self.table_name = :segmentor_sessions
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                IN_PROGRESS = 0 # default
         
     | 
| 
      
 16 
     | 
    
         
            +
                DRAFT = 1
         
     | 
| 
      
 17 
     | 
    
         
            +
                ACTIVE = 2
         
     | 
| 
      
 18 
     | 
    
         
            +
                ARCHIVED = 3
         
     | 
| 
      
 19 
     | 
    
         
            +
                FAILED = 4
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                belongs_to :segment, class_name: '::Segmentor::Segment'
         
     | 
| 
      
 22 
     | 
    
         
            +
                has_many :targets, class_name: '::Segmentor::Target', dependent: :destroy
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                validates :status, presence: true
         
     | 
| 
      
 25 
     | 
    
         
            +
                validates :status, inclusion: { in: [IN_PROGRESS, DRAFT, ACTIVE, ARCHIVED, FAILED] }
         
     | 
| 
      
 26 
     | 
    
         
            +
                validates :segment_id, presence: true
         
     | 
| 
      
 27 
     | 
    
         
            +
                validate :only_one_active_session_per_segment
         
     | 
| 
      
 28 
     | 
    
         
            +
                validates :reason, presence: true, if: :failed?
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                scope :active, -> { where(status: ACTIVE) }
         
     | 
| 
      
 31 
     | 
    
         
            +
                scope :latest, -> { order(created_at: :desc).limit(1).first }
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                before_create :generate_session_id
         
     | 
| 
      
 34 
     | 
    
         
            +
                after_commit :call_segment_callbacks
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
                def mark_as_active!
         
     | 
| 
      
 37 
     | 
    
         
            +
                  # only draft sessions can be made active
         
     | 
| 
      
 38 
     | 
    
         
            +
                  raise ::Segmentor::Errors::SessionStateError, 'only draft sessions can be made active' unless status == DRAFT
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
                  self.status = ACTIVE
         
     | 
| 
      
 41 
     | 
    
         
            +
                  save!
         
     | 
| 
      
 42 
     | 
    
         
            +
                end
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                def mark_as_draft!
         
     | 
| 
      
 45 
     | 
    
         
            +
                  # archived and failed sessions cannot be turned into draft
         
     | 
| 
      
 46 
     | 
    
         
            +
                  if status == ARCHIVED || status == FAILED
         
     | 
| 
      
 47 
     | 
    
         
            +
                    raise ::Segmentor::Errors::SessionStateError, 'failed and archived sessions cannot be turned into draft'
         
     | 
| 
      
 48 
     | 
    
         
            +
                  end
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                  self.status = DRAFT
         
     | 
| 
      
 51 
     | 
    
         
            +
                  self.reason = nil
         
     | 
| 
      
 52 
     | 
    
         
            +
                  save!
         
     | 
| 
      
 53 
     | 
    
         
            +
                end
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
                def mark_as_archived!
         
     | 
| 
      
 56 
     | 
    
         
            +
                  # only active and failed sessions can be archived
         
     | 
| 
      
 57 
     | 
    
         
            +
                  unless status == ACTIVE || status == FAILED
         
     | 
| 
      
 58 
     | 
    
         
            +
                    raise ::Segmentor::Errors::SessionStateError, 'only active and failed sessions can be archived'
         
     | 
| 
      
 59 
     | 
    
         
            +
                  end
         
     | 
| 
      
 60 
     | 
    
         
            +
             
     | 
| 
      
 61 
     | 
    
         
            +
                  self.status = ARCHIVED
         
     | 
| 
      
 62 
     | 
    
         
            +
                  save!
         
     | 
| 
      
 63 
     | 
    
         
            +
                end
         
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
      
 65 
     | 
    
         
            +
                def mark_as_failed!(reason)
         
     | 
| 
      
 66 
     | 
    
         
            +
                  # archived sessions cannot be marked as failed
         
     | 
| 
      
 67 
     | 
    
         
            +
                  raise ::Segmentor::Errors::SessionStateError, 'archived sessions cannot be marked as failed' if status == ARCHIVED
         
     | 
| 
      
 68 
     | 
    
         
            +
             
     | 
| 
      
 69 
     | 
    
         
            +
                  self.status = FAILED
         
     | 
| 
      
 70 
     | 
    
         
            +
                  self.reason = reason
         
     | 
| 
      
 71 
     | 
    
         
            +
                  save!
         
     | 
| 
      
 72 
     | 
    
         
            +
                end
         
     | 
| 
      
 73 
     | 
    
         
            +
             
     | 
| 
      
 74 
     | 
    
         
            +
                private
         
     | 
| 
      
 75 
     | 
    
         
            +
             
     | 
| 
      
 76 
     | 
    
         
            +
                def only_one_active_session_per_segment
         
     | 
| 
      
 77 
     | 
    
         
            +
                  # only one active session per segment is allowed
         
     | 
| 
      
 78 
     | 
    
         
            +
                  if status == ACTIVE && ::Segmentor::Session.where(status: ACTIVE, segment_id: segment_id).count.positive?
         
     | 
| 
      
 79 
     | 
    
         
            +
                    raise ::Segmentor::Errors::SessionStateError, 'only one active session per segment is allowed'
         
     | 
| 
      
 80 
     | 
    
         
            +
                  end
         
     | 
| 
      
 81 
     | 
    
         
            +
                end
         
     | 
| 
      
 82 
     | 
    
         
            +
             
     | 
| 
      
 83 
     | 
    
         
            +
                def generate_session_id
         
     | 
| 
      
 84 
     | 
    
         
            +
                  # generate a session id
         
     | 
| 
      
 85 
     | 
    
         
            +
                  self.session_id ||= SecureRandom.uuid
         
     | 
| 
      
 86 
     | 
    
         
            +
                end
         
     | 
| 
      
 87 
     | 
    
         
            +
             
     | 
| 
      
 88 
     | 
    
         
            +
                def failed?
         
     | 
| 
      
 89 
     | 
    
         
            +
                  status == FAILED
         
     | 
| 
      
 90 
     | 
    
         
            +
                end
         
     | 
| 
      
 91 
     | 
    
         
            +
             
     | 
| 
      
 92 
     | 
    
         
            +
                def call_segment_callbacks
         
     | 
| 
      
 93 
     | 
    
         
            +
                  segment.send(segment.class._after_session_change, self) if segment.class._after_session_change.present?
         
     | 
| 
      
 94 
     | 
    
         
            +
                end
         
     | 
| 
      
 95 
     | 
    
         
            +
             
     | 
| 
      
 96 
     | 
    
         
            +
              end
         
     | 
| 
      
 97 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,19 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Segmentor
         
     | 
| 
      
 4 
     | 
    
         
            +
              # Source runs the code and returns an array of ::Segmentor::Target
         
     | 
| 
      
 5 
     | 
    
         
            +
              # you can dervie from this for different types of code sources (ie Ruby, JS, etc)
         
     | 
| 
      
 6 
     | 
    
         
            +
              class Source < ActiveRecord::Base
         
     | 
| 
      
 7 
     | 
    
         
            +
                self.table_name = 'segmentor_sources'
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                belongs_to :segment, class_name: '::Segmentor::Segment', foreign_key: :segment_id
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
                validates :code, presence: true
         
     | 
| 
      
 12 
     | 
    
         
            +
                validates :type, presence: true
         
     | 
| 
      
 13 
     | 
    
         
            +
                validates :segment, presence: true
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                def execute
         
     | 
| 
      
 16 
     | 
    
         
            +
                  raise NotImplementedError, 'execute must be implemented in a subclass'
         
     | 
| 
      
 17 
     | 
    
         
            +
                end
         
     | 
| 
      
 18 
     | 
    
         
            +
              end
         
     | 
| 
      
 19 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,26 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module Segmentor
         
     | 
| 
      
 2 
     | 
    
         
            +
              module Sources
         
     | 
| 
      
 3 
     | 
    
         
            +
                # RubySource is a source that evaluates ruby code to generate a list of targets
         
     | 
| 
      
 4 
     | 
    
         
            +
                # DO NOT USE THIS SOURCE IN UNSECURE ENVIRONMENTS
         
     | 
| 
      
 5 
     | 
    
         
            +
                class RubySource < ::Segmentor::Source
         
     | 
| 
      
 6 
     | 
    
         
            +
                  def execute
         
     | 
| 
      
 7 
     | 
    
         
            +
                    # evaluate the code
         
     | 
| 
      
 8 
     | 
    
         
            +
                    # return the result
         
     | 
| 
      
 9 
     | 
    
         
            +
                    begin
         
     | 
| 
      
 10 
     | 
    
         
            +
                      targets = eval(code)
         
     | 
| 
      
 11 
     | 
    
         
            +
                    rescue StandardError => e
         
     | 
| 
      
 12 
     | 
    
         
            +
                      raise ::Segmentor::Errors::EvaluationError, e.message
         
     | 
| 
      
 13 
     | 
    
         
            +
                    end
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                    targets = targets.is_a?(Array) ? targets : [targets]
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                    # all targets should be a ::Segment::Target
         
     | 
| 
      
 18 
     | 
    
         
            +
                    if targets.any? { |target| !target.is_a?(::Segmentor::Target) }
         
     | 
| 
      
 19 
     | 
    
         
            +
                      raise ::Segmentor::Errors::EvaluationError, 'all returned items should be ::Segment::Target'
         
     | 
| 
      
 20 
     | 
    
         
            +
                    end
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                    targets
         
     | 
| 
      
 23 
     | 
    
         
            +
                  end
         
     | 
| 
      
 24 
     | 
    
         
            +
                end
         
     | 
| 
      
 25 
     | 
    
         
            +
              end
         
     | 
| 
      
 26 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,66 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Segmentor
         
     | 
| 
      
 4 
     | 
    
         
            +
              # Target is single piece of data, returned by a
         
     | 
| 
      
 5 
     | 
    
         
            +
              class Target < ActiveRecord::Base
         
     | 
| 
      
 6 
     | 
    
         
            +
                self.table_name = :segmentor_targets
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                belongs_to :session, class_name: '::Segmentor::Session'
         
     | 
| 
      
 9 
     | 
    
         
            +
                belongs_to :segment, class_name: '::Segmentor::Segment'
         
     | 
| 
      
 10 
     | 
    
         
            +
                belongs_to :user
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                validates :user_id, presence: true
         
     | 
| 
      
 13 
     | 
    
         
            +
                validates :user_id, uniqueness: { scope: :session_id }
         
     | 
| 
      
 14 
     | 
    
         
            +
                validates :segment_id, presence: true
         
     | 
| 
      
 15 
     | 
    
         
            +
                validates :session_id, presence: true
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                before_validation :set_segment_if_nil
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                def self.new_with_user(user)
         
     | 
| 
      
 20 
     | 
    
         
            +
                  ::Segmentor::Target.new(user_id: user.id, payload: {})
         
     | 
| 
      
 21 
     | 
    
         
            +
                end
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                def self.new_with_user_and_payload(user, payload)
         
     | 
| 
      
 24 
     | 
    
         
            +
                  ::Segmentor::Target.new(user_id: user.id, payload: payload)
         
     | 
| 
      
 25 
     | 
    
         
            +
                end
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                def issue_receipt!(rendered_value = nil)
         
     | 
| 
      
 28 
     | 
    
         
            +
                  ::Segmentor::Receipt.record_receipt(self, rendered_value)
         
     | 
| 
      
 29 
     | 
    
         
            +
                end
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                def most_recent_receipt
         
     | 
| 
      
 32 
     | 
    
         
            +
                  segment.receipts.where(user_id: user_id).order(:sent_at).last
         
     | 
| 
      
 33 
     | 
    
         
            +
                end
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
                def get_binding
         
     | 
| 
      
 36 
     | 
    
         
            +
                  binding
         
     | 
| 
      
 37 
     | 
    
         
            +
                end
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                def preview(&block)
         
     | 
| 
      
 40 
     | 
    
         
            +
                  raise ArgumentError, 'block required' unless block_given?
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
                  block.call(self, segment.notifier_context)
         
     | 
| 
      
 43 
     | 
    
         
            +
                end
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
                def can_be_notified?
         
     | 
| 
      
 46 
     | 
    
         
            +
                  most_recent_receipt = segment.receipts.where(user_id: user_id).order(:sent_at).last
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
                  # notification can be sent if there is a receipt for this target that
         
     | 
| 
      
 49 
     | 
    
         
            +
                  # is older than the repeat frequency of its segment
         
     | 
| 
      
 50 
     | 
    
         
            +
                  return true if most_recent_receipt.nil?
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
                  # we have a receipt. notify only if:
         
     | 
| 
      
 53 
     | 
    
         
            +
                  # segment repeat frequency is not NO_REPEAT
         
     | 
| 
      
 54 
     | 
    
         
            +
                  # receipt is older than segment repeat frequency
         
     | 
| 
      
 55 
     | 
    
         
            +
                  segment.repeat_frequency != ::Segmentor::Segment::NO_REPEAT &&
         
     | 
| 
      
 56 
     | 
    
         
            +
                    most_recent_receipt.sent_at < segment.repeat_frequency.days.ago
         
     | 
| 
      
 57 
     | 
    
         
            +
                end
         
     | 
| 
      
 58 
     | 
    
         
            +
             
     | 
| 
      
 59 
     | 
    
         
            +
                private
         
     | 
| 
      
 60 
     | 
    
         
            +
             
     | 
| 
      
 61 
     | 
    
         
            +
                def set_segment_if_nil
         
     | 
| 
      
 62 
     | 
    
         
            +
                  self.segment_id ||= session.segment_id
         
     | 
| 
      
 63 
     | 
    
         
            +
                end
         
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
      
 65 
     | 
    
         
            +
              end
         
     | 
| 
      
 66 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/segmentor.rb
    ADDED
    
    | 
         @@ -0,0 +1,11 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require 'active_support/all'
         
     | 
| 
      
 4 
     | 
    
         
            +
            require 'rails/all'
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
            Dir["#{File.dirname(__FILE__)}/segmentor/**/*.rb"].each { |f| load(f) }
         
     | 
| 
      
 7 
     | 
    
         
            +
            load "#{File.dirname(__FILE__)}/generators/segmentor/segmentor.rb"
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
            # Segmentor module
         
     | 
| 
      
 10 
     | 
    
         
            +
            module Segmentor
         
     | 
| 
      
 11 
     | 
    
         
            +
            end
         
     | 
    
        data/segmentor.gemspec
    ADDED
    
    | 
         @@ -0,0 +1,45 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            lib = File.expand_path('lib', __dir__)
         
     | 
| 
      
 4 
     | 
    
         
            +
            $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
         
     | 
| 
      
 5 
     | 
    
         
            +
            require 'segmentor/version'
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            Gem::Specification.new do |spec|
         
     | 
| 
      
 8 
     | 
    
         
            +
              spec.name = 'segmentor'
         
     | 
| 
      
 9 
     | 
    
         
            +
              spec.version = Segmentor::VERSION
         
     | 
| 
      
 10 
     | 
    
         
            +
              spec.authors = ['Khash Sajadi']
         
     | 
| 
      
 11 
     | 
    
         
            +
              spec.email = ['khash@cloud66.com']
         
     | 
| 
      
 12 
     | 
    
         
            +
              spec.license = 'Apache-2.0'
         
     | 
| 
      
 13 
     | 
    
         
            +
              spec.homepage = 'https://github.com/cloud66-oss/segmentor'
         
     | 
| 
      
 14 
     | 
    
         
            +
              spec.required_ruby_version = '>= 3.0.0'
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
              spec.summary = 'Segmentor creates slices from your data that can be used for notifications'
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
              # Specify which files should be added to the gem when it is released.
         
     | 
| 
      
 19 
     | 
    
         
            +
              # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
         
     | 
| 
      
 20 
     | 
    
         
            +
              spec.files = Dir.chdir(File.expand_path(__dir__)) do
         
     | 
| 
      
 21 
     | 
    
         
            +
                `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
         
     | 
| 
      
 22 
     | 
    
         
            +
              end
         
     | 
| 
      
 23 
     | 
    
         
            +
              spec.bindir = 'exe'
         
     | 
| 
      
 24 
     | 
    
         
            +
              spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
         
     | 
| 
      
 25 
     | 
    
         
            +
              spec.require_paths = ['lib']
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
              spec.add_dependency 'actionpack', '~> 6.1'
         
     | 
| 
      
 28 
     | 
    
         
            +
              spec.add_dependency 'activerecord', '~> 6.1'
         
     | 
| 
      
 29 
     | 
    
         
            +
              spec.add_dependency 'activesupport', '~> 6.1'
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
              spec.add_development_dependency 'bootsnap', '~> 1.9'
         
     | 
| 
      
 32 
     | 
    
         
            +
              spec.add_development_dependency 'bundler', '~> 2.0'
         
     | 
| 
      
 33 
     | 
    
         
            +
              spec.add_development_dependency 'factory_bot', '~> 6.2'
         
     | 
| 
      
 34 
     | 
    
         
            +
              spec.add_development_dependency 'generator_spec', '~> 0.9'
         
     | 
| 
      
 35 
     | 
    
         
            +
              spec.add_development_dependency 'parser', '3.0.0.0'
         
     | 
| 
      
 36 
     | 
    
         
            +
              spec.add_development_dependency 'pry', '~> 0.13.1'
         
     | 
| 
      
 37 
     | 
    
         
            +
              spec.add_development_dependency 'rails', '~> 6.1'
         
     | 
| 
      
 38 
     | 
    
         
            +
              spec.add_development_dependency 'rake', '~> 13.0'
         
     | 
| 
      
 39 
     | 
    
         
            +
              spec.add_development_dependency 'rspec', '~> 3.10'
         
     | 
| 
      
 40 
     | 
    
         
            +
              spec.add_development_dependency 'rspec-rails', '~> 5.0'
         
     | 
| 
      
 41 
     | 
    
         
            +
              spec.add_development_dependency 'rubocop', '~> 1.4'
         
     | 
| 
      
 42 
     | 
    
         
            +
              spec.add_development_dependency 'rubocop-performance', '~> 1.9'
         
     | 
| 
      
 43 
     | 
    
         
            +
              spec.add_development_dependency 'rufo', '~> 0.12'
         
     | 
| 
      
 44 
     | 
    
         
            +
              spec.add_development_dependency 'sqlite3', '~> 1.4'
         
     | 
| 
      
 45 
     | 
    
         
            +
            end
         
     |