├── packer.sh ├── src ├── DateTime │ ├── ClockInterface.php │ ├── Clock.php │ └── Date.php ├── Request │ ├── ResolverInterface.php │ ├── RequestInterface.php │ ├── NullRequest.php │ ├── ConsoleRequest.php │ ├── PhpRequest.php │ └── BasicResolver.php ├── Callbacks │ ├── EnvironmentData.php │ ├── GlobalMetaData.php │ ├── RequestUser.php │ ├── RequestContext.php │ ├── CustomUser.php │ ├── RequestMetaData.php │ ├── RequestCookies.php │ └── RequestSession.php ├── Shutdown │ ├── PhpShutdownStrategy.php │ └── ShutdownStrategyInterface.php ├── FeatureDataStore.php ├── Middleware │ ├── NotificationSkipper.php │ ├── BreadcrumbData.php │ ├── DiscardClasses.php │ ├── CallbackBridge.php │ └── SessionData.php ├── Internal │ ├── FeatureFlagDelegate.php │ └── GuzzleCompat.php ├── FeatureFlag.php ├── Pipeline.php ├── Env.php ├── Utils.php ├── Breadcrumbs │ ├── Recorder.php │ └── Breadcrumb.php ├── ErrorTypes.php ├── Stacktrace.php ├── Handler.php ├── HttpClient.php ├── SessionTracker.php ├── Configuration.php ├── Report.php └── Client.php ├── utility └── bugsnag-prepend.php ├── LICENSE.txt ├── composer.json └── ARCHITECTURE.md /packer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | rm -rf build vendor composer.lock 4 | composer install -o -n --prefer-dist 5 | php -d phar.readonly=false packager.php 6 | -------------------------------------------------------------------------------- /src/DateTime/ClockInterface.php: -------------------------------------------------------------------------------- 1 | setMetaData(['Environment' => $_ENV]); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Shutdown/PhpShutdownStrategy.php: -------------------------------------------------------------------------------- 1 | shouldIgnoreErrorCode($lastError['type'])) { 11 | return; 12 | } 13 | $report = Bugsnag\Report::fromPHPError( 14 | $client->getConfig(), 15 | $lastError['type'], 16 | $lastError['message'], 17 | $lastError['file'], 18 | $lastError['line'], 19 | true 20 | ); 21 | $report->setSeverity('error'); 22 | $report->setUnhandled(true); 23 | $report->setSeverityReason([ 24 | 'type' => 'unhandledException', 25 | ]); 26 | $client->notify($report); 27 | $client->flush(); 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /src/Request/RequestInterface.php: -------------------------------------------------------------------------------- 1 | config = $config; 27 | } 28 | 29 | /** 30 | * Execute the global meta data callback. 31 | * 32 | * @param \Bugsnag\Report $report the bugsnag report instance 33 | * 34 | * @return void 35 | */ 36 | public function __invoke(Report $report) 37 | { 38 | if ($data = $this->config->getMetaData()) { 39 | $report->setMetaData($data); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Callbacks/RequestUser.php: -------------------------------------------------------------------------------- 1 | resolver = $resolver; 27 | } 28 | 29 | /** 30 | * Execute the request user callback. 31 | * 32 | * @param \Bugsnag\Report $report the bugsnag report instance 33 | * 34 | * @return void 35 | */ 36 | public function __invoke(Report $report) 37 | { 38 | if ($id = $this->resolver->resolve()->getUserId()) { 39 | $report->setUser(['id' => $id]); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Callbacks/RequestContext.php: -------------------------------------------------------------------------------- 1 | resolver = $resolver; 27 | } 28 | 29 | /** 30 | * Execute the request context callback. 31 | * 32 | * @param \Bugsnag\Report $report the bugsnag report instance 33 | * 34 | * @return void 35 | */ 36 | public function __invoke(Report $report) 37 | { 38 | if ($context = $this->resolver->resolve()->getContext()) { 39 | $report->setContext($context); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Callbacks/CustomUser.php: -------------------------------------------------------------------------------- 1 | resolver = $resolver; 27 | } 28 | 29 | /** 30 | * Execute the user data callback. 31 | * 32 | * @param \Bugsnag\Report $report the bugsnag report instance 33 | * 34 | * @return void 35 | */ 36 | public function __invoke(Report $report) 37 | { 38 | $resolver = $this->resolver; 39 | 40 | try { 41 | if ($user = $resolver()) { 42 | $report->setUser($user); 43 | } 44 | } catch (Exception $e) { 45 | // Ignore any errors. 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Callbacks/RequestMetaData.php: -------------------------------------------------------------------------------- 1 | resolver = $resolver; 27 | } 28 | 29 | /** 30 | * Execute the request meta data callback. 31 | * 32 | * @param \Bugsnag\Report $report the bugsnag report instance 33 | * 34 | * @return void 35 | */ 36 | public function __invoke(Report $report) 37 | { 38 | if ($data = $this->resolver->resolve()->getMetaData()) { 39 | $report->setMetaData($data); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Callbacks/RequestCookies.php: -------------------------------------------------------------------------------- 1 | resolver = $resolver; 27 | } 28 | 29 | /** 30 | * Execute the request cookies callback. 31 | * 32 | * @param \Bugsnag\Report $report the bugsnag report instance 33 | * 34 | * @return void 35 | */ 36 | public function __invoke(Report $report) 37 | { 38 | if ($data = $this->resolver->resolve()->getCookies()) { 39 | $report->setMetaData(['cookies' => $data]); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Callbacks/RequestSession.php: -------------------------------------------------------------------------------- 1 | resolver = $resolver; 27 | } 28 | 29 | /** 30 | * Execute the request session callback. 31 | * 32 | * @param \Bugsnag\Report $report the bugsnag report instance 33 | * 34 | * @return void 35 | */ 36 | public function __invoke(Report $report) 37 | { 38 | if ($data = $this->resolver->resolve()->getSession()) { 39 | $report->setMetaData(['session' => $data]); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Bugsnag 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /src/FeatureDataStore.php: -------------------------------------------------------------------------------- 1 | $featureFlags 25 | * 26 | * @return void 27 | */ 28 | public function addFeatureFlags(array $featureFlags); 29 | 30 | /** 31 | * Remove a single feature flag by name. 32 | * 33 | * @param string $name 34 | * 35 | * @return void 36 | */ 37 | public function clearFeatureFlag($name); 38 | 39 | /** 40 | * Remove all feature flags. 41 | * 42 | * @return void 43 | */ 44 | public function clearFeatureFlags(); 45 | } 46 | -------------------------------------------------------------------------------- /src/DateTime/Date.php: -------------------------------------------------------------------------------- 1 | now(); 21 | 22 | return self::format($date); 23 | } 24 | 25 | /** 26 | * @param DateTimeImmutable $date 27 | * 28 | * @return string 29 | */ 30 | private static function format(DateTimeImmutable $date) 31 | { 32 | $dateTime = $date->format('Y-m-d\TH:i:s'); 33 | 34 | // The milliseconds format character ("v") was introduced in PHP 7.0, so 35 | // we need to take microseconds (PHP 5.2+) and convert to milliseconds 36 | $microseconds = $date->format('u'); 37 | $milliseconds = substr($microseconds, 0, 3); 38 | 39 | $offset = $date->format('P'); 40 | 41 | return "{$dateTime}.{$milliseconds}{$offset}"; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Middleware/NotificationSkipper.php: -------------------------------------------------------------------------------- 1 | config = $config; 27 | } 28 | 29 | /** 30 | * Execute the notification skipper middleware. 31 | * 32 | * @param \Bugsnag\Report $report the bugsnag report instance 33 | * @param callable $next the next stage callback 34 | * 35 | * @return void 36 | */ 37 | public function __invoke(Report $report, callable $next) 38 | { 39 | if (!$this->config->shouldNotify()) { 40 | return; 41 | } 42 | 43 | $next($report); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Middleware/BreadcrumbData.php: -------------------------------------------------------------------------------- 1 | recorder = $recorder; 27 | } 28 | 29 | /** 30 | * Execute the breadcrumb data middleware. 31 | * 32 | * @param \Bugsnag\Report $report the bugsnag report instance 33 | * @param callable $next the next stage callback 34 | * 35 | * @return void 36 | */ 37 | public function __invoke(Report $report, callable $next) 38 | { 39 | foreach ($this->recorder as $breadcrumb) { 40 | $report->addBreadcrumb($breadcrumb); 41 | } 42 | 43 | $next($report); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Shutdown/ShutdownStrategyInterface.php: -------------------------------------------------------------------------------- 1 | config = $config; 21 | } 22 | 23 | /** 24 | * @param \Bugsnag\Report $report 25 | * @param callable $next 26 | * 27 | * @return void 28 | */ 29 | public function __invoke(Report $report, callable $next) 30 | { 31 | $errors = $report->getErrors(); 32 | 33 | foreach ($this->config->getDiscardClasses() as $discardClass) { 34 | foreach ($errors as $error) { 35 | if ($error['errorClass'] === $discardClass 36 | || @preg_match($discardClass, $error['errorClass']) === 1 37 | ) { 38 | syslog(LOG_INFO, sprintf( 39 | 'Discarding event because error class "%s" matched discardClasses configuration', 40 | $error['errorClass'] 41 | )); 42 | 43 | return; 44 | } 45 | } 46 | } 47 | 48 | $next($report); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bugsnag/bugsnag", 3 | "type": "library", 4 | "description": "Official Bugsnag notifier for PHP applications.", 5 | "keywords": ["bugsnag", "exceptions", "errors", "logging", "tracking"], 6 | "homepage": "https://github.com/bugsnag/bugsnag-php", 7 | "license": "MIT", 8 | "authors": [{ 9 | "name": "James Smith", 10 | "email": "notifiers@bugsnag.com", 11 | "homepage": "https://bugsnag.com" 12 | }], 13 | "require": { 14 | "php": ">=5.5", 15 | "composer/ca-bundle": "^1.0", 16 | "guzzlehttp/guzzle": "^5.0|^6.0|^7.0" 17 | }, 18 | "require-dev": { 19 | "guzzlehttp/psr7": "^1.3|^2.0", 20 | "mtdowling/burgomaster": "dev-master#72151eddf5f0cf101502b94bf5031f9c53501a04", 21 | "phpunit/phpunit": "^4.8.36|^7.5.15|^9.3.10", 22 | "php-mock/php-mock-phpunit": "^1.1|^2.1", 23 | "sebastian/version": ">=1.0.3" 24 | }, 25 | "autoload": { 26 | "psr-4" : { 27 | "Bugsnag\\" : "src/" 28 | } 29 | }, 30 | "autoload-dev": { 31 | "psr-4" : { 32 | "Bugsnag\\Tests\\" : "tests/" 33 | } 34 | }, 35 | "extra": { 36 | "branch-alias": { 37 | "dev-master": "3.20-dev" 38 | } 39 | }, 40 | "scripts": { 41 | "test": "vendor/bin/phpunit" 42 | }, 43 | "minimum-stability": "dev", 44 | "prefer-stable": true 45 | } 46 | -------------------------------------------------------------------------------- /src/Middleware/CallbackBridge.php: -------------------------------------------------------------------------------- 1 | callback = $callback; 26 | } 27 | 28 | /** 29 | * Execute the add callback bridge middleware. 30 | * 31 | * @param \Bugsnag\Report $report the bugsnag report instance 32 | * @param callable $next the next stage callback 33 | * 34 | * @return void 35 | */ 36 | public function __invoke(Report $report, callable $next) 37 | { 38 | $initialUnhandled = $report->getUnhandled(); 39 | $initialSeverity = $report->getSeverity(); 40 | $initialReason = $report->getSeverityReason(); 41 | 42 | $callback = $this->callback; 43 | 44 | if ($callback($report) !== false) { 45 | $report->setUnhandled($initialUnhandled); 46 | if ($report->getSeverity() != $initialSeverity) { 47 | // Severity has been changed via callbacks -> severity reason should be userCallbackSetSeverity 48 | $report->setSeverityReason([ 49 | 'type' => 'userCallbackSetSeverity', 50 | ]); 51 | } else { 52 | // Otherwise we ensure the original severity reason is preserved 53 | $report->setSeverityReason($initialReason); 54 | } 55 | 56 | $next($report); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Middleware/SessionData.php: -------------------------------------------------------------------------------- 1 | client = $client; 30 | $this->sessionTracker = $client->getSessionTracker(); 31 | } 32 | 33 | /** 34 | * Attaches session information to the Report, if the SessionTracker has a 35 | * current session. Note that this is not the same as the PHP session, but 36 | * refers to the current request. 37 | * 38 | * If the SessionTracker does not have a current session, the report will 39 | * not be changed. 40 | * 41 | * @param \Bugsnag\Report $report 42 | * @param callable $next 43 | * 44 | * @return void 45 | */ 46 | public function __invoke(Report $report, callable $next) 47 | { 48 | $session = $this->sessionTracker->getCurrentSession(); 49 | 50 | if (isset($session['events'])) { 51 | if ($report->getUnhandled()) { 52 | $session['events']['unhandled'] += 1; 53 | } else { 54 | $session['events']['handled'] += 1; 55 | } 56 | 57 | $report->setSessionData($session); 58 | $this->sessionTracker->setCurrentSession($session); 59 | } 60 | 61 | $next($report); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Internal/FeatureFlagDelegate.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | private $storage = []; 17 | 18 | /** 19 | * @param string $name 20 | * @param string|null $variant 21 | * 22 | * @return void 23 | */ 24 | public function add($name, $variant) 25 | { 26 | // ensure we're not about to add a duplicate flag 27 | $this->remove($name); 28 | 29 | $this->storage[] = new FeatureFlag($name, $variant); 30 | } 31 | 32 | /** 33 | * @param FeatureFlag[] $featureFlags 34 | * @phpstan-param list $featureFlags 35 | * 36 | * @return void 37 | */ 38 | public function merge(array $featureFlags) 39 | { 40 | foreach ($featureFlags as $flag) { 41 | if ($flag instanceof FeatureFlag) { 42 | $this->remove($flag->getName()); 43 | 44 | $this->storage[] = $flag; 45 | } 46 | } 47 | } 48 | 49 | /** 50 | * @param string $name 51 | * 52 | * @return void 53 | */ 54 | public function remove($name) 55 | { 56 | foreach ($this->storage as $index => $flag) { 57 | if ($flag->getName() === $name) { 58 | unset($this->storage[$index]); 59 | 60 | // reindex the array to prevent holes 61 | $this->storage = array_values($this->storage); 62 | 63 | break; 64 | } 65 | } 66 | } 67 | 68 | /** 69 | * @return void 70 | */ 71 | public function clear() 72 | { 73 | $this->storage = []; 74 | } 75 | 76 | /** 77 | * Get the list of stored feature flags as an array. 78 | * 79 | * @return \Bugsnag\FeatureFlag[] 80 | */ 81 | public function toArray() 82 | { 83 | return $this->storage; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/FeatureFlag.php: -------------------------------------------------------------------------------- 1 | name = $name; 28 | 29 | // ensure the variant can only be null or a string as the API only 30 | // accepts strings (null values will be omitted from the payload) 31 | if ($variant !== null && !is_string($variant)) { 32 | $json = json_encode($variant); 33 | 34 | // if JSON encoding fails, omit the variant 35 | $variant = $json === false ? null : $json; 36 | } 37 | 38 | $this->variant = $variant; 39 | } 40 | 41 | /** 42 | * Get the feature flag's name. 43 | * 44 | * @return string 45 | */ 46 | public function getName() 47 | { 48 | return $this->name; 49 | } 50 | 51 | /** 52 | * Get the feature flag's variant. 53 | * 54 | * @return string|null 55 | */ 56 | public function getVariant() 57 | { 58 | return $this->variant; 59 | } 60 | 61 | /** 62 | * Convert this feature flag into the format used by the Bugsnag Event API. 63 | * 64 | * This has two forms, either with a variant: 65 | * { "featureFlag": "name", "variant": "variant" } 66 | * 67 | * or if the feature flag has no variant: 68 | * { "featureFlag": "no variant" } 69 | * 70 | * @return array[] 71 | * @phpstan-return array{featureFlag: string, variant?: string} 72 | */ 73 | public function toArray() 74 | { 75 | if (is_string($this->variant)) { 76 | return ['featureFlag' => $this->name, 'variant' => $this->variant]; 77 | } 78 | 79 | return ['featureFlag' => $this->name]; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Internal/GuzzleCompat.php: -------------------------------------------------------------------------------- 1 | =') 21 | && version_compare($version, '6.0.0', '<'); 22 | } 23 | 24 | return false; 25 | } 26 | 27 | /** 28 | * Get the base URL/URI option name, which depends on the Guzzle version. 29 | * 30 | * @return string 31 | */ 32 | public static function getBaseUriOptionName() 33 | { 34 | return self::isUsingGuzzle5() ? 'base_url' : 'base_uri'; 35 | } 36 | 37 | /** 38 | * Get the base URL/URI, which depends on the Guzzle version. 39 | * 40 | * @param GuzzleHttp\ClientInterface $guzzle 41 | * 42 | * @return mixed 43 | */ 44 | public static function getBaseUri(GuzzleHttp\ClientInterface $guzzle) 45 | { 46 | // TODO: validate this by running PHPStan with Guzzle 5 47 | return self::isUsingGuzzle5() 48 | ? $guzzle->getBaseUrl() // @phpstan-ignore-line 49 | : $guzzle->getConfig(self::getBaseUriOptionName()); 50 | } 51 | 52 | /** 53 | * Apply the given $requestOptions to the Guzzle $options array, if they are 54 | * not already set. 55 | * 56 | * The layout of request options differs in Guzzle 5 to 6/7; in Guzzle 5 57 | * request options live in a 'defaults' array, but in 6/7 they are in the 58 | * top level 59 | * 60 | * @param array $options 61 | * @param array $requestOptions 62 | * 63 | * @return array 64 | */ 65 | public static function applyRequestOptions(array $options, array $requestOptions) 66 | { 67 | if (self::isUsingGuzzle5()) { 68 | if (!isset($options['defaults'])) { 69 | $options['defaults'] = []; 70 | } 71 | 72 | foreach ($requestOptions as $key => $value) { 73 | if (!isset($options['defaults'][$key])) { 74 | $options['defaults'][$key] = $value; 75 | } 76 | } 77 | 78 | return $options; 79 | } 80 | 81 | foreach ($requestOptions as $key => $value) { 82 | if (!isset($options[$key])) { 83 | $options[$key] = $value; 84 | } 85 | } 86 | 87 | return $options; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Request/ConsoleRequest.php: -------------------------------------------------------------------------------- 1 | command = $command; 24 | } 25 | 26 | /** 27 | * Are we currently processing a request? 28 | * 29 | * @return bool 30 | */ 31 | public function isRequest() 32 | { 33 | return false; 34 | } 35 | 36 | /** 37 | * Get the session data. 38 | * 39 | * @return array 40 | */ 41 | public function getSession() 42 | { 43 | return []; 44 | } 45 | 46 | /** 47 | * Get the cookies. 48 | * 49 | * @return array 50 | */ 51 | public function getCookies() 52 | { 53 | return []; 54 | } 55 | 56 | /** 57 | * Get the request formatted as meta data. 58 | * 59 | * @return array 60 | */ 61 | public function getMetaData() 62 | { 63 | if (count($this->command) == 0) { 64 | return ['console' => [ 65 | 'Command' => 'Command could not be retrieved', ], 66 | ]; 67 | } 68 | $commandString = implode(' ', $this->command); 69 | $primaryCommand = $this->command[0]; 70 | $arguments = []; 71 | $options = []; 72 | foreach (array_slice($this->command, 1) as $arg) { 73 | if (isset($arg[0]) && $arg[0] === '-') { 74 | $options[] = $arg; 75 | } else { 76 | $arguments[] = $arg; 77 | } 78 | } 79 | $data = [ 80 | 'Input' => $commandString, 81 | 'Command' => $primaryCommand, 82 | 'Arguments' => $arguments, 83 | 'Options' => $options, 84 | ]; 85 | 86 | return ['console' => $data]; 87 | } 88 | 89 | /** 90 | * Get the request context. 91 | * 92 | * @return string|null 93 | */ 94 | public function getContext() 95 | { 96 | return implode(' ', array_slice($this->command, 0, 4)); 97 | } 98 | 99 | /** 100 | * Get the request user id. 101 | * 102 | * @return string|null 103 | */ 104 | public function getUserId() 105 | { 106 | return null; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Pipeline.php: -------------------------------------------------------------------------------- 1 | pipes = $pipes; 24 | } 25 | 26 | /** 27 | * Append the given pipe to the pipeline. 28 | * 29 | * @param callable $pipe a new pipe to pass through 30 | * 31 | * @return $this 32 | */ 33 | public function pipe(callable $pipe) 34 | { 35 | $this->pipes[] = $pipe; 36 | 37 | return $this; 38 | } 39 | 40 | /** 41 | * Add a pipe to the pipeline before a given class. 42 | * 43 | * @param callable $pipe a new pipe to pass through 44 | * @param string $beforeClass to class to insert the pipe before 45 | * 46 | * @return $this 47 | */ 48 | public function insertBefore(callable $pipe, $beforeClass) 49 | { 50 | $beforePosition = null; 51 | foreach ($this->pipes as $index => $callable) { 52 | $class = get_class($callable); 53 | if ($class === $beforeClass) { 54 | $beforePosition = $index; 55 | break; 56 | } 57 | } 58 | if ($beforePosition === null) { 59 | $this->pipes[] = $pipe; 60 | } else { 61 | array_splice($this->pipes, $beforePosition, 0, [$pipe]); 62 | } 63 | 64 | return $this; 65 | } 66 | 67 | /** 68 | * Run the pipeline. 69 | * 70 | * @param mixed $passable the item to send through the pipeline 71 | * @param callable $destination the final distination callback 72 | * 73 | * @return mixed 74 | */ 75 | public function execute($passable, callable $destination) 76 | { 77 | $first = function ($passable) use ($destination) { 78 | return call_user_func($destination, $passable); 79 | }; 80 | 81 | $pipes = array_reverse($this->pipes); 82 | 83 | return call_user_func(array_reduce($pipes, $this->getSlice(), $first), $passable); 84 | } 85 | 86 | /** 87 | * Get the closure that represents a slice. 88 | * 89 | * @return \Closure 90 | */ 91 | protected function getSlice() 92 | { 93 | return function ($stack, $pipe) { 94 | return function ($passable) use ($stack, $pipe) { 95 | return call_user_func($pipe, $passable, $stack); 96 | }; 97 | }; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Env.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class Recorder implements Countable, Iterator 12 | { 13 | /** 14 | * The maximum number of breadcrumbs to store. 15 | * 16 | * @var int 17 | */ 18 | private $maxBreadcrumbs = 50; 19 | 20 | /** 21 | * The recorded breadcrumbs. 22 | * 23 | * @var \Bugsnag\Breadcrumbs\Breadcrumb[] 24 | */ 25 | private $breadcrumbs = []; 26 | 27 | /** 28 | * The iteration position. 29 | * 30 | * @var int 31 | */ 32 | private $position = 0; 33 | 34 | /** 35 | * Record a breadcrumb. 36 | * 37 | * @param \Bugsnag\Breadcrumbs\Breadcrumb $breadcrumb 38 | * 39 | * @return void 40 | */ 41 | public function record(Breadcrumb $breadcrumb) 42 | { 43 | $this->breadcrumbs[] = $breadcrumb; 44 | 45 | // drop the oldest breadcrumb if we're over the max 46 | if ($this->count() > $this->maxBreadcrumbs) { 47 | array_shift($this->breadcrumbs); 48 | } 49 | } 50 | 51 | /** 52 | * Clear all recorded breadcrumbs. 53 | * 54 | * @return void 55 | */ 56 | public function clear() 57 | { 58 | $this->position = 0; 59 | $this->breadcrumbs = []; 60 | } 61 | 62 | /** 63 | * Set the maximum number of breadcrumbs that are allowed to be stored. 64 | * 65 | * This must be an integer between 0 and 100 (inclusive). 66 | * 67 | * @param int $maxBreadcrumbs 68 | * 69 | * @return void 70 | */ 71 | public function setMaxBreadcrumbs($maxBreadcrumbs) 72 | { 73 | if (!is_int($maxBreadcrumbs) || $maxBreadcrumbs < 0 || $maxBreadcrumbs > 100) { 74 | error_log( 75 | 'Bugsnag Warning: maxBreadcrumbs should be an integer between 0 and 100 (inclusive)' 76 | ); 77 | 78 | return; 79 | } 80 | 81 | $this->maxBreadcrumbs = $maxBreadcrumbs; 82 | 83 | // drop the oldest breadcrumbs if we're over the max 84 | if ($this->count() > $this->maxBreadcrumbs) { 85 | $this->breadcrumbs = array_slice( 86 | $this->breadcrumbs, 87 | $this->count() - $this->maxBreadcrumbs 88 | ); 89 | } 90 | } 91 | 92 | /** 93 | * Get the maximum number of breadcrumbs that are allowed to be stored. 94 | * 95 | * @return int 96 | */ 97 | public function getMaxBreadcrumbs() 98 | { 99 | return $this->maxBreadcrumbs; 100 | } 101 | 102 | /** 103 | * Get the number of stored breadcrumbs. 104 | * 105 | * @return int 106 | */ 107 | #[\ReturnTypeWillChange] 108 | public function count() 109 | { 110 | return count($this->breadcrumbs); 111 | } 112 | 113 | /** 114 | * Get the current item. 115 | * 116 | * @return \Bugsnag\Breadcrumbs\Breadcrumb 117 | */ 118 | #[\ReturnTypeWillChange] 119 | public function current() 120 | { 121 | return $this->breadcrumbs[$this->position]; 122 | } 123 | 124 | /** 125 | * Get the current key. 126 | * 127 | * @return int 128 | */ 129 | #[\ReturnTypeWillChange] 130 | public function key() 131 | { 132 | return $this->position; 133 | } 134 | 135 | /** 136 | * Advance the key position. 137 | * 138 | * @return void 139 | */ 140 | #[\ReturnTypeWillChange] 141 | public function next() 142 | { 143 | $this->position++; 144 | } 145 | 146 | /** 147 | * Rewind the key position. 148 | * 149 | * @return void 150 | */ 151 | #[\ReturnTypeWillChange] 152 | public function rewind() 153 | { 154 | $this->position = 0; 155 | } 156 | 157 | /** 158 | * Is the current key position set? 159 | * 160 | * @return bool 161 | */ 162 | #[\ReturnTypeWillChange] 163 | public function valid() 164 | { 165 | return $this->position < $this->count(); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/Request/PhpRequest.php: -------------------------------------------------------------------------------- 1 | server = $server; 56 | $this->session = $session; 57 | $this->cookies = $cookies; 58 | $this->headers = $headers; 59 | $this->input = $input; 60 | } 61 | 62 | /** 63 | * Are we currently processing a request? 64 | * 65 | * @return bool 66 | */ 67 | public function isRequest() 68 | { 69 | return true; 70 | } 71 | 72 | /** 73 | * Get the session data. 74 | * 75 | * @return array 76 | */ 77 | public function getSession() 78 | { 79 | return $this->session; 80 | } 81 | 82 | /** 83 | * Get the cookies. 84 | * 85 | * @return array 86 | */ 87 | public function getCookies() 88 | { 89 | return $this->cookies; 90 | } 91 | 92 | /** 93 | * Get the request formatted as meta data. 94 | * 95 | * @return array 96 | */ 97 | public function getMetaData() 98 | { 99 | $data = []; 100 | 101 | $data['url'] = $this->getCurrentUrl(); 102 | 103 | if (isset($this->server['REQUEST_METHOD'])) { 104 | $data['httpMethod'] = $this->server['REQUEST_METHOD']; 105 | } 106 | 107 | $data['params'] = $this->input; 108 | 109 | $data['clientIp'] = $this->getRequestIp(); 110 | 111 | if (isset($this->server['HTTP_USER_AGENT'])) { 112 | $data['userAgent'] = $this->server['HTTP_USER_AGENT']; 113 | } 114 | 115 | if ($this->headers) { 116 | $data['headers'] = $this->headers; 117 | } 118 | 119 | return ['request' => $data]; 120 | } 121 | 122 | /** 123 | * Get the request context. 124 | * 125 | * @return string|null 126 | */ 127 | public function getContext() 128 | { 129 | if (isset($this->server['REQUEST_METHOD']) && isset($this->server['REQUEST_URI'])) { 130 | return $this->server['REQUEST_METHOD'] . ' ' . strtok($this->server['REQUEST_URI'], '?'); 131 | } 132 | 133 | return null; 134 | } 135 | 136 | /** 137 | * Get the request user id. 138 | * 139 | * @return string|null 140 | */ 141 | public function getUserId() 142 | { 143 | return $this->getRequestIp(); 144 | } 145 | 146 | /** 147 | * Get the request url. 148 | * 149 | * @return string 150 | */ 151 | protected function getCurrentUrl() 152 | { 153 | $schema = ((!empty($this->server['HTTPS']) && $this->server['HTTPS'] !== 'off') || (!empty($this->server['SERVER_PORT']) && $this->server['SERVER_PORT'] == 443)) ? 'https://' : 'http://'; 154 | 155 | $host = isset($this->server['HTTP_HOST']) ? $this->server['HTTP_HOST'] : 'localhost'; 156 | 157 | return $schema . $host . $this->server['REQUEST_URI']; 158 | } 159 | 160 | /** 161 | * Get the request ip. 162 | * 163 | * @return string|null 164 | */ 165 | protected function getRequestIp() 166 | { 167 | if (isset($this->server['HTTP_X_FORWARDED_FOR'])) { 168 | return $this->server['HTTP_X_FORWARDED_FOR']; 169 | } 170 | 171 | if (isset($this->server['REMOTE_ADDR'])) { 172 | return $this->server['REMOTE_ADDR']; 173 | } 174 | 175 | return null; 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/Breadcrumbs/Breadcrumb.php: -------------------------------------------------------------------------------- 1 | '; 118 | } else { 119 | $metaData['BreadcrumbError'] = 'Breadcrumb name must be a string - '.gettype($name).' provided instead'; 120 | $name = ''; 121 | } 122 | } elseif ($name === '') { 123 | $metaData['BreadcrumbError'] = 'Empty string provided as the breadcrumb name'; 124 | $name = ''; 125 | } 126 | 127 | $types = static::getTypes(); 128 | 129 | if (!in_array($type, $types, true)) { 130 | throw new InvalidArgumentException(sprintf('The breadcrumb type must be one of the set of %d standard types.', count($types))); 131 | } 132 | 133 | $this->timestamp = Date::now(); 134 | $this->name = $name; 135 | $this->type = $type; 136 | $this->metaData = $metaData; 137 | } 138 | 139 | /** 140 | * Get the breadcrumb as an array. 141 | * 142 | * Note that this is without the meta data. 143 | * 144 | * @return array 145 | */ 146 | public function toArray() 147 | { 148 | return [ 149 | 'timestamp' => $this->timestamp, 150 | 'name' => $this->name, 151 | 'type' => $this->type, 152 | ]; 153 | } 154 | 155 | /** 156 | * Get the breadcrumb meta data. 157 | * 158 | * Note that this still needs sanitizing before use. 159 | * 160 | * @return array 161 | */ 162 | public function getMetaData() 163 | { 164 | return $this->metaData; 165 | } 166 | 167 | /** 168 | * Get the set of valid breadrum types. 169 | * 170 | * @return array 171 | */ 172 | public static function getTypes() 173 | { 174 | return [ 175 | static::NAVIGATION_TYPE, 176 | static::REQUEST_TYPE, 177 | static::PROCESS_TYPE, 178 | static::LOG_TYPE, 179 | static::USER_TYPE, 180 | static::STATE_TYPE, 181 | static::ERROR_TYPE, 182 | static::MANUAL_TYPE, 183 | ]; 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/Request/BasicResolver.php: -------------------------------------------------------------------------------- 1 | $value) { 66 | if (substr($name, 0, 5) == 'HTTP_') { 67 | $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; 68 | } elseif ($name === 'CONTENT_TYPE') { 69 | $headers['Content-Type'] = $value; 70 | } elseif ($name === 'CONTENT_LENGTH') { 71 | $headers['Content-Length'] = $value; 72 | } 73 | } 74 | 75 | return $headers; 76 | } 77 | 78 | /** 79 | * Get the input params. 80 | * 81 | * Note how we're caching this result for ever, across all instances. 82 | * 83 | * This is because the input stream can only be read once on PHP 5.5, and 84 | * PHP is natively only designed to process one request, then shutdown. 85 | * Some applications can be designed to handle multiple requests using 86 | * their own request objects, thus will need to implement their own bugsnag 87 | * request resolver. 88 | * 89 | * @param array $server the server variables 90 | * @param array $params the array of parameters for this request type 91 | * @param bool $fallbackToInput if true, uses input when params is null 92 | * 93 | * @return array|null 94 | */ 95 | protected static function getInputParams(array $server, array $params, $fallbackToInput = false) 96 | { 97 | static $result; 98 | 99 | if ($result !== null) { 100 | return $result ?: null; 101 | } 102 | 103 | $result = $params; 104 | 105 | if ($fallbackToInput === true) { 106 | $result = $result ?: static::parseInput($server, static::readInput()); 107 | } 108 | 109 | return $result ?: null; 110 | } 111 | 112 | /** 113 | * Read the PHP input stream. 114 | * 115 | * @return string|false 116 | */ 117 | protected static function readInput() 118 | { 119 | return file_get_contents('php://input') ?: false; 120 | } 121 | 122 | /** 123 | * Parse the given input string. 124 | * 125 | * @param array $server the server variables 126 | * @param string|null $input the http request input 127 | * 128 | * @return array|null 129 | */ 130 | protected static function parseInput(array $server, $input) 131 | { 132 | if (!$input) { 133 | return null; 134 | } 135 | 136 | if (isset($server['CONTENT_TYPE']) && stripos($server['CONTENT_TYPE'], 'application/json') === 0) { 137 | return (array) json_decode($input, true) ?: null; 138 | } 139 | 140 | if (strtoupper($server['REQUEST_METHOD']) === 'PUT') { 141 | parse_str($input, $params); 142 | 143 | return (array) $params ?: null; 144 | } 145 | 146 | return null; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/ErrorTypes.php: -------------------------------------------------------------------------------- 1 | [ 23 | 'name' => 'PHP Fatal Error', 24 | 'severity' => 'error', 25 | ], 26 | 27 | E_WARNING => [ 28 | 'name' => 'PHP Warning', 29 | 'severity' => 'warning', 30 | ], 31 | 32 | E_PARSE => [ 33 | 'name' => 'PHP Parse Error', 34 | 'severity' => 'error', 35 | ], 36 | 37 | E_NOTICE => [ 38 | 'name' => 'PHP Notice', 39 | 'severity' => 'info', 40 | ], 41 | 42 | E_CORE_ERROR => [ 43 | 'name' => 'PHP Core Error', 44 | 'severity' => 'error', 45 | ], 46 | 47 | E_CORE_WARNING => [ 48 | 'name' => 'PHP Core Warning', 49 | 'severity' => 'warning', 50 | ], 51 | 52 | E_COMPILE_ERROR => [ 53 | 'name' => 'PHP Compile Error', 54 | 'severity' => 'error', 55 | ], 56 | 57 | E_COMPILE_WARNING => [ 58 | 'name' => 'PHP Compile Warning', 59 | 'severity' => 'warning', 60 | ], 61 | 62 | E_USER_ERROR => [ 63 | 'name' => 'User Error', 64 | 'severity' => 'error', 65 | ], 66 | 67 | E_USER_WARNING => [ 68 | 'name' => 'User Warning', 69 | 'severity' => 'warning', 70 | ], 71 | 72 | E_USER_NOTICE => [ 73 | 'name' => 'User Notice', 74 | 'severity' => 'info', 75 | ], 76 | 77 | E_RECOVERABLE_ERROR => [ 78 | 'name' => 'PHP Recoverable Error', 79 | 'severity' => 'error', 80 | ], 81 | 82 | E_DEPRECATED => [ 83 | 'name' => 'PHP Deprecated', 84 | 'severity' => 'info', 85 | ], 86 | 87 | E_USER_DEPRECATED => [ 88 | 'name' => 'User Deprecated', 89 | 'severity' => 'info', 90 | ], 91 | ]; 92 | 93 | // Conditionally add E_STRICT if PHP version is below 8.4 94 | // https://php.watch/versions/8.4/E_STRICT-deprecated 95 | if (PHP_VERSION_ID < 80400) { 96 | static::$ERROR_TYPES[E_STRICT] = [ 97 | 'name' => 'PHP Strict', 98 | 'severity' => 'info', 99 | ]; 100 | } 101 | } 102 | 103 | /** 104 | * Get the error types map. 105 | * 106 | * @return array[] 107 | */ 108 | protected static function getErrorTypes() 109 | { 110 | if (static::$ERROR_TYPES === null) 111 | static::initializeErrorTypes(); 112 | return static::$ERROR_TYPES; 113 | } 114 | 115 | /** 116 | * Is the given error code fatal? 117 | * 118 | * @param int $code the error code 119 | * 120 | * @return bool 121 | */ 122 | public static function isFatal($code) 123 | { 124 | return static::getSeverity($code) === 'error'; 125 | } 126 | 127 | /** 128 | * Get the name of the given error code. 129 | * 130 | * @param int $code the error code 131 | * 132 | * @return string 133 | */ 134 | public static function getName($code) 135 | { 136 | $errorTypes = static::getErrorTypes(); 137 | 138 | if (array_key_exists($code, $errorTypes)) { 139 | return $errorTypes[$code]['name']; 140 | } 141 | 142 | return 'Unknown'; 143 | } 144 | 145 | /** 146 | * Get the severity of the given error code. 147 | * 148 | * @param int $code the error code 149 | * 150 | * @return string 151 | */ 152 | public static function getSeverity($code) 153 | { 154 | $errorTypes = static::getErrorTypes(); 155 | 156 | if (array_key_exists($code, $errorTypes)) { 157 | return $errorTypes[$code]['severity']; 158 | } 159 | 160 | return 'error'; 161 | } 162 | 163 | /** 164 | * Get the levels for the given severity. 165 | * 166 | * @param string $severity the given severity 167 | * 168 | * @return int 169 | */ 170 | public static function getLevelsForSeverity($severity) 171 | { 172 | $levels = 0; 173 | $errorTypes = static::getErrorTypes(); 174 | 175 | foreach ($errorTypes as $level => $info) { 176 | if ($info['severity'] == $severity) { 177 | $levels |= $level; 178 | } 179 | } 180 | 181 | return $levels; 182 | } 183 | 184 | /** 185 | * Get a list of all PHP error codes. 186 | * 187 | * @return int[] 188 | */ 189 | public static function getAllCodes() 190 | { 191 | return array_keys(static::getErrorTypes()); 192 | } 193 | 194 | /** 195 | * Convert the given error code to a string representation. 196 | * 197 | * For example, E_ERROR => 'E_ERROR'. 198 | * 199 | * @param int $code 200 | * 201 | * @return string 202 | */ 203 | public static function codeToString($code) 204 | { 205 | $map = [ 206 | E_ERROR => 'E_ERROR', 207 | E_WARNING => 'E_WARNING', 208 | E_PARSE => 'E_PARSE', 209 | E_NOTICE => 'E_NOTICE', 210 | E_CORE_ERROR => 'E_CORE_ERROR', 211 | E_CORE_WARNING => 'E_CORE_WARNING', 212 | E_COMPILE_ERROR => 'E_COMPILE_ERROR', 213 | E_COMPILE_WARNING => 'E_COMPILE_WARNING', 214 | E_USER_ERROR => 'E_USER_ERROR', 215 | E_USER_WARNING => 'E_USER_WARNING', 216 | E_USER_NOTICE => 'E_USER_NOTICE', 217 | E_RECOVERABLE_ERROR => 'E_RECOVERABLE_ERROR', 218 | E_DEPRECATED => 'E_DEPRECATED', 219 | E_USER_DEPRECATED => 'E_USER_DEPRECATED', 220 | ]; 221 | 222 | // Conditionally add E_STRICT if PHP version is below 8.4 223 | // https://php.watch/versions/8.4/E_STRICT-deprecated 224 | if (PHP_VERSION_ID < 80400) { 225 | $map[E_STRICT] = 'E_STRICT'; 226 | } 227 | 228 | return isset($map[$code]) ? $map[$code] : 'Unknown'; 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/Stacktrace.php: -------------------------------------------------------------------------------- 1 | addFrame($file, $line, '[unknown]'); 68 | 69 | return $stacktrace; 70 | } 71 | 72 | /** 73 | * Create a new stacktrace instance from a backtrace. 74 | * 75 | * @param \Bugsnag\Configuration $config the configuration instance 76 | * @param array $backtrace the associated backtrace 77 | * @param string $topFile the top file to use 78 | * @param int $topLine the top line to use 79 | * 80 | * @return static 81 | */ 82 | public static function fromBacktrace(Configuration $config, array $backtrace, $topFile, $topLine) 83 | { 84 | // @phpstan-ignore-next-line 85 | $stacktrace = new static($config); 86 | 87 | // PHP backtrace's are misaligned, we need to shift the file/line down a frame 88 | foreach ($backtrace as $frame) { 89 | if (!static::frameInsideBugsnag($frame)) { 90 | $stacktrace->addFrame( 91 | $topFile, 92 | $topLine, 93 | isset($frame['function']) ? $frame['function'] : null, 94 | isset($frame['class']) ? $frame['class'] : null 95 | ); 96 | } 97 | 98 | if (isset($frame['file']) && isset($frame['line'])) { 99 | $topFile = $frame['file']; 100 | $topLine = $frame['line']; 101 | } else { 102 | $topFile = '[internal]'; 103 | $topLine = 0; 104 | } 105 | } 106 | 107 | // Add a final stackframe for the "main" method 108 | $stacktrace->addFrame($topFile, $topLine, '[main]'); 109 | 110 | return $stacktrace; 111 | } 112 | 113 | /** 114 | * Does the given frame internally belong to bugsnag. 115 | * 116 | * @param array $frame the given frame to check 117 | * 118 | * @return bool 119 | */ 120 | public static function frameInsideBugsnag(array $frame) 121 | { 122 | return isset($frame['class']) && strpos($frame['class'], 'Bugsnag\\') === 0 && substr_count($frame['class'], '\\') === 1; 123 | } 124 | 125 | /** 126 | * Create a new stacktrace instance. 127 | * 128 | * @param \Bugsnag\Configuration $config the configuration instance 129 | * 130 | * @return void 131 | */ 132 | public function __construct(Configuration $config) 133 | { 134 | $this->config = $config; 135 | } 136 | 137 | /** 138 | * Get the array representation. 139 | * 140 | * @return array[] 141 | */ 142 | public function &toArray() 143 | { 144 | return $this->frames; 145 | } 146 | 147 | /** 148 | * Get the stacktrace frames. 149 | * 150 | * This is the same as calling toArray. 151 | * 152 | * @return array[] 153 | */ 154 | public function &getFrames() 155 | { 156 | return $this->frames; 157 | } 158 | 159 | /** 160 | * Add the given frame to the stacktrace. 161 | * 162 | * @param string $file the associated file 163 | * @param int $line the line number 164 | * @param string $method the method called 165 | * @param string|null $class the associated class 166 | * 167 | * @return void 168 | */ 169 | public function addFrame($file, $line, $method, $class = null) 170 | { 171 | // Account for special "filenames" in eval'd code 172 | $matches = []; 173 | if (preg_match("/^(.*?)\((\d+)\) : (?:eval\(\)'d code|runtime-created function)$/", $file, $matches)) { 174 | $file = $matches[1]; 175 | $line = $matches[2]; 176 | } 177 | 178 | // Construct the frame 179 | $frame = [ 180 | 'lineNumber' => (int) $line, 181 | 'method' => $class ? "$class::$method" : $method, 182 | ]; 183 | 184 | // Attach some lines of code for context 185 | if ($this->config->shouldSendCode()) { 186 | $frame['code'] = $this->getCode($file, $line, static::NUM_LINES); 187 | } 188 | 189 | // Check if this frame is inProject 190 | $frame['inProject'] = $this->config->isInProject($file); 191 | 192 | // Strip out projectRoot from start of file path 193 | $frame['file'] = $this->config->getStrippedFilePath($file); 194 | 195 | $this->frames[] = $frame; 196 | } 197 | 198 | /** 199 | * Remove the frame at the given index from the stacktrace. 200 | * 201 | * @param int $index 202 | * 203 | * @throws \InvalidArgumentException 204 | * 205 | * @return void 206 | */ 207 | public function removeFrame($index) 208 | { 209 | if (!isset($this->frames[$index])) { 210 | throw new InvalidArgumentException('Invalid frame index to remove.'); 211 | } 212 | 213 | array_splice($this->frames, $index, 1); 214 | } 215 | 216 | /** 217 | * Extract the code for the given file and lines. 218 | * 219 | * @param string $path the path to the file 220 | * @param int $line the line to centre about 221 | * @param int $numLines the number of lines to fetch 222 | * 223 | * @return string[]|null 224 | */ 225 | protected function getCode($path, $line, $numLines) 226 | { 227 | if (empty($path) || empty($line) || !file_exists($path)) { 228 | return null; 229 | } 230 | 231 | try { 232 | $file = new SplFileObject($path); 233 | $file->seek(PHP_INT_MAX); 234 | 235 | $bounds = static::getBounds($line, $numLines, $file->key() + 1); 236 | 237 | $code = []; 238 | 239 | $file->seek($bounds[0] - 1); 240 | while ($file->key() < $bounds[1]) { 241 | $code[$file->key() + 1] = rtrim(substr($file->current(), 0, static::MAX_LENGTH)); 242 | $file->next(); 243 | } 244 | 245 | return $code; 246 | } catch (RuntimeException $ex) { 247 | return null; 248 | } 249 | } 250 | 251 | /** 252 | * Get the start and end positions for the given line. 253 | * 254 | * @param int $line the line to centre about 255 | * @param int $num the number of lines to fetch 256 | * @param int $max the maximum line number 257 | * 258 | * @return int[] 259 | */ 260 | protected static function getBounds($line, $num, $max) 261 | { 262 | $start = max($line - floor($num / 2), 1); 263 | 264 | $end = $start + ($num - 1); 265 | 266 | if ($end > $max) { 267 | $end = $max; 268 | $start = max($end - ($num - 1), 1); 269 | } 270 | 271 | return [$start, $end]; 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /src/Handler.php: -------------------------------------------------------------------------------- 1 | registerBugsnagHandlers(true); 76 | 77 | return $handler; 78 | } 79 | 80 | /** 81 | * Register our handlers and preserve those previously registered. 82 | * 83 | * @param \Bugsnag\Client|string|null $client client instance or api key 84 | * 85 | * @return static 86 | * 87 | * @deprecated Use {@see Handler::register} instead. 88 | */ 89 | public static function registerWithPrevious($client = null) 90 | { 91 | return self::register($client); 92 | } 93 | 94 | /** 95 | * Register our handlers, optionally saving those previously registered. 96 | * 97 | * @param bool $callPrevious whether or not to call the previous handlers 98 | * 99 | * @return void 100 | */ 101 | protected function registerBugsnagHandlers($callPrevious) 102 | { 103 | $this->registerErrorHandler($callPrevious); 104 | $this->registerExceptionHandler($callPrevious); 105 | $this->registerShutdownHandler(); 106 | } 107 | 108 | /** 109 | * Register the bugsnag error handler and save the returned value. 110 | * 111 | * @param bool $callPrevious whether or not to call the previous handler 112 | * 113 | * @return void 114 | */ 115 | public function registerErrorHandler($callPrevious) 116 | { 117 | $previous = set_error_handler([$this, 'errorHandler']); 118 | 119 | if ($callPrevious) { 120 | $this->previousErrorHandler = $previous; 121 | } 122 | } 123 | 124 | /** 125 | * Register the bugsnag exception handler and save the returned value. 126 | * 127 | * @param bool $callPrevious whether or not to call the previous handler 128 | * 129 | * @return void 130 | */ 131 | public function registerExceptionHandler($callPrevious) 132 | { 133 | $previous = set_exception_handler([$this, 'exceptionHandler']); 134 | 135 | if (!$callPrevious) { 136 | return; 137 | } 138 | 139 | // If there is no previous exception handler, we create one that re-raises 140 | // the exception in order to trigger PHP's default exception handler 141 | if (!is_callable($previous)) { 142 | $previous = static function ($throwable) { 143 | throw $throwable; 144 | }; 145 | } 146 | 147 | $this->previousExceptionHandler = $previous; 148 | } 149 | 150 | /** 151 | * Register our shutdown handler. 152 | * 153 | * PHP will call shutdown functions in the order they were registered. 154 | * 155 | * @return void 156 | */ 157 | public function registerShutdownHandler() 158 | { 159 | // Reserve some memory that we can free in the shutdown handler 160 | $this->reservedMemory = str_repeat(' ', 1024 * 32); 161 | 162 | register_shutdown_function([$this, 'shutdownHandler']); 163 | } 164 | 165 | /** 166 | * Create a new exception handler instance. 167 | * 168 | * @param \Bugsnag\Client $client 169 | * 170 | * @return void 171 | */ 172 | public function __construct(Client $client) 173 | { 174 | $this->client = $client; 175 | } 176 | 177 | /** 178 | * Exception handler callback. 179 | * 180 | * @param Throwable $throwable the exception was was thrown 181 | * 182 | * @return void 183 | */ 184 | public function exceptionHandler($throwable) 185 | { 186 | $this->notifyThrowable($throwable); 187 | 188 | // If we don't have a previous handler to call, there's nothing left to do 189 | if (!$this->previousExceptionHandler) { 190 | return; 191 | } 192 | 193 | // These empty catches exist to set $exceptionFromPreviousHandler — we 194 | // support both PHP 5 & 7 so can't have a single Throwable catch 195 | try { 196 | call_user_func($this->previousExceptionHandler, $throwable); 197 | 198 | return; 199 | } catch (Throwable $exceptionFromPreviousHandler) { 200 | // TODO: if we drop support for PHP 5, we can remove this catch, which 201 | // fixes the PHPStan issue here 202 | // @phpstan-ignore-next-line 203 | } catch (Exception $exceptionFromPreviousHandler) { 204 | } 205 | 206 | // If the previous handler threw the same exception that we are currently 207 | // handling then it's trying to force PHP's native exception handler to run 208 | // In this case we disable our shutdown handler (to avoid reporting it 209 | // twice) and re-throw the exception 210 | if ($throwable === $exceptionFromPreviousHandler) { 211 | self::$enableShutdownHandler = false; 212 | 213 | throw $throwable; 214 | } 215 | 216 | // The previous handler raised a new exception so send a notification 217 | // for it too. We don't want the previous handler to run for this 218 | // exception, as it may keep throwing new exceptions 219 | $this->notifyThrowable($exceptionFromPreviousHandler); 220 | } 221 | 222 | /** 223 | * Send a notification for the given throwable. 224 | * 225 | * @param Throwable $throwable 226 | * 227 | * @return void 228 | */ 229 | private function notifyThrowable($throwable) 230 | { 231 | $report = Report::fromPHPThrowable( 232 | $this->client->getConfig(), 233 | $throwable 234 | ); 235 | 236 | $report->setSeverity('error'); 237 | $report->setUnhandled(true); 238 | $report->setSeverityReason(['type' => 'unhandledException']); 239 | 240 | $this->client->notify($report); 241 | } 242 | 243 | /** 244 | * Error handler callback. 245 | * 246 | * @param int $errno the level of the error raised 247 | * @param string $errstr the error message 248 | * @param string $errfile the filename that the error was raised in 249 | * @param int $errline the line number the error was raised at 250 | * 251 | * @return bool 252 | */ 253 | public function errorHandler($errno, $errstr, $errfile = '', $errline = 0) 254 | { 255 | if (!$this->client->getConfig()->shouldIgnoreErrorCode($errno)) { 256 | $report = Report::fromPHPError( 257 | $this->client->getConfig(), 258 | $errno, 259 | $errstr, 260 | $errfile, 261 | $errline, 262 | false 263 | ); 264 | 265 | $report->setUnhandled(true); 266 | $report->setSeverityReason([ 267 | 'type' => 'unhandledError', 268 | 'attributes' => [ 269 | 'errorType' => ErrorTypes::getName($errno), 270 | ], 271 | ]); 272 | 273 | $this->client->notify($report); 274 | } 275 | 276 | if ($this->previousErrorHandler) { 277 | return call_user_func( 278 | $this->previousErrorHandler, 279 | $errno, 280 | $errstr, 281 | $errfile, 282 | $errline 283 | ); 284 | } 285 | 286 | return false; 287 | } 288 | 289 | /** 290 | * Shutdown handler callback. 291 | * 292 | * @return void 293 | */ 294 | public function shutdownHandler() 295 | { 296 | // Free the reserved memory to give ourselves some room to work 297 | $this->reservedMemory = null; 298 | 299 | // If we're disabled, do nothing. This avoids reporting twice if the 300 | // exception handler is forcing the native PHP handler to run 301 | if (!self::$enableShutdownHandler) { 302 | return; 303 | } 304 | 305 | $lastError = error_get_last(); 306 | 307 | // If this is an OOM and memory increase is enabled, bump the memory 308 | // limit so we can report it 309 | if ($lastError !== null 310 | && $this->client->getMemoryLimitIncrease() !== null 311 | && preg_match($this->oomRegex, $lastError['message'], $matches) === 1 312 | ) { 313 | $currentMemoryLimit = (int) $matches[1]; 314 | $newMemoryLimit = $currentMemoryLimit + $this->client->getMemoryLimitIncrease(); 315 | 316 | ini_set('memory_limit', (string) $newMemoryLimit); 317 | } 318 | 319 | // Check if a fatal error caused this shutdown 320 | if (!is_null($lastError) && ErrorTypes::isFatal($lastError['type']) && !$this->client->getConfig()->shouldIgnoreErrorCode($lastError['type'])) { 321 | $report = Report::fromPHPError( 322 | $this->client->getConfig(), 323 | $lastError['type'], 324 | $lastError['message'], 325 | $lastError['file'], 326 | $lastError['line'], 327 | true 328 | ); 329 | 330 | $report->setSeverity('error'); 331 | $report->setUnhandled(true); 332 | $report->setSeverityReason([ 333 | 'type' => 'unhandledException', 334 | ]); 335 | 336 | $this->client->notify($report); 337 | } 338 | 339 | // Flush any buffered errors 340 | $this->client->flush(); 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /src/HttpClient.php: -------------------------------------------------------------------------------- 1 | config = $config; 68 | $this->guzzle = $guzzle; 69 | 70 | // substitute invalid UTF-8 characters when possible (PHP 7.2+) 71 | if (defined('JSON_INVALID_UTF8_SUBSTITUTE')) { 72 | $this->jsonEncodeFlags |= JSON_INVALID_UTF8_SUBSTITUTE; 73 | } 74 | } 75 | 76 | /** 77 | * Add a report to the queue. 78 | * 79 | * @param \Bugsnag\Report $report 80 | * 81 | * @return void 82 | */ 83 | public function queue(Report $report) 84 | { 85 | $this->queue[] = $report; 86 | } 87 | 88 | /** 89 | * Notify Bugsnag of a deployment. 90 | * 91 | * @param array $data the deployment information 92 | * 93 | * @return void 94 | * 95 | * @deprecated Use {@see self::sendBuildReport} instead. 96 | */ 97 | public function deploy(array $data) 98 | { 99 | $app = $this->config->getAppData(); 100 | 101 | $data['releaseStage'] = $app['releaseStage']; 102 | 103 | if (isset($app['version'])) { 104 | $data['appVersion'] = $app['version']; 105 | } 106 | 107 | $data['apiKey'] = $this->config->getApiKey(); 108 | 109 | $uri = rtrim($this->config->getNotifyEndpoint(), '/').'/deploy'; 110 | 111 | $this->post($uri, ['json' => $data]); 112 | } 113 | 114 | /** 115 | * Notify Bugsnag of a build. 116 | * 117 | * @param array $buildInfo the build information 118 | * 119 | * @return void 120 | */ 121 | public function sendBuildReport(array $buildInfo) 122 | { 123 | $app = $this->config->getAppData(); 124 | 125 | if (!isset($app['version'])) { 126 | error_log('Bugsnag Warning: App version is not set. Unable to send build report.'); 127 | 128 | return; 129 | } 130 | 131 | $data = ['appVersion' => $app['version']]; 132 | 133 | $sourceControl = []; 134 | 135 | if (isset($buildInfo['repository'])) { 136 | $sourceControl['repository'] = $buildInfo['repository']; 137 | } 138 | 139 | if (isset($buildInfo['provider'])) { 140 | $sourceControl['provider'] = $buildInfo['provider']; 141 | } 142 | 143 | if (isset($buildInfo['revision'])) { 144 | $sourceControl['revision'] = $buildInfo['revision']; 145 | } 146 | 147 | if (!empty($sourceControl)) { 148 | $data['sourceControl'] = $sourceControl; 149 | } 150 | 151 | if (isset($buildInfo['builder'])) { 152 | $data['builderName'] = $buildInfo['builder']; 153 | } else { 154 | $data['builderName'] = Utils::getBuilderName(); 155 | } 156 | 157 | if (isset($buildInfo['buildTool'])) { 158 | $data['buildTool'] = $buildInfo['buildTool']; 159 | } else { 160 | $data['buildTool'] = 'bugsnag-php'; 161 | } 162 | 163 | $data['releaseStage'] = $app['releaseStage']; 164 | $data['apiKey'] = $this->config->getApiKey(); 165 | 166 | $this->post($this->config->getBuildEndpoint(), ['json' => $data]); 167 | } 168 | 169 | /** 170 | * Deliver everything on the queue to Bugsnag. 171 | * 172 | * @return void 173 | * 174 | * @deprecated Use {HttpClient::sendEvents} instead. 175 | */ 176 | public function send() 177 | { 178 | $this->sendEvents(); 179 | } 180 | 181 | /** 182 | * Deliver everything on the queue to Bugsnag. 183 | * 184 | * @return void 185 | */ 186 | public function sendEvents() 187 | { 188 | if (!$this->queue) { 189 | return; 190 | } 191 | 192 | $this->deliverEvents( 193 | $this->config->getNotifyEndpoint(), 194 | $this->getEventPayload() 195 | ); 196 | 197 | $this->queue = []; 198 | } 199 | 200 | /** 201 | * Build the request data to send. 202 | * 203 | * @return array 204 | * 205 | * @deprecated Use {@see HttpClient::getEventPayload} instead. 206 | */ 207 | protected function build() 208 | { 209 | return $this->getEventPayload(); 210 | } 211 | 212 | /** 213 | * Get the event payload to send. 214 | * 215 | * @return array 216 | */ 217 | protected function getEventPayload() 218 | { 219 | $events = []; 220 | 221 | foreach ($this->queue as $report) { 222 | $event = $report->toArray(); 223 | 224 | if ($event) { 225 | $events[] = $event; 226 | } 227 | } 228 | 229 | return [ 230 | 'apiKey' => $this->config->getApiKey(), 231 | 'notifier' => $this->config->getNotifier(), 232 | 'events' => $events, 233 | ]; 234 | } 235 | 236 | /** 237 | * Send a session data payload to Bugsnag. 238 | * 239 | * @param array $payload 240 | * 241 | * @return void 242 | */ 243 | public function sendSessions(array $payload) 244 | { 245 | $this->post( 246 | $this->config->getSessionEndpoint(), 247 | [ 248 | 'json' => $payload, 249 | 'headers' => $this->getHeaders(self::SESSION_PAYLOAD_VERSION), 250 | ] 251 | ); 252 | } 253 | 254 | /** 255 | * Builds the array of headers to send. 256 | * 257 | * @param string $version The payload version to use. This defaults to the 258 | * notify payload version if not given. The default 259 | * value should not be relied upon and will be removed 260 | * in the next major release. 261 | * 262 | * @return array 263 | */ 264 | protected function getHeaders($version = self::NOTIFY_PAYLOAD_VERSION) 265 | { 266 | return [ 267 | 'Bugsnag-Api-Key' => $this->config->getApiKey(), 268 | 'Bugsnag-Sent-At' => Date::now(), 269 | 'Bugsnag-Payload-Version' => $version, 270 | 'Content-Type' => 'application/json', 271 | ]; 272 | } 273 | 274 | /** 275 | * Send a POST request to Bugsnag. 276 | * 277 | * @param string $uri the uri to hit 278 | * @param array $options the request options 279 | * 280 | * @return void 281 | */ 282 | protected function post($uri, array $options = []) 283 | { 284 | if (GuzzleCompat::isUsingGuzzle5()) { 285 | // TODO: validate this by running PHPStan with Guzzle 5 286 | // @phpstan-ignore-next-line 287 | $this->guzzle->post($uri, $options); 288 | } else { 289 | $this->guzzle->request('POST', $uri, $options); 290 | } 291 | } 292 | 293 | /** 294 | * Deliver the given events to the notification API. 295 | * 296 | * @param string $uri the uri to hit 297 | * @param array $data the data send 298 | * 299 | * @return void 300 | * 301 | * @deprecated Use {HttpClient::deliverEvents} instead 302 | */ 303 | protected function postJson($uri, array $data) 304 | { 305 | $this->deliverEvents($uri, $data); 306 | } 307 | 308 | /** 309 | * Deliver the given events to the notification API. 310 | * 311 | * @param string $uri the uri to hit 312 | * @param array $data the data send 313 | * 314 | * @return void 315 | */ 316 | protected function deliverEvents($uri, array $data) 317 | { 318 | // Try to send the whole lot, or without the meta data for the first 319 | // event. If failed, try to send the first event, and then the rest of 320 | // them, recursively. Decrease by a constant and concquer if you like. 321 | // Note that the base case is satisfied as soon as the payload is small 322 | // enought to send, or when it's simply discarded. 323 | try { 324 | $normalized = $this->normalize($data); 325 | } catch (RuntimeException $e) { 326 | if (count($data['events']) > 1) { 327 | $event = array_shift($data['events']); 328 | 329 | $this->deliverEvents($uri, array_merge($data, ['events' => [$event]])); 330 | $this->deliverEvents($uri, $data); 331 | } else { 332 | error_log('Bugsnag Warning: '.$e->getMessage()); 333 | } 334 | 335 | return; 336 | } 337 | 338 | try { 339 | $this->post( 340 | $uri, 341 | [ 342 | 'body' => $normalized, 343 | 'headers' => $this->getHeaders(self::NOTIFY_PAYLOAD_VERSION), 344 | ] 345 | ); 346 | } catch (Exception $e) { 347 | error_log('Bugsnag Warning: Couldn\'t notify. '.$e->getMessage()); 348 | } 349 | } 350 | 351 | /** 352 | * Normalize the given data to ensure it's the correct size. 353 | * 354 | * @param array $data the data to normalize 355 | * 356 | * @throws RuntimeException 357 | * 358 | * @return string the JSON encoded data after normalization 359 | */ 360 | protected function normalize(array $data) 361 | { 362 | $body = json_encode($data, $this->jsonEncodeFlags); 363 | 364 | if ($this->length($body) <= static::MAX_SIZE) { 365 | return $body; 366 | } 367 | 368 | unset($data['events'][0]['metaData']); 369 | 370 | $body = json_encode($data, $this->jsonEncodeFlags); 371 | 372 | if ($this->length($body) > static::MAX_SIZE) { 373 | throw new RuntimeException('Payload too large'); 374 | } 375 | 376 | return $body; 377 | } 378 | 379 | /** 380 | * Get the length of the given string in bytes. 381 | * 382 | * @param string $str the string to get the length of 383 | * 384 | * @return int 385 | */ 386 | protected function length($str) 387 | { 388 | return function_exists('mb_strlen') ? mb_strlen($str, '8bit') : strlen($str); 389 | } 390 | } 391 | -------------------------------------------------------------------------------- /src/SessionTracker.php: -------------------------------------------------------------------------------- 1 | config = $config; 122 | $this->http = $http === null 123 | ? new HttpClient($config, $config->getSessionClient()) 124 | : $http; 125 | } 126 | 127 | /** 128 | * @param Configuration $config 129 | * 130 | * @return void 131 | * 132 | * @deprecated Change the Configuration via the Client object instead. 133 | */ 134 | public function setConfig(Configuration $config) 135 | { 136 | $this->config = $config; 137 | } 138 | 139 | /** 140 | * @return void 141 | */ 142 | public function startSession() 143 | { 144 | $currentTime = date('Y-m-d\TH:i:00'); 145 | 146 | $session = [ 147 | 'id' => uniqid('', true), 148 | 'startedAt' => $currentTime, 149 | 'events' => [ 150 | 'handled' => 0, 151 | 'unhandled' => 0, 152 | ], 153 | ]; 154 | 155 | $this->setCurrentSession($session); 156 | $this->incrementSessions($currentTime); 157 | } 158 | 159 | /** 160 | * @param array $session 161 | * 162 | * @return void 163 | */ 164 | public function setCurrentSession(array $session) 165 | { 166 | if (is_callable($this->sessionFunction)) { 167 | call_user_func($this->sessionFunction, $session); 168 | } else { 169 | $this->currentSession = $session; 170 | } 171 | } 172 | 173 | /** 174 | * @return array 175 | */ 176 | public function getCurrentSession() 177 | { 178 | if (is_callable($this->sessionFunction)) { 179 | $currentSession = call_user_func($this->sessionFunction); 180 | 181 | if (is_array($currentSession)) { 182 | return $currentSession; 183 | } 184 | 185 | return []; 186 | } 187 | 188 | return $this->currentSession; 189 | } 190 | 191 | /** 192 | * @return void 193 | */ 194 | public function sendSessions() 195 | { 196 | $locked = false; 197 | if (is_callable($this->lockFunction) && is_callable($this->unlockFunction)) { 198 | call_user_func($this->lockFunction); 199 | $locked = true; 200 | } 201 | 202 | try { 203 | $this->deliverSessions(); 204 | } finally { 205 | if ($locked) { 206 | call_user_func($this->unlockFunction); 207 | } 208 | } 209 | } 210 | 211 | /** 212 | * @param callable $lock 213 | * @param callable $unlock 214 | * 215 | * @return void 216 | */ 217 | public function setLockFunctions($lock, $unlock) 218 | { 219 | if (!is_callable($lock) || !is_callable($unlock)) { 220 | throw new InvalidArgumentException('Both lock and unlock functions must be callable'); 221 | } 222 | 223 | $this->lockFunction = $lock; 224 | $this->unlockFunction = $unlock; 225 | } 226 | 227 | /** 228 | * @param callable $function 229 | * 230 | * @return void 231 | */ 232 | public function setRetryFunction($function) 233 | { 234 | if (!is_callable($function)) { 235 | throw new InvalidArgumentException('The retry function must be callable'); 236 | } 237 | 238 | $this->retryFunction = $function; 239 | } 240 | 241 | /** 242 | * @param callable $function 243 | * 244 | * @return void 245 | */ 246 | public function setStorageFunction($function) 247 | { 248 | if (!is_callable($function)) { 249 | throw new InvalidArgumentException('Storage function must be callable'); 250 | } 251 | 252 | $this->storageFunction = $function; 253 | } 254 | 255 | /** 256 | * @param callable $function 257 | * 258 | * @return void 259 | */ 260 | public function setSessionFunction($function) 261 | { 262 | if (!is_callable($function)) { 263 | throw new InvalidArgumentException('Session function must be callable'); 264 | } 265 | 266 | $this->sessionFunction = $function; 267 | } 268 | 269 | /** 270 | * @param string $minute 271 | * @param int $count 272 | * @param bool $deliver 273 | * 274 | * @return void 275 | */ 276 | protected function incrementSessions($minute, $count = 1, $deliver = true) 277 | { 278 | $locked = false; 279 | 280 | if (is_callable($this->lockFunction) && is_callable($this->unlockFunction)) { 281 | call_user_func($this->lockFunction); 282 | $locked = true; 283 | } 284 | 285 | try { 286 | $sessionCounts = $this->getSessionCounts(); 287 | 288 | if (array_key_exists($minute, $sessionCounts)) { 289 | $sessionCounts[$minute] += $count; 290 | } else { 291 | $sessionCounts[$minute] = $count; 292 | } 293 | 294 | $this->setSessionCounts($sessionCounts); 295 | 296 | if (count($sessionCounts) > self::$MAX_SESSION_COUNT) { 297 | $this->trimOldestSessions(); 298 | } 299 | 300 | $lastSent = $this->getLastSent(); 301 | 302 | if ($deliver && ((time() - $lastSent) > self::$DELIVERY_INTERVAL)) { 303 | $this->deliverSessions(); 304 | } 305 | } finally { 306 | if ($locked) { 307 | call_user_func($this->unlockFunction); 308 | } 309 | } 310 | } 311 | 312 | /** 313 | * @return array 314 | */ 315 | protected function getSessionCounts() 316 | { 317 | if (is_callable($this->storageFunction)) { 318 | $sessionCounts = call_user_func($this->storageFunction, self::$SESSION_COUNTS_KEY); 319 | 320 | if (is_array($sessionCounts)) { 321 | return $sessionCounts; 322 | } 323 | 324 | return []; 325 | } 326 | 327 | return $this->sessionCounts; 328 | } 329 | 330 | /** 331 | * @param array $sessionCounts 332 | * 333 | * @return void 334 | */ 335 | protected function setSessionCounts(array $sessionCounts) 336 | { 337 | if (is_callable($this->storageFunction)) { 338 | call_user_func($this->storageFunction, self::$SESSION_COUNTS_KEY, $sessionCounts); 339 | } 340 | 341 | $this->sessionCounts = $sessionCounts; 342 | } 343 | 344 | /** 345 | * @return void 346 | */ 347 | protected function trimOldestSessions() 348 | { 349 | $sessions = $this->getSessionCounts(); 350 | 351 | // Sort the session counts so that the oldest minutes are first 352 | // i.e. '2000-01-01T00:00:00' should be after '2000-01-01T00:01:00' 353 | uksort($sessions, function ($a, $b) { 354 | return strtotime($b) - strtotime($a); 355 | }); 356 | 357 | $sessionCounts = array_slice($sessions, 0, self::$MAX_SESSION_COUNT); 358 | 359 | $this->setSessionCounts($sessionCounts); 360 | } 361 | 362 | /** 363 | * @param array $sessions 364 | * 365 | * @return array 366 | */ 367 | protected function constructPayload(array $sessions) 368 | { 369 | $formattedSessions = []; 370 | foreach ($sessions as $minute => $count) { 371 | $formattedSessions[] = ['startedAt' => $minute, 'sessionsStarted' => $count]; 372 | } 373 | 374 | return [ 375 | 'notifier' => $this->config->getNotifier(), 376 | 'device' => $this->config->getDeviceData(), 377 | 'app' => $this->config->getAppData(), 378 | 'sessionCounts' => $formattedSessions, 379 | ]; 380 | } 381 | 382 | /** 383 | * @return void 384 | */ 385 | protected function deliverSessions() 386 | { 387 | $sessions = $this->getSessionCounts(); 388 | 389 | $this->setSessionCounts([]); 390 | 391 | if (count($sessions) === 0) { 392 | return; 393 | } 394 | 395 | if (!$this->config->shouldNotify()) { 396 | return; 397 | } 398 | 399 | $payload = $this->constructPayload($sessions); 400 | 401 | $this->setLastSent(); 402 | 403 | try { 404 | $this->http->sendSessions($payload); 405 | } catch (Exception $e) { 406 | error_log('Bugsnag Warning: Couldn\'t notify. ' . $e->getMessage()); 407 | 408 | if (is_callable($this->retryFunction)) { 409 | call_user_func($this->retryFunction, $sessions); 410 | } else { 411 | foreach ($sessions as $minute => $count) { 412 | $this->incrementSessions($minute, $count, false); 413 | } 414 | } 415 | } 416 | } 417 | 418 | /** 419 | * @return void 420 | */ 421 | protected function setLastSent() 422 | { 423 | $time = time(); 424 | 425 | if (is_callable($this->storageFunction)) { 426 | call_user_func($this->storageFunction, self::$SESSIONS_LAST_SENT_KEY, $time); 427 | } else { 428 | $this->lastSent = $time; 429 | } 430 | } 431 | 432 | /** 433 | * @return int 434 | */ 435 | protected function getLastSent() 436 | { 437 | if (is_callable($this->storageFunction)) { 438 | $lastSent = call_user_func($this->storageFunction, self::$SESSIONS_LAST_SENT_KEY); 439 | 440 | // $lastSent may be a string despite us storing an integer because 441 | // some storage backends will convert all values into strings 442 | // note: some invalid integers pass 'is_numeric' (e.g. bigger than 443 | // PHP_INT_MAX) but these get cast to '0', which is the default anyway 444 | if (is_numeric($lastSent)) { 445 | return (int) $lastSent; 446 | } 447 | 448 | return 0; 449 | } 450 | 451 | return $this->lastSent; 452 | } 453 | } 454 | -------------------------------------------------------------------------------- /ARCHITECTURE.md: -------------------------------------------------------------------------------- 1 | # Bugsnag-PHP Notifier Architecture 2 | 3 | Version 1.0.0 4 | 5 | Last updated 30/08/17 6 | 7 | ## Introduction 8 | This document is one of a series describing the layout of the individual Bugsnag notifier libraries. Their purpose is to make it easier to understand the layout and working logic involved in the notifiers for new contributors and users, and the preferred ways of extending and modifying said libraries. 9 | 10 | ## Dependencies 11 | - [composer/ca-bundle](https://github.com/composer/ca-bundle) 12 | - [guzzlehttp/guzzle](https://github.com/guzzle/guzzle) 13 | - [vlucas/phpdotenv](https://github.com/vlucas/phpdotenv) 14 | 15 | ## Dev Dependencies 16 | - [graham-campbell/testbench-core](https://github.com/GrahamCampbell/Laravel-TestBench-Core) 17 | - [mockery/mockery](https://github.com/mockery/mockery) 18 | - [mtdowling/burgomaster](https://github.com/mtdowling/Burgomaster) 19 | - [phpunit/phpunit](https://github.com/sebastianbergmann/phpunit) 20 | - [php-mock/php-mock-phpunit](https://github.com/php-mock/php-mock-phpunit) 21 | 22 | ## Bugsnag-PHP Architecture 23 | All code required to run the Bugsnag PHP notifier can be found within the `src` directory. 24 | 25 | ### The Client Object 26 | The main Bugsnag object that will be used in applications for the purpose of catching and notify of errors is the client object. This provides an API for the user to call on for the majority of the functions they will seek to utilise in the library, including: `notify`, `notify-exception`, `notify-error`, `leave-breadcrumb`, and `deploy`. 27 | 28 | The client object has three constructor arguments that modify the way it will operate: 29 | - `configuration` Accepts a `Configuration` object for the user to customize behavior of the Bugsnag notifier. The configuration options will be set in the most appropriate method for the framework being used, and parsed into this format. 30 | - `resolver` Accepts an object that implements the `ResolverInterface`. This object will be responsible for returning an object that implements the `RequestInterface`, which will need to populate a `Report` upon request. By default if not given a resolver the client will create a `BasicResolver`. 31 | - `guzzle` Accepts a `Guzzle` object provided by the guzzler library to use as an HTTP request object in the `HTTPClient` object. By default the client will create one using the static `ENDPOINT` variable. If it's necessary to change these defaults, the static `make_guzzle` may be used with a defined base url and options array, with the result being passed into this argument. 32 | 33 | When utilising this library in a framework-specific way it is advised that the client object creation is handled in a way that makes the most sense for access to the notify functions and in order to extract the most relevant data, e.g. by using a different `resolver` with framework-specific functions, or wrapping the client in a service provider. 34 | 35 | There is a provided static `make` method that creates the client object with suitable default `Configuration` and `Guzzle` arguments with the `api_key` and `endpoint` allowing the user to set their Bugsnag settings. 36 | 37 | Another important aspect of the client object's construction is the registering of callbacks with a `pipeline` object. If created with the `make` method the default callbacks will be added to the `pipeline` unless 38 | the `defaults` argument is set to false. These default callbacks will be responsible for populating the `report` object with information when necessary, and will should be customized to extract the most relevant information from the error and `resolver` objects used in the framework. 39 | 40 | ### The Handler Class 41 | The handler class provides static functions for registering the Bugsnag unhandled error and exception handlers, as well as a shutdown handler. This is separate to the client object as these handlers and methods of registering them will change across frameworks. 42 | 43 | The static registration functions take an instance of the client object to utilise as a source of application information and as a notifier when an unhandled error or exception occurs. 44 | 45 | As this is an optional part of the library that must be initiated separately it should be replaced by an appropriate method of hooking into the error handlers or loggers of the relevant framework. It must respond to these events by creating a `Report` object from the `client` and exception, and then it must pass it into the client object's `notify` function for it to be sent off to the Bugsnag notification server. 46 | 47 | ### The Report Object 48 | The Report class is used to create readable information from a PHP exception or throwable which can later be used to populate an HTTP request to notify Bugsnag. It is accessible through three static methods: 49 | - `fromPHPError` to create from a given decompiled PHP Error from the error handler 50 | - `fromPHPThrowable` to create from a PHP Throwable object 51 | - `fromNamedError` to create from name and message strings 52 | 53 | These methods should be used to create a report object to ensure that the correct fields are populated for the later notification stages. 54 | 55 | ### The Pipeline and Callbacks 56 | Upon being passed to the `notify` function, the report object will also be populated with information provided by a series of callbacks created with the `client` object. Registered with the `registerCallback` method, each callback will be passed the `report` in turn and can populate it with additional information if necessary. 57 | 58 | The pipeline object itself is responsible for executing these callbacks as a series of closures until they have all been able to access the `report` object and modify its content. The pipeline object is merely a method of utilising the callbacks, and does not need to be modified per framework. 59 | 60 | The default callbacks, registered through the `client` object's `registerDefaultCallbacks` use the `Resolver` and its `Request` objects to extract data about the current environment in the server, where the error-causing request originated from, and any more metadata it can report. 61 | 62 | There are two callbacks registered with the pipeline automatically by the client: `BreadcrumbData` which ensures that the recorded `Breadcrumb` objects are attached to the `report`, and `NotificationSkipper` which stops the notification process in the event that the notification should not be sent i.e. when missing an `api_key` or a non-releasing `releaseStage`. 63 | 64 | It is recommended to create callback functions that can extract additional information from the framework to attach as metadata if necessary. These callbacks are automatically wrapped in a `CallbackBridge` when registered to the pipeline which ensures they will automatically be called. 65 | 66 | ### The HTTPClient 67 | Once the `report` object has been populated by the `pipeline` a callback is finally triggered in the `notify` function that will send the `report` to the HTTPClient. This object exists on the `client` and is intiated with a `guzzle` client that it will use to call off to the configured endpoint. 68 | 69 | This object will queue each `report` object it is given until `send` is called. At this point it will iteratively create a single object containing all of the data from the `queue`, and ensure that the payload is correctly set up to be sent while remaining under the payload size limit. 70 | 71 | Once the payload has been fully constructed it will be posted to the configured endpoint via the guzzle object. 72 | 73 | The HTTPClient is also responsible for the `deploy` call. 74 | 75 | ### Breadcrumbs 76 | Bugsnag tracks a series of actions manually dictated by the user to be sent to the Bugsnag notify endpoint along with an exception. These actions are stored as breadcrumb objects, and are stored in the recorder object in the `client`. The recorder acts like a circular array, storing up to 25 breadcrumb objects at a time before the oldest breadcrumbs get overwritten. This limit is imposed to ensure the size of the payload sent to Bugsnag does not exceed the API limits 77 | 78 | The breadcrumb data is attached to the `report` payload by the BreadcrumbData callback, which is initiated by the `client` object in its construction. 79 | 80 | When the `notify` function is called, manually or automatically, a breadcrumb is logged with details of the error or exception being sent. 81 | 82 | # Other Bugsnag PHP Framework Libraries 83 | This section covers the other available Bugsnag notifiers for PHP frameworks and how they are implemented and connected to the main Bugsnag-PHP libary and each other. 84 | 85 | ## [Bugsnag-PSR-Logger](https://github.com/bugsnag/bugsnag-psr-logger) 86 | This library implements the [PSR-3 Logger Interface](http://www.php-fig.org/psr/psr-3/) specification to enable users to attach Bugsnag into a standardized logging system. It consists of three classes: 87 | - `AbstractLogger` an abstract class which implements the PSR spec `LoggerInterface`, which requires a `log` method in its implementors 88 | - `BugsnagLogger` a logger class which extends the above class. This logger will record `debug` or `info` logs as `breadcrumb` objects, and will notify Bugsnag of any other log type 89 | - `MultiLogger` again extends the `AbstractLogger`, but accepts and array of loggers in its construction, allowing other PSR compliant loggers to be used simultaneously with the `BugsnagLogger` 90 | 91 | ## [Bugsnag-Wordpress](https://github.com/bugsnag/bugsnag-wordpress) 92 | This plugin for wordpress enables Bugsnag through the plugins menu of the wordpress site. It requires an older version of this library (~ 2.2) and so some of the methods and features will likely have been refactored for the newer versions. 93 | 94 | ## [Bugsnag-Symfony](https://github.com/bugsnag/bugsnag-symfony) 95 | This library provides an extended version of the base Bugsnag-PHP library customized for the [Symfony PHP framework](symfony.com). 96 | 97 | ### Dependency Management 98 | The libary is bundled for inclusion in the Symfony app framework through the `RegisterBundle` function used for adding Symfony extensions as laid out [here](https://symfony.com/doc/current/bundles.html). It utilises the `DependencyInjection` folder to set up the extension through the `Configuration.php` file to define the necessary default configuration options, drawing the rest from the app's `config.yml`. This configuration is then read in and processed by the `BugsnagExtension.php` file, and the arguments are set on the container. 99 | 100 | To build the client when requested, the `Configuration.php` defines a factory for the framework to use, `ClientFactory.php`. This factory wraps the creation methods for the `client` object and ensures that the client is configured correctly to get information from the framework and respond to events. The `ClientFactory` configuration can be found in the Bugsnag service definition in `Resources/services.yml`. 101 | 102 | ### Customizing the `Client` object 103 | The configuration passed through to the `ClientFactory` will modify several of the `client` object's properties. In additional to the [configuration options](https://docs.bugsnag.com/platforms/php/symfony/configuration-options/) for the user to setup their particular configuration, it also defines the `resolver` object which gathers information that populates the `report` objects used in the notify method. 104 | 105 | The `ClientFactory` registers the default callbacks to the `pipeline` to extract data for the report, but also adds an additional callback specific to Symfony to extract a user identifier from the user specific `Token` object. 106 | 107 | Once the `client` has been created it is returned to the Symfony instance as a service, allowing it to be accessed through the application with the 108 | appropriate Symfony service access methods. 109 | 110 | ### Listening for Events 111 | The library does not utilise the basic PHP `Handler` object to listen to events, rather it connects an event listener `BugsnagListener.php` to the Symfony instance in the `services.yml` file, connecting specific events directly to a method via the `tags` descriptor as mentioned in the [official documentation](https://symfony.com/doc/current/event_dispatcher.html). 112 | 113 | ### Symfony Resolver and Request 114 | As defined in `Configuration.php` the client will use the `SymfonyResolver` class as its default resolver. This resolver ensures that there is a Symfony specific `Request` object available before handing it off to a `SymfonyRequest` object. This object implements all the methods from the `RequestInterface`, allowing the default callbacks to extract and append data to reports whenever a notification occurs. 115 | 116 | ## [Bugsnag-Laravel](https://github.com/bugsnag/bugsnag-laravel) 117 | The Bugsnag-Laravel library again extends the Bugsnag-PHP library and customizes its operation for the [Laravel application framework](https://laravel.com/). 118 | 119 | ### Dependency Management 120 | Laravel uses a very similar dependency management system to Symfony, wrapping classes in `ServiceProviders` that can then be called later through an `Alias` or a `Facade`. The `BugsnagServiceProvider` class implements `boot` and `register` functions as described in the [service provider](https://laravel.com/docs/5.4/providers) documentation. The `boot` function intialises the configuration and options of the provider, while the `register` function returns a singleton accessible throught the application. 121 | 122 | ### Customizing the `Client` object 123 | The `register` function mentioned above creates the `client` object with a base `configuration` and `guzzle`, as well as a `LaravelResolver` to handle retrieving data from created `LaravelRequest` objects in the `Resolver`-`Request` pattern. 124 | 125 | It draws its configuration from the Laravel `config` object which the framework automatically populates from the `.env` file or a created `Bugsnag.php` configuration file. 126 | 127 | The default callbacks are registered to the `pipeline` in the `setupCallbacks` function, along with customized callbacks to extract custom and user information from the framework to attach to the report. 128 | 129 | ### Listening for events 130 | The Laravel notifier uses the [Bugsnag-PSR-Logger](https://github.com/bugsnag/bugsnag-psr-logger) in order to automatically receive error and exception events from the Laravel framework, which is the Laravel preferred method instead of directly registering an `error-handler`. 131 | 132 | The notifier wraps the PSR logger in a pair of classes, the `LaravelLogger` and `MultiLogger` for singular and multi-logging setups respectively. These are added to the framework by aliasing the frameworks PSR-logger interface and class to the `bugsnag.logger` class or `bugsnag.multi` classes depending on the users setup. This must be done manually within the `AppServiceProvider`. 133 | 134 | ### Notifying of Deployments 135 | While deployment notifications can be sent through the client object, Laravel library also provided a deploy command through the `DeployCommand` class. This must be registered through the `commands` array in the `Kernel.php` Laravel file. 136 | 137 | ## [Bugsnag-Silex](https://github.com/bugsnag/bugsnag-silex) 138 | The Bugsnag-Silex library adds Silex-specific methods and data-gathering for the [Silex micro-framework](silex.symfony.com). 139 | 140 | ### Dependency Management 141 | Being based on Symfony, Silex uses a similar service provider system to Laravel and Symfony, where the provider is passed to the app using the `register` function in the app's main startup file. The provider is an implementation of the `ServiceProviderInterface` class, with a `register` function that adds `client` and `resolver` objects to the Silex container. 142 | 143 | The provider is split into three classes, `AbstractServiceProvider` as a base class and `Silex1ServiceProvider` and `Silex2ServiceProvider` as extensions of this base. The version specific provider classes operate in the same way except the `Silex1ServiceProvider` defines a required `boot` function that does nothing. 144 | 145 | Both classes call into their base class method `makeClient` to produce the `client` being registered. 146 | 147 | ### Customizing the `Client` object 148 | Silex configuration options are added in an environment-specific file in the `config` folder. These options are then automatically pulled into the app container when the environment is started. In the `makeClient` function this config is pulled in and used to setup a newly created client object, along with the earlier created `resolver`. 149 | 150 | This `SilexResolver` is used to retrieve data for each report using the `SilexRequest` class in the previously mentioned `Resolver`-`Request` pattern. 151 | 152 | Standard default callbacks are registered along with an additional callback to detect the user if one isn't already configured. 153 | 154 | ### Listening for events 155 | The Silex framework requires manual registering of error and exception handlers, which requires the user add a `notifyException` call into an error handler registered to the app container's `error` function. 156 | 157 | ## [Bugsnag-Magento](https://github.com/bugsnag/bugsnag-magento) 158 | The Bugsnag-Magento module enables Bugsnag functionality through the Magento admin panel. It uses an older version of the Bugsnag-PHP library packaged with the module and so some of the methods and features will likely have been refactored by later versions. 159 | -------------------------------------------------------------------------------- /src/Configuration.php: -------------------------------------------------------------------------------- 1 | 'Bugsnag PHP (Official)', 103 | 'version' => '3.30.0', 104 | 'url' => 'https://bugsnag.com', 105 | ]; 106 | 107 | /** 108 | * The fallback app type. 109 | * 110 | * @var string|null 111 | */ 112 | protected $fallbackType; 113 | 114 | /** 115 | * The application data. 116 | * 117 | * @var string[] 118 | */ 119 | protected $appData = []; 120 | 121 | /** 122 | * The device data. 123 | * 124 | * @var string[] 125 | */ 126 | protected $deviceData = []; 127 | 128 | /** 129 | * The meta data. 130 | * 131 | * @var array[] 132 | */ 133 | protected $metaData = []; 134 | 135 | /** 136 | * The associated feature flags. 137 | * 138 | * @var FeatureFlagDelegate 139 | */ 140 | private $featureFlags; 141 | 142 | /** 143 | * The error reporting level. 144 | * 145 | * @var int|null 146 | */ 147 | protected $errorReportingLevel; 148 | 149 | /** 150 | * Whether to track sessions. 151 | * 152 | * @var bool 153 | */ 154 | protected $autoCaptureSessions = false; 155 | 156 | /** 157 | * A client to use to send sessions. 158 | * 159 | * @var \GuzzleHttp\ClientInterface|null 160 | * 161 | * @deprecated This will be removed in the next major version. 162 | */ 163 | protected $sessionClient; 164 | 165 | /** 166 | * @var string 167 | */ 168 | protected $notifyEndpoint; 169 | 170 | /** 171 | * @var string 172 | */ 173 | protected $sessionEndpoint; 174 | 175 | /** 176 | * @var string 177 | */ 178 | protected $buildEndpoint; 179 | 180 | /** 181 | * The amount to increase the memory_limit to handle an OOM. 182 | * 183 | * The default is 5MiB and can be disabled by setting it to 'null' 184 | * 185 | * @var int|null 186 | */ 187 | protected $memoryLimitIncrease = 5242880; 188 | 189 | /** 190 | * An array of classes that should not be sent to Bugsnag. 191 | * 192 | * This can contain both fully qualified class names and regular expressions. 193 | * 194 | * @var array 195 | */ 196 | protected $discardClasses = []; 197 | 198 | /** 199 | * An array of metadata keys that should be redacted. 200 | * 201 | * @var string[] 202 | */ 203 | protected $redactedKeys = []; 204 | 205 | /** 206 | * Create a new config instance. 207 | * 208 | * @param string $apiKey your bugsnag api key 209 | * 210 | * @throws \InvalidArgumentException 211 | * 212 | * @return void 213 | */ 214 | public function __construct($apiKey) 215 | { 216 | if (!is_string($apiKey)) { 217 | throw new InvalidArgumentException('Invalid API key'); 218 | } 219 | $this->apiKey = $apiKey; 220 | 221 | if ($this->isHubApiKey()) { 222 | $this->notifyEndpoint = self::HUB_NOTIFY_ENDPOINT; 223 | $this->sessionEndpoint = self::HUB_SESSION_ENDPOINT; 224 | $this->buildEndpoint = self::HUB_BUILD_ENDPOINT; 225 | } else { 226 | $this->notifyEndpoint = self::NOTIFY_ENDPOINT; 227 | $this->sessionEndpoint = self::SESSION_ENDPOINT; 228 | $this->buildEndpoint = self::BUILD_ENDPOINT; 229 | } 230 | 231 | $this->fallbackType = php_sapi_name(); 232 | $this->featureFlags = new FeatureFlagDelegate(); 233 | 234 | // Add PHP runtime version to device data 235 | $this->mergeDeviceData(['runtimeVersions' => ['php' => phpversion()]]); 236 | } 237 | 238 | /** 239 | * Checks if the API Key is associated with the InsightHub instance. 240 | * 241 | * @return bool 242 | */ 243 | public function isHubApiKey() 244 | { 245 | // Does the API key start with 00000 246 | return strpos($this->apiKey, '00000') === 0; 247 | } 248 | 249 | /** 250 | * Get the Bugsnag API Key. 251 | * 252 | * @return string 253 | */ 254 | public function getApiKey() 255 | { 256 | return $this->apiKey; 257 | } 258 | 259 | /** 260 | * Sets whether errors should be batched together and send at the end of each request. 261 | * 262 | * @param bool $batchSending whether to batch together errors 263 | * 264 | * @return $this 265 | */ 266 | public function setBatchSending($batchSending) 267 | { 268 | $this->batchSending = $batchSending; 269 | 270 | return $this; 271 | } 272 | 273 | /** 274 | * Is batch sending is enabled? 275 | * 276 | * @return bool 277 | */ 278 | public function isBatchSending() 279 | { 280 | return $this->batchSending; 281 | } 282 | 283 | /** 284 | * Set which release stages should be allowed to notify Bugsnag. 285 | * 286 | * Eg ['production', 'development']. 287 | * 288 | * @param string[]|null $notifyReleaseStages array of release stages to notify for 289 | * 290 | * @return $this 291 | */ 292 | public function setNotifyReleaseStages($notifyReleaseStages = null) 293 | { 294 | $this->notifyReleaseStages = $notifyReleaseStages; 295 | 296 | return $this; 297 | } 298 | 299 | /** 300 | * Should we notify Bugsnag based on the current release stage? 301 | * 302 | * @return bool 303 | */ 304 | public function shouldNotify() 305 | { 306 | if (!$this->notifyReleaseStages) { 307 | return true; 308 | } 309 | 310 | return in_array($this->getAppData()['releaseStage'], $this->notifyReleaseStages, true); 311 | } 312 | 313 | /** 314 | * Set the strings to filter out from metaData arrays before sending then. 315 | * 316 | * Eg. ['password', 'credit_card']. 317 | * 318 | * @deprecated Use redactedKeys instead 319 | * 320 | * @param string[] $filters an array of metaData filters 321 | * 322 | * @return $this 323 | */ 324 | public function setFilters(array $filters) 325 | { 326 | $this->filters = $filters; 327 | 328 | return $this; 329 | } 330 | 331 | /** 332 | * Get the array of metaData filters. 333 | * 334 | * @deprecated Use redactedKeys instead 335 | * 336 | * @return string[] 337 | */ 338 | public function getFilters() 339 | { 340 | return $this->filters; 341 | } 342 | 343 | /** 344 | * Set the project root. 345 | * 346 | * @param string|null $projectRoot the project root path 347 | * 348 | * @return void 349 | */ 350 | public function setProjectRoot($projectRoot) 351 | { 352 | $projectRootRegex = $projectRoot ? '/^' . preg_quote($projectRoot, '/') . '[\\/]?/i' : null; 353 | $this->setProjectRootRegex($projectRootRegex); 354 | } 355 | 356 | /** 357 | * Set the project root regex. 358 | * 359 | * @param string|null $projectRootRegex the project root path 360 | * 361 | * @return void 362 | */ 363 | public function setProjectRootRegex($projectRootRegex) 364 | { 365 | if ($projectRootRegex && @preg_match($projectRootRegex, '') === false) { 366 | throw new InvalidArgumentException('Invalid project root regex: ' . $projectRootRegex); 367 | } 368 | 369 | $this->projectRootRegex = $projectRootRegex; 370 | $this->setStripPathRegex($projectRootRegex); 371 | } 372 | 373 | /** 374 | * Is the given file in the project? 375 | * 376 | * @param string $file 377 | * 378 | * @return bool 379 | */ 380 | public function isInProject($file) 381 | { 382 | return $this->projectRootRegex && preg_match($this->projectRootRegex, $file); 383 | } 384 | 385 | /** 386 | * Set the strip path. 387 | * 388 | * @param string|null $stripPath the absolute strip path 389 | * 390 | * @return void 391 | */ 392 | public function setStripPath($stripPath) 393 | { 394 | $stripPathRegex = $stripPath ? '/^' . preg_quote($stripPath, '/') . '[\\/]?/i' : null; 395 | $this->setStripPathRegex($stripPathRegex); 396 | } 397 | 398 | /** 399 | * Set the regular expression used to strip paths from stacktraces. 400 | * 401 | * @param string|null $stripPathRegex 402 | * 403 | * @return void 404 | */ 405 | public function setStripPathRegex($stripPathRegex) 406 | { 407 | if ($stripPathRegex && @preg_match($stripPathRegex, '') === false) { 408 | throw new InvalidArgumentException('Invalid strip path regex: ' . $stripPathRegex); 409 | } 410 | 411 | $this->stripPathRegex = $stripPathRegex; 412 | } 413 | 414 | /** 415 | * Set the stripped file path. 416 | * 417 | * @param string $file 418 | * 419 | * @return string 420 | */ 421 | public function getStrippedFilePath($file) 422 | { 423 | return $this->stripPathRegex ? preg_replace($this->stripPathRegex, '', $file) : $file; 424 | } 425 | 426 | /** 427 | * Set if we should we send a small snippet of the code that crashed. 428 | * 429 | * This can help you diagnose even faster from within your dashboard. 430 | * 431 | * @param bool $sendCode whether to send code to Bugsnag 432 | * 433 | * @return $this 434 | */ 435 | public function setSendCode($sendCode) 436 | { 437 | $this->sendCode = $sendCode; 438 | 439 | return $this; 440 | } 441 | 442 | /** 443 | * Should we send a small snippet of the code that crashed? 444 | * 445 | * @return bool 446 | */ 447 | public function shouldSendCode() 448 | { 449 | return $this->sendCode; 450 | } 451 | 452 | /** 453 | * Sets the notifier to report as to Bugsnag. 454 | * 455 | * This should only be set by other notifier libraries. 456 | * 457 | * @param string[] $notifier an array of name, version, url. 458 | * 459 | * @return $this 460 | */ 461 | public function setNotifier(array $notifier) 462 | { 463 | $this->notifier = $notifier; 464 | 465 | return $this; 466 | } 467 | 468 | /** 469 | * Get the notifier to report as to Bugsnag. 470 | * 471 | * @return string[] 472 | */ 473 | public function getNotifier() 474 | { 475 | return $this->notifier; 476 | } 477 | 478 | /** 479 | * Set your app's semantic version, eg "1.2.3". 480 | * 481 | * @param string|null $appVersion the app's version 482 | * 483 | * @return $this 484 | */ 485 | public function setAppVersion($appVersion) 486 | { 487 | $this->appData['version'] = $appVersion; 488 | 489 | return $this; 490 | } 491 | 492 | /** 493 | * Set your release stage, eg "production" or "development". 494 | * 495 | * @param string|null $releaseStage the app's current release stage 496 | * 497 | * @return $this 498 | */ 499 | public function setReleaseStage($releaseStage) 500 | { 501 | $this->appData['releaseStage'] = $releaseStage; 502 | 503 | return $this; 504 | } 505 | 506 | /** 507 | * Set the type of application executing the code. 508 | * 509 | * This is usually used to represent if you are running plain PHP code 510 | * "php", via a framework, eg "laravel", or executing through delayed 511 | * worker code, eg "resque". 512 | * 513 | * @param string|null $type the current type 514 | * 515 | * @return $this 516 | */ 517 | public function setAppType($type) 518 | { 519 | $this->appData['type'] = $type; 520 | 521 | return $this; 522 | } 523 | 524 | /** 525 | * Set the fallback application type. 526 | * 527 | * This is should be used only by libraries to set an fallback app type. 528 | * 529 | * @param string|null $type the fallback type 530 | * 531 | * @return $this 532 | */ 533 | public function setFallbackType($type) 534 | { 535 | $this->fallbackType = $type; 536 | 537 | return $this; 538 | } 539 | 540 | /** 541 | * Get the application data. 542 | * 543 | * @return array 544 | */ 545 | public function getAppData() 546 | { 547 | return array_merge(array_filter(['type' => $this->fallbackType, 'releaseStage' => 'production']), array_filter($this->appData)); 548 | } 549 | 550 | /** 551 | * Set the hostname. 552 | * 553 | * @param string|null $hostname the hostname 554 | * 555 | * @return $this 556 | */ 557 | public function setHostname($hostname) 558 | { 559 | $this->deviceData['hostname'] = $hostname; 560 | 561 | return $this; 562 | } 563 | 564 | /** 565 | * Adds new data fields to the device data collection. 566 | * 567 | * @param array $data an associative array containing the new data to be added 568 | * 569 | * @return $this 570 | */ 571 | public function mergeDeviceData($data) 572 | { 573 | $this->deviceData = array_merge_recursive($this->deviceData, $data); 574 | 575 | return $this; 576 | } 577 | 578 | /** 579 | * Get the device data. 580 | * 581 | * @return array 582 | */ 583 | public function getDeviceData() 584 | { 585 | return array_merge($this->getHostname(), array_filter($this->deviceData)); 586 | } 587 | 588 | /** 589 | * Get the hostname if possible. 590 | * 591 | * @return array 592 | */ 593 | protected function getHostname() 594 | { 595 | $disabled = explode(',', ini_get('disable_functions')); 596 | 597 | if (function_exists('php_uname') && !in_array('php_uname', $disabled, true)) { 598 | return ['hostname' => php_uname('n')]; 599 | } 600 | 601 | if (function_exists('gethostname') && !in_array('gethostname', $disabled, true)) { 602 | return ['hostname' => gethostname()]; 603 | } 604 | 605 | return []; 606 | } 607 | 608 | /** 609 | * Set custom metadata to send to Bugsnag. 610 | * 611 | * You can use this to add custom tabs of data to each error on your 612 | * Bugsnag dashboard. 613 | * 614 | * @param array[] $metaData an array of arrays of custom data 615 | * @param bool $merge should we merge the meta data 616 | * 617 | * @return $this 618 | */ 619 | public function setMetaData(array $metaData, $merge = true) 620 | { 621 | $this->metaData = $merge ? array_merge_recursive($this->metaData, $metaData) : $metaData; 622 | 623 | return $this; 624 | } 625 | 626 | /** 627 | * Get the custom metadata to send to Bugsnag. 628 | * 629 | * @return array[] 630 | */ 631 | public function getMetaData() 632 | { 633 | return $this->metaData; 634 | } 635 | 636 | /** 637 | * Add a single feature flag to all future reports. 638 | * 639 | * @param string $name 640 | * @param string|null $variant 641 | * 642 | * @return void 643 | */ 644 | public function addFeatureFlag($name, $variant = null) 645 | { 646 | $this->featureFlags->add($name, $variant); 647 | } 648 | 649 | /** 650 | * Add multiple feature flags to all future reports. 651 | * 652 | * @param FeatureFlag[] $featureFlags 653 | * @phpstan-param list $featureFlags 654 | * 655 | * @return void 656 | */ 657 | public function addFeatureFlags(array $featureFlags) 658 | { 659 | $this->featureFlags->merge($featureFlags); 660 | } 661 | 662 | /** 663 | * Remove the feature flag with the given name from all future reports. 664 | * 665 | * @param string $name 666 | * 667 | * @return void 668 | */ 669 | public function clearFeatureFlag($name) 670 | { 671 | $this->featureFlags->remove($name); 672 | } 673 | 674 | /** 675 | * Remove all feature flags from all future reports. 676 | * 677 | * @return void 678 | */ 679 | public function clearFeatureFlags() 680 | { 681 | $this->featureFlags->clear(); 682 | } 683 | 684 | /** 685 | * @internal 686 | * 687 | * @return FeatureFlagDelegate 688 | */ 689 | public function getFeatureFlagsCopy() 690 | { 691 | return clone $this->featureFlags; 692 | } 693 | 694 | /** 695 | * Set Bugsnag's error reporting level. 696 | * 697 | * If this is not set, we'll use your current PHP error_reporting value 698 | * from your ini file or error_reporting(...) calls. 699 | * 700 | * @param int|null $errorReportingLevel the error reporting level integer 701 | * 702 | * @return $this 703 | */ 704 | public function setErrorReportingLevel($errorReportingLevel) 705 | { 706 | if (!$this->isSubsetOfErrorReporting($errorReportingLevel)) { 707 | $missingLevels = implode(', ', $this->getMissingErrorLevelNames($errorReportingLevel)); 708 | $message = 709 | 'Bugsnag Warning: errorReportingLevel cannot contain values that are not in error_reporting. ' . 710 | "Any errors of these levels will be ignored: {$missingLevels}."; 711 | 712 | error_log($message); 713 | } 714 | 715 | $this->errorReportingLevel = $errorReportingLevel; 716 | 717 | return $this; 718 | } 719 | 720 | /** 721 | * Check if the given error reporting level is a subset of error_reporting. 722 | * 723 | * For example, if $level contains E_WARNING then error_reporting must too. 724 | * 725 | * @param int|null $level 726 | * 727 | * @return bool 728 | */ 729 | private function isSubsetOfErrorReporting($level) 730 | { 731 | if (!is_int($level)) { 732 | return true; 733 | } 734 | 735 | $errorReporting = error_reporting(); 736 | 737 | // If all of the bits in $level are also in $errorReporting, ORing them 738 | // together will result in the same value as $errorReporting because 739 | // there are no new bits to add 740 | return ($errorReporting | $level) === $errorReporting; 741 | } 742 | 743 | /** 744 | * Get a list of error level names that are in $level but not error_reporting. 745 | * 746 | * For example, if error_reporting is E_NOTICE and $level is E_ERROR then 747 | * this will return ['E_ERROR'] 748 | * 749 | * @param int $level 750 | * 751 | * @return string[] 752 | */ 753 | private function getMissingErrorLevelNames($level) 754 | { 755 | $missingLevels = []; 756 | $errorReporting = error_reporting(); 757 | 758 | foreach (ErrorTypes::getAllCodes() as $code) { 759 | // $code is "missing" if it's in $level but not in $errorReporting 760 | if (($code & $level) && !($code & $errorReporting)) { 761 | $missingLevels[] = ErrorTypes::codeToString($code); 762 | } 763 | } 764 | 765 | return $missingLevels; 766 | } 767 | 768 | /** 769 | * Should we ignore the given error code? 770 | * 771 | * @param int $code the error code 772 | * 773 | * @return bool 774 | */ 775 | public function shouldIgnoreErrorCode($code) 776 | { 777 | // If the code is not in error_reporting then it is either totally 778 | // disabled or is being suppressed with '@' 779 | if (!(error_reporting() & $code)) { 780 | return true; 781 | } 782 | 783 | // Filter the error code further against our error reporting level, which 784 | // can be lower than error_reporting 785 | if (isset($this->errorReportingLevel)) { 786 | return !($this->errorReportingLevel & $code); 787 | } 788 | 789 | return false; 790 | } 791 | 792 | /** 793 | * Set event notification endpoint. 794 | * 795 | * @param string $endpoint 796 | * 797 | * @return $this 798 | */ 799 | public function setNotifyEndpoint($endpoint) 800 | { 801 | $this->notifyEndpoint = $endpoint; 802 | 803 | return $this; 804 | } 805 | 806 | /** 807 | * Get event notification endpoint. 808 | * 809 | * @return string 810 | */ 811 | public function getNotifyEndpoint() 812 | { 813 | return $this->notifyEndpoint; 814 | } 815 | 816 | /** 817 | * Set session delivery endpoint. 818 | * 819 | * @param string $endpoint 820 | * 821 | * @return $this 822 | */ 823 | public function setSessionEndpoint($endpoint) 824 | { 825 | $this->sessionEndpoint = $endpoint; 826 | 827 | return $this; 828 | } 829 | 830 | /** 831 | * Get session delivery endpoint. 832 | * 833 | * @return string 834 | */ 835 | public function getSessionEndpoint() 836 | { 837 | return $this->sessionEndpoint; 838 | } 839 | 840 | /** 841 | * Set the build endpoint. 842 | * 843 | * @param string $endpoint the build endpoint 844 | * 845 | * @return $this 846 | */ 847 | public function setBuildEndpoint($endpoint) 848 | { 849 | $this->buildEndpoint = $endpoint; 850 | 851 | return $this; 852 | } 853 | 854 | /** 855 | * Get the build endpoint. 856 | * 857 | * @return string 858 | */ 859 | public function getBuildEndpoint() 860 | { 861 | return $this->buildEndpoint; 862 | } 863 | 864 | /** 865 | * Set session tracking state. 866 | * 867 | * @param bool $track whether to track sessions 868 | * 869 | * @return $this 870 | */ 871 | public function setAutoCaptureSessions($track) 872 | { 873 | $this->autoCaptureSessions = $track; 874 | 875 | return $this; 876 | } 877 | 878 | /** 879 | * Whether should be auto-capturing sessions. 880 | * 881 | * @return bool 882 | */ 883 | public function shouldCaptureSessions() 884 | { 885 | return $this->autoCaptureSessions; 886 | } 887 | 888 | /** 889 | * Get the session client. 890 | * 891 | * @return \GuzzleHttp\ClientInterface 892 | * 893 | * @deprecated This will be removed in the next major version. 894 | */ 895 | public function getSessionClient() 896 | { 897 | if (is_null($this->sessionClient)) { 898 | $this->sessionClient = Client::makeGuzzle($this->sessionEndpoint); 899 | } 900 | 901 | return $this->sessionClient; 902 | } 903 | 904 | /** 905 | * Set the amount to increase the memory_limit when an OOM is triggered. 906 | * 907 | * This is an amount of bytes or 'null' to disable increasing the limit. 908 | * 909 | * @param int|null $value 910 | * 911 | * @return $this 912 | */ 913 | public function setMemoryLimitIncrease($value) 914 | { 915 | $this->memoryLimitIncrease = $value; 916 | 917 | return $this; 918 | } 919 | 920 | /** 921 | * Get the amount to increase the memory_limit when an OOM is triggered. 922 | * 923 | * This will return 'null' if this feature is disabled. 924 | * 925 | * @return int|null 926 | */ 927 | public function getMemoryLimitIncrease() 928 | { 929 | return $this->memoryLimitIncrease; 930 | } 931 | 932 | /** 933 | * Set the array of classes that should not be sent to Bugsnag. 934 | * 935 | * @param array $discardClasses 936 | * 937 | * @return $this 938 | */ 939 | public function setDiscardClasses(array $discardClasses) 940 | { 941 | $this->discardClasses = $discardClasses; 942 | 943 | return $this; 944 | } 945 | 946 | /** 947 | * Get the array of classes that should not be sent to Bugsnag. 948 | * 949 | * This can contain both fully qualified class names and regular expressions. 950 | * 951 | * @return array 952 | */ 953 | public function getDiscardClasses() 954 | { 955 | return $this->discardClasses; 956 | } 957 | 958 | /** 959 | * Set the array of metadata keys that should be redacted. 960 | * 961 | * @param string[] $redactedKeys 962 | * 963 | * @return $this 964 | */ 965 | public function setRedactedKeys(array $redactedKeys) 966 | { 967 | $this->redactedKeys = $redactedKeys; 968 | 969 | return $this; 970 | } 971 | 972 | /** 973 | * Get the array of metadata keys that should be redacted. 974 | * 975 | * @return string[] 976 | */ 977 | public function getRedactedKeys() 978 | { 979 | return $this->redactedKeys; 980 | } 981 | } 982 | -------------------------------------------------------------------------------- /src/Report.php: -------------------------------------------------------------------------------- 1 | setPHPError($code, $message, $file, $line, $fatal) 160 | ->setUnhandled(false) 161 | ->setSeverityReason(['type' => 'handledError']); 162 | 163 | return $report; 164 | } 165 | 166 | /** 167 | * Create a new report from a PHP throwable. 168 | * 169 | * @param \Bugsnag\Configuration $config the config instance 170 | * @param \Throwable $throwable the throwable instance 171 | * 172 | * @return static 173 | */ 174 | public static function fromPHPThrowable(Configuration $config, $throwable) 175 | { 176 | // @phpstan-ignore-next-line 177 | $report = new static($config); 178 | 179 | $report->setPHPThrowable($throwable) 180 | ->setUnhandled(false) 181 | ->setSeverityReason(['type' => 'handledException']); 182 | 183 | return $report; 184 | } 185 | 186 | /** 187 | * Create a new report from a named error. 188 | * 189 | * @param \Bugsnag\Configuration $config the config instance 190 | * @param string $name the error name 191 | * @param string|null $message the error message 192 | * 193 | * @return static 194 | */ 195 | public static function fromNamedError(Configuration $config, $name, $message = null) 196 | { 197 | // @phpstan-ignore-next-line 198 | $report = new static($config); 199 | 200 | $report->setName($name) 201 | ->setMessage($message) 202 | ->setStacktrace(Stacktrace::generate($config)) 203 | ->setUnhandled(false) 204 | ->setSeverityReason(['type' => 'handledError']); 205 | 206 | return $report; 207 | } 208 | 209 | /** 210 | * Create a new report instance. 211 | * 212 | * This is only for for use only by the static methods above. 213 | * 214 | * @param \Bugsnag\Configuration $config the config instance 215 | * 216 | * @return void 217 | */ 218 | protected function __construct(Configuration $config) 219 | { 220 | $this->config = $config; 221 | $this->time = Date::now(); 222 | $this->featureFlags = $config->getFeatureFlagsCopy(); 223 | } 224 | 225 | /** 226 | * Get the original error. 227 | * 228 | * @return \Throwable|array|null 229 | */ 230 | public function getOriginalError() 231 | { 232 | return $this->originalError; 233 | } 234 | 235 | /** 236 | * Set the PHP throwable. 237 | * 238 | * @param \Throwable $throwable the throwable instance 239 | * 240 | * @throws \InvalidArgumentException 241 | * 242 | * @return $this 243 | */ 244 | public function setPHPThrowable($throwable) 245 | { 246 | // TODO: if we drop support for PHP 5, we can remove this check for 247 | // 'Exception', which fixes the PHPStan issue here 248 | // @phpstan-ignore-next-line 249 | if (!$throwable instanceof Throwable && !$throwable instanceof Exception) { 250 | throw new InvalidArgumentException('The throwable must implement Throwable or extend Exception.'); 251 | } 252 | 253 | $this->originalError = $throwable; 254 | 255 | $this->setName(get_class($throwable)) 256 | ->setMessage($throwable->getMessage()) 257 | ->setStacktrace(Stacktrace::fromBacktrace($this->config, $throwable->getTrace(), $throwable->getFile(), $throwable->getLine())); 258 | 259 | if (method_exists($throwable, 'getPrevious')) { 260 | $this->setPrevious($throwable->getPrevious()); 261 | } 262 | 263 | return $this; 264 | } 265 | 266 | /** 267 | * Set the PHP error. 268 | * 269 | * @param int $code the error code 270 | * @param string|null $message the error message 271 | * @param string $file the error file 272 | * @param int $line the error line 273 | * @param bool $fatal if the error was fatal 274 | * 275 | * @return $this 276 | */ 277 | public function setPHPError($code, $message, $file, $line, $fatal = false) 278 | { 279 | $this->originalError = [ 280 | 'code' => $code, 281 | 'message' => $message, 282 | 'file' => $file, 283 | 'line' => $line, 284 | 'fatal' => $fatal, 285 | ]; 286 | 287 | if ($fatal) { 288 | // Generating stacktrace for PHP fatal errors is not possible, 289 | // since this code executes when the PHP process shuts down, 290 | // rather than at the time of the crash. 291 | // 292 | // In these situations, we generate a "stacktrace" containing only 293 | // the line and file number where the crash occurred. 294 | $stacktrace = Stacktrace::fromFrame($this->config, $file, $line); 295 | } else { 296 | $stacktrace = Stacktrace::generate($this->config); 297 | } 298 | 299 | $this->setName(ErrorTypes::getName($code)) 300 | ->setMessage($message) 301 | ->setSeverity(ErrorTypes::getSeverity($code)) 302 | ->setStacktrace($stacktrace); 303 | 304 | return $this; 305 | } 306 | 307 | /** 308 | * Set the bugsnag stacktrace. 309 | * 310 | * @param \Bugsnag\Stacktrace $stacktrace the stacktrace instance 311 | * 312 | * @return $this 313 | */ 314 | protected function setStacktrace(Stacktrace $stacktrace) 315 | { 316 | $this->stacktrace = $stacktrace; 317 | 318 | return $this; 319 | } 320 | 321 | /** 322 | * Gets the severity reason. 323 | * 324 | * @return array 325 | */ 326 | public function getSeverityReason() 327 | { 328 | if (!array_key_exists('type', $this->severityReason)) { 329 | syslog(LOG_WARNING, 'Severity reason should always have a "type" set'); 330 | $this->severityReason['type'] = 'userSpecifiedSeverity'; 331 | } 332 | 333 | return $this->severityReason; 334 | } 335 | 336 | /** 337 | * Sets the unhandled payload. 338 | * 339 | * @return $this 340 | */ 341 | public function setSeverityReason(array $severityReason) 342 | { 343 | $this->severityReason = $severityReason; 344 | 345 | return $this; 346 | } 347 | 348 | /** 349 | * Sets the unhandled flag. 350 | * 351 | * @param bool $unhandled 352 | * 353 | * @return $this 354 | */ 355 | public function setUnhandled($unhandled) 356 | { 357 | $this->unhandled = $unhandled; 358 | 359 | return $this; 360 | } 361 | 362 | /** 363 | * Returns the unhandled flag. 364 | * 365 | * @return bool 366 | */ 367 | public function getUnhandled() 368 | { 369 | return $this->unhandled; 370 | } 371 | 372 | /** 373 | * Get the bugsnag stacktrace. 374 | * 375 | * @return \Bugsnag\Stacktrace 376 | */ 377 | public function getStacktrace() 378 | { 379 | return $this->stacktrace; 380 | } 381 | 382 | /** 383 | * Set the previous throwable. 384 | * 385 | * @param \Throwable $throwable the previous throwable 386 | * 387 | * @return $this 388 | */ 389 | protected function setPrevious($throwable) 390 | { 391 | if ($throwable) { 392 | $this->previous = static::fromPHPThrowable($this->config, $throwable); 393 | } 394 | 395 | return $this; 396 | } 397 | 398 | /** 399 | * Set the error name. 400 | * 401 | * @param string $name the error name 402 | * 403 | * @throws \InvalidArgumentException 404 | * 405 | * @return $this 406 | */ 407 | public function setName($name) 408 | { 409 | if (is_scalar($name) || (is_object($name) && method_exists($name, '__toString'))) { 410 | $this->name = (string) $name; 411 | } else { 412 | throw new InvalidArgumentException('The name must be a string.'); 413 | } 414 | 415 | if ($this->name === '') { 416 | $this->name = 'Error'; 417 | } 418 | 419 | return $this; 420 | } 421 | 422 | /** 423 | * Get the error name. 424 | * 425 | * @return string 426 | */ 427 | public function getName() 428 | { 429 | return $this->name; 430 | } 431 | 432 | /** 433 | * Set the error message. 434 | * 435 | * @param string|null $message the error message 436 | * 437 | * @throws \InvalidArgumentException 438 | * 439 | * @return $this 440 | */ 441 | public function setMessage($message) 442 | { 443 | if ($message === null) { 444 | $this->message = null; 445 | } elseif ( 446 | is_scalar($message) 447 | || (is_object($message) && method_exists($message, '__toString')) 448 | ) { 449 | $this->message = (string) $message; 450 | } else { 451 | throw new InvalidArgumentException('The message must be a string.'); 452 | } 453 | 454 | return $this; 455 | } 456 | 457 | /** 458 | * Get the error message. 459 | * 460 | * @return string|null 461 | */ 462 | public function getMessage() 463 | { 464 | return $this->message; 465 | } 466 | 467 | /** 468 | * Set the error severity. 469 | * 470 | * @param string|null $severity the error severity 471 | * 472 | * @throws \InvalidArgumentException 473 | * 474 | * @return $this 475 | */ 476 | public function setSeverity($severity) 477 | { 478 | if (in_array($severity, ['error', 'warning', 'info', null], true)) { 479 | $this->severity = $severity; 480 | } else { 481 | throw new InvalidArgumentException('The severity must be either "error", "warning", or "info".'); 482 | } 483 | 484 | return $this; 485 | } 486 | 487 | /** 488 | * Get the error severity. 489 | * 490 | * @return string 491 | */ 492 | public function getSeverity() 493 | { 494 | return $this->severity ?: 'warning'; 495 | } 496 | 497 | /** 498 | * Set a context representing the current type of request, or location in code. 499 | * 500 | * @param string|null $context the current context 501 | * 502 | * @return $this 503 | */ 504 | public function setContext($context) 505 | { 506 | $this->context = $context; 507 | 508 | return $this; 509 | } 510 | 511 | /** 512 | * Get the error context. 513 | * 514 | * @return string|null 515 | */ 516 | public function getContext() 517 | { 518 | return $this->context; 519 | } 520 | 521 | /** 522 | * Set the grouping hash. 523 | * 524 | * @param string|null $groupingHash the grouping hash 525 | * 526 | * @return $this 527 | */ 528 | public function setGroupingHash($groupingHash) 529 | { 530 | $this->groupingHash = $groupingHash; 531 | 532 | return $this; 533 | } 534 | 535 | /** 536 | * Get the grouping hash. 537 | * 538 | * @return string|null 539 | */ 540 | public function getGroupingHash() 541 | { 542 | return $this->groupingHash; 543 | } 544 | 545 | /** 546 | * Set the error meta data. 547 | * 548 | * @param array[] $metaData an array of arrays of custom data 549 | * @param bool $merge should we merge the meta data 550 | * 551 | * @return $this 552 | */ 553 | public function setMetaData(array $metaData, $merge = true) 554 | { 555 | $this->metaData = $merge ? array_merge_recursive($this->metaData, $metaData) : $metaData; 556 | 557 | return $this; 558 | } 559 | 560 | /** 561 | * Adds a tab to the meta data. 562 | * Conflicting keys will be merged if able, otherwise the new values will be accepted. 563 | * Null values will be deleted from the metadata. 564 | * 565 | * @param array[] $metadata an array of custom data to attach to the report 566 | * 567 | * @return $this 568 | */ 569 | public function addMetaData(array $metadata) 570 | { 571 | $this->metaData = array_replace_recursive($this->metaData, $metadata); 572 | $this->metaData = $this->removeNullElements($this->metaData); 573 | 574 | return $this; 575 | } 576 | 577 | /** 578 | * Get the error meta data. 579 | * 580 | * @return array[] 581 | */ 582 | public function getMetaData() 583 | { 584 | return $this->metaData; 585 | } 586 | 587 | /** 588 | * Add a single feature flag to this report. 589 | * 590 | * @param string $name 591 | * @param string|null $variant 592 | * 593 | * @return void 594 | */ 595 | public function addFeatureFlag($name, $variant = null) 596 | { 597 | $this->featureFlags->add($name, $variant); 598 | } 599 | 600 | /** 601 | * Add multiple feature flags to this report. 602 | * 603 | * @param FeatureFlag[] $featureFlags 604 | * @phpstan-param list $featureFlags 605 | * 606 | * @return void 607 | */ 608 | public function addFeatureFlags(array $featureFlags) 609 | { 610 | $this->featureFlags->merge($featureFlags); 611 | } 612 | 613 | /** 614 | * Remove the feature flag with the given name from this report. 615 | * 616 | * @param string $name 617 | * 618 | * @return void 619 | */ 620 | public function clearFeatureFlag($name) 621 | { 622 | $this->featureFlags->remove($name); 623 | } 624 | 625 | /** 626 | * Remove all feature flags from this report. 627 | * 628 | * @return void 629 | */ 630 | public function clearFeatureFlags() 631 | { 632 | $this->featureFlags->clear(); 633 | } 634 | 635 | /** 636 | * Get the list of feature flags for this report. 637 | * 638 | * @return \Bugsnag\FeatureFlag[] 639 | */ 640 | public function getFeatureFlags() 641 | { 642 | return $this->featureFlags->toArray(); 643 | } 644 | 645 | /** 646 | * Set the current user. 647 | * 648 | * @param array $user the current user 649 | * 650 | * @return $this 651 | */ 652 | public function setUser(array $user) 653 | { 654 | $this->user = $user; 655 | 656 | return $this; 657 | } 658 | 659 | /** 660 | * Get the current user. 661 | * 662 | * @return array 663 | */ 664 | public function getUser() 665 | { 666 | return $this->user; 667 | } 668 | 669 | /** 670 | * Add a breadcrumb to the report. 671 | * 672 | * @param \Bugsnag\Breadcrumbs\Breadcrumb $breadcrumb 673 | * 674 | * @return void 675 | */ 676 | public function addBreadcrumb(Breadcrumb $breadcrumb) 677 | { 678 | $data = $breadcrumb->toArray(); 679 | 680 | if ($metaData = $this->cleanupObj($breadcrumb->getMetaData(), true)) { 681 | $data['metaData'] = $metaData; 682 | 683 | if (strlen(json_encode($data)) > Breadcrumb::MAX_SIZE) { 684 | unset($data['metaData']); 685 | } 686 | } 687 | 688 | $this->breadcrumbs[] = $data; 689 | } 690 | 691 | /** 692 | * Get the report summary. 693 | * 694 | * @return string[] 695 | */ 696 | public function getSummary() 697 | { 698 | $summary = []; 699 | 700 | $name = $this->getName(); 701 | $message = $this->getMessage(); 702 | 703 | if ($name !== $message) { 704 | $summary['name'] = $name; 705 | } 706 | 707 | $summary['message'] = $message; 708 | 709 | $summary['severity'] = $this->getSeverity(); 710 | 711 | return array_filter($summary); 712 | } 713 | 714 | /** 715 | * Sets the session data. 716 | * 717 | * @return void 718 | */ 719 | public function setSessionData(array $session) 720 | { 721 | $this->session = $session; 722 | } 723 | 724 | /** 725 | * Get a list of all errors in a fixed format of: 726 | * - 'errorClass' 727 | * - 'errorMessage' 728 | * - 'type' (always 'php'). 729 | * 730 | * @return array 731 | */ 732 | public function getErrors() 733 | { 734 | $errors = [$this->toError()]; 735 | $previous = $this->previous; 736 | 737 | while ($previous) { 738 | $errors[] = $previous->toError(); 739 | $previous = $previous->previous; 740 | } 741 | 742 | return $errors; 743 | } 744 | 745 | /** 746 | * @return array 747 | */ 748 | private function toError() 749 | { 750 | return [ 751 | 'errorClass' => $this->name, 752 | 'errorMessage' => $this->message, 753 | 'type' => 'php', 754 | ]; 755 | } 756 | 757 | /** 758 | * Get the array representation. 759 | * 760 | * @return array 761 | */ 762 | public function toArray() 763 | { 764 | $event = [ 765 | 'app' => $this->config->getAppData(), 766 | 'device' => array_merge(['time' => $this->time], $this->config->getDeviceData()), 767 | 'user' => $this->cleanupObj($this->getUser(), true), 768 | 'context' => $this->getContext(), 769 | 'payloadVersion' => HttpClient::NOTIFY_PAYLOAD_VERSION, 770 | 'severity' => $this->getSeverity(), 771 | 'exceptions' => $this->exceptionArray(), 772 | 'breadcrumbs' => $this->breadcrumbs, 773 | 'metaData' => $this->cleanupObj($this->getMetaData(), true), 774 | 'unhandled' => $this->getUnhandled(), 775 | 'severityReason' => $this->getSeverityReason(), 776 | 'featureFlags' => array_map( 777 | function (FeatureFlag $flag) { 778 | return $flag->toArray(); 779 | }, 780 | $this->featureFlags->toArray() 781 | ), 782 | ]; 783 | 784 | if ($hash = $this->getGroupingHash()) { 785 | $event['groupingHash'] = $hash; 786 | } 787 | 788 | if (isset($this->session)) { 789 | $event['session'] = $this->session; 790 | } 791 | 792 | return $event; 793 | } 794 | 795 | /** 796 | * Get the exception array. 797 | * 798 | * @return array 799 | */ 800 | protected function exceptionArray() 801 | { 802 | $exceptionArray = [$this->exceptionObject()]; 803 | $previous = $this->previous; 804 | while ($previous) { 805 | $exceptionArray[] = $previous->exceptionObject(); 806 | $previous = $previous->previous; 807 | } 808 | 809 | return $this->cleanupObj($exceptionArray, false); 810 | } 811 | 812 | /** 813 | * Get serializable representation of the exception causing this report. 814 | * 815 | * @return array 816 | */ 817 | protected function exceptionObject() 818 | { 819 | return [ 820 | 'errorClass' => $this->name, 821 | 'message' => $this->message, 822 | 'stacktrace' => $this->stacktrace->toArray(), 823 | ]; 824 | } 825 | 826 | /** 827 | * Cleanup the given object. 828 | * 829 | * @param mixed $obj the data to cleanup 830 | * @param bool $isMetaData if it is meta data 831 | * 832 | * @return mixed 833 | */ 834 | protected function cleanupObj($obj, $isMetaData) 835 | { 836 | if (is_null($obj)) { 837 | return null; 838 | } 839 | 840 | if (is_array($obj)) { 841 | $clean = []; 842 | 843 | foreach ($obj as $key => $value) { 844 | $clean[$key] = $this->shouldFilter($key, $isMetaData) ? '[FILTERED]' : $this->cleanupObj($value, $isMetaData); 845 | } 846 | 847 | return $clean; 848 | } 849 | 850 | if (is_string($obj)) { 851 | // on PHP 7.2+ we can use the 'JSON_INVALID_UTF8_SUBSTITUTE' flag to 852 | // substitute invalid UTF-8 characters when encoding so can return 853 | // strings as-is and let PHP handle invalid UTF-8 854 | // note: we check PHP 7.2+ specifically rather than if the flag 855 | // exists because some code defines the flag as '0' when it doesn't 856 | // exist to avoid having to check for it existing. This completely 857 | // breaks the flag as it will not function, so we can't know if the 858 | // flag can be used just from it being defined 859 | if (version_compare(PHP_VERSION, '7.2', '>=')) { 860 | return $obj; 861 | } 862 | 863 | // if we have the mbstring extension available, use that to detect 864 | // encodings and handle conversions to UTF-8 865 | if (function_exists('mb_check_encoding') && !mb_check_encoding($obj, 'UTF-8')) { 866 | return mb_convert_encoding($obj, 'UTF-8', mb_list_encodings()); 867 | } 868 | 869 | return $obj; 870 | } 871 | 872 | if (is_object($obj)) { 873 | if ($obj instanceof UnitEnum) { 874 | return $this->enumToString($obj); 875 | } 876 | 877 | return $this->cleanupObj(json_decode(json_encode($obj), true), $isMetaData); 878 | } 879 | 880 | return $obj; 881 | } 882 | 883 | /** 884 | * Should we filter the given element. 885 | * 886 | * @param string $key the associated key 887 | * @param bool $isMetaData if it is meta data 888 | * 889 | * @return bool 890 | */ 891 | protected function shouldFilter($key, $isMetaData) 892 | { 893 | if (!$isMetaData) { 894 | return false; 895 | } 896 | 897 | foreach ($this->config->getFilters() as $filter) { 898 | if (stripos($key, $filter) !== false) { 899 | return true; 900 | } 901 | } 902 | 903 | foreach ($this->config->getRedactedKeys() as $redactedKey) { 904 | if (@preg_match($redactedKey, $key) === 1) { 905 | return true; 906 | } elseif (Utils::stringCaseEquals($redactedKey, $key)) { 907 | return true; 908 | } 909 | } 910 | 911 | return false; 912 | } 913 | 914 | /** 915 | * Recursively remove null elements. 916 | * 917 | * @param array $array the array to remove null elements from 918 | * 919 | * @return array 920 | */ 921 | protected function removeNullElements($array) 922 | { 923 | foreach ($array as $key => $val) { 924 | if (is_array($val)) { 925 | $array[$key] = $this->removeNullElements($val); 926 | } elseif (is_null($val)) { 927 | unset($array[$key]); 928 | } 929 | } 930 | 931 | return $array; 932 | } 933 | 934 | /** 935 | * Convert the given enum to a string. 936 | * 937 | * @param UnitEnum $enum 938 | * 939 | * @return string 940 | */ 941 | private function enumToString(UnitEnum $enum) 942 | { 943 | // e.g. My\Enum::SomeCase 944 | $string = sprintf('%s::%s', get_class($enum), $enum->name); 945 | 946 | // add the value, if there is one 947 | if ($enum instanceof BackedEnum) { 948 | $string .= sprintf(' (%s)', $enum->value); 949 | } 950 | 951 | return $string; 952 | } 953 | } 954 | -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | get('BUGSNAG_API_KEY')); 106 | $guzzle = static::makeGuzzle($notifyEndpoint ?: $env->get('BUGSNAG_ENDPOINT')); 107 | 108 | // @phpstan-ignore-next-line 109 | $client = new static($config, null, $guzzle); 110 | 111 | if ($defaults) { 112 | $client->registerDefaultCallbacks(); 113 | } 114 | 115 | return $client; 116 | } 117 | 118 | /** 119 | * @param \Bugsnag\Configuration $config 120 | * @param \Bugsnag\Request\ResolverInterface|null $resolver 121 | * @param \GuzzleHttp\ClientInterface|null $guzzle 122 | * @param \Bugsnag\Shutdown\ShutdownStrategyInterface|null $shutdownStrategy 123 | */ 124 | public function __construct( 125 | Configuration $config, 126 | $resolver = null, 127 | $guzzle = null, 128 | $shutdownStrategy = null 129 | ) { 130 | $guzzle = $guzzle ?: self::makeGuzzle(); 131 | 132 | $this->syncNotifyEndpointWithGuzzleBaseUri($config, $guzzle); 133 | 134 | $this->config = $config; 135 | $this->resolver = $resolver ?: new BasicResolver(); 136 | $this->recorder = new Recorder(); 137 | $this->pipeline = new Pipeline(); 138 | $this->http = new HttpClient($config, $guzzle); 139 | $this->sessionTracker = new SessionTracker($config, $this->http); 140 | 141 | $this->registerMiddleware(new NotificationSkipper($config)); 142 | $this->registerMiddleware(new DiscardClasses($config)); 143 | $this->registerMiddleware(new BreadcrumbData($this->recorder)); 144 | $this->registerMiddleware(new SessionData($this)); 145 | 146 | // Shutdown strategy is used to trigger flush() calls when batch sending is enabled 147 | $shutdownStrategy = $shutdownStrategy ?: new PhpShutdownStrategy(); 148 | $shutdownStrategy->registerShutdownStrategy($this); 149 | } 150 | 151 | /** 152 | * Make a new guzzle client instance. 153 | * 154 | * @param string|null $base 155 | * @param array $options 156 | * 157 | * @return GuzzleHttp\ClientInterface 158 | */ 159 | public static function makeGuzzle($base = null, array $options = []) 160 | { 161 | $options = self::resolveGuzzleOptions($base, $options); 162 | 163 | return new GuzzleHttp\Client($options); 164 | } 165 | 166 | /** 167 | * @param string|null $base 168 | * @param array $options 169 | * 170 | * @return array 171 | */ 172 | private static function resolveGuzzleOptions($base, array $options) 173 | { 174 | $key = GuzzleCompat::getBaseUriOptionName(); 175 | $options[$key] = $base ?: Configuration::NOTIFY_ENDPOINT; 176 | 177 | $path = static::getCaBundlePath(); 178 | 179 | if ($path) { 180 | $options['verify'] = $path; 181 | } 182 | 183 | return GuzzleCompat::applyRequestOptions( 184 | $options, 185 | [ 186 | 'timeout' => self::DEFAULT_TIMEOUT_S, 187 | 'connect_timeout' => self::DEFAULT_TIMEOUT_S, 188 | ] 189 | ); 190 | } 191 | 192 | /** 193 | * Ensure the notify endpoint is synchronised with Guzzle's base URL. 194 | * 195 | * @param \Bugsnag\Configuration $configuration 196 | * @param \GuzzleHttp\ClientInterface $guzzle 197 | * 198 | * @return void 199 | */ 200 | private function syncNotifyEndpointWithGuzzleBaseUri( 201 | Configuration $configuration, 202 | GuzzleHttp\ClientInterface $guzzle 203 | ) { 204 | // Don't change the endpoint if one is already set, otherwise we could be 205 | // resetting it back to the default as the Guzzle base URL will always 206 | // be set by 'makeGuzzle'. 207 | if ($configuration->getNotifyEndpoint() !== Configuration::NOTIFY_ENDPOINT) { 208 | return; 209 | } 210 | 211 | $base = GuzzleCompat::getBaseUri($guzzle); 212 | 213 | if (is_string($base) || (is_object($base) && method_exists($base, '__toString'))) { 214 | $configuration->setNotifyEndpoint((string) $base); 215 | } 216 | } 217 | 218 | /** 219 | * Get the ca bundle path if one exists. 220 | * 221 | * @return string|false 222 | */ 223 | protected static function getCaBundlePath() 224 | { 225 | if (version_compare(PHP_VERSION, '5.6.0') >= 0 || !class_exists(CaBundle::class)) { 226 | return false; 227 | } 228 | 229 | return realpath(CaBundle::getSystemCaRootBundlePath()); 230 | } 231 | 232 | /** 233 | * Get the config instance. 234 | * 235 | * @return \Bugsnag\Configuration 236 | */ 237 | public function getConfig() 238 | { 239 | return $this->config; 240 | } 241 | 242 | /** 243 | * Get the pipeline instance. 244 | * 245 | * @return \Bugsnag\Pipeline 246 | */ 247 | public function getPipeline() 248 | { 249 | return $this->pipeline; 250 | } 251 | 252 | /** 253 | * Regsier a new notification callback. 254 | * 255 | * @param callable $callback 256 | * 257 | * @return $this 258 | */ 259 | public function registerCallback(callable $callback) 260 | { 261 | $this->registerMiddleware(new CallbackBridge($callback)); 262 | 263 | return $this; 264 | } 265 | 266 | /** 267 | * Regsier all our default callbacks. 268 | * 269 | * @return $this 270 | */ 271 | public function registerDefaultCallbacks() 272 | { 273 | $this->registerCallback(new GlobalMetaData($this->config)) 274 | ->registerCallback(new RequestMetaData($this->resolver)) 275 | ->registerCallback(new RequestSession($this->resolver)) 276 | ->registerCallback(new RequestUser($this->resolver)) 277 | ->registerCallback(new RequestContext($this->resolver)); 278 | 279 | return $this; 280 | } 281 | 282 | /** 283 | * Register a middleware object to the pipeline. 284 | * 285 | * @param callable $middleware 286 | * 287 | * @return $this 288 | */ 289 | public function registerMiddleware(callable $middleware) 290 | { 291 | $this->pipeline->pipe($middleware); 292 | 293 | return $this; 294 | } 295 | 296 | /** 297 | * Record the given breadcrumb. 298 | * 299 | * @param string $name the name of the breadcrumb 300 | * @param string|null $type the type of breadcrumb 301 | * @param array $metaData additional information about the breadcrumb 302 | * 303 | * @return void 304 | */ 305 | public function leaveBreadcrumb($name, $type = null, array $metaData = []) 306 | { 307 | $type = in_array($type, Breadcrumb::getTypes(), true) ? $type : Breadcrumb::MANUAL_TYPE; 308 | 309 | $this->recorder->record(new Breadcrumb($name, $type, $metaData)); 310 | } 311 | 312 | /** 313 | * Clear all recorded breadcrumbs. 314 | * 315 | * @return void 316 | */ 317 | public function clearBreadcrumbs() 318 | { 319 | $this->recorder->clear(); 320 | } 321 | 322 | /** 323 | * Notify Bugsnag of a non-fatal/handled throwable. 324 | * 325 | * @param \Throwable $throwable the throwable to notify Bugsnag about 326 | * @param callable|null $callback the customization callback 327 | * 328 | * @return void 329 | */ 330 | public function notifyException($throwable, $callback = null) 331 | { 332 | $report = Report::fromPHPThrowable($this->config, $throwable); 333 | 334 | $this->notify($report, $callback); 335 | } 336 | 337 | /** 338 | * Notify Bugsnag of a non-fatal/handled error. 339 | * 340 | * @param string $name the name of the error, a short (1 word) string 341 | * @param string $message the error message 342 | * @param callable|null $callback the customization callback 343 | * 344 | * @return void 345 | */ 346 | public function notifyError($name, $message, $callback = null) 347 | { 348 | $report = Report::fromNamedError($this->config, $name, $message); 349 | 350 | $this->notify($report, $callback); 351 | } 352 | 353 | /** 354 | * Notify Bugsnag of the given error report. 355 | * 356 | * This may simply involve queuing it for later if we're batching. 357 | * 358 | * @param \Bugsnag\Report $report the error report to send 359 | * @param callable|null $callback the customization callback 360 | * 361 | * @return void 362 | */ 363 | public function notify(Report $report, $callback = null) 364 | { 365 | $this->pipeline->execute($report, function ($report) use ($callback) { 366 | if ($callback) { 367 | $resolvedReport = null; 368 | 369 | $bridge = new CallbackBridge($callback); 370 | $bridge($report, function ($report) use (&$resolvedReport) { 371 | $resolvedReport = $report; 372 | }); 373 | if ($resolvedReport) { 374 | $report = $resolvedReport; 375 | } else { 376 | return; 377 | } 378 | } 379 | 380 | $this->http->queue($report); 381 | }); 382 | 383 | $this->leaveBreadcrumb($report->getName(), Breadcrumb::ERROR_TYPE, $report->getSummary()); 384 | 385 | if (!$this->config->isBatchSending()) { 386 | $this->flush(); 387 | } 388 | } 389 | 390 | /** 391 | * Notify Bugsnag of a deployment. 392 | * 393 | * @param string|null $repository the repository from which you are deploying the code 394 | * @param string|null $branch the source control branch from which you are deploying 395 | * @param string|null $revision the source control revision you are currently deploying 396 | * 397 | * @return void 398 | * 399 | * @deprecated Use {@see Client::build} instead. 400 | */ 401 | public function deploy($repository = null, $branch = null, $revision = null) 402 | { 403 | $this->build($repository, $revision); 404 | } 405 | 406 | /** 407 | * Notify Bugsnag of a build. 408 | * 409 | * @param string|null $repository the repository from which you are deploying the code 410 | * @param string|null $revision the source control revision you are currently deploying 411 | * @param string|null $provider the provider of the source control for the build 412 | * @param string|null $builderName the name of who or what is making the build 413 | * 414 | * @return void 415 | */ 416 | public function build($repository = null, $revision = null, $provider = null, $builderName = null) 417 | { 418 | $data = []; 419 | 420 | if ($repository) { 421 | $data['repository'] = $repository; 422 | } 423 | 424 | if ($revision) { 425 | $data['revision'] = $revision; 426 | } 427 | 428 | if ($provider) { 429 | $data['provider'] = $provider; 430 | } 431 | 432 | if ($builderName) { 433 | $data['builder'] = $builderName; 434 | } 435 | 436 | $this->http->sendBuildReport($data); 437 | } 438 | 439 | /** 440 | * Flush any buffered reports. 441 | * 442 | * @return void 443 | */ 444 | public function flush() 445 | { 446 | $this->http->sendEvents(); 447 | } 448 | 449 | /** 450 | * Start tracking a session. 451 | * 452 | * @return void 453 | */ 454 | public function startSession() 455 | { 456 | $this->sessionTracker->startSession(); 457 | } 458 | 459 | /** 460 | * Returns the session tracker. 461 | * 462 | * @return \Bugsnag\SessionTracker 463 | */ 464 | public function getSessionTracker() 465 | { 466 | return $this->sessionTracker; 467 | } 468 | 469 | // Forward calls to Configuration: 470 | 471 | /** 472 | * Get the Bugsnag API Key. 473 | * 474 | * @return string 475 | */ 476 | public function getApiKey() 477 | { 478 | return $this->config->getApiKey(); 479 | } 480 | 481 | /** 482 | * Sets whether errors should be batched together and send at the end of each request. 483 | * 484 | * @param bool $batchSending whether to batch together errors 485 | * 486 | * @return $this 487 | */ 488 | public function setBatchSending($batchSending) 489 | { 490 | $this->config->setBatchSending($batchSending); 491 | 492 | return $this; 493 | } 494 | 495 | /** 496 | * Is batch sending is enabled? 497 | * 498 | * @return bool 499 | */ 500 | public function isBatchSending() 501 | { 502 | return $this->config->isBatchSending(); 503 | } 504 | 505 | /** 506 | * Set which release stages should be allowed to notify Bugsnag. 507 | * 508 | * Eg ['production', 'development']. 509 | * 510 | * @param string[]|null $notifyReleaseStages array of release stages to notify for 511 | * 512 | * @return $this 513 | */ 514 | public function setNotifyReleaseStages($notifyReleaseStages = null) 515 | { 516 | $this->config->setNotifyReleaseStages($notifyReleaseStages); 517 | 518 | return $this; 519 | } 520 | 521 | /** 522 | * Should we notify Bugsnag based on the current release stage? 523 | * 524 | * @return bool 525 | */ 526 | public function shouldNotify() 527 | { 528 | return $this->config->shouldNotify(); 529 | } 530 | 531 | /** 532 | * Set the strings to filter out from metaData arrays before sending then. 533 | * 534 | * Eg. ['password', 'credit_card']. 535 | * 536 | * @deprecated Use redactedKeys instead 537 | * 538 | * @param string[] $filters an array of metaData filters 539 | * 540 | * @return $this 541 | */ 542 | public function setFilters(array $filters) 543 | { 544 | $this->config->setFilters($filters); 545 | 546 | return $this; 547 | } 548 | 549 | /** 550 | * Get the array of metaData filters. 551 | * 552 | * @deprecated Use redactedKeys instead 553 | * 554 | * @return string[] 555 | */ 556 | public function getFilters() 557 | { 558 | return $this->config->getFilters(); 559 | } 560 | 561 | /** 562 | * Set the project root. 563 | * 564 | * @param string|null $projectRoot the project root path 565 | * 566 | * @return void 567 | */ 568 | public function setProjectRoot($projectRoot) 569 | { 570 | $this->config->setProjectRoot($projectRoot); 571 | } 572 | 573 | /** 574 | * Set the project root regex. 575 | * 576 | * @param string|null $projectRootRegex the project root path 577 | * 578 | * @return void 579 | */ 580 | public function setProjectRootRegex($projectRootRegex) 581 | { 582 | $this->config->setProjectRootRegex($projectRootRegex); 583 | } 584 | 585 | /** 586 | * Is the given file in the project? 587 | * 588 | * @param string $file 589 | * 590 | * @return bool 591 | */ 592 | public function isInProject($file) 593 | { 594 | return $this->config->isInProject($file); 595 | } 596 | 597 | /** 598 | * Set the strip path. 599 | * 600 | * @param string|null $stripPath the absolute strip path 601 | * 602 | * @return void 603 | */ 604 | public function setStripPath($stripPath) 605 | { 606 | $this->config->setStripPath($stripPath); 607 | } 608 | 609 | /** 610 | * Set the regular expression used to strip paths from stacktraces. 611 | * 612 | * @param string|null $stripPathRegex 613 | * 614 | * @return void 615 | */ 616 | public function setStripPathRegex($stripPathRegex) 617 | { 618 | $this->config->setStripPathRegex($stripPathRegex); 619 | } 620 | 621 | /** 622 | * Get the stripped file path. 623 | * 624 | * @param string $file 625 | * 626 | * @return string 627 | */ 628 | public function getStrippedFilePath($file) 629 | { 630 | return $this->config->getStrippedFilePath($file); 631 | } 632 | 633 | /** 634 | * Set if we should we send a small snippet of the code that crashed. 635 | * 636 | * This can help you diagnose even faster from within your dashboard. 637 | * 638 | * @param bool $sendCode whether to send code to Bugsnag 639 | * 640 | * @return $this 641 | */ 642 | public function setSendCode($sendCode) 643 | { 644 | $this->config->setSendCode($sendCode); 645 | 646 | return $this; 647 | } 648 | 649 | /** 650 | * Should we send a small snippet of the code that crashed? 651 | * 652 | * @return bool 653 | */ 654 | public function shouldSendCode() 655 | { 656 | return $this->config->shouldSendCode(); 657 | } 658 | 659 | /** 660 | * Sets the notifier to report as to Bugsnag. 661 | * 662 | * This should only be set by other notifier libraries. 663 | * 664 | * @param string[] $notifier an array of name, version, url. 665 | * 666 | * @return $this 667 | */ 668 | public function setNotifier(array $notifier) 669 | { 670 | $this->config->setNotifier($notifier); 671 | 672 | return $this; 673 | } 674 | 675 | /** 676 | * Get the notifier to report as to Bugsnag. 677 | * 678 | * @return string[] 679 | */ 680 | public function getNotifier() 681 | { 682 | return $this->config->getNotifier(); 683 | } 684 | 685 | /** 686 | * Set your app's semantic version, eg "1.2.3". 687 | * 688 | * @param string|null $appVersion the app's version 689 | * 690 | * @return $this 691 | */ 692 | public function setAppVersion($appVersion) 693 | { 694 | $this->config->setAppVersion($appVersion); 695 | 696 | return $this; 697 | } 698 | 699 | /** 700 | * Set your release stage, eg "production" or "development". 701 | * 702 | * @param string|null $releaseStage the app's current release stage 703 | * 704 | * @return $this 705 | */ 706 | public function setReleaseStage($releaseStage) 707 | { 708 | $this->config->setReleaseStage($releaseStage); 709 | 710 | return $this; 711 | } 712 | 713 | /** 714 | * Set the type of application executing the code. 715 | * 716 | * This is usually used to represent if you are running plain PHP code 717 | * "php", via a framework, eg "laravel", or executing through delayed 718 | * worker code, eg "resque". 719 | * 720 | * @param string|null $type the current type 721 | * 722 | * @return $this 723 | */ 724 | public function setAppType($type) 725 | { 726 | $this->config->setAppType($type); 727 | 728 | return $this; 729 | } 730 | 731 | /** 732 | * Set the fallback application type. 733 | * 734 | * This is should be used only by libraries to set an fallback app type. 735 | * 736 | * @param string|null $type the fallback type 737 | * 738 | * @return $this 739 | */ 740 | public function setFallbackType($type) 741 | { 742 | $this->config->setFallbackType($type); 743 | 744 | return $this; 745 | } 746 | 747 | /** 748 | * Get the application data. 749 | * 750 | * @return array 751 | */ 752 | public function getAppData() 753 | { 754 | return $this->config->getAppData(); 755 | } 756 | 757 | /** 758 | * Set the hostname. 759 | * 760 | * @param string|null $hostname the hostname 761 | * 762 | * @return $this 763 | */ 764 | public function setHostname($hostname) 765 | { 766 | $this->config->setHostname($hostname); 767 | 768 | return $this; 769 | } 770 | 771 | /** 772 | * Get the device data. 773 | * 774 | * @return array 775 | */ 776 | public function getDeviceData() 777 | { 778 | return $this->config->getDeviceData(); 779 | } 780 | 781 | /** 782 | * Set custom metadata to send to Bugsnag. 783 | * 784 | * You can use this to add custom tabs of data to each error on your 785 | * Bugsnag dashboard. 786 | * 787 | * @param array[] $metaData an array of arrays of custom data 788 | * @param bool $merge should we merge the meta data 789 | * 790 | * @return $this 791 | */ 792 | public function setMetaData(array $metaData, $merge = true) 793 | { 794 | $this->config->setMetaData($metaData, $merge); 795 | 796 | return $this; 797 | } 798 | 799 | /** 800 | * Get the custom metadata to send to Bugsnag. 801 | * 802 | * @return array[] 803 | */ 804 | public function getMetaData() 805 | { 806 | return $this->config->getMetaData(); 807 | } 808 | 809 | /** 810 | * Add a single feature flag to all future reports. 811 | * 812 | * @param string $name 813 | * @param string|null $variant 814 | * 815 | * @return void 816 | */ 817 | public function addFeatureFlag($name, $variant = null) 818 | { 819 | $this->config->addFeatureFlag($name, $variant); 820 | } 821 | 822 | /** 823 | * Add multiple feature flags to all future reports. 824 | * 825 | * @param FeatureFlag[] $featureFlags 826 | * @phpstan-param list $featureFlags 827 | * 828 | * @return void 829 | */ 830 | public function addFeatureFlags(array $featureFlags) 831 | { 832 | $this->config->addFeatureFlags($featureFlags); 833 | } 834 | 835 | /** 836 | * Remove the feature flag with the given name from all future reports. 837 | * 838 | * @param string $name 839 | * 840 | * @return void 841 | */ 842 | public function clearFeatureFlag($name) 843 | { 844 | $this->config->clearFeatureFlag($name); 845 | } 846 | 847 | /** 848 | * Remove all feature flags from all future reports. 849 | * 850 | * @return void 851 | */ 852 | public function clearFeatureFlags() 853 | { 854 | $this->config->clearFeatureFlags(); 855 | } 856 | 857 | /** 858 | * Set Bugsnag's error reporting level. 859 | * 860 | * If this is not set, we'll use your current PHP error_reporting value 861 | * from your ini file or error_reporting(...) calls. 862 | * 863 | * @param int|null $errorReportingLevel the error reporting level integer 864 | * 865 | * @return $this 866 | */ 867 | public function setErrorReportingLevel($errorReportingLevel) 868 | { 869 | $this->config->setErrorReportingLevel($errorReportingLevel); 870 | 871 | return $this; 872 | } 873 | 874 | /** 875 | * Should we ignore the given error code? 876 | * 877 | * @param int $code the error code 878 | * 879 | * @return bool 880 | */ 881 | public function shouldIgnoreErrorCode($code) 882 | { 883 | return $this->config->shouldIgnoreErrorCode($code); 884 | } 885 | 886 | /** 887 | * Set notification delivery endpoint. 888 | * 889 | * @param string $endpoint 890 | * 891 | * @return $this 892 | */ 893 | public function setNotifyEndpoint($endpoint) 894 | { 895 | $this->config->setNotifyEndpoint($endpoint); 896 | 897 | return $this; 898 | } 899 | 900 | /** 901 | * Get notification delivery endpoint. 902 | * 903 | * @return string 904 | */ 905 | public function getNotifyEndpoint() 906 | { 907 | return $this->config->getNotifyEndpoint(); 908 | } 909 | 910 | /** 911 | * Set session delivery endpoint. 912 | * 913 | * @param string $endpoint 914 | * 915 | * @return $this 916 | */ 917 | public function setSessionEndpoint($endpoint) 918 | { 919 | $this->config->setSessionEndpoint($endpoint); 920 | 921 | return $this; 922 | } 923 | 924 | /** 925 | * Get session delivery endpoint. 926 | * 927 | * @return string 928 | */ 929 | public function getSessionEndpoint() 930 | { 931 | return $this->config->getSessionEndpoint(); 932 | } 933 | 934 | /** 935 | * Set the build endpoint. 936 | * 937 | * @param string $endpoint the build endpoint 938 | * 939 | * @return $this 940 | */ 941 | public function setBuildEndpoint($endpoint) 942 | { 943 | $this->config->setBuildEndpoint($endpoint); 944 | 945 | return $this; 946 | } 947 | 948 | /** 949 | * Get the build endpoint. 950 | * 951 | * @return string 952 | */ 953 | public function getBuildEndpoint() 954 | { 955 | return $this->config->getBuildEndpoint(); 956 | } 957 | 958 | /** 959 | * Set session tracking state. 960 | * 961 | * @param bool $track whether to track sessions 962 | * 963 | * @return $this 964 | */ 965 | public function setAutoCaptureSessions($track) 966 | { 967 | $this->config->setAutoCaptureSessions($track); 968 | 969 | return $this; 970 | } 971 | 972 | /** 973 | * Whether should be auto-capturing sessions. 974 | * 975 | * @return bool 976 | */ 977 | public function shouldCaptureSessions() 978 | { 979 | return $this->config->shouldCaptureSessions(); 980 | } 981 | 982 | /** 983 | * Get the session client. 984 | * 985 | * @return \GuzzleHttp\ClientInterface 986 | * 987 | * @deprecated This will be removed in the next major version. 988 | */ 989 | public function getSessionClient() 990 | { 991 | return $this->config->getSessionClient(); 992 | } 993 | 994 | /** 995 | * Set the amount to increase the memory_limit when an OOM is triggered. 996 | * 997 | * This is an amount of bytes or 'null' to disable increasing the limit. 998 | * 999 | * @param int|null $value 1000 | * 1001 | * @return Configuration 1002 | */ 1003 | public function setMemoryLimitIncrease($value) 1004 | { 1005 | return $this->config->setMemoryLimitIncrease($value); 1006 | } 1007 | 1008 | /** 1009 | * Get the amount to increase the memory_limit when an OOM is triggered. 1010 | * 1011 | * This will return 'null' if this feature is disabled. 1012 | * 1013 | * @return int|null 1014 | */ 1015 | public function getMemoryLimitIncrease() 1016 | { 1017 | return $this->config->getMemoryLimitIncrease(); 1018 | } 1019 | 1020 | /** 1021 | * Set the array of classes that should not be sent to Bugsnag. 1022 | * 1023 | * @param array $discardClasses 1024 | * 1025 | * @return $this 1026 | */ 1027 | public function setDiscardClasses(array $discardClasses) 1028 | { 1029 | $this->config->setDiscardClasses($discardClasses); 1030 | 1031 | return $this; 1032 | } 1033 | 1034 | /** 1035 | * Get the array of classes that should not be sent to Bugsnag. 1036 | * 1037 | * This can contain both fully qualified class names and regular expressions. 1038 | * 1039 | * @return array 1040 | */ 1041 | public function getDiscardClasses() 1042 | { 1043 | return $this->config->getDiscardClasses(); 1044 | } 1045 | 1046 | /** 1047 | * Set the array of metadata keys that should be redacted. 1048 | * 1049 | * @param string[] $redactedKeys 1050 | * 1051 | * @return $this 1052 | */ 1053 | public function setRedactedKeys(array $redactedKeys) 1054 | { 1055 | $this->config->setRedactedKeys($redactedKeys); 1056 | 1057 | return $this; 1058 | } 1059 | 1060 | /** 1061 | * Get the array of metadata keys that should be redacted. 1062 | * 1063 | * @return string[] 1064 | */ 1065 | public function getRedactedKeys() 1066 | { 1067 | return $this->config->getRedactedKeys(); 1068 | } 1069 | 1070 | /** 1071 | * @param int $maxBreadcrumbs 1072 | * 1073 | * @return $this 1074 | */ 1075 | public function setMaxBreadcrumbs($maxBreadcrumbs) 1076 | { 1077 | $this->recorder->setMaxBreadcrumbs($maxBreadcrumbs); 1078 | 1079 | return $this; 1080 | } 1081 | 1082 | /** 1083 | * @return int 1084 | */ 1085 | public function getMaxBreadcrumbs() 1086 | { 1087 | return $this->recorder->getMaxBreadcrumbs(); 1088 | } 1089 | } 1090 | --------------------------------------------------------------------------------