Skip to content

feat(rails): capture Action Cable exceptions - #335

Open
marandaneto wants to merge 1 commit into
mainfrom
feat/rails-action-cable-zPpC27
Open

marandaneto wants to merge 1 commit into
mainfrom
feat/rails-action-cable-zPpC27

Conversation

@marandaneto

Copy link
Copy Markdown
Member

💡 Motivation and Context

Action Cable runs connection callbacks and channel actions outside the normal HTTP exception middleware. Errors escaping those operations currently go unreported by the Rails integration.

This adds automatic capture around connection open/close, channel subscribe/unsubscribe, and dispatched channel actions. It uses the existing auto_capture_exceptions option, which remains false by default, and respects excluded_exceptions. No new public configuration or methods are added. The included changeset selects a minor release of posthog-rails only.

Each operation gets isolated context from the connection environment and restores the caller's context afterward. Authenticated controller or connection users take precedence over sanitized PostHog header identity. Existing user lookup options apply. capture_user_context = false disables user lookup, and use_tracing_headers = false disables header correlation. A controller resolver is never passed a connection. Explicit capture IDs and properties keep their existing precedence.

Channel parameters and action data use Rails filter_parameters. Raw environments, cookies, authorization headers, subscription identifiers, and URL query strings are not attached. Applications should review filtering and before_send for their own sensitive data. Return values and escaping exceptions stay unchanged. Errors consumed by application handlers, including channel rescue_from, are not captured at these boundaries.

Action Cable remains an optional runtime dependency and loads lazily. Its added dependency is development/test-only. The wrappers do not create or enrich spans. Rails 5.2 has a fallback load hook, but local validation used Rails 7.2.3.1 only.

The relevant pinned SDK specs cover exception metadata, tracing headers, exception capture, and before_send; none defines an Action Cable-specific API. New events use source rails.action_cable, level error, and a middleware mechanism with handled: false. One contract difference remains: this integration passes source and level through the existing Ruby exception property bag rather than a typed internal metadata channel. This PR does not redesign the core exception API.

💚 How did you test it?

Validated on Ruby 4.0.7, Bundler 4.0.13, and Rails 7.2.3.1 against base 58f5335f895ebebc417bb4a48d22351ff96b5fb0:

  • Focused Action Cable suite: 24 examples, 0 failures. Uses real framework callbacks/actions, a real Rails boot in a subprocess, and lazy-load checks.
  • Full base suite: 2176 examples, 0 failures, 40 pending cases (38 optional integration cases and 2 existing pending cases).
  • Full optional OpenTelemetry suite: 2176 examples, 0 failures, 2 existing pending cases.
  • bundle exec rubocop: 152 files, no offenses.
  • bundle exec rake public_api:check and git diff --check: passed. No public API snapshot change was needed.

The Authoring test audit retained all new cases. It checked independent event oracles, identity precedence, filtered data, error/return preservation, nested capture deduplication, concurrent socket isolation, client ownership, instrumentation failures, and hook drops. The historical Ruby/Rails matrix was not run locally.

Exact-HEAD autoreview on commit 72ba70aa210dad9f0bdb258e5825824d58a28fbc against origin/main completed with no findings. No review transcript is posted here.

📝 Checklist

  • All commits are signed (see Commit signing)
  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed.
  • No breaking change or entry added to the changelog.

If releasing new changes

  • Ran pnpm changeset to generate a changeset file

The existing hand-authored .changeset/rails-action-cable-exceptions.md is included. pnpm changeset was not run, and no duplicate changeset or version bump was added.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Human DRI: @marandaneto. Pi's worker subagent reviewed and prepared this feature using local file tools, Bash, Git, GitHub CLI, RSpec, RuboCop, Rake, and the autoreview helper with claude-code/claude-opus-5-5. Local Pi session ID: 01a1b0e4-9039-71a4-a297-fed90e502ce9. There is no public session URL.

The implementation keeps automatic capture opt-in, uses authenticated identity only for exception attribution, and avoids retaining the upgrade request's client or annotating unrelated spans. This draft requires human review before merge.

@marandaneto marandaneto self-assigned this Nov 6, 2026
@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown

posthog-ruby-sync Compliance Report

Date: 2026-11-06T14:36:20.999509+00:00
Duration: 55781ms

⚠️ Some Tests Failed

39/47 tests passed, 8 failed


Capture Tests

