A Ruby client library for sending events to Valued.
This library:
- Is well tested (100% test coverage).
- Does not make assumptions about your application/library desgin.
- Does not patch any external classes or modules.
- Only depends on one other gem (concurrent-ruby).
- Is considered thread-safe and compatible with all common concurrency models (multi-threading, forking, actors, event loops, etc).
Add the following to your Gemfile
and run bundle install
:
gem "valued-client"
gem "concurrent-ruby-ext", "~> 1.1" # optional, but will speed up processing
You can create a Valued::Client
instance and call action
, page_view
, sync
etc directly on the client.
require "valued"
# Get the token for authentication.
token = ENV.fetch("VALUED_TOKEN") # or wherever you store credentials
# Create a client
client = Valued::Client.new(token)
# Record a page view event for the user with the id 123
client.page_view("https://big.company.com/learn/to/waltz", "user.id" => 123)
# Record an action
client.action("report.generated", {
customer: { id: 12 },
user: { id: 123 },
attributes: { format: :pdf }
})
# Sync user data
client.sync_user({
id: 123,
name: "Josh Kalderimis",
email: "[email protected]",
location: { country: "NZ", region: "Wellington" }
})
You can use Valued::Scope
to build up data incrementally:
scope = Valued::Scope.new(client)
scope["user.id"] = 123
# user nagivates to customer with id 12
scope.with("customer.id" => 12) do
scope.page_view("https://big.company.com/reports/12")
scope.action("report.generated")
end
# trigger an action without a customer
scope.page_view("https://big.company.com/profile")
scope.action("profile.updated")
This is also handy for building up sync data:
scope.user = {
id: 42, name: "Arthur Dent", email: "[email protected]"
}
scope.customer = {
id: 1, name: "BBC Radio"
}
scope.sync
If you are in an environment that makes sharing global scope easier than explicitely passing around scope, like a Rails application, you can use one globally shared Client instance and a thread-local scope directly via the Valued
module:
Valued.connect(token)
# Wrap this in a scope so we don't leak a user id.
Valued.scope do
Valued["user.id"] = 123
# user nagivates to customer with id 12
Valued.with("customer.id" => 12) do
Valued.page_view("https://big.company.com/reports/12")
Valued.action("report.generated")
end
# trigger an action without a customer
Valued.page_view("https://big.company.com/profile")
Valued.action("profile.updated")
end
You can also connect an existing client instance to the global scope:
client = Valued::Client.new(token)
# reuse client globally
Valued.connect(client)
Valued.action("profile.updated", "user.id" => 42)
In the examples above, you have to construct the data hashes for users and similar objects yourself when interacting with Valued. Sometimes it would be nice if you could hand your own user and customer objects to valued-client
:
Valued.action("profile.updated", user: User.current)
You have two options to do so.
# Assuming this is your user model
class User
attr_accessor :name, :email, :id
def to_valued_data = { name: name, email: email, id: id }
end
This option is handy if you want to keep your Valued logic out of your models, or if you want to create logic for objects outside your control:
Valued::Data.register(User) do |user|
{ id: user.id, name: user.name, email: user.email }
end
Imagine users would have a nested location object. You could handle this in the user conversion logic:
Valued::Data.register(User) do |user|
{ id: user.id, location: { country: user.location.country, region: user.region }}
end
However, you would need to repeat this logic for customers as well. Instead, you can register a converter for your location objects:
Valued::Data.register(User) {{ id: _1.id, location: _1.location }}
Valued::Data.register(Location) {{ country: _1.country, region: _1.region }}
By default, Valued::Client
will use Net::HTTP
for sending events. You can replace the HTTP client used by either passing a block to Valued::Client.new
/Valued.connect
, or any object that implements #call
.
require "faraday"
token = ENV.fetch("VALUED_TOKEN")
endpoint = ENV["VALUED_ENDPOINT"] || "https://ingres.valued.app/events"
Valued.connect do |data|
Faraday.post(endpoint, data.to_json, {
"Content-Type" => "application/json",
"Authorization" => "Bearer #{token}"
})
end
Here is a more complex example for reusing headers generated by valued-client
:
require "faraday"
class MyBackend
def initialize(...)
@connection = Valued::Connection.new(...)
end
def call(data)
Faraday.post(@connection.endpoint, data.to_json, @connection.headers)
end
end
token = ENV.fetch("VALUED_TOKEN")
Valued.connect MyBackend.new(token)
By default, this library will send events to Valued out of band to not unnecessarily block your business logic. The only exception is if a test environment is detected, in which case the HTTP requests will be sent synchronously.
You can control this behavior by setting a custom executor
:
# use a fixed threadpool
Valued::Connection.executor = Concurrent::FixedThreadPool.new(5)
# disable background processing
Valued::Connection.executor = Concurrent::ImmediateExecutor.new
You can also reuse an existing executor to avoid a dedicated background thread for Valued.
You do not need to use any of the executors provided by concurrent-ruby. The only thing the executor
needs to implement is a method called post
that will schedule the passed block to be executed some time soon.
Here is an example for using EventMachine's defer
method:
class MyExecutor
def post(&block) = EventMachine.defer(block)
end
Valued::Connection.executor = MyExecutor.new
This gem is incompatible with the valued gem.