bento-sdk 0.5.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 +7 -0
- data/lib/bento/analytics.rb +47 -0
- data/lib/bento/core/client.rb +47 -0
- data/lib/bento/core/error.rb +26 -0
- data/lib/bento/core/response.rb +23 -0
- data/lib/bento/core/validators/base.rb +18 -0
- data/lib/bento/core/validators/email_validators.rb +10 -0
- data/lib/bento/core/validators/event_validators.rb +46 -0
- data/lib/bento/core/version.rb +3 -0
- data/lib/bento/resources/emails.rb +59 -0
- data/lib/bento/resources/events.rb +77 -0
- data/lib/bento/resources/spam.rb +26 -0
- data/lib/bento/resources/subscribers.rb +137 -0
- data/lib/bento/sdk/backoff_policy.rb +49 -0
- data/lib/bento/sdk/client.rb +123 -0
- data/lib/bento/sdk/configuration.rb +85 -0
- data/lib/bento/sdk/defaults.rb +37 -0
- data/lib/bento/sdk/field_parser.rb +104 -0
- data/lib/bento/sdk/logging.rb +60 -0
- data/lib/bento/sdk/message_batch.rb +72 -0
- data/lib/bento/sdk/response.rb +15 -0
- data/lib/bento/sdk/transport.rb +144 -0
- data/lib/bento/sdk/utils.rb +91 -0
- data/lib/bento/sdk/version.rb +5 -0
- data/lib/bento/sdk/worker.rb +69 -0
- data/lib/bento-sdk.rb +68 -0
- metadata +180 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: c620722fa7d6cb6cfa291f2a8aeda31dbc237a6096387dd43534868d934d3396
|
4
|
+
data.tar.gz: 6426e53c3fdb01e6c38bfe48602f1bc8e95a66280335775932bce4c0e0792c95
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 50c445e4580135141dcf05baf11bb68a354c90b85352d221abfeeac83f0d4acc16beadfdf22561221bb3302b8ee422723a04afbd53e41684a11647190aa3e9e1
|
7
|
+
data.tar.gz: a5564c55a86070b04120d4e8125c6a3b8df694ad860584bdb5f5b48eecf2848b5889178e0fcff4c98ea3af6ff38eaa5322459246e0265ec61921162c3902eb2f
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require "bento/sdk/version"
|
2
|
+
require "bento/sdk/defaults"
|
3
|
+
require "bento/sdk/utils"
|
4
|
+
require "bento/sdk/field_parser"
|
5
|
+
require "bento/sdk/client"
|
6
|
+
require "bento/sdk/worker"
|
7
|
+
require "bento/sdk/transport"
|
8
|
+
require "bento/sdk/response"
|
9
|
+
require "bento/sdk/logging"
|
10
|
+
|
11
|
+
module Bento
|
12
|
+
class << self
|
13
|
+
attr_writer :write_key
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.configure(&block)
|
17
|
+
yield self
|
18
|
+
end
|
19
|
+
|
20
|
+
class Analytics
|
21
|
+
# Initializes a new instance of {Bento::Analytics::Client}, to which all
|
22
|
+
# method calls are proxied.
|
23
|
+
#
|
24
|
+
# @param options includes options that are passed down to
|
25
|
+
# {Bento::Analytics::Client#initialize}
|
26
|
+
# @option options [Boolean] :stub (false) If true, requests don't hit the
|
27
|
+
# server and are stubbed to be successful.
|
28
|
+
def initialize(options = {})
|
29
|
+
Transport.stub = options[:stub] if options.key?(:stub)
|
30
|
+
@client = Bento::Analytics::Client.new options
|
31
|
+
end
|
32
|
+
|
33
|
+
def method_missing(message, *args, &block)
|
34
|
+
if @client.respond_to? message
|
35
|
+
@client.send message, *args, &block
|
36
|
+
else
|
37
|
+
super
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def respond_to_missing?(method_name, include_private = false)
|
42
|
+
@client.respond_to?(method_name) || super
|
43
|
+
end
|
44
|
+
|
45
|
+
include Logging
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Bento
|
2
|
+
# Client class for interacting with the Bento API.
|
3
|
+
# This class provides methods for making HTTP GET and POST requests to the API,
|
4
|
+
# handles authentication, connection errors, and response parsing.
|
5
|
+
# It also supports a development mode for local testing.
|
6
|
+
class Client
|
7
|
+
def get(endpoint)
|
8
|
+
handle_connection_errors { parse_response(conn.get(endpoint)) }
|
9
|
+
end
|
10
|
+
|
11
|
+
def post(endpoint, payload = nil)
|
12
|
+
handle_connection_errors { parse_response(conn.post(endpoint, payload)) }
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def parse_response(response)
|
18
|
+
if response.success?
|
19
|
+
JSON.parse(response.body)
|
20
|
+
else
|
21
|
+
Bento::Error.raise_with_response(response)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def authorization
|
26
|
+
@authorization ||= "Basic #{Base64.strict_encode64("#{Bento.publishable_key}:#{Bento.secret_key}")}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def conn
|
30
|
+
Faraday.new(
|
31
|
+
url: Bento.dev_mode ? 'http://localhost:3000' : 'https://app.bentonow.com',
|
32
|
+
headers: {
|
33
|
+
'Content-Type' => 'application/json',
|
34
|
+
'Accept' => 'application/json',
|
35
|
+
'User-Agent' => 'bento-rails-' + Bento.site_uuid,
|
36
|
+
'Authorization' => authorization
|
37
|
+
}
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
def handle_connection_errors
|
42
|
+
yield
|
43
|
+
rescue Faraday::ConnectionFailed => exception
|
44
|
+
raise Bento::ConnectionError, "Failed to connect to the server: #{exception.message}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Bento
|
2
|
+
# Custom error class for handling Bento API errors.
|
3
|
+
# This class encapsulates the HTTP response and provides
|
4
|
+
# a formatted error message for easier debugging and error handling.
|
5
|
+
class Error < StandardError
|
6
|
+
attr_reader :response
|
7
|
+
|
8
|
+
def initialize(response)
|
9
|
+
@response = response
|
10
|
+
super(formatted_error_message)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Raises an instance of Bento::Error with the given response
|
14
|
+
def self.raise_with_response(response)
|
15
|
+
raise new(response)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def formatted_error_message
|
21
|
+
"HTTP #{response.status}: #{response.body}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
# Example usage:
|
26
|
+
# Bento::Error.raise_with_response(response)
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Bento
|
2
|
+
# Represents a response from the Bento API.
|
3
|
+
# This class encapsulates the API response data and provides
|
4
|
+
# methods to check the status of the operation (success or failure)
|
5
|
+
# based on the number of successful and failed results.
|
6
|
+
class Response
|
7
|
+
attr_reader :results, :failed
|
8
|
+
|
9
|
+
def initialize(response)
|
10
|
+
@response = response
|
11
|
+
@results = response['results'].to_i
|
12
|
+
@failed = response['failed'].to_i
|
13
|
+
end
|
14
|
+
|
15
|
+
def success?
|
16
|
+
failed.zero?
|
17
|
+
end
|
18
|
+
|
19
|
+
def failure?
|
20
|
+
failed.positive?
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Bento
|
2
|
+
module Validators
|
3
|
+
module Base
|
4
|
+
def validate_email(email)
|
5
|
+
raise ArgumentError, 'Email is required' if email.nil? || email.empty?
|
6
|
+
raise ArgumentError, 'Invalid email format' unless email =~ /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
|
7
|
+
end
|
8
|
+
|
9
|
+
def validate_type(type)
|
10
|
+
raise ArgumentError, 'Type is required' if type.nil? || type.empty?
|
11
|
+
end
|
12
|
+
|
13
|
+
def validate_fields(fields)
|
14
|
+
raise ArgumentError, 'Fields must be a hash' unless fields.is_a?(Hash)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module Bento
|
2
|
+
module Validators
|
3
|
+
module EmailValidators
|
4
|
+
def validate_author(author)
|
5
|
+
raise ArgumentError, 'Author is required' if author.nil? || author.empty?
|
6
|
+
# Additional validation can be implemented based on system requirements
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Bento
|
2
|
+
module Validators
|
3
|
+
module EventValidators
|
4
|
+
def validate_details(details)
|
5
|
+
raise ArgumentError, 'Details must be a hash' unless details.is_a?(Hash)
|
6
|
+
validate_unique(details[:unique]) if details.key?(:unique)
|
7
|
+
validate_value(details[:value]) if details.key?(:value)
|
8
|
+
validate_cart(details[:cart]) if details.key?(:cart)
|
9
|
+
end
|
10
|
+
|
11
|
+
def validate_unique(unique)
|
12
|
+
raise ArgumentError, 'Unique must be a hash' unless unique.is_a?(Hash)
|
13
|
+
raise ArgumentError, 'Unique key is required' unless unique.key?(:key)
|
14
|
+
end
|
15
|
+
|
16
|
+
def validate_value(value)
|
17
|
+
raise ArgumentError, 'Value must be a hash' unless value.is_a?(Hash)
|
18
|
+
raise ArgumentError, 'Currency is required in value' unless value.key?(:currency)
|
19
|
+
raise ArgumentError, 'Amount is required in value' unless value.key?(:amount)
|
20
|
+
end
|
21
|
+
|
22
|
+
def validate_cart(cart)
|
23
|
+
raise ArgumentError, 'Cart must be a hash' unless cart.is_a?(Hash)
|
24
|
+
validate_cart_items(cart[:items]) if cart.key?(:items)
|
25
|
+
end
|
26
|
+
|
27
|
+
def validate_cart_items(items)
|
28
|
+
raise ArgumentError, 'Cart items must be an array' unless items.is_a?(Array)
|
29
|
+
items.each do |item|
|
30
|
+
raise ArgumentError, 'Cart item must be a hash' unless item.is_a?(Hash)
|
31
|
+
raise ArgumentError, 'Product SKU is required in cart item' unless item.key?(:product_sku)
|
32
|
+
raise ArgumentError, 'Product name is required in cart item' unless item.key?(:product_name)
|
33
|
+
raise ArgumentError, 'Quantity is required in cart item' unless item.key?(:quantity)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def validate_event(event)
|
38
|
+
raise ArgumentError, 'Event must be a hash' unless event.is_a?(Hash)
|
39
|
+
validate_email(event[:email])
|
40
|
+
validate_type(event[:type])
|
41
|
+
validate_fields(event[:fields]) if event.key?(:fields)
|
42
|
+
validate_details(event[:details]) if event.key?(:details)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Bento
|
2
|
+
class Emails
|
3
|
+
class << self
|
4
|
+
include Bento::Validators::Base
|
5
|
+
include Bento::Validators::EmailValidators
|
6
|
+
# Send an email that honors subscription status
|
7
|
+
def send(to:, from:, subject:, html_body:, personalizations: {})
|
8
|
+
validate_email(to)
|
9
|
+
validate_author(from)
|
10
|
+
|
11
|
+
payload = {
|
12
|
+
to: to,
|
13
|
+
from: from,
|
14
|
+
subject: subject,
|
15
|
+
html_body: html_body,
|
16
|
+
personalizations: personalizations
|
17
|
+
}
|
18
|
+
|
19
|
+
send_bulk([payload])
|
20
|
+
end
|
21
|
+
|
22
|
+
# Send a transactional email that always sends, even if user is unsubscribed
|
23
|
+
def send_transactional(to:, from:, subject:, html_body:, personalizations: {})
|
24
|
+
validate_email(to)
|
25
|
+
validate_author(from)
|
26
|
+
|
27
|
+
payload = {
|
28
|
+
to: to,
|
29
|
+
from: from,
|
30
|
+
subject: subject,
|
31
|
+
html_body: html_body,
|
32
|
+
personalizations: personalizations,
|
33
|
+
transactional: true
|
34
|
+
}
|
35
|
+
|
36
|
+
send_bulk([payload])
|
37
|
+
end
|
38
|
+
|
39
|
+
def send_bulk(emails)
|
40
|
+
raise ArgumentError, 'Emails must be an array' unless emails.is_a?(Array)
|
41
|
+
emails.each { |email| validate_email(email[:to]); validate_email(email[:from]) }
|
42
|
+
|
43
|
+
payload = { emails: emails }.to_json
|
44
|
+
response = client.post("api/v1/batch/emails?#{URI.encode_www_form(default_params)}", payload)
|
45
|
+
Bento::Response.new(response)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def client
|
51
|
+
@client ||= Bento::Client.new
|
52
|
+
end
|
53
|
+
|
54
|
+
def default_params
|
55
|
+
{ site_uuid: Bento.config.site_uuid }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module Bento
|
2
|
+
class Events
|
3
|
+
class << self
|
4
|
+
include Bento::Validators::Base
|
5
|
+
include Bento::Validators::EventValidators
|
6
|
+
# Track an event
|
7
|
+
# Usage examples:
|
8
|
+
#
|
9
|
+
# Basic event:
|
10
|
+
# Bento::Events.track(email: 'test@test.com', type: '$completed_onboarding')
|
11
|
+
#
|
12
|
+
# Event with fields:
|
13
|
+
# Bento::Events.track(
|
14
|
+
# email: 'test@test.com',
|
15
|
+
# type: '$completed_onboarding',
|
16
|
+
# fields: { first_name: 'Jesse', last_name: 'Pinkman' }
|
17
|
+
# )
|
18
|
+
#
|
19
|
+
# Complex event with fields and details:
|
20
|
+
# Bento::Events.track(
|
21
|
+
# email: 'test@test.com',
|
22
|
+
# type: '$purchase',
|
23
|
+
# fields: { first_name: 'Jesse' },
|
24
|
+
# details: {
|
25
|
+
# unique: { key: 'test123' },
|
26
|
+
# value: { currency: 'USD', amount: 8000 },
|
27
|
+
# cart: {
|
28
|
+
# items: [
|
29
|
+
# {
|
30
|
+
# product_sku: 'SKU123',
|
31
|
+
# product_name: 'Test',
|
32
|
+
# quantity: 100
|
33
|
+
# }
|
34
|
+
# ],
|
35
|
+
# abandoned_checkout_url: 'https://test.com'
|
36
|
+
# }
|
37
|
+
# }
|
38
|
+
# )
|
39
|
+
def track(email:, type:, fields: {}, details: {})
|
40
|
+
validate_email(email)
|
41
|
+
validate_type(type)
|
42
|
+
validate_fields(fields)
|
43
|
+
validate_details(details)
|
44
|
+
|
45
|
+
event = {
|
46
|
+
email: email,
|
47
|
+
type: type
|
48
|
+
}
|
49
|
+
event[:fields] = fields unless fields.empty?
|
50
|
+
event[:details] = details unless details.empty?
|
51
|
+
|
52
|
+
import([event])
|
53
|
+
end
|
54
|
+
|
55
|
+
# Batch track multiple events
|
56
|
+
# Usage: Bento::Events.import([{email: 'test@bentonow.com', type: 'Login'}, {email: 'test@bentonow.com', type: 'Purchase', fields: { first_name: 'Jesse', last_name: 'Hanley' }}])
|
57
|
+
def import(events)
|
58
|
+
raise ArgumentError, 'Events must be an array' unless events.is_a?(Array)
|
59
|
+
events.each { |event| validate_event(event) }
|
60
|
+
|
61
|
+
payload = { events: events }.to_json
|
62
|
+
response = client.post("api/v1/batch/events?#{URI.encode_www_form(default_params)}", payload)
|
63
|
+
Bento::Response.new(response)
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def client
|
69
|
+
@client ||= Bento::Client.new
|
70
|
+
end
|
71
|
+
|
72
|
+
def default_params
|
73
|
+
{ site_uuid: Bento.config.site_uuid }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Bento
|
2
|
+
class Spam
|
3
|
+
class << self
|
4
|
+
def valid?(email)
|
5
|
+
payload = {
|
6
|
+
email: email
|
7
|
+
}
|
8
|
+
|
9
|
+
response = client.post('api/v1/experimental/validation', payload.to_json)
|
10
|
+
|
11
|
+
return response['valid']
|
12
|
+
end
|
13
|
+
|
14
|
+
# Example: Bento::Spam.risky?('test@bentonow.com')
|
15
|
+
def risky?(email)
|
16
|
+
!self.valid?(email)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def client
|
22
|
+
@client ||= Bento::Client.new
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
module Bento
|
2
|
+
class Subscribers
|
3
|
+
class << self
|
4
|
+
# Find a subscriber by email or uuid
|
5
|
+
# Usage: Bento::Subscribers.find_by(email: 'test@bentonow.com')
|
6
|
+
# or: Bento::Subscribers.find_by(uuid: 'subscriber-uuid')
|
7
|
+
def find_by(email: nil, uuid: nil)
|
8
|
+
params = default_params
|
9
|
+
params[:email] = email if email
|
10
|
+
params[:uuid] = uuid if uuid
|
11
|
+
response = client.get("api/v1/fetch/subscribers?#{URI.encode_www_form(params)}")
|
12
|
+
|
13
|
+
if response['data'].nil?
|
14
|
+
raise StandardError, 'Bento Error: No user found with the given email or uuid'
|
15
|
+
else
|
16
|
+
Subscriber.new(response['data'])
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Find or create a subscriber by email or uuid
|
21
|
+
# Usage: Bento::Subscribers.find_or_create_by(email: 'test@bentonow.com')
|
22
|
+
# or: Bento::Subscribers.find_or_create_by(uuid: 'subscriber-uuid')
|
23
|
+
def find_or_create_by(email: nil)
|
24
|
+
params = default_params
|
25
|
+
payload = {
|
26
|
+
subscriber: {
|
27
|
+
email: email
|
28
|
+
}.compact
|
29
|
+
}.to_json
|
30
|
+
|
31
|
+
response = client.post("api/v1/fetch/subscribers?#{URI.encode_www_form(params)}", payload)
|
32
|
+
|
33
|
+
if response['data'].nil?
|
34
|
+
raise StandardError, 'Bento Error: No user found with the given email or uuid'
|
35
|
+
else
|
36
|
+
Subscriber.new(response['data'])
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Import or update subscribers in bulk
|
41
|
+
# Usage: Bento::Subscribers.import([{email: 'user1@example.com', first_name: 'John'}, {email: 'user2@example.com', last_name: 'Doe'}])
|
42
|
+
def import(subscribers)
|
43
|
+
payload = { subscribers: subscribers }.to_json
|
44
|
+
response = client.post("api/v1/batch/subscribers?#{URI.encode_www_form(default_params)}", payload)
|
45
|
+
Bento::Response.new(response)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Run a command to change a subscriber's data
|
49
|
+
# Usage: Bento::Subscribers.run_command(command: 'add_tag', email: 'test@bentonow.com', query: 'new_tag')
|
50
|
+
def run_command(command:, email:, query: nil)
|
51
|
+
payload = {
|
52
|
+
command: [{
|
53
|
+
command: command,
|
54
|
+
email: email,
|
55
|
+
query: query
|
56
|
+
}]
|
57
|
+
}.to_json
|
58
|
+
|
59
|
+
response = client.post("api/v1/fetch/commands?#{URI.encode_www_form(default_params)}", payload)
|
60
|
+
Bento::Response.new(response)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Add a tag to a subscriber
|
64
|
+
# Usage: Bento::Subscribers.add_tag(email: 'test@bentonow.com', tag: 'new_tag')
|
65
|
+
def add_tag(email:, tag:)
|
66
|
+
run_command(command: 'add_tag', email: email, query: tag)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Add a tag to a subscriber via an event
|
70
|
+
# Usage: Bento::Subscribers.add_tag_via_event(email: 'test@bentonow.com', tag: 'event_tag')
|
71
|
+
def add_tag_via_event(email:, tag:)
|
72
|
+
run_command(command: 'add_tag_via_event', email: email, query: tag)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Remove a tag from a subscriber
|
76
|
+
# Usage: Bento::Subscribers.remove_tag(email: 'test@bentonow.com', tag: 'old_tag')
|
77
|
+
def remove_tag(email:, tag:)
|
78
|
+
run_command(command: 'remove_tag', email: email, query: tag)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Add a field to a subscriber
|
82
|
+
# Usage: Bento::Subscribers.add_field(email: 'test@bentonow.com', key: 'company', value: 'Acme Inc')
|
83
|
+
def add_field(email:, key:, value:)
|
84
|
+
run_command(command: 'add_field', email: email, query: { key: key, value: value })
|
85
|
+
end
|
86
|
+
|
87
|
+
# Remove a field from a subscriber
|
88
|
+
# Usage: Bento::Subscribers.remove_field(email: 'test@bentonow.com', field: 'company')
|
89
|
+
def remove_field(email:, field:)
|
90
|
+
run_command(command: 'remove_field', email: email, query: field)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Subscribe a user
|
94
|
+
# Usage: Bento::Subscribers.subscribe(email: 'test@bentonow.com')
|
95
|
+
def subscribe(email:)
|
96
|
+
run_command(command: 'subscribe', email: email)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Unsubscribe a user
|
100
|
+
# Usage: Bento::Subscribers.unsubscribe(email: 'test@bentonow.com')
|
101
|
+
def unsubscribe(email:)
|
102
|
+
run_command(command: 'unsubscribe', email: email)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Change a subscriber's email
|
106
|
+
# Usage: Bento::Subscribers.change_email(old_email: 'old@example.com', new_email: 'new@example.com')
|
107
|
+
def change_email(old_email:, new_email:)
|
108
|
+
run_command(command: 'change_email', email: old_email, query: new_email)
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
def client
|
114
|
+
@client ||= Bento::Client.new
|
115
|
+
end
|
116
|
+
|
117
|
+
def default_params
|
118
|
+
{ site_uuid: Bento.config.site_uuid }
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
class Subscriber
|
124
|
+
attr_reader :id, :uuid, :email, :fields, :cached_tag_ids, :unsubscribed_at, :navigation_url
|
125
|
+
|
126
|
+
def initialize(data)
|
127
|
+
@id = data['id']
|
128
|
+
attributes = data['attributes']
|
129
|
+
@uuid = attributes['uuid']
|
130
|
+
@email = attributes['email']
|
131
|
+
@fields = attributes['fields']
|
132
|
+
@cached_tag_ids = attributes['cached_tag_ids']
|
133
|
+
@unsubscribed_at = attributes['unsubscribed_at']
|
134
|
+
@navigation_url = attributes['navigation_url']
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require "bento/sdk/defaults"
|
2
|
+
|
3
|
+
module Bento
|
4
|
+
class Analytics
|
5
|
+
class BackoffPolicy
|
6
|
+
include Bento::Analytics::Defaults::BackoffPolicy
|
7
|
+
|
8
|
+
# @param [Hash] opts
|
9
|
+
# @option opts [Numeric] :min_timeout_ms The minimum backoff timeout
|
10
|
+
# @option opts [Numeric] :max_timeout_ms The maximum backoff timeout
|
11
|
+
# @option opts [Numeric] :multiplier The value to multiply the current
|
12
|
+
# interval with for each retry attempt
|
13
|
+
# @option opts [Numeric] :randomization_factor The randomization factor
|
14
|
+
# to use to create a range around the retry interval
|
15
|
+
def initialize(opts = {})
|
16
|
+
@min_timeout_ms = opts[:min_timeout_ms] || MIN_TIMEOUT_MS
|
17
|
+
@max_timeout_ms = opts[:max_timeout_ms] || MAX_TIMEOUT_MS
|
18
|
+
@multiplier = opts[:multiplier] || MULTIPLIER
|
19
|
+
@randomization_factor = opts[:randomization_factor] || RANDOMIZATION_FACTOR
|
20
|
+
|
21
|
+
@attempts = 0
|
22
|
+
end
|
23
|
+
|
24
|
+
# @return [Numeric] the next backoff interval, in milliseconds.
|
25
|
+
def next_interval
|
26
|
+
interval = @min_timeout_ms * (@multiplier**@attempts)
|
27
|
+
interval = add_jitter(interval, @randomization_factor)
|
28
|
+
|
29
|
+
@attempts += 1
|
30
|
+
|
31
|
+
[interval, @max_timeout_ms].min
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def add_jitter(base, randomization_factor)
|
37
|
+
random_number = rand
|
38
|
+
max_deviation = base * randomization_factor
|
39
|
+
deviation = random_number * max_deviation
|
40
|
+
|
41
|
+
if random_number < 0.5
|
42
|
+
base - deviation
|
43
|
+
else
|
44
|
+
base + deviation
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|