⚠️ 23/30 tests passed, 7 failed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 8ms
Format Validation.Event Has Uuid ✅ 5ms
Format Validation.Event Has Lib Properties ✅ 5ms
Format Validation.Distinct Id Is String ✅ 7ms
Format Validation.Token Is Present ✅ 6ms
Format Validation.Custom Properties Preserved ✅ 6ms
Format Validation.Event Has Timestamp ✅ 6ms
Format Validation.Non Utc Event Timestamp Is Converted To Utc ✅ 5ms
Retry Behavior.Retries On 503 ✅ 5317ms
Retry Behavior.Does Not Retry On 400 ❌ 5ms
Retry Behavior.Does Not Retry On 401 ❌ 5ms
Retry Behavior.Respects Retry After Header ✅ 8013ms
Retry Behavior.Implements Backoff ❌ 300ms
Retry Behavior.Retries On 500 ✅ 5152ms
Retry Behavior.Retries On 502 ✅ 5152ms
Retry Behavior.Retries On 504 ✅ 5114ms
Retry Behavior.Max Retries Respected ❌ 466ms
Deduplication.Generates Unique Uuids ✅ 12ms
Deduplication.Preserves Uuid On Retry ✅ 5111ms
Deduplication.Preserves Uuid And Timestamp On Retry ✅ 10298ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 5119ms
Deduplication.No Duplicate Events In Batch ✅ 14ms
Deduplication.Different Events Have Different Uuids ✅ 7ms
Compression.Sends Gzip When Enabled ✅ 4ms
Batch Format.Uses Proper Batch Structure ✅ 4ms
Batch Format.Flush With No Events Sends Nothing ✅ 3ms
Batch Format.Multiple Events Batched Together ❌ 11ms
Error Handling.Does Not Retry On 403 ❌ 4ms
Error Handling.Does Not Retry On 413 ❌ 3ms
Error Handling.Retries On 408 ✅ 5158ms

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

⚠️ 16/17 tests passed, 1 failed

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 ✅ 7ms
Request Payload.Flags Request Omits Authorization Header ✅ 6ms
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 ✅ 7ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 4ms
Retry Behavior.Retries Flags On 502 ✅ 107ms
Retry Behavior.Retries Flags On 504 ✅ 108ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 7ms

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']

@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown

posthog-ruby-async Compliance Report

Date: 2026-11-06T14:37:02.267017+00:00
Duration: 98595ms

⚠️ Some Tests Failed

46/47 tests passed, 1 failed


Capture Tests

✅ 30/30 tests passed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 10ms
Format Validation.Event Has Uuid ✅ 105ms
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 ✅ 109ms
Retry Behavior.Retries On 503 ✅ 5412ms
Retry Behavior.Does Not Retry On 400 ✅ 2109ms
Retry Behavior.Does Not Retry On 401 ✅ 2109ms
Retry Behavior.Respects Retry After Header ✅ 8116ms
Retry Behavior.Implements Backoff ✅ 15723ms
Retry Behavior.Retries On 500 ✅ 5212ms
Retry Behavior.Retries On 502 ✅ 5213ms
Retry Behavior.Retries On 504 ✅ 5208ms
Retry Behavior.Max Retries Respected ✅ 15622ms
Deduplication.Generates Unique Uuids ✅ 112ms
Deduplication.Preserves Uuid On Retry ✅ 5211ms
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 ✅ 4ms
Batch Format.Multiple Events Batched Together ✅ 112ms
Error Handling.Does Not Retry On 403 ✅ 2110ms
Error Handling.Does Not Retry On 413 ✅ 2110ms
Error Handling.Retries On 408 ✅ 5213ms

Feature_Flags Tests

⚠️ 16/17 tests passed, 1 failed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 107ms
Request Payload.Flags Request Uses V2 Query Param ✅ 107ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 107ms
Request Payload.Flags Request Omits Authorization Header ✅ 108ms
Request Payload.Token In Flags Body Matches Init ✅ 107ms
Request Payload.Groups Round Trip ✅ 108ms
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 ❌ 108ms
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 ✅ 112ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 107ms
Retry Behavior.Retries Flags On 502 ✅ 243ms
Retry Behavior.Retries Flags On 504 ✅ 210ms
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']

@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown

SDK performance: posthog-ruby

