├── BugsnagBundle.php ├── Command └── .gitkeep ├── DependencyInjection ├── BaseConfiguration.php ├── BugsnagExtension.php ├── ClientFactory.php ├── ConfigurationWithReturnType.php └── ConfigurationWithoutReturnType.php ├── EventListener ├── BugsnagListener.php └── BugsnagShutdown.php ├── LICENSE ├── Request ├── SymfonyRequest.php └── SymfonyResolver.php ├── Resources ├── config │ ├── .gitkeep │ └── services.yml ├── docs │ └── index.rst ├── public │ └── .gitkeep ├── translations │ └── .gitkeep └── views │ └── .gitkeep ├── composer.json └── create-configuration-class-alias.php /BugsnagBundle.php: -------------------------------------------------------------------------------- 1 | getRootNode($treeBuilder); 30 | 31 | $rootNode 32 | ->children() 33 | ->scalarNode('api_key') 34 | ->defaultValue(getenv('BUGSNAG_API_KEY') ?: null) 35 | ->end() 36 | ->scalarNode('endpoint') 37 | ->defaultValue(getenv('BUGSNAG_ENDPOINT') ?: null) 38 | ->end() 39 | ->booleanNode('callbacks') 40 | ->defaultValue(true) 41 | ->end() 42 | ->booleanNode('user') 43 | ->defaultValue(true) 44 | ->end() 45 | ->scalarNode('app_type') 46 | ->defaultNull() 47 | ->end() 48 | ->scalarNode('app_version') 49 | ->defaultNull() 50 | ->end() 51 | ->booleanNode('batch_sending') 52 | ->defaultValue(true) 53 | ->end() 54 | ->scalarNode('hostname') 55 | ->defaultNull() 56 | ->end() 57 | ->booleanNode('send_code') 58 | ->defaultValue(true) 59 | ->end() 60 | ->scalarNode('release_stage') 61 | ->defaultNull() 62 | ->end() 63 | ->scalarNode('strip_path') 64 | ->defaultNull() 65 | ->end() 66 | ->scalarNode('project_root') 67 | ->defaultNull() 68 | ->end() 69 | ->booleanNode('auto_notify') 70 | ->defaultValue(true) 71 | ->end() 72 | ->scalarNode('resolver') 73 | ->defaultValue(SymfonyResolver::class) 74 | ->end() 75 | ->scalarNode('factory') 76 | ->defaultValue(ClientFactory::class) 77 | ->end() 78 | ->scalarNode('client') 79 | ->defaultValue(Client::class) 80 | ->end() 81 | ->scalarNode('listener') 82 | ->defaultValue(BugsnagListener::class) 83 | ->end() 84 | ->arrayNode('notify_release_stages') 85 | ->prototype('scalar')->end() 86 | ->treatNullLike([]) 87 | ->defaultValue([]) 88 | ->end() 89 | ->arrayNode('filters') 90 | ->prototype('scalar')->end() 91 | ->treatNullLike([]) 92 | ->defaultValue([]) 93 | ->end() 94 | ->scalarNode('shutdown') 95 | ->defaultValue(BugsnagShutdown::class) 96 | ->end() 97 | ->scalarNode('strip_path_regex') 98 | ->defaultNull() 99 | ->end() 100 | ->scalarNode('project_root_regex') 101 | ->defaultNull() 102 | ->end() 103 | ->scalarNode('guzzle') 104 | ->defaultNull() 105 | ->end() 106 | ->scalarNode('memory_limit_increase') 107 | ->defaultFalse() 108 | ->end() 109 | ->arrayNode('discard_classes') 110 | ->prototype('scalar')->end() 111 | ->treatNullLike([]) 112 | ->defaultValue([]) 113 | ->end() 114 | ->arrayNode('redacted_keys') 115 | ->prototype('scalar')->end() 116 | ->treatNullLike([]) 117 | ->defaultValue([]) 118 | ->end() 119 | ->arrayNode('feature_flags') 120 | ->prototype('array') 121 | ->children() 122 | ->scalarNode('name') 123 | ->isRequired() 124 | ->validate() 125 | ->ifTrue(function ($value) { 126 | return !is_string($value); 127 | }) 128 | ->thenInvalid('Feature flag name should be a string, got %s') 129 | ->end() 130 | ->end() 131 | ->scalarNode('variant')->end() 132 | ->end() 133 | ->end() 134 | ->treatNullLike([]) 135 | ->defaultValue([]) 136 | ->end() 137 | ->scalarNode('max_breadcrumbs') 138 | ->defaultNull() 139 | ->end(); 140 | 141 | return $treeBuilder; 142 | } 143 | 144 | /** 145 | * Returns the root node of TreeBuilder with backwards compatibility 146 | * for pre-Symfony 4.1. 147 | * 148 | * @param TreeBuilder $treeBuilder a TreeBuilder to extract/create the root node 149 | * from 150 | * 151 | * @return NodeDefinition the root node of the config 152 | */ 153 | protected function getRootNode(TreeBuilder $treeBuilder) 154 | { 155 | if (\method_exists($treeBuilder, 'getRootNode')) { 156 | return $treeBuilder->getRootNode(); 157 | } 158 | 159 | return $treeBuilder->root(self::ROOT_NAME); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /DependencyInjection/BugsnagExtension.php: -------------------------------------------------------------------------------- 1 | processConfiguration(new Configuration(), $configs); 23 | 24 | $loader = new YamlFileLoader( 25 | $container, 26 | new FileLocator(__DIR__.'/../Resources/config') 27 | ); 28 | 29 | $loader->load('services.yml'); 30 | 31 | foreach ($config as $key => $value) { 32 | $container->setParameter("bugsnag.{$key}", $value); 33 | } 34 | 35 | if ($container->hasParameter('kernel.project_dir')) { 36 | $symfonyRoot = $container->getParameter('kernel.project_dir'); 37 | } else { 38 | $symfonyRoot = realpath( 39 | $container->getParameter('kernel.root_dir').'/..' 40 | ); 41 | } 42 | 43 | $container->setParameter('bugsnag.symfony_root', $symfonyRoot); 44 | 45 | // Register an alias for 'bugsnag.guzzle' if the user has supplied a 46 | // service to use 47 | if (isset($config['guzzle'])) { 48 | $container->setAlias('bugsnag.guzzle', $config['guzzle']); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /DependencyInjection/ClientFactory.php: -------------------------------------------------------------------------------- 1 | 'example', 'variant' => 'test'], ['name' => 'another name']] 219 | * 220 | * @var array[] 221 | */ 222 | private $featureFlags; 223 | 224 | /** 225 | * The maximum number of breadcrumbs that are allowed to be stored. 226 | * 227 | * @var int|null 228 | */ 229 | private $maxBreadcrumbs; 230 | 231 | /** 232 | * @param SymfonyResolver $resolver 233 | * @param TokenStorageInterface|null $tokens 234 | * @param AuthorizationCheckerInterface|null $checker 235 | * @param string|null $key 236 | * @param string|null $endpoint 237 | * @param bool $callbacks 238 | * @param bool $user 239 | * @param string|null $type 240 | * @param string|null $version 241 | * @param bool $batch 242 | * @param string|null $hostname 243 | * @param bool $code 244 | * @param string|null $strip 245 | * @param string|null $project 246 | * @param string|null $root 247 | * @param string|null $env 248 | * @param string|null $stage 249 | * @param string[]|null $stages 250 | * @param string[]|null $filters 251 | * @param ShutdownStrategyInterface $shutdownStrategy 252 | * @param string|null $stripPathRegex 253 | * @param string|null $projectRootRegex 254 | * @param GuzzleHttp\ClientInterface|null $guzzle 255 | * @param int|null|false $memoryLimitIncrease 256 | * @param array $discardClasses 257 | * @param string[] $redactedKeys 258 | * @param array[] $featureFlags 259 | * @param int|null $maxBreadcrumbs 260 | * 261 | * @return void 262 | */ 263 | public function __construct( 264 | SymfonyResolver $resolver, 265 | $tokens = null, 266 | $checker = null, 267 | $key = null, 268 | $endpoint = null, 269 | $callbacks = true, 270 | $user = true, 271 | $type = null, 272 | $version = true, 273 | $batch = null, 274 | $hostname = null, 275 | $code = true, 276 | $strip = null, 277 | $project = null, 278 | $root = null, 279 | $env = null, 280 | $stage = null, 281 | $stages = null, 282 | $filters = null, 283 | $shutdownStrategy = null, 284 | $stripPathRegex = null, 285 | $projectRootRegex = null, 286 | $guzzle = null, 287 | $memoryLimitIncrease = false, 288 | array $discardClasses = [], 289 | array $redactedKeys = [], 290 | array $featureFlags = [], 291 | $maxBreadcrumbs = null 292 | ) { 293 | $this->resolver = $resolver; 294 | $this->tokens = $tokens; 295 | $this->checker = $checker; 296 | $this->key = $key; 297 | $this->endpoint = $endpoint; 298 | $this->callbacks = $callbacks; 299 | $this->user = $user; 300 | $this->type = $type; 301 | $this->version = $version; 302 | $this->batch = $batch; 303 | $this->hostname = $hostname; 304 | $this->code = $code; 305 | $this->strip = $strip; 306 | $this->project = $project; 307 | $this->root = $root; 308 | $this->env = $env; 309 | $this->stage = $stage; 310 | $this->stages = $stages; 311 | $this->filters = $filters; 312 | $this->shutdownStrategy = $shutdownStrategy; 313 | $this->stripPathRegex = $stripPathRegex; 314 | $this->projectRootRegex = $projectRootRegex; 315 | $this->guzzle = $guzzle === null 316 | ? Client::makeGuzzle() 317 | : $guzzle; 318 | $this->memoryLimitIncrease = $memoryLimitIncrease; 319 | $this->discardClasses = $discardClasses; 320 | $this->redactedKeys = $redactedKeys; 321 | $this->featureFlags = $featureFlags; 322 | $this->maxBreadcrumbs = $maxBreadcrumbs; 323 | } 324 | 325 | /** 326 | * Make a new client instance. 327 | * 328 | * @return \Bugsnag\Client 329 | */ 330 | public function make() 331 | { 332 | $client = new Client( 333 | new Config($this->key ?: ''), 334 | $this->resolver, 335 | $this->guzzle, 336 | $this->shutdownStrategy 337 | ); 338 | 339 | if ($this->callbacks) { 340 | $client->registerDefaultCallbacks(); 341 | } 342 | 343 | if ($this->tokens && $this->checker && $this->user) { 344 | $this->setupUserDetection($client, $this->tokens, $this->checker); 345 | } 346 | 347 | $this->setupPaths($client); 348 | 349 | $client->setReleaseStage($this->stage ?: ($this->env === 'prod' ? 'production' : $this->env)); 350 | 351 | $client->setAppVersion($this->version); 352 | $client->setFallbackType('Console'); 353 | $client->setAppType($this->type); 354 | 355 | $client->setBatchSending($this->batch); 356 | $client->setHostname($this->hostname); 357 | $client->setSendCode($this->code); 358 | 359 | $client->getConfig()->mergeDeviceData(['runtimeVersions' => ['symfony' => Kernel::VERSION]]); 360 | 361 | $client->setNotifier([ 362 | 'name' => 'Bugsnag Symfony', 363 | 'version' => BugsnagBundle::VERSION, 364 | 'url' => 'https://github.com/bugsnag/bugsnag-symfony', 365 | ]); 366 | 367 | if ($this->endpoint !== null) { 368 | $client->setNotifyEndpoint($this->endpoint); 369 | } 370 | 371 | if ($this->stages) { 372 | $client->setNotifyReleaseStages($this->stages); 373 | } 374 | 375 | if ($this->filters) { 376 | $client->setFilters($this->filters); 377 | } 378 | 379 | // "false" is used as a sentinel here because "null" is a valid value 380 | if ($this->memoryLimitIncrease !== false) { 381 | $client->setMemoryLimitIncrease($this->memoryLimitIncrease); 382 | } 383 | 384 | if ($this->discardClasses) { 385 | $client->setDiscardClasses($this->discardClasses); 386 | } 387 | 388 | if ($this->redactedKeys) { 389 | $client->setRedactedKeys($this->redactedKeys); 390 | } 391 | 392 | if ($this->featureFlags) { 393 | $featureFlags = array_map(function (array $flag) { 394 | if (array_key_exists('variant', $flag)) { 395 | return new FeatureFlag($flag['name'], $flag['variant']); 396 | } 397 | 398 | return new FeatureFlag($flag['name']); 399 | }, $this->featureFlags); 400 | 401 | $client->addFeatureFlags($featureFlags); 402 | } 403 | 404 | if ($this->maxBreadcrumbs !== null) { 405 | $client->setMaxBreadcrumbs($this->maxBreadcrumbs); 406 | } 407 | 408 | return $client; 409 | } 410 | 411 | /** 412 | * Setup user detection. 413 | * 414 | * @param \Bugsnag\Client $client 415 | * @param \Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface $tokens 416 | * @param \Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface $checker 417 | * 418 | * @return void 419 | */ 420 | protected function setupUserDetection(Client $client, TokenStorageInterface $tokens, AuthorizationCheckerInterface $checker) 421 | { 422 | $client->registerCallback(new CustomUser(function () use ($tokens, $checker) { 423 | $token = $tokens->getToken(); 424 | 425 | if (!$token || !$checker->isGranted('IS_AUTHENTICATED_REMEMBERED')) { 426 | return; 427 | } 428 | 429 | $user = $token->getUser(); 430 | 431 | if ($user instanceof UserInterface) { 432 | // Symfony 5.3 introduced 'getUserIdentifier' to replace 'getUsername' 433 | // Symfony 6.0 removed 'getUsername' 434 | if (method_exists(UserInterface::class, 'getUserIdentifier')) { 435 | return ['id' => $user->getUserIdentifier()]; 436 | } 437 | 438 | // Symfony 5.4 and below use 'getUsername' ('getUserIdentifier' 439 | // wasn't added to the interface until Symfony 6.0 for BC) 440 | if (method_exists(UserInterface::class, 'getUsername')) { 441 | return ['id' => $user->getUsername()]; 442 | } 443 | 444 | // if neither method exists then we don't know how to deal with 445 | // this version of Symfony's UserInterface, so can't get an ID 446 | return; 447 | } 448 | 449 | return ['id' => (string) $user]; 450 | })); 451 | } 452 | 453 | /** 454 | * Setup the client paths. 455 | * 456 | * @param Client $client 457 | * 458 | * @return void 459 | */ 460 | protected function setupPaths(Client $client) 461 | { 462 | if ($this->projectRootRegex !== null) { 463 | $client->setProjectRootRegex($this->projectRootRegex); 464 | } elseif ($this->project !== null) { 465 | $client->setProjectRoot($this->project); 466 | } else { 467 | $client->setProjectRoot($this->root.DIRECTORY_SEPARATOR.'src'); 468 | } 469 | 470 | if ($this->stripPathRegex !== null) { 471 | $client->setStripPathRegex($this->stripPathRegex); 472 | } elseif ($this->strip !== null) { 473 | $client->setStripPath($this->strip); 474 | } else { 475 | $client->setStripPath($this->root); 476 | } 477 | } 478 | } 479 | -------------------------------------------------------------------------------- /DependencyInjection/ConfigurationWithReturnType.php: -------------------------------------------------------------------------------- 1 | client = $client; 66 | $this->resolver = $resolver; 67 | $this->auto = $auto; 68 | } 69 | 70 | /** 71 | * Handle an incoming request. 72 | * 73 | * @param GetResponseEvent|RequestEvent $event 74 | * 75 | * @return void 76 | */ 77 | public function onKernelRequest($event) 78 | { 79 | // Compatibility with Symfony < 5 and Symfony >=5 80 | if (!$event instanceof GetResponseEvent && !$event instanceof RequestEvent) { 81 | throw new InvalidArgumentException('onKernelRequest function only accepts GetResponseEvent and RequestEvent arguments'); 82 | } 83 | 84 | if (!$this->isMainRequest($event->getRequestType())) { 85 | return; 86 | } 87 | 88 | $this->client->setFallbackType('HTTP'); 89 | 90 | $this->resolver->set($event->getRequest()); 91 | } 92 | 93 | /** 94 | * Handle an http kernel exception. 95 | * 96 | * @param GetResponseForExceptionEvent|ExceptionEvent $event 97 | * 98 | * @return void 99 | */ 100 | public function onKernelException($event) 101 | { 102 | $throwable = $this->resolveThrowable($event); 103 | 104 | if ($this->isOom($throwable) 105 | && $this->client->getMemoryLimitIncrease() !== null 106 | && preg_match($this->oomRegex, $throwable->getMessage(), $matches) === 1 107 | ) { 108 | $currentMemoryLimit = (int) $matches[1]; 109 | 110 | ini_set('memory_limit', $currentMemoryLimit + $this->client->getMemoryLimitIncrease()); 111 | } 112 | 113 | $this->sendNotify($throwable, []); 114 | } 115 | 116 | /** 117 | * Handle a console exception (used instead of ConsoleErrorEvent before 118 | * Symfony 3.3 and kept for backwards compatibility). 119 | * 120 | * @param \Symfony\Component\Console\Event\ConsoleExceptionEvent $event 121 | * 122 | * @return void 123 | */ 124 | public function onConsoleException(ConsoleExceptionEvent $event) 125 | { 126 | $meta = ['status' => $event->getExitCode()]; 127 | if ($event->getCommand()) { 128 | $meta['name'] = $event->getCommand()->getName(); 129 | } 130 | $this->sendNotify($event->getException(), ['command' => $meta]); 131 | } 132 | 133 | /** 134 | * Handle a console error. 135 | * 136 | * @param \Symfony\Component\Console\Event\ConsoleErrorEvent $event 137 | * 138 | * @return void 139 | */ 140 | public function onConsoleError(ConsoleErrorEvent $event) 141 | { 142 | $meta = ['status' => $event->getExitCode()]; 143 | if ($event->getCommand()) { 144 | $meta['name'] = $event->getCommand()->getName(); 145 | } 146 | $this->sendNotify($event->getError(), ['command' => $meta]); 147 | } 148 | 149 | /** 150 | * Handle a failing message. 151 | * 152 | * @param \Symfony\Component\Messenger\Event\WorkerMessageFailedEvent $event 153 | * 154 | * @return void 155 | */ 156 | public function onWorkerMessageFailed(WorkerMessageFailedEvent $event) 157 | { 158 | $this->sendNotify( 159 | $event->getThrowable(), 160 | ['Messenger' => ['willRetry' => $event->willRetry()]] 161 | ); 162 | 163 | // Normally we flush after a message has been handled, but this event 164 | // doesn't fire for failed messages so we have to flush here instead 165 | $this->client->flush(); 166 | } 167 | 168 | /** 169 | * Flush any accumulated reports after a message has been handled. 170 | * 171 | * In batch sending mode reports are usually sent on shutdown but workers 172 | * are (generally) long running processes so this doesn't work. Instead we 173 | * flush after each handled message 174 | * 175 | * @param WorkerMessageHandledEvent $event 176 | * 177 | * @return void 178 | */ 179 | public function onWorkerMessageHandled(WorkerMessageHandledEvent $event) 180 | { 181 | $this->client->flush(); 182 | } 183 | 184 | /** 185 | * @param \Throwable $throwable 186 | * @param array $meta 187 | * 188 | * @return void 189 | */ 190 | private function sendNotify($throwable, $meta) 191 | { 192 | if (!$this->auto) { 193 | return; 194 | } 195 | 196 | $report = Report::fromPHPThrowable( 197 | $this->client->getConfig(), 198 | $throwable 199 | ); 200 | $report->setUnhandled(true); 201 | $report->setSeverity('error'); 202 | $report->setSeverityReason([ 203 | 'type' => 'unhandledExceptionMiddleware', 204 | 'attributes' => [ 205 | 'framework' => 'Symfony', 206 | ], 207 | ]); 208 | $report->setMetaData($meta); 209 | 210 | $this->client->notify($report); 211 | } 212 | 213 | /** 214 | * @param GetResponseForExceptionEvent|ExceptionEvent $event 215 | * 216 | * @return \Throwable 217 | */ 218 | private function resolveThrowable($event) 219 | { 220 | // Compatibility with Symfony < 5 and Symfony >=5 221 | // The additional `method_exists` check is to prevent errors in Symfony 4.3 222 | // where the ExceptionEvent exists and is used but doesn't implement 223 | // the `getThrowable` method, which was introduced in Symfony 4.4 224 | if ($event instanceof ExceptionEvent && method_exists($event, 'getThrowable')) { 225 | return $event->getThrowable(); 226 | } 227 | 228 | if ($event instanceof GetResponseForExceptionEvent) { 229 | return $event->getException(); 230 | } 231 | 232 | throw new InvalidArgumentException('onKernelException function only accepts GetResponseForExceptionEvent and ExceptionEvent arguments'); 233 | } 234 | 235 | /** 236 | * Check if this $throwable is an OOM. 237 | * 238 | * This will be represented by an "OutOfMemoryError" on Symfony 4.4+ or an 239 | * "OutOfMemoryException" on earlier versions. 240 | * 241 | * @param \Throwable $throwable 242 | * 243 | * @return bool 244 | */ 245 | private function isOom($throwable) 246 | { 247 | return $throwable instanceof OutOfMemoryError 248 | || $throwable instanceof OutOfMemoryException; 249 | } 250 | 251 | /** 252 | * Check if the given request type is the "main request" 253 | * 254 | * @param int $requestType 255 | * @return bool 256 | */ 257 | private function isMainRequest($requestType) 258 | { 259 | // Symfony 7+ uses the 'MAIN_REQUEST' constant 260 | return defined(HttpKernelInterface::class.'::MAIN_REQUEST') 261 | ? $requestType === HttpKernelInterface::MAIN_REQUEST 262 | : $requestType === HttpKernelInterface::MASTER_REQUEST; 263 | } 264 | 265 | /** 266 | * @return array 267 | */ 268 | public static function getSubscribedEvents() 269 | { 270 | $listeners = [ 271 | KernelEvents::REQUEST => ['onKernelRequest', 256], 272 | KernelEvents::EXCEPTION => ['onKernelException', 128], 273 | ]; 274 | 275 | // Added ConsoleEvents in Symfony 2.3 276 | if (class_exists(ConsoleEvents::class)) { 277 | // Added with ConsoleEvents::ERROR in Symfony 3.3 to deprecate ConsoleEvents::EXCEPTION 278 | if (class_exists(ConsoleErrorEvent::class)) { 279 | $listeners[ConsoleEvents::ERROR] = ['onConsoleError', 128]; 280 | } else { 281 | $listeners[ConsoleEvents::EXCEPTION] = ['onConsoleException', 128]; 282 | } 283 | } 284 | 285 | if (class_exists(WorkerMessageFailedEvent::class)) { 286 | // This must run after Symfony's "SendFailedMessageForRetryListener" 287 | // as it sets the "willRetry" flag 288 | $listeners[WorkerMessageFailedEvent::class] = ['onWorkerMessageFailed', 64]; 289 | } 290 | 291 | if (class_exists(WorkerMessageHandledEvent::class)) { 292 | $listeners[WorkerMessageHandledEvent::class] = ['onWorkerMessageHandled', 128]; 293 | } 294 | 295 | return $listeners; 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /EventListener/BugsnagShutdown.php: -------------------------------------------------------------------------------- 1 | ['onTerminate', 10], 39 | ]; 40 | 41 | // Added ConsoleEvents in Symfony 2.3 42 | if (class_exists('Symfony\Component\Console\ConsoleEvents')) { 43 | $listeners[ConsoleEvents::TERMINATE] = ['onTerminate', 10]; 44 | } 45 | 46 | return $listeners; 47 | } 48 | 49 | /** 50 | * Called when Symfony shuts down the kernel (after response has been sent). 51 | * 52 | * @return void 53 | */ 54 | public function onTerminate() 55 | { 56 | if ($this->client) { 57 | $this->client->flush(); 58 | } 59 | } 60 | 61 | /** 62 | * Implement the ShutdownStrategyInterface. 63 | * 64 | * @param \Bugsnag\Client $client 65 | * 66 | * @return void 67 | */ 68 | public function registerShutdownStrategy(Client $client) 69 | { 70 | /* 71 | * Set a reference to the client. This "enables" the onTerminate event. 72 | */ 73 | $this->client = $client; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 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 | -------------------------------------------------------------------------------- /Request/SymfonyRequest.php: -------------------------------------------------------------------------------- 1 | request = $request; 27 | } 28 | 29 | /** 30 | * Are we currently processing a request? 31 | * 32 | * @return bool 33 | */ 34 | public function isRequest() 35 | { 36 | return true; 37 | } 38 | 39 | /** 40 | * Get the session data. 41 | * 42 | * @return array 43 | */ 44 | public function getSession() 45 | { 46 | $session = null; 47 | if ($this->request->hasPreviousSession()) { 48 | $session = $this->request->getSession(); 49 | } 50 | 51 | return $session ? $session->all() : []; 52 | } 53 | 54 | /** 55 | * Get the cookies. 56 | * 57 | * @return array 58 | */ 59 | public function getCookies() 60 | { 61 | return $this->request->cookies->all(); 62 | } 63 | 64 | /** 65 | * Get the request formatted as meta data. 66 | * 67 | * @return array 68 | */ 69 | public function getMetaData() 70 | { 71 | $data = []; 72 | 73 | $data['url'] = $this->request->getUri(); 74 | 75 | $data['httpMethod'] = $this->request->getMethod(); 76 | 77 | $data['params'] = $this->getInput() + $this->request->query->all(); 78 | 79 | $data['clientIp'] = $this->request->getClientIp(); 80 | 81 | if ($agent = $this->request->headers->get('User-Agent')) { 82 | $data['userAgent'] = $agent; 83 | } 84 | 85 | if ($headers = $this->request->headers->all()) { 86 | $data['headers'] = $headers; 87 | } 88 | 89 | return ['request' => $data]; 90 | } 91 | 92 | /** 93 | * Get the input source for the request. 94 | * 95 | * This is based on Laravel's input source generation. 96 | * 97 | * @return array 98 | */ 99 | protected function getInput() 100 | { 101 | if ($this->isJsonContentType($this->request->headers->get('CONTENT_TYPE'))) { 102 | $parsed = json_decode($this->request->getContent(), true); 103 | 104 | if (is_array($parsed)) { 105 | return $parsed; 106 | } 107 | } 108 | 109 | return $this->request->request->all(); 110 | } 111 | 112 | /** 113 | * Get the request context. 114 | * 115 | * @return string|null 116 | */ 117 | public function getContext() 118 | { 119 | return $this->request->getMethod().' '.$this->request->getPathInfo(); 120 | } 121 | 122 | /** 123 | * Get the request user id. 124 | * 125 | * @return string|null 126 | */ 127 | public function getUserId() 128 | { 129 | return $this->request->getClientIp(); 130 | } 131 | 132 | /** 133 | * @param mixed $contentType 134 | * 135 | * @return bool 136 | */ 137 | private function isJsonContentType($contentType) 138 | { 139 | if (is_string($contentType)) { 140 | return stripos($contentType, '/json') !== false 141 | || stripos($contentType, '+json') !== false; 142 | } 143 | 144 | return false; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /Request/SymfonyResolver.php: -------------------------------------------------------------------------------- 1 | request = $request; 28 | } 29 | 30 | /** 31 | * Resolve the current request. 32 | * 33 | * @return \Bugsnag\Request\RequestInterface 34 | */ 35 | public function resolve() 36 | { 37 | if (!$this->request) { 38 | return new NullRequest(); 39 | } 40 | 41 | return new SymfonyRequest($this->request); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Resources/config/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bugsnag/bugsnag-symfony/6d4b47a45970d6e4647c6ea62e57637a5ee959bd/Resources/config/.gitkeep -------------------------------------------------------------------------------- /Resources/config/services.yml: -------------------------------------------------------------------------------- 1 | services: 2 | bugsnag.resolver: 3 | class: '%bugsnag.resolver%' 4 | 5 | bugsnag.factory: 6 | class: '%bugsnag.factory%' 7 | arguments: 8 | - '@bugsnag.resolver' 9 | - '@?security.token_storage' 10 | - '@?security.authorization_checker' 11 | - '%bugsnag.api_key%' 12 | - '%bugsnag.endpoint%' 13 | - '%bugsnag.callbacks%' 14 | - '%bugsnag.user%' 15 | - '%bugsnag.app_type%' 16 | - '%bugsnag.app_version%' 17 | - '%bugsnag.batch_sending%' 18 | - '%bugsnag.hostname%' 19 | - '%bugsnag.send_code%' 20 | - '%bugsnag.strip_path%' 21 | - '%bugsnag.project_root%' 22 | - '%bugsnag.symfony_root%' 23 | - '%kernel.environment%' 24 | - '%bugsnag.release_stage%' 25 | - '%bugsnag.notify_release_stages%' 26 | - '%bugsnag.filters%' 27 | - '@bugsnag.shutdown' 28 | - '%bugsnag.strip_path_regex%' 29 | - '%bugsnag.project_root_regex%' 30 | - '@?bugsnag.guzzle' 31 | - '%bugsnag.memory_limit_increase%' 32 | - '%bugsnag.discard_classes%' 33 | - '%bugsnag.redacted_keys%' 34 | - '%bugsnag.feature_flags%' 35 | - '%bugsnag.max_breadcrumbs%' 36 | 37 | bugsnag: 38 | class: '%bugsnag.client%' 39 | factory: ['@bugsnag.factory', make] 40 | public: true 41 | 42 | bugsnag.listener: 43 | class: '%bugsnag.listener%' 44 | arguments: 45 | - '@bugsnag' 46 | - '@bugsnag.resolver' 47 | - '%bugsnag.auto_notify%' 48 | tags: 49 | - { name: "kernel.event_subscriber" } 50 | 51 | # A Bugsnag ShutdownStrategy implemented via a Symfony event subscriber 52 | bugsnag.shutdown: 53 | class: '%bugsnag.shutdown%' 54 | tags: 55 | - { name: "kernel.event_subscriber" } 56 | -------------------------------------------------------------------------------- /Resources/docs/index.rst: -------------------------------------------------------------------------------- 1 | Bugsnag Notifier for Symfony 2 | ============================ 3 | 4 | The Bugsnag Notifier for Symfony gives you instant notification of errors and exceptions in your symfony PHP applications. 5 | 6 | See our main PHP repo for all information regarding usage at https://github.com/bugsnag/bugsnag-php. 7 | -------------------------------------------------------------------------------- /Resources/public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bugsnag/bugsnag-symfony/6d4b47a45970d6e4647c6ea62e57637a5ee959bd/Resources/public/.gitkeep -------------------------------------------------------------------------------- /Resources/translations/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bugsnag/bugsnag-symfony/6d4b47a45970d6e4647c6ea62e57637a5ee959bd/Resources/translations/.gitkeep -------------------------------------------------------------------------------- /Resources/views/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bugsnag/bugsnag-symfony/6d4b47a45970d6e4647c6ea62e57637a5ee959bd/Resources/views/.gitkeep -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bugsnag/bugsnag-symfony", 3 | "description": "Official Bugsnag notifier for Symfony applications.", 4 | "keywords": ["bugsnag", "exceptions", "errors", "logging", "tracking", "symfony"], 5 | "homepage": "https://github.com/bugsnag/bugsnag-symfony", 6 | "type": "symfony-bundle", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "James Smith", 11 | "email": "notifiers@bugsnag.com" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=5.5", 16 | "bugsnag/bugsnag": "^3.29.0", 17 | "symfony/config": "^2.7|^3|^4|^5|^6|^7", 18 | "symfony/console": "^2.7|^3|^4|^5|^6|^7", 19 | "symfony/dependency-injection": "^2.7|^3|^4|^5|^6|^7", 20 | "symfony/http-foundation": "^2.7|^3|^4|^5|^6|^7", 21 | "symfony/http-kernel": "^2.7|^3|^4|^5|^6|^7", 22 | "symfony/security-core": "^2.7|^3|^4|^5|^6|^7" 23 | }, 24 | "require-dev": { 25 | "graham-campbell/testbench-core": "^1.1", 26 | "mockery/mockery": "^0.9.4|^1.3.1", 27 | "phpunit/phpunit": "^4.8.36|^7.5.15|^9.3.10" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "Bugsnag\\BugsnagBundle\\": "" 32 | }, 33 | "files": ["create-configuration-class-alias.php"] 34 | }, 35 | "extra": { 36 | "branch-alias": { 37 | "dev-master": "1.6-dev" 38 | } 39 | }, 40 | "scripts": { 41 | "test": "phpunit" 42 | }, 43 | "minimum-stability": "dev", 44 | "prefer-stable": true 45 | } 46 | -------------------------------------------------------------------------------- /create-configuration-class-alias.php: -------------------------------------------------------------------------------- 1 | = 7 7 | ? \Bugsnag\BugsnagBundle\DependencyInjection\ConfigurationWithReturnType::class 8 | : \Bugsnag\BugsnagBundle\DependencyInjection\ConfigurationWithoutReturnType::class; 9 | 10 | class_alias($class, \Bugsnag\BugsnagBundle\DependencyInjection\Configuration::class); 11 | } 12 | --------------------------------------------------------------------------------