assaf / vanity

Experiment Driven Development for Ruby
http://vanity.labnotes.org
MIT License
1.55k stars 269 forks source link

Use Vanity.track! in rack or plain ruby #331

Closed Curter29 closed 6 years ago

Curter29 commented 6 years ago

Hello, i have RoR application and some plain ruby scripts, i want track metric in plain ruby. Just try

user = User.find(1)
Vanity.track!(:pay_page, {identity: user})

But nothing count in dashboard, just return [#<Proc:0x00ADDR (lambda)>]

What i'm doing wrong?

phillbaker commented 6 years ago

Hi @Curter29, sorry for the slow reply, passing the identity should work - are you also doing the setup needed for vanity to connect to the database?

To use Vanity from anywhere, you'll need to do some of the setup currently done in vanity/frameworks/rails.rb, specifically:

$redis = Redis.new # or from elsewhere
Vanity.configure do |config|
  # ... any config
end
Vanity.connect!(
  adapter: :redis,
  redis: $redis
)
Vanity.load!

Then per request:

# Tracks an action associated with a metric. Useful for calling from a
# Rack handler. Note that a user should already be added to an experiment
# via #ab_test before this is called - otherwise, the conversion will be
# tracked, but the user will not be added to the experiment.

identity_object = Identity.new(env['rack.session']) 
Vanity.track!(:click, {
  # can be any object that responds to `to_s` with a string 
  # that contains the unique identifier or the string identifier itself
  :identity=>identity_object, 
  :values=>[1] # optional
})

Does your user object return its id on to_s? Otherwise, try passing in the user id itself

bensheldon commented 6 years ago

This was really helpful to explain how to use Vanity.track! outside of a controller/view context. My follow-on question is:

How do I use ab_test (or equivalent) inside of a plain old ruby object?

e.g. Given I have my identity string, how can I have Vanity assign and return an alternative for a given experiment.

phillbaker commented 6 years ago

Good question - this should have been more clearly documented years ago 😄 !

Vanity pulls the identity from a "context" object that responds to vanity_identity.

So this should work: defining Vanity.context (this is how the ActionMailer integration works)

class AVanityContext
  def vanity_identity
    "123"
  end
end

Vanity.context = AVanityContext.new() # Any object that responds to `#vanity_identity`
Vanity.ab_test(:invite_subject)

If you're using plain ruby objects, you could also alias something in your identity model to respond similarly and then set that as the vanity context:

class User
  alias_method :vanity_identity, :id
end

Currently Vanity is structured mostly around a request-based model, so this works great in the traditional request/response, but one disadvantage of this is that it's indirect and using a "global". The Vanity.context variable should be thread safe though.

If it helps, you can also customize the behavior:

ab_test "Project widget" do
      alternatives :small, :medium, :large
      identify do |identity_object|
        identity_object.id
      end
end
bensheldon commented 6 years ago

This is super helpful info and very well explains what Vanity is doing under the hood. Thank you @phillbaker!

I put together a little service object that tries to make it as declarative as possible:

class VanityDispatcher
  attr_reader :id

  def initialize(id: nil)
    @id = id
  end

  def track!(metric_name)
    within_context { Vanity.track!(metric_name) }
  end

  def ab_test(metric_name)
    within_context { Vanity.ab_test(metric_name) }
  end

  private

  def within_context
    old_context = Vanity.context
    context = OpenStruct.new(vanity_identity: id)

    Vanity.context = context
    result = yield
    Vanity.context = old_context

    result
  end
end

So then I can use it directly like:

alternative_name = VanityDispatcher.new(id: user.id).ab_test(:my_metric)

Thought it might be helpful to share in case others are using Vanity outside of the typical request-response.