Benchmarks 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
Test Measurement Before PR With PR Change Assessment
Capture and deliver events (32-byte payload, burst)
Shard: capture
Allocated memory per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ Ruby has no cumulative allocated-byte counter here; retained ObjectSpace bytes are not allocation volume.​; head:​ Ruby has no cumulative allocated-byte counter here; retained ObjectSpace bytes are not allocation volume.​
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Allocated memory per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ Ruby has no cumulative allocated-byte counter here; retained ObjectSpace bytes are not allocation volume.​; head:​ Ruby has no cumulative allocated-byte counter here; retained ObjectSpace bytes are not allocation volume.​
Capture and deliver events (32-byte payload, steady)
Shard: capture
Allocated memory per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ Ruby has no cumulative allocated-byte counter here; retained ObjectSpace bytes are not allocation volume.​; head:​ Ruby has no cumulative allocated-byte counter here; retained ObjectSpace bytes are not allocation volume.​
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Allocated memory per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ Ruby has no cumulative allocated-byte counter here; retained ObjectSpace bytes are not allocation volume.​; head:​ Ruby has no cumulative allocated-byte counter here; retained ObjectSpace bytes are not allocation volume.​
Queue an event (32-byte payload)
Shard: capture
Allocated memory per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ Background serialization has no public enqueue completion boundary; use completed-delivery workloads for process-wide allocation counts.​; head:​ Background serialization has no public enqueue completion boundary; use completed-delivery workloads for process-wide allocation counts.​
Queue an event (32-byte payload)
Shard: capture
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ Background serialization has no public enqueue completion boundary; use completed-delivery workloads for process-wide allocation counts.​; head:​ Background serialization has no public enqueue completion boundary; use completed-delivery workloads for process-wide allocation counts.​
Queue an event (4,096-byte payload)
Shard: capture
Allocated memory per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ Background serialization has no public enqueue completion boundary; use completed-delivery workloads for process-wide allocation counts.​; head:​ Background serialization has no public enqueue completion boundary; use completed-delivery workloads for process-wide allocation counts.​
Queue an event (4,096-byte payload)
Shard: capture
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ Background serialization has no public enqueue completion boundary; use completed-delivery workloads for process-wide allocation counts.​; head:​ Background serialization has no public enqueue completion boundary; use completed-delivery workloads for process-wide allocation counts.​
Evaluate all 10 flags
Shard: flags-scaling
Allocated memory per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ Ruby has no cumulative allocated-byte counter here; retained ObjectSpace bytes are not allocation volume.​; head:​ Ruby has no cumulative allocated-byte counter here; retained ObjectSpace bytes are not allocation volume.​
Evaluate all 100 flags
Shard: flags-scaling
Allocated memory per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ Ruby has no cumulative allocated-byte counter here; retained ObjectSpace bytes are not allocation volume.​; head:​ Ruby has no cumulative allocated-byte counter here; retained ObjectSpace bytes are not allocation volume.​
Evaluate all 1,000 flags
Shard: flags-scaling
Allocated memory per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ Ruby has no cumulative allocated-byte counter here; retained ObjectSpace bytes are not allocation volume.​; head:​ Ruby has no cumulative allocated-byte counter here; retained ObjectSpace bytes are not allocation volume.​
Evaluate one boolean flag
Shard: flags-single
Allocated memory per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ Ruby has no cumulative allocated-byte counter here; retained ObjectSpace bytes are not allocation volume.​; head:​ Ruby has no cumulative allocated-byte counter here; retained ObjectSpace bytes are not allocation volume.​
Evaluate a cohort-filtered flag
Shard: flags-single
Allocated memory per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ Ruby has no cumulative allocated-byte counter here; retained ObjectSpace bytes are not allocation volume.​; head:​ Ruby has no cumulative allocated-byte counter here; retained ObjectSpace bytes are not allocation volume.​
Evaluate a flag (early match)
Shard: flags-single
Allocated memory per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ Ruby has no cumulative allocated-byte counter here; retained ObjectSpace bytes are not allocation volume.​; head:​ Ruby has no cumulative allocated-byte counter here; retained ObjectSpace bytes are not allocation volume.​
Evaluate a flag (late match)
Shard: flags-single
Allocated memory per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ Ruby has no cumulative allocated-byte counter here; retained ObjectSpace bytes are not allocation volume.​; head:​ Ruby has no cumulative allocated-byte counter here; retained ObjectSpace bytes are not allocation volume.​
Evaluate a flag (no match)
Shard: flags-single
Allocated memory per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ Ruby has no cumulative allocated-byte counter here; retained ObjectSpace bytes are not allocation volume.​; head:​ Ruby has no cumulative allocated-byte counter here; retained ObjectSpace bytes are not allocation volume.​
Evaluate a property-filtered flag
Shard: flags-single
Allocated memory per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ Ruby has no cumulative allocated-byte counter here; retained ObjectSpace bytes are not allocation volume.​; head:​ Ruby has no cumulative allocated-byte counter here; retained ObjectSpace bytes are not allocation volume.​
Evaluate a regex-filtered flag
Shard: flags-single
Allocated memory per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ Ruby has no cumulative allocated-byte counter here; retained ObjectSpace bytes are not allocation volume.​; head:​ Ruby has no cumulative allocated-byte counter here; retained ObjectSpace bytes are not allocation volume.​
Evaluate a rollout flag
Shard: flags-single
Allocated memory per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ Ruby has no cumulative allocated-byte counter here; retained ObjectSpace bytes are not allocation volume.​; head:​ Ruby has no cumulative allocated-byte counter here; retained ObjectSpace bytes are not allocation volume.​
Evaluate a multivariate flag
Shard: flags-single
Allocated memory per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ Ruby has no cumulative allocated-byte counter here; retained ObjectSpace bytes are not allocation volume.​; head:​ Ruby has no cumulative allocated-byte counter here; retained ObjectSpace bytes are not allocation volume.​
Capture and deliver events (32-byte payload, burst)
Shard: capture
Allocations per operation 56.27 count/op 56.27 count/op No observed change Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Time per operation 107.95 µs/op 107.96 µs/op <0.1% more time Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Idle process memory (RSS) 28.43 MiB 28.36 MiB 0.3% less memory Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Observed peak process memory (RSS) 31.19 MiB 31.12 MiB 0.2% less memory Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Managed heap after settling 5.28 MiB 5.28 MiB <0.1% less memory Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Process memory after settling (RSS) 30.43 MiB 30.35 MiB 0.2% less memory Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Process CPU time per operation 17.89 µs/op 17.71 µs/op 1% less time Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Lifetime peak process memory (RSS) 31.02 MiB 30.96 MiB 0.2% less memory Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Allocations per operation 58.65 count/op 58.65 count/op No observed change Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Time per operation 107.89 µs/op 107.88 µs/op <0.1% less time Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Idle process memory (RSS) 33.25 MiB 33.26 MiB <0.1% more memory Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Observed peak process memory (RSS) 42.46 MiB 42.47 MiB <0.1% more memory Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Managed heap after settling 9.15 MiB 9.15 MiB <0.1% less memory Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Process memory after settling (RSS) 40.73 MiB 40.75 MiB <0.1% more memory Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Process CPU time per operation 25.12 µs/op 25.44 µs/op 1.3% more time Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Lifetime peak process memory (RSS) 42.23 MiB 42.21 MiB <0.1% less memory Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Allocations per operation 58.68 count/op 58.68 count/op No observed change Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Time per operation 1.01 ms/op 1.01 ms/op <0.1% less time Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Idle process memory (RSS) 28.34 MiB 28.33 MiB <0.1% less memory Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Observed peak process memory (RSS) 30.46 MiB 30.44 MiB <0.1% less memory Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Managed heap after settling 5.28 MiB 5.28 MiB <0.1% more memory Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Process memory after settling (RSS) 30.46 MiB 30.44 MiB <0.1% less memory Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Process CPU time per operation 27.6 µs/op 28.01 µs/op 1.5% more time Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Lifetime peak process memory (RSS) 30.46 MiB 30.44 MiB <0.1% less memory Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Allocations per operation 58.68 count/op 58.68 count/op No observed change Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Time per operation 1.01 ms/op 1.01 ms/op <0.1% more time Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Idle process memory (RSS) 33.26 MiB 33.25 MiB <0.1% less memory Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Observed peak process memory (RSS) 40.6 MiB 40.61 MiB <0.1% more memory Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Managed heap after settling 9.15 MiB 9.15 MiB <0.1% less memory Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Process memory after settling (RSS) 40.31 MiB 40.27 MiB 0.1% less memory Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Process CPU time per operation 34.58 µs/op 34.2 µs/op 1.1% less time Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Lifetime peak process memory (RSS) 40.88 MiB 40.91 MiB <0.1% more memory Not flagged
Queue an event (32-byte payload)
Shard: capture
Caller-thread CPU time per operation 7.66 µs/op 7.68 µs/op 0.2% more time Not flagged
Queue an event (32-byte payload)
Shard: capture
Time per operation 7.66 µs/op 7.68 µs/op 0.2% more time Not flagged
Queue an event (4,096-byte payload)
Shard: capture
Caller-thread CPU time per operation 7.41 µs/op 7.47 µs/op 0.8% more time Not flagged
Queue an event (4,096-byte payload)
Shard: capture
Time per operation 7.41 µs/op 7.46 µs/op 0.7% more time Not flagged
Evaluate all 10 flags
Shard: flags-scaling
Allocations per operation 343.02 count/op 343.02 count/op No observed change Not flagged
Evaluate all 10 flags
Shard: flags-scaling
Time per operation 75.54 µs/op 75.71 µs/op 0.2% more time Not flagged
Evaluate all 10 flags
Shard: flags-scaling
Process CPU time per operation 75.53 µs/op 75.71 µs/op 0.2% more time Not flagged
Evaluate all 100 flags
Shard: flags-scaling
Allocations per operation 3,324 count/op 3,324 count/op No observed change Not flagged
Evaluate all 100 flags
Shard: flags-scaling
Time per operation 723.02 µs/op 752.62 µs/op 4.1% more time Not flagged
Evaluate all 100 flags
Shard: flags-scaling
Process CPU time per operation 723.03 µs/op 752.61 µs/op 4.1% more time Not flagged
Evaluate all 1,000 flags
Shard: flags-scaling
Allocations per operation 33,113.62 count/op 33,113.56 count/op <0.1% fewer Not flagged
Evaluate all 1,000 flags
Shard: flags-scaling
Time per operation 7.18 ms/op 7.18 ms/op <0.1% less time Not flagged
Evaluate all 1,000 flags
Shard: flags-scaling
Process CPU time per operation 7.18 ms/op 7.18 ms/op <0.1% less time Not flagged
Evaluate 1,000 flags under concurrent load
Shard: flags-memory
Idle process memory (RSS) 38.92 MiB 38.88 MiB <0.1% less memory Not flagged
Evaluate 1,000 flags under concurrent load
Shard: flags-memory
Observed peak process memory (RSS) 40.92 MiB 40.28 MiB 1.6% less memory Not flagged
Evaluate 1,000 flags under concurrent load
Shard: flags-memory
Managed heap after settling 7.97 MiB 7.97 MiB <0.1% less memory Not flagged
Evaluate 1,000 flags under concurrent load
Shard: flags-memory
Process memory after settling (RSS) 39.39 MiB 39.31 MiB 0.2% less memory Not flagged
Evaluate 1,000 flags under concurrent load
Shard: flags-memory
Lifetime peak process memory (RSS) 40.71 MiB 40.01 MiB 1.7% less memory Not flagged
Refresh 1,000 flag definitions
Shard: flags-memory
Idle process memory (RSS) 38.11 MiB 37.7 MiB 1.1% less memory Not flagged
Refresh 1,000 flag definitions
Shard: flags-memory
Observed peak process memory (RSS) 41.45 MiB 41.46 MiB <0.1% more memory Not flagged
Refresh 1,000 flag definitions
Shard: flags-memory
Managed heap after settling 9.48 MiB 9.48 MiB <0.1% less memory Not flagged
Refresh 1,000 flag definitions
Shard: flags-memory
Process memory after settling (RSS) 41.44 MiB 41.44 MiB <0.1% more memory Not flagged
Refresh 1,000 flag definitions
Shard: flags-memory
Lifetime peak process memory (RSS) 41.44 MiB 41.44 MiB <0.1% more memory Not flagged
Evaluate one boolean flag
Shard: flags-single
Allocations per operation 38 count/op 38 count/op No observed change Not flagged
Evaluate one boolean flag
Shard: flags-single
Time per operation 11.88 µs/op 11.84 µs/op 0.4% less time Not flagged
Evaluate one boolean flag
Shard: flags-single
Process CPU time per operation 11.88 µs/op 11.84 µs/op 0.4% less time Not flagged
Evaluate a cohort-filtered flag
Shard: flags-single
Allocations per operation 38 count/op 38 count/op No observed change Not flagged
Evaluate a cohort-filtered flag
Shard: flags-single
Time per operation 18.87 µs/op 18.92 µs/op 0.3% more time Not flagged
Evaluate a cohort-filtered flag
Shard: flags-single
Process CPU time per operation 18.87 µs/op 18.92 µs/op 0.3% more time Not flagged
Evaluate a flag (early match)
Shard: flags-single
Allocations per operation 55 count/op 55 count/op No observed change Not flagged
Evaluate a flag (early match)
Shard: flags-single
Time per operation 22.98 µs/op 22.91 µs/op 0.3% less time Not flagged
Evaluate a flag (early match)
Shard: flags-single
Process CPU time per operation 22.98 µs/op 22.91 µs/op 0.3% less time Not flagged
Evaluate a flag (late match)
Shard: flags-single
Allocations per operation 76 count/op 76 count/op No observed change Not flagged
Evaluate a flag (late match)
Shard: flags-single
Time per operation 31.72 µs/op 32.38 µs/op 2.1% more time Not flagged
Evaluate a flag (late match)
Shard: flags-single
Process CPU time per operation 31.72 µs/op 32.38 µs/op 2.1% more time Not flagged
Evaluate a flag (no match)
Shard: flags-single
Allocations per operation 67 count/op 67 count/op No observed change Not flagged
Evaluate a flag (no match)
Shard: flags-single
Time per operation 27.43 µs/op 27.58 µs/op 0.5% more time Not flagged
Evaluate a flag (no match)
Shard: flags-single
Process CPU time per operation 27.43 µs/op 27.57 µs/op 0.5% more time Not flagged
Evaluate a property-filtered flag
Shard: flags-single
Allocations per operation 34 count/op 34 count/op No observed change Not flagged
Evaluate a property-filtered flag
Shard: flags-single
Time per operation 13.42 µs/op 13.44 µs/op 0.2% more time Not flagged
Evaluate a property-filtered flag
Shard: flags-single
Process CPU time per operation 13.42 µs/op 13.44 µs/op 0.2% more time Not flagged
Evaluate a regex-filtered flag
Shard: flags-single
Allocations per operation 99 count/op 99 count/op <0.1% fewer Not flagged
Evaluate a regex-filtered flag
Shard: flags-single
Time per operation 40.07 µs/op 40.28 µs/op 0.5% more time Not flagged
Evaluate a regex-filtered flag
Shard: flags-single
Process CPU time per operation 40.06 µs/op 40.28 µs/op 0.5% more time Not flagged
Evaluate a rollout flag
Shard: flags-single
Allocations per operation 31 count/op 31 count/op No observed change Not flagged
Evaluate a rollout flag
Shard: flags-single
Time per operation 10.85 µs/op 10.9 µs/op 0.5% more time Not flagged
Evaluate a rollout flag
Shard: flags-single
Process CPU time per operation 10.85 µs/op 10.9 µs/op 0.5% more time Not flagged
Evaluate a multivariate flag
Shard: flags-single
Allocations per operation 37 count/op 37 count/op No observed change Not flagged
Evaluate a multivariate flag
Shard: flags-single
Time per operation 13.49 µs/op 13.69 µs/op 1.5% more time Not flagged
Evaluate a multivariate flag
Shard: flags-single
Process CPU time per operation 13.49 µs/op 13.69 µs/op 1.5% more time Not flagged

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 58f5335f895ebebc417bb4a48d22351ff96b5fb0 → 72ba70aa210dad9f0bdb258e5825824d58a28fbc.

