Repository navigation
feat(rails): add controller exception reporting helper - #339
marandaneto wants to merge 1 commit into
Conversation
posthog-ruby-sync Compliance ReportDate: 2026-11-06T14:37:20.557233+00:00
|
| Test | Status | Duration |
|---|---|---|
| Format Validation.Event Has Required Fields | ✅ | 7ms |
| Format Validation.Event Has Uuid | ✅ | 5ms |
| Format Validation.Event Has Lib Properties | ✅ | 4ms |
| Format Validation.Distinct Id Is String | ✅ | 7ms |
| Format Validation.Token Is Present | ✅ | 5ms |
| Format Validation.Custom Properties Preserved | ✅ | 5ms |
| Format Validation.Event Has Timestamp | ✅ | 6ms |
| Format Validation.Non Utc Event Timestamp Is Converted To Utc | ✅ | 5ms |
| Retry Behavior.Retries On 503 | ✅ | 5371ms |
| Retry Behavior.Does Not Retry On 400 | ❌ | 6ms |
| Retry Behavior.Does Not Retry On 401 | ❌ | 4ms |
| Retry Behavior.Respects Retry After Header | ✅ | 8011ms |
| Retry Behavior.Implements Backoff | ❌ | 232ms |
| Retry Behavior.Retries On 500 | ✅ | 5156ms |
| Retry Behavior.Retries On 502 | ✅ | 5113ms |
| Retry Behavior.Retries On 504 | ✅ | 5115ms |
| Retry Behavior.Max Retries Respected | ❌ | 539ms |
| Deduplication.Generates Unique Uuids | ✅ | 12ms |
| Deduplication.Preserves Uuid On Retry | ✅ | 5109ms |
| Deduplication.Preserves Uuid And Timestamp On Retry | ✅ | 10368ms |
| Deduplication.Preserves Uuid And Timestamp On Batch Retry | ✅ | 5116ms |
| Deduplication.No Duplicate Events In Batch | ✅ | 21ms |
| Deduplication.Different Events Have Different Uuids | ✅ | 18ms |
| Compression.Sends Gzip When Enabled | ✅ | 8ms |
| Batch Format.Uses Proper Batch Structure | ✅ | 4ms |
| Batch Format.Flush With No Events Sends Nothing | ✅ | 2ms |
| Batch Format.Multiple Events Batched Together | ❌ | 12ms |
| Error Handling.Does Not Retry On 403 | ❌ | 3ms |
| Error Handling.Does Not Retry On 413 | ❌ | 4ms |
| Error Handling.Retries On 408 | ✅ | 5111ms |
Failures
retry_behavior.does_not_retry_on_400
500, message='Internal Server Error', url='http://sdk-adapter:8080/capture'
retry_behavior.does_not_retry_on_401
500, message='Internal Server Error', url='http://sdk-adapter:8080/capture'
retry_behavior.implements_backoff
500, message='Internal Server Error', url='http://sdk-adapter:8080/capture'
retry_behavior.max_retries_respected
500, message='Internal Server Error', url='http://sdk-adapter:8080/capture'
batch_format.multiple_events_batched_together
Expected 1 requests, got 5
error_handling.does_not_retry_on_403
500, message='Internal Server Error', url='http://sdk-adapter:8080/capture'
error_handling.does_not_retry_on_413
500, message='Internal Server Error', url='http://sdk-adapter:8080/capture'
Feature_Flags Tests
View Details
| Test | Status | Duration |
|---|---|---|
| Request Payload.Request With Person Properties Device Id | ✅ | 7ms |
| Request Payload.Flags Request Uses V2 Query Param | ✅ | 7ms |
| Request Payload.Flags Request Hits Flags Path Not Decide | ✅ | 6ms |
| Request Payload.Flags Request Omits Authorization Header | ✅ | 5ms |
| Request Payload.Token In Flags Body Matches Init | ✅ | 5ms |
| Request Payload.Groups Round Trip | ✅ | 4ms |
| Request Payload.Groups Default To Empty Object | ✅ | 5ms |
| Request Payload.Disable Geoip False Propagates As Geoip Disable False | ✅ | 4ms |
| Request Payload.Disable Geoip Omitted Defaults To False | ❌ | 4ms |
| Request Payload.Flag Keys To Evaluate Contains Only Requested Key | ✅ | 5ms |
| Request Lifecycle.No Flags Request On Init Alone | ✅ | 1ms |
| Request Lifecycle.No Flags Request On Normal Capture | ✅ | 4ms |
| Request Lifecycle.Two Flag Calls Produce Two Remote Requests | ✅ | 6ms |
| Request Lifecycle.Mock Response Value Is Returned To Caller | ✅ | 5ms |
| Retry Behavior.Retries Flags On 502 | ✅ | 105ms |
| Retry Behavior.Retries Flags On 504 | ✅ | 108ms |
| Side Effect Events.Get Feature Flag Captures Feature Flag Called Event | ✅ | 5ms |
Failures
request_payload.disable_geoip_omitted_defaults_to_false
Field 'geoip_disable' not found in /flags request body at path 'geoip_disable'. Available keys: ['distinct_id', 'groups', 'person_properties', 'group_properties', 'flag_keys_to_evaluate', 'token']
posthog-ruby-async Compliance ReportDate: 2026-11-06T14:37:56.202024+00:00
|
| Test | Status | Duration |
|---|---|---|
| Format Validation.Event Has Required Fields | ✅ | 109ms |
| Format Validation.Event Has Uuid | ✅ | 107ms |
| Format Validation.Event Has Lib Properties | ✅ | 109ms |
| Format Validation.Distinct Id Is String | ✅ | 107ms |
| Format Validation.Token Is Present | ✅ | 107ms |
| Format Validation.Custom Properties Preserved | ✅ | 107ms |
| Format Validation.Event Has Timestamp | ✅ | 107ms |
| Format Validation.Non Utc Event Timestamp Is Converted To Utc | ✅ | 8ms |
| Retry Behavior.Retries On 503 | ✅ | 5312ms |
| Retry Behavior.Does Not Retry On 400 | ✅ | 2109ms |
| Retry Behavior.Does Not Retry On 401 | ✅ | 2108ms |
| Retry Behavior.Respects Retry After Header | ✅ | 8116ms |
| Retry Behavior.Implements Backoff | ✅ | 15623ms |
| Retry Behavior.Retries On 500 | ✅ | 5211ms |
| Retry Behavior.Retries On 502 | ✅ | 5213ms |
| Retry Behavior.Retries On 504 | ✅ | 5212ms |
| Retry Behavior.Max Retries Respected | ✅ | 15623ms |
| Deduplication.Generates Unique Uuids | ✅ | 111ms |
| Deduplication.Preserves Uuid On Retry | ✅ | 5212ms |
| Deduplication.Preserves Uuid And Timestamp On Retry | ✅ | 10317ms |
| Deduplication.Preserves Uuid And Timestamp On Batch Retry | ✅ | 5215ms |
| Deduplication.No Duplicate Events In Batch | ✅ | 112ms |
| Deduplication.Different Events Have Different Uuids | ✅ | 107ms |
| Compression.Sends Gzip When Enabled | ✅ | 106ms |
| Batch Format.Uses Proper Batch Structure | ✅ | 107ms |
| Batch Format.Flush With No Events Sends Nothing | ✅ | 5ms |
| Batch Format.Multiple Events Batched Together | ✅ | 113ms |
| Error Handling.Does Not Retry On 403 | ✅ | 2108ms |
| Error Handling.Does Not Retry On 413 | ✅ | 2110ms |
| Error Handling.Retries On 408 | ✅ | 5212ms |
Feature_Flags Tests
View Details
| Test | Status | Duration |
|---|---|---|
| Request Payload.Request With Person Properties Device Id | ✅ | 108ms |
| Request Payload.Flags Request Uses V2 Query Param | ✅ | 106ms |
| Request Payload.Flags Request Hits Flags Path Not Decide | ✅ | 107ms |
| Request Payload.Flags Request Omits Authorization Header | ✅ | 107ms |
| Request Payload.Token In Flags Body Matches Init | ✅ | 108ms |
| Request Payload.Groups Round Trip | ✅ | 107ms |
| Request Payload.Groups Default To Empty Object | ✅ | 107ms |
| Request Payload.Disable Geoip False Propagates As Geoip Disable False | ✅ | 108ms |
| Request Payload.Disable Geoip Omitted Defaults To False | ❌ | 107ms |
| Request Payload.Flag Keys To Evaluate Contains Only Requested Key | ✅ | 107ms |
| Request Lifecycle.No Flags Request On Init Alone | ✅ | 4ms |
| Request Lifecycle.No Flags Request On Normal Capture | ✅ | 105ms |
| Request Lifecycle.Two Flag Calls Produce Two Remote Requests | ✅ | 111ms |
| Request Lifecycle.Mock Response Value Is Returned To Caller | ✅ | 106ms |
| Retry Behavior.Retries Flags On 502 | ✅ | 248ms |
| Retry Behavior.Retries Flags On 504 | ✅ | 236ms |
| Side Effect Events.Get Feature Flag Captures Feature Flag Called Event | ✅ | 111ms |
Failures
request_payload.disable_geoip_omitted_defaults_to_false
Field 'geoip_disable' not found in /flags request body at path 'geoip_disable'. Available keys: ['distinct_id', 'groups', 'person_properties', 'group_properties', 'flag_keys_to_evaluate', 'token']
SDK performance: posthog-rubyBenchmarks completed. No changes flagged for review. Flags require at least a 5% change in the median and all six pairs agreeing on direction. This is a noise filter, not proof of a performance change or a CI gate. View all results
Values are medians of paired measurements. Not flagged does not mean unchanged. Unavailable means a measurement could not be provided, not zero. Estimated allocation measurements retain their instrumentation uncertainty. RSS measures the whole process, not just SDK allocations; observed peaks are sampled checkpoints. Paired ranges in the diagnostics describe variability, not confidence intervals. Each shard uses its own runner; compare Before PR against With PR within a row, not across shards. Compared 102 metric rows; 102 rows shown in all results. Omitted or shortened data remain in |
💡 Motivation and Context
Public API addition: Rails controllers and API controllers gain a private
capture_exception(exception, distinct_id = nil, additional_properties = {}, flags: nil, mechanism: nil)helper. It is not a controller action and does not replace an existing helper with that name. API agreement can happen in maintainer review.Applications that handle an exception in a controller can report it with the request's filtered parameters and configured user identity without assembling that context themselves. Manual reporting works when
auto_capture_exceptionsis off and bypasses automatic exception exclusions. It keeps the core manual handled-exception defaults, duplicate prevention and capture result. Missing or disabled clients return false without creating a client.Explicit identity wins over configured current-user identity, which wins over request identity. Explicit properties, flags and mechanism are preserved. Request enrichment uses Rails
filter_parameters, honors user-context and tracing-header settings, and strips queries and fragments from inferred URLs. Caller properties and exception messages are not parameter-filtered. Failed enrichment does not drop the original report or log raw diagnostic messages.The helper reads existing Rails parameter caches or query/path parameters without proactively parsing request bodies. It includes response status only when already known, including after a performed response. Rails can independently parse and log malformed bodies before an action runs. This helper does not suppress those framework diagnostics. Request context is not retained after the request.
The automatic middleware shares the extracted enrichment implementation but keeps its existing behavior. The capture-exception, exception-event-metadata and tracing-headers specifications were checked for touched behavior. This helper preserves existing core capture semantics and does not introduce an SDK-wide ignored-exception policy or change unrelated exception metadata behavior.
Includes one Rails-only minor changeset and an intentional API snapshot entry for
ControllerHelpers. The private method signature is protected by a regression test rather than listed as a public instance method in the snapshot. No package versions were changed.💚 How did you test it?
Local runtime: Ruby 4.0.7, Bundler 4.0.13, Rails 7.2.3.1.
bundle exec rspec: 2190 examples, 0 failures, 41 pending without optional dependencies.BUNDLE_GEMFILE=gemfiles/otel.gemfile bundle exec rspec, with the existing sharedBUNDLE_PATHset explicitly: 2190 examples, 0 failures, 2 existing pending.bundle exec rubocop: 153 files inspected, no offenses.bundle exec rake public_api:checkandgit diff --check: passed.Tests dispatch real API and full-controller actions through request middleware and inspect captured events. Fresh-process Rails boot tests verify lazy installation, existing-helper precedence, private visibility and action exclusion. Cases cover explicit arguments, user/header precedence, filtered caches, malformed and uncached bodies, metadata failures, finalized response status, duplicate prevention, disabled clients and request cleanup.
The test-audit Authoring gate retained all 38 new examples. A temporary controlled mutation made manual capture parse the body. Both parser-avoidance regression cases failed on the unintended parser call, then passed after restoring the implementation. No tests were weakened. The historical Ruby/Rails matrix was not run locally, and the eight separate feature branches were not combined.
The required isolated autoreview ran against exact committed HEAD
eec450269d51961c3e151bd94055156173dcb7e9and base58f5335f895ebebc417bb4a48d22351ff96b5fb0. It reported no findings.📝 Checklist
If releasing new changes
pnpm changesetto generate a changeset fileThe hand-authored
.changeset/rails-controller-exception-helper.mdis included.pnpm changesetwas not run and no duplicate changeset was generated.🤖 Agent context
Autonomy: Human-driven (agent-assisted)
marandaneto is the human DRI and PR assignee. Pi's
workerimplementation subagent reviewed and published the existing feature work using bash, Git and GitHub CLI. The requiredautoreviewhelper used Pi withclaude-code/claude-opus-5-5in isolated, tools-disabled review mode. Local Pi session ID:01a1b0e4-9039-71a4-a297-fed90e502ce9. No public session URL is available.The implementation keeps manual reporting independent from automatic capture, reads body caches instead of invoking parsers, and preserves the automatic middleware behavior while sharing enrichment code. Human review is required before merge. No self-approval or auto-merge is enabled.