├── .codeclimate.yml ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── check-pr-title.yml │ ├── release.yml │ └── run-tests.yml ├── .php_cs ├── .styleci.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── composer.json ├── scripts └── SyncSourceCodeWithPackageVersion.php └── src ├── ArgumentValueNormalizer.php ├── BacktraceFactory.php ├── Breadcrumbs.php ├── BulkEventDispatcher.php ├── CheckIn.php ├── CheckInsClient.php ├── CheckInsClientWithErrorHandling.php ├── CheckInsManager.php ├── Concerns ├── FiltersData.php └── Newable.php ├── Config.php ├── Contracts ├── ApiClient.php ├── Handler.php ├── Reporter.php └── SyncCheckIns.php ├── CustomNotification.php ├── Environment.php ├── ExceptionNotification.php ├── Exceptions ├── ServiceException.php └── ServiceExceptionFactory.php ├── FileSource.php ├── Handlers ├── BeforeEventHandler.php ├── BeforeNotifyHandler.php ├── ErrorHandler.php ├── EventHandler.php ├── ExceptionHandler.php ├── Handler.php └── ShutdownHandler.php ├── Honeybadger.php ├── HoneybadgerClient.php ├── LogEventHandler.php ├── LogHandler.php ├── RawNotification.php ├── Request.php └── Support ├── Arr.php ├── EvictingQueue.php └── Repository.php /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: "2" # required to adjust maintainability checks 2 | checks: 3 | method-complexity: 4 | config: 5 | threshold: 6 6 | method-lines: 7 | config: 8 | threshold: 30 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## What are the steps to reproduce this issue? 4 | 5 | 1. … 6 | 2. … 7 | 3. … 8 | 9 | ## What happens? 10 | … 11 | 12 | ## What were you expecting to happen? 13 | … 14 | 15 | ## Any logs, error output, etc? 16 | … 17 | 18 | ## Any other comments? 19 | … 20 | 21 | ## What versions are you using? 22 | **Operating System:** … 23 | **Package Version:** … 24 | 25 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Status 2 | **READY/WIP/HOLD** 3 | 4 | ## Description 5 | A few sentences describing the overall goals of the pull request's commits. 6 | 7 | ## Related PRs 8 | List related PRs against other branches: 9 | 10 | branch | PR 11 | ------ | ------ 12 | other_pr_production | [link]() 13 | other_pr_master | [link]() 14 | 15 | 16 | ## Todos 17 | - [ ] Tests 18 | - [ ] Documentation 19 | - [ ] Changelog Entry (unreleased) 20 | 21 | ## Steps to Test or Reproduce 22 | Outline the steps to test or reproduce the PR here. 23 | 24 | ```bash 25 | > git pull --prune 26 | > git checkout 27 | > vendor/bin/phpunit 28 | ``` 29 | 30 | 1. 31 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: composer 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | time: "09:00" 8 | timezone: America/Los_Angeles 9 | open-pull-requests-limit: 99 10 | -------------------------------------------------------------------------------- /.github/workflows/check-pr-title.yml: -------------------------------------------------------------------------------- 1 | name: Check PR Title 2 | 3 | on: 4 | pull_request: 5 | branches: [ master ] 6 | types: [opened, edited, synchronize, reopened] 7 | 8 | jobs: 9 | commitlint: 10 | name: Check PR title 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Setup Node.js 14 | uses: actions/setup-node@v4 15 | with: 16 | node-version: '18.x' 17 | 18 | - name: Setup 19 | run: | 20 | npm install -g @commitlint/cli @commitlint/config-conventional 21 | echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js 22 | 23 | - name: Verify PR title is in the correct format 24 | env: 25 | TITLE: ${{ github.event.pull_request.title }} 26 | run: | 27 | echo $TITLE | npx commitlint -V 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release package 2 | 3 | on: 4 | workflow_dispatch: 5 | workflow_run: 6 | workflows: [Run Tests] 7 | types: [completed] 8 | branches: [master] 9 | 10 | permissions: 11 | contents: write 12 | pull-requests: write 13 | 14 | jobs: 15 | release-if-needed: 16 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Create Release PR 20 | uses: googleapis/release-please-action@v4 21 | id: release 22 | with: 23 | release-type: php 24 | 25 | - name: Checkout 26 | if: ${{ steps.release.outputs.prs_created == 'true' }} 27 | uses: actions/checkout@v4 28 | with: 29 | ref: ${{ fromJSON(steps.release.outputs.pr).headBranchName }} 30 | 31 | - name: Update version in Honeybadger.php 32 | if: ${{ steps.release.outputs.prs_created == 'true' }} 33 | run: php scripts/SyncSourceCodeWithPackageVersion.php "${{ fromJSON(steps.release.outputs.pr).title }}" 34 | 35 | - uses: stefanzweifel/git-auto-commit-action@v5 36 | if: ${{ steps.release.outputs.prs_created == 'true' }} 37 | with: 38 | create_branch: false 39 | commit_message: "chore: update version" 40 | commit_user_name: "honeybadger-robot" 41 | commit_user_email: "honeybadger-robot@honeybadger.io" 42 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: "0 0 * * *" 8 | 9 | jobs: 10 | run: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | composer-flags: ["--prefer-stable", "--prefer-lowest"] 15 | php-versions: ['7.3', '7.4', '8.0', '8.1', '8.2', '8.4'] 16 | exclude: 17 | - {php-versions: '8.1', composer-flags: "--prefer-lowest"} 18 | - {php-versions: '8.2', composer-flags: "--prefer-lowest"} 19 | - {php-versions: '8.4', composer-flags: "--prefer-lowest"} 20 | name: PHP ${{ matrix.php-versions }} Tests (${{ matrix.composer-flags }}) 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v3 24 | 25 | - name: Setup PHP 26 | uses: shivammathur/setup-php@v2 27 | with: 28 | php-version: ${{ matrix.php-versions }} 29 | ini-values: error_reporting=E_ALL, display_errors=On, zend.exception_ignore_args=Off 30 | extensions: curl, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, iconv, xdebug 31 | coverage: xdebug 32 | 33 | - name: Install dependencies 34 | run: composer update ${{ matrix.composer-flags }} --prefer-dist --no-interaction 35 | 36 | - name: Run Tests 37 | run: vendor/bin/phpunit 38 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | true, 5 | 'array_syntax' => ['syntax' => 'short'], 6 | 'no_multiline_whitespace_before_semicolons' => true, 7 | 'no_short_echo_tag' => true, 8 | 'no_unused_imports' => true, 9 | 'not_operator_with_successor_space' => true, 10 | 'no_useless_else' => true, 11 | 'ordered_imports' => [ 12 | 'sortAlgorithm' => 'length', 13 | ], 14 | 'single_quote' => true, 15 | 'ternary_operator_spaces' => true, 16 | 'trailing_comma_in_multiline_array' => true, 17 | 'trim_array_spaces' => true, 18 | ]; 19 | 20 | $excludes = [ 21 | 'vendor', 22 | ]; 23 | 24 | return PhpCsFixer\Config::create() 25 | ->setRules($rules) 26 | ->setFinder( 27 | PhpCsFixer\Finder::create() 28 | ->in(__DIR__) 29 | ->exclude($excludes) 30 | ->notName('README.md') 31 | ->notName('*.xml') 32 | ->notName('*.yml') 33 | ); 34 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel 2 | 3 | disabled: 4 | - laravel_braces 5 | - single_class_element_per_statement 6 | - laravel_phpdoc_alignment 7 | - laravel_phpdoc_separation 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. See [Keep a 3 | CHANGELOG](http://keepachangelog.com/) for how to update this file. This project 4 | adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | ## [2.24.1](https://github.com/honeybadger-io/honeybadger-php/compare/v2.24.0...v2.24.1) (2025-06-02) 7 | 8 | 9 | ### Performance Improvements 10 | 11 | * increase defaults for events dispatcher ([#229](https://github.com/honeybadger-io/honeybadger-php/issues/229)) ([11ec9d3](https://github.com/honeybadger-io/honeybadger-php/commit/11ec9d3e02dfcb6574a1d465b3a625e74974a9b8)) 12 | 13 | ## [2.24.0](https://github.com/honeybadger-io/honeybadger-php/compare/v2.23.0...v2.24.0) (2025-05-02) 14 | 15 | 16 | ### Features 17 | 18 | * add Insights event sampling ([#227](https://github.com/honeybadger-io/honeybadger-php/issues/227)) ([bb506c8](https://github.com/honeybadger-io/honeybadger-php/commit/bb506c89755d0b83f1f7451c7dae6fa5eeff7ba5)) 19 | 20 | ## [2.23.0](https://github.com/honeybadger-io/honeybadger-php/compare/v2.22.1...v2.23.0) (2025-01-07) 21 | 22 | 23 | ### Features 24 | 25 | * add support for beforeNotify and beforeEvent handlers ([#222](https://github.com/honeybadger-io/honeybadger-php/issues/222)) ([534634c](https://github.com/honeybadger-io/honeybadger-php/commit/534634c66bd22f0ddfa11d535c4f958aa898d0de)) 26 | 27 | 28 | ### Bug Fixes 29 | 30 | * **checkins:** read error message from response body ([#224](https://github.com/honeybadger-io/honeybadger-php/issues/224)) ([f6fd084](https://github.com/honeybadger-io/honeybadger-php/commit/f6fd084522a371b992d9544dfd36469eff43689e)) 31 | 32 | ## [2.22.1](https://github.com/honeybadger-io/honeybadger-php/compare/v2.22.0...v2.22.1) (2024-12-09) 33 | 34 | 35 | ### Performance Improvements 36 | 37 | * add PHP 8.4 to test matrix ([#218](https://github.com/honeybadger-io/honeybadger-php/issues/218)) ([de5747f](https://github.com/honeybadger-io/honeybadger-php/commit/de5747f3c1f818c1caf6750cd103f376ed442204)) 38 | 39 | ## [2.22.0](https://github.com/honeybadger-io/honeybadger-php/compare/v2.21.0...v2.22.0) (2024-11-16) 40 | 41 | 42 | ### Features 43 | 44 | * ignore breadcrumbs with empty message ([#215](https://github.com/honeybadger-io/honeybadger-php/issues/215)) ([645a672](https://github.com/honeybadger-io/honeybadger-php/commit/645a672f26ce5a249017d3e79405bcb3f6ee45e5)) 45 | 46 | ## [2.21.0](https://github.com/honeybadger-io/honeybadger-php/compare/v2.20.0...v2.21.0) (2024-11-01) 47 | 48 | 49 | ### Features 50 | 51 | * add events api exception message ([#213](https://github.com/honeybadger-io/honeybadger-php/issues/213)) ([e0a0b90](https://github.com/honeybadger-io/honeybadger-php/commit/e0a0b90c6a1acfc188be3cabaf7907d448824bba)) 52 | * send user agent in http clients ([#207](https://github.com/honeybadger-io/honeybadger-php/issues/207)) ([3bd7466](https://github.com/honeybadger-io/honeybadger-php/commit/3bd7466f258711de43676db5240c05129926fe43)) 53 | 54 | 55 | ### Bug Fixes 56 | 57 | * add noop handler for events exceptions ([#208](https://github.com/honeybadger-io/honeybadger-php/issues/208)) ([3eb1947](https://github.com/honeybadger-io/honeybadger-php/commit/3eb1947f5f0f1f7f49782d9020f7a75964fbb0cb)) 58 | 59 | ## [2.20.0](https://github.com/honeybadger-io/honeybadger-php/compare/v2.19.5...v2.20.0) (2024-10-24) 60 | 61 | 62 | ### Features 63 | 64 | * make endpoint app url configurable ([#204](https://github.com/honeybadger-io/honeybadger-php/issues/204)) ([f6f5dcf](https://github.com/honeybadger-io/honeybadger-php/commit/f6f5dcfea3076239a673b13b012ce24f047e7e81)) 65 | 66 | ## [2.19.5](https://github.com/honeybadger-io/honeybadger-php/compare/v2.19.4...v2.19.5) (2024-10-11) 67 | 68 | 69 | ### Bug Fixes 70 | 71 | * use RFC3339 extended date format ([#200](https://github.com/honeybadger-io/honeybadger-php/issues/200)) ([1cf4ba3](https://github.com/honeybadger-io/honeybadger-php/commit/1cf4ba3f8fb29e78f929633e298faf2ab3b68409)) 72 | 73 | ## [2.19.4](https://github.com/honeybadger-io/honeybadger-php/compare/v2.19.3...v2.19.4) (2024-08-28) 74 | 75 | 76 | ### Bug Fixes 77 | 78 | * check if api key is set in Honeybadger::event ([#198](https://github.com/honeybadger-io/honeybadger-php/issues/198)) ([9fb1390](https://github.com/honeybadger-io/honeybadger-php/commit/9fb13908b7d2f53a0c902a8ab66afdc239afba7f)) 79 | 80 | ## [2.19.3](https://github.com/honeybadger-io/honeybadger-php/compare/v2.19.2...v2.19.3) (2024-07-06) 81 | 82 | 83 | ### Bug Fixes 84 | 85 | * revert to deprecated version of logger levels for wider support ([#196](https://github.com/honeybadger-io/honeybadger-php/issues/196)) ([4ab5c01](https://github.com/honeybadger-io/honeybadger-php/commit/4ab5c01c790a6d1cc06d9b8ebeea35d33d2e3fca)) 86 | 87 | 88 | ### Miscellaneous Chores 89 | 90 | * minor typo fix ([a3df488](https://github.com/honeybadger-io/honeybadger-php/commit/a3df48835266f21f5ed5ab16f5d2bf6b96f39fdc)) 91 | 92 | ## [2.19.2] - 2024-07-06 93 | ### Fixed 94 | - Events: Honeybadger.flushEvents() should check if events are enabled before sending to Honeybadger 95 | ### Changed 96 | - Events: Register shutdown handler by default 97 | 98 | ## [2.19.1] - 2024-06-29 99 | ### Fixed 100 | - Events: Allow Honeybadger.event() without event_type 101 | 102 | ## [2.19.0] - 2024-06-28 103 | ### Added 104 | - Events: Honeybadger.event() method to send custom events to Honeybadger 105 | - Events: Monolog logger to send logs as events to Honeybadger 106 | 107 | ## [2.18.0] - 2023-12-28 108 | ### Changed 109 | - Check-Ins: Remove project_id from configuration API 110 | 111 | ## [2.17.3] - 2023-12-04 112 | ### Fixed 113 | - Use $request->getContentTypeFormat 114 | 115 | ## [2.17.2] - 2023-11-16 116 | ### Refactored 117 | - Check-Ins: checkins to check-ins 118 | 119 | ## [2.17.1] - 2023-11-15 120 | ### Fixed 121 | - Check-Ins: Do not allow check-ins with same names and project id 122 | - Check-Ins: Send empty string for optional values so that they will be updated when unset 123 | 124 | ## [2.17.0] - 2023-10-27 125 | ### Added 126 | - Check-Ins: Support for slug configuration within the package 127 | 128 | ## [2.16.0] - 2023-10-17 129 | ### Added 130 | - Allow calling the checkin method with a checkin id or name 131 | 132 | ## [2.15.0] - 2023-10-06 133 | ### Added 134 | - Support for checkins configuration within the package 135 | 136 | ## [2.14.1] - 2023-08-06 137 | ### Fixed 138 | - LogHandler: Check log level before writing the log [#168](https://github.com/honeybadger-io/honeybadger-php/pull/168) 139 | 140 | ## [2.14.0] - 2023-02-16 141 | - Monolog 3 support 142 | 143 | ## [2.13.0] - 2022-09-08 144 | ### Modified 145 | - Remove spatie/regex dependency ([#165](https://github.com/honeybadger-io/honeybadger-php/pull/165)) 146 | 147 | ## [2.12.1] - 2022-05-18 148 | ### Added 149 | - Fix occasionally missing backtrace in errors (closes #162) ([341aefc](https://github.com/honeybadger-io/honeybadger-php/commit/341aefc3bb17c7beb71e81b4d7f918125f69c8ff)) 150 | 151 | ## [2.12.0] - 2022-05-06 152 | ### Added 153 | - Format errors differently from exceptions ([#160](https://github.com/honeybadger-io/honeybadger-php/pull/160)) 154 | - `capture_deprecations` option to disable capturing deprecation warnings ([#160](https://github.com/honeybadger-io/honeybadger-php/pull/160)) 155 | 156 | ## [2.11.3] - 2022-02-07 157 | ### Fixed 158 | - Fix deprecation warning for type mismatch on PHP 8.1 ([#158](https://github.com/honeybadger-io/honeybadger-php/pull/158)) 159 | 160 | ## [2.11.2] - 2021-11-30 161 | ### Fixed 162 | - Remove unnecessary environment variables ([#155](https://github.com/honeybadger-io/honeybadger-php/pull/155)) 163 | - PHP 8.1 fixes ([092b1bb7e8b2733fad40d6724a59e4370391a8e9](https://github.com/honeybadger-io/honeybadger-php/commit/092b1bb7e8b2733fad40d6724a59e4370391a8e9))) 164 | 165 | ## [2.11.1] - 2021-11-03 166 | ### Fixed 167 | - Filter configured query parameters from the URL ([#154](https://github.com/honeybadger-io/honeybadger-php/pull/154)) 168 | 169 | ## [2.11.0] - 2021-06-29 170 | ### Added 171 | - Improve how Monolog messages are formatted/displayed ([#152](https://github.com/honeybadger-io/honeybadger-php/pull/152)) 172 | 173 | ## [2.10.0] - 2021-06-16 174 | ### Added 175 | - Add endpoint config option ([#150](https://github.com/honeybadger-io/honeybadger-php/pull/150)) 176 | - Set default client timeout ([#151](https://github.com/honeybadger-io/honeybadger-php/pull/151)) 177 | 178 | ## [2.9.2] - 2021-05-10 179 | ### Fixed 180 | - Handle silenced errors properly on PHP 8 ([#149](https://github.com/honeybadger-io/honeybadger-php/pull/149)) 181 | 182 | ## [2.9.1] - 2021-05-10 183 | ### Fixed 184 | - Allow for spatie/regex v2 ([#148](https://github.com/honeybadger-io/honeybadger-php/pull/148)) 185 | 186 | ## [2.9.0] - 2021-05-09 187 | ### Modified 188 | - Make internal methods of client `protected` ([953c7b258bc75d33a7045c6005ed0a5eda3f9a11](https://github.com/honeybadger-io/honeybadger-php/commit/953c7b258bc75d33a7045c6005ed0a5eda3f9a11)) 189 | 190 | ## [2.8.3] - 2021-04-13 191 | ### Fixed 192 | - Fix return type signature of `get()` method ([e340a6e7a16fa9f452bd2eb6731f7d488e85ef11](https://github.com/honeybadger-io/honeybadger-php/commit/e340a6e7a16fa9f452bd2eb6731f7d488e85ef11)) 193 | 194 | ## [2.8.2] - 2021-04-12 195 | ### Added 196 | - Added `get()` method to config repository ([3800e78518618779279742cc375b1f61943f1dcc](https://github.com/honeybadger-io/honeybadger-php/commit/3800e78518618779279742cc375b1f61943f1dcc)) 197 | 198 | ## [2.8.1] - 2021-03-25 199 | ### Fixed 200 | - Capture the previous exception when throwing generic ServiceException ([#143](https://github.com/honeybadger-io/honeybadger-php/pull/143)) 201 | 202 | ## [2.8.0] - 2021-03-16 203 | ### Added 204 | - Added support for collecting breadcrumbs ([#141](https://github.com/honeybadger-io/honeybadger-php/pull/141)) 205 | 206 | ## [2.7.0] - 2021-03-09 207 | ### Added 208 | - Added support for array parameters and chaining in `context()` method ([#136](https://github.com/honeybadger-io/honeybadger-php/pull/136)) 209 | 210 | ### Fixed 211 | - Send empty context as JSON object, not array ([#138](https://github.com/honeybadger-io/honeybadger-php/pull/138)) 212 | - Serialise objects in backtrace arguments as literals, not strings ([#133](https://github.com/honeybadger-io/honeybadger-php/pull/133)) 213 | 214 | ## [2.6.0] - 2021-02-24 215 | ### Fixed 216 | - The size of each backtrace argument is now limited to nesting depth of 10 and 50 array keys ([#134](https://github.com/honeybadger-io/honeybadger-php/pull/134)). 217 | 218 | ## [2.5.0] - 2021-02-19 219 | ### Added 220 | - Added `service_exception_handler` config item to allow users configure how ServiceExceptions should be handled ([#129](https://github.com/honeybadger-io/honeybadger-php/pull/129)) 221 | 222 | ### Fixed 223 | - `vendor_paths` on Windows are now matched correctly. ([128](https://github.com/honeybadger-io/honeybadger-php/pull/128)) 224 | 225 | ## [2.4.1] - 2021-02-15 226 | ### Fixed 227 | - Fixed default value for upgrading older installations ([#126](https://github.com/honeybadger-io/honeybadger-php/pull/126)) 228 | 229 | ## [2.4.0] - 2021-02-15 230 | ### Added 231 | - Added config for Guzzle SSL verification ([#124](https://github.com/honeybadger-io/honeybadger-php/pull/124)) ([#123](https://github.com/honeybadger-io/honeybadger-php/pull/123)) 232 | 233 | ## [2.3.0] - 2020-11-29 234 | ### Changed 235 | - Added PHP8 Support ([#118](https://github.com/honeybadger-io/honeybadger-php/pull/118)) 236 | 237 | ## [2.2.2] - 2020-11-6 238 | ### Fixed 239 | - Fixed an issue filtering keyed arrays ([#117](https://github.com/honeybadger-io/honeybadger-php/pull/117)) 240 | 241 | ## [2.2.1] - 2020-09-14 242 | - Changed the seprator for flex version dependencies in the composer file. Might be causing an issue ([#115](https://github.com/honeybadger-io/honeybadger-php/pull/115)) 243 | - Updated the mimimum version of Guzzle to `7.0.1` ([#115](https://github.com/honeybadger-io/honeybadger-php/pull/115)) 244 | 245 | ## [2.2.0] - 2020-09-08 246 | ## Added 247 | - Backtrace context for app/vendor files for filtering in HB UI ([#114](https://github.com/honeybadger-io/honeybadger-php/pull/114)) 248 | - Environment context for raw and custom notifications ([#113](https://github.com/honeybadger-io/honeybadger-php/pull/113)) 249 | 250 | ## [2.1.0] - 2020-02-10 251 | ### Changed 252 | - Improved log reporter payload ([#106](https://github.com/honeybadger-io/honeybadger-php/pull/106)) 253 | 254 | ## [2.0.2] - 2020-02-10 255 | ### Fixed 256 | - Fixed an issue with error reporting ([#104](https://github.com/honeybadger-io/honeybadger-php/pull/104)) 257 | 258 | ### Changed 259 | - Added array to doc block for context ([#103](https://github.com/honeybadger-io/honeybadger-php/pull/103)) 260 | 261 | ## [2.0.1] - 2019-11-18 262 | ### Fixed 263 | - Fixed an issue where a payload containing recursive data couldn't be posted to the backend ([#96](https://github.com/honeybadger-io/honeybadger-php/pull/96)) 264 | - Fixed an issue where the previous exception handler is not callable but called ([#97](https://github.com/honeybadger-io/honeybadger-php/pull/97)) 265 | 266 | ## [2.0.0] - 2019-09-21 267 | ### Changed 268 | - Updated Monolog dependency to 2.0 269 | - Remove support for PHP 7.1 270 | 271 | ## [1.7.1] - 2019-09-13 272 | ### Fixed 273 | - Default args for backtrace functions ([#92](https://github.com/honeybadger-io/honeybadger-php/pull/92)) 274 | 275 | ## [1.7.0] - 2019-09-04 276 | ### Added 277 | - Methods to set the component and action ([#87](https://github.com/honeybadger-io/honeybadger-php/pull/87)) 278 | - Class and type to backtrace frames ([#72](https://github.com/honeybadger-io/honeybadger-php/pull/72/)) 279 | 280 | ### Changed 281 | - Backtrace args with objects now send only the class name ([#89](https://github.com/honeybadger-io/honeybadger-php/pull/89)) 282 | 283 | ## [1.6.0] - 2019-07-18 284 | ### Added 285 | - Added the ability to pass additional API parameters to exception captures specifically component and action ([#85](https://github.com/honeybadger-io/honeybadger-php/pull/85)) 286 | - Adds fingerprint and tags to the additional parameters ([#76](https://github.com/honeybadger-io/honeybadger-php/pull/76)) 287 | - Adds method arguments to backtrace where possible ([#86](https://github.com/honeybadger-io/honeybadger-php/pull/86)) 288 | 289 | ## [1.5.1] - 2019-06-10 290 | ### Fixed 291 | * Error handler reporting supressed errors ([#83](https://github.com/honeybadger-io/honeybadger-php/pull/83)) 292 | 293 | ## [1.5.0] 2019-05-30 294 | 295 | ### Added 296 | * New option for whether the library should send notifications back to the Honeybadger API ([#82](https://github.com/honeybadger-io/honeybadger-php/pull/82)) 297 | 298 | ## [1.4.0] 2019-04-17 299 | 300 | ### Added 301 | * Fully customizable notification method ([#70](https://github.com/honeybadger-io/honeybadger-php/pull/70)) 302 | * Ability to reset context ([#71](https://github.com/honeybadger-io/honeybadger-php/pull/71)) 303 | * Monolog Handler ([#70](https://github.com/honeybadger-io/honeybadger-php/pull/70)) 304 | * PHPUnit 8 support ([#79](https://github.com/honeybadger-io/honeybadger-php/pull/79)) 305 | 306 | ## Fixed 307 | * Empty `api_key` value ([#80](https://github.com/honeybadger-io/honeybadger-php/pull/80)) 308 | 309 | ## [1.3.0] 2018-12-17 310 | ### Added 311 | * PHP 7.3 to the Travis build matrix ([#68](https://github.com/honeybadger-io/honeybadger-php/pull/68)) 312 | 313 | ### Removed 314 | * php-cs-fixer dev dependency ([#69](https://github.com/honeybadger-io/honeybadger-php/pull/69)) 315 | 316 | ## [1.2.1] 2018-11-08 317 | ### Fixed 318 | - Fixed an issue with merging a custom notifier from the config ([#67](https://github.com/honeybadger-io/honeybadger-php/pull/67)) 319 | 320 | ## [1.2.0] - 2018-09-13 321 | ### Changed 322 | - Lowered required version of `symfony/http-foundation` ([#65](https://github.com/honeybadger-io/honeybadger-php/pull/65)) 323 | 324 | ## [1.1.0] - 2018-08-17 325 | ### Changed 326 | - Allow `null` value for `api_key` config to improve local project development. 327 | 328 | ## [1.0.0] - 2018-07-07 329 | ### Changed 330 | - Full library rewrite 331 | - PHP 7.1|7.2 requirement 332 | - See [README](README.md) for new installation, usage, and configuration details 333 | 334 | ## [0.4.1] - 2018-06-12 335 | ## Fixed 336 | - PHP 5.5 support (#54) 337 | - Fixes port duplication in URL (#53) 338 | 339 | ## [0.4.0] - 2018-04-08 340 | ### Added 341 | - Adds the ability to disable and restore error and exception handlers (#50) 342 | - Adds the ability to filter reported keys (#48) 343 | 344 | ## [0.3.2] - 2018-03-11 345 | ### Fixed 346 | - Fixes a bug in proxy URL configuration based on settings 347 | 348 | ## [0.3.1] - 2016-10-31 349 | ### Fixed 350 | - Fix a bug where `$config` was not initialized until calling 351 | `Honeybadger::init()`. 352 | 353 | ## [0.3.0] - 2016-08-29 354 | ### Added 355 | - Updated fig standards 356 | - Adding official support 357 | 358 | ## [0.2.0] - 2015-06-22 359 | ### Fixed 360 | - Fixes inefficiency in notice building - #1 361 | - Fixes missing breaks in Slim logger - #3 362 | - Fixes package name in documentation - #4 363 | 364 | ## [0.1.0] - 2013-04-05 365 | ### Added 366 | - Initial release, -Gabriel Evans 367 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | Please read and understand the contribution guide before creating an issue or pull request. 6 | 7 | ## Etiquette 8 | 9 | This project is open source, and as such, the maintainers give their free time to build and maintain the source code 10 | held within. They make the code freely available in the hope that it will be of use to other developers. It would be 11 | extremely unfair for them to suffer abuse or anger for their hard work. 12 | 13 | Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the 14 | world that developers are civilized and selfless people. 15 | 16 | It's the duty of the maintainer to ensure that all submissions to the project are of sufficient 17 | quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. 18 | 19 | ## Viability 20 | 21 | When requesting or submitting new features, first consider whether it might be useful to others. Open 22 | source projects are used by many developers, who may have entirely different needs to your own. Think about 23 | whether or not your feature is likely to be used by other users of the project. 24 | 25 | ## Procedure 26 | 27 | Before filing an issue: 28 | 29 | - Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. 30 | - Check to make sure your feature suggestion isn't already present within the project. 31 | - Check the pull requests tab to ensure that the bug doesn't have a fix in progress. 32 | - Check the pull requests tab to ensure that the feature isn't already in progress. 33 | 34 | Before submitting a pull request: 35 | 36 | - Check the codebase to ensure that your feature doesn't already exist. 37 | - Check the pull requests to ensure that another person hasn't already submitted the feature or fix. 38 | 39 | ## Requirements 40 | 41 | If the project maintainer has any additional requirements, you will find them listed here. 42 | 43 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - We have provided [tooling](README.md#code-style) to make this as easy as possible. 44 | 45 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 46 | 47 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 48 | 49 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 50 | 51 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 52 | 53 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 54 | 55 | **Happy coding**! 56 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Honeybadger Industries LLC 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 | # Honeybadger PHP library 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/honeybadger-io/honeybadger-php.svg?style=flat-square)](https://packagist.org/packages/honeybadger-io/honeybadger-php) 4 | [![Total Downloads](https://img.shields.io/packagist/dt/honeybadger-io/honeybadger-php.svg?style=flat-square)](https://packagist.org/packages/honeybadger-io/honeybadger-php) 5 | ![Run Tests](https://github.com/honeybadger-io/honeybadger-php/workflows/Run%20Tests/badge.svg) 6 | [![StyleCI](https://styleci.io/repos/9077424/shield)](https://github.styleci.io/repos/9077424) 7 | 8 | This is the client library for integrating apps with the :zap: [Honeybadger Exception Notifier for PHP](https://www.honeybadger.io/for/php/?utm_source=github&utm_medium=readme&utm_campaign=php&utm_content=Honeybadger+Exception+Notifier+for+PHP). 9 | 10 | ## Framework Integrations 11 | 12 | * Laravel - [honeybadger-io/honeybadger-laravel](https://github.com/honeybadger-io/honeybadger-laravel) 13 | 14 | ## Documentation and Support 15 | 16 | For comprehensive documentation and support, [check out our documentation site](https://docs.honeybadger.io/lib/php/index.html). 17 | 18 | ## Testing 19 | 20 | ``` bash 21 | composer test 22 | ``` 23 | 24 | ## Code Style 25 | This project follows the [PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md). In addition, StyleCI will apply the [Laravel preset](https://docs.styleci.io/presets#laravel). 26 | 27 | ## Changelog 28 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 29 | 30 | ## Contributing 31 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 32 | 33 | ## Releasing 34 | We have enabled GitHub integration with [Packagist](https://packagist.org). Packagist is automatically notified when a new release is made on GitHub. 35 | 36 | Releases are automated, using [Github Actions](https://github.com/honeybadger-io/honeybadger-php/blob/master/.github/workflows/release.yml): 37 | - When a PR is merged on master, the [run-tests.yml](https://github.com/honeybadger-io/honeybadger-php/blob/master/.github/workflows/run-tests.yml) workflow is executed, which runs the tests. 38 | - If the tests pass, the [release.yml](https://github.com/honeybadger-io/honeybadger-php/blob/master/.github/workflows/release.yml) workflow will be executed. 39 | - Depending on the commit message, a release PR will be created with the suggested the version bump and changelog. Note: Not all commit messages trigger a new release, for example, chore: ... will not trigger a release. 40 | - If the release PR is merged, the release.yml workflow will be executed again, and this time it will create a github release. 41 | 42 | ## Security 43 | If you discover any security related issues, please email security@honeybadger.io instead of using the issue tracker. 44 | 45 | ## Credits 46 | - [TJ Miller](https://github.com/sixlive) 47 | - [Ivy Evans](https://github.com/ivy) 48 | - [All Contributors](../../contributors) 49 | 50 | ## License 51 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 52 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "honeybadger-io/honeybadger-php", 3 | "description": "Honeybadger PHP library", 4 | "keywords": [ 5 | "logging", 6 | "debugging", 7 | "monitoring", 8 | "errors", 9 | "exceptions", 10 | "honeybadger-io", 11 | "honeybadger-php" 12 | ], 13 | "homepage": "https://github.com/honeybadger-io/honeybadger-php", 14 | "license": "MIT", 15 | "authors": [ 16 | { 17 | "name": "TJ Miller", 18 | "email": "oss@tjmiller.co", 19 | "homepage": "https://tjmiller.co", 20 | "role": "Developer" 21 | } 22 | ], 23 | "require": { 24 | "php": "^7.2|^8.0", 25 | "ext-json": "*", 26 | "guzzlehttp/guzzle": "^6.3|^7.0.1", 27 | "monolog/monolog": "^2.0|^3.0", 28 | "symfony/http-foundation": ">=3.3|^4.1" 29 | }, 30 | "require-dev": { 31 | "larapack/dd": "^1.1", 32 | "mockery/mockery": "^1.4", 33 | "phpunit/phpunit": "^8.5|^9.4" 34 | }, 35 | "extra": { 36 | "branch-alias": { 37 | "dev-master": "2.x-dev" 38 | } 39 | }, 40 | "autoload": { 41 | "psr-4": { 42 | "Honeybadger\\": "src" 43 | } 44 | }, 45 | "autoload-dev": { 46 | "psr-4": { 47 | "Honeybadger\\Tests\\": "tests" 48 | }, 49 | "files": [ 50 | "tests/helpers.php" 51 | ] 52 | }, 53 | "scripts": { 54 | "test": "vendor/bin/phpunit", 55 | "test:coverage": "vendor/bin/phpunit --coverage-html coverage" 56 | }, 57 | "config": { 58 | "sort-packages": true 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /scripts/SyncSourceCodeWithPackageVersion.php: -------------------------------------------------------------------------------- 1 | \n"; 6 | exit(1); 7 | } 8 | 9 | // Given the title of the PR, we can extract the version from it 10 | // Example: chore(master): release 4.2.0 11 | $prTitle = $argv[1]; 12 | $versionInput = trim(preg_replace('/^chore\(master\): release /', '', $prTitle)); 13 | $filePath = 'src/Honeybadger.php'; 14 | 15 | // Read the content of the file 16 | $fileContent = file_get_contents($filePath); 17 | if ($fileContent === false) { 18 | echo "Error reading file: {$filePath}\n"; 19 | exit(1); 20 | } 21 | 22 | // Replace the version line 23 | $updatedContent = preg_replace( 24 | '/const VERSION = \'.*?\';/', 25 | "const VERSION = '{$versionInput}';", 26 | $fileContent 27 | ); 28 | 29 | // Check if replacement was successful 30 | if ($updatedContent === null) { 31 | echo "Error updating version.\n"; 32 | exit(1); 33 | } 34 | 35 | // Write the updated content back to the file 36 | $result = file_put_contents($filePath, $updatedContent); 37 | if ($result === false) { 38 | echo "Error writing updated content back to file.\n"; 39 | exit(1); 40 | } 41 | 42 | echo "Version updated to {$versionInput} in {$filePath}.\n"; 43 | -------------------------------------------------------------------------------- /src/ArgumentValueNormalizer.php: -------------------------------------------------------------------------------- 1 | static::MAX_DEPTH) { 23 | $n = count($value); 24 | $items = $n > 1 ? 'items' : 'item'; 25 | 26 | return "Array($n $items)"; 27 | } 28 | 29 | return static::normalizeArray($value, $currentDepth); 30 | 31 | case 'object': 32 | return static::normalizeObject($value); 33 | 34 | default: 35 | return $value; 36 | } 37 | } 38 | 39 | protected static function normalizeArray(array $array, int $currentDepth = 0): array 40 | { 41 | $normalized = []; 42 | $keyCount = 0; 43 | foreach ($array as $key => $item) { 44 | $keyCount++; 45 | if ($keyCount > static::MAX_KEYS_IN_ARRAY) { 46 | break; 47 | } 48 | $normalized[$key] = static::normalize($item, $currentDepth + 1); 49 | } 50 | 51 | return $normalized; 52 | } 53 | 54 | protected static function normalizeObject(object $object): string 55 | { 56 | $class = get_class($object); 57 | 58 | // The [LITERAL] token indicates to the Honeybadger UI that this value should be rendered as-is, without any surrounding quotes. See issue #133. 59 | return "[LITERAL]Object($class)"; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/BacktraceFactory.php: -------------------------------------------------------------------------------- 1 | exception = $exception; 29 | $this->config = $config; 30 | } 31 | 32 | /** 33 | * @return array 34 | */ 35 | public function trace(): array 36 | { 37 | $backtrace = $this->offsetForThrownException( 38 | $this->exception->getTrace() 39 | ); 40 | 41 | return $this->formatBacktrace($backtrace); 42 | } 43 | 44 | /** 45 | * @return array 46 | */ 47 | public function previous(): array 48 | { 49 | return $this->formatPrevious($this->exception); 50 | } 51 | 52 | /** 53 | * @param \Throwable $e 54 | * @param array $previousCauses 55 | * 56 | * @return array 57 | */ 58 | private function formatPrevious(Throwable $e, array $previousCauses = []): array 59 | { 60 | if ($e = $e->getPrevious()) { 61 | $previousCauses[] = [ 62 | 'class' => get_class($e), 63 | 'message' => $e->getMessage(), 64 | 'backtrace' => (new self($e, $this->config))->trace(), 65 | ]; 66 | 67 | return $this->formatPrevious($e, $previousCauses); 68 | } 69 | 70 | return $previousCauses; 71 | } 72 | 73 | /** 74 | * @param array $backtrace 75 | * 76 | * @return array 77 | */ 78 | private function offsetForThrownException(array $backtrace): array 79 | { 80 | if ($this->exception instanceof ErrorException) { 81 | // For errors (ie not exceptions), the trace wrongly starts from 82 | // when we created the wrapping ErrorException class. 83 | // So we unwind it to the actual error location 84 | while (strpos($backtrace[0]['class'] ?? '', 'Honeybadger\\') !== false) { 85 | array_shift($backtrace); 86 | } 87 | } 88 | 89 | $backtrace[0] = array_merge($backtrace[0] ?? [], [ 90 | 'line' => $this->exception->getLine(), 91 | 'file' => $this->exception->getFile(), 92 | ]); 93 | 94 | return $backtrace; 95 | } 96 | 97 | /** 98 | * @param array $backtrace 99 | * 100 | * @return array 101 | */ 102 | private function formatBacktrace(array $backtrace): array 103 | { 104 | return array_map(function ($frame) { 105 | if (!array_key_exists('file', $frame)) { 106 | $context = $this->contextWithoutFile($frame); 107 | } else { 108 | $context = $this->contextWithFile($frame); 109 | } 110 | 111 | return array_merge($context, [ 112 | 'method' => $frame['function'] ?? null, 113 | 'args' => $this->parseArgs($frame['args'] ?? []), 114 | 'class' => $frame['class'] ?? null, 115 | 'type' => $frame['type'] ?? null, 116 | ]); 117 | }, $backtrace); 118 | } 119 | 120 | /** 121 | * Parse method arguments and make any transformations. 122 | * 123 | * @param array $args 124 | * 125 | * @return array 126 | */ 127 | private function parseArgs(array $args): array 128 | { 129 | return array_map(function ($arg) { 130 | return ArgumentValueNormalizer::normalize($arg); 131 | }, $args); 132 | } 133 | 134 | /** 135 | * @param array $frame 136 | * 137 | * @return array 138 | */ 139 | private function contextWithoutFile(array $frame): array 140 | { 141 | if (!empty($frame['class'])) { 142 | $filename = sprintf('%s%s%s', $frame['class'], $frame['type'], $frame['function']); 143 | 144 | try { 145 | $reflect = new ReflectionClass($frame['class']); 146 | $filename = $reflect->getFileName(); 147 | } catch (ReflectionException $e) { 148 | // Forget it if we run into errors, it's not worth it. 149 | } 150 | } elseif (!empty($frame['function'])) { 151 | $filename = sprintf('%s(anonymous)', $frame['function']); 152 | } else { 153 | $filename = sprintf('(anonymous)'); 154 | } 155 | 156 | if (empty($filename)) { 157 | $filename = '[Anonymous function]'; 158 | } 159 | 160 | return [ 161 | 'source' => null, 162 | 'file' => $filename, 163 | 'number' => '0', 164 | ]; 165 | } 166 | 167 | /** 168 | * @param array $frame 169 | * 170 | * @return array 171 | */ 172 | private function contextWithFile(array $frame): array 173 | { 174 | return [ 175 | 'source' => (new FileSource($frame['file'], $frame['line']))->getSource(), 176 | 'file' => $frame['file'], 177 | 'number' => (string)$frame['line'], 178 | 'context' => $this->fileFromApplication($frame['file'], $this->config['vendor_paths']) 179 | ? 'app' : 'all', 180 | ]; 181 | } 182 | 183 | private function fileFromApplication(string $filePath, array $vendorPaths): bool 184 | { 185 | $path = $this->appendProjectRootToFilePath($filePath); 186 | 187 | // On Windows, file paths use backslashes, so we have to normalise them 188 | $path = str_replace('\\', '/', $path); 189 | 190 | if (preg_match('/' . array_shift($vendorPaths) . '/', $path)) { 191 | return false; 192 | } 193 | 194 | if (!empty($vendorPaths)) { 195 | return $this->fileFromApplication($filePath, $vendorPaths); 196 | } 197 | 198 | return true; 199 | } 200 | 201 | private function appendProjectRootToFilePath(string $filePath): string 202 | { 203 | $pregProjectRoot = preg_quote($this->config['project_root'] . '/', '/'); 204 | 205 | return $this->config['project_root'] 206 | ? preg_replace('/' . $pregProjectRoot . '/', '', $filePath) 207 | : ''; 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/Breadcrumbs.php: -------------------------------------------------------------------------------- 1 | (string) $item['message'], 18 | 'category' => (string) ($item['category'] ?? 'custom'), 19 | 'metadata' => $this->sanitize($item['metadata'] ?? []), 20 | 'timestamp' => $item['timestamp'] ?? date('c'), 21 | ]; 22 | 23 | return parent::add($item); 24 | } 25 | 26 | /** 27 | * Limit an array to a simple [key => value] (one level) containing only primitives. 28 | */ 29 | private function sanitize(array $data): array 30 | { 31 | $sanitized = []; 32 | foreach ($data as $key => $value) { 33 | if (is_array($value) || is_object($value) || is_resource($value)) { 34 | $value = '[DEPTH]'; 35 | } 36 | 37 | if (is_string($value)) { 38 | // Limit strings to 64kB. 39 | $value = substr($value, 0, 64000); 40 | } 41 | 42 | $sanitized[$key] = $value; 43 | } 44 | 45 | return $sanitized; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/BulkEventDispatcher.php: -------------------------------------------------------------------------------- 1 | client = $client; 38 | $eventsConfig = $config->get('events') ?? []; 39 | $this->maxEvents = $eventsConfig['bulk_threshold'] ?? self::BULK_THRESHOLD; 40 | $this->dispatchInterval = $eventsConfig['dispatch_interval_seconds'] ?? self::DISPATCH_INTERVAL_SECONDS; 41 | $this->lastDispatchTime = time(); 42 | } 43 | 44 | public function addEvent($event) 45 | { 46 | $this->events[] = $event; 47 | 48 | if (count($this->events) >= $this->maxEvents || (time() - $this->lastDispatchTime) >= $this->dispatchInterval) { 49 | $this->dispatchEvents(); 50 | } 51 | } 52 | 53 | public function flushEvents() 54 | { 55 | if (!$this->hasEvents()) { 56 | return; 57 | } 58 | 59 | $this->dispatchEvents(); 60 | } 61 | 62 | public function hasEvents(): bool { 63 | return !empty($this->events); 64 | } 65 | 66 | private function dispatchEvents() 67 | { 68 | if (!$this->hasEvents()) { 69 | return; 70 | } 71 | 72 | $this->client->events($this->events); 73 | 74 | $this->events = []; 75 | $this->lastDispatchTime = time(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/CheckIn.php: -------------------------------------------------------------------------------- 1 | id = $params['id'] ?? null; 84 | $this->name = $params['name'] ?? null; 85 | $this->slug = $params['slug'] ?? null; 86 | $this->scheduleType = $params['schedule_type'] ?? null; 87 | $this->reportPeriod = $params['report_period'] ?? null; 88 | $this->gracePeriod = $params['grace_period'] ?? null; 89 | $this->cronSchedule = $params['cron_schedule'] ?? null; 90 | $this->cronTimezone = $params['cron_timezone'] ?? null; 91 | $this->deleted = false; 92 | } 93 | 94 | public function isDeleted(): bool 95 | { 96 | return $this->deleted; 97 | } 98 | 99 | public function markAsDeleted(): void 100 | { 101 | $this->deleted = true; 102 | } 103 | 104 | /** 105 | * @throws ServiceException 106 | */ 107 | public function validate(): void { 108 | if ($this->slug === null) { 109 | throw ServiceException::invalidConfig('slug is required for each check-in'); 110 | } 111 | 112 | $slug = $this->slug; 113 | 114 | if (in_array($this->scheduleType, ['simple', 'cron']) === false) { 115 | throw ServiceException::invalidConfig("$slug [schedule_type] must be either 'simple' or 'cron'"); 116 | } 117 | 118 | if ($this->scheduleType === 'simple' && $this->reportPeriod === null) { 119 | throw ServiceException::invalidConfig("$slug [report_period] is required for simple check-ins"); 120 | } 121 | 122 | if ($this->scheduleType === 'cron' && $this->cronSchedule === null) { 123 | throw ServiceException::invalidConfig("$slug [cron_schedule] is required for cron check-ins"); 124 | } 125 | } 126 | 127 | public function asRequestData(): array 128 | { 129 | $result = [ 130 | 'name' => $this->name ?? '', 131 | 'schedule_type' => $this->scheduleType, 132 | 'slug' => $this->slug, 133 | 'grace_period' => $this->gracePeriod ?? '', 134 | ]; 135 | 136 | if ($this->scheduleType === 'simple') { 137 | $result['report_period'] = $this->reportPeriod; 138 | } 139 | 140 | if ($this->scheduleType === 'cron') { 141 | $result['cron_schedule'] = $this->cronSchedule; 142 | $result['cron_timezone'] = $this->cronTimezone ?? ''; 143 | } 144 | 145 | return $result; 146 | } 147 | 148 | /** 149 | * Compares two checkins, usually the one from the API and the one from the config file. 150 | * If the one in the config file does not match the checkin from the API, 151 | * then we issue an update request. 152 | */ 153 | public function isInSync(CheckIn $other): bool 154 | { 155 | $ignoreNameCheck = $this->name === null; 156 | $ignoreGracePeriodCheck = $this->gracePeriod === null; 157 | $ignoreCronTimezoneCheck = $this->cronTimezone === null; 158 | 159 | return $this->slug === $other->slug 160 | && $this->scheduleType === $other->scheduleType 161 | && $this->reportPeriod === $other->reportPeriod 162 | && $this->cronSchedule === $other->cronSchedule 163 | && ($ignoreNameCheck || $this->name === $other->name) 164 | && ($ignoreGracePeriodCheck || $this->gracePeriod === $other->gracePeriod) 165 | && ($ignoreCronTimezoneCheck || $this->cronTimezone === $other->cronTimezone); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/CheckInsClient.php: -------------------------------------------------------------------------------- 1 | hasPersonalAuthToken()) { 24 | throw ServiceException::missingPersonalAuthToken(); 25 | } 26 | 27 | try { 28 | $url = sprintf('v2/project_keys/%s', $projectApiKey); 29 | $response = $this->client->get($url); 30 | 31 | if ($response->getStatusCode() !== Response::HTTP_OK) { 32 | throw (new ServiceExceptionFactory($response))->make(); 33 | } 34 | 35 | $data = json_decode($response->getBody(), true); 36 | 37 | return $data['project']['id']; 38 | } catch (ServiceException $e) { 39 | throw $e; 40 | } catch (Throwable $e) { 41 | throw ServiceException::generic($e); 42 | } 43 | } 44 | 45 | /** 46 | * @param string $projectId 47 | * @return CheckIn[]|null 48 | * 49 | * @throws ServiceException 50 | */ 51 | public function listForProject(string $projectId): ?array 52 | { 53 | if (! $this->hasPersonalAuthToken()) { 54 | throw ServiceException::missingPersonalAuthToken(); 55 | } 56 | 57 | try { 58 | $url = sprintf('v2/projects/%s/check_ins', $projectId); 59 | $response = $this->client->get($url); 60 | 61 | if ($response->getStatusCode() !== Response::HTTP_OK) { 62 | throw (new ServiceExceptionFactory($response))->make(); 63 | } 64 | 65 | $data = json_decode($response->getBody(), true); 66 | 67 | return array_map(function ($checkIn) use ($projectId) { 68 | return new CheckIn($checkIn); 69 | }, $data['results']); 70 | } catch (ServiceException $e) { 71 | throw $e; 72 | } catch (Throwable $e) { 73 | throw ServiceException::generic($e); 74 | } 75 | } 76 | 77 | /** 78 | * @throws ServiceException 79 | */ 80 | public function get(string $projectId, string $checkinId): CheckIn 81 | { 82 | if (! $this->hasPersonalAuthToken()) { 83 | throw ServiceException::missingPersonalAuthToken(); 84 | } 85 | 86 | try { 87 | $url = sprintf('v2/projects/%s/check_ins/%s', $projectId, $checkinId); 88 | $response = $this->client->get($url); 89 | 90 | if ($response->getStatusCode() !== Response::HTTP_OK) { 91 | throw (new ServiceExceptionFactory($response))->make(); 92 | } 93 | 94 | $data = json_decode($response->getBody(), true); 95 | 96 | return new CheckIn($data); 97 | } catch (ServiceException $e) { 98 | throw $e; 99 | } catch (Throwable $e) { 100 | throw ServiceException::generic($e); 101 | } 102 | } 103 | 104 | /** 105 | * @throws ServiceException 106 | */ 107 | public function create(string $projectId, CheckIn $checkIn): CheckIn 108 | { 109 | if (! $this->hasPersonalAuthToken()) { 110 | throw ServiceException::missingPersonalAuthToken(); 111 | } 112 | 113 | try { 114 | $url = sprintf('v2/projects/%s/check_ins', $projectId); 115 | $response = $this->client->post($url, [ 116 | 'json' => [ 117 | 'check_in' => $checkIn->asRequestData() 118 | ] 119 | ]); 120 | 121 | if ($response->getStatusCode() !== Response::HTTP_CREATED) { 122 | throw (new ServiceExceptionFactory($response))->make(); 123 | } 124 | 125 | $data = json_decode($response->getBody(), true); 126 | 127 | return new CheckIn($data); 128 | } catch (ServiceException $e) { 129 | throw $e; 130 | } catch (Throwable $e) { 131 | throw ServiceException::generic($e); 132 | } 133 | } 134 | 135 | /** 136 | * @throws ServiceException 137 | */ 138 | public function update(string $projectId, CheckIn $checkIn): CheckIn 139 | { 140 | if (! $this->hasPersonalAuthToken()) { 141 | throw ServiceException::missingPersonalAuthToken(); 142 | } 143 | 144 | try { 145 | $url = sprintf('v2/projects/%s/check_ins/%s', $projectId, $checkIn->id); 146 | $response = $this->client->put($url, [ 147 | 'json' => [ 148 | 'check_in' => $checkIn->asRequestData() 149 | ] 150 | ]); 151 | 152 | if ($response->getStatusCode() !== Response::HTTP_NO_CONTENT) { 153 | throw (new ServiceExceptionFactory($response))->make(); 154 | } 155 | 156 | return $checkIn; 157 | } catch (ServiceException $e) { 158 | throw $e; 159 | } catch (Throwable $e) { 160 | throw ServiceException::generic($e); 161 | } 162 | } 163 | 164 | /** 165 | * @throws ServiceException 166 | */ 167 | public function remove(string $projectId, string $checkInId): void { 168 | if (! $this->hasPersonalAuthToken()) { 169 | throw ServiceException::missingPersonalAuthToken(); 170 | } 171 | 172 | try { 173 | $url = sprintf('v2/projects/%s/check_ins/%s', $projectId, $checkInId); 174 | $response = $this->client->delete($url); 175 | 176 | if ($response->getStatusCode() !== Response::HTTP_NO_CONTENT) { 177 | throw (new ServiceExceptionFactory($response))->make(); 178 | } 179 | } catch (ServiceException $e) { 180 | throw $e; 181 | } catch (Throwable $e) { 182 | throw ServiceException::generic($e); 183 | } 184 | } 185 | 186 | public function makeClient(): Client 187 | { 188 | return new Client([ 189 | 'base_uri' => $this->config['app_endpoint'], 190 | RequestOptions::HTTP_ERRORS => false, 191 | RequestOptions::AUTH => [ 192 | $this->config['personal_auth_token'], '' 193 | ], 194 | RequestOptions::HEADERS => [ 195 | 'User-Agent' => $this->getUserAgent(), 196 | ], 197 | ]); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/CheckInsClientWithErrorHandling.php: -------------------------------------------------------------------------------- 1 | config = $config; 24 | $this->baseClient = new CheckInsClient($config, $httpClient); 25 | } 26 | 27 | /** 28 | * @return mixed|null 29 | */ 30 | public function __call($name, $arguments) 31 | { 32 | try { 33 | return $this->baseClient->{$name}(...$arguments); 34 | } 35 | catch (ServiceException $e) { 36 | $this->handleServiceException($e); 37 | } 38 | catch (Throwable $e) { 39 | $this->handleServiceException(ServiceException::generic($e)); 40 | } 41 | 42 | return null; 43 | } 44 | 45 | protected function handleServiceException(ServiceException $e): void 46 | { 47 | $serviceExceptionHandler = $this->config['service_exception_handler']; 48 | call_user_func_array($serviceExceptionHandler, [$e]); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/CheckInsManager.php: -------------------------------------------------------------------------------- 1 | config = new Config($config); 29 | $this->client = $client ?? new CheckInsClient($this->config); 30 | } 31 | 32 | 33 | /** 34 | * @param array $checkIns 35 | * @return CheckIn[] 36 | * 37 | * @throws ServiceException 38 | */ 39 | public function sync(array $checkIns): array 40 | { 41 | $localCheckIns = $this->getLocalCheckIns($checkIns); 42 | $projectId = $this->client->getProjectId($this->config->get('api_key')); 43 | $remoteCheckIns = $this->client->listForProject($projectId) ?? []; 44 | $createdOrUpdated = $this->synchronizeLocalCheckIns($projectId, $localCheckIns, $remoteCheckIns); 45 | $removed = $this->removeNotFoundCheckIns($projectId, $localCheckIns, $remoteCheckIns); 46 | 47 | return array_merge($createdOrUpdated, $removed); 48 | } 49 | 50 | /** 51 | * @param array $checkIns 52 | * @return CheckIn[] 53 | * 54 | * @throws ServiceException 55 | */ 56 | private function getLocalCheckIns(array $checkIns): array 57 | { 58 | $localCheckIns = array_map(function ($checkIn) { 59 | $checkIn = new CheckIn($checkIn); 60 | $checkIn->validate(); 61 | 62 | return $checkIn; 63 | }, $checkIns); 64 | 65 | // check that there are no check-ins with same slug 66 | $checkInSlugs = array_unique(array_map(function ($checkIn) { 67 | return $checkIn->slug; 68 | }, $localCheckIns)); 69 | 70 | if (count($checkInSlugs) !== count($localCheckIns)) { 71 | throw ServiceException::invalidConfig('Check-ins must have unique slug values'); 72 | } 73 | 74 | return $localCheckIns; 75 | } 76 | 77 | /** 78 | * Loop through local check-ins array and 79 | * create or update each check-ins. 80 | * 81 | * @param string $projectId 82 | * @param CheckIn[] $localCheckIns 83 | * @param CheckIn[] $remoteCheckIns 84 | * @return CheckIn[] 85 | * 86 | * @throws ServiceException 87 | */ 88 | private function synchronizeLocalCheckIns(string $projectId, array $localCheckIns, array $remoteCheckIns): array 89 | { 90 | $result = []; 91 | 92 | foreach ($localCheckIns as $localCheckIn) { 93 | $remoteCheckIn = null; 94 | $filtered = array_filter($remoteCheckIns, function ($checkIn) use ($localCheckIn) { 95 | return $checkIn->slug === $localCheckIn->slug; 96 | }); 97 | if (count($filtered) > 0) { 98 | $remoteCheckIn = array_values($filtered)[0]; 99 | } 100 | 101 | if ($remoteCheckIn) { 102 | $localCheckIn->id = $remoteCheckIn->id; 103 | if (! $remoteCheckIn->isInSync($localCheckIn)) { 104 | if ($updated = $this->update($projectId, $localCheckIn)) { 105 | $result[] = $updated; 106 | } 107 | } else { 108 | // no change - just add to resulting array 109 | $result[] = $remoteCheckIn; 110 | } 111 | } 112 | else if ($created = $this->create($projectId, $localCheckIn)) { 113 | $result[] = $created; 114 | } 115 | } 116 | 117 | return $result; 118 | } 119 | 120 | /** 121 | * Loop through existing check-ins and 122 | * remove any that are not in the local check-ins array. 123 | * 124 | * @param string $projectId 125 | * @param CheckIn[] $localCheckIns 126 | * @param CheckIn[] $remoteCheckIns 127 | * @return CheckIn[] 128 | * 129 | * @throws ServiceException 130 | */ 131 | private function removeNotFoundCheckIns(string $projectId, array $localCheckIns, array $remoteCheckIns): array 132 | { 133 | $result = []; 134 | 135 | foreach ($remoteCheckIns as $remoteCheckIn) { 136 | $filtered = array_filter($localCheckIns, function ($checkIn) use ($remoteCheckIn) { 137 | return $checkIn->slug === $remoteCheckIn->slug; 138 | }); 139 | if (count($filtered) === 0) { 140 | $this->remove($projectId, $remoteCheckIn); 141 | $result[] = $remoteCheckIn; 142 | } 143 | } 144 | 145 | return $result; 146 | } 147 | 148 | /** 149 | * @throws ServiceException 150 | */ 151 | private function create(string $projectId, CheckIn $checkIn): ?CheckIn 152 | { 153 | return $this->client->create($projectId, $checkIn); 154 | } 155 | 156 | /** 157 | * @throws ServiceException 158 | */ 159 | private function update(string $projectId, CheckIn $checkIn): ?CheckIn 160 | { 161 | return $this->client->update($projectId, $checkIn); 162 | } 163 | 164 | /** 165 | * @throws ServiceException 166 | */ 167 | private function remove(string $projectId, CheckIn $checkIn): void 168 | { 169 | $this->client->remove($projectId, $checkIn->id); 170 | $checkIn->markAsDeleted(); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/Concerns/FiltersData.php: -------------------------------------------------------------------------------- 1 | keysToFilter = array_merge($this->keysToFilter, $keysToFilter); 21 | 22 | return $this; 23 | } 24 | 25 | /** 26 | * @param array $values 27 | * @return array 28 | */ 29 | private function filter(array $values): array 30 | { 31 | return Arr::mapWithKeys($values, function ($value, $key) { 32 | if (is_array($value) && ! Arr::isAssociative($value)) { 33 | return $value; 34 | } 35 | 36 | if (is_array($value)) { 37 | return $this->filter($value); 38 | } 39 | 40 | if ($value !== '' && in_array($key, $this->keysToFilter)) { 41 | return '[FILTERED]'; 42 | } 43 | 44 | return $value; 45 | }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Concerns/Newable.php: -------------------------------------------------------------------------------- 1 | newInstanceArgs(func_get_args()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Config.php: -------------------------------------------------------------------------------- 1 | items = $this->mergeConfig($config); 16 | $checkinsRaw = $this->get('checkins') ?? []; 17 | $checkins = array_map(function ($checkin) { 18 | return new CheckIn($checkin); 19 | }, $checkinsRaw); 20 | $this->set('checkins', $checkins); 21 | } 22 | 23 | /** 24 | * @param array $config 25 | * 26 | * @return array 27 | */ 28 | private function mergeConfig($config = []): array 29 | { 30 | $result = array_merge([ 31 | 'api_key' => null, 32 | 'personal_auth_token' => null, 33 | 'endpoint' => Honeybadger::API_URL, 34 | 'app_endpoint' => Honeybadger::APP_URL, 35 | 'notifier' => [ 36 | 'name' => 'honeybadger-php', 37 | 'url' => 'https://github.com/honeybadger-io/honeybadger-php', 38 | 'version' => Honeybadger::VERSION, 39 | ], 40 | 'environment_name' => 'production', 41 | 'report_data' => true, 42 | 'service_exception_handler' => function (ServiceException $e) { 43 | throw $e; 44 | }, 45 | 'events_exception_handler' => function (ServiceException $e) { 46 | // default: noop 47 | // this should be a noop operation by default. 48 | // many events could be sent from your application and in case of errors, even logging them will add too much noise. 49 | // you may want to override this in your application to log or handle the error in a different way (or debug). 50 | }, 51 | 'environment' => [ 52 | 'filter' => [], 53 | 'include' => [], 54 | ], 55 | 'request' => [ 56 | 'filter' => [], 57 | ], 58 | 'version' => '', 59 | 'hostname' => gethostname(), 60 | 'project_root' => '', 61 | 'handlers' => [ 62 | 'exception' => true, 63 | 'error' => true, 64 | 'shutdown' => true, 65 | ], 66 | 'client' => [ 67 | 'timeout' => 15, 68 | 'proxy' => [], 69 | 'verify' => true, 70 | ], 71 | 'excluded_exceptions' => [], 72 | 'capture_deprecations' => false, 73 | 'vendor_paths' => [ 74 | 'vendor\/.*', 75 | ], 76 | 'breadcrumbs' => [ 77 | 'enabled' => true, 78 | ], 79 | 'checkins' => [], 80 | 'events' => [ 81 | 'enabled' => false, 82 | 'bulk_threshold' => BulkEventDispatcher::BULK_THRESHOLD, 83 | 'dispatch_interval_seconds' => BulkEventDispatcher::DISPATCH_INTERVAL_SECONDS, 84 | 'sample_rate' => 100 85 | ], 86 | ], $config); 87 | 88 | if (!isset($result['handlers']['shutdown'])) { 89 | // the 'shutdown' field is new, array_merge only merges on the first level 90 | // so we need to manually set it if the config has a 'handlers' key but not a 'shutdown' key inside 91 | $result['handlers']['shutdown'] = true; 92 | } 93 | 94 | return $result; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Contracts/ApiClient.php: -------------------------------------------------------------------------------- 1 | config = $config; 27 | $this->client = $httpClient ?? $this->makeClient(); 28 | } 29 | 30 | /** 31 | * @return Client 32 | */ 33 | public abstract function makeClient(): Client; 34 | 35 | protected function handleServiceException(ServiceException $e): void 36 | { 37 | $serviceExceptionHandler = $this->config['service_exception_handler']; 38 | call_user_func_array($serviceExceptionHandler, [$e]); 39 | } 40 | 41 | protected function handleEventsException(ServiceException $e): void 42 | { 43 | $eventsExceptionHandler = $this->config['events_exception_handler']; 44 | call_user_func_array($eventsExceptionHandler, [$e]); 45 | } 46 | 47 | public function hasPersonalAuthToken(): bool 48 | { 49 | return !empty($this->config['personal_auth_token']); 50 | } 51 | 52 | public function getUserAgent(): string 53 | { 54 | $userAgent = 'Honeybadger PHP; ' . PHP_VERSION; 55 | if (isset($this->config['notifier'], $this->config['notifier']['name'], $this->config['notifier']['version'])) { 56 | $userAgent = $this->config['notifier']['name'] . ' ' . $this->config['notifier']['version'] . '; ' . PHP_VERSION; 57 | } 58 | 59 | return $userAgent; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/Contracts/Handler.php: -------------------------------------------------------------------------------- 1 | config = $config; 26 | $this->context = $context; 27 | } 28 | 29 | /** 30 | * @param array $payload 31 | * @return array 32 | */ 33 | public function make(array $payload): array 34 | { 35 | return array_merge( 36 | [], 37 | ['notifier' => $this->config['notifier']], 38 | [ 39 | 'error' => [ 40 | 'class' => $payload['title'] ?? '', 41 | 'message' => $payload['message'] ?? '', 42 | ], 43 | ], 44 | ['request' => [ 45 | 'context' => (object) $this->context->all(), ], 46 | ], 47 | ['server' => (object) [ 48 | 'environment_name' => $this->config['environment_name'], 49 | ]] 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Environment.php: -------------------------------------------------------------------------------- 1 | server = array_merge( 58 | $server ?? $_SERVER, 59 | $env ?? $_ENV 60 | ); 61 | 62 | $this->keysToFilter = ['HTTP_AUTHORIZATION']; 63 | } 64 | 65 | /** 66 | * @return array 67 | */ 68 | public function values(): array 69 | { 70 | return $this->filter($this->data()); 71 | } 72 | 73 | /** 74 | * @param array $keysToInclude 75 | * @return \Honeybadger\Environment 76 | */ 77 | public function include(array $keysToInclude): self 78 | { 79 | $this->includeKeys = array_merge($this->includeKeys, $keysToInclude); 80 | 81 | return $this; 82 | } 83 | 84 | /** 85 | * @return array 86 | */ 87 | private function data(): array 88 | { 89 | return array_filter($this->server, function ($key) { 90 | return $this->autoIncludeKey($key) || in_array($key, $this->includeKeys); 91 | }, ARRAY_FILTER_USE_KEY); 92 | } 93 | 94 | /** 95 | * @param string $key 96 | * @return bool 97 | */ 98 | private function whitelistKey(string $key): bool 99 | { 100 | return in_array($key, self::KEY_WHITELIST); 101 | } 102 | 103 | /** 104 | * @param string $key 105 | * @return bool 106 | */ 107 | private function httpKey(string $key): bool 108 | { 109 | return strpos($key, 'HTTP_') === 0; 110 | } 111 | 112 | /** 113 | * @param string $key 114 | * @return bool 115 | */ 116 | private function autoIncludeKey(string $key): bool 117 | { 118 | return $this->whitelistKey($key) || $this->httpKey($key); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/ExceptionNotification.php: -------------------------------------------------------------------------------- 1 | config = $config; 62 | $this->context = $context; 63 | $this->breadcrumbs = $breadcrumbs; 64 | } 65 | 66 | /** 67 | * @param \Throwable $e 68 | * @param ?\Symfony\Component\HttpFoundation\Request $request 69 | * @param array $additionalParams 70 | * @return array 71 | */ 72 | public function make(Throwable $e, ?FoundationRequest $request = null, array $additionalParams = []): array 73 | { 74 | $this->throwable = $e; 75 | $this->backtrace = $this->makeBacktrace(); 76 | $this->request = $this->makeRequest($request); 77 | $this->additionalParams = $additionalParams; 78 | $this->environment = $this->makeEnvironment(); 79 | 80 | return $this->format(); 81 | } 82 | 83 | /** 84 | * @return array 85 | */ 86 | private function format(): array 87 | { 88 | return [ 89 | 'breadcrumbs' => [ 90 | 'enabled' => $this->config['breadcrumbs']['enabled'], 91 | 'trail' => $this->breadcrumbs->toArray(), 92 | ], 93 | 'notifier' => $this->config['notifier'], 94 | 'error' => [ 95 | 'class' => $this->getExceptionType(), 96 | 'message' => $this->throwable->getMessage(), 97 | 'backtrace' => $this->backtrace->trace(), 98 | 'causes' => $this->backtrace->previous(), 99 | 'fingerprint' => Arr::get($this->additionalParams, 'fingerprint', null), 100 | 'tags' => Arr::wrap(Arr::get($this->additionalParams, 'tags', null)), 101 | ], 102 | 'request' => [ 103 | // Important to set empty maps to stdClass so they don't get JSON-encoded as arrays 104 | 'cgi_data' => $this->environment->values() ?: new stdClass, 105 | 'params' => $this->request->params() ?: new stdClass, 106 | 'session' => $this->request->session() ?: new stdClass, 107 | 'url' => $this->request->url(), 108 | 'context' => $this->context->except(['honeybadger_component', 'honeybadger_action']) ?: new stdClass, 109 | 'component' => Arr::get($this->additionalParams, 'component', null) ?? Arr::get($this->context, 'honeybadger_component', null), 110 | 'action' => Arr::get($this->additionalParams, 'action', null) ?? Arr::get($this->context, 'honeybadger_action', null), 111 | ], 112 | 'server' => [ 113 | 'pid' => getmypid(), 114 | 'version' => $this->config['version'], 115 | 'hostname' => $this->config['hostname'], 116 | 'project_root' => $this->config['project_root'], 117 | 'environment_name' => $this->config['environment_name'], 118 | ], 119 | ]; 120 | } 121 | 122 | /** 123 | * @return \Honeybadger\Environment 124 | */ 125 | private function makeEnvironment(): Environment 126 | { 127 | return (new Environment) 128 | ->filterKeys($this->config['environment']['filter']) 129 | ->include($this->config['environment']['include']); 130 | } 131 | 132 | /** 133 | * @return \Honeybadger\BacktraceFactory 134 | */ 135 | private function makeBacktrace(): BacktraceFactory 136 | { 137 | return new BacktraceFactory($this->throwable, $this->config); 138 | } 139 | 140 | /** 141 | * @param ?\Symfony\Component\HttpFoundation\Request $request 142 | * @return \Honeybadger\Request 143 | */ 144 | private function makeRequest(?FoundationRequest $request = null): Request 145 | { 146 | return (new Request($request)) 147 | ->filterKeys($this->config['request']['filter']); 148 | } 149 | 150 | private function getExceptionType(): string 151 | { 152 | // ErrorExceptions are not exceptions, but wrappers around errors. 153 | if ($this->throwable instanceof ErrorException) { 154 | $severity = $this->throwable->getSeverity(); 155 | $severityName = $this->friendlyErrorName($severity); 156 | 157 | return $severity == E_DEPRECATED || $severity == E_USER_DEPRECATED 158 | ? "Deprecation Warning ($severityName)" 159 | : "Error ($severityName)"; 160 | } 161 | 162 | return get_class($this->throwable); 163 | } 164 | 165 | protected function friendlyErrorName(int $severity): string 166 | { 167 | // See https://www.php.net/manual/en/errorfunc.constants.php 168 | return [ 169 | E_ERROR => 'E_ERROR', 170 | E_WARNING => 'E_WARNING', 171 | E_PARSE => 'E_PARSE', 172 | E_NOTICE => 'E_NOTICE', 173 | E_CORE_ERROR => 'E_CORE_ERROR', 174 | E_CORE_WARNING => 'E_CORE_WARNING', 175 | E_COMPILE_ERROR => 'E_COMPILE_ERROR', 176 | E_COMPILE_WARNING => 'E_COMPILE_WARNING', 177 | E_USER_ERROR => 'E_USER_ERROR', 178 | E_USER_WARNING => 'E_USER_WARNING', 179 | E_USER_NOTICE => 'E_USER_NOTICE', 180 | E_STRICT => 'E_STRICT', 181 | E_RECOVERABLE_ERROR => 'E_RECOVERABLE_ERROR', 182 | E_DEPRECATED => 'E_DEPRECATED', 183 | E_USER_DEPRECATED => 'E_USER_DEPRECATED', 184 | ][$severity] ?? ''; 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/Exceptions/ServiceException.php: -------------------------------------------------------------------------------- 1 | getMessage() 67 | : 'There was an error sending the payload to Honeybadger.'; 68 | 69 | return new static($message, 0, $e); 70 | } 71 | 72 | /** 73 | * @param string $message 74 | * @return self 75 | */ 76 | public static function invalidConfig(string $message): self 77 | { 78 | return new static("The configuration is invalid: $message"); 79 | } 80 | 81 | /** 82 | * @return self 83 | */ 84 | public static function missingPersonalAuthToken(): self 85 | { 86 | return new static("Missing personal auth token. This token is required to use Honeybadger's Data APIs."); 87 | } 88 | 89 | /** 90 | * @param string $message 91 | * @return self 92 | */ 93 | public static function withMessage(string $message): self 94 | { 95 | return new static($message); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Exceptions/ServiceExceptionFactory.php: -------------------------------------------------------------------------------- 1 | response = $response; 21 | } 22 | 23 | public function make(bool $isFromEventsApi = false): ServiceException 24 | { 25 | return $this->exception($isFromEventsApi); 26 | } 27 | 28 | private function exception(bool $isFromEventsApi = false): ServiceException 29 | { 30 | try { 31 | $message = $this->response->getBody()->getContents(); 32 | if (!empty($message)) { 33 | $data = json_decode($message, true); 34 | if (isset($data['errors'])) { 35 | return ServiceException::withMessage($data['errors']); 36 | } 37 | } 38 | } catch (\Exception $e) { 39 | // Do nothing 40 | // Fallback to default error messages based on status code 41 | } 42 | 43 | if ($this->response->getStatusCode() === Response::HTTP_FORBIDDEN) { 44 | return ServiceException::invalidApiKey(); 45 | } 46 | 47 | if ($this->response->getStatusCode() === Response::HTTP_UNPROCESSABLE_ENTITY) { 48 | return ServiceException::invalidPayload(); 49 | } 50 | 51 | if ($this->response->getStatusCode() === Response::HTTP_TOO_MANY_REQUESTS) { 52 | return $isFromEventsApi 53 | ? ServiceException::eventsRateLimit() 54 | : ServiceException::rateLimit(); 55 | } 56 | 57 | if ($this->response->getStatusCode() === Response::HTTP_INTERNAL_SERVER_ERROR) { 58 | return ServiceException::serverError(); 59 | } 60 | 61 | return ServiceException::unexpectedResponseCode($this->response->getStatusCode()); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/FileSource.php: -------------------------------------------------------------------------------- 1 | filename = $filename; 32 | $this->lineNumber = $lineNumber < 0 ? 0 : $lineNumber; 33 | $this->radius = $radius; 34 | } 35 | 36 | /** 37 | * @return array 38 | */ 39 | public function getSource(): array 40 | { 41 | if (! $this->canReadFile()) { 42 | return []; 43 | } 44 | 45 | return array_slice( 46 | $this->fileLines($this->readFile()), 47 | $this->startingLineNumber(), 48 | ($this->radius * 2) + 1, 49 | $preserveKeys = true 50 | ); 51 | } 52 | 53 | /** 54 | * @param string $line 55 | * @return string 56 | */ 57 | private function trimLine(string $line): string 58 | { 59 | $trimmed = trim($line, "\n\r\0\x0B"); 60 | 61 | return preg_replace(['/\s*$/D', '/\t/'], ['', ' '], $trimmed); 62 | } 63 | 64 | /** 65 | * @return bool 66 | */ 67 | private function canReadFile(): bool 68 | { 69 | return is_file($this->filename) && is_readable($this->filename); 70 | } 71 | 72 | /** 73 | * @return \SplFileObject 74 | */ 75 | private function readFile(): SplFileObject 76 | { 77 | return new SplFileObject($this->filename, 'r'); 78 | } 79 | 80 | /** 81 | * @param \SplFileObject $file 82 | * @return array 83 | */ 84 | private function fileLines(SplFileObject $file): array 85 | { 86 | $lines = []; 87 | while (! $file->eof()) { 88 | $lines[] = $this->trimLine($file->fgets()); 89 | } 90 | 91 | // Set the array to base 1 so it actually reflects the line number of code 92 | array_unshift($lines, null); 93 | unset($lines[0]); 94 | 95 | return $lines; 96 | } 97 | 98 | /** 99 | * @return int 100 | */ 101 | private function startingLineNumber(): int 102 | { 103 | $start = $this->lineNumber - ($this->radius + 1); 104 | 105 | return $start >= 0 ? $start : 0; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Handlers/BeforeEventHandler.php: -------------------------------------------------------------------------------- 1 | previousHandler = set_error_handler([$this, 'handle']); 33 | } 34 | 35 | public function handle(int $level, string $error, ?string $file = null, ?int $line = null) 36 | { 37 | // When the @ operator is used, it temporarily changes `error_reporting()`'s return value 38 | // to reflect what error types should be reported. This means we should get 0 (no errors). 39 | $errorReportingLevel = error_reporting(); 40 | $isSilenced = ($errorReportingLevel == 0); 41 | 42 | if (PHP_MAJOR_VERSION >= 8) { 43 | // In PHP 8+, some errors are unsilenceable, so we should respect that. 44 | if (in_array($level, self::PHP8_UNSILENCEABLE_ERRORS)) { 45 | $isSilenced = false; 46 | } else { 47 | // If an error is silenced, `error_reporting()` won't return 0, 48 | // but rather a bitmask of the unsilenceable errors. 49 | $unsilenceableErrorsBitmask = array_reduce( 50 | self::PHP8_UNSILENCEABLE_ERRORS, function ($bitMask, $errLevel) { 51 | return $bitMask | $errLevel; 52 | }); 53 | $isSilenced = $errorReportingLevel === $unsilenceableErrorsBitmask; 54 | } 55 | } 56 | 57 | if ($isSilenced) { 58 | return false; 59 | } 60 | 61 | $this->honeybadger->notify( 62 | $this->wrapError($error, $level, $file, $line) 63 | ); 64 | 65 | if (is_callable($this->previousHandler)) { 66 | call_user_func($this->previousHandler, $level, $error, $file, $line); 67 | } 68 | } 69 | 70 | protected function wrapError(string $error, int $level, ?string $file, ?int $line): ErrorException 71 | { 72 | return new ErrorException($error, 0, $level, $file, $line); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Handlers/EventHandler.php: -------------------------------------------------------------------------------- 1 | listeners[] = $closure; 12 | } 13 | 14 | public function handle(array &$payload): bool 15 | { 16 | foreach ($this->listeners as $listener) { 17 | if ($listener($payload) === false) { 18 | return false; 19 | } 20 | } 21 | 22 | return true; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Handlers/ExceptionHandler.php: -------------------------------------------------------------------------------- 1 | previousHandler = set_exception_handler([$this, 'handle']); 21 | } 22 | 23 | /** 24 | * @param \Throwable $e 25 | * @return void 26 | * 27 | * @throws \Honeybadger\Exceptions\ServiceException 28 | */ 29 | public function handle(Throwable $e): void 30 | { 31 | $this->honeybadger->notify($e); 32 | 33 | if (is_callable($this->previousHandler)) { 34 | call_user_func($this->previousHandler, $e); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Handlers/Handler.php: -------------------------------------------------------------------------------- 1 | honeybadger = $honeybadger; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Handlers/ShutdownHandler.php: -------------------------------------------------------------------------------- 1 | honeybadger->flushEvents(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Honeybadger.php: -------------------------------------------------------------------------------- 1 | config = new Config($config); 86 | 87 | $this->client = new HoneybadgerClient($this->config, $client); 88 | $this->checkInsClient = new CheckInsClientWithErrorHandling($this->config, $client); 89 | $this->context = new Repository; 90 | $this->breadcrumbs = new Breadcrumbs(40); 91 | $this->events = $eventsDispatcher ?? new BulkEventDispatcher($this->config, $this->client); 92 | 93 | $this->setHandlers(); 94 | } 95 | 96 | /** 97 | * {@inheritdoc} 98 | */ 99 | public function notify(Throwable $throwable, ?FoundationRequest $request = null, array $additionalParams = []): array 100 | { 101 | if (! $this->shouldReport($throwable)) { 102 | return []; 103 | } 104 | 105 | $notification = new ExceptionNotification($this->config, $this->context, $this->breadcrumbs); 106 | 107 | if ($this->config['breadcrumbs']['enabled']) { 108 | $this->addBreadcrumb('Honeybadger Notice', [ 109 | 'message' => $throwable->getMessage(), 110 | 'name' => get_class($throwable), 111 | ], 'notice'); 112 | } 113 | 114 | $notification = $notification->make($throwable, $request, $additionalParams); 115 | if (!$this->beforeNotifyHandlers->handle($notification)) { 116 | return []; 117 | } 118 | 119 | return $this->client->notification($notification); 120 | } 121 | 122 | /** 123 | * {@inheritdoc} 124 | */ 125 | public function customNotification(array $payload): array 126 | { 127 | if (empty($this->config['api_key']) || ! $this->config['report_data']) { 128 | return []; 129 | } 130 | 131 | $notification = (new CustomNotification($this->config, $this->context)) 132 | ->make($payload); 133 | 134 | return $this->client->notification($notification); 135 | } 136 | 137 | /** 138 | * {@inheritdoc} 139 | */ 140 | public function rawNotification(callable $callable): array 141 | { 142 | if (empty($this->config['api_key']) || ! $this->config['report_data']) { 143 | return []; 144 | } 145 | 146 | $notification = (new RawNotification($this->config, $this->context)) 147 | ->make($callable($this->config, $this->context)); 148 | 149 | return $this->client->notification($notification); 150 | } 151 | 152 | /** 153 | * {@inheritdoc} 154 | */ 155 | public function checkin(string $idOrSlug): void 156 | { 157 | if ($this->isCheckInSlug($idOrSlug)) { 158 | $this->client->checkInWithSlug($this->config->get('api_key'), $idOrSlug); 159 | return; 160 | } 161 | 162 | $this->client->checkIn($idOrSlug); 163 | } 164 | 165 | private function isCheckInSlug(string $idOrSlug): bool 166 | { 167 | $checkIns = $this->config->get('checkins') ?? []; 168 | if (count($checkIns) > 0) { 169 | $filtered = array_filter($checkIns, function ($checkIn) use ($idOrSlug) { 170 | return $checkIn->slug === $idOrSlug; 171 | }); 172 | return count($filtered) > 0; 173 | } 174 | 175 | return false; 176 | } 177 | 178 | /** 179 | * @throws ServiceException 180 | */ 181 | private function getCheckInByName(string $projectId, string $name): ?CheckIn { 182 | $checkIns = $this->checkInsClient->listForProject($projectId) ?? []; 183 | $filtered = array_filter($checkIns, function ($checkIn) use ($name) { 184 | return $checkIn->name === $name; 185 | }); 186 | if (count($filtered) > 0) { 187 | return array_values($filtered)[0]; 188 | } 189 | 190 | return null; 191 | } 192 | 193 | /** 194 | * {@inheritdoc} 195 | */ 196 | public function context($key, $value = null) 197 | { 198 | if (is_array($key)) { 199 | foreach ($key as $contextKey => $contextValue) { 200 | $this->context->set($contextKey, $contextValue); 201 | } 202 | } else { 203 | $this->context->set($key, $value); 204 | } 205 | 206 | return $this; 207 | } 208 | 209 | /** 210 | * @return void 211 | */ 212 | public function resetContext(): void 213 | { 214 | $this->context = new Repository; 215 | } 216 | 217 | public function addBreadcrumb(string $message, array $metadata = [], string $category = 'custom'): Reporter 218 | { 219 | if ($this->config['breadcrumbs']['enabled'] && !empty($message)) { 220 | $this->breadcrumbs->add([ 221 | 'message' => $message, 222 | 'metadata' => $metadata, 223 | 'category' => $category, 224 | ]); 225 | } 226 | 227 | return $this; 228 | } 229 | 230 | public function clear(): Reporter 231 | { 232 | $this->context = new Repository; 233 | $this->breadcrumbs->clear(); 234 | 235 | return $this; 236 | } 237 | 238 | /** 239 | * {@inheritdoc} 240 | */ 241 | public function event($eventTypeOrPayload, ?array $payload = null): void 242 | { 243 | if (empty($this->config['api_key']) || ! $this->config['events']['enabled']) { 244 | return; 245 | } 246 | 247 | if (is_array($eventTypeOrPayload)) { 248 | $payload = $eventTypeOrPayload; 249 | } else { 250 | $payload = $payload ?? []; 251 | $payload['event_type'] = $eventTypeOrPayload; 252 | } 253 | 254 | if (empty($payload)) { 255 | return; 256 | } 257 | 258 | $event = array_merge( 259 | ['ts' => (new DateTime())->format(DATE_RFC3339_EXTENDED)], 260 | $payload 261 | ); 262 | 263 | // if 'ts' is set, we need to make sure it's a string in the correct format 264 | if ($event['ts'] instanceof DateTime) { 265 | $event['ts'] = $event['ts']->format(DATE_RFC3339_EXTENDED); 266 | } 267 | 268 | if (!$this->beforeEventHandlers->handle($event)) { 269 | return; 270 | } 271 | 272 | // Apply sampling after before_event callbacks 273 | if (!$this->shouldSampleEvent($event)) { 274 | return; 275 | } 276 | 277 | // Remove internal metadata before sending 278 | if (isset($event['_hb'])) { 279 | unset($event['_hb']); 280 | } 281 | 282 | $this->events->addEvent($event); 283 | } 284 | 285 | /** 286 | * {@inheritdoc} 287 | */ 288 | public function flushEvents(): void 289 | { 290 | if (!$this->config['events']['enabled']) { 291 | return; 292 | } 293 | 294 | $this->events->flushEvents(); 295 | } 296 | 297 | /** 298 | * {@inheritDoc} 299 | */ 300 | public function beforeNotify(callable $callback): void 301 | { 302 | $this->beforeNotifyHandlers->register($callback); 303 | } 304 | 305 | /** 306 | * {@inheritDoc} 307 | */ 308 | public function beforeEvent(callable $callback): void 309 | { 310 | $this->beforeEventHandlers->register($callback); 311 | } 312 | 313 | /** 314 | * @return Repository 315 | */ 316 | public function getContext(): Repository 317 | { 318 | return $this->context; 319 | } 320 | 321 | /** 322 | * @return void 323 | */ 324 | private function setHandlers(): void 325 | { 326 | $this->beforeNotifyHandlers = new BeforeNotifyHandler(); 327 | $this->beforeEventHandlers = new BeforeEventHandler(); 328 | 329 | if ($this->config['handlers']['exception']) { 330 | (new ExceptionHandler($this))->register(); 331 | } 332 | 333 | if ($this->config['handlers']['error']) { 334 | (new ErrorHandler($this))->register(); 335 | } 336 | 337 | if ($this->config['handlers']['shutdown']) { 338 | (new ShutdownHandler($this))->register(); 339 | } 340 | } 341 | 342 | /** 343 | * @param \Throwable $throwable 344 | * @return bool 345 | */ 346 | protected function excludedException(Throwable $throwable): bool 347 | { 348 | return $throwable instanceof ServiceException 349 | || in_array( 350 | get_class($throwable), 351 | $this->config['excluded_exceptions'] 352 | ); 353 | } 354 | 355 | /** 356 | * @param \Throwable $throwable 357 | * @return bool 358 | */ 359 | protected function shouldReport(Throwable $throwable): bool 360 | { 361 | if ($throwable instanceof ErrorException 362 | && in_array($throwable->getSeverity(), [E_DEPRECATED, E_USER_DEPRECATED]) 363 | && $this->config['capture_deprecations'] == false) { 364 | return false; 365 | } 366 | 367 | return ! $this->excludedException($throwable) 368 | && ! empty($this->config['api_key']) 369 | && $this->config['report_data']; 370 | } 371 | 372 | /** 373 | * @param string $component 374 | * @return self 375 | */ 376 | public function setComponent(string $component): self 377 | { 378 | $this->context('honeybadger_component', $component); 379 | 380 | return $this; 381 | } 382 | 383 | /** 384 | * @param string $action 385 | * @return self 386 | */ 387 | public function setAction(string $action): self 388 | { 389 | $this->context('honeybadger_action', $action); 390 | 391 | return $this; 392 | } 393 | 394 | /** 395 | * Determines if an event should be sampled based on sampling rate configuration 396 | * and any override in the event payload. 397 | * 398 | * @param array $event The event payload 399 | * @return bool Whether the event should be sent 400 | */ 401 | protected function shouldSampleEvent(array $event): bool 402 | { 403 | // Get the configured sampling rate (0-100) 404 | $samplingRate = $this->config['events']['sample_rate'] ?? 100; 405 | 406 | // Check for override in event payload 407 | if (isset($event['_hb']['sample_rate']) && is_numeric($event['_hb']['sample_rate'])) { 408 | $samplingRate = (int) $event['_hb']['sample_rate']; 409 | } 410 | 411 | // If sampling rate is 0, don't send any events 412 | if ($samplingRate <= 0) { 413 | return false; 414 | } 415 | 416 | // If sampling rate is 100 or greater, send all events 417 | if ($samplingRate >= 100) { 418 | return true; 419 | } 420 | 421 | // If requestId is present, use it for consistent sampling 422 | if (isset($event['requestId'])) { 423 | // Use CRC32 of requestId for consistent sampling, 424 | // and use sprintf to convert to unsigned integer 425 | $crc = sprintf('%u', crc32((string) $event['requestId'])); 426 | return $crc % 100 < $samplingRate; 427 | } 428 | 429 | // Otherwise, use random sampling 430 | return mt_rand(0, 99) < $samplingRate; 431 | } 432 | } 433 | -------------------------------------------------------------------------------- /src/HoneybadgerClient.php: -------------------------------------------------------------------------------- 1 | client->post( 23 | 'v1/notices', 24 | ['body' => json_encode($notification, JSON_PARTIAL_OUTPUT_ON_ERROR)] 25 | ); 26 | } catch (Throwable $e) { 27 | $this->handleServiceException(ServiceException::generic($e)); 28 | 29 | return []; 30 | } 31 | 32 | if ($response->getStatusCode() !== Response::HTTP_CREATED) { 33 | $this->handleServiceException((new ServiceExceptionFactory($response))->make()); 34 | 35 | return []; 36 | } 37 | 38 | return (string) $response->getBody() 39 | ? json_decode($response->getBody(), true) 40 | : []; 41 | } 42 | 43 | /** 44 | * @param string $checkInId 45 | * @return void 46 | */ 47 | public function checkIn(string $checkInId): void 48 | { 49 | try { 50 | $response = $this->client->head(sprintf('v1/check_in/%s', $checkInId)); 51 | 52 | if ($response->getStatusCode() !== Response::HTTP_OK) { 53 | $this->handleServiceException((new ServiceExceptionFactory($response))->make()); 54 | } 55 | } catch (Throwable $e) { 56 | $this->handleServiceException(ServiceException::generic($e)); 57 | } 58 | } 59 | 60 | public function checkInWithSlug(string $apiKey, string $checkInSlug): void 61 | { 62 | try { 63 | $response = $this->client->head(sprintf('v1/check_in/%s/%s', $apiKey, $checkInSlug)); 64 | 65 | if ($response->getStatusCode() !== Response::HTTP_OK) { 66 | $this->handleServiceException((new ServiceExceptionFactory($response))->make()); 67 | } 68 | } catch (Throwable $e) { 69 | $this->handleServiceException(ServiceException::generic($e)); 70 | } 71 | } 72 | 73 | /** 74 | * @param array $events 75 | * @return void 76 | */ 77 | public function events(array $events): void 78 | { 79 | try { 80 | $ndjson = implode("\n", array_map('json_encode', $events)); 81 | $response = $this->client->post( 82 | 'v1/events', 83 | ['body' => $ndjson] 84 | ); 85 | } catch (Throwable $e) { 86 | $this->handleEventsException(ServiceException::generic($e)); 87 | 88 | return; 89 | } 90 | 91 | if ($response->getStatusCode() !== Response::HTTP_CREATED) { 92 | $this->handleEventsException((new ServiceExceptionFactory($response))->make(true)); 93 | } 94 | } 95 | 96 | public function makeClient(): Client 97 | { 98 | 99 | return new Client([ 100 | 'base_uri' => $this->config['endpoint'], 101 | RequestOptions::HTTP_ERRORS => false, 102 | RequestOptions::HEADERS => [ 103 | 'X-API-Key' => $this->config['api_key'], 104 | 'Accept' => 'application/json', 105 | 'Content-Type' => 'application/json', 106 | 'User-Agent' => $this->getUserAgent(), 107 | ], 108 | RequestOptions::TIMEOUT => $this->config['client']['timeout'], 109 | RequestOptions::PROXY => $this->config['client']['proxy'], 110 | RequestOptions::VERIFY => $this->config['client']['verify'] ?? true, 111 | ]); 112 | } 113 | 114 | 115 | } 116 | -------------------------------------------------------------------------------- /src/LogEventHandler.php: -------------------------------------------------------------------------------- 1 | honeybadger = $honeybadger; 27 | } 28 | 29 | /** 30 | * @param array|LogRecord $record 31 | */ 32 | protected function write($record): void 33 | { 34 | if (!$this->isHandling($record)) { 35 | return; 36 | } 37 | 38 | $eventPayload = $this->getEventPayloadFromMonologRecord($record); 39 | $this->honeybadger->event('log', $eventPayload); 40 | } 41 | 42 | /** 43 | * @param array|LogRecord $record 44 | * @return array 45 | */ 46 | protected function getEventPayloadFromMonologRecord($record): array { 47 | $payload = [ 48 | 'ts' => $record['datetime']->format(DATE_RFC3339_EXTENDED), 49 | 'severity' => strtolower($record['level_name']), 50 | 'message' => $record['message'], 51 | 'channel' => $record['channel'], 52 | ]; 53 | 54 | if (isset($record['context']) && $record['context'] != null) { 55 | $payload = array_merge($payload, $record['context']); 56 | } 57 | 58 | return $payload; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/LogHandler.php: -------------------------------------------------------------------------------- 1 | honeybadger = $honeybadger; 28 | } 29 | 30 | /** 31 | * @param array|\Monolog\LogRecord $record 32 | */ 33 | protected function write($record): void 34 | { 35 | if (!$this->isHandling($record)) { 36 | return; 37 | } 38 | 39 | $this->honeybadger->rawNotification(function ($config) use ($record) { 40 | return [ 41 | 'notifier' => array_merge($config['notifier'], ['name' => 'Honeybadger Log Handler']), 42 | 'error' => $this->getHoneybadgerErrorFromMonologRecord($record, $config), 43 | 'request' => [ 44 | 'context' => $this->getHoneybadgerContextFromMonologRecord($record), 45 | ], 46 | 'server' => [ 47 | 'environment_name' => $config['environment_name'], 48 | 'time' => $record['datetime']->format("Y-m-d\TH:i:sP"), 49 | ], 50 | ]; 51 | }); 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | public function getFormatter(): FormatterInterface 58 | { 59 | return new LineFormatter('[%datetime%] %channel%.%level_name%: %message%'); 60 | } 61 | 62 | /** 63 | * @param array|\Monolog\LogRecord $record 64 | */ 65 | protected function getHoneybadgerErrorFromMonologRecord($record, $config): array 66 | { 67 | $error = [ 68 | 'tags' => [ 69 | 'log', 70 | sprintf('%s.%s', $record['channel'], $record['level_name']), 71 | ], 72 | 'fingerprint' => md5($record['level_name'].$record['message']), 73 | ]; 74 | $e = $record['context']['exception'] ?? null; 75 | if ($e instanceof \Throwable) { 76 | $error['class'] = get_class($e); 77 | $error['message'] = $e->getMessage(); 78 | $error['backtrace'] = (new BacktraceFactory($e, $config))->trace(); 79 | } else { 80 | $error['class'] = "{$record['level_name']} Log"; 81 | $error['message'] = $record['message']; 82 | } 83 | 84 | return $error; 85 | } 86 | 87 | /** 88 | * @param array|\Monolog\LogRecord $record 89 | */ 90 | protected function getHoneybadgerContextFromMonologRecord($record): array 91 | { 92 | $context = $record['context']; 93 | $context['level_name'] = $record['level_name']; 94 | $context['log_channel'] = $record['channel']; 95 | 96 | $e = $context['exception'] ?? null; 97 | if ($e && $e instanceof \Throwable) { 98 | // Format Exception objects properly 99 | $context['exception'] = [ 100 | 'message' => $e->getMessage(), 101 | 'code' => $e->getCode(), 102 | 'file' => $e->getFile(), 103 | 'line' => $e->getLine(), 104 | ]; 105 | } 106 | 107 | return $context; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/RawNotification.php: -------------------------------------------------------------------------------- 1 | config = $config; 27 | $this->context = $context; 28 | } 29 | 30 | /** 31 | * @param array $payload 32 | * @return array 33 | * 34 | * @throws \InvalidArgumentException 35 | */ 36 | public function make(array $payload): array 37 | { 38 | $payload = array_merge( 39 | [], 40 | ['notifier' => $this->config['notifier']], 41 | ['error' => []], 42 | ['request' => ['context' => (object) $this->context->all()], 43 | ], 44 | ['server' => (object) [ 45 | 'environment_name' => $this->config['environment_name'], 46 | ]], 47 | $payload 48 | ); 49 | 50 | $this->validatePayload($payload); 51 | 52 | return $payload; 53 | } 54 | 55 | /** 56 | * @param array $payload 57 | * @return void 58 | * 59 | * @throws \InvalidArgumentException 60 | */ 61 | private function validatePayload(array $payload): void 62 | { 63 | if (empty($payload['error']['class'])) { 64 | throw new InvalidArgumentException('The notification error.class field is required'); 65 | } 66 | 67 | if (empty($payload['notifier']['name'])) { 68 | throw new InvalidArgumentException('The notification notifier.name field is required'); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Request.php: -------------------------------------------------------------------------------- 1 | request = $request ?? FoundationRequest::createFromGlobals(); 24 | 25 | $this->keysToFilter = [ 26 | 'password', 27 | 'password_confirmation', 28 | ]; 29 | } 30 | 31 | /** 32 | * @return string 33 | */ 34 | public function url(): string 35 | { 36 | $url = $this->httpRequest() 37 | ? $this->request->getUri() 38 | : ''; 39 | 40 | if (! $url) { 41 | return $url; 42 | } 43 | 44 | // Manually filter out sensitive data from URL query string 45 | $queryString = parse_url($url, PHP_URL_QUERY) ?? ''; 46 | $filteredQueryParams = array_map(function ($keyAndValue) { 47 | $parts = explode('=', $keyAndValue); 48 | if (isset($parts[1]) && $parts[1] !== '' 49 | && in_array($parts[0], $this->keysToFilter)) { 50 | return "{$parts[0]}=[FILTERED]"; 51 | } 52 | 53 | return $keyAndValue; 54 | }, explode('&', $queryString)); 55 | 56 | return $queryString 57 | ? str_replace($queryString, implode('&', $filteredQueryParams), $url) 58 | : $url; 59 | } 60 | 61 | /** 62 | * @return array 63 | */ 64 | public function params(): array 65 | { 66 | if (! $this->httpRequest()) { 67 | return []; 68 | } 69 | 70 | return [ 71 | 'method' => $this->request->getMethod(), 72 | 'query' => $this->filter($this->request->query->all()), 73 | 'data' => $this->filter($this->data()), 74 | ]; 75 | } 76 | 77 | /** 78 | * @return array 79 | */ 80 | public function session(): array 81 | { 82 | return $this->request->hasSession() && $this->request->getSession() 83 | ? $this->filter($this->request->getSession()->all()) 84 | : []; 85 | } 86 | 87 | /** 88 | * @return bool 89 | */ 90 | private function httpRequest(): bool 91 | { 92 | return isset($_SERVER['REQUEST_METHOD']); 93 | } 94 | 95 | private function getRequestContentType(): ?string 96 | { 97 | if (method_exists($this->request, 'getContentType')) { 98 | return $this->request->getContentType(); 99 | } 100 | 101 | return $this->request->getContentTypeFormat(); 102 | } 103 | 104 | /** 105 | * @return array 106 | */ 107 | private function data(): array 108 | { 109 | $contentType = $this->getRequestContentType(); 110 | if ($contentType === 'json') { 111 | return json_decode($this->request->getContent(), true) ?: []; 112 | } 113 | 114 | if ($contentType === 'form') { 115 | return $this->request->request->all(); 116 | } 117 | 118 | return []; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Support/Arr.php: -------------------------------------------------------------------------------- 1 | offsetExists($key); 62 | } 63 | 64 | return array_key_exists($key, $array); 65 | } 66 | 67 | /** 68 | * @param array $array 69 | * @param callable $callback 70 | * @return array 71 | */ 72 | public static function mapWithKeys(array $array, callable $callback): array 73 | { 74 | $newArray = []; 75 | 76 | foreach ($array as $key => $item) { 77 | $newArray[$key] = $callback($item, $key); 78 | } 79 | 80 | return $newArray; 81 | } 82 | 83 | /** 84 | * If the given value is not an array and not null, wrap it in one. 85 | * 86 | * @param mixed $value 87 | * @return array 88 | */ 89 | public static function wrap($value) 90 | { 91 | if (is_null($value)) { 92 | return []; 93 | } 94 | 95 | return is_array($value) ? $value : [$value]; 96 | } 97 | 98 | /** 99 | * If the array is associative. 100 | * 101 | * @param array $data 102 | * @return bool 103 | */ 104 | public static function isAssociative(array $data): bool 105 | { 106 | return array_keys($data) !== range(0, count($data) - 1); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Support/EvictingQueue.php: -------------------------------------------------------------------------------- 1 | capacity = $capacity; 23 | $this->items = $items; 24 | } 25 | 26 | /** 27 | * @param mixed $item 28 | * 29 | * @return self 30 | */ 31 | public function add($item) 32 | { 33 | $this->items[] = $item; 34 | 35 | if (count($this->items) > $this->capacity) { 36 | array_shift($this->items); 37 | } 38 | 39 | return $this; 40 | } 41 | 42 | public function toArray(): array 43 | { 44 | return $this->items; 45 | } 46 | 47 | /** 48 | * Clear the queue. 49 | */ 50 | public function clear(): self 51 | { 52 | $this->items = []; 53 | 54 | return $this; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Support/Repository.php: -------------------------------------------------------------------------------- 1 | items = $items; 15 | } 16 | 17 | /** 18 | * @param string $key 19 | * @param mixed $value 20 | * @return array 21 | */ 22 | public function set(string $key, $value): array 23 | { 24 | $this->items[$key] = $value; 25 | 26 | return $this->items; 27 | } 28 | 29 | /** 30 | * @param string $key 31 | * @return mixed 32 | */ 33 | public function get(string $key) 34 | { 35 | return $this->items[$key] ?? null; 36 | } 37 | 38 | /** 39 | * @param string $key 40 | * @param mixed $value 41 | * @return void 42 | */ 43 | public function __set(string $key, $value): void 44 | { 45 | $this->set($key, $value); 46 | } 47 | 48 | /** 49 | * @return array 50 | */ 51 | public function all(): array 52 | { 53 | return $this->items; 54 | } 55 | 56 | /** 57 | * @param string|int $offset 58 | * @return bool 59 | */ 60 | public function offsetExists($offset): bool 61 | { 62 | return isset($this->items[$offset]); 63 | } 64 | 65 | /** 66 | * @param int|string $offset 67 | * @return mixed 68 | */ 69 | #[\ReturnTypeWillChange] 70 | public function offsetGet($offset) 71 | { 72 | return $this->items[$offset]; 73 | } 74 | 75 | /** 76 | * @param int|string $offset 77 | * @param mixed $value 78 | * @return void 79 | */ 80 | public function offsetSet($offset, $value): void 81 | { 82 | $this->items[$offset] = $value; 83 | } 84 | 85 | /** 86 | * @param int|string $offset 87 | * @return void 88 | */ 89 | public function offsetUnset($offset): void 90 | { 91 | unset($this->items[$offset]); 92 | } 93 | 94 | /** 95 | * Return all values except those specified. 96 | * 97 | * @param string|array $keys 98 | * @return array 99 | */ 100 | public function except($keys): array 101 | { 102 | $items = $this->items; 103 | 104 | if (is_array($keys)) { 105 | foreach ($keys as $key) { 106 | unset($items[$key]); 107 | } 108 | 109 | return $items; 110 | } 111 | 112 | unset($items[$keys]); 113 | 114 | return $items; 115 | } 116 | } 117 | --------------------------------------------------------------------------------