Full report and diagnostics

102 metric rows; 102 rows shown in all results. Omitted or shortened data remain in comparison.json, including per-runner environments, resource limits, measurement methods, samples and checkpoints.

@marandaneto
marandaneto marked this pull request as ready for review November 6, 2026 14:48
@marandaneto
marandaneto requested a review from a team as a code owner November 6, 2026 14:48

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostHog Review · Standard · reviewed 72ba70a: 1 Must fix, 1 Should fix.

See this review and its findings in PostHog Review. Comment @posthog review --deep for a Deep review. A Deep review takes longer and looks deeper than a Standard review.

Comment on lines +91 to +93
properties['$channel_params'] = safe_serialize(filter_sensitive_params(@channel.params))
if @data
filtered_data = safe_serialize(filter_sensitive_params(@data))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 · Include Action Cable's configured parameter filters

When an app configures sensitive keys through config.action_cable.filter_parameters, these calls ignore them: inherited filter_sensitive_params reads only the application-wide filters. Consequently, $channel_params and $action_data send values that Action Cable's own logging masks using connection.config.filter_parameters. Include the connection's filters before serializing these properties.

Comment on lines +24 to +26
rescue StandardError => e
capture_exception(e, env)
raise

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 · Respect connection rescue handlers before capturing channel errors

