Skip to content

Latest commit

 

History

History

valued-client

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 

Valued Ruby client

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).

Installation

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

Simple usage

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" }
})

Incremental scope building

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

Global scope

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)

Object mapping

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.

Option 1: Define to_valued_data

# Assuming this is your user model
class User
  attr_accessor :name, :email, :id
  def to_valued_data = { name: name, email: email, id: id }
end

Option 2: Registering a converter

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

Nested objects

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 }}

Custom HTTP client

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)

Background processing

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

Known issues

This gem is incompatible with the valued gem.