├── .php-cs-fixer.cache ├── .php-cs-fixer.php ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json └── src ├── Api.php ├── Concerns ├── HasContext.php └── UsesTime.php ├── Context ├── ConsoleContext.php ├── ContextContextDetector.php ├── ContextDetectorInterface.php ├── ContextInterface.php └── RequestContext.php ├── Contracts └── ProvidesFlareContext.php ├── Enums ├── GroupingTypes.php └── MessageLevels.php ├── Flare.php ├── Frame.php ├── Glows ├── Glow.php └── Recorder.php ├── Http ├── Client.php ├── Exceptions │ ├── BadResponse.php │ ├── BadResponseCode.php │ ├── InvalidData.php │ ├── MissingParameter.php │ └── NotFound.php └── Response.php ├── Middleware ├── AddGlows.php ├── AnonymizeIp.php └── CensorRequestBodyFields.php ├── Report.php ├── Solutions └── ReportSolution.php ├── Stacktrace ├── Codesnippet.php ├── File.php ├── Frame.php └── Stacktrace.php ├── Time ├── SystemTime.php └── Time.php ├── Truncation ├── AbstractTruncationStrategy.php ├── ReportTrimmer.php ├── TrimContextItemsStrategy.php ├── TrimStringsStrategy.php └── TruncationStrategy.php ├── View.php └── helpers.php /.php-cs-fixer.cache: -------------------------------------------------------------------------------- 1 | {"php":"8.3.6","version":"3.54.0","indent":" ","lineEnding":"\n","rules":{"binary_operator_spaces":true,"blank_line_after_opening_tag":true,"blank_line_between_import_groups":true,"blank_lines_before_namespace":true,"braces_position":{"allow_single_line_empty_anonymous_classes":true},"class_definition":{"inline_constructor_arguments":false,"space_before_parenthesis":true},"compact_nullable_type_declaration":true,"declare_equal_normalize":true,"lowercase_cast":true,"lowercase_static_reference":true,"new_with_parentheses":true,"no_blank_lines_after_class_opening":true,"no_leading_import_slash":true,"no_whitespace_in_blank_line":true,"ordered_class_elements":{"order":["use_trait"]},"ordered_imports":{"sort_algorithm":"alpha"},"return_type_declaration":true,"short_scalar_cast":true,"single_import_per_statement":{"group_to_single_imports":false},"single_trait_insert_per_statement":true,"ternary_operator_spaces":true,"unary_operator_spaces":true,"visibility_required":true,"blank_line_after_namespace":true,"constant_case":true,"control_structure_braces":true,"control_structure_continuation_position":true,"elseif":true,"function_declaration":true,"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"method_argument_space":{"on_multiline":"ensure_fully_multiline","keep_multiple_spaces_after_comma":true},"no_break_comment":true,"no_closing_tag":true,"no_multiple_statements_per_line":true,"no_space_around_double_colon":true,"no_spaces_after_function_name":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_class_element_per_statement":{"elements":["property"]},"single_line_after_imports":true,"spaces_inside_parentheses":true,"statement_indentation":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"encoding":true,"full_opening_tag":true,"array_syntax":{"syntax":"short"},"no_unused_imports":true,"not_operator_with_successor_space":true,"trailing_comma_in_multiline":true,"phpdoc_scalar":true,"blank_line_before_statement":{"statements":["break","continue","declare","return","throw","try"]},"phpdoc_single_line_var_spacing":true,"phpdoc_var_without_name":true,"class_attributes_separation":{"elements":{"method":"one"}}},"hashes":{"src\/Time\/SystemTime.php":"5838b8473deee6b6bda2f623aa526c82","src\/Time\/Time.php":"8aafad8ab84c7fc24567f6e476760464","src\/Api.php":"f390f7ff27063456cf153988ce5728a4","src\/Context\/ContextDetectorInterface.php":"5800d7348e4d65e7a4b4882c9c997d9f","src\/Context\/ContextContextDetector.php":"f4a9f8a79a1b87f9f832eaf08b2947f0","src\/Context\/RequestContext.php":"31c67b3cc5ba23d3b977b1bc1da5f910","src\/Context\/ContextInterface.php":"3bdd9d26710a740fda8321a2459ff62d","src\/Context\/ConsoleContext.php":"143c2f6872d95bc63eb7ed3b7195944e","src\/Contracts\/ProvidesFlareContext.php":"1fdcd629c55934bed980d31c5b356ac2","src\/Middleware\/CensorRequestBodyFields.php":"bd0c8007ae202a3c537867b5af9cf9e8","src\/Middleware\/AddGlows.php":"0ea8fc9d5cd432b4ca968dbe4e1b1ed5","src\/Middleware\/AnonymizeIp.php":"dec24eec79f306e919bdd23a2c1f52f2","src\/Flare.php":"74c279120164f45f5f08ed01e0f85932","src\/Http\/Response.php":"227867e59dd43be5bddca3b2b67d4b42","src\/Http\/Exceptions\/NotFound.php":"6f287649302eea50b946130dae38bd78","src\/Http\/Exceptions\/BadResponseCode.php":"33d5627019762becccf85f5b76611b07","src\/Http\/Exceptions\/MissingParameter.php":"f8a6b4f6df96e8b83ad1d3ac6a407fe1","src\/Http\/Exceptions\/InvalidData.php":"9750c391272f8c81025b3f25688d2d40","src\/Http\/Exceptions\/BadResponse.php":"93de87dd1b1d1224af3ed1226997838a","src\/Http\/Client.php":"7d8fe30f64e03b6e9f15a01a4f226c09","src\/Stacktrace\/Codesnippet.php":"a84034438ff5cdbd91bb8190adf4c49d","src\/Stacktrace\/File.php":"cc876e0d5653c825169683a21408b9fd","src\/Stacktrace\/Frame.php":"d4830df33515fff9ffac4dcbdb880fe2","src\/Stacktrace\/Stacktrace.php":"25fda1876c0b136384a0d10ef1a7f7ff","src\/Report.php":"9a24a5cc718dd2d741ee727f2416189a","src\/Concerns\/UsesTime.php":"223e34a3535f8d8bb25f116ab14d31b4","src\/Concerns\/HasContext.php":"66fc2f103c975caa8add0ccac82ed889","src\/Enums\/GroupingTypes.php":"ac4e64e5b533239c4fc04550f46a53f0","src\/Enums\/MessageLevels.php":"554000eae485763dfa86c4a3c9557289","src\/Frame.php":"17b71f917a03163ed0db422403365b3c","src\/helpers.php":"bf6711b669fb13ea826a45f06ece592b","src\/Truncation\/ReportTrimmer.php":"882d478d5f059ca1ab749b4f5f9d32b8","src\/Truncation\/TrimStringsStrategy.php":"fe01e317779996dd139c34a4d4e6bee5","src\/Truncation\/AbstractTruncationStrategy.php":"537f1dfad1e0a71671732d41012014ab","src\/Truncation\/TrimContextItemsStrategy.php":"c5d684520cf8e74685bff433c4751570","src\/Truncation\/TruncationStrategy.php":"162cbf435ad2834ec8ed0d7fdf89cd94","src\/Glows\/Glow.php":"79578a19bd423f05ba6bb1499ba7dfd7","src\/Glows\/Recorder.php":"fb13026d2aff0b8df8f8135cc873cea6","src\/View.php":"0def7a1a0a05355a055c3d27208c1000","src\/Solutions\/ReportSolution.php":"f4db1ad1b6ffb0b577ab60ae9e768658","tests\/Context\/ConsoleContextTest.php":"d065eddcb44614e4c8547c001f94fe0b","tests\/Context\/RequestContextTest.php":"303624a9707e764afb72789456cd3e74","tests\/TestClasses\/DumpDriver.php":"0875601fd193dfd4c824d1f6503de135","tests\/TestClasses\/ReportDriver.php":"90ff034be75259c7690f40cdc1610478","tests\/TestClasses\/Assert.php":"58146226b9abbe5e2fd986bfe7026760","tests\/TestClasses\/FakeTime.php":"1dcc91d8b27471edebf1bca14120de75","tests\/TestClasses\/CodeSnippetDriver.php":"ec0a577e8439ed182fa26b8589c8588b","tests\/TestClasses\/ExceptionWithContext.php":"e54083c65cc360a4b7a0b952a4c1480f","tests\/Stacktrace\/__snapshots__\/StrackTraceTest__it_can_detect_application_frames__1.php":"c5636ad81ad1e31a59ed58308e817ffb","tests\/Stacktrace\/StrackTraceTest.php":"5e636bf92bb42e45c040fec60110f303","tests\/Stacktrace\/CodesnippetTest.php":"8fbdc5f4cfeb1a74470d5b667100b712","tests\/Stacktrace\/FileTest.php":"1345f8095cb7b6e91e8fab12e530f362","tests\/Stacktrace\/ThrowAndReturnExceptionAction.php":"d3082e7de31e585a9ee245e489623607","tests\/Stacktrace\/CodeSnippetDriver.php":"9070f1bf86126eb68e4e635de950bc27","tests\/Mocks\/FakeClient.php":"daf949fdfd01336a3b74e5a8b897db72","tests\/Concerns\/MatchesDumpSnapshots.php":"e6851012e51f874d984fccfa6f2e753e","tests\/Concerns\/MatchesReportSnapshots.php":"83e6b3fcbae50867d4625de801e0e4d8","tests\/Concerns\/MatchesCodeSnippetSnapshots.php":"93546ec5663b755d6d88a11bd821ade9","tests\/TestCase.php":"9a7cc26443481361efead324ffb45f71","tests\/Truncation\/TrimStringsStrategyTest.php":"1a53f17c7ea580bed2e61172c846ec28","tests\/Truncation\/TrimContextItemsStrategyTest.php":"b5311373bcef228e7c4d3db96946e8b8","tests\/FlareTest.php":"4340dc96a20cfb1bc422564e88d8554d","tests\/Glows\/RecorderTest.php":"8f1e7402aa8384809c14865a51c39a3f","tests\/ReportTest.php":"ccd6157e538d57d85ecd7d45aaffa565"}} -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | notPath('bootstrap/*') 5 | ->notPath('storage/*') 6 | ->notPath('resources/view/mail/*') 7 | ->in([ 8 | __DIR__ . '/src', 9 | __DIR__ . '/tests', 10 | ]) 11 | ->name('*.php') 12 | ->notName('*.blade.php') 13 | ->notName('GitConflictController.php') 14 | ->ignoreDotFiles(true) 15 | ->ignoreVCS(true); 16 | 17 | return (new PhpCsFixer\Config()) 18 | ->setRules([ 19 | '@PSR12' => true, 20 | 'array_syntax' => ['syntax' => 'short'], 21 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 22 | 'no_unused_imports' => true, 23 | 'not_operator_with_successor_space' => true, 24 | 'trailing_comma_in_multiline' => true, 25 | 'phpdoc_scalar' => true, 26 | 'unary_operator_spaces' => true, 27 | 'binary_operator_spaces' => true, 28 | 'blank_line_before_statement' => [ 29 | 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], 30 | ], 31 | 'phpdoc_single_line_var_spacing' => true, 32 | 'phpdoc_var_without_name' => true, 33 | 'class_attributes_separation' => [ 34 | 'elements' => [ 35 | 'method' => 'one', 36 | ], 37 | ], 38 | 'method_argument_space' => [ 39 | 'on_multiline' => 'ensure_fully_multiline', 40 | 'keep_multiple_spaces_after_comma' => true, 41 | ], 42 | 'single_trait_insert_per_statement' => true, 43 | ]) 44 | ->setFinder($finder); 45 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `flare-client-php` will be documented in this file 4 | 5 | ## 1.9.1 - 2021-09-13 6 | 7 | - let `report` return the created report 8 | 9 | ## 1.9.0 - 2021-09-13 10 | 11 | - add report tracking uuid 12 | 13 | ## 1.8.1 - 2021-05-31 14 | 15 | - improve compatibility with Symfony 5.3 16 | 17 | ## 1.8.0 - 2021-04-30 18 | 19 | - add ability to ignore errors and exceptions (#23) 20 | - fix curl parameters 21 | 22 | ## 1.7.0 - 2021-04-12 23 | 24 | - use new Flare endpoint and allow 1 redirect to it 25 | 26 | ## 1.6.1 - 2021-04-08 27 | 28 | - make `censorRequestBodyFields` chainable 29 | 30 | ## 1.6.0 - 2021-04-08 31 | 32 | - add ability to censor request body fields (#18) 33 | 34 | ## 1.5.0 - 2021-03-31 35 | 36 | - add `determineVersionUsing` 37 | 38 | ## 1.4.0 - 2021-02-16 39 | 40 | - remove custom grouping 41 | 42 | ## 1.3.7 - 2020-10-21 43 | 44 | - allow PHP 8 45 | 46 | ## 1.3.6 - 2020-09-18 47 | 48 | - remove `larapack/dd` (#15) 49 | 50 | ## 1.3.5 - 2020-08-26 51 | 52 | - allow Laravel 8 (#13) 53 | 54 | ## 1.3.4 - 2020-07-14 55 | 56 | - use directory separator constant 57 | 58 | ## 1.3.3 - 2020-07-14 59 | 60 | - fix tests by requiring symfony/mime 61 | - display real exception class for view errors (see https://github.com/facade/ignition/discussions/237) 62 | 63 | ## 1.3.2 - 2020-03-02 64 | 65 | - allow L7 66 | 67 | ## 1.3.1 - 2019-12-15 68 | 69 | - allow var-dumper v5.0 70 | 71 | ## 1.3.0 - 2019-11-27 72 | 73 | - Allow custom grouping types 74 | 75 | ## 1.2.1 - 2019-11-19 76 | 77 | - Let `registerFlareHandlers` return $this 78 | 79 | ## 1.2.0 - 2019-11-19 80 | 81 | - Add `registerFlareHandlers` method to register error and exception handlers in non-Laravel applications 82 | - Fix get requests with query parameters (#4) 83 | 84 | ## 1.1.2 - 2019-11-08 85 | 86 | - Ignore invalid mime type detection issues 87 | 88 | ## 1.1.1 - 2019-10-07 89 | 90 | - Wrap filesize detection in try-catch block 91 | 92 | ## 1.1.0 - 2019-09-27 93 | 94 | - Add ability to log messages 95 | 96 | ## 1.0.4 - 2019-09-11 97 | 98 | - Fixes an issue when sending exceptions inside a queue worker 99 | 100 | ## 1.0.3 - 2019-09-05 101 | 102 | - Ensure valid session data 103 | 104 | ## 1.0.2 - 2019-09-05 105 | 106 | - Fix error when uploading multiple files using an array name 107 | 108 | ## 1.0.1 - 2019-09-02 109 | 110 | - Fix issue with uploaded files in request context 111 | 112 | ## 1.0.0 - 2019-08-30 113 | 114 | - initial release 115 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Facade 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Send PHP errors to Flare 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/facade/flare-client-php.svg?style=flat-square)](https://packagist.org/packages/facade/flare-client-php) 4 | ![Tests](https://github.com/facade/flare-client-php/workflows/Run%20tests/badge.svg) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/facade/flare-client-php.svg?style=flat-square)](https://packagist.org/packages/facade/flare-client-php) 6 | 7 | **This is an old version of the Flare client suited for older Laravel (< 9) and PHP (< 8.0) versions, if you're using a more up to date version of Laravel or PHP please use [spatie/flare-client-php](https://github.com/spatie/flare-client-php)** 8 | 9 | This repository contains a PHP client to send PHP errors to [Flare](https://flareapp.io). 10 | 11 | ![Screenshot of error in Flare](https://facade.github.io/flare-client-php/screenshot.png) 12 | 13 | ## Documentation 14 | 15 | You can find the documentation of this package at [the docs of Flare](https://flareapp.io/docs/flare/general/welcome-to-flare). 16 | 17 | ## Changelog 18 | 19 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 20 | 21 | ## Testing 22 | 23 | ``` bash 24 | composer test 25 | ``` 26 | 27 | ## Contributing 28 | 29 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 30 | 31 | ## Security 32 | 33 | If you discover any security related issues, please email support@flareapp.io instead of using the issue tracker. 34 | 35 | ## License 36 | 37 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 38 | 39 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "facade/flare-client-php", 3 | "description": "Send PHP errors to Flare", 4 | "keywords": [ 5 | "facade", 6 | "flare", 7 | "exception", 8 | "reporting" 9 | ], 10 | "homepage": "https://github.com/facade/flare-client-php", 11 | "license": "MIT", 12 | "require": { 13 | "php": "^7.1|^8.0", 14 | "facade/ignition-contracts": "~1.0", 15 | "illuminate/pipeline": "^5.5|^6.0|^7.0|^8.0", 16 | "symfony/http-foundation": "^3.3|^4.1|^5.0", 17 | "symfony/mime": "^3.4|^4.0|^5.1", 18 | "symfony/var-dumper": "^3.4|^4.0|^5.0" 19 | }, 20 | "require-dev": { 21 | "friendsofphp/php-cs-fixer": "^2.14", 22 | "spatie/phpunit-snapshot-assertions": "^2.0", 23 | "phpunit/phpunit": "^7.5" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "Facade\\FlareClient\\": "src" 28 | }, 29 | "files": [ 30 | "src/helpers.php" 31 | ] 32 | }, 33 | "autoload-dev": { 34 | "psr-4": { 35 | "Facade\\FlareClient\\Tests\\": "tests" 36 | } 37 | }, 38 | "scripts": { 39 | "format": "vendor/bin/php-cs-fixer fix --allow-risky=yes", 40 | "test": "vendor/bin/phpunit", 41 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage" 42 | 43 | }, 44 | "config": { 45 | "sort-packages": true 46 | }, 47 | "extra": { 48 | "branch-alias": { 49 | "dev-master": "1.0-dev" 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Api.php: -------------------------------------------------------------------------------- 1 | client = $client; 23 | 24 | register_shutdown_function([$this, 'sendQueuedReports']); 25 | } 26 | 27 | public static function sendReportsInBatches(bool $batchSending = true) 28 | { 29 | static::$sendInBatches = $batchSending; 30 | } 31 | 32 | public function report(Report $report) 33 | { 34 | try { 35 | if (static::$sendInBatches) { 36 | $this->addReportToQueue($report); 37 | } else { 38 | $this->sendReportToApi($report); 39 | } 40 | } catch (Exception $e) { 41 | // 42 | } 43 | } 44 | 45 | public function sendTestReport(Report $report) 46 | { 47 | $this->sendReportToApi($report); 48 | } 49 | 50 | protected function addReportToQueue(Report $report) 51 | { 52 | $this->queue[] = $report; 53 | } 54 | 55 | public function sendQueuedReports() 56 | { 57 | try { 58 | foreach ($this->queue as $report) { 59 | $this->sendReportToApi($report); 60 | } 61 | } catch (Exception $e) { 62 | // 63 | } finally { 64 | $this->queue = []; 65 | } 66 | } 67 | 68 | protected function sendReportToApi(Report $report) 69 | { 70 | $this->client->post('reports', $this->truncateReport($report->toArray())); 71 | } 72 | 73 | protected function truncateReport(array $payload): array 74 | { 75 | return (new ReportTrimmer())->trim($payload); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Concerns/HasContext.php: -------------------------------------------------------------------------------- 1 | stage = $stage; 19 | 20 | return $this; 21 | } 22 | 23 | public function messageLevel(?string $messageLevel) 24 | { 25 | $this->messageLevel = $messageLevel; 26 | 27 | return $this; 28 | } 29 | 30 | public function getGroup(string $groupName = 'context', $default = []): array 31 | { 32 | return $this->userProvidedContext[$groupName] ?? $default; 33 | } 34 | 35 | public function context($key, $value) 36 | { 37 | return $this->group('context', [$key => $value]); 38 | } 39 | 40 | public function group(string $groupName, array $properties) 41 | { 42 | $group = $this->userProvidedContext[$groupName] ?? []; 43 | 44 | $this->userProvidedContext[$groupName] = array_merge_recursive_distinct( 45 | $group, 46 | $properties 47 | ); 48 | 49 | return $this; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Concerns/UsesTime.php: -------------------------------------------------------------------------------- 1 | getCurrentTime(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Context/ConsoleContext.php: -------------------------------------------------------------------------------- 1 | arguments = $arguments; 13 | } 14 | 15 | public function toArray(): array 16 | { 17 | return [ 18 | 'arguments' => $this->arguments, 19 | ]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Context/ContextContextDetector.php: -------------------------------------------------------------------------------- 1 | runningInConsole()) { 10 | return new ConsoleContext($_SERVER['argv'] ?? []); 11 | } 12 | 13 | return new RequestContext(); 14 | } 15 | 16 | private function runningInConsole(): bool 17 | { 18 | if (isset($_ENV['APP_RUNNING_IN_CONSOLE'])) { 19 | return $_ENV['APP_RUNNING_IN_CONSOLE'] === 'true'; 20 | } 21 | 22 | if (isset($_ENV['FLARE_FAKE_WEB_REQUEST'])) { 23 | return false; 24 | } 25 | 26 | return in_array(php_sapi_name(), ['cli', 'phpdb']); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Context/ContextDetectorInterface.php: -------------------------------------------------------------------------------- 1 | request = $request ?? Request::createFromGlobals(); 19 | } 20 | 21 | public function getRequest(): array 22 | { 23 | return [ 24 | 'url' => $this->request->getUri(), 25 | 'ip' => $this->request->getClientIp(), 26 | 'method' => $this->request->getMethod(), 27 | 'useragent' => $this->request->headers->get('User-Agent'), 28 | ]; 29 | } 30 | 31 | private function getFiles(): array 32 | { 33 | if (is_null($this->request->files)) { 34 | return []; 35 | } 36 | 37 | return $this->mapFiles($this->request->files->all()); 38 | } 39 | 40 | protected function mapFiles(array $files) 41 | { 42 | return array_map(function ($file) { 43 | if (is_array($file)) { 44 | return $this->mapFiles($file); 45 | } 46 | 47 | if (! $file instanceof UploadedFile) { 48 | return; 49 | } 50 | 51 | try { 52 | $fileSize = $file->getSize(); 53 | } catch (\RuntimeException $e) { 54 | $fileSize = 0; 55 | } 56 | 57 | try { 58 | $mimeType = $file->getMimeType(); 59 | } catch (InvalidArgumentException $e) { 60 | $mimeType = 'undefined'; 61 | } 62 | 63 | return [ 64 | 'pathname' => $file->getPathname(), 65 | 'size' => $fileSize, 66 | 'mimeType' => $mimeType, 67 | ]; 68 | }, $files); 69 | } 70 | 71 | public function getSession(): array 72 | { 73 | try { 74 | $session = $this->request->getSession(); 75 | } catch (\Exception $exception) { 76 | $session = []; 77 | } 78 | 79 | return $session ? $this->getValidSessionData($session) : []; 80 | } 81 | 82 | /** 83 | * @param SessionInterface $session 84 | * @return array 85 | */ 86 | protected function getValidSessionData($session): array 87 | { 88 | try { 89 | json_encode($session->all()); 90 | } catch (Throwable $e) { 91 | return []; 92 | } 93 | 94 | return $session->all(); 95 | } 96 | 97 | public function getCookies(): array 98 | { 99 | return $this->request->cookies->all(); 100 | } 101 | 102 | public function getHeaders(): array 103 | { 104 | return $this->request->headers->all(); 105 | } 106 | 107 | public function getRequestData(): array 108 | { 109 | return [ 110 | 'queryString' => $this->request->query->all(), 111 | 'body' => $this->request->request->all(), 112 | 'files' => $this->getFiles(), 113 | ]; 114 | } 115 | 116 | public function toArray(): array 117 | { 118 | return [ 119 | 'request' => $this->getRequest(), 120 | 'request_data' => $this->getRequestData(), 121 | 'headers' => $this->getHeaders(), 122 | 'cookies' => $this->getCookies(), 123 | 'session' => $this->getSession(), 124 | ]; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Contracts/ProvidesFlareContext.php: -------------------------------------------------------------------------------- 1 | determineVersionCallable = $determineVersionCallable; 75 | } 76 | 77 | public function reportErrorLevels(int $reportErrorLevels) 78 | { 79 | $this->reportErrorLevels = $reportErrorLevels; 80 | } 81 | 82 | public function filterExceptionsUsing(callable $filterExceptionsCallable) 83 | { 84 | $this->filterExceptionsCallable = $filterExceptionsCallable; 85 | } 86 | 87 | public function filterReportsUsing(callable $filterReportsCallable) 88 | { 89 | $this->filterReportsCallable = $filterReportsCallable; 90 | } 91 | 92 | /** 93 | * @return null|string 94 | */ 95 | public function version() 96 | { 97 | if (! $this->determineVersionCallable) { 98 | return null; 99 | } 100 | 101 | return ($this->determineVersionCallable)(); 102 | } 103 | 104 | public function __construct(Client $client, ContextDetectorInterface $contextDetector = null, Container $container = null, array $middleware = []) 105 | { 106 | $this->client = $client; 107 | $this->recorder = new Recorder(); 108 | $this->contextDetector = $contextDetector ?? new ContextContextDetector(); 109 | $this->container = $container; 110 | $this->middleware = $middleware; 111 | $this->api = new Api($this->client); 112 | 113 | $this->registerDefaultMiddleware(); 114 | } 115 | 116 | public function getMiddleware(): array 117 | { 118 | return $this->middleware; 119 | } 120 | 121 | public function registerFlareHandlers() 122 | { 123 | $this->registerExceptionHandler(); 124 | $this->registerErrorHandler(); 125 | 126 | return $this; 127 | } 128 | 129 | public function registerExceptionHandler() 130 | { 131 | $this->previousExceptionHandler = set_exception_handler([$this, 'handleException']); 132 | 133 | return $this; 134 | } 135 | 136 | public function registerErrorHandler() 137 | { 138 | $this->previousErrorHandler = set_error_handler([$this, 'handleError']); 139 | 140 | return $this; 141 | } 142 | 143 | private function registerDefaultMiddleware() 144 | { 145 | return $this->registerMiddleware(new AddGlows($this->recorder)); 146 | } 147 | 148 | public function registerMiddleware($callable) 149 | { 150 | $this->middleware[] = $callable; 151 | 152 | return $this; 153 | } 154 | 155 | public function getMiddlewares(): array 156 | { 157 | return $this->middleware; 158 | } 159 | 160 | public function glow( 161 | string $name, 162 | string $messageLevel = MessageLevels::INFO, 163 | array $metaData = [] 164 | ) { 165 | $this->recorder->record(new Glow($name, $messageLevel, $metaData)); 166 | } 167 | 168 | public function handleException(Throwable $throwable) 169 | { 170 | $this->report($throwable); 171 | 172 | if ($this->previousExceptionHandler) { 173 | call_user_func($this->previousExceptionHandler, $throwable); 174 | } 175 | } 176 | 177 | public function handleError($code, $message, $file = '', $line = 0) 178 | { 179 | $exception = new ErrorException($message, 0, $code, $file, $line); 180 | 181 | $this->report($exception); 182 | 183 | if ($this->previousErrorHandler) { 184 | return call_user_func( 185 | $this->previousErrorHandler, 186 | $message, 187 | $code, 188 | $file, 189 | $line 190 | ); 191 | } 192 | } 193 | 194 | public function applicationPath(string $applicationPath) 195 | { 196 | $this->applicationPath = $applicationPath; 197 | 198 | return $this; 199 | } 200 | 201 | public function report(Throwable $throwable, callable $callback = null): ?Report 202 | { 203 | if (! $this->shouldSendReport($throwable)) { 204 | return null; 205 | } 206 | 207 | $report = $this->createReport($throwable); 208 | 209 | if (! is_null($callback)) { 210 | call_user_func($callback, $report); 211 | } 212 | 213 | $this->sendReportToApi($report); 214 | 215 | return $report; 216 | } 217 | 218 | protected function shouldSendReport(Throwable $throwable): bool 219 | { 220 | if ($this->reportErrorLevels && $throwable instanceof Error) { 221 | return $this->reportErrorLevels & $throwable->getCode(); 222 | } 223 | 224 | if ($this->reportErrorLevels && $throwable instanceof ErrorException) { 225 | return $this->reportErrorLevels & $throwable->getSeverity(); 226 | } 227 | 228 | if ($this->filterExceptionsCallable && $throwable instanceof Exception) { 229 | return call_user_func($this->filterExceptionsCallable, $throwable); 230 | } 231 | 232 | return true; 233 | } 234 | 235 | public function reportMessage(string $message, string $logLevel, callable $callback = null) 236 | { 237 | $report = $this->createReportFromMessage($message, $logLevel); 238 | 239 | if (! is_null($callback)) { 240 | call_user_func($callback, $report); 241 | } 242 | 243 | $this->sendReportToApi($report); 244 | } 245 | 246 | public function sendTestReport(Throwable $throwable) 247 | { 248 | $this->api->sendTestReport($this->createReport($throwable)); 249 | } 250 | 251 | private function sendReportToApi(Report $report) 252 | { 253 | if ($this->filterReportsCallable) { 254 | if (! call_user_func($this->filterReportsCallable, $report)) { 255 | return; 256 | } 257 | } 258 | 259 | try { 260 | $this->api->report($report); 261 | } catch (Exception $exception) { 262 | } 263 | } 264 | 265 | public function reset() 266 | { 267 | $this->api->sendQueuedReports(); 268 | 269 | $this->userProvidedContext = []; 270 | $this->recorder->reset(); 271 | } 272 | 273 | private function applyAdditionalParameters(Report $report) 274 | { 275 | $report 276 | ->stage($this->stage) 277 | ->messageLevel($this->messageLevel) 278 | ->setApplicationPath($this->applicationPath) 279 | ->userProvidedContext($this->userProvidedContext); 280 | } 281 | 282 | public function anonymizeIp() 283 | { 284 | $this->registerMiddleware(new AnonymizeIp()); 285 | 286 | return $this; 287 | } 288 | 289 | public function censorRequestBodyFields(array $fieldNames) 290 | { 291 | $this->registerMiddleware(new CensorRequestBodyFields($fieldNames)); 292 | 293 | return $this; 294 | } 295 | 296 | public function createReport(Throwable $throwable): Report 297 | { 298 | $report = Report::createForThrowable( 299 | $throwable, 300 | $this->contextDetector->detectCurrentContext(), 301 | $this->applicationPath, 302 | $this->version() 303 | ); 304 | 305 | return $this->applyMiddlewareToReport($report); 306 | } 307 | 308 | public function createReportFromMessage(string $message, string $logLevel): Report 309 | { 310 | $report = Report::createForMessage( 311 | $message, 312 | $logLevel, 313 | $this->contextDetector->detectCurrentContext(), 314 | $this->applicationPath 315 | ); 316 | 317 | return $this->applyMiddlewareToReport($report); 318 | } 319 | 320 | protected function applyMiddlewareToReport(Report $report): Report 321 | { 322 | $this->applyAdditionalParameters($report); 323 | 324 | $report = (new Pipeline($this->container)) 325 | ->send($report) 326 | ->through($this->middleware) 327 | ->then(function ($report) { 328 | return $report; 329 | }); 330 | 331 | return $report; 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /src/Frame.php: -------------------------------------------------------------------------------- 1 | file = $file; 28 | 29 | $this->lineNumber = $lineNumber; 30 | 31 | $this->method = $method; 32 | 33 | $this->class = $class; 34 | } 35 | 36 | public function toArray(): array 37 | { 38 | $codeSnippet = (new Codesnippet()) 39 | ->snippetLineCount(9) 40 | ->surroundingLine($this->lineNumber) 41 | ->get($this->file); 42 | 43 | return [ 44 | 'line_number' => $this->lineNumber, 45 | 'method' => $this->getFullMethod(), 46 | 'code_snippet' => $codeSnippet, 47 | 'file' => $this->file, 48 | ]; 49 | } 50 | 51 | private function getFullMethod(): string 52 | { 53 | $method = $this->method; 54 | 55 | if ($class = $this->class ?? false) { 56 | $method = "{$class}::{$method}"; 57 | } 58 | 59 | return $method; 60 | } 61 | 62 | public function getFile(): string 63 | { 64 | return $this->file; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Glows/Glow.php: -------------------------------------------------------------------------------- 1 | name = $name; 27 | $this->messageLevel = $messageLevel; 28 | $this->metaData = $metaData; 29 | $this->microtime = $microtime ?? microtime(true); 30 | } 31 | 32 | public function toArray() 33 | { 34 | return [ 35 | 'time' => $this->getCurrentTime(), 36 | 'name' => $this->name, 37 | 'message_level' => $this->messageLevel, 38 | 'meta_data' => $this->metaData, 39 | 'microtime' => $this->microtime, 40 | ]; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Glows/Recorder.php: -------------------------------------------------------------------------------- 1 | glows[] = $glow; 14 | 15 | $this->glows = array_slice($this->glows, static::GLOW_LIMIT * -1, static::GLOW_LIMIT); 16 | } 17 | 18 | public function glows(): array 19 | { 20 | return $this->glows; 21 | } 22 | 23 | public function reset() 24 | { 25 | $this->glows = []; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Http/Client.php: -------------------------------------------------------------------------------- 1 | apiToken = $apiToken; 31 | 32 | $this->apiSecret = $apiSecret; 33 | 34 | if (! $baseUrl) { 35 | throw MissingParameter::create('baseUrl'); 36 | } 37 | 38 | $this->baseUrl = $baseUrl; 39 | 40 | if (! $timeout) { 41 | throw MissingParameter::create('timeout'); 42 | } 43 | 44 | $this->timeout = $timeout; 45 | } 46 | 47 | /** 48 | * @param string $url 49 | * @param array $arguments 50 | * 51 | * @return array|false 52 | */ 53 | public function get(string $url, array $arguments = []) 54 | { 55 | return $this->makeRequest('get', $url, $arguments); 56 | } 57 | 58 | /** 59 | * @param string $url 60 | * @param array $arguments 61 | * 62 | * @return array|false 63 | */ 64 | public function post(string $url, array $arguments = []) 65 | { 66 | return $this->makeRequest('post', $url, $arguments); 67 | } 68 | 69 | /** 70 | * @param string $url 71 | * @param array $arguments 72 | * 73 | * @return array|false 74 | */ 75 | public function patch(string $url, array $arguments = []) 76 | { 77 | return $this->makeRequest('patch', $url, $arguments); 78 | } 79 | 80 | /** 81 | * @param string $url 82 | * @param array $arguments 83 | * 84 | * @return array|false 85 | */ 86 | public function put(string $url, array $arguments = []) 87 | { 88 | return $this->makeRequest('put', $url, $arguments); 89 | } 90 | 91 | /** 92 | * @param string $method 93 | * @param array $arguments 94 | * 95 | * @return array|false 96 | */ 97 | public function delete(string $method, array $arguments = []) 98 | { 99 | return $this->makeRequest('delete', $method, $arguments); 100 | } 101 | 102 | /** 103 | * @param string $httpVerb 104 | * @param string $url 105 | * @param array $arguments 106 | * 107 | * @return array 108 | */ 109 | private function makeRequest(string $httpVerb, string $url, array $arguments = []) 110 | { 111 | $queryString = http_build_query([ 112 | 'key' => $this->apiToken, 113 | 'secret' => $this->apiSecret, 114 | ]); 115 | 116 | $fullUrl = "{$this->baseUrl}/{$url}?{$queryString}"; 117 | 118 | $headers = [ 119 | 'x-api-token: '.$this->apiToken, 120 | ]; 121 | 122 | $response = $this->makeCurlRequest($httpVerb, $fullUrl, $headers, $arguments); 123 | 124 | if ($response->getHttpResponseCode() === 422) { 125 | throw InvalidData::createForResponse($response); 126 | } 127 | 128 | if ($response->getHttpResponseCode() === 404) { 129 | throw NotFound::createForResponse($response); 130 | } 131 | 132 | if ($response->getHttpResponseCode() !== 200 && $response->getHttpResponseCode() !== 204) { 133 | throw BadResponseCode::createForResponse($response); 134 | } 135 | 136 | return $response->getBody(); 137 | } 138 | 139 | public function makeCurlRequest(string $httpVerb, string $fullUrl, array $headers = [], array $arguments = []): Response 140 | { 141 | $curlHandle = $this->getCurlHandle($fullUrl, $headers); 142 | 143 | switch ($httpVerb) { 144 | case 'post': 145 | curl_setopt($curlHandle, CURLOPT_POST, true); 146 | $this->attachRequestPayload($curlHandle, $arguments); 147 | 148 | break; 149 | 150 | case 'get': 151 | curl_setopt($curlHandle, CURLOPT_URL, $fullUrl.'&'.http_build_query($arguments)); 152 | 153 | break; 154 | 155 | case 'delete': 156 | curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, 'DELETE'); 157 | 158 | break; 159 | 160 | case 'patch': 161 | curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, 'PATCH'); 162 | $this->attachRequestPayload($curlHandle, $arguments); 163 | 164 | break; 165 | 166 | case 'put': 167 | curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, 'PUT'); 168 | $this->attachRequestPayload($curlHandle, $arguments); 169 | 170 | break; 171 | } 172 | 173 | $body = json_decode(curl_exec($curlHandle), true); 174 | $headers = curl_getinfo($curlHandle); 175 | $error = curl_error($curlHandle); 176 | 177 | return new Response($headers, $body, $error); 178 | } 179 | 180 | private function attachRequestPayload(&$curlHandle, array $data) 181 | { 182 | $encoded = json_encode($data); 183 | 184 | $this->lastRequest['body'] = $encoded; 185 | curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $encoded); 186 | } 187 | 188 | /** 189 | * @param string $fullUrl 190 | * @param array $headers 191 | * 192 | * @return resource 193 | */ 194 | private function getCurlHandle(string $fullUrl, array $headers = []) 195 | { 196 | $curlHandle = curl_init(); 197 | 198 | curl_setopt($curlHandle, CURLOPT_URL, $fullUrl); 199 | 200 | curl_setopt($curlHandle, CURLOPT_HTTPHEADER, array_merge([ 201 | 'Accept: application/json', 202 | 'Content-Type: application/json', 203 | ], $headers)); 204 | 205 | curl_setopt($curlHandle, CURLOPT_USERAGENT, 'Laravel/Flare API 1.0'); 206 | curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, true); 207 | curl_setopt($curlHandle, CURLOPT_TIMEOUT, $this->timeout); 208 | curl_setopt($curlHandle, CURLOPT_SSL_VERIFYPEER, true); 209 | curl_setopt($curlHandle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); 210 | curl_setopt($curlHandle, CURLOPT_ENCODING, ''); 211 | curl_setopt($curlHandle, CURLINFO_HEADER_OUT, true); 212 | curl_setopt($curlHandle, CURLOPT_FOLLOWLOCATION, true); 213 | curl_setopt($curlHandle, CURLOPT_MAXREDIRS, 1); 214 | 215 | return $curlHandle; 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/Http/Exceptions/BadResponse.php: -------------------------------------------------------------------------------- 1 | getError()}"); 16 | 17 | $exception->response = $response; 18 | 19 | return $exception; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Http/Exceptions/BadResponseCode.php: -------------------------------------------------------------------------------- 1 | response = $response; 21 | 22 | $bodyErrors = isset($response->getBody()['errors']) ? $response->getBody()['errors'] : []; 23 | 24 | $exception->errors = $bodyErrors; 25 | 26 | return $exception; 27 | } 28 | 29 | public static function getMessageForResponse(Response $response) 30 | { 31 | return "Response code {$response->getHttpResponseCode()} returned"; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Http/Exceptions/InvalidData.php: -------------------------------------------------------------------------------- 1 | headers = $headers; 16 | 17 | $this->body = $body; 18 | 19 | $this->error = $error; 20 | } 21 | 22 | /** 23 | * @return mixed 24 | */ 25 | public function getHeaders() 26 | { 27 | return $this->headers; 28 | } 29 | 30 | /** 31 | * @return mixed 32 | */ 33 | public function getBody() 34 | { 35 | return $this->body; 36 | } 37 | 38 | /** 39 | * @return bool 40 | */ 41 | public function hasBody() 42 | { 43 | return $this->body != false; 44 | } 45 | 46 | /** 47 | * @return mixed 48 | */ 49 | public function getError() 50 | { 51 | return $this->error; 52 | } 53 | 54 | /** 55 | * @return null|int 56 | */ 57 | public function getHttpResponseCode() 58 | { 59 | if (! isset($this->headers['http_code'])) { 60 | return; 61 | } 62 | 63 | return (int) $this->headers['http_code']; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Middleware/AddGlows.php: -------------------------------------------------------------------------------- 1 | recorder = $recorder; 16 | } 17 | 18 | public function handle(Report $report, $next) 19 | { 20 | foreach ($this->recorder->glows() as $glow) { 21 | $report->addGlow($glow); 22 | } 23 | 24 | return $next($report); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Middleware/AnonymizeIp.php: -------------------------------------------------------------------------------- 1 | allContext(); 12 | 13 | $context['request']['ip'] = null; 14 | 15 | $report->userProvidedContext($context); 16 | 17 | return $next($report); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Middleware/CensorRequestBodyFields.php: -------------------------------------------------------------------------------- 1 | fieldNames = $fieldNames; 14 | } 15 | 16 | public function handle(Report $report, $next) 17 | { 18 | $context = $report->allContext(); 19 | 20 | foreach ($this->fieldNames as $fieldName) { 21 | if (isset($context['request_data']['body'][$fieldName])) { 22 | $context['request_data']['body'][$fieldName] = ''; 23 | } 24 | } 25 | 26 | $report->userProvidedContext($context); 27 | 28 | return $next($report); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Report.php: -------------------------------------------------------------------------------- 1 | setApplicationPath($applicationPath) 83 | ->throwable($throwable) 84 | ->useContext($context) 85 | ->exceptionClass(self::getClassForThrowable($throwable)) 86 | ->message($throwable->getMessage()) 87 | ->stackTrace(Stacktrace::createForThrowable($throwable, $applicationPath)) 88 | ->exceptionContext($throwable) 89 | ->setApplicationVersion($version); 90 | } 91 | 92 | protected static function getClassForThrowable(Throwable $throwable): string 93 | { 94 | if ($throwable instanceof \Facade\Ignition\Exceptions\ViewException) { 95 | if ($previous = $throwable->getPrevious()) { 96 | return get_class($previous); 97 | } 98 | } 99 | 100 | return get_class($throwable); 101 | } 102 | 103 | public static function createForMessage(string $message, string $logLevel, ContextInterface $context, ?string $applicationPath = null): self 104 | { 105 | $stacktrace = Stacktrace::create($applicationPath); 106 | 107 | return (new static()) 108 | ->setApplicationPath($applicationPath) 109 | ->message($message) 110 | ->useContext($context) 111 | ->exceptionClass($logLevel) 112 | ->stacktrace($stacktrace) 113 | ->openFrameIndex($stacktrace->firstApplicationFrameIndex()); 114 | } 115 | 116 | public function __construct() 117 | { 118 | $this->trackingUuid = self::$fakeTrackingUuid ?? $this->generateUuid(); 119 | } 120 | 121 | public function trackingUuid(): string 122 | { 123 | return $this->trackingUuid; 124 | } 125 | 126 | public function exceptionClass(string $exceptionClass) 127 | { 128 | $this->exceptionClass = $exceptionClass; 129 | 130 | return $this; 131 | } 132 | 133 | public function getExceptionClass(): string 134 | { 135 | return $this->exceptionClass; 136 | } 137 | 138 | public function throwable(Throwable $throwable) 139 | { 140 | $this->throwable = $throwable; 141 | 142 | return $this; 143 | } 144 | 145 | public function getThrowable(): ?Throwable 146 | { 147 | return $this->throwable; 148 | } 149 | 150 | public function message(string $message) 151 | { 152 | $this->message = $message; 153 | 154 | return $this; 155 | } 156 | 157 | public function getMessage(): string 158 | { 159 | return $this->message; 160 | } 161 | 162 | public function stacktrace(Stacktrace $stacktrace) 163 | { 164 | $this->stacktrace = $stacktrace; 165 | 166 | return $this; 167 | } 168 | 169 | public function getStacktrace(): Stacktrace 170 | { 171 | return $this->stacktrace; 172 | } 173 | 174 | public function notifierName(string $notifierName) 175 | { 176 | $this->notifierName = $notifierName; 177 | 178 | return $this; 179 | } 180 | 181 | public function languageVersion(string $languageVersion) 182 | { 183 | $this->languageVersion = $languageVersion; 184 | 185 | return $this; 186 | } 187 | 188 | public function frameworkVersion(string $frameworkVersion) 189 | { 190 | $this->frameworkVersion = $frameworkVersion; 191 | 192 | return $this; 193 | } 194 | 195 | public function useContext(ContextInterface $request) 196 | { 197 | $this->context = $request; 198 | 199 | return $this; 200 | } 201 | 202 | public function openFrameIndex(?int $index) 203 | { 204 | $this->openFrameIndex = $index; 205 | 206 | return $this; 207 | } 208 | 209 | public function setApplicationPath(?string $applicationPath) 210 | { 211 | $this->applicationPath = $applicationPath; 212 | 213 | return $this; 214 | } 215 | 216 | public function getApplicationPath(): ?string 217 | { 218 | return $this->applicationPath; 219 | } 220 | 221 | public function setApplicationVersion(?string $applicationVersion) 222 | { 223 | $this->applicationVersion = $applicationVersion; 224 | 225 | return $this; 226 | } 227 | 228 | public function getApplicationVersion(): ?string 229 | { 230 | return $this->applicationVersion; 231 | } 232 | 233 | public function view(?View $view) 234 | { 235 | $this->view = $view; 236 | 237 | return $this; 238 | } 239 | 240 | public function addGlow(Glow $glow) 241 | { 242 | $this->glows[] = $glow->toArray(); 243 | 244 | return $this; 245 | } 246 | 247 | public function addSolution(Solution $solution) 248 | { 249 | $this->solutions[] = ReportSolution::fromSolution($solution)->toArray(); 250 | 251 | return $this; 252 | } 253 | 254 | public function userProvidedContext(array $userProvidedContext) 255 | { 256 | $this->userProvidedContext = $userProvidedContext; 257 | 258 | return $this; 259 | } 260 | 261 | /** @deprecated */ 262 | public function groupByTopFrame() 263 | { 264 | $this->groupBy = GroupingTypes::TOP_FRAME; 265 | 266 | return $this; 267 | } 268 | 269 | /** @deprecated */ 270 | public function groupByException() 271 | { 272 | $this->groupBy = GroupingTypes::EXCEPTION; 273 | 274 | return $this; 275 | } 276 | 277 | public function allContext(): array 278 | { 279 | $context = $this->context->toArray(); 280 | 281 | $context = array_merge_recursive_distinct($context, $this->exceptionContext); 282 | 283 | return array_merge_recursive_distinct($context, $this->userProvidedContext); 284 | } 285 | 286 | private function exceptionContext(Throwable $throwable) 287 | { 288 | if ($throwable instanceof ProvidesFlareContext) { 289 | $this->exceptionContext = $throwable->context(); 290 | } 291 | 292 | return $this; 293 | } 294 | 295 | public function toArray() 296 | { 297 | return [ 298 | 'notifier' => $this->notifierName ?? 'Flare Client', 299 | 'language' => 'PHP', 300 | 'framework_version' => $this->frameworkVersion, 301 | 'language_version' => $this->languageVersion ?? phpversion(), 302 | 'exception_class' => $this->exceptionClass, 303 | 'seen_at' => $this->getCurrentTime(), 304 | 'message' => $this->message, 305 | 'glows' => $this->glows, 306 | 'solutions' => $this->solutions, 307 | 'stacktrace' => $this->stacktrace->toArray(), 308 | 'context' => $this->allContext(), 309 | 'stage' => $this->stage, 310 | 'message_level' => $this->messageLevel, 311 | 'open_frame_index' => $this->openFrameIndex, 312 | 'application_path' => $this->applicationPath, 313 | 'application_version' => $this->applicationVersion, 314 | 'tracking_uuid' => $this->trackingUuid, 315 | ]; 316 | } 317 | 318 | /* 319 | * Found on https://stackoverflow.com/questions/2040240/php-function-to-generate-v4-uuid/15875555#15875555 320 | */ 321 | private function generateUuid(): string 322 | { 323 | // Generate 16 bytes (128 bits) of random data or use the data passed into the function. 324 | $data = $data ?? random_bytes(16); 325 | 326 | // Set version to 0100 327 | $data[6] = chr(ord($data[6]) & 0x0f | 0x40); 328 | // Set bits 6-7 to 10 329 | $data[8] = chr(ord($data[8]) & 0x3f | 0x80); 330 | 331 | // Output the 36 character UUID. 332 | return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); 333 | } 334 | } 335 | -------------------------------------------------------------------------------- /src/Solutions/ReportSolution.php: -------------------------------------------------------------------------------- 1 | solution = $solution; 16 | } 17 | 18 | public static function fromSolution(SolutionContract $solution) 19 | { 20 | return new static($solution); 21 | } 22 | 23 | public function toArray(): array 24 | { 25 | $isRunnable = ($this->solution instanceof RunnableSolution); 26 | 27 | return [ 28 | 'class' => get_class($this->solution), 29 | 'title' => $this->solution->getSolutionTitle(), 30 | 'description' => $this->solution->getSolutionDescription(), 31 | 'links' => $this->solution->getDocumentationLinks(), 32 | 'action_description' => $isRunnable ? $this->solution->getSolutionActionDescription() : null, 33 | 'is_runnable' => $isRunnable, 34 | ]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Stacktrace/Codesnippet.php: -------------------------------------------------------------------------------- 1 | surroundingLine = $surroundingLine; 18 | 19 | return $this; 20 | } 21 | 22 | public function snippetLineCount(int $snippetLineCount): self 23 | { 24 | $this->snippetLineCount = $snippetLineCount; 25 | 26 | return $this; 27 | } 28 | 29 | public function get(string $fileName): array 30 | { 31 | if (! file_exists($fileName)) { 32 | return []; 33 | } 34 | 35 | try { 36 | $file = new File($fileName); 37 | 38 | [$startLineNumber, $endLineNumber] = $this->getBounds($file->numberOfLines()); 39 | 40 | $code = []; 41 | 42 | $line = $file->getLine($startLineNumber); 43 | 44 | $currentLineNumber = $startLineNumber; 45 | 46 | while ($currentLineNumber <= $endLineNumber) { 47 | $code[$currentLineNumber] = rtrim(substr($line, 0, 250)); 48 | 49 | $line = $file->getNextLine(); 50 | $currentLineNumber++; 51 | } 52 | 53 | return $code; 54 | } catch (RuntimeException $exception) { 55 | return []; 56 | } 57 | } 58 | 59 | private function getBounds($totalNumberOfLineInFile): array 60 | { 61 | $startLine = max($this->surroundingLine - floor($this->snippetLineCount / 2), 1); 62 | 63 | $endLine = $startLine + ($this->snippetLineCount - 1); 64 | 65 | if ($endLine > $totalNumberOfLineInFile) { 66 | $endLine = $totalNumberOfLineInFile; 67 | $startLine = max($endLine - ($this->snippetLineCount - 1), 1); 68 | } 69 | 70 | return [$startLine, $endLine]; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Stacktrace/File.php: -------------------------------------------------------------------------------- 1 | file = new SplFileObject($path); 15 | } 16 | 17 | public function numberOfLines(): int 18 | { 19 | $this->file->seek(PHP_INT_MAX); 20 | 21 | return $this->file->key() + 1; 22 | } 23 | 24 | public function getLine(int $lineNumber = null): string 25 | { 26 | if (is_null($lineNumber)) { 27 | return $this->getNextLine(); 28 | } 29 | 30 | $this->file->seek($lineNumber - 1); 31 | 32 | return $this->file->current(); 33 | } 34 | 35 | public function getNextLine(): string 36 | { 37 | $this->file->next(); 38 | 39 | return $this->file->current(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Stacktrace/Frame.php: -------------------------------------------------------------------------------- 1 | file = $file; 30 | 31 | $this->lineNumber = $lineNumber; 32 | 33 | $this->method = $method; 34 | 35 | $this->class = $class; 36 | 37 | $this->isApplicationFrame = $isApplicationFrame; 38 | } 39 | 40 | public function toArray(): array 41 | { 42 | $codeSnippet = (new Codesnippet()) 43 | ->snippetLineCount(31) 44 | ->surroundingLine($this->lineNumber) 45 | ->get($this->file); 46 | 47 | return [ 48 | 'line_number' => $this->lineNumber, 49 | 'method' => $this->method, 50 | 'class' => $this->class, 51 | 'code_snippet' => $codeSnippet, 52 | 'file' => $this->file, 53 | 'is_application_frame' => $this->isApplicationFrame, 54 | ]; 55 | } 56 | 57 | public function getFile(): string 58 | { 59 | return $this->file; 60 | } 61 | 62 | public function getLinenumber(): int 63 | { 64 | return $this->lineNumber; 65 | } 66 | 67 | public function isApplicationFrame() 68 | { 69 | return $this->isApplicationFrame; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Stacktrace/Stacktrace.php: -------------------------------------------------------------------------------- 1 | getTrace(), $applicationPath, $throwable->getFile(), $throwable->getLine()); 18 | } 19 | 20 | public static function create(?string $applicationPath = null): self 21 | { 22 | $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS & ~DEBUG_BACKTRACE_PROVIDE_OBJECT); 23 | 24 | return new static($backtrace, $applicationPath); 25 | } 26 | 27 | public function __construct(array $backtrace, ?string $applicationPath = null, string $topmostFile = null, string $topmostLine = null) 28 | { 29 | $this->applicationPath = $applicationPath; 30 | 31 | $currentFile = $topmostFile; 32 | $currentLine = $topmostLine; 33 | 34 | foreach ($backtrace as $rawFrame) { 35 | if (! $this->frameFromFlare($rawFrame) && ! $this->fileIgnored($currentFile)) { 36 | $this->frames[] = new Frame( 37 | $currentFile, 38 | $currentLine, 39 | $rawFrame['function'] ?? null, 40 | $rawFrame['class'] ?? null, 41 | $this->frameFileFromApplication($currentFile) 42 | ); 43 | } 44 | 45 | $currentFile = $rawFrame['file'] ?? 'unknown'; 46 | $currentLine = $rawFrame['line'] ?? 0; 47 | } 48 | 49 | $this->frames[] = new Frame( 50 | $currentFile, 51 | $currentLine, 52 | '[top]' 53 | ); 54 | } 55 | 56 | protected function frameFromFlare(array $rawFrame): bool 57 | { 58 | return isset($rawFrame['class']) && strpos($rawFrame['class'], 'Facade\\FlareClient\\') === 0; 59 | } 60 | 61 | protected function frameFileFromApplication(string $frameFilename): bool 62 | { 63 | $relativeFile = str_replace('\\', DIRECTORY_SEPARATOR, $frameFilename); 64 | 65 | if (! empty($this->applicationPath)) { 66 | $relativeFile = array_reverse(explode($this->applicationPath ?? '', $frameFilename, 2))[0]; 67 | } 68 | 69 | if (strpos($relativeFile, DIRECTORY_SEPARATOR . 'vendor') === 0) { 70 | return false; 71 | } 72 | 73 | return true; 74 | } 75 | 76 | protected function fileIgnored(string $currentFile): bool 77 | { 78 | $currentFile = str_replace('\\', DIRECTORY_SEPARATOR, $currentFile); 79 | 80 | $ignoredFiles = [ 81 | '/ignition/src/helpers.php', 82 | ]; 83 | 84 | foreach ($ignoredFiles as $ignoredFile) { 85 | if (strstr($currentFile, $ignoredFile) !== false) { 86 | return true; 87 | } 88 | } 89 | 90 | return false; 91 | } 92 | 93 | public function firstFrame(): Frame 94 | { 95 | return $this->frames[0]; 96 | } 97 | 98 | public function toArray(): array 99 | { 100 | return array_map(function (Frame $frame) { 101 | return $frame->toArray(); 102 | }, $this->frames); 103 | } 104 | 105 | public function firstApplicationFrame(): ?Frame 106 | { 107 | foreach ($this->frames as $index => $frame) { 108 | if ($frame->isApplicationFrame()) { 109 | return $frame; 110 | } 111 | } 112 | 113 | return null; 114 | } 115 | 116 | public function firstApplicationFrameIndex(): ?int 117 | { 118 | foreach ($this->frames as $index => $frame) { 119 | if ($frame->isApplicationFrame()) { 120 | return $index; 121 | } 122 | } 123 | 124 | return null; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Time/SystemTime.php: -------------------------------------------------------------------------------- 1 | getTimestamp(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Time/Time.php: -------------------------------------------------------------------------------- 1 | reportTrimmer = $reportTrimmer; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Truncation/ReportTrimmer.php: -------------------------------------------------------------------------------- 1 | strategies as $strategy) { 17 | if (! $this->needsToBeTrimmed($payload)) { 18 | break; 19 | } 20 | 21 | $payload = (new $strategy($this))->execute($payload); 22 | } 23 | 24 | return $payload; 25 | } 26 | 27 | public function needsToBeTrimmed(array $payload): bool 28 | { 29 | return strlen(json_encode($payload)) > self::getMaxPayloadSize(); 30 | } 31 | 32 | public static function getMaxPayloadSize(): int 33 | { 34 | return self::$maxPayloadSize; 35 | } 36 | 37 | public static function setMaxPayloadSize(int $maxPayloadSize): void 38 | { 39 | self::$maxPayloadSize = $maxPayloadSize; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Truncation/TrimContextItemsStrategy.php: -------------------------------------------------------------------------------- 1 | reportTrimmer->needsToBeTrimmed($payload)) { 16 | break; 17 | } 18 | 19 | $payload['context'] = $this->iterateContextItems($payload['context'], $threshold); 20 | } 21 | 22 | return $payload; 23 | } 24 | 25 | protected function iterateContextItems(array $contextItems, int $threshold): array 26 | { 27 | array_walk($contextItems, [$this, 'trimContextItems'], $threshold); 28 | 29 | return $contextItems; 30 | } 31 | 32 | protected function trimContextItems(&$value, $key, int $threshold) 33 | { 34 | if (is_array($value)) { 35 | if (count($value) > $threshold) { 36 | $value = array_slice($value, $threshold * -1, $threshold); 37 | } 38 | 39 | array_walk($value, [$this, 'trimContextItems'], $threshold); 40 | } 41 | 42 | return $value; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Truncation/TrimStringsStrategy.php: -------------------------------------------------------------------------------- 1 | reportTrimmer->needsToBeTrimmed($payload)) { 16 | break; 17 | } 18 | 19 | $payload = $this->trimPayloadString($payload, $threshold); 20 | } 21 | 22 | return $payload; 23 | } 24 | 25 | protected function trimPayloadString(array $payload, int $threshold): array 26 | { 27 | array_walk_recursive($payload, function (&$value) use ($threshold) { 28 | if (is_string($value) && strlen($value) > $threshold) { 29 | $value = substr($value, 0, $threshold); 30 | } 31 | }); 32 | 33 | return $payload; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Truncation/TruncationStrategy.php: -------------------------------------------------------------------------------- 1 | file = $file; 19 | $this->data = $data; 20 | } 21 | 22 | public static function create(string $file, array $data = []): self 23 | { 24 | return new static($file, $data); 25 | } 26 | 27 | private function dumpViewData($variable): string 28 | { 29 | $cloner = new VarCloner(); 30 | 31 | $dumper = new HtmlDumper(); 32 | $dumper->setDumpHeader(''); 33 | 34 | $output = fopen('php://memory', 'r+b'); 35 | 36 | $dumper->dump($cloner->cloneVar($variable)->withMaxDepth(1), $output, [ 37 | 'maxDepth' => 1, 38 | 'maxStringLength' => 160, 39 | ]); 40 | 41 | return stream_get_contents($output, -1, 0); 42 | } 43 | 44 | public function toArray() 45 | { 46 | return [ 47 | 'file' => $this->file, 48 | 'data' => array_map([$this, 'dumpViewData'], $this->data), 49 | ]; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/helpers.php: -------------------------------------------------------------------------------- 1 | &$value) { 8 | if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) { 9 | $merged[$key] = array_merge_recursive_distinct($merged[$key], $value); 10 | } else { 11 | $merged[$key] = $value; 12 | } 13 | } 14 | 15 | return $merged; 16 | } 17 | } 18 | --------------------------------------------------------------------------------