When a channel error reaches a connection's rescue_from handler, Rails invokes that handler in Subscriptions#execute_command, outside these wrappers. ActionCableCapture#call therefore already emits an unhandled event even when the application consumes the error and report_rescued_exceptions is false. Defer capture until connection handlers have run.

return properties unless @channel

properties['$channel_class'] = @channel.class.name
properties['$channel_params'] = safe_serialize(filter_sensitive_params(@channel.params))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium: Cable-specific sensitive parameter filters are ignored

The inherited filter_sensitive_params reads only Rails.application.config.filter_parameters, whereas Action Cable redacts action arguments using connection.config.filter_parameters, which supports Cable-specific rules. (raw.githubusercontent.com) With a field such as :credential configured only in the Cable filters, captured exceptions send its value verbatim in $channel_params here and $action_data at line 93; merge the effective connection filters with the application filters before serializing either payload.

@veria-ai

veria-ai Bot commented Nov 6, 2026

Copy link
Copy Markdown

PR overview

The PR adds Action Cable exception capture to the Rails integration, including channel parameters and action data in captured events.

One issue remains open: exception capture uses application-level parameter filters but ignores Cable-specific filters, allowing values intended to be redacted to appear in exception telemetry. The exposure is limited to fields covered only by Cable-specific rules; no issues have been addressed yet.

