Class: PostHog::Client

Inherits:
Object
  • Object
show all
Includes:
Logging, Utils
Defined in:
lib/posthog/client.rb

Constant Summary

Constants included from Utils

Utils::UTC_OFFSET_WITHOUT_COLON, Utils::UTC_OFFSET_WITH_COLON

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Logging

included, #logger

Methods included from Utils

convert_to_datetime, date_in_iso8601, datetime_in_iso8601, deep_symbolize_keys, formatted_offset, get_by_symbol_or_string_key, is_valid_regex, isoify_dates, isoify_dates!, monotonic_time, seconds_to_utc_offset, stringify_keys, symbolize_keys, symbolize_keys!, time_in_iso8601

Constructor Details

#initialize(opts = {}) ⇒ Client

Returns a new instance of Client.

Parameters:

  • opts (Hash) (defaults to: {})

    Client configuration.

Options Hash (opts):

  • :api_key (String, nil)

    Your project's API key. Missing or blank values disable the client.

  • :secret_key (String, nil)

    The credential used for local feature flag evaluation and remote config. Accepts either a Personal API Key (phx_...) or a Project Secret API Key (phs_...). Required for local feature flag evaluation.

  • :personal_api_key (String, nil)

    @deprecated Use :secret_key instead. Retained as an alias; when both are supplied, :secret_key wins.

  • :host (String)

    Fully qualified hostname of the PostHog server. Defaults to https://us.i.posthog.com.

  • :max_queue_size (Integer)

    Maximum number of calls to remain queued. Defaults to 10_000.

  • :batch_size (Integer)

    Maximum number of events to send in one async batch.

  • :flush_interval_seconds (Numeric)

    Maximum seconds to wait for an async batch to fill before sending. Defaults to 5.

  • :test_mode (Boolean)

    true if messages should remain queued for testing. Defaults to false.

  • :sync_mode (Boolean)

    true to send events synchronously on the calling thread. Useful in forking environments like Sidekiq and Resque. Defaults to false.

  • :on_error (Proc)

    Callback invoked as on_error.call(status, error) for API or serialization errors.

  • :feature_flags_polling_interval (Integer)

    How often to poll for feature flag definition changes, in seconds. Defaults to 30.

  • :feature_flag_request_timeout_seconds (Integer)

    How long to wait for feature flag evaluation, in seconds. Defaults to 3.

  • :max_retries (Integer)

    How many times to retry batch uploads after the first send attempt. Defaults to the transport default. Set to 0 to disable retrying.

  • :feature_flag_request_max_retries (Integer)

    How many times to retry a flag request after a transient network error. Each retry sleeps on the calling thread before retrying, so this adds to worst-case latency. Defaults to 1. Set to 0 to disable retrying.

  • :feature_flags_async_load (Boolean)

    true to fetch feature flag definitions for local evaluation only on the poller's background thread instead of the calling thread. The constructor returns without waiting for definitions: the poller fetches immediately on boot and, if that fails, keeps retrying on its regular polling interval until a load succeeds. Until then local evaluation treats definitions as absent (check #feature_flags_loaded?), so evaluations return nil with only_evaluate_locally: true, or fall back to the remote flags endpoint (which still runs on the calling thread) without it. Defaults to false.

  • :before_send (Proc)

    A callback that receives the event hash and should return either a modified hash to be sent to PostHog or nil to prevent the event from being sent. e.g. before_send: ->(event) { event }.

  • :disable_singleton_warning (Boolean)

    true to suppress the warning when multiple clients share the same API key. Use only when you intentionally need multiple clients. Defaults to false.

  • :skip_ssl_verification (Boolean)

    true to disable SSL certificate verification for requests. Intended only for local development or custom deployments.

  • :compress_request (Boolean)

    Set to false to disable gzip compression for batch uploads.

  • :flag_definition_cache_provider (Object)

    An object implementing the FlagDefinitionCacheProvider interface for distributed flag definition caching.

  • :is_server (Boolean)

    true to stamp captured events with $is_server => true so PostHog attributes them as server-side. Defaults to true. Set to false when running posthog-ruby as a client/CLI so the device OS is attributed normally and $is_server is omitted.



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/posthog/client.rb', line 123

