Improve Sentry instrumentation, coroutine isolation and performance
This improves Sentry's data collection, tracing and behavior in long-lived workers. It brings the integration up to date with the released PHP SDK, fixes missing or incorrectly attributed telemetry, and reduces repeated work in request collection and scope access.
Data collection and configuration
- Require
sentry/sentry ^4.34and support itsdata_collectionpolicy across requests, responses, user information, database queries, Redis commands, queued jobs and console input. Leaving the option unset preserves the existing collection settings. Command arguments and options are filtered by name instead of exposing the raw command line under the new policy. - Honor
default_integrations, callable integration overrides, custom transports, HTTP clients and loggers. The asynchronous pool is the default transport; explicit SDK configuration takes precedence. A send without a DSN or Spotlight returns before borrowing a transport or starting a coroutine. - Keep the configurable feature list, use the upstream integration class names, and combine Redis instrumentation with
CacheIntegrationwhile retaining independent switches. Disabled features avoid their recording work; inactive Sentry registers no instrumentation listeners, middleware, Guzzle aspect, coroutine propagation hook or Blade decorator. - Fix
sentry:publishhandling of exported, spaced, multiline and empty dotenv assignments. An empty DSN no longer skips the prompt. Explicitly captured validation exceptions follow the application'signore_exceptionssetting; the framework exception handler still excludes them from automatic reporting.
Correct tracing and coroutine lifetimes
- Record each outgoing Guzzle transfer once when it settles, using its final prepared request and response. Redirects previously produced duplicate breadcrumbs and could finish spans with the first response. Rejected HTTP responses now retain their status and response data. The caller's
on_statscallback still runs on every hop, and rejections pass through unchanged. This covers the HTTP client, direct Guzzle clients and SDKs built on Guzzle. - Add configurable HTTP origin capture. Synchronous origin lookup runs only above the configured duration threshold; asynchronous calls capture their caller before it returns. Classify generated AOP frames using their original source so vendor proxies aren't mistaken for application code.
- Preserve parent scopes and spans across nested synchronous jobs and notifications. A job's sampling middleware can only resample the transaction that job started, so it cannot drop an enclosing request or parent-job trace. Failed-job context remains available for exception reporting.
- Keep bulk cache spans open until their last key result. Nested failover reads and writes no longer finish their enclosing span early; whole-operation failures still finish immediately.
- Keep scheduled check-in IDs in coroutine context instead of application log context, and copy the finishing check-in so asynchronous delivery cannot change the earlier in-progress event.
- Let child events inherit request information without replacing the application's request context or cloning its request. Keep scopes isolated, add the SDK's attachment support and callback-failure behavior, and let coroutine cancellation propagate.
- Remove request-level bootstrap spans: workers boot before requests arrive, so those spans had incorrect timestamps. Avoid eagerly decoding JSON bodies and opening uploaded files while converting requests; moving an upload no longer causes event capture to fail. Preserve sensitive-cookie filtering and numeric cookie/header names.
- Check the SDK body policy before reading request content. Excluded bodies are not materialized, and eligible content is read once and reused for request conversion and legacy JSON parsing.
Worker startup and related fixes
Sentry sends boot-time events and buffered logs once before the server forks. The master does not keep SDK shared connections for its workers to inherit, and workers restore the configured sharing behavior. Resolved logging channels are closed and forgotten before the fork so each worker creates its own handlers.
Sentry log channels and the deprecation channel are derived again from each worker's rebuilt configuration. Previously, literal copies of boot-time settings could overwrite the worker's settings. Explicit application channels remain intact.
The same HTTP completion correction applies to Telescope's client-request watcher. Sentry, Telescope and the HTTP client share rejected-response extraction for Guzzle 7 and 8. The exception renderer and Telescope also use the AOP source mapping. Process lifecycle log observers now accept the dispatcher's event-name/payload arguments rather than throwing before the process runs.
Performance and resource usage
The retained optimizations use direct cookie-name comparisons, construct valid PSR-7 headers together instead of cloning per header, and avoid repeated coroutine-context lookups and scope-stack copies. They add no request cache or worker-lifetime lookup registry.
Request collection, in microseconds per call:
| Case | Before optimization | After | Reduction |
|---|---|---|---|
| Legacy GET | 28.5 | 20.7 | 27% |
| Legacy JSON POST, about 2 KB | 53.7 | 45.4 | 15% |
data_collection GET | 27.8 | 19.4 | 30% |
data_collection JSON POST, about 2 KB | 32.0 | 23.7 | 26% |
An isolated Hub::getSpan() comparison measured 0.446 → 0.210 µs. Copying a populated scope into a child coroutine measured 5.49 → 2.96 µs between the original and final packages.
For the fully sampled mixed HTTP workload at concurrency 32:
| Metric | Original package | Final package |
|---|---|---|
| Total server CPU per request, including work after the response | 2,815 µs | 2,769 µs |
| Requests/sec | 393 | 400 |
| p50 latency | 80.74 ms | 79.38 ms |
| p99 latency | 98.58 ms | 95.12 ms |
| PHP heap after the complete workload and collection | 11.15 MiB | 11.17 MiB |
The CPU reduction is 1.6%. These latency figures include queueing in a single worker driven to capacity. Small differences in other configurations were within run-to-run variation. Fully sampled tracing still has a substantial absolute cost: in this mixed fixture it adds about 1,484 µs CPU per request over excluding Sentry, while errors-only adds about 267 µs. Sampling remains important at high traffic levels.
Correct outgoing HTTP observation costs more than the previous incomplete recording:
| Guzzle case, including the underlying operation | Original | Final |
|---|---|---|
| Explicit instrumentation opt-out | 77.4 µs | 77.4 µs |
| Sentry inactive | 76.9 µs | 76.0 µs |
| Sampled synchronous request | 141.4 µs | 153.6 µs |
| Sampled asynchronous request | 143.5 µs | 155.8 µs |
| Pending sampled async request, default origin capture | 11,471 bytes | 18,991 bytes |
The approximately 12 µs increase works out to 0.06 CPU cores at 5,000 instrumented calls/sec and 0.12 at 10,000 calls/sec. These are calculations from isolated timings, not measured throughput at those rates, and exclude transaction serialization and delivery. The extra memory is about 7.34 KiB per outstanding sampled request, released when that work finishes. With 100 ms average duration, those rates imply about 3.6 MiB and 7.2 MiB extra pending memory respectively. Disabling origin capture avoids about 3.67 KiB per pending request in this fixture while preserving completion recording.
Removing the completion observer would lose final responses, redirects and rejection information. Capturing async origins at completion cannot recover a caller that has already returned. Both paths remain; synchronous origins are threshold-gated and origin capture can be disabled.
Transport saturation stayed bounded: after repeated waves, both revisions returned to ten idle connections with no borrowed connections, waiting borrowers or pending sends. Every application request succeeded. The deliberately slow sink received only about 11% of attempted telemetry on both revisions; this is a bounded transport, not a lossless queue. Separate checks verify that delivery resumes after saturation.
Measurements used an idle six-vCPU machine, PHP 8.4.25, Swoole 6.3.0RC1, matching dependencies, independent production-only apps, OPcache enabled and JIT disabled. Tables use medians of three alternating process pairs. The committed harness and results include source revisions, workloads, variation, memory checks and measurement limits. Later fixture dependency-injection and post-saturation checks were smoke-tested; the historical timings are identified separately. The tables also predate the bulk-cache counter and excluded-body read corrections, which were verified with regression tests.
Documentation and verification
Update the Sentry integration guide, Laravel porting guidance, package differences and upstream tracking notes. The guide covers configuration, instrumentation, OTLP correlation, child coroutines and transport behavior, and links to Sentry's documentation for general SDK usage.
Profiling remains disabled by default. The documentation warns that the current profiler can attribute other coroutines' work to a transaction; adoption of the proposed released SDK/Excimer APIs is tracked separately. This does not affect error reporting or tracing.
Reconcile applicable upstream source and test coverage, preserving Hypervel's coroutine and transport tests. Add regression coverage for the corrected lifetimes, configuration, request data, redirects, failures and cleanup. Validation includes affected package suites, formatting and source/type-fixture analysis, with Guzzle 7 and 8 compatibility coverage in CI.
[!NOTE]
Improve Sentry instrumentation with data-collection policies and coroutine isolation
- Replaces the legacy
send_default_pii behavior with a configurable data_collection policy in sentry.php. The policy now governs user info, cookies, headers, request/response bodies, SQL query parameters, queue payloads, and console command input, with sensitive-value filtering throughout.
- Moves per-request and per-job state into
CoroutineContext for coroutine isolation: queue jobs use nested job frames, scheduled check-ins store their state per coroutine, and notification spans use an owner stack so terminal events only finish the matching span. See QueueIntegration.php and NotificationsIntegration.php.
- Rewrites Guzzle instrumentation in GuzzleHttpClientAspect.php to record spans and breadcrumbs when the request promise settles, including rejected requests and redirects, with optional slow-request origin tracing (250ms default threshold).
- Reworks request conversion in HypervelRequestFetcher.php: manual PSR-7 construction replaces the Symfony bridge, with policy-controlled bodies, parsed-body selection by media type, and uploaded-file conversion.
- Skips Sentry sends immediately when no DSN and no Spotlight are configured in HttpPoolTransport.php, and builds pooled transports from the
ClientBuilder in HttpTransportPool.php.
- Removes the process-wide bootstrap timestamp and bootstrap span from HTTP request transactions in Middleware.php, replacing them with request/response data collection during termination.
- Behavioral Change: feature classes are renamed to integrations (
QueueFeature → QueueIntegration, LogFeature → LogIntegration, etc.), send_default_pii is reported as IGNORED when data collection is configured, the Sentry SDK constraint moves from ^4.32 to ^4.34, and symfony/psr-http-message-bridge is replaced by vlucas/phpdotenv in composer.json.
Macroscope summarized b6550a3.