Open issues (1)

Fixed/addressed: 0 · PR risk: 5/10

@dustinbyrne dustinbyrne left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two reproduced cases need correction before approval: Cable-only sensitive-parameter filters are omitted, and exceptions consumed by a connection's rescue_from are emitted as unhandled. The existing Action Cable examples pass but do not cover these cases.

AI-assisted review.

return properties unless @channel

properties['$channel_class'] = @channel.class.name
properties['$channel_params'] = safe_serialize(filter_sensitive_params(@channel.params))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Apply effective Action Cable sensitive-parameter filters

IMPORTANT · REQUIRED · introduced
Location: posthog-rails/lib/posthog/rails/action_cable.rb:91–94 — inline line 91, RIGHT.

With automatic capture enabled, a channel failure puts plaintext values protected by Cable-specific filters into $channel_params and $action_data. Expected: those values remain redacted, as they are in Cable's own diagnostics.

An ordinary application can configure:

# Rails application configuration
config.filter_parameters = [:password]
config.action_cable.filter_parameters = [:credential]

# PostHog configuration
PostHog::Rails.config.auto_capture_exceptions = true

A client subscribing with credential: 'subscription-secret', then performing an action with credential: 'action-secret', triggers the issue when that action raises. The inherited helper reads only application-wide filters (posthog-rails/lib/posthog/rails/parameter_filter.rb:51–58), whereas Rails' channel filter reads connection.config.filter_parameters. Cable-only filters are not copied back into the application's filter list.

Executed result on head:

'$channel_params' => { 'credential' => 'subscription-secret' },
'$action_data' => {
  'action' => 'speak',
  'credential' => 'action-secret',
  'password' => '[FILTERED]'
}

The native Cable filter redacts credential in the same fixture, and the SDK correctly redacts the application-wide password: filtering runs, but uses an incomplete rule set. Both assertions below fail on head:

expect(properties['$channel_params']['credential']).to eq('[FILTERED]')
expect(properties['$action_data']['credential']).to eq('[FILTERED]')

The equivalent base control emits no Cable event and therefore has no new disclosure. This is newly exported data, not a formatting change to an existing event. The reproduction uses real server/connection/channel objects; only application configuration is mocked. It requires enabled capture, an eligible failure, Cable-only sensitive rules and no later scrubber. No network delivery was performed; the plaintext was observed in the queued event consumed by normal transport.

Consider applying both application-wide and effective connection/Cable filters before serializing either property, preserving older-Rails/test-connection fallbacks. Add regression coverage for both payloads, both filter sources and nested values. The executed regression fixture and base/head results are included below; no fix or post-fix success is claimed.

Executed regression fixture and base/head results

Runtime: Ruby 3.4.7, Rails/Action Cable 7.2.3.1, JSON 2.21.2, equivalent dependencies and configuration. Execution used explicit gem activation and Ruby/RSpec (ruby -Ilib:posthog-rails/lib:spec -rrspec/autorun regression_spec.rb --format documentation) in an isolated, network-disabled environment. REVIEW_SNAPSHOT=base selects the base control; REVIEW_SNAPSHOT=head selects head assertions.

Base 58f5335f895ebebc417bb4a48d22351ff96b5fb0: 3 examples, 0 failures, exit 0. Head 72ba70aa210dad9f0bdb258e5825824d58a28fbc: 3 examples, 2 failed examples, exit 1. The failed examples are the two behaviors reviewed here; the channel-local rescue negative control passes on both. No fix was applied or tested.

require 'spec_helper'
require 'rails'
require 'action_dispatch'
require 'rack/mock'
require 'action_cable'
require 'posthog/rails'

class ReviewCableError < StandardError; end
class ReviewCableConnection < ActionCable::Connection::Base
  attr_accessor :handled_error
  rescue_from ReviewCableError do |error|
    self.handled_error = error
  end