def initialize(opts = {})
  symbolize_keys!(opts)

  opts[:api_key] = normalize_string_option(opts[:api_key])
  opts[:secret_key] = normalize_string_option(opts[:secret_key], blank_as_nil: true)
  opts[:personal_api_key] = normalize_string_option(opts[:personal_api_key], blank_as_nil: true)
  opts[:host] = normalize_host_option(opts[:host])

  if opts[:secret_key].nil? && !opts[:personal_api_key].nil?
    logger.warn(
      'The :personal_api_key option is deprecated; use :secret_key instead. It accepts either a ' \
      'Personal API Key (phx_...) or a Project Secret API Key (phs_...).'
    )
  end
  secret_key = opts[:secret_key] || opts[:personal_api_key]

  @queue = Queue.new
  @queue_mutex = Mutex.new
  @api_key = opts[:api_key]
  @disabled = @api_key.nil? || @api_key.empty?
  @max_queue_size = opts[:max_queue_size] || Defaults::Queue::MAX_SIZE
  @worker_mutex = Mutex.new
  @shutdown_mutex = Mutex.new
  @shutdown_condition = ConditionVariable.new
  @shutdown = false
  @shutdown_complete = false
  @shutdown_result = false
  @sync_mode = opts[:sync_mode] == true && !opts[:test_mode] && !@disabled
  @on_error = opts[:on_error] || proc { |status, error| }
  @worker = if opts[:test_mode] || @disabled
              NoopWorker.new(@queue)
            elsif @sync_mode
              nil
            else
              SendWorker.new(@queue, @api_key, opts)
            end
  if @sync_mode
    @transport = Transport.new(
      api_host: opts[:host],
      skip_ssl_verification: opts[:skip_ssl_verification],
      retries: opts.key?(:max_retries) ? opts[:max_retries].to_i + 1 : 3,
      compress_request: opts[:compress_request]
    )
    @sync_lock = Mutex.new
  end
  @worker_thread = nil
  @feature_flags_poller = nil
  @secret_key = secret_key
  @personal_api_key = secret_key

  if @disabled && !opts[:silence_disabled_client_error]
    logger.error('api_key is missing or empty after trimming whitespace; check your project API key')
  end

  # Warn when multiple clients are created with the same API key (can cause dropped events)
  unless @disabled || opts[:test_mode] || opts[:disable_singleton_warning]
    previous_count = self.class._increment_instance_count(@api_key)
    if previous_count >= 1
      logger.warn(
        'Multiple PostHog client instances detected for the same API key. ' \
        'This can cause dropped events and inconsistent behavior. ' \
        'Use a singleton pattern: instantiate once and reuse the client. ' \
        'See https://posthog.com/docs/libraries/ruby'
      )
    end
  end

  unless @disabled
    @feature_flags_poller =
      FeatureFlagsPoller.new(
        opts[:feature_flags_polling_interval],
        secret_key,
        @api_key,
        opts[:host],
        opts[:feature_flag_request_timeout_seconds] || Defaults::FeatureFlags::FLAG_REQUEST_TIMEOUT_SECONDS,
        opts[:on_error],
        flag_definition_cache_provider: opts[:flag_definition_cache_provider],
        feature_flag_request_max_retries: opts[:feature_flag_request_max_retries],
        async_load: opts[:feature_flags_async_load] == true
      )
  end

  @distinct_id_has_sent_flag_calls_mutex = Mutex.new
  @distinct_id_has_sent_flag_calls = SizeLimitedHash.new(Defaults::MAX_HASH_SIZE) do |hash, key|
    hash[key] = SizeLimitedArray.new(Defaults::MAX_HASH_SIZE)
  end

  @before_send = opts[:before_send]
  @is_server = opts.fetch(:is_server, true) != false
  @deprecation_emitted_for = Concurrent::Set.new
end

Class Method Details

._decrement_instance_count(api_key) ⇒ Object



71
72
73
74
75
76
# File 'lib/posthog/client.rb', line 71

def _decrement_instance_count(api_key)
  @instances_mutex.synchronize do
    count = (@instances_by_api_key[api_key] || 1) - 1
    @instances_by_api_key[api_key] = [count, 0].max
  end
end

._increment_instance_count(api_key) ⇒ Object



63
64
65
66
67
68
69
# File 'lib/posthog/client.rb', line 63

def _increment_instance_count(api_key)
  @instances_mutex.synchronize do
    count = @instances_by_api_key[api_key] || 0
    @instances_by_api_key[api_key] = count + 1
    count
  end
end

.reset_instance_tracking!Object

Resets instance tracking. Used primarily for testing. In production, instance counts persist for the lifetime of the process.



57
58
59
60
61
# File 'lib/posthog/client.rb', line 57

def reset_instance_tracking!
  @instances_mutex.synchronize do
    @instances_by_api_key = {}
  end
end

Instance Method Details

#alias(attrs) ⇒ Boolean

Aliases a user from one id to another

Parameters:

  • attrs (Hash)

Options Hash (attrs):

  • :alias (String)

    The alias to give the distinct id

  • :message_id (String)

    Deprecated. Use :uuid instead. If :uuid is absent or invalid and :message_id is a valid UUID, it is sent as uuid for backwards compatibility. If neither value is valid, the SDK generates a uuid. SDK metadata is sent as $lib and $lib_version properties.

  • :timestamp (Time)

    When the event occurred (optional)

  • :distinct_id (String)

    The ID for this user in your database

Returns:

  • (Boolean)

    Whether the alias event was queued or sent.



447
448
449
450
451
452
453
# File 'lib/posthog/client.rb', line 447

def alias(attrs)
  return false if @disabled

  symbolize_keys! attrs
  attrs[:is_server] = @is_server
  enqueue(FieldParser.parse_for_alias(attrs))
end

#capture(attrs) ⇒ Boolean

Note:

If :distinct_id is omitted, request/context distinct_id is used when available; otherwise a UUID is generated and the event is marked personless with $process_person_profile: false.

Captures an event

Parameters:

  • attrs (Hash)

