From 013d1742977dbf72aec4044a28ec959ecd9ced96 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Wed, 20 May 2026 14:39:15 +0200 Subject: [PATCH] docs: document public API annotations --- lib/posthog/backoff_policy.rb | 5 +- lib/posthog/client.rb | 125 +++-- lib/posthog/exception_capture.rb | 22 + lib/posthog/feature_flag.rb | 21 +- lib/posthog/feature_flag_error.rb | 1 + lib/posthog/feature_flag_evaluations.rb | 43 +- lib/posthog/feature_flag_result.rb | 33 +- lib/posthog/feature_flags.rb | 10 + lib/posthog/field_parser.rb | 11 + lib/posthog/flag_definition_cache.rb | 14 +- lib/posthog/logging.rb | 9 +- lib/posthog/message_batch.rb | 9 +- lib/posthog/noop_worker.rb | 8 +- lib/posthog/response.rb | 8 +- lib/posthog/send_feature_flags_options.rb | 19 +- lib/posthog/send_worker.rb | 21 +- lib/posthog/transport.rb | 19 +- lib/posthog/utils.rb | 10 + posthog-rails/README.md | 457 +----------------- posthog-rails/lib/posthog/rails.rb | 12 + posthog-rails/lib/posthog/rails/active_job.rb | 8 +- .../lib/posthog/rails/capture_exceptions.rb | 7 +- .../lib/posthog/rails/configuration.rb | 26 +- .../lib/posthog/rails/error_subscriber.rb | 12 +- .../lib/posthog/rails/parameter_filter.rb | 4 + posthog-rails/lib/posthog/rails/railtie.rb | 43 +- .../lib/posthog/rails/request_context.rb | 5 + .../lib/posthog/rails/request_metadata.rb | 4 + .../rails/rescued_exception_interceptor.rb | 9 +- .../lib/posthog/rails/tracing_headers.rb | 7 + 30 files changed, 436 insertions(+), 546 deletions(-) diff --git a/lib/posthog/backoff_policy.rb b/lib/posthog/backoff_policy.rb index b578f30..816f140 100644 --- a/lib/posthog/backoff_policy.rb +++ b/lib/posthog/backoff_policy.rb @@ -3,10 +3,13 @@ require 'posthog/defaults' module PostHog + # Retry backoff policy used by the SDK transport. + # + # @api private class BackoffPolicy include PostHog::Defaults::BackoffPolicy - # @param [Hash] opts + # @param opts [Hash] # @option opts [Numeric] :min_timeout_ms The minimum backoff timeout # @option opts [Numeric] :max_timeout_ms The maximum backoff timeout # @option opts [Numeric] :multiplier The value to multiply the current diff --git a/lib/posthog/client.rb b/lib/posthog/client.rb index a035340..1f04a5f 100644 --- a/lib/posthog/client.rb +++ b/lib/posthog/client.rb @@ -50,29 +50,28 @@ def _decrement_instance_count(api_key) end end - # @param [Hash] opts - # @option opts [String] :api_key Your project's api_key - # @option opts [String] :personal_api_key Your personal API key - # @option opts [FixNum] :max_queue_size Maximum number of calls to be - # remain queued. Defaults to 10_000. - # @option opts [Bool] :test_mode +true+ if messages should remain - # queued for testing. Defaults to +false+. - # @option opts [Bool] :sync_mode +true+ to send events synchronously - # on the calling thread. Useful in forking environments like Sidekiq - # and Resque. Defaults to +false+. - # @option opts [Proc] :on_error Handles error calls from the API. - # @option opts [String] :host Fully qualified hostname of the PostHog server. Defaults to `https://us.i.posthog.com` - # @option opts [Integer] :feature_flags_polling_interval How often to poll for feature flag definition changes. - # Measured in seconds, defaults to 30. - # @option opts [Integer] :feature_flag_request_timeout_seconds How long to wait for feature flag evaluation. - # Measured in seconds, defaults to 3. - # @option opts [Proc] :before_send 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 }` - # @option opts [Bool] :disable_singleton_warning +true+ to suppress the warning when multiple clients - # share the same API key. Use only when you intentionally need multiple clients. Defaults to +false+. - # @option opts [Object] :flag_definition_cache_provider An object implementing the - # {FlagDefinitionCacheProvider} interface for distributed flag definition caching. - # EXPERIMENTAL: This API may change in future minor version bumps. + # @param opts [Hash] Client configuration. + # @option opts [String] :api_key Your project's API key. Required. + # @option opts [String, nil] :personal_api_key Your personal API key. Required for local feature flag evaluation. + # @option opts [String] :host Fully qualified hostname of the PostHog server. Defaults to `https://us.i.posthog.com`. + # @option opts [Integer] :max_queue_size Maximum number of calls to remain queued. Defaults to 10_000. + # @option opts [Integer] :batch_size Maximum number of events to send in one async batch. + # @option opts [Boolean] :test_mode +true+ if messages should remain queued for testing. Defaults to +false+. + # @option opts [Boolean] :sync_mode +true+ to send events synchronously on the calling thread. Useful in + # forking environments like Sidekiq and Resque. Defaults to +false+. + # @option opts [Proc] :on_error Callback invoked as `on_error.call(status, error)` for API or serialization errors. + # @option opts [Integer] :feature_flags_polling_interval How often to poll for feature flag definition changes, + # in seconds. Defaults to 30. + # @option opts [Integer] :feature_flag_request_timeout_seconds How long to wait for feature flag evaluation, + # in seconds. Defaults to 3. + # @option opts [Proc] :before_send 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 }`. + # @option opts [Boolean] :disable_singleton_warning +true+ to suppress the warning when multiple clients share + # the same API key. Use only when you intentionally need multiple clients. Defaults to +false+. + # @option opts [Boolean] :skip_ssl_verification +true+ to disable SSL certificate verification for requests. + # Intended only for local development or custom deployments. + # @option opts [Object] :flag_definition_cache_provider An object implementing the {FlagDefinitionCacheProvider} + # interface for distributed flag definition caching. EXPERIMENTAL: This API may change in future minor versions. def initialize(opts = {}) symbolize_keys!(opts) @@ -143,7 +142,9 @@ def initialize(opts = {}) # Synchronously waits until the worker has cleared the queue. # # Use only for scripts which are not long-running, and will specifically - # exit + # exit. + # + # @return [void] def flush if @sync_mode # Wait for any in-flight sync send to complete @@ -159,7 +160,9 @@ def flush # Clears the queue without waiting. # - # Use only in test mode + # Use only in test mode. + # + # @return [void] def clear @queue.clear end @@ -176,8 +179,10 @@ def clear # # @option attrs [String] :event Event name # @option attrs [Hash] :properties Event properties (optional) - # @option attrs [Bool, Hash, SendFeatureFlagsOptions] :send_feature_flags - # Whether to send feature flags with this event, or configuration for feature flag evaluation (optional) + # @option attrs [Hash] :groups Group analytics mapping from group type to group key (optional) + # @option attrs [Boolean, Hash, SendFeatureFlagsOptions] :send_feature_flags + # Deprecated. Whether to send feature flags with this event, or configuration for feature flag evaluation + # (optional) # @option attrs [PostHog::FeatureFlagEvaluations] :flags A snapshot returned by # {#evaluate_flags}. When present, `$feature/` and `$active_feature_flags` are # attached from the snapshot without making an additional /flags request, and this @@ -189,6 +194,7 @@ def clear # @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`. + # @return [Boolean] Whether the event was queued or sent. # @macro common_attrs def capture(attrs) symbolize_keys! attrs @@ -268,9 +274,10 @@ def capture(attrs) # @param [String] distinct_id The ID for the user (optional, defaults to request/context distinct_id # or a generated UUID) # @param [Hash] additional_properties Additional properties to include with the exception event (optional) - # @param [PostHog::FeatureFlagEvaluations] flags A snapshot returned by {#evaluate_flags}. + # @param flags [PostHog::FeatureFlagEvaluations, nil] A snapshot returned by {#evaluate_flags}. # Forwarded to the inner {#capture} call so the captured `$exception` event carries the # same `$feature/` and `$active_feature_flags` properties as the snapshot. + # @return [Boolean, nil] Whether the exception event was queued or sent, or nil if the input could not be parsed. def capture_exception(exception, distinct_id = nil, additional_properties = {}, flags: nil) exception_info = ExceptionCapture.build_parsed_exception(exception) @@ -295,6 +302,7 @@ def capture_exception(exception, distinct_id = nil, additional_properties = {}, # @param [Hash] attrs # # @option attrs [Hash] :properties User properties (optional) + # @return [Boolean] Whether the identify event was queued or sent. # @macro common_attrs def identify(attrs) symbolize_keys! attrs @@ -309,6 +317,7 @@ def identify(attrs) # @option attrs [String] :group_key Group key # @option attrs [Hash] :properties Group properties (optional) # @option attrs [String] :distinct_id Distinct ID (optional) + # @return [Boolean] Whether the group identify event was queued or sent. # @macro common_attrs def group_identify(attrs) symbolize_keys! attrs @@ -320,23 +329,32 @@ def group_identify(attrs) # @param [Hash] attrs # # @option attrs [String] :alias The alias to give the distinct id + # @return [Boolean] Whether the alias event was queued or sent. # @macro common_attrs def alias(attrs) symbolize_keys! attrs enqueue(FieldParser.parse_for_alias(attrs)) end - # @return [Hash] pops the last message from the queue + # @return [Hash] Pops the last message from the queue. Intended for test mode. def dequeue_last_message @queue.pop end - # @return [Fixnum] number of messages in the queue + # @return [Integer] Number of messages in the queue. Intended for test mode. def queued_messages @queue.length end - # @deprecated Use {#evaluate_flags} and {FeatureFlagEvaluations#is_enabled} instead. + # @deprecated Use {#evaluate_flags} and {FeatureFlagEvaluations#enabled?} instead. + # @param flag_key [String, Symbol] The unique key of the feature flag. + # @param distinct_id [String] The distinct id of the user. + # @param groups [Hash] Group analytics mapping from group type to group key. + # @param person_properties [Hash] Properties to use when evaluating the user locally or remotely. + # @param group_properties [Hash] Properties to use when evaluating groups locally or remotely. + # @param only_evaluate_locally [Boolean] Skip the remote /flags call. + # @param send_feature_flag_events [Boolean] Whether to capture `$feature_flag_called` for this access. + # @return [Boolean, nil] Whether the flag is enabled, or nil when the flag could not be evaluated. # TODO: In future version, rename to `feature_flag_enabled?` def is_feature_enabled( # rubocop:disable Naming/PredicateName flag_key, @@ -366,8 +384,8 @@ def is_feature_enabled( # rubocop:disable Naming/PredicateName !!response end - # @param [String, Symbol] flag_key The unique flag key of the feature flag - # @return [String] The decrypted value of the feature flag payload + # @param flag_key [String, Symbol] The unique flag key of the remote config feature flag. + # @return [Hash] The parsed remote config payload response. def get_remote_config_payload(flag_key) @feature_flags_poller.get_remote_config_payload(flag_key.to_s) end @@ -379,8 +397,10 @@ def get_remote_config_payload(flag_key) # @param [Hash] groups # @param [Hash] person_properties key-value pairs of properties to associate with the user. # @param [Hash] group_properties + # @param only_evaluate_locally [Boolean] Skip the remote /flags call. + # @param send_feature_flag_events [Boolean] Whether to capture `$feature_flag_called` for this access. # - # @return [String, nil] The value of the feature flag + # @return [String, Boolean, nil] The value of the feature flag # # The provided properties are used to calculate feature flags locally, if possible. # @@ -420,6 +440,14 @@ def get_feature_flag( # @deprecated Use {#evaluate_flags} and {FeatureFlagEvaluations#get_flag} / # {FeatureFlagEvaluations#get_flag_payload} instead. + # @param key [String, Symbol] The unique key of the feature flag. + # @param distinct_id [String] The distinct id of the user. + # @param groups [Hash] Group analytics mapping from group type to group key. + # @param person_properties [Hash] Properties to use when evaluating the user locally or remotely. + # @param group_properties [Hash] Properties to use when evaluating groups locally or remotely. + # @param only_evaluate_locally [Boolean] Skip the remote /flags call. + # @param send_feature_flag_events [Boolean] Whether to capture `$feature_flag_called` for this access. + # @return [PostHog::FeatureFlagResult, nil] def get_feature_flag_result( key, distinct_id, @@ -456,7 +484,8 @@ def get_feature_flag_result( # @param [Hash] person_properties key-value pairs of properties to associate with the user # @param [Hash] group_properties # @param [Boolean] only_evaluate_locally Skip the remote /flags call entirely - # @param [Boolean] disable_geoip Stamped on captured access events + # @param [Boolean, nil] disable_geoip When true, disables GeoIP lookup for remote evaluation and stamps captured + # access events. # @param [Array] flag_keys 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 @@ -580,6 +609,7 @@ def evaluate_flags( # @param [Hash] groups # @param [Hash] person_properties key-value pairs of properties to associate with the user. # @param [Hash] group_properties + # @param only_evaluate_locally [Boolean] Skip the remote /flags call. # # @return [Hash] String (not symbol) key value pairs of flag and their values def get_all_flags( @@ -602,11 +632,12 @@ def get_all_flags( # # @param [String, Symbol] key The key of the feature flag # @param [String] distinct_id The distinct id of the user - # @option [String or boolean] match_value The value of the feature flag to be matched - # @option [Hash] groups - # @option [Hash] person_properties key-value pairs of properties to associate with the user. - # @option [Hash] group_properties - # @option [Boolean] only_evaluate_locally + # @param match_value [String, Boolean, nil] The value of the feature flag to be matched + # @param groups [Hash] + # @param person_properties [Hash] key-value pairs of properties to associate with the user. + # @param group_properties [Hash] + # @param only_evaluate_locally [Boolean] + # @return [Object, nil] The parsed payload for the matched flag value. # # @deprecated Use {#evaluate_flags} and {FeatureFlagEvaluations#get_flag_payload} instead. def get_feature_flag_payload( @@ -639,10 +670,10 @@ def get_feature_flag_payload( # featureFlagPayloads: A hash of feature flag payloads # # @param [String] distinct_id The distinct id of the user - # @option [Hash] groups - # @option [Hash] person_properties key-value pairs of properties to associate with the user. - # @option [Hash] group_properties - # @option [Boolean] only_evaluate_locally + # @param groups [Hash] + # @param person_properties [Hash] key-value pairs of properties to associate with the user. + # @param group_properties [Hash] + # @param only_evaluate_locally [Boolean] Skip the remote /flags call. # def get_all_flags_and_payloads( distinct_id, @@ -664,6 +695,9 @@ def get_all_flags_and_payloads( response end + # Reload locally cached feature flag definitions. + # + # @return [void] def reload_feature_flags unless @personal_api_key logger.error( @@ -674,6 +708,9 @@ def reload_feature_flags @feature_flags_poller.load_feature_flags(true) end + # Flush pending events and stop background resources. + # + # @return [void] def shutdown self.class._decrement_instance_count(@api_key) if @api_key @feature_flags_poller.shutdown_poller diff --git a/lib/posthog/exception_capture.rb b/lib/posthog/exception_capture.rb index 73976de..b9cd5d7 100644 --- a/lib/posthog/exception_capture.rb +++ b/lib/posthog/exception_capture.rb @@ -11,6 +11,9 @@ # 💖 open source (under MIT License) module PostHog + # Builds PostHog exception payloads from Ruby exception objects. + # + # @api private module ExceptionCapture RUBY_INPUT_FORMAT = / ^ \s* (?: [a-zA-Z]: | uri:classloader: )? ([^:]+ | <.*>): @@ -18,6 +21,8 @@ module ExceptionCapture (?: :in\s('|`)(?:([\w:]+)\#)?([^']+)')?$ /x + # @param value [Exception, String, Object] Exception input to parse. + # @return [Hash, nil] Parsed exception payload, or nil when the input is unsupported. def self.build_parsed_exception(value) title, message, backtrace = coerce_exception_input(value) return nil if title.nil? @@ -25,6 +30,10 @@ def self.build_parsed_exception(value) build_single_exception_from_data(title, message, backtrace) end + # @param title [String] + # @param message [String, nil] + # @param backtrace [Array, nil] + # @return [Hash] def self.build_single_exception_from_data(title, message, backtrace) { 'type' => title, @@ -37,6 +46,8 @@ def self.build_single_exception_from_data(title, message, backtrace) } end + # @param backtrace [Array, nil] + # @return [Hash, nil] def self.build_stacktrace(backtrace) return nil unless backtrace && !backtrace.empty? @@ -50,6 +61,8 @@ def self.build_stacktrace(backtrace) } end + # @param line [String] + # @return [Hash, nil] def self.parse_backtrace_line(line) match = line.match(RUBY_INPUT_FORMAT) return nil unless match @@ -72,6 +85,8 @@ def self.parse_backtrace_line(line) frame end + # @param path [String] + # @return [Boolean] def self.gem_path?(path) path.include?('/gems/') || path.include?('/ruby/') || @@ -79,6 +94,11 @@ def self.gem_path?(path) path.include?('/.rvm/') end + # @param frame [Hash] + # @param file_path [String] + # @param lineno [Integer] + # @param context_size [Integer] + # @return [void] def self.add_context_lines(frame, file_path, lineno, context_size = 5) lines = File.readlines(file_path) return if lines.empty? @@ -97,6 +117,8 @@ def self.add_context_lines(frame, file_path, lineno, context_size = 5) # Silently ignore file read errors end + # @param value [Exception, String, Object] + # @return [Array] Three-item array of title, message, and backtrace. def self.coerce_exception_input(value) if value.is_a?(String) title = 'Error' diff --git a/lib/posthog/feature_flag.rb b/lib/posthog/feature_flag.rb index af7bcdd..952158c 100644 --- a/lib/posthog/feature_flag.rb +++ b/lib/posthog/feature_flag.rb @@ -1,10 +1,13 @@ # frozen_string_literal: true -# Represents a feature flag returned by /flags v2 module PostHog + # Represents a feature flag returned by /flags v2. + # + # @api private class FeatureFlag attr_reader :key, :enabled, :variant, :reason, :metadata, :failed + # @param json [Hash] Raw feature flag data returned by /flags. def initialize(json) json.transform_keys!(&:to_s) @key = json['key'] @@ -15,15 +18,21 @@ def initialize(json) @failed = json['failed'] end + # @return [String, Boolean] The variant value when present, otherwise the enabled status. # TODO: Rename to `value` in future version def get_value # rubocop:disable Naming/AccessorMethodName @variant || @enabled end + # @return [Object, nil] The flag payload from metadata. def payload @metadata&.payload end + # @param key [String, Symbol] The feature flag key. + # @param value [String, Boolean] The feature flag value. + # @param payload [Object, nil] The feature flag payload. + # @return [PostHog::FeatureFlag] def self.from_value_and_payload(key, value, payload) new({ 'key' => key, @@ -40,10 +49,13 @@ def self.from_value_and_payload(key, value, payload) end end - # Represents the reason why a flag was enabled/disabled + # Represents the reason why a flag was enabled/disabled. + # + # @api private class EvaluationReason attr_reader :code, :description, :condition_index + # @param json [Hash] Raw reason data returned by /flags. def initialize(json) json.transform_keys!(&:to_s) @code = json['code'] @@ -52,10 +64,13 @@ def initialize(json) end end - # Represents metadata about a feature flag + # Represents metadata about a feature flag. + # + # @api private class FeatureFlagMetadata attr_reader :id, :version, :payload, :description + # @param json [Hash] Raw metadata returned by /flags. def initialize(json) json.transform_keys!(&:to_s) @id = json['id'] diff --git a/lib/posthog/feature_flag_error.rb b/lib/posthog/feature_flag_error.rb index 7a1319e..6a4421f 100644 --- a/lib/posthog/feature_flag_error.rb +++ b/lib/posthog/feature_flag_error.rb @@ -17,6 +17,7 @@ module PostHog # # For API errors with status codes, use the api_error() method which returns # a string like "api_error_500". + # @api private class FeatureFlagError ERRORS_WHILE_COMPUTING = 'errors_while_computing_flags' FLAG_MISSING = 'flag_missing' diff --git a/lib/posthog/feature_flag_evaluations.rb b/lib/posthog/feature_flag_evaluations.rb index 19d617f..e475677 100644 --- a/lib/posthog/feature_flag_evaluations.rb +++ b/lib/posthog/feature_flag_evaluations.rb @@ -4,7 +4,7 @@ module PostHog # A snapshot of feature flag evaluations for one distinct_id, returned by - # PostHog::Client#evaluate_flags. Calls to {#is_enabled} / {#get_flag} fire the + # PostHog::Client#evaluate_flags. Calls to {#enabled?} / {#get_flag} fire the # `$feature_flag_called` event (deduped through the existing per-distinct_id # cache); {#get_flag_payload} does not. Pass the snapshot to `capture(flags:)` # to attach `$feature/` and `$active_feature_flags` without a second @@ -19,8 +19,32 @@ class FeatureFlagEvaluations Host = Struct.new(:capture_flag_called_event_if_needed, :log_warning, keyword_init: true) - attr_reader :distinct_id, :groups, :request_id, :evaluated_at, :flag_definitions_loaded_at + # @return [String] The distinct id these evaluations belong to. + attr_reader :distinct_id + # @return [Hash, nil] Group analytics mapping used for evaluation. + attr_reader :groups + + # @return [String, nil] Request id returned by the /flags endpoint. + attr_reader :request_id + + # @return [String, nil] Evaluation timestamp returned by the /flags endpoint. + attr_reader :evaluated_at + + # @return [Time, nil] When local flag definitions were loaded. + attr_reader :flag_definitions_loaded_at + + # @param host [Host, nil] Internal host callbacks used to record flag access events. + # @param distinct_id [String, nil] The distinct id these evaluations belong to. + # @param flags [Hash] Evaluated flags keyed by flag key. + # @param groups [Hash, nil] Group analytics mapping from group type to group key. + # @param disable_geoip [Boolean, nil] Whether GeoIP was disabled during evaluation. + # @param request_id [String, nil] The request id returned by the /flags endpoint. + # @param evaluated_at [String, nil] The evaluation timestamp returned by the /flags endpoint. + # @param flag_definitions_loaded_at [Time, nil] When local flag definitions were loaded. + # @param errors_while_computing [Boolean] Whether the server reported errors while computing flags. + # @param quota_limited [Boolean] Whether feature flag evaluation was quota limited. + # @param accessed [Array, Set, nil] Flag keys already accessed by this snapshot. def initialize( host: nil, distinct_id: nil, @@ -47,10 +71,13 @@ def initialize( @accessed = Set.new(accessed || []) end + # @return [Array] The evaluated flag keys in this snapshot. def keys @flags.keys end + # @param key [String, Symbol] The feature flag key. + # @return [Boolean] true when the flag is enabled, false when disabled or missing. def enabled?(key) key = key.to_s flag = @flags[key] @@ -58,6 +85,9 @@ def enabled?(key) flag&.enabled ? true : false end + # @param key [String, Symbol] The feature flag key. + # @return [String, Boolean, nil] Variant string for multivariate flags, true/false for boolean flags, + # or nil when the flag was not returned by the evaluation. def get_flag(key) key = key.to_s flag = @flags[key] @@ -65,6 +95,8 @@ def get_flag(key) _flag_value(flag) end + # @param key [String, Symbol] The feature flag key. + # @return [Object, nil] The parsed payload for the flag, if any. def get_flag_payload(key) flag = @flags[key.to_s] flag&.payload @@ -73,10 +105,14 @@ def get_flag_payload(key) # Order-dependent: if nothing has been accessed yet, the returned snapshot is # empty. The method honors its name — pre-access flags before calling this if # you want a populated result. + # @return [PostHog::FeatureFlagEvaluations] A snapshot containing only flags already accessed with + # {#enabled?} or {#get_flag}. def only_accessed _clone_with(@flags.slice(*@accessed)) end + # @param keys [Array, String, Symbol] Flag keys to keep in the returned snapshot. + # @return [PostHog::FeatureFlagEvaluations] A snapshot containing only the requested keys that exist. def only(keys) keys = Array(keys).map(&:to_s) missing = keys.reject { |k| @flags.key?(k) } @@ -92,6 +128,9 @@ def only(keys) # Builds the `$feature/` and `$active_feature_flags` properties for a # captured event. Called from PostHog::Client#capture when `flags:` is set. + # + # @api private + # @return [Hash] def _get_event_properties properties = {} active = [] diff --git a/lib/posthog/feature_flag_result.rb b/lib/posthog/feature_flag_result.rb index 8185d65..b0ba451 100644 --- a/lib/posthog/feature_flag_result.rb +++ b/lib/posthog/feature_flag_result.rb @@ -6,8 +6,19 @@ module PostHog # Represents the result of a feature flag evaluation # containing both the flag value and payload class FeatureFlagResult - attr_reader :key, :variant, :payload + # @return [String, Symbol] The feature flag key. + attr_reader :key + # @return [String, nil] The variant key for multivariate flags. + attr_reader :variant + + # @return [Object, nil] The parsed feature flag payload. + attr_reader :payload + + # @param key [String, Symbol] The feature flag key. + # @param enabled [Boolean] Whether the feature flag is enabled. + # @param variant [String, nil] The variant key for multivariate flags. + # @param payload [Object, nil] The parsed feature flag payload. def initialize(key:, enabled:, variant: nil, payload: nil) @key = key @enabled = enabled @@ -15,18 +26,27 @@ def initialize(key:, enabled:, variant: nil, payload: nil) @payload = payload end - # Returns the effective value of the feature flag - # variant if present, otherwise enabled status + # Returns the effective value of the feature flag: variant if present, + # otherwise enabled status. + # + # @return [String, Boolean] def value @variant || @enabled end - # Returns whether or not the feature flag evaluated as enabled + # Returns whether or not the feature flag evaluated as enabled. + # + # @return [Boolean] def enabled? @enabled end - # Factory method to create from flag value and payload + # Factory method to create from flag value and payload. + # + # @param key [String, Symbol] The feature flag key. + # @param value [String, Boolean, nil] The raw feature flag value. + # @param payload [Object, String, nil] The raw or JSON-encoded feature flag payload. + # @return [PostHog::FeatureFlagResult, nil] def self.from_value_and_payload(key, value, payload) return nil if value.nil? @@ -43,6 +63,9 @@ def self.from_value_and_payload(key, value, payload) # returned when the body is not valid JSON); already-deserialized values # pass through. Public so {FeatureFlagEvaluations} can normalize payloads # the same way {FeatureFlagResult} does. + # + # @param payload [Object, String, nil] The raw payload value. + # @return [Object, nil] The parsed payload. def self.parse_payload(payload) return nil if payload.nil? return payload unless payload.is_a?(String) diff --git a/lib/posthog/feature_flags.rb b/lib/posthog/feature_flags.rb index db51e82..85e9750 100644 --- a/lib/posthog/feature_flags.rb +++ b/lib/posthog/feature_flags.rb @@ -20,10 +20,20 @@ class InconclusiveMatchError < StandardError class RequiresServerEvaluation < StandardError end + # Polls and evaluates feature flag definitions for {PostHog::Client}. + # + # @api private class FeatureFlagsPoller include PostHog::Logging include PostHog::Utils + # @param polling_interval [Integer, nil] Seconds between local feature flag definition polls. + # @param personal_api_key [String, nil] Personal API key used to fetch local evaluation definitions. + # @param project_api_key [String] Project API key. + # @param host [String] PostHog API host URL. + # @param feature_flag_request_timeout_seconds [Integer] Timeout for feature flag requests. + # @param on_error [Proc, nil] Callback invoked as `on_error.call(status, error)`. + # @param flag_definition_cache_provider [Object, nil] Optional {FlagDefinitionCacheProvider} implementation. def initialize( polling_interval, personal_api_key, diff --git a/lib/posthog/field_parser.rb b/lib/posthog/field_parser.rb index 5fc4398..6c00523 100644 --- a/lib/posthog/field_parser.rb +++ b/lib/posthog/field_parser.rb @@ -3,6 +3,9 @@ require 'posthog/logging' module PostHog + # Converts public SDK method arguments into PostHog API event payloads. + # + # @api private class FieldParser class << self include PostHog::Utils @@ -14,6 +17,8 @@ class << self # - "properties" # - "groups" # - "uuid" + # @param fields [Hash] + # @return [Hash] def parse_for_capture(fields) common = parse_common_fields(fields) @@ -45,6 +50,8 @@ def parse_for_capture(fields) # In addition to the common fields, identify accepts: # # - "properties" + # @param fields [Hash] + # @return [Hash] def parse_for_identify(fields) common = parse_common_fields(fields) @@ -63,6 +70,8 @@ def parse_for_identify(fields) ) end + # @param fields [Hash] + # @return [Hash] def parse_for_group_identify(fields) properties = fields[:properties] || {} group_type = fields[:group_type] @@ -92,6 +101,8 @@ def parse_for_group_identify(fields) # In addition to the common fields, alias accepts: # # - "alias" + # @param fields [Hash] + # @return [Hash] def parse_for_alias(fields) common = parse_common_fields(fields) diff --git a/lib/posthog/flag_definition_cache.rb b/lib/posthog/flag_definition_cache.rb index 975d99d..e7a14b5 100644 --- a/lib/posthog/flag_definition_cache.rb +++ b/lib/posthog/flag_definition_cache.rb @@ -14,26 +14,31 @@ module PostHog # # == Required Methods # - # [+flag_definitions+] + # @!method flag_definitions # Retrieve cached flag definitions. Return a Hash with +:flags+, # +:group_type_mapping+, and +:cohorts+ keys, or +nil+ if the cache # is empty. Returning +nil+ triggers an API fetch when no flags are # loaded yet (emergency fallback). + # @return [Hash, nil] # - # [+should_fetch_flag_definitions?+] + # @!method should_fetch_flag_definitions? # Return +true+ if this instance should fetch new definitions from the # API, +false+ to read from cache instead. Use for distributed lock # coordination so only one worker fetches at a time. + # @return [Boolean] # - # [+on_flag_definitions_received(data)+] + # @!method on_flag_definitions_received(data) # Called after successfully fetching new definitions from the API. # +data+ is a Hash with +:flags+, +:group_type_mapping+, and +:cohorts+ # keys (plain Ruby types, not Concurrent:: wrappers). Store it in your # external cache. + # @param data [Hash] + # @return [void] # - # [+shutdown+] + # @!method shutdown # Called when the PostHog client shuts down. Release any distributed # locks and clean up resources. + # @return [void] # # == Error Handling # @@ -66,6 +71,7 @@ module FlagDefinitionCacheProvider # # @param provider [Object] the cache provider to validate # @raise [ArgumentError] if any required methods are missing + # @return [void] def self.validate!(provider) missing = REQUIRED_METHODS.reject { |m| provider.respond_to?(m) } return if missing.empty? diff --git a/lib/posthog/logging.rb b/lib/posthog/logging.rb index acbfdca..05b85ee 100644 --- a/lib/posthog/logging.rb +++ b/lib/posthog/logging.rb @@ -3,8 +3,12 @@ require 'logger' module PostHog - # Wraps an existing logger and adds a prefix to all messages + # Wraps an existing logger and adds a prefix to all messages. + # + # @api private class PrefixedLogger + # @param logger [Logger, #debug, #info, #warn, #error] + # @param prefix [String] def initialize(logger, prefix) @logger = logger @prefix = prefix @@ -37,6 +41,7 @@ def level module Logging class << self + # @return [Logger, PostHog::PrefixedLogger] The logger used by the SDK. def logger return @logger if @logger @@ -52,6 +57,7 @@ def logger @logger = PrefixedLogger.new(base_logger, '[posthog-ruby]') end + # @param logger [Logger, #debug, #info, #warn, #error] Custom logger used by the SDK. attr_writer :logger end @@ -63,6 +69,7 @@ def logger end end + # @return [Logger, PostHog::PrefixedLogger] The logger used by the SDK. def logger Logging.logger end diff --git a/lib/posthog/message_batch.rb b/lib/posthog/message_batch.rb index b8e6b7c..0628ef8 100644 --- a/lib/posthog/message_batch.rb +++ b/lib/posthog/message_batch.rb @@ -4,7 +4,9 @@ require 'posthog/logging' module PostHog - # A batch of `Message`s to be sent to the API + # A batch of messages to be sent to the API. + # + # @api private class MessageBatch class JSONGenerationError < StandardError end @@ -13,12 +15,15 @@ class JSONGenerationError < StandardError include PostHog::Logging include PostHog::Defaults::MessageBatch + # @param max_message_count [Integer] Maximum number of messages in the batch. def initialize(max_message_count) @messages = [] @max_message_count = max_message_count @json_size = 0 end + # @param message [Hash] Message to add to the batch. + # @return [Array, nil] def <<(message) begin message_json = message.to_json @@ -35,10 +40,12 @@ def <<(message) end end + # @return [Boolean] Whether the batch is full. def full? item_count_exhausted? || size_exhausted? end + # @return [void] def clear @messages.clear @json_size = 0 diff --git a/lib/posthog/noop_worker.rb b/lib/posthog/noop_worker.rb index a30e347..10ce72e 100644 --- a/lib/posthog/noop_worker.rb +++ b/lib/posthog/noop_worker.rb @@ -1,21 +1,27 @@ # frozen_string_literal: true -# A worker that doesn't consume jobs module PostHog + # A worker that doesn't consume jobs. + # + # @api private class NoopWorker + # @param queue [Queue] def initialize(queue) @queue = queue end + # @return [void] def run # Does nothing end + # @return [Boolean] # TODO: Rename to `requesting?` in future version def is_requesting? # rubocop:disable Naming/PredicateName false end + # @return [void] def shutdown # Does nothing end diff --git a/lib/posthog/response.rb b/lib/posthog/response.rb index 2864956..076b496 100644 --- a/lib/posthog/response.rb +++ b/lib/posthog/response.rb @@ -1,12 +1,14 @@ # frozen_string_literal: true module PostHog + # API response wrapper returned by the SDK transport. + # + # @api private class Response attr_reader :status, :error - # public: Simple class to wrap responses from the API - # - # + # @param status [Integer] HTTP status code, or -1 for SDK/transport errors. + # @param error [String, nil] Error message returned by the API or SDK. def initialize(status = 200, error = nil) @status = status @error = error diff --git a/lib/posthog/send_feature_flags_options.rb b/lib/posthog/send_feature_flags_options.rb index 04129f2..c94d37f 100644 --- a/lib/posthog/send_feature_flags_options.rb +++ b/lib/posthog/send_feature_flags_options.rb @@ -3,16 +3,29 @@ require 'posthog/utils' module PostHog - # Options for configuring feature flag behavior in capture calls + # Options for configuring deprecated feature flag behavior in capture calls. + # + # @deprecated Prefer passing a {PostHog::FeatureFlagEvaluations} snapshot to `capture(flags:)`. class SendFeatureFlagsOptions - attr_reader :only_evaluate_locally, :person_properties, :group_properties + # @return [Boolean, nil] Whether remote feature flag evaluation should be skipped. + attr_reader :only_evaluate_locally + # @return [Hash] Person properties to use for feature flag evaluation. + attr_reader :person_properties + + # @return [Hash] Group properties to use for feature flag evaluation. + attr_reader :group_properties + + # @param only_evaluate_locally [Boolean, nil] Skip remote feature flag evaluation. + # @param person_properties [Hash, nil] Person properties to use for feature flag evaluation. + # @param group_properties [Hash, nil] Group properties to use for feature flag evaluation. def initialize(only_evaluate_locally: nil, person_properties: nil, group_properties: nil) @only_evaluate_locally = only_evaluate_locally @person_properties = person_properties || {} @group_properties = group_properties || {} end + # @return [Hash] A hash representation suitable for `capture(send_feature_flags:)`. def to_h { only_evaluate_locally: @only_evaluate_locally, @@ -21,6 +34,8 @@ def to_h } end + # @param hash [Hash] + # @return [PostHog::SendFeatureFlagsOptions, nil] def self.from_hash(hash) return nil unless hash.is_a?(Hash) diff --git a/lib/posthog/send_worker.rb b/lib/posthog/send_worker.rb index 794559d..ac44a4d 100644 --- a/lib/posthog/send_worker.rb +++ b/lib/posthog/send_worker.rb @@ -6,6 +6,9 @@ require 'posthog/utils' module PostHog + # Background worker that batches and sends queued events. + # + # @api private class SendWorker include PostHog::Utils include PostHog::Defaults @@ -16,12 +19,13 @@ class SendWorker # The worker continuously takes messages off the queue # and makes requests to the posthog.com api # - # queue - Queue synchronized between client and worker - # api_key - String of the project's API key - # options - Hash of worker options - # batch_size - Fixnum of how many items to send in a batch - # on_error - Proc of what to do on an error - # + # @param queue [Queue] Queue synchronized between client and worker. + # @param api_key [String] Project API key. + # @param options [Hash] Worker options. + # @option options [Integer] :batch_size How many items to send in a batch. + # @option options [Proc] :on_error Callback invoked as `on_error.call(status, error)`. + # @option options [String] :host PostHog API host URL. + # @option options [Boolean] :skip_ssl_verification Disable SSL certificate verification. def initialize(queue, api_key, options = {}) symbolize_keys! options @queue = queue @@ -33,8 +37,9 @@ def initialize(queue, api_key, options = {}) @transport = Transport.new api_host: options[:host], skip_ssl_verification: options[:skip_ssl_verification] end - # public: Continuously runs the loop to check for new events + # Continuously runs the loop to check for new events. # + # @return [void] def run until Thread.current[:should_exit] return if @queue.empty? @@ -54,12 +59,14 @@ def run @transport.shutdown end + # @return [void] def shutdown @transport.shutdown end # public: Check whether we have outstanding requests. # + # @return [Boolean] Whether the worker has outstanding requests. # TODO: Rename to `requesting?` in future version def is_requesting? # rubocop:disable Naming/PredicateName @lock.synchronize { !@batch.empty? } diff --git a/lib/posthog/transport.rb b/lib/posthog/transport.rb index 77ea1ec..ab0872f 100644 --- a/lib/posthog/transport.rb +++ b/lib/posthog/transport.rb @@ -10,11 +10,24 @@ require 'json' module PostHog + # HTTP transport used by the SDK workers. + # + # @api private class Transport include PostHog::Defaults::Request include PostHog::Utils include PostHog::Logging + # @param options [Hash] Transport configuration. + # @option options [String] :api_host Full PostHog API host URL. + # @option options [String] :host Hostname to connect to. + # @option options [Integer] :port Port to connect to. + # @option options [Boolean] :ssl Whether to use HTTPS. + # @option options [Hash] :headers HTTP headers for batch requests. + # @option options [String] :path HTTP path for batch requests. + # @option options [Integer] :retries Number of retry attempts for retryable failures. + # @option options [PostHog::BackoffPolicy] :backoff_policy Backoff policy used between retries. + # @option options [Boolean] :skip_ssl_verification Disable SSL certificate verification. def initialize(options = {}) if options[:api_host] uri = URI.parse(options[:api_host]) @@ -43,6 +56,8 @@ def initialize(options = {}) # Sends a batch of messages to the API # + # @param api_key [String] Project API key. + # @param batch [PostHog::MessageBatch, Array] Batch of messages to send. # @return [Response] API response def send(api_key, batch) logger.debug("Sending request for #{batch.length} items") @@ -72,7 +87,9 @@ def send(api_key, batch) end end - # Closes a persistent connection if it exists + # Closes a persistent connection if it exists. + # + # @return [void] def shutdown @http.finish if @http.started? end diff --git a/lib/posthog/utils.rb b/lib/posthog/utils.rb index 58ee8ff..633a4a5 100644 --- a/lib/posthog/utils.rb +++ b/lib/posthog/utils.rb @@ -6,6 +6,9 @@ module PostHog class InconclusiveMatchError < StandardError end + # Utility helpers used internally by the SDK. + # + # @api private module Utils module_function @@ -145,12 +148,19 @@ def get_by_symbol_or_string_key(hash, key) end end + # Hash that clears itself when it reaches a maximum length. + # + # @api private class SizeLimitedHash < Hash + # @param max_length [Integer] def initialize(max_length, ...) super(...) @max_length = max_length end + # @param key [Object] + # @param value [Object] + # @return [Object] def []=(key, value) clear if length >= @max_length super diff --git a/posthog-rails/README.md b/posthog-rails/README.md index 6d19268..bbfe5dd 100644 --- a/posthog-rails/README.md +++ b/posthog-rails/README.md @@ -1,458 +1,9 @@ # PostHog Rails -Official PostHog integration for Ruby on Rails applications. Automatically track exceptions, instrument background jobs, and capture user analytics. +Official PostHog integration for Ruby on Rails applications. -## Features +For installation, configuration, usage, and troubleshooting, see the official documentation: -- 🚨 **Automatic exception tracking** - Captures unhandled and rescued exceptions -- 🔄 **ActiveJob instrumentation** - Tracks background job exceptions -- 👤 **User context** - Automatically associates exceptions with the current user -- 🎯 **Smart filtering** - Excludes common Rails exceptions (404s, etc.) by default -- 📊 **Rails 7.0+ error reporter** - Integrates with Rails' built-in error reporting -- ⚙️ **Highly configurable** - Customize what gets tracked +https://posthog.com/docs/libraries/ruby-on-rails -## Installation - -Add to your Gemfile: - -```ruby -gem 'posthog-ruby' -gem 'posthog-rails' -``` - -Then run: - -```bash -bundle install -``` - -**Note:** `posthog-rails` depends on `posthog-ruby`, but it's recommended to explicitly include both gems in your Gemfile for clarity. - -### Generate the Initializer - -Run the install generator to create the PostHog initializer: - -```bash -rails generate posthog:install -``` - -This will create `config/initializers/posthog.rb` with sensible defaults and documentation. - -## Configuration - -PostHog.init creates a single client instance used across your app. Avoid creating multiple `PostHog::Client` instances with the same API key — it can cause dropped events. - -The generated initializer at `config/initializers/posthog.rb` includes all available options: - -```ruby -# Rails-specific configuration -PostHog::Rails.configure do |config| - config.auto_capture_exceptions = true # Enable automatic exception capture (default: false) - config.report_rescued_exceptions = true # Report exceptions Rails rescues (default: false) - config.auto_instrument_active_job = true # Instrument background jobs (default: false) - config.use_tracing_headers = true # Use PostHog tracing headers for identity/session context (default: true) - config.capture_user_context = true # Include authenticated user info in exceptions - config.current_user_method = :current_user # Method to get current user - config.user_id_method = nil # Method to get ID from user (auto-detect) - - # Add additional exceptions to ignore - config.excluded_exceptions = ['MyCustomError'] -end - -# Core PostHog client initialization -PostHog.init do |config| - # Required: Your PostHog API key - config.api_key = ENV['POSTHOG_API_KEY'] - - # Optional: Your PostHog instance URL - config.host = 'https://us.i.posthog.com' # or https://eu.i.posthog.com - - # Optional: Personal API key for feature flags - config.personal_api_key = ENV['POSTHOG_PERSONAL_API_KEY'] - - # Error callback - config.on_error = proc { |status, msg| - Rails.logger.error("PostHog error: #{msg}") - } -end -``` - -You can also configure Rails options directly: - -```ruby -PostHog::Rails.config.auto_capture_exceptions = true -``` - -### Environment Variables - -The recommended approach is to use environment variables: - -```bash -# .env -POSTHOG_API_KEY=your_project_api_key -POSTHOG_PERSONAL_API_KEY=your_personal_api_key # Optional, for feature flags -``` - -## Usage - -### Automatic Exception Tracking - -When `auto_capture_exceptions` is enabled, exceptions are automatically captured: - -```ruby -class PostsController < ApplicationController - def show - @post = Post.find(params[:id]) - # Any exception here is automatically captured - end -end -``` - -### Manual Event Tracking - -Track custom events anywhere in your Rails app: - -```ruby -# Track an event -PostHog.capture( - distinct_id: current_user.id, - event: 'post_created', - properties: { title: @post.title } -) - -# Identify a user -PostHog.identify( - distinct_id: current_user.id, - properties: { - email: current_user.email, - plan: current_user.plan - } -) - -# Track an exception manually -PostHog.capture_exception( - exception, - current_user.id, - { custom_property: 'value' } -) -``` - -### Background Jobs - -When `auto_instrument_active_job` is enabled, ActiveJob exceptions are automatically captured: - -```ruby -class EmailJob < ApplicationJob - def perform(user_id) - user = User.find(user_id) - UserMailer.welcome(user).deliver_now - # Exceptions are automatically captured with job context - end -end -``` - -#### Associating Jobs with Users - -By default, PostHog tries to extract a `distinct_id` from job arguments by looking for a `user_id` key in hash arguments: - -```ruby -class ProcessOrderJob < ApplicationJob - def perform(order_id, options = {}) - # PostHog will automatically use options[:user_id] or options['user_id'] - # as the distinct_id if present - end -end - -# Call with user context -ProcessOrderJob.perform_later(order.id, user_id: current_user.id) -``` - -#### Custom Distinct ID Extraction - -For more control, use the `posthog_distinct_id` class method to define exactly how to extract the user's distinct ID from your job arguments: - -```ruby -class SendWelcomeEmailJob < ApplicationJob - posthog_distinct_id ->(user, options) { user.id } - - def perform(user, options = {}) - UserMailer.welcome(user).deliver_now - end -end -``` - -You can also use a block: - -```ruby -class ProcessOrderJob < ApplicationJob - posthog_distinct_id do |order, notify_user_id| - notify_user_id - end - - def perform(order, notify_user_id) - # Process the order... - end -end -``` - -The proc/block receives the same arguments as `perform`, so you can extract the distinct ID however makes sense for your job. - -> **Note:** Currently only ActiveJob is supported. Support for other job runners (Sidekiq, Resque, Good Job, etc.) is planned for future releases. Contributions are welcome! - -### Feature Flags - -Use feature flags in your Rails app: - -```ruby -class PostsController < ApplicationController - def show - if PostHog.is_feature_enabled('new-post-design', current_user.id) - render 'posts/show_new' - else - render 'posts/show' - end - end -end -``` - -### Rails 7.0+ Error Reporter - -PostHog integrates with Rails' built-in error reporting: - -```ruby -# These errors are automatically sent to PostHog -Rails.error.handle do - # Code that might raise an error -end - -Rails.error.record(exception, context: { user_id: current_user.id }) -``` - -PostHog will automatically extract the user's distinct ID from either `user_id` or `distinct_id` in the context hash (checking `user_id` first). Any other context keys are included as properties on the exception event. - -## Configuration Options - -### Core PostHog Options - -| Option | Type | Default | Description | -|--------|------|---------|-------------| -| `api_key` | String | **required** | Your PostHog project API key | -| `host` | String | `https://us.i.posthog.com` | PostHog instance URL | -| `personal_api_key` | String | `nil` | For feature flag evaluation | -| `max_queue_size` | Integer | `10000` | Max events to queue | -| `test_mode` | Boolean | `false` | Don't send events (for testing) | -| `on_error` | Proc | `nil` | Error callback | -| `feature_flags_polling_interval` | Integer | `30` | Seconds between flag polls | - -### Rails-Specific Options - -Configure these via `PostHog::Rails.configure` or `PostHog::Rails.config`: - -| Option | Type | Default | Description | -|--------|------|---------|-------------| -| `auto_capture_exceptions` | Boolean | `false` | Automatically capture exceptions | -| `report_rescued_exceptions` | Boolean | `false` | Report exceptions Rails rescues | -| `auto_instrument_active_job` | Boolean | `false` | Instrument ActiveJob | -| `use_tracing_headers` | Boolean | `true` | Use PostHog tracing headers as request-scoped default `distinct_id` and `$session_id` values | -| `capture_user_context` | Boolean | `true` | Include authenticated user info in exceptions | -| `current_user_method` | Symbol | `:current_user` | Controller method for user | -| `user_id_method` | Symbol | `nil` | Method to extract ID from user object (auto-detect if nil) | -| `excluded_exceptions` | Array | `[]` | Additional exceptions to ignore | - -### Understanding Exception Tracking Options - -**`auto_capture_exceptions`** - Master switch for all automatic error tracking (default: `false`) -- When `true`: All exceptions are automatically captured and sent to PostHog -- When `false` (default): No automatic error tracking (you must manually call `PostHog.capture_exception`) -- **Use case:** Enable to get automatic error tracking - -**`report_rescued_exceptions`** - Control exceptions that Rails handles gracefully (default: `false`) -- When `true`: Capture exceptions that Rails rescues and shows error pages for (404s, 500s, etc.) -- When `false` (default): Only capture truly unhandled exceptions that crash your app -- **Use case:** Enable along with `auto_capture_exceptions` for complete error visibility - -**Example:** - -```ruby -# Scenario: User visits /posts/999999 (post doesn't exist) -def show - @post = Post.find(params[:id]) # Raises ActiveRecord::RecordNotFound -end -``` - -| Configuration | Result | -|---------------|--------| -| `auto_capture_exceptions = true`
`report_rescued_exceptions = true` | ✅ Exception captured | -| `auto_capture_exceptions = true`
`report_rescued_exceptions = false` | ❌ Not captured (Rails rescued it) | -| `auto_capture_exceptions = false` | ❌ Not captured (automatic tracking disabled, this is the default) | - -**Recommendation:** Enable both options to get complete visibility into all errors. Set `report_rescued_exceptions = false` if you only want to track critical crashes. - -## Excluded Exceptions by Default - -The following exceptions are not reported by default (common 4xx errors): - -- `AbstractController::ActionNotFound` -- `ActionController::BadRequest` -- `ActionController::InvalidAuthenticityToken` -- `ActionController::InvalidCrossOriginRequest` -- `ActionController::MethodNotAllowed` -- `ActionController::NotImplemented` -- `ActionController::ParameterMissing` -- `ActionController::RoutingError` -- `ActionController::UnknownFormat` -- `ActionController::UnknownHttpMethod` -- `ActionDispatch::Http::Parameters::ParseError` -- `ActiveRecord::RecordNotFound` -- `ActiveRecord::RecordNotUnique` - -You can add more with `PostHog::Rails.config.excluded_exceptions = ['MyException']`. - -## Request Context - -PostHog Rails automatically applies request-scoped context to events captured during web requests. Request metadata such as `$current_url`, `$request_method`, `$request_path`, `$user_agent`, and `$ip` is added to event properties. When present, PostHog tracing headers (`X-PostHog-Distinct-Id` and `X-PostHog-Session-Id`) are also used as default `distinct_id` and `$session_id` values. Explicit `distinct_id` and properties passed to `PostHog.capture` always take precedence. - -Disable tracing header identity/session capture if you do not want client-supplied PostHog tracing headers used for server-side events. Request metadata is still captured: - -```ruby -PostHog::Rails.config.use_tracing_headers = false -``` - -## User Context - -PostHog Rails automatically captures authenticated user information from your controllers for exceptions. Authenticated Rails user context takes precedence over client-supplied tracing headers for exception identity: - -```ruby -class ApplicationController < ActionController::Base - # PostHog will automatically call this method - def current_user - @current_user ||= User.find_by(id: session[:user_id]) - end -end -``` - -If your user method has a different name, configure it: - -```ruby -PostHog::Rails.config.current_user_method = :logged_in_user -``` - -### User ID Extraction - -By default, PostHog Rails auto-detects the user's distinct ID by trying these methods in order: - -1. `posthog_distinct_id` - Define this on your User model for full control -2. `distinct_id` - Common analytics convention -3. `id` - Standard ActiveRecord primary key -4. `pk` - Primary key alias -5. `uuid` - For UUID-based primary keys - -**Option 1: Configure a specific method** - -```ruby -# config/initializers/posthog.rb -PostHog::Rails.config.user_id_method = :email # or :external_id, :customer_id, etc. -``` - -**Option 2: Define a method on your User model** - -```ruby -class User < ApplicationRecord - def posthog_distinct_id - # Custom logic for your distinct ID - "user_#{id}" # or external_id, or any unique identifier - end -end -``` - -This approach is useful when you want to: -- Use a different identifier than the database ID (e.g., `external_id`) -- Prefix IDs to distinguish user types -- Use composite identifiers - -## Sensitive Data Filtering - -PostHog Rails automatically filters sensitive parameters: - -- `password` -- `password_confirmation` -- `token` -- `secret` -- `api_key` -- `authenticity_token` - -Long parameter values are also truncated to 10,000 characters. - -## Testing - -In your test environment, you can disable PostHog or use test mode: - -```ruby -# config/environments/test.rb -PostHog.init do |config| - config.test_mode = true # Events are queued but not sent -end -``` - -Or in your tests: - -```ruby -# spec/rails_helper.rb -RSpec.configure do |config| - config.before(:each) do - allow(PostHog).to receive(:capture) - end -end -``` - -## Contributing - -See [CONTRIBUTING.md](CONTRIBUTING.md) for package-specific development instructions. - -## Architecture - -PostHog Rails uses the following components: - -- **Railtie** - Hooks into Rails initialization -- **Middleware** - Three middleware components provide request context and capture exceptions: - - `RequestContext` - Applies request metadata and optional PostHog tracing header identity/session context during Rails requests - - `RescuedExceptionInterceptor` - Catches rescued exceptions - - `CaptureExceptions` - Reports all exceptions to PostHog -- **ActiveJob** - Prepends exception handling to `perform_now` -- **Error Subscriber** - Integrates with Rails 7.0+ error reporter - -## Troubleshooting - -### Exceptions not being captured - -1. Verify PostHog is initialized: - ```ruby - Rails.console - > PostHog.initialized? - => true - ``` - -2. Check your excluded exceptions list -3. Verify middleware is installed: - ```ruby - Rails.application.middleware - ``` - -### User context not working - -1. Verify `current_user_method` matches your controller method -2. Check that the user object responds to one of: `posthog_distinct_id`, `distinct_id`, `id`, `pk`, or `uuid` -3. If using a custom identifier, set `PostHog::Rails.config.user_id_method = :your_method` -4. Enable logging to see what's being captured - -### Feature flags not working - -Ensure you've set `personal_api_key`: - -```ruby -config.personal_api_key = ENV['POSTHOG_PERSONAL_API_KEY'] -``` - -## License - -MIT License. See [LICENSE](../LICENSE) for details. +Keeping usage docs in one place avoids stale examples in this repository. diff --git a/posthog-rails/lib/posthog/rails.rb b/posthog-rails/lib/posthog/rails.rb index 5e6d124..24c1727 100644 --- a/posthog-rails/lib/posthog/rails.rb +++ b/posthog-rails/lib/posthog/rails.rb @@ -18,29 +18,41 @@ module Rails IN_WEB_REQUEST_KEY = :posthog_in_web_request class << self + # @return [PostHog::Rails::Configuration] Rails integration configuration. def config @config ||= Configuration.new end + # @param config [PostHog::Rails::Configuration] Rails integration configuration. attr_writer :config + # Configure Rails integration options. + # + # @yieldparam config [PostHog::Rails::Configuration] + # @return [void] def configure yield config if block_given? end # Mark that we're in a web request context # CaptureExceptions middleware will handle exception capture + # @api private + # @return [void] def enter_web_request Thread.current[IN_WEB_REQUEST_KEY] = true end # Clear web request context (called at end of request) + # @api private + # @return [void] def exit_web_request Thread.current[IN_WEB_REQUEST_KEY] = false end # Check if we're currently in a web request context # Used by ErrorSubscriber to avoid duplicate captures + # @api private + # @return [Boolean] def in_web_request? Thread.current[IN_WEB_REQUEST_KEY] == true end diff --git a/posthog-rails/lib/posthog/rails/active_job.rb b/posthog-rails/lib/posthog/rails/active_job.rb index c4b5b41..55372c7 100644 --- a/posthog-rails/lib/posthog/rails/active_job.rb +++ b/posthog-rails/lib/posthog/rails/active_job.rb @@ -13,7 +13,12 @@ def self.prepended(base) end module ClassMethods - # DSL for defining how to extract distinct_id from job arguments + # DSL for defining how to extract distinct_id from job arguments. + # + # @param proc [Proc, nil] Callable that receives the job's perform arguments. + # @yield The block receives the job's perform arguments. + # @return [Proc, nil] The configured extractor. + # # Example: # class MyJob < ApplicationJob # posthog_distinct_id ->(user, arg1, arg2) { user.id } @@ -25,6 +30,7 @@ def posthog_distinct_id(proc = nil, &block) @posthog_distinct_id_proc = proc || block end + # @return [Proc, nil] The configured distinct_id extractor. def posthog_distinct_id_proc @posthog_distinct_id_proc end diff --git a/posthog-rails/lib/posthog/rails/capture_exceptions.rb b/posthog-rails/lib/posthog/rails/capture_exceptions.rb index c8c2801..928507d 100644 --- a/posthog-rails/lib/posthog/rails/capture_exceptions.rb +++ b/posthog-rails/lib/posthog/rails/capture_exceptions.rb @@ -4,14 +4,19 @@ module PostHog module Rails - # Middleware that captures exceptions and sends them to PostHog + # Middleware that captures exceptions and sends them to PostHog. + # + # @api private class CaptureExceptions include ParameterFilter + # @param app [#call] Rack application. def initialize(app) @app = app end + # @param env [Hash] Rack environment. + # @return [Array] Rack response. def call(env) # Signal that we're in a web request context # ErrorSubscriber will skip capture for web requests to avoid duplicates diff --git a/posthog-rails/lib/posthog/rails/configuration.rb b/posthog-rails/lib/posthog/rails/configuration.rb index 169b9f8..e755ebe 100644 --- a/posthog-rails/lib/posthog/rails/configuration.rb +++ b/posthog-rails/lib/posthog/rails/configuration.rb @@ -3,31 +3,33 @@ module PostHog module Rails class Configuration - # Whether to automatically capture exceptions from Rails + # @return [Boolean] Whether to automatically capture exceptions from Rails. Defaults to false. attr_accessor :auto_capture_exceptions - # Whether to capture exceptions that Rails rescues (e.g., with rescue_from) + # @return [Boolean] Whether to capture exceptions that Rails rescues (e.g., with rescue_from). Defaults to false. attr_accessor :report_rescued_exceptions - # Whether to automatically instrument ActiveJob + # @return [Boolean] Whether to automatically instrument ActiveJob. Defaults to false. attr_accessor :auto_instrument_active_job - # List of exception classes to ignore (in addition to default) + # @return [Array] Exception class names to ignore in addition to the defaults. attr_accessor :excluded_exceptions - # Whether to use PostHog tracing headers for request-scoped identity/session context + # @return [Boolean] Whether to use PostHog tracing headers for request-scoped identity/session context. + # Defaults to true. attr_accessor :use_tracing_headers - # Whether to capture the current user context in exceptions + # @return [Boolean] Whether to capture the current user context in exceptions. Defaults to true. attr_accessor :capture_user_context - # Method name to call on controller to get user ID (default: :current_user) + # @return [Symbol] Method name to call on controller to get the current user. Defaults to :current_user. attr_accessor :current_user_method - # Method name to call on user object to get distinct_id (default: auto-detect) - # When nil, tries: posthog_distinct_id, distinct_id, id, pk, uuid in order + # @return [Symbol, nil] Method name to call on the user object to get distinct_id. When nil, tries: + # posthog_distinct_id, distinct_id, id, pk, uuid in order. attr_accessor :user_id_method + # @return [PostHog::Rails::Configuration] def initialize @auto_capture_exceptions = false @report_rescued_exceptions = false @@ -39,7 +41,9 @@ def initialize @user_id_method = nil end - # Default exceptions that Rails apps typically don't want to track + # Default exceptions that Rails apps typically don't want to track. + # + # @return [Array] def default_excluded_exceptions [ 'AbstractController::ActionNotFound', @@ -58,6 +62,8 @@ def default_excluded_exceptions ] end + # @param exception [Exception] The exception to check. + # @return [Boolean] Whether the exception should be captured. def should_capture_exception?(exception) exception_name = exception.class.name !all_excluded_exceptions.include?(exception_name) diff --git a/posthog-rails/lib/posthog/rails/error_subscriber.rb b/posthog-rails/lib/posthog/rails/error_subscriber.rb index 0d163c7..dc7c95c 100644 --- a/posthog-rails/lib/posthog/rails/error_subscriber.rb +++ b/posthog-rails/lib/posthog/rails/error_subscriber.rb @@ -4,11 +4,19 @@ module PostHog module Rails - # Rails 7.0+ error reporter integration - # This integrates with Rails.error.handle and Rails.error.record + # Rails 7.0+ error reporter integration. + # This integrates with Rails.error.handle and Rails.error.record. + # + # @api private class ErrorSubscriber include ParameterFilter + # @param error [Exception] Error reported by Rails. + # @param handled [Boolean] + # @param severity [Symbol, String] + # @param context [Hash] + # @param source [String, nil] + # @return [void] def report(error, handled:, severity:, context:, source: nil) return unless PostHog::Rails.config&.auto_capture_exceptions return unless PostHog::Rails.config&.should_capture_exception?(error) diff --git a/posthog-rails/lib/posthog/rails/parameter_filter.rb b/posthog-rails/lib/posthog/rails/parameter_filter.rb index 6ae197f..3551363 100644 --- a/posthog-rails/lib/posthog/rails/parameter_filter.rb +++ b/posthog-rails/lib/posthog/rails/parameter_filter.rb @@ -9,6 +9,8 @@ module Rails # It automatically detects the correct Rails parameter filtering API based on # the Rails version. # + # @api private + # # @example Usage in a class # class MyClass # include PostHog::Rails::ParameterFilter @@ -24,10 +26,12 @@ module ParameterFilter MAX_DEPTH = 10 if ::Rails.version.to_f >= 6.0 + # @return [Class] Rails parameter filter backend. def self.backend ActiveSupport::ParameterFilter end else + # @return [Class] Rails parameter filter backend. def self.backend ActionDispatch::Http::ParameterFilter end diff --git a/posthog-rails/lib/posthog/rails/railtie.rb b/posthog-rails/lib/posthog/rails/railtie.rb index 63c5e18..ef2d0d2 100644 --- a/posthog-rails/lib/posthog/rails/railtie.rb +++ b/posthog-rails/lib/posthog/rails/railtie.rb @@ -21,7 +21,11 @@ class << self get_all_flags ].freeze - # Initialize PostHog client with a block configuration + # Initialize the singleton PostHog client used by Rails delegators. + # + # @param options [Hash] Core {PostHog::Client} options. + # @yieldparam config [PostHog::Rails::InitConfig] Block-based core SDK configuration. + # @return [PostHog::Client] def init(options = {}) # If block given, yield to configuration if block_given? @@ -42,11 +46,14 @@ def init(options = {}) end end + # @return [Boolean] Whether {PostHog.init} has created a client. def initialized? !@client.nil? end - # Fallback for any client methods not explicitly defined + # Fallback for any client methods not explicitly defined. + # + # @api private # rubocop:disable Lint/RedundantSafeNavigation def method_missing(method_name, ...) if client&.respond_to?(method_name) @@ -57,6 +64,7 @@ def method_missing(method_name, ...) end end + # @api private def respond_to_missing?(method_name, include_private = false) client&.respond_to?(method_name) || super end @@ -118,6 +126,8 @@ def ensure_initialized! at_exit { PostHog.client&.shutdown if PostHog.initialized? } end + # @api private + # @return [void] def insert_middleware_after(app, target, middleware) # During initialization, app.config.middleware is a MiddlewareStackProxy # which only supports recording operations (insert_after, use, etc.) @@ -125,6 +135,8 @@ def insert_middleware_after(app, target, middleware) app.config.middleware.insert_after(target, middleware) end + # @api private + # @return [void] def insert_middleware_before(app, target, middleware) # During initialization, app.config.middleware is a MiddlewareStackProxy # which only supports recording operations (insert_before, use, etc.) @@ -132,6 +144,8 @@ def insert_middleware_before(app, target, middleware) app.config.middleware.insert_before(target, middleware) end + # @api private + # @return [void] def self.register_error_subscriber return unless PostHog::Rails.config&.auto_capture_exceptions @@ -142,6 +156,8 @@ def self.register_error_subscriber PostHog::Logging.logger.warn("Backtrace: #{e.backtrace&.first(5)&.join("\n")}") end + # @api private + # @return [Boolean] def self.rails_version_above_7? ::Rails.version.to_f >= 7.0 end @@ -149,51 +165,74 @@ def self.rails_version_above_7? # Configuration wrapper for the init block class InitConfig + # @param base_options [Hash] Initial core SDK options. def initialize(base_options = {}) @base_options = base_options end # Core PostHog options + # + # @param value [String] + # @return [String] def api_key=(value) @base_options[:api_key] = value end + # @param value [String, nil] + # @return [String, nil] def personal_api_key=(value) @base_options[:personal_api_key] = value end + # @param value [String] + # @return [String] def host=(value) @base_options[:host] = value end + # @param value [Integer] + # @return [Integer] def max_queue_size=(value) @base_options[:max_queue_size] = value end + # @param value [Boolean] + # @return [Boolean] def test_mode=(value) @base_options[:test_mode] = value end + # @param value [Boolean] + # @return [Boolean] def sync_mode=(value) @base_options[:sync_mode] = value end + # @param value [Proc] + # @return [Proc] def on_error=(value) @base_options[:on_error] = value end + # @param value [Integer] + # @return [Integer] def feature_flags_polling_interval=(value) @base_options[:feature_flags_polling_interval] = value end + # @param value [Integer] + # @return [Integer] def feature_flag_request_timeout_seconds=(value) @base_options[:feature_flag_request_timeout_seconds] = value end + # @param value [Proc] + # @return [Proc] def before_send=(value) @base_options[:before_send] = value end + # @return [Hash] Core SDK options suitable for {PostHog::Client.new}. def to_client_options @base_options end diff --git a/posthog-rails/lib/posthog/rails/request_context.rb b/posthog-rails/lib/posthog/rails/request_context.rb index c951a71..d94fec2 100644 --- a/posthog-rails/lib/posthog/rails/request_context.rb +++ b/posthog-rails/lib/posthog/rails/request_context.rb @@ -7,11 +7,16 @@ module PostHog module Rails # Rack middleware that creates a request-local PostHog context from tracing headers. + # + # @api private class RequestContext + # @param app [#call] Rack application. def initialize(app) @app = app end + # @param env [Hash] Rack environment. + # @return [Array] Rack response. def call(env) request = build_request(env) diff --git a/posthog-rails/lib/posthog/rails/request_metadata.rb b/posthog-rails/lib/posthog/rails/request_metadata.rb index dcaa106..7533a96 100644 --- a/posthog-rails/lib/posthog/rails/request_metadata.rb +++ b/posthog-rails/lib/posthog/rails/request_metadata.rb @@ -5,9 +5,13 @@ module PostHog module Rails # Internal helpers for extracting request metadata owned by RequestContext. + # + # @api private module RequestMetadata module_function + # @param request [Object] Rack or Rails request object. + # @return [Hash] Event properties extracted from the request. def extract(request) properties = {} add_property(properties, '$current_url', current_url(request)) diff --git a/posthog-rails/lib/posthog/rails/rescued_exception_interceptor.rb b/posthog-rails/lib/posthog/rails/rescued_exception_interceptor.rb index 3658a69..ddc9d7d 100644 --- a/posthog-rails/lib/posthog/rails/rescued_exception_interceptor.rb +++ b/posthog-rails/lib/posthog/rails/rescued_exception_interceptor.rb @@ -2,14 +2,19 @@ module PostHog module Rails - # Middleware that intercepts exceptions that are rescued by Rails + # Middleware that intercepts exceptions that are rescued by Rails. # This middleware runs before ShowExceptions and captures the exception - # so we can report it even if Rails rescues it + # so we can report it even if Rails rescues it. + # + # @api private class RescuedExceptionInterceptor + # @param app [#call] Rack application. def initialize(app) @app = app end + # @param env [Hash] Rack environment. + # @return [Array] Rack response. def call(env) @app.call(env) rescue StandardError => e diff --git a/posthog-rails/lib/posthog/rails/tracing_headers.rb b/posthog-rails/lib/posthog/rails/tracing_headers.rb index 8fec5f7..aa9b2be 100644 --- a/posthog-rails/lib/posthog/rails/tracing_headers.rb +++ b/posthog-rails/lib/posthog/rails/tracing_headers.rb @@ -3,12 +3,16 @@ module PostHog module Rails # Helpers for extracting and sanitizing PostHog tracing headers from Rack/Rails requests. + # + # @api private module TracingHeaders MAX_HEADER_VALUE_LENGTH = 1000 CONTROL_CHARACTERS = /[[:cntrl:]]/ module_function + # @param value [Object] + # @return [String, nil] def sanitize_header_value(value) return nil unless value.is_a?(String) @@ -18,6 +22,9 @@ def sanitize_header_value(value) sanitized[0, MAX_HEADER_VALUE_LENGTH] end + # @param request_or_env [Object, Hash] Rack request, Rails request, or Rack env hash. + # @param header_name [String] + # @return [String, nil] def extract_header(request_or_env, header_name) candidates = header_candidates(header_name)