end
class ReviewCableChannel < ActionCable::Channel::Base
  def speak(data)
    raise ReviewCableError, 'action failed'
  end
end
class ReviewLocalRescueChannel < ActionCable::Channel::Base
  rescue_from ReviewCableError do |_error| end
  def speak(data)
    raise ReviewCableError, 'handled locally'
  end
end

RSpec.describe 'Action Cable review regressions' do
  let(:client) { PostHog::Client.new(api_key: 'review-test', test_mode: true) }
  let(:server) do
    ActionCable::Server::Base.new.tap do |value|
      value.config.logger = Logger.new(File::NULL)
      value.config.filter_parameters = [:credential]
    end
  end
  let(:connection) { ReviewCableConnection.new(server, Rack::MockRequest.env_for('/cable')) }
  before do
    PostHog.client = client
    PostHog::Rails.config = PostHog::Rails::Configuration.new
    PostHog::Rails.config.auto_capture_exceptions = true
    allow(Rails).to receive(:application).and_return(
      double('application', env_config: {}, config: double(root: nil, filter_parameters: [:password]))
    )
    initializer = PostHog::Rails::Railtie.initializers.find { |item| item.name == 'posthog.action_cable' }
    initializer&.run(Rails.application)
  end
  after do
    client.shutdown
    PostHog.client = nil
  end

  it 'redacts secrets configured on Action Cable only' do
    channel = ReviewCableChannel.new(connection, 'id', {'credential' => 'subscription-secret'})
    expect(channel.send(:parameter_filter).filter('credential' => 'action-secret')).to eq('credential' => '[FILTERED]')
    expect { channel.perform_action('action' => 'speak', 'credential' => 'action-secret', 'password' => 'app-secret') }.to raise_error(ReviewCableError)
    if ENV['REVIEW_SNAPSHOT'] == 'base'
      expect(client.queued_messages).to eq(0)
      puts 'BASE privacy: no Cable event emitted'
    else
      properties = client.dequeue_last_message[:properties]
      puts "HEAD privacy emitted: #{properties.slice('$channel_params', '$action_data').inspect}"
      expect(properties['$action_data']['password']).to eq('[FILTERED]')
      aggregate_failures do
        expect(properties['$channel_params']['credential']).to eq('[FILTERED]')
        expect(properties['$action_data']['credential']).to eq('[FILTERED]')
      end
    end
  end

  it 'waits for connection rescue_from before deciding to capture' do
    expect(PostHog::Rails.config.report_rescued_exceptions).to eq(false)
    identifier = {'channel' => 'ReviewCableChannel'}.to_json
    connection.subscriptions.execute_command('command' => 'subscribe', 'identifier' => identifier)
    expect(client.queued_messages).to eq(0)
    connection.subscriptions.execute_command('command' => 'message', 'identifier' => identifier,
                                             'data' => {'action' => 'speak'}.to_json)
    expect(connection.handled_error).to be_a(ReviewCableError)
    count = client.queued_messages
    puts "#{ENV['REVIEW_SNAPSHOT']} connection handler ran; queued=#{count}"
    if count > 0
      puts "emitted mechanism=#{client.dequeue_last_message[:properties]['$exception_list'].first['mechanism'].inspect}"
    end
    expect(count).to eq(0)
  end

  it 'does not capture an action consumed by a channel-local handler' do
    ReviewLocalRescueChannel.new(connection, 'local').perform_action('action' => 'speak')
    expect(client.queued_messages).to eq(0)
  end
end

enrich_current_span: false) do
yield
rescue StandardError => e
capture_exception(e, env)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Wait for connection rescue handling before reporting an unhandled failure

IMPORTANT · REQUIRED · introduced
Location: posthog-rails/lib/posthog/rails/action_cable.rb:24–26 — inline line 25, RIGHT.

When a connection's application rescue_from consumes a channel exception, this wrapper still queues it as unhandled. Expected: no automatic event under the documented application-consumed behavior and default report_rescued_exceptions = false.

This is a supported application pattern:

class ApplicationCable::Connection < ActionCable::Connection::Base
  rescue_from ExpectedCableError do |error|
    handle_expected_failure(error)
  end
end

For a channel action raising ExpectedCableError, the ordinary dispatch sequence is:

  1. The channel raises.
  2. The new wrapper captures with handled: false, then re-raises.
  3. The enclosing Rails subscription dispatcher invokes the connection handler, which consumes the error.

Executed regression: Successfully subscribe and send a failing message through real Subscriptions#execute_command, verify the connection handler ran, then assert:

expect(PostHog::Rails.config.report_rescued_exceptions).to eq(false)
expect(connection.handled_error).to be_a(ReviewCableError)
expect(client.queued_messages).to eq(0)

Base passes with zero events. Head fails with one event whose mechanism is:

{ 'type' => 'middleware', 'handled' => false, 'exception_id' => 0 }

A channel-local rescue_from control passes on both revisions, isolating the missed enclosing handler. Application handling still works: the impact is unsolicited capture and incorrect error-tracking classification, not an application crash.

