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!, seconds_to_utc_offset, stringify_keys, symbolize_keys, symbolize_keys!, time_in_iso8601, uid

Constructor Details

#initialize(opts = {}) ⇒ Client

Returns a new instance of Client.

Parameters:

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

Options Hash (opts):

  • :api_key (String)

    Your project’s api_key

  • :personal_api_key (String)

    Your personal API key

  • :max_queue_size (FixNum)

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

  • :test_mode (Bool)

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

  • :sync_mode (Bool)

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

  • :on_error (Proc)

    Handles error calls from the API.

  • :host (String)

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

  • :feature_flags_polling_interval (Integer)

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

  • :feature_flag_request_timeout_seconds (Integer)

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

  • :before_send (Proc)

    A block 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 (Bool)

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

  • :flag_definition_cache_provider (Object)

    An object implementing the FlagDefinitionCacheProvider interface for distributed flag definition caching. EXPERIMENTAL: This API may change in future minor version bumps.



74
75
76
77
78
79
80
81
82
83
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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/posthog/client.rb', line 74

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

  opts[:host] ||= 'https://app.posthog.com'

  @queue = Queue.new
  @api_key = opts[:api_key]
  @max_queue_size = opts[:max_queue_size] || Defaults::Queue::MAX_SIZE
  @worker_mutex = Mutex.new
  @sync_mode = opts[:sync_mode] == true && !opts[:test_mode]
  @on_error = opts[:on_error] || proc { |status, error| }
  @worker = if opts[:test_mode]
              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: 3
    )
    @sync_lock = Mutex.new
  end
  @worker_thread = nil
  @feature_flags_poller = nil
  @personal_api_key = opts[:personal_api_key]

  check_api_key!

  # Warn when multiple clients are created with the same API key (can cause dropped events)
  unless 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

  @feature_flags_poller =
    FeatureFlagsPoller.new(
      opts[:feature_flags_polling_interval],
      opts[:personal_api_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]
    )

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

  @before_send = opts[:before_send]
end

Class Method Details

._decrement_instance_count(api_key) ⇒ Object



43
44
45
46
47
48
# File 'lib/posthog/client.rb', line 43

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



35
36
37
38
39
40
41
# File 'lib/posthog/client.rb', line 35

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.



29
30
31
32
33
# File 'lib/posthog/client.rb', line 29

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

Instance Method Details

#alias(attrs) ⇒ Object

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)

    ID that uniquely identifies a message across the API. (optional)

  • :timestamp (Time)

    When the event occurred (optional)

  • :distinct_id (String)

    The ID for this user in your database



278
279
280
281
# File 'lib/posthog/client.rb', line 278

def alias(attrs)
  symbolize_keys! attrs
  enqueue(FieldParser.parse_for_alias(attrs))
end

#capture(attrs) ⇒ Object

Captures an event

Parameters:

  • attrs (Hash)

Options Hash (attrs):

  • :event (String)

    Event name

  • :properties (Hash)

    Event properties (optional)

  • :send_feature_flags (Bool, Hash, SendFeatureFlagsOptions)

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

  • :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)

    ID that uniquely identifies a message across the API. (optional)

  • :timestamp (Time)

    When the event occurred (optional)

  • :distinct_id (String)

    The ID for this user in your database



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
214
215
216
217
218
# File 'lib/posthog/client.rb', line 180

def capture(attrs)
  symbolize_keys! attrs

  send_feature_flags_param = attrs[:send_feature_flags]
  if send_feature_flags_param
    # 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

  enqueue(FieldParser.parse_for_capture(attrs))
end

#capture_exception(exception, distinct_id = nil, additional_properties = {}) ⇒ Object

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 a generated UUID)

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

    Additional properties to include with the exception event (optional)



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

def capture_exception(exception, distinct_id = nil, additional_properties = {})
  exception_info = ExceptionCapture.build_parsed_exception(exception)

  return if exception_info.nil?

  no_distinct_id_was_provided = distinct_id.nil?
  distinct_id ||= SecureRandom.uuid

  properties = { '$exception_list' => [exception_info] }
  properties.merge!(additional_properties) if additional_properties && !additional_properties.empty?
  properties['$process_person_profile'] = false if no_distinct_id_was_provided

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

  capture(event_data)
end

#clearObject

Clears the queue without waiting.

Use only in test mode



157
158
159
# File 'lib/posthog/client.rb', line 157

def clear
  @queue.clear
end

#dequeue_last_messageHash

Returns pops the last message from the queue.

Returns:

  • (Hash)

    pops the last message from the queue



284
285
286
# File 'lib/posthog/client.rb', line 284

def dequeue_last_message
  @queue.pop
end

#flushObject

Synchronously waits until the worker has cleared the queue.

Use only for scripts which are not long-running, and will specifically exit



141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/posthog/client.rb', line 141

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

  while !@queue.empty? || @worker.is_requesting?
    ensure_worker_running
    sleep(0.1)
  end
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: {})

Returns:

  • (Hash)

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



434
435
436
437
438
439
440
441
442
443
444
445
# File 'lib/posthog/client.rb', line 434