Options Hash (attrs):

  • :event (String)

    Event name

  • :properties (Hash)

    Event properties (optional)

  • :groups (Hash)

    Group analytics mapping from group type to group key (optional)

  • :send_feature_flags (Boolean, Hash, SendFeatureFlagsOptions)

    Deprecated. Whether to send feature flags with this event, or configuration for feature flag evaluation (optional)

  • :flags (PostHog::FeatureFlagEvaluations)

    A snapshot returned by #evaluate_flags. When present, $feature/<key> and $active_feature_flags are attached from the snapshot without making an additional /flags request, and this takes precedence over :send_feature_flags.

  • :uuid (String)

    ID that uniquely identifies an event; events in PostHog are deduplicated by the combination of teamId, timestamp date, event name, distinct id, and UUID

  • :message_id (String)

    Deprecated. Use :uuid instead. If :uuid is absent or invalid and :message_id is a valid UUID, it is sent as uuid for backwards compatibility. If neither value is valid, the SDK generates a uuid. SDK metadata is sent as $lib and $lib_version properties.

  • :timestamp (Time)

    When the event occurred (optional)

  • :distinct_id (String)

    The ID for this user in your database

Returns:

  • (Boolean)

    Whether the event was queued or sent.



291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
# File 'lib/posthog/client.rb', line 291

def capture(attrs)
  return false if @disabled

  symbolize_keys! attrs
  minimal_flag_called_event = attrs.delete(:_minimal_flag_called_event) == true
  enrich_capture_attrs_with_context(attrs)

  # Precedence: an explicit `flags` snapshot always wins, regardless of
  # `send_feature_flags`. The snapshot guarantees the event carries the same
  # values the developer branched on with no additional network call.
  if attrs[:flags]
    if attrs[:flags].is_a?(FeatureFlagEvaluations)
      if attrs[:send_feature_flags]
        logger.warn(
          '[FEATURE FLAGS] Both `flags` and `send_feature_flags` were passed to ' \
          'capture(); using `flags` and ignoring `send_feature_flags`.'
        )
      end
      snapshot_props = attrs[:flags]._get_event_properties
      attrs[:properties] = snapshot_props.merge(attrs[:properties] || {})
      attrs.delete(:flags)
      attrs.delete(:send_feature_flags)
    else
      logger.warn(
        '[FEATURE FLAGS] capture(flags:) expects a PostHog::FeatureFlagEvaluations snapshot ' \
        "from `client.evaluate_flags(...)`; got #{attrs[:flags].class}. Ignoring."
      )
      attrs.delete(:flags)
    end
  end

  send_feature_flags_param = attrs[:send_feature_flags]
  if send_feature_flags_param && !@disabled
    _emit_deprecation(
      :capture_send_feature_flags,
      '`send_feature_flags` on `capture` is deprecated and will be removed in a future major ' \
      'version. Pass a `flags` snapshot from `client.evaluate_flags(...)` instead — it ' \
      'avoids a second `/flags` request per capture and guarantees the event carries the ' \
      'exact flag values your code branched on.'
    )
    # Handle different types of send_feature_flags parameter
    case send_feature_flags_param
    when true
      # Backward compatibility: simple boolean
      feature_variants = @feature_flags_poller.get_feature_variants(attrs[:distinct_id], attrs[:groups] || {})
    when Hash
      # Hash with options
      options = SendFeatureFlagsOptions.from_hash(send_feature_flags_param)
      feature_variants = @feature_flags_poller.get_feature_variants(
        attrs[:distinct_id],
        attrs[:groups] || {},
        options ? options.person_properties : {},
        options ? options.group_properties : {},
        options ? options.only_evaluate_locally : false
      )
    when SendFeatureFlagsOptions
      # SendFeatureFlagsOptions object
      feature_variants = @feature_flags_poller.get_feature_variants(
        attrs[:distinct_id],
        attrs[:groups] || {},
        send_feature_flags_param.person_properties,
        send_feature_flags_param.group_properties,
        send_feature_flags_param.only_evaluate_locally || false
      )
    else
      # Invalid type, treat as false
      feature_variants = nil
    end

    attrs[:feature_variants] = feature_variants if feature_variants
  end

  attrs[:is_server] = @is_server
  message = FieldParser.parse_for_capture(attrs)
  # Minimal events are built from the allowlist after full assembly so
  # context properties and parser-added metadata can never leak in.
  if minimal_flag_called_event
    message[:properties] = message[:properties].slice(*MINIMAL_FLAG_CALLED_EVENT_PROPERTIES)
  end
  enqueue(message)
end

#capture_exception(exception, distinct_id = nil, additional_properties = {}, flags: nil, mechanism: nil) ⇒ Boolean?

Captures an exception as an event

Parameters:

  • exception (Exception, String, Object)

    The exception to capture, a string message, or exception-like object

  • distinct_id (String) (defaults to: nil)

    The ID for the user (optional, defaults to request/context distinct_id or a generated UUID)

  • additional_properties (Hash) (defaults to: {})

    Additional properties to include with the exception event (optional)

  • flags (PostHog::FeatureFlagEvaluations, nil) (defaults to: nil)

    A snapshot returned by #evaluate_flags. Forwarded to the inner #capture call so the captured $exception event carries the same $feature/<key> and $active_feature_flags properties as the snapshot.

  • mechanism (Hash, nil) (defaults to: nil)

    How the exception was captured, e.g. { 'type' => 'rails', 'handled' => false } for automatic integrations. Defaults to { 'type' => 'generic', 'handled' => true } for manual captures.

Returns:

  • (Boolean, nil)

    Whether the exception event was queued or sent, or nil if the input could not be parsed.



386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
# File 'lib/posthog/client.rb', line 386

def capture_exception(exception, distinct_id = nil, additional_properties = {}, flags: nil, mechanism: nil)
  return false if @disabled

  exception_list = ExceptionCapture.build_exception_list(exception, mechanism: mechanism)

  return if exception_list.nil?

  properties = { '$exception_list' => exception_list }
  properties.merge!(additional_properties) if additional_properties && !additional_properties.empty?

  event_data = {
    distinct_id: distinct_id,
    event: '$exception',
    properties: properties,
    timestamp: Time.now
  }
  event_data[:flags] = flags if flags

  capture(event_data)
end

#clearvoid

This method returns an undefined value.

Clears the queue without waiting.

Use only in test mode.



256
257
258
# File 'lib/posthog/client.rb', line 256

def clear
  @queue_mutex.synchronize { @queue.clear }
end

#dequeue_last_messageHash

Returns Pops the last message from the queue. Intended for test mode.

Returns:

  • (Hash)

    Pops the last message from the queue. Intended for test mode.



456
457
458
# File 'lib/posthog/client.rb', line 456

def dequeue_last_message
  @queue.pop
end

#enabled?Boolean

Whether the client will actually send events. It is disabled when the api_key is missing or blank, in which case every capture call no-ops.

Returns:

  • (Boolean)


873
874
875
# File 'lib/posthog/client.rb', line 873

def enabled?
  !@disabled
end

#evaluate_flags(distinct_id, groups: {}, person_properties: {}, group_properties: {}, only_evaluate_locally: false, disable_geoip: nil, flag_keys: nil) ⇒ PostHog::FeatureFlagEvaluations

Evaluate feature flags for a distinct id and return a snapshot.

The returned FeatureFlagEvaluations can be queried with is_enabled / get_flag / get_flag_payload, narrowed with only_accessed / only, and passed to #capture via the flags: option to attach $feature/<key> and $active_feature_flags without an extra /flags request.

Parameters:

  • distinct_id (String)

    The distinct id of the user

  • groups (Hash) (defaults to: {})
  • person_properties (Hash) (defaults to: {})

    key-value pairs of properties to associate with the user

  • group_properties (Hash) (defaults to: {})
  • only_evaluate_locally (Boolean) (defaults to: false)

    Skip the remote /flags call entirely

  • disable_geoip (Boolean, nil) (defaults to: nil)

    When true, disables GeoIP lookup for remote evaluation and stamps captured access events.

  • flag_keys (Array<String, Symbol>) (defaults to: nil)

    When set, scopes the underlying /flags request to only these flag keys (sent as flag_keys_to_evaluate). Distinct from FeatureFlagEvaluations#only, which filters the already-fetched snapshot in memory.

Returns:



614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
# File 'lib/posthog/client.rb', line 614

def evaluate_flags(
  distinct_id,
  groups: {},
  person_properties: {},
  group_properties: {},
  only_evaluate_locally: false,
  disable_geoip: nil,
  flag_keys: nil
)
  host = _feature_flag_evaluations_host

  if distinct_id.nil? || distinct_id.to_s.empty?
    return FeatureFlagEvaluations.new(host: host, distinct_id: '', flags: {})
  end

  return FeatureFlagEvaluations.new(host: host, distinct_id: distinct_id, flags: {}, groups: groups) if @disabled

  person_properties, group_properties = add_local_person_and_group_properties(
    groups, person_properties, group_properties
  )

  records = {}
  locally_evaluated_keys = Set.new
  flag_keys_set = flag_keys&.to_set(&:to_s)

  @feature_flags_poller.load_feature_flags
  poller_flags_by_key = @feature_flags_poller.feature_flags_by_key || {}

  poller_flags_by_key.each do |key, definition|
    next if flag_keys_set && !flag_keys_set.include?(key.to_s)

    begin
      match = @feature_flags_poller.send(
        :_compute_flag_locally,
        definition, distinct_id, groups, person_properties, group_properties
      )
    rescue PostHog::RequiresServerEvaluation, PostHog::InconclusiveMatchError, StandardError
      next
    end

    next if match.nil?

    records[key.to_s] = FeatureFlagEvaluations::EvaluatedFlagRecord.new(
      key: key.to_s,
      enabled: match.is_a?(String) || (match ? true : false),
      variant: match.is_a?(String) ? match : nil,
      payload: FeatureFlagResult.parse_payload(
        @feature_flags_poller.send(:_compute_flag_payload_locally, key, match)
      ),
      id: definition[:id],
      version: nil,
      reason: FeatureFlagEvaluations::EVALUATED_LOCALLY_REASON,
      locally_evaluated: true,
      has_experiment: definition[:has_experiment]
    )
    locally_evaluated_keys << key.to_s
  end

  request_id = nil
  evaluated_at = nil
  errors_while_computing = false
  quota_limited = false
  # Server-controlled gate for minimal `$feature_flag_called` events. When
  # the snapshot uses a remote /flags response, the response's top-level
  # `minimalFlagCalledEvents` field governs; a local-only snapshot reads
  # the gate polled with the flag definitions.
  minimal_flag_called_events = @feature_flags_poller.minimal_flag_called_events

  # Skip the remote `/flags` round-trip when the caller scoped the request
  # to a fixed set of `flag_keys` and we've already resolved every one of
  # them locally. Without `flag_keys` set, we can't know whether the server
  # has flags we don't have definitions for, so we still hit `/flags`.
  all_requested_flags_resolved_locally = flag_keys_set && (flag_keys_set - locally_evaluated_keys).empty?

  if !only_evaluate_locally && !all_requested_flags_resolved_locally
    # The gate is team-level, so the /flags response gate supersedes the
    # poller gate for mixed snapshots — both signals come from the same
    # server and agree in steady state. When the response omits the gate,
    # the whole snapshot fails safe to full events.
    minimal_flag_called_events = false
    begin
      flags_response = @feature_flags_poller.get_flags(
        distinct_id, groups, person_properties, group_properties, flag_keys, disable_geoip
      )
      minimal_flag_called_events = flags_response[:minimalFlagCalledEvents] == true
      request_id = flags_response[:requestId]
      evaluated_at = flags_response[:evaluatedAt]
      errors_while_computing = flags_response[:errorsWhileComputingFlags] == true
      quota_limited = (flags_response[:quotaLimited] || []).include?('feature_flags')
      remote_flags = flags_response[:flags] || {}
      remote_flags.each do |key, ff|
        key_str = key.to_s
        next if locally_evaluated_keys.include?(key_str)

         = ff.
        reason = ff.reason
        records[key_str] = FeatureFlagEvaluations::EvaluatedFlagRecord.new(
          key: key_str,
          enabled: ff.enabled ? true : false,
          variant: ff.variant,
          payload: FeatureFlagResult.parse_payload(ff.payload),
          id:  ? .id : nil,
          version:  ? .version : nil,
          reason: reason ? (reason.description || reason.code) : nil,
          locally_evaluated: false,
          has_experiment: &.has_experiment
        )
      end
    rescue StandardError => e
      @on_error&.call(-1, "Error evaluating flags remotely: #{e}")
    end
  end

  FeatureFlagEvaluations.new(
    host: host,
    distinct_id: distinct_id,
    flags: records,
    groups: groups,
    disable_geoip: disable_geoip,
    request_id: request_id,
    evaluated_at: evaluated_at,
    flag_definitions_loaded_at: @feature_flags_poller.flag_definitions_loaded_at,
    errors_while_computing: errors_while_computing,
    quota_limited: quota_limited,
    minimal_flag_called_events: minimal_flag_called_events
  )
end

#feature_flags_loaded?Boolean

Whether feature flag definitions for local evaluation are currently loaded. False until the first successful load, and false again if a quota-limited (402) response discards the definitions.

Returns:

  • (Boolean)


863
864
865
866
867
# File 'lib/posthog/client.rb', line 863

def feature_flags_loaded?
  return false if @disabled

  @feature_flags_poller.definitions_loaded?
end

#flush(timeout: nil) ⇒ Boolean

When sync_mode is enabled, blocks until in-flight requests are complete and returns true. Timeout parameter has no effect.

Otherwise, waits until the worker has cleared the queue or timeout is hit. Note: The asynchronous wait polls and can be starved if another thread is actively continuing to enqueue new events.

Parameters:

  • timeout (Numeric, nil) (defaults to: nil)

    Maximum seconds to wait for pending events to be sent, or nil to wait indefinitely.

Returns:

  • (Boolean)

    true if all pending events were sent, false if the timeout elapsed first.



226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'lib/posthog/client.rb', line 226

def flush(timeout: nil)
  if @sync_mode
    # Wait for any in-flight sync send to complete
    @sync_lock.synchronize {} # rubocop:disable Lint/EmptyBlock
    return true
  end

  if @worker.is_a?(NoopWorker)
    clear
    return true
  end

  deadline = timeout && (monotonic_time + timeout)

  while !@queue.empty? || @worker.is_requesting?
    ensure_worker_running
    @worker.request_flush
    remaining = deadline && (deadline - monotonic_time)
    return false if remaining && remaining <= 0

    sleep(remaining ? remaining.clamp(0, 0.1) : 0.1)
  end
  true
end

#get_all_flags(distinct_id, groups: {}, person_properties: {}, group_properties: {}, only_evaluate_locally: false) ⇒ Hash

Returns all flags for a given user

Parameters:

  • distinct_id (String)

    The distinct id of the user

  • groups (Hash) (defaults to: {})
  • person_properties (Hash) (defaults to: {})

    key-value pairs of properties to associate with the user.

  • group_properties (Hash) (defaults to: {})
  • only_evaluate_locally (Boolean) (defaults to: false)

    Skip the remote /flags call.