This contradicts the new README's application-consumed noncapture promise and existing rescued-exception configuration documentation. The exception-metadata contract distinguishes consumed from escaping failures; it does not independently mandate suppression of every handled event. Suppression here follows this integration's declared policy. Backend Cymbal derives $exception_handled from mechanism disposition, making the misclassification observable downstream.

Consider preserving operation context while deferring the reporting decision until applicable application rescue handling completes. Preserve genuinely unhandled capture, original errors/returns and single handler invocation. If reporting despite later connection handling is intentional, explicitly resolve that policy and make classification/documentation accurate. The executed base/head regression fixture and results are included below; no proposed fix was tested.

Executed regression fixture and base/head results

Runtime: Ruby 3.4.7, Rails/Action Cable 7.2.3.1, JSON 2.21.2, equivalent dependencies and configuration. Execution used explicit gem activation and Ruby/RSpec (ruby -Ilib:posthog-rails/lib:spec -rrspec/autorun regression_spec.rb --format documentation) in an isolated, network-disabled environment. REVIEW_SNAPSHOT=base selects the base control; REVIEW_SNAPSHOT=head selects head assertions.

Base 58f5335f895ebebc417bb4a48d22351ff96b5fb0: 3 examples, 0 failures, exit 0. Head 72ba70aa210dad9f0bdb258e5825824d58a28fbc: 3 examples, 2 failed examples, exit 1. The failed examples are the two behaviors reviewed here; the channel-local rescue negative control passes on both. No fix was applied or tested.

require 'spec_helper'
require 'rails'
require 'action_dispatch'
require 'rack/mock'
require 'action_cable'
require 'posthog/rails'

class ReviewCableError < StandardError; end
class ReviewCableConnection < ActionCable::Connection::Base
  attr_accessor :handled_error
  rescue_from ReviewCableError do |error|
    self.handled_error = error
  end
end
class ReviewCableChannel < ActionCable::Channel::Base
  def speak(data)
    raise ReviewCableError, 'action failed'
  end
end
class ReviewLocalRescueChannel < ActionCable::Channel::Base
  rescue_from ReviewCableError do |_error| end
  def speak(data)
    raise ReviewCableError, 'handled locally'
  end
end

RSpec.describe 'Action Cable review regressions' do
  let(:client) { PostHog::Client.new(api_key: 'review-test', test_mode: true) }
  let(:server) do
    ActionCable::Server::Base.new.tap do |value|
      value.config.logger = Logger.new(File::NULL)
      value.config.filter_parameters = [:credential]
    end
  end
  let(:connection) { ReviewCableConnection.new(server, Rack::MockRequest.env_for('/cable')) }
  before do
    PostHog.client = client
    PostHog::Rails.config = PostHog::Rails::Configuration.new
    PostHog::Rails.config.auto_capture_exceptions = true
    allow(Rails).to receive(:application).and_return(
      double('application', env_config: {}, config: double(root: nil, filter_parameters: [:password]))
    )
    initializer = PostHog::Rails::Railtie.initializers.find { |item| item.name == 'posthog.action_cable' }
    initializer&.run(Rails.application)
  end
  after do
    client.shutdown
    PostHog.client = nil
  end

  it 'redacts secrets configured on Action Cable only' do
    channel = ReviewCableChannel.new(connection, 'id', {'credential' => 'subscription-secret'})
    expect(channel.send(:parameter_filter).filter('credential' => 'action-secret')).to eq('credential' => '[FILTERED]')
    expect { channel.perform_action('action' => 'speak', 'credential' => 'action-secret', 'password' => 'app-secret') }.to raise_error(ReviewCableError)
    if ENV['REVIEW_SNAPSHOT'] == 'base'
      expect(client.queued_messages).to eq(0)
      puts 'BASE privacy: no Cable event emitted'
    else
      properties = client.dequeue_last_message[:properties]
      puts "HEAD privacy emitted: #{properties.slice('$channel_params', '$action_data').inspect}"
      expect(properties['$action_data']['password']).to eq('[FILTERED]')
      aggregate_failures do
        expect(properties['$channel_params']['credential']).to eq('[FILTERED]')
        expect(properties['$action_data']['credential']).to eq('[FILTERED]')
      end
    end
  end

  it 'waits for connection rescue_from before deciding to capture' do
    expect(PostHog::Rails.config.report_rescued_exceptions).to eq(false)
    identifier = {'channel' => 'ReviewCableChannel'}.to_json
    connection.subscriptions.execute_command('command' => 'subscribe', 'identifier' => identifier)
    expect(client.queued_messages).to eq(0)
    connection.subscriptions.execute_command('command' => 'message', 'identifier' => identifier,
                                             'data' => {'action' => 'speak'}.to_json)
    expect(connection.handled_error).to be_a(ReviewCableError)
    count = client.queued_messages
    puts "#{ENV['REVIEW_SNAPSHOT']} connection handler ran; queued=#{count}"
    if count > 0
      puts "emitted mechanism=#{client.dequeue_last_message[:properties]['$exception_list'].first['mechanism'].inspect}"
    end
    expect(count).to eq(0)
  end

  it 'does not capture an action consumed by a channel-local handler' do
    ReviewLocalRescueChannel.new(connection, 'local').perform_action('action' => 'speak')
    expect(client.queued_messages).to eq(0)
  end
end

@dustinbyrne
dustinbyrne requested a review from a team November 6, 2026 17:50

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants