Lightweight Sentry client with no dependencies.
This package is an alternative to the official PHP client for Sentry.
The library consists of a client class, an event-model model that matches the shape of the Sentry API, and an interface for extensions.
The API deviates from the Unified API recommendation - our goal is to log detailed exceptions, and capture log-entries leading up to those exceptions, with as little coupling and dependency on the client as possible.
With most members declared as protected
, you can further extend and override/enhance various aspects
of exception/error/request-processing with simple code that modifies the (fully type-hinted) model.
This client has most of the features of the official client plus some extras.
All features are opt-in, and the package ships with the following extensions:
EnvironmentReporter
: Reports PHP and OS versions, server name, site name, etc.RequestReporter
: Reports details about the (PSR-7) HTTP Request.ExceptionReporter
: Provides detailed stack-traces with source-code context, paths/filenames, line-numbers, etc.ClientSniffer
: ParsesUser-Agent
for client (browser or bot) name/version/OS and adds useful tags.ClientIPDetector
: ParsesX-Forwarded-For
andForwarded
headers for User IP logging behind proxies.BreadcrumbLogger
: Reports PSR-3 log-events as "breadcrumbs".
Non-features:
- No built-in error-handler: your framework/stack/app probably has one, and this client should be very easy to integrate just about anywhere.
- No post-data recording: scrubbing/sanitization is unreliable. (if you're willing to take the risk, the fields are there in the model, and you can implement your own extension.)
Most modern frameworks by now have some sort of DI container and an error-handler.
To avoid getting caught up in the specifics of various frameworks, in this section, we'll demonstrate how to bootstrap and integrate the client independently of any specific framework.
To bootstrap the client itself, you need a Sentry DSN,
an EventCapture
implementation, and a your choice of extensions - this example uses DirectEventCapture
and most of the built-in extensions:
$client = new SentryClient(
new DirectEventCapture(
new DSN("https://[email protected]/1234567"),
"tcp://proxy.example.com:5100" // optional: required if you're behind a proxy
),
[
new EnvironmentReporter(),
new RequestReporter(),
new ExceptionReporter(),
new ClientSniffer(),
new ClientIPDetector(),
]
);
$client->sample_rate = 50; // optional: percentage of calls to `captureException()` to actually capture
Some extensions support additional options, which will be described in the Configuration section.
To capture PHP errors, we'll need to add an error-handler that maps errors to instances of the
built-in ErrorException
class.
Note that most frameworks and error-handlers already have something like this built-in - an existing
error-handler likely is designed to be the only global error-handler, and will likely offer an abstraction
and some sort of API over set_error_handler()
.
The following simplified error-handler throws for all error-levels except E_NOTICE
and E_WARNING
, which it
silently captures to the $client
instance we created above:
set_error_handler(
function ($severity, $message, $file, $line) use ($client) {
$error = new ErrorException($message, 0, $severity, $file, $line);
if ($severity & (E_ALL & ~E_NOTICE & ~E_WARNING)) {
throw $error;
} else {
$client->captureException($error);
}
},
);
Now that we have ErrorException
being thrown for PHP errors, we can handle any error/exception
consistently with a try
/catch
-statement surrounding any statements in the top-level script:
try {
// ... dispatch your router or middleware-stack, etc. ...
} catch (Throwable $error) {
$client->captureException($error);
// ... render an apology page for disappointed users ...
}
We now have basic error-handling and silent capture of warnings and notices.
In a PSR-15 middleware context, we can capture useful additional
information about the ServerRequestInterface
instance that initiated the error - typically, we'll do this
with a middleware at the top of the middleware-stack, which will delegate to the next middleware from
a try
/catch
-block.
Here's a basic example of an anonymous middleware being added to an array of middlewares:
$middlewares = [
new class ($client) implements MiddlewareInterface {
/**
* @var SentryClient
*/
private $client;
public function __construct(SentryClient $client)
{
$this->client = $client;
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
try {
return $handler->handle($request);
} catch (Throwable $error) {
$this->client->captureException($error, $request);
// ... return an apology response for disappointed users ...
}
}
},
// ... the rest of your middleware stack ...
];
Any error caught by this middleware won't bubble to our first try
/catch
-statement in the
top-level script - instead, the call to captureException()
from the middleware includes both the
$error
and the $request
(which doesn't exist yet in the top-level script) from which the client
can capture a lot more detail.
In addition, your application may log incidental useful information (such as database queries) during normal operation - on failure, this information can help you diagnose the events leading up to an error.
These log entries can be captured as so-called "breadcrumbs" using the provided BreadcrumbLogger
,
which is both a PSR-3 LoggerInterface
implementation and
a SentryClientExtension
.
You might want to bootstrap this as your primary logger - maybe you only care about log-entries that
lead to an error condition. However, it's more likely you'll want another logger to send log-events to
a persisted log, as well as recording them as breadcrumbs. (For example, you may try the
monolog/monolog
package, which allows you to log to
any combination of PSR-3 and monolog-handlers, filter the log-entries, and so on - or
mouf/utils.log.psr.multi-logger
,
if you prefer something simple.)
Note that log-entries will buffer in the logger until you capture an exception - if your
application is a CLI script handling many requests, you will need to manually call clear()
on the logger instance at the end of a successful request, since otherwise log-entries will
accumulate across requests.
Notice: PHP 7.4 introduced a new ini setting (zend.exception_ignore_args
) for excluding arguments from stack traces.
If you want arguments to be passed on to Sentry you will need to turn this setting off.
A few of the extensions support optional constructor arguments to configure some optional features.
The constructor accepts two optional argument:
$root_path
- if specified, the project root-path will be removed from visible filenames in stack-traces.$max_string_length
- specifies the maximum length at which reported PHP values will be truncated.$filters
- specifies one or more filename patterns to be filtered from stack-traces.
In addition, the public $error_levels
property lets you customize how PHP error-levels map to
Sentry severity-levels. The default configuration matches that of the official 2.0 client.
If your client base uses rare or exotic clients, you can add your own regular expression patterns to
to the public $browser_patterns
and $os_patterns
properties to enhance the browser classifications.
The default configuration will recognize most common browsers and versions, operating systems and bots.
Replace or add to $user_ip_headers
to detect client IP addresses in unusual environments.
The built-in patterns support most ordinary cache/proxy-servers.
The default $log_levels
match those of the official (2.0) client - if needed, you can
override with custom mappings from PSR-5 log-level to Sentry severity-level.
A buffered EventCapture
implementation is available.
If you're capturing exceptions/errors only, you probably don't need this - but if you're mapping
warnings and notices to ErrorException
with an error-handler, you may wish to avoid blocking the
response while posting to Sentry.
You can accomplish this using the BufferedEventCapture
decorator, which allows you to defer the
actual HTTP request(s) until after you've sent the response to the user.
In this case, you will need to bootstrap the capture and client instances separately:
$buffer = new BufferedEventCapture(
new DirectEventCapture(
new DSN("https://[email protected]/1234567")
),
);
$client = new SentryClient($buffer);
Since Events will now get buffered rather than directly posted to Sentry, you will need to manually flush the Events at the of the request.
In a typical PHP FCGI SAPI-environment, you can accomplish this by explicitly closing the HTTP
response immediately before flushing the events to Sentry - for example, from an index.php
file:
register_shutdown_function(function () use ($buffer) {
fastcgi_finish_request();
$buffer->flush();
});
The client class and extensions were designed with project-specific extensions in mind and
contain many protected
methods designed for you to extend and override their behavior.
Using protected
overrides, you can customize how events get created and captured, how
exceptions and requests get processed, client IP detection and filtering, how PHP values are
formatted in stack-traces, and many other details.
Please refer to the source-code for available protected
methods - and note that we're
committed to versioning this package semantically: any breaking changes to protected
methods
will be versioned as major releases.
Openly opinionated fluff:
Version 1.x of the official Sentry PHP client is dated, and not a good fit for a modern (PSR-7/15) application stack.
Version 2.0 (currently in development) is an architectural masterpiece - which is not what we need/want from something that's going to essentially collect some data and perform a simple JSON POST request.
Specifically:
- We don't need an error-handler - every modern application stack has one already.
- We don't want a complex architecture and custom middleware - simple functions will do.
- We don't want to record post-data - there are too many risks.
- Fewer lines of code ~> fewer bugs (hopefully; you don't want the error-logger itself to break down.)
- No dependencies: no conflicts, no fuss.
We want something simple, fast and transparent.
We also insist on code with good IDE support, which provides better insight for someone reading/modifying the
code, and reduces the potential for silent errors - the official client juggles array
values, whereas our
model formally describes the JSON body-shape with a plain PHP class-hierarchy that implements JsonSerializable
.
Note that we only model the portion of the JSON body shape that makes sense in a PHP context - if you find something to be missing or incorrect, pull-requests are more than welcome.