Returns:

  • (Hash)

    String (not symbol) key value pairs of flag and their values



751
752
753
754
755
756
757
758
759
760
761
762
763
764
# File 'lib/posthog/client.rb', line 751

def get_all_flags(
  distinct_id,
  groups: {},
  person_properties: {},
  group_properties: {},
  only_evaluate_locally: false
)
  return {} if @disabled

  person_properties, group_properties = add_local_person_and_group_properties(groups, person_properties,
                                                                              group_properties)
  @feature_flags_poller.get_all_flags(distinct_id, groups, person_properties, group_properties,
                                      only_evaluate_locally)
end

#get_all_flags_and_payloads(distinct_id, groups: {}, person_properties: {}, group_properties: {}, only_evaluate_locally: false) ⇒ Hash

Returns all flags and payloads for a given user

Parameters:

  • distinct_id (String)

    The distinct id of the user

  • groups (Hash) (defaults to: {})
  • person_properties (Hash) (defaults to: {})

    key-value pairs of properties to associate with the user.

  • group_properties (Hash) (defaults to: {})
  • only_evaluate_locally (Boolean) (defaults to: false)

    Skip the remote /flags call.

Returns:

  • (Hash)

    A hash with the following keys: featureFlags: A hash of feature flags featureFlagPayloads: A hash of feature flag payloads



818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
# File 'lib/posthog/client.rb', line 818

def get_all_flags_and_payloads(
  distinct_id,
  groups: {},
  person_properties: {},
  group_properties: {},
  only_evaluate_locally: false
)
  return { featureFlags: {}, featureFlagPayloads: {} } if @disabled

  person_properties, group_properties = add_local_person_and_group_properties(
    groups, person_properties, group_properties
  )
  response = @feature_flags_poller.get_all_flags_and_payloads(
    distinct_id, groups, person_properties, group_properties, only_evaluate_locally
  )

  # Remove internal information
  response.delete(:requestId)
  response.delete(:evaluatedAt)
  response.delete(:flagDetails)
  response.delete(:minimalFlagCalledEvents)
  response
end

#get_feature_flag(key, distinct_id, groups: {}, person_properties: {}, group_properties: {}, only_evaluate_locally: false, send_feature_flag_events: true) ⇒ String, ...

Returns whether the given feature flag is enabled for the given user or not

The provided properties are used to calculate feature flags locally, if possible.

groups are a mapping from group type to group key. So, if you have a group type of "organization" and a group key of "5", you would pass groups="5". group_properties take the format: { group_type_name: { group_properties } } So, for example, if you have the group type "organization" and the group key "5", with the properties name, and employee count, you'll send these as:

    group_properties: {"organization": {"name": "PostHog", "employees": 11}}

Parameters:

  • key (String, Symbol)

    The key of the feature flag

  • distinct_id (String)

    The distinct id of the user

  • groups (Hash) (defaults to: {})
  • person_properties (Hash) (defaults to: {})

    key-value pairs of properties to associate with the user.

  • group_properties (Hash) (defaults to: {})
  • only_evaluate_locally (Boolean) (defaults to: false)

    Skip the remote /flags call.

  • send_feature_flag_events (Boolean) (defaults to: true)

    Whether to capture $feature_flag_called for this access.

Returns:

  • (String, Boolean, nil)

    The value of the feature flag



535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
# File 'lib/posthog/client.rb', line 535

def get_feature_flag(
  key,
  distinct_id,
  groups: {},
  person_properties: {},
  group_properties: {},
  only_evaluate_locally: false,
  send_feature_flag_events: true
)
  _emit_deprecation(
    :get_feature_flag,
    '`get_feature_flag` is deprecated and will be removed in a future major version. ' \
    'Use `client.evaluate_flags(distinct_id, ...)` and call `flags.get_flag(key)` instead — ' \
    'this consolidates flag evaluation into a single `/flags` request per incoming request.'
  )
  # Bypass the public `get_feature_flag_result` so the user only sees one deprecation warning.
  result = _get_feature_flag_result(
    key, distinct_id,
    groups: groups, person_properties: person_properties, group_properties: group_properties,
    only_evaluate_locally: only_evaluate_locally, send_feature_flag_events: send_feature_flag_events
  )
  result&.value
end

#get_feature_flag_payload(key, distinct_id, match_value: nil, groups: {}, person_properties: {}, group_properties: {}, only_evaluate_locally: false) ⇒ Object?

Deprecated.

Use #get_feature_flag_result instead, which returns both the flag value and payload and properly raises the $feature_flag_called event.

Returns payload for a given feature flag

Parameters:

  • key (String, Symbol)

    The key of the feature flag

  • distinct_id (String)

    The distinct id of the user

  • match_value (String, Boolean, nil) (defaults to: nil)

    The value of the feature flag to be matched

  • groups (Hash) (defaults to: {})
  • person_properties (Hash) (defaults to: {})

    key-value pairs of properties to associate with the user.

  • group_properties (Hash) (defaults to: {})
  • only_evaluate_locally (Boolean) (defaults to: false)

Returns:

  • (Object, nil)

    The parsed payload for the matched flag value.



781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
# File 'lib/posthog/client.rb', line 781

def get_feature_flag_payload(
  key,
  distinct_id,
  match_value: nil,
  groups: {},
  person_properties: {},
  group_properties: {},
  only_evaluate_locally: false
)
  _emit_deprecation(
    :get_feature_flag_payload,
    '`get_feature_flag_payload` is deprecated and will be removed in a future major version. ' \
    'Use `client.evaluate_flags(distinct_id, ...)` and call `flags.get_flag_payload(key)` ' \
    'instead — this consolidates flag evaluation into a single `/flags` request per ' \
    'incoming request.'
  )
  return nil if @disabled

  key = key.to_s
  person_properties, group_properties = add_local_person_and_group_properties(groups, person_properties,
                                                                              group_properties)
  @feature_flags_poller.get_feature_flag_payload(key, distinct_id, match_value, groups, person_properties,
                                                 group_properties, only_evaluate_locally)
end

#get_feature_flag_result(key, distinct_id, groups: {}, person_properties: {}, group_properties: {}, only_evaluate_locally: false, send_feature_flag_events: true) ⇒ PostHog::FeatureFlagResult?

Parameters:

  • key (String, Symbol)

    The unique key of the feature flag.

  • distinct_id (String)

    The distinct id of the user.

  • groups (Hash) (defaults to: {})

    Group analytics mapping from group type to group key.

  • person_properties (Hash) (defaults to: {})

    Properties to use when evaluating the user locally or remotely.

  • group_properties (Hash) (defaults to: {})

    Properties to use when evaluating groups locally or remotely.

  • only_evaluate_locally (Boolean) (defaults to: false)

    Skip the remote /flags call.

  • send_feature_flag_events (Boolean) (defaults to: true)

    Whether to capture $feature_flag_called for this access.

Returns:



569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
# File 'lib/posthog/client.rb', line 569

def get_feature_flag_result(
  key,
  distinct_id,
  groups: {},
  person_properties: {},
  group_properties: {},
  only_evaluate_locally: false,
  send_feature_flag_events: true
)
  _emit_deprecation(
    :get_feature_flag_result,
    '`get_feature_flag_result` is deprecated and will be removed in a future major version. ' \
    'Use `client.evaluate_flags(distinct_id, ...)` and call `flags.get_flag(key)` / ' \
    '`flags.get_flag_payload(key)` instead — this consolidates flag evaluation into a single ' \
    '`/flags` request per incoming request.'
  )
  return nil if @disabled

  _get_feature_flag_result(
    key, distinct_id,
    groups: groups, person_properties: person_properties, group_properties: group_properties,
    only_evaluate_locally: only_evaluate_locally, send_feature_flag_events: send_feature_flag_events
  )
end

#get_remote_config_payload(flag_key) ⇒ Hash

Returns The parsed remote config payload response.

Parameters:

  • flag_key (String, Symbol)

    The unique flag key of the remote config feature flag.

Returns:

  • (Hash)

    The parsed remote config payload response.



505
506
507
508
509
# File 'lib/posthog/client.rb', line 505

def get_remote_config_payload(flag_key)
  return nil if @disabled

  @feature_flags_poller.get_remote_config_payload(flag_key.to_s)
end

#group_identify(attrs) ⇒ Boolean

Identifies a group

Parameters:

  • attrs (Hash)

Options Hash (attrs):

  • :group_type (String)

    Group type

  • :group_key (String)

    Group key

  • :properties (Hash)

    Group properties (optional)

  • :distinct_id (String)

    Distinct ID (optional)

  • :message_id (String)

    Deprecated. Use :uuid instead. If :uuid is absent or invalid and :message_id is a valid UUID, it is sent as uuid for backwards compatibility. If neither value is valid, the SDK generates a uuid. SDK metadata is sent as $lib and $lib_version properties.

  • :timestamp (Time)

    When the event occurred (optional)

  • :distinct_id (String)

    The ID for this user in your database

Returns:

  • (Boolean)

    Whether the group identify event was queued or sent.



432
433
434
435
436
437
438
# File 'lib/posthog/client.rb', line 432

def group_identify(attrs)
  return false if @disabled

  symbolize_keys! attrs
  attrs[:is_server] = @is_server
  enqueue(FieldParser.parse_for_group_identify(attrs))
end

#identify(attrs) ⇒ Boolean

Identifies a user

Parameters:

  • attrs (Hash)

Options Hash (attrs):

  • :properties (Hash)

    User properties (optional)

  • :message_id (String)

    Deprecated. Use :uuid instead. If :uuid is absent or invalid and :message_id is a valid UUID, it is sent as uuid for backwards compatibility. If neither value is valid, the SDK generates a uuid. SDK metadata is sent as $lib and $lib_version properties.

  • :timestamp (Time)

    When the event occurred (optional)

  • :distinct_id (String)

    The ID for this user in your database

Returns:

  • (Boolean)

    Whether the identify event was queued or sent.



414
415
416
417
418
419
420
# File 'lib/posthog/client.rb', line 414