def get_all_flags(
  distinct_id,
  groups: {},
  person_properties: {},
  group_properties: {},
  only_evaluate_locally: false
)
  person_properties, group_properties = add_local_person_and_group_properties(distinct_id, 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

  • [Hash] (Hash)

    a customizable set of options

  • [Boolean] (Hash)

    a customizable set of options

Returns:

  • (Hash)

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



487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
# File 'lib/posthog/client.rb', line 487

def get_all_flags_and_payloads(
  distinct_id,
  groups: {},
  person_properties: {},
  group_properties: {},
  only_evaluate_locally: false
)
  person_properties, group_properties = add_local_person_and_group_properties(
    distinct_id, 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
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: “‘ruby

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

“‘

Parameters:

  • key (String)

    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: {})

Returns:

  • (String, nil)

    The value of the feature flag



344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
# File 'lib/posthog/client.rb', line 344

def get_feature_flag(
  key,
  distinct_id,
  groups: {},
  person_properties: {},
  group_properties: {},
  only_evaluate_locally: false,
  send_feature_flag_events: true
)
  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)

    The key of the feature flag

  • distinct_id (String)

    The distinct id of the user

  • [String (Hash)

    a customizable set of options

  • [Hash] (Hash)

    a customizable set of options

  • [Boolean] (Hash)

    a customizable set of options



460
461
462
463
464
465
466
467
468
469
470
471
472
473
# File 'lib/posthog/client.rb', line 460

def get_feature_flag_payload(
  key,
  distinct_id,
  match_value: nil,
  groups: {},
  person_properties: {},
  group_properties: {},
  only_evaluate_locally: false
)
  person_properties, group_properties = add_local_person_and_group_properties(distinct_id, 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) ⇒ FeatureFlagResult?

Returns both the feature flag value and payload in a single call. This method raises the $feature_flag_called event with the payload included.

Parameters:

  • key (String)

    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)
  • send_feature_flag_events (Boolean) (defaults to: true)

Returns:

  • (FeatureFlagResult, nil)

    A FeatureFlagResult object containing the flag value and payload, or nil if the flag evaluation returned nil



378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
# File 'lib/posthog/client.rb', line 378

def get_feature_flag_result(
  key,
  distinct_id,
  groups: {},
  person_properties: {},
  group_properties: {},
  only_evaluate_locally: false,
  send_feature_flag_events: true
)
  person_properties, group_properties = add_local_person_and_group_properties(
    distinct_id,
    groups,
    person_properties,
    group_properties
  )
  feature_flag_response, flag_was_locally_evaluated, request_id, evaluated_at, feature_flag_error, payload =
    @feature_flags_poller.get_feature_flag(
      key,
      distinct_id,
      groups,
      person_properties,
      group_properties,
      only_evaluate_locally
    )
  feature_flag_reported_key = "#{key}_#{feature_flag_response}"

  if !@distinct_id_has_sent_flag_calls[distinct_id].include?(feature_flag_reported_key) && send_feature_flag_events
    properties = {
      '$feature_flag' => key,
      '$feature_flag_response' => feature_flag_response,
      'locally_evaluated' => flag_was_locally_evaluated
    }
    properties['$feature_flag_request_id'] = request_id if request_id
    properties['$feature_flag_evaluated_at'] = evaluated_at if evaluated_at
    properties['$feature_flag_error'] = feature_flag_error if feature_flag_error

    capture(
      distinct_id: distinct_id,
      event: '$feature_flag_called',
      properties: properties,
      groups: groups
    )
    @distinct_id_has_sent_flag_calls[distinct_id] << feature_flag_reported_key
  end

  FeatureFlagResult.from_value_and_payload(key, feature_flag_response, payload)
end

#get_remote_config_payload(flag_key) ⇒ String

Returns The decrypted value of the feature flag payload.

Parameters:

  • flag_key (String)

    The unique flag key of the feature flag

Returns:

  • (String)

    The decrypted value of the feature flag payload



319
320
321
# File 'lib/posthog/client.rb', line 319

def get_remote_config_payload(flag_key)
  @feature_flags_poller.get_remote_config_payload(flag_key)
end

#group_identify(attrs) ⇒ Object

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)

    ID that uniquely identifies a message across the API. (optional)

  • :timestamp (Time)

    When the event occurred (optional)

  • :distinct_id (String)

    The ID for this user in your database



267
268
269
270
# File 'lib/posthog/client.rb', line 267

def group_identify(attrs)
  symbolize_keys! attrs
  enqueue(FieldParser.parse_for_group_identify(attrs))
end

#identify(attrs) ⇒ Object

Identifies a user

Parameters:

  • attrs (Hash)

Options Hash (attrs):

  • :properties (Hash)

    User properties (optional)

  • :message_id (String)

    ID that uniquely identifies a message across the API. (optional)

  • :timestamp (Time)

    When the event occurred (optional)

  • :distinct_id (String)

    The ID for this user in your database



253
254
255
256
# File 'lib/posthog/client.rb', line 253

def identify(attrs)
  symbolize_keys! attrs
  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) ⇒ Object

TODO: In future version, rename to feature_flag_enabled?



294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
# File 'lib/posthog/client.rb', line 294

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
)
  response = get_feature_flag(
    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
  )
  return nil if response.nil?

  !!response
end

#queued_messagesFixnum

Returns number of messages in the queue.

Returns:

  • (Fixnum)

    number of messages in the queue



289
290
291
# File 'lib/posthog/client.rb', line 289

def queued_messages
  @queue.length
end

#reload_feature_flagsObject



507
508
509
510
511
512
513
514
515
# File 'lib/posthog/client.rb', line 507

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

#shutdownObject



517
518
519
520
521
522
523
524
525
526
# File 'lib/posthog/client.rb', line 517

def shutdown
  self.class._decrement_instance_count(@api_key) if @api_key
  @feature_flags_poller.shutdown_poller
  flush
  if @sync_mode
    @sync_lock.synchronize { @transport&.shutdown }
  else
    @worker&.shutdown
  end
end