Class: Trevosdk::Client
- Inherits:
-
Object
- Object
- Trevosdk::Client
- Defined in:
- lib/trevosdk/client.rb
Overview
Server client: deterministic assignment and event tracking for any Ruby runtime.
Evaluation reads an immutable config snapshot and never touches the network, so calls are safe from Rails controllers, Sidekiq jobs, and scripts alike.
Constant Summary collapse
- EXPOSURE_EVENT =
"$experiment_exposure"
Class Method Summary collapse
-
.derive_alias_url(ingestion_url) ⇒ Object
Alias lives on the same service as events, so an ingestion override must move alias too.
- .parse_force_variants(raw) ⇒ Object
Instance Method Summary collapse
- #alias(anonymous_id, user_id) ⇒ Object
- #close ⇒ Object
- #flush ⇒ Object
- #get_variant(experiment_key, user_id: nil, anonymous_id: nil, track_exposure: true) ⇒ Object
-
#initialize(secret_key, bootstrap_config: nil, config_url: CONFIG_URL, ingestion_url: INGESTION_URL, alias_url: nil, poll_interval_s: DEFAULT_POLL_INTERVAL_S, flush_interval_s: DEFAULT_FLUSH_INTERVAL_S, max_batch_size: DEFAULT_MAX_BATCH_SIZE, force_variants: nil, transport: nil, on_error: nil) ⇒ Client
constructor
A new instance of Client.
- #ready(timeout_s = nil) ⇒ Object
- #track(event_name, user_id: nil, anonymous_id: nil, properties: nil, insert_id: nil) ⇒ Object
Constructor Details
#initialize(secret_key, bootstrap_config: nil, config_url: CONFIG_URL, ingestion_url: INGESTION_URL, alias_url: nil, poll_interval_s: DEFAULT_POLL_INTERVAL_S, flush_interval_s: DEFAULT_FLUSH_INTERVAL_S, max_batch_size: DEFAULT_MAX_BATCH_SIZE, force_variants: nil, transport: nil, on_error: nil) ⇒ Client
Returns a new instance of Client.
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
# File 'lib/trevosdk/client.rb', line 20 def initialize(secret_key, bootstrap_config: nil, config_url: CONFIG_URL, ingestion_url: INGESTION_URL, alias_url: nil, poll_interval_s: DEFAULT_POLL_INTERVAL_S, flush_interval_s: DEFAULT_FLUSH_INTERVAL_S, max_batch_size: DEFAULT_MAX_BATCH_SIZE, force_variants: nil, transport: nil, on_error: nil) raise ArgumentError, "Trevosdk::Client requires a secret_key" if secret_key.to_s.empty? if !bootstrap_config.nil? && !bootstrap_config.is_a?(Array) raise ArgumentError, "bootstrap_config must be an array of experiment configs — pass " \ 'response["experiments"], not the whole config response' end @secret_key = secret_key @transport = transport || DEFAULT_TRANSPORT @on_error = on_error || ->(error) { warn("[Trevo] #{error.}") } @alias_url = alias_url || self.class.derive_alias_url(ingestion_url) @experiments = (bootstrap_config || []).to_h { |config| [config["experimentKey"], config] } @exposures = ExposureDeduper.new(SERVER_EXPOSURE_DEDUP_MAX_KEYS) @exposures_mutex = Mutex.new @forced = self.class.parse_force_variants(ENV[FORCE_VARIANTS_ENV]) .merge((force_variants || {}).transform_keys(&:to_s)) @batcher = EventBatcher.new( ingestion_url: ingestion_url, secret_key: secret_key, max_batch_size: [1, max_batch_size].max, flush_interval_s: [MIN_FLUSH_INTERVAL_S, flush_interval_s].max, transport: @transport, on_error: @on_error ) @poller = ConfigPoller.new( config_url: config_url, secret_key: secret_key, interval_s: [MIN_POLL_INTERVAL_S, poll_interval_s].max, transport: @transport, on_config: ->(configs) { swap_config(configs) }, on_error: @on_error ) @start_mutex = Mutex.new @started = false @closed = false end |
Class Method Details
.derive_alias_url(ingestion_url) ⇒ Object
Alias lives on the same service as events, so an ingestion override must move alias too.
66 67 68 69 70 71 |
# File 'lib/trevosdk/client.rb', line 66 def self.derive_alias_url(ingestion_url) return ALIAS_URL if ingestion_url == INGESTION_URL trimmed = ingestion_url.sub(%r{/+\z}, "") return "#{trimmed.delete_suffix("/events")}/alias" if trimmed.end_with?("/events") ALIAS_URL end |
.parse_force_variants(raw) ⇒ Object
73 74 75 76 77 78 79 80 81 82 |
# File 'lib/trevosdk/client.rb', line 73 def self.parse_force_variants(raw) return {} if raw.nil? || raw.empty? raw.split(",").each_with_object({}) do |pair, parsed| separator = pair.index(":") next if separator.nil? || separator.zero? || separator == pair.length - 1 key = pair[...separator].strip variant = pair[(separator + 1)..].strip parsed[key] = variant unless key.empty? || variant.empty? end end |
Instance Method Details
#alias(anonymous_id, user_id) ⇒ Object
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
# File 'lib/trevosdk/client.rb', line 124 def alias(anonymous_id, user_id) ensure_started if anonymous_id.to_s.empty? || user_id.to_s.empty? raise ArgumentError, "alias requires both an anonymous_id and a user_id" end response = @transport.call( TransportRequest.new( method: "POST", url: @alias_url, headers: Trevosdk.auth_headers(@secret_key), body: JSON.generate({anonymousId: anonymous_id, userId: user_id}) ) ) if response.status == 409 raise "Trevo alias rejected: anonymousId \"#{anonymous_id}\" is already linked to a different user" end raise "Trevo alias failed with status #{response.status}" unless (200..299).cover?(response.status) nil end |
#close ⇒ Object
154 155 156 157 158 159 |
# File 'lib/trevosdk/client.rb', line 154 def close return if @closed @closed = true @poller.stop @batcher.shutdown end |
#flush ⇒ Object
150 151 152 |
# File 'lib/trevosdk/client.rb', line 150 def flush @batcher.flush end |
#get_variant(experiment_key, user_id: nil, anonymous_id: nil, track_exposure: true) ⇒ Object
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
# File 'lib/trevosdk/client.rb', line 84 def get_variant(experiment_key, user_id: nil, anonymous_id: nil, track_exposure: true) return Core::CONTROL_VARIANT if experiment_key.to_s.empty? ensure_started config = @experiments[experiment_key] # Overrides win, but only for a variant the experiment has, and never # record an exposure — see docs/sdk/bucketing-spec.md §6. override = @forced[experiment_key] if override && config && config["variants"].any? { |v| v["name"] == override } return override end # An empty-string user_id must fall through to the anonymous id the # browser is bucketing with. bucketing_id = user_id.to_s.empty? ? anonymous_id : user_id return Core::CONTROL_VARIANT if bucketing_id.to_s.empty? || config.nil? variant = Core.assign_variant(bucketing_id, config) if track_exposure now_ms = (Time.now.to_f * 1000).to_i should_log = @exposures_mutex.synchronize do @exposures.should_log?(bucketing_id, experiment_key, variant, now_ms) end if should_log enqueue_event( EXPOSURE_EVENT, user_id, anonymous_id, {"experimentKey" => experiment_key, "variantName" => variant}, nil ) end end variant end |
#ready(timeout_s = nil) ⇒ Object
145 146 147 148 |
# File 'lib/trevosdk/client.rb', line 145 def ready(timeout_s = nil) ensure_started @poller.wait_ready(timeout_s) end |
#track(event_name, user_id: nil, anonymous_id: nil, properties: nil, insert_id: nil) ⇒ Object
120 121 122 |
# File 'lib/trevosdk/client.rb', line 120 def track(event_name, user_id: nil, anonymous_id: nil, properties: nil, insert_id: nil) enqueue_event(event_name, user_id, anonymous_id, properties, insert_id) end |