def identify(attrs)
  return false if @disabled

  symbolize_keys! attrs
  attrs[:is_server] = @is_server
  enqueue(FieldParser.parse_for_identify(attrs))
end

#is_feature_enabled(flag_key, distinct_id, groups: {}, person_properties: {}, group_properties: {}, only_evaluate_locally: false, send_feature_flag_events: true) ⇒ Boolean?

TODO: In future version, rename to feature_flag_enabled?

Parameters:

  • flag_key (String, Symbol)

    The unique key of the feature flag.

  • distinct_id (String)

    The distinct id of the user.

  • groups (Hash) (defaults to: {})

    Group analytics mapping from group type to group key.

  • person_properties (Hash) (defaults to: {})

    Properties to use when evaluating the user locally or remotely.

  • group_properties (Hash) (defaults to: {})

    Properties to use when evaluating groups locally or remotely.

  • only_evaluate_locally (Boolean) (defaults to: false)

    Skip the remote /flags call.

  • send_feature_flag_events (Boolean) (defaults to: true)

    Whether to capture $feature_flag_called for this access.

Returns:

  • (Boolean, nil)

    Whether the flag is enabled, or nil when the flag could not be evaluated.



475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
# File 'lib/posthog/client.rb', line 475

def is_feature_enabled( # rubocop:disable Naming/PredicateName
  flag_key,
  distinct_id,
  groups: {},
  person_properties: {},
  group_properties: {},
  only_evaluate_locally: false,
  send_feature_flag_events: true
)
  _emit_deprecation(
    :is_feature_enabled,
    '`is_feature_enabled` is deprecated and will be removed in a future major version. ' \
    'Use `client.evaluate_flags(distinct_id, ...)` and call `flags.enabled?(key)` instead — ' \
    'this consolidates flag evaluation into a single `/flags` request per incoming request.'
  )
  # Bypass the public `get_feature_flag` so the user only sees a single deprecation
  # warning per call, not a cascade.
  result = _get_feature_flag_result(
    flag_key, distinct_id,
    groups: groups, person_properties: person_properties, group_properties: group_properties,
    only_evaluate_locally: only_evaluate_locally, send_feature_flag_events: send_feature_flag_events
  )
  response = result&.value
  return nil if response.nil?

  !!response
end

#queued_messagesInteger

Returns Number of messages in the queue. Intended for test mode.

Returns:

  • (Integer)

    Number of messages in the queue. Intended for test mode.



461
462
463
# File 'lib/posthog/client.rb', line 461

def queued_messages
  @queue.length
end

#reload_feature_flagsvoid

This method returns an undefined value.

Reload locally cached feature flag definitions synchronously on the calling thread.



846
847
848
849
850
851
852
853
854
855
856
# File 'lib/posthog/client.rb', line 846

def reload_feature_flags
  return if @disabled

  unless @secret_key
    logger.error(
      'You need to specify a secret_key to locally evaluate feature flags'
    )
    return
  end
  @feature_flags_poller.load_feature_flags(true)
end

#shutdown(timeout: nil) ⇒ Boolean

Flush pending events and stop background resources.

When sync_mode is not set, this method calls the flush method; see flush method documentation for timeout semantics. Unlike flush, this method stops the worker. So any events still queued after the timeout will not be sent. After the timeout, this method may wait up to one additional second for worker and transport cleanup, which may continue after it returns.

Parameters:

  • timeout (Numeric, nil) (defaults to: nil)

    Maximum seconds to wait for pending events to be sent, or nil to wait indefinitely. Has no effect when sync_mode is true.

Returns:

  • (Boolean)

    true if all pending events were sent, false if the timeout elapsed first.



890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
# File 'lib/posthog/client.rb', line 890

def shutdown(timeout: nil)
  deadline = timeout && (monotonic_time + timeout)
  shutdown_result = @shutdown_mutex.synchronize do
    if @shutdown
      until @shutdown_complete
        # Avoid deadlocking when an in-flight send's on_error callback
        # re-enters shutdown while the original caller is waiting for it.
        break if Thread.current == @worker_thread || (@sync_mode && @sync_lock.owned?)

        remaining = deadline && (deadline - monotonic_time)
        break if remaining && remaining <= 0

        @shutdown_condition.wait(@shutdown_mutex, remaining)
      end
      @shutdown_complete ? @shutdown_result : false
    else
      @shutdown = true
      nil
    end
  end
  return shutdown_result unless shutdown_result.nil?

  flushed = false
  begin
    self.class._decrement_instance_count(@api_key) unless @disabled
    @feature_flags_poller&.shutdown_poller
    flushed =
      if @sync_mode
        # Waiting for @sync_lock lets any in-flight sync send finish before
        # the connection is closed.
        @sync_lock.synchronize { @transport&.shutdown }
        true
      else
        drained = flush(timeout: timeout)
        @worker&.shutdown
        @worker_thread&.join(1)
        drained
      end
    @distinct_id_has_sent_flag_calls_mutex.synchronize do
      @distinct_id_has_sent_flag_calls.clear
    end
    flushed
  ensure
    @shutdown_mutex.synchronize do
      @shutdown_result = flushed
      @shutdown_complete = true
      @shutdown_condition.broadcast
    end
  end
end