├── .gitignore ├── view ├── adminhtml │ ├── requirejs-config.js │ ├── templates │ │ └── system │ │ │ └── config │ │ │ ├── button.phtml │ │ │ └── deployment-config-info.phtml │ └── web │ │ └── js │ │ └── testsentry.js └── frontend │ ├── layout │ └── default.xml │ └── templates │ └── script │ ├── logrocket_init.phtml │ ├── sentry.phtml │ └── sentry_init.phtml ├── .github ├── SECURITY.md ├── PULL_REQUEST_TEMPLATE.md ├── workflows │ ├── analyse.yml │ └── phpcs.yml ├── issue_template.md ├── ISSUE_TEMPLATE │ ├── feature.yml │ └── bug.yml └── CONTRIBUTING.md ├── registration.php ├── etc ├── module.xml ├── adminhtml │ ├── routes.xml │ └── system.xml ├── events.xml ├── frontend │ └── di.xml ├── acl.xml ├── config.xml └── di.xml ├── phpstan.neon ├── i18n └── en_US.csv ├── Model ├── Config │ └── Source │ │ ├── ScriptTagPlacement.php │ │ └── LogLevel.php ├── ReleaseIdentifier.php ├── PerformanceTracingDto.php ├── Collector │ └── SentryRelatedCspCollector.php ├── SentryCron.php ├── SentryLog.php ├── SentryInteraction.php └── SentryPerformance.php ├── Plugin ├── SampleRequest.php ├── CronScheduleCheckIn.php ├── LogrocketCustomerInfo.php ├── MonologPlugin.php ├── Profiling │ ├── TemplatePlugin.php │ ├── DbQueryLoggerPlugin.php │ ├── EventManagerPlugin.php │ ├── ExchangePlugin.php │ ├── Cache.php │ └── QueuePlugin.php ├── CspModeConfigManagerPlugin.php └── GlobalExceptionCatcher.php ├── Cache └── Frontend │ └── Factory.php ├── LICENSE ├── Block ├── Adminhtml │ └── System │ │ └── Config │ │ ├── Button.php │ │ └── DeploymentConfigInfo.php └── SentryScript.php ├── Observer └── StripUnnecessaryFrames.php ├── composer.json ├── Controller └── Adminhtml │ └── Test │ └── Sentry.php ├── Logger └── Handler │ └── Sentry.php ├── Helper ├── Version.php └── Data.php ├── Test └── Unit │ └── Plugin │ └── GlobalExceptionCatcherTest.php ├── README.md └── CHANGELOG.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | composer.lock 4 | /vendor 5 | vendor 6 | -------------------------------------------------------------------------------- /view/adminhtml/requirejs-config.js: -------------------------------------------------------------------------------- 1 | var config = { 2 | paths: { 3 | 'justbetter/testsentry': 'JustBetter_Sentry/js/testsentry' 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | If you discover any security related issues, please email security@justbetter.nl instead of using the issue tracker. -------------------------------------------------------------------------------- /registration.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /etc/adminhtml/routes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /etc/events.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - vendor/bitexpert/phpstan-magento/extension.neon 3 | parameters: 4 | paths: 5 | - . 6 | excludePaths: 7 | - vendor 8 | - Test/* 9 | - Logger/Handler/Sentry.php 10 | level: 6 11 | ignoreErrors: 12 | - 13 | identifier: missingType.iterableValue 14 | treatPhpDocTypesAsCertain: false 15 | -------------------------------------------------------------------------------- /etc/frontend/di.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /i18n/en_US.csv: -------------------------------------------------------------------------------- 1 | How to configure Sentry?,How to configure Sentry? 2 | "This module uses the Magento Deployment Configuration for most it's configuration. This means that you need to add this array to your `app/etc/env.php`:","This module uses the Magento Deployment Configuration for most it's configuration. This means that you need to add this array to your `app/etc/env.php`:" 3 | -------------------------------------------------------------------------------- /Model/Config/Source/ScriptTagPlacement.php: -------------------------------------------------------------------------------- 1 | 'head.additional', 'label' => __('head.additional')], 18 | ['value' => 'before.body.end', 'label' => __('before.body.end')], 19 | ]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Model/ReleaseIdentifier.php: -------------------------------------------------------------------------------- 1 | version->getValue(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | **Summary** 7 | 8 | 13 | 14 | **Result** 15 | 16 | 21 | 22 | **Checklist** 23 | 24 | - [ ] I've ran `composer run codestyle` 25 | - [ ] I've ran `composer run analyse` -------------------------------------------------------------------------------- /etc/acl.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /view/frontend/layout/default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Model/Config/Source/LogLevel.php: -------------------------------------------------------------------------------- 1 | Logger::NOTICE, 'label' => __('Notice')], 19 | ['value' => Logger::WARNING, 'label' => __('Warning')], 20 | ['value' => Logger::CRITICAL, 'label' => __('Critical')], 21 | ['value' => Logger::ALERT, 'label' => __('Alert')], 22 | ['value' => Logger::ALERT, 'label' => __('Emergency')], 23 | ]; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /view/adminhtml/templates/system/config/button.phtml: -------------------------------------------------------------------------------- 1 | 7 |
8 | 13 |
14 | 24 | -------------------------------------------------------------------------------- /Plugin/SampleRequest.php: -------------------------------------------------------------------------------- 1 | sentryPerformance = $sentryPerformance; 22 | } 23 | 24 | /** 25 | * Add our toolbar to the response. 26 | * 27 | * @param ResponseInterface $response 28 | */ 29 | public function beforeSendResponse(ResponseInterface $response): void 30 | { 31 | $this->sentryPerformance->finishTransaction($response); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/analyse.yml: -------------------------------------------------------------------------------- 1 | name: PHPStan 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: true 14 | matrix: 15 | php: [8.2, 8.3] 16 | stability: [prefer-lowest, prefer-stable] 17 | 18 | name: PHPStan - P${{ matrix.php }} - ${{ matrix.stability }} 19 | 20 | steps: 21 | - name: Checkout code 22 | uses: actions/checkout@v2 23 | 24 | - name: Setup PHP 25 | uses: shivammathur/setup-php@v2 26 | with: 27 | php-version: ${{ matrix.php }} 28 | extensions: dom, curl, libxml, mbstring, zip, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo 29 | coverage: none 30 | 31 | - name: Install dependencies 32 | run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction 33 | 34 | - name: Analyse 35 | run: composer run analyse 36 | -------------------------------------------------------------------------------- /.github/workflows/phpcs.yml: -------------------------------------------------------------------------------- 1 | name: Code Style 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: true 14 | matrix: 15 | php: [8.2, 8.3] 16 | stability: [prefer-lowest, prefer-stable] 17 | 18 | name: PHPCS - P${{ matrix.php }} - ${{ matrix.stability }} 19 | 20 | steps: 21 | - name: Checkout code 22 | uses: actions/checkout@v2 23 | 24 | - name: Setup PHP 25 | uses: shivammathur/setup-php@v2 26 | with: 27 | php-version: ${{ matrix.php }} 28 | extensions: dom, curl, libxml, mbstring, zip, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo 29 | coverage: none 30 | 31 | - name: Install dependencies 32 | run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction 33 | 34 | - name: Analyse 35 | run: composer run phpcs 36 | -------------------------------------------------------------------------------- /view/adminhtml/web/js/testsentry.js: -------------------------------------------------------------------------------- 1 | define([ 2 | "jquery", 3 | "jquery/ui", 4 | "mage/translate", 5 | "Magento_Ui/js/modal/alert" 6 | ], function ($, validation, $t, alert) { 7 | "use strict"; 8 | 9 | $.widget('justbetter.testSentry', { 10 | options: { 11 | ajaxUrl: '', 12 | testSentry: '#sentry_general_sent', 13 | domainSentry: '#sentry_general_domain' 14 | }, 15 | _create: function () { 16 | var self = this; 17 | self.element.addClass('required-entry'); 18 | 19 | self.element.click(function (e) { 20 | e.preventDefault(); 21 | self._ajaxSubmit(); 22 | }); 23 | }, 24 | 25 | _ajaxSubmit: function () { 26 | $.get({ 27 | url: this.options.ajaxUrl, 28 | dataType: 'json', 29 | showLoader: true, 30 | success: function (result) { 31 | alert({ 32 | title: result.status ? $t('Success') : $t('Error'), 33 | content: result.content ? result.content : result.message 34 | }); 35 | } 36 | }); 37 | } 38 | }); 39 | 40 | return $.justbetter.testSentry; 41 | }); 42 | -------------------------------------------------------------------------------- /Plugin/CronScheduleCheckIn.php: -------------------------------------------------------------------------------- 1 | sentryCron->sendScheduleStatus($subject); 38 | 39 | return $result; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Cache/Frontend/Factory.php: -------------------------------------------------------------------------------- 1 | \JustBetter\Sentry\Plugin\Profiling\Cache::class, 27 | ]; 28 | 29 | parent::__construct($objectManager, $filesystem, $resource, $enforcedOptions, $decorators); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 JustBetter 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Model/PerformanceTracingDto.php: -------------------------------------------------------------------------------- 1 | scope; 34 | } 35 | 36 | /** 37 | * Get parent span. 38 | * 39 | * @return Span|null 40 | */ 41 | public function getParentSpan(): ?Span 42 | { 43 | return $this->parentSpan; 44 | } 45 | 46 | /** 47 | * Get span. 48 | * 49 | * @return Span|null 50 | */ 51 | public function getSpan(): ?Span 52 | { 53 | return $this->span; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /view/adminhtml/templates/system/config/deployment-config-info.phtml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 |

escapeHtml(__('How to configure Sentry?')); ?>

10 |

escapeHtml(__('This module uses the')); ?> 11 | 12 | escapeHtml(__('Magento Deployment Configuration')); ?> 13 | 14 | escapeHtml(__('for most it\'s configuration. This means that you need to add this array to your `app/etc/env.php`:')); ?> 15 |

16 | 17 |
18 |  [
20 |     'dsn' => 'example.com',
21 |     'logrocket_key' => 'example/example',
22 |     'environment' => null,
23 |     'log_level' => \Monolog\Level::Warning,
24 |     'error_types' => E_ALL,
25 |     'ignore_exceptions' => [],
26 |     'mage_mode_development' => false,
27 | ]"); ?>
28 |     
29 | 30 |

escapeHtml(__("Release ID: %1", $block->getVersion())); ?>

31 | 32 | -------------------------------------------------------------------------------- /Plugin/LogrocketCustomerInfo.php: -------------------------------------------------------------------------------- 1 | customerSession->isLoggedIn()) { 34 | return $result; 35 | } 36 | 37 | $customer = $this->currentCustomer->getCustomer(); 38 | 39 | $result['email'] = $customer->getEmail(); 40 | $result['fullname'] = $customer->getFirstname().' '.$customer->getLastname(); 41 | 42 | return $result; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /etc/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 1 7 | 0 8 | 0 9 | before.body.end 10 | 0 11 | 0 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /Block/Adminhtml/System/Config/Button.php: -------------------------------------------------------------------------------- 1 | unsScope(); 25 | 26 | return parent::render($element); 27 | } 28 | 29 | /** 30 | * Get the button and scripts contents. 31 | * 32 | * @param \Magento\Framework\Data\Form\Element\AbstractElement $element 33 | * 34 | * @return string 35 | */ 36 | protected function _getElementHtml(AbstractElement $element) 37 | { 38 | $originalData = $element->getOriginalData(); 39 | $this->addData( 40 | [ 41 | 'button_label' => $originalData['button_label'], 42 | 'button_url' => $this->getUrl($originalData['button_url']), 43 | 'html_id' => $element->getHtmlId(), 44 | ] 45 | ); 46 | 47 | return $this->_toHtml(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Block/Adminhtml/System/Config/DeploymentConfigInfo.php: -------------------------------------------------------------------------------- 1 | _toHtml(); 42 | } 43 | 44 | /** 45 | * Get static version. 46 | * 47 | * @return string 48 | */ 49 | public function getVersion() 50 | { 51 | return $this->version->getValue(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Plugin/MonologPlugin.php: -------------------------------------------------------------------------------- 1 | containsHandler($handlers)) { 31 | array_unshift($handlers, $this->sentryHandler); 32 | } 33 | 34 | return [$handlers]; 35 | } 36 | 37 | /** 38 | * Check if the Sentry handler is already in the list of handlers. 39 | * 40 | * @param array $handlers 41 | * 42 | * @return bool 43 | */ 44 | public function containsHandler(array $handlers): bool 45 | { 46 | foreach ($handlers as $handler) { 47 | if ($handler instanceof Sentry) { 48 | return true; 49 | } 50 | } 51 | 52 | return false; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | 13 | 14 | **Do you want to request a *feature* or report a *bug*?** 15 | 16 | 24 | 25 | **Bug: What is the current behavior?** 26 | 27 | **Bug: What is the expected behavior?** 28 | 29 | **Bug: What is the proposed solution?** 30 | 31 | **Feature: What is your use case for such a feature?** 32 | 33 | **Feature: What is your proposed configuration entry? The new option to add? What is the behavior?** 34 | 35 | **What is the version of Magento and of Sentry extension you are using? Always use the latest version of the extension one before opening a bug issue.** 36 | 37 | 38 | -------------------------------------------------------------------------------- /Plugin/Profiling/TemplatePlugin.php: -------------------------------------------------------------------------------- 1 | getModuleName())) { 26 | $tags['magento.module'] = $subject->getModuleName(); 27 | } 28 | 29 | $context = SpanContext::make() 30 | ->setOp('template.render') 31 | ->setDescription($subject->getNameInLayout() ?: $fileName) 32 | ->setTags($tags) 33 | ->setData([ 34 | 'block_name' => $subject->getNameInLayout(), 35 | 'block_class' => get_class($subject), 36 | 'module' => $subject->getModuleName(), 37 | 'template' => $fileName, 38 | ]) 39 | ->setOrigin('auto.template'); 40 | 41 | $tracingDto = SentryPerformance::traceStart($context); 42 | 43 | try { 44 | return $callable($fileName); 45 | } finally { 46 | SentryPerformance::traceEnd($tracingDto); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Plugin/Profiling/DbQueryLoggerPlugin.php: -------------------------------------------------------------------------------- 1 | tracingDto = SentryPerformance::traceStart( 29 | SpanContext::make() 30 | ->setOp('db.sql.query') 31 | ->setStartTimestamp(microtime(true)) 32 | ->setOrigin('auto.db') 33 | ); 34 | } 35 | 36 | /** 37 | * Stops the previously create span (span created in `beforeStartTimer`). 38 | * 39 | * @param LoggerInterface $subject 40 | * @param string $type 41 | * @param string $sql 42 | * @param array $bind 43 | * @param mixed $result 44 | * 45 | * @return void 46 | */ 47 | public function beforeLogStats(LoggerInterface $subject, $type, $sql, $bind = [], $result = null): void 48 | { 49 | if ($this->tracingDto === null) { 50 | return; 51 | } 52 | 53 | $this->tracingDto->getSpan()?->setDescription($sql); 54 | SentryPerformance::traceEnd($this->tracingDto); 55 | $this->tracingDto = null; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Create a feature request. 3 | title: "[⭐]: " 4 | labels: ["enhancement"] 5 | type: feature 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Thanks for participating in this project! 11 | Please fill out the following sections to help us understand what you want. 12 | 13 | In any case, 14 | - make sure you are using the latest version of the extension; 15 | - do at least one search in current issues or feature requests, there might be a similar request already where you could help out; 16 | - do include as many details as possible; 17 | - type: textarea 18 | id: requested-feature 19 | attributes: 20 | label: What feature would you like to see added? 21 | validations: 22 | required: true 23 | - type: textarea 24 | id: current-behavior 25 | attributes: 26 | label: If it is related to current behavior, what is the current behavior? 27 | - type: textarea 28 | id: additional-information 29 | attributes: 30 | label: do you have any additional information for us? 31 | - type: checkboxes 32 | attributes: 33 | label: Is there an existing Feature Request/PR for this? 34 | description: Please search to see if a feature request or PR already exists for the feature you would like. 35 | options: 36 | - label: I have searched the existing issues and pull requests 37 | required: true 38 | - type: checkboxes 39 | attributes: 40 | label: Are you willing to create a pull request for this? 41 | description: Thank you so much for your willingness to help us out! We really appreciate it. 42 | options: 43 | - label: I would be willing to create a pull request for this feature 44 | -------------------------------------------------------------------------------- /view/frontend/templates/script/logrocket_init.phtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | useLogRocketIdentify()): ?> 5 | 6 | define('customerData', 7 | ['jquery', 'Magento_Customer/js/customer-data'], 8 | function ($, customerData) { 9 | 'use strict'; 10 | 11 | var getCustomerInfo = function () { 12 | var customer = customerData.get('customer'); 13 | 14 | return customer(); 15 | }; 16 | 17 | var isLoggedIn = function (customerInfo) { 18 | return customerInfo && customerInfo.firstname; 19 | }; 20 | 21 | return function () { 22 | var deferred = $.Deferred(); 23 | var customerInfo = getCustomerInfo(); 24 | 25 | if (customerInfo && customerInfo.data_id) { 26 | deferred.resolve(isLoggedIn(customerInfo), customerInfo); 27 | } else { 28 | customerData.reload(['customer'], false) 29 | .done(function () { 30 | customerInfo = getCustomerInfo() 31 | deferred.resolve(isLoggedIn(customerInfo), customerInfo); 32 | }) 33 | .fail(function () { 34 | deferred.reject(); 35 | }); 36 | } 37 | 38 | return deferred; 39 | }; 40 | 41 | } 42 | ); 43 | 44 | require(["customerData"], function (customerData) { 45 | 46 | customerData().then(function (loggedIn, data) { 47 | if (!loggedIn) { 48 | return; 49 | } 50 | 51 | LogRocket.identify(data.websiteId, { 52 | name: data.fullname, 53 | email: data.email 54 | }); 55 | 56 | }); 57 | }); 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /Model/Collector/SentryRelatedCspCollector.php: -------------------------------------------------------------------------------- 1 | dataHelper->isActive()) { 27 | return $policies; 28 | } 29 | 30 | if ($this->dataHelper->useScriptTag()) { 31 | $policies[] = new FetchPolicy( 32 | 'script-src', 33 | false, 34 | ['https://browser.sentry-cdn.com'] 35 | ); 36 | 37 | $dsn = $this->dataHelper->getDsn(); 38 | $dsnHost = is_string($dsn) ? UriFactory::factory($dsn)->getHost() : null; 39 | if (!empty($dsnHost)) { 40 | $policies[] = new FetchPolicy( 41 | 'connect-src', 42 | false, 43 | [$dsnHost] 44 | ); 45 | } 46 | } 47 | 48 | if ($this->dataHelper->isSpotlightEnabled()) { 49 | $policies[] = new FetchPolicy( 50 | 'script-src', 51 | false, 52 | ['https://unpkg.com/@spotlightjs/'] 53 | ); 54 | } 55 | 56 | if ($this->dataHelper->useLogrocket()) { 57 | $policies[] = new FetchPolicy( 58 | 'script-src', 59 | false, 60 | ['https://cdn.lr-ingest.io'] 61 | ); 62 | } 63 | 64 | return $policies; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report. 3 | title: "[🐛]: " 4 | labels: ["bug"] 5 | type: bug 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Thanks for participating in this project! 11 | Please fill out the following sections to help us understand the issue you're experiencing. 12 | 13 | In any case, 14 | - make sure you are using the latest version of the extension; 15 | - do at least one search in current issues or questions, your question might already be answered; 16 | - do include as many details as possible - site URL with the issue, screenshots when it's a visual issue, console errors, ...; 17 | - type: textarea 18 | id: current-behavior 19 | attributes: 20 | label: What is the current behavior? 21 | validations: 22 | required: true 23 | - type: textarea 24 | id: expected-behavior 25 | attributes: 26 | label: What is the expected behavior? 27 | validations: 28 | required: true 29 | - type: textarea 30 | id: steps-to-reproduce 31 | attributes: 32 | label: How can we reproduce the issue? 33 | - type: textarea 34 | id: proposed-solution 35 | attributes: 36 | label: What is the proposed solution? 37 | - type: textarea 38 | id: additional-information 39 | attributes: 40 | label: do you have any additional information for us? 41 | description: Please include any additional information that may be helpful in resolving the issue. Like logs, screenshots, or any other relevant details. 42 | - type: checkboxes 43 | attributes: 44 | label: Is there an existing issue for this? 45 | description: Please search to see if an issue already exists for the bug you encountered. 46 | options: 47 | - label: I have searched the existing issues 48 | required: true 49 | - type: checkboxes 50 | attributes: 51 | label: Are you willing to create a pull request for this? 52 | description: Thank you so much for your willingness to help us out! We really appreciate it. 53 | options: 54 | - label: If i have a fix i would be willing to create a pull request -------------------------------------------------------------------------------- /Observer/StripUnnecessaryFrames.php: -------------------------------------------------------------------------------- 1 | sentryHelper->getCleanStacktrace()) { 31 | return; 32 | } 33 | 34 | /** @var Event $event */ 35 | $event = $observer->getEvent()->getSentryEvent()->getEvent(); 36 | 37 | foreach ($event->getExceptions() as $exception) { 38 | $stacktrace = $exception->getStacktrace(); 39 | if (!$stacktrace) { 40 | continue; 41 | } 42 | 43 | $stacktraceLength = count($stacktrace->getFrames()) - 1; 44 | // Get the last frame in the stacktrace and make sure it's never removed. 45 | $lastFrame = $stacktrace->getFrame($stacktraceLength--); 46 | for ($i = $stacktraceLength; $i >= 0; $i--) { 47 | $frame = $stacktrace->getFrame($i); 48 | $file = $frame->getFile(); 49 | if ($file === $lastFrame->getFile()) { 50 | // Anything in the file that threw the exception is relevant. 51 | continue; 52 | } 53 | 54 | if ($this->shouldSkipFrame($frame)) { 55 | $stacktrace->removeFrame($i); 56 | } 57 | } 58 | } 59 | } 60 | 61 | /** 62 | * Check if the frame should be skipped. 63 | * 64 | * @param Frame $frame 65 | * 66 | * @return bool 67 | */ 68 | public function shouldSkipFrame(Frame $frame): bool 69 | { 70 | $file = $frame->getFile(); 71 | 72 | return str_ends_with($file, 'Interceptor.php') 73 | || str_ends_with($file, 'Proxy.php') 74 | || str_ends_with($file, 'Factory.php'); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /view/frontend/templates/script/sentry.phtml: -------------------------------------------------------------------------------- 1 | canUseScriptTag($block->getNameInLayout())) { 6 | return; 7 | } 8 | ?> 9 | 10 | useScriptTag()): ?> 11 | isTracingEnabled()) { 15 | $bundleFile .= '.tracing'; 16 | } 17 | 18 | if ($block->useSessionReplay()) { 19 | $bundleFile .= '.replay'; 20 | } 21 | 22 | $bundleFile .= '.min.js'; 23 | 24 | $remoteFile = sprintf( 25 | 'https://browser.sentry-cdn.com/%s/%s', 26 | $escaper->escapeHtmlAttr($block->getJsSdkVersion()), 27 | $bundleFile 28 | ); 29 | ?> 30 | renderTag('script', ['src' => $remoteFile, 'crossorigin' => 'anonymous']) ?> 31 | renderTag('script', [], $block->getLayout()->createBlock(\JustBetter\Sentry\Block\SentryScript::class) 32 | ->setTemplate('JustBetter_Sentry::script/sentry_init.phtml') 33 | ->toHtml(), false); ?> 34 | 35 | useLogRocket()): ?> 36 | renderTag('script', ['src' => 'https://cdn.lr-ingest.io/LogRocket.min.js', 'crossorigin' => 'anonymous']) ?> 37 | renderTag( 38 | 'script', 39 | [], 40 | "window.LogRocket && window.LogRocket.init('" . /* @noEscape */ trim($block->getLogrocketKey()) . "');", 41 | false 42 | ); ?> 43 | renderTag( 44 | 'script', 45 | [], 46 | 'LogRocket.getSessionURL(sessionURL => { 47 | Sentry.configureScope(scope => { 48 | scope.setExtra("sessionURL", sessionURL); 49 | }); 50 | });', 51 | false 52 | ); ?> 53 | renderTag('script', [], $block->getLayout()->createBlock(\JustBetter\Sentry\Block\SentryScript::class) 54 | ->setTemplate('JustBetter_Sentry::script/logrocket_init.phtml') 55 | ->toHtml(), false); ?> 56 | 57 | 58 | isSpotlightEnabled()): ?> 59 | renderTag('script', ['src' => 'https://unpkg.com/@spotlightjs/overlay@latest/dist/sentry-spotlight.iife.js', 'crossorigin' => 'anonymous']) ?> 60 | -------------------------------------------------------------------------------- /view/frontend/templates/script/sentry_init.phtml: -------------------------------------------------------------------------------- 1 | 2 | if (typeof Sentry !== 'undefined') { 3 | Sentry.init({ 4 | dsn: 'escapeUrl(trim($block->getDSN())) ?>', 5 | release: 'escapeHtml(trim($block->getVersion())) ?>', 6 | environment: 'escapeHtml(trim($block->getEnvironment())) ?>', 7 | integrations: [ 8 | isTracingEnabled()): ?> 9 | Sentry.browserTracingIntegration({ 10 | enableInp: true, 11 | }), 12 | 13 | useSessionReplay()): ?> 14 | Sentry.replayIntegration({ 15 | blockAllMedia: escapeHtml($block->getReplayBlockMedia() ? 'true' : 'false') ?>, 16 | maskAllText: escapeHtml($block->getReplayMaskText() ? 'true' : 'false') ?>, 17 | }) 18 | 19 | ], 20 | isTracingEnabled()): ?> 21 | tracesSampleRate: escapeHtml($block->getTracingSampleRate()) ?>, 22 | 23 | useSessionReplay()): ?> 24 | replaysSessionSampleRate: escapeHtml($block->getReplaySessionSampleRate()) ?>, 25 | replaysOnErrorSampleRate: escapeHtml($block->getReplayErrorSampleRate()) ?>, 26 | 27 | ignoreErrors: getIgnoreJsErrors() ?>, 28 | stripStaticContentVersion() || $block->stripStoreCode()): ?> 29 | beforeSend: function(event) { 30 | event.exception.values.map(function (value) { 31 | if (value.stacktrace === undefined || ! value.stacktrace) { 32 | return value; 33 | } 34 | 35 | stripStaticContentVersion()): ?> 36 | value.stacktrace.frames.map(function (frame) { 37 | frame.filename = frame.filename.replace(/version[0-9]{10}\//, ''); 38 | return frame; 39 | }); 40 | 41 | 42 | stripStoreCode()): ?> 43 | value.stacktrace.frames.map(function (frame) { 44 | 45 | frame.filename = frame.filename.replace('/escapeHtml($block->getStoreCode()); ?>/', '/'); 46 | 47 | return frame; 48 | }); 49 | 50 | 51 | return value; 52 | }); 53 | return event; 54 | } 55 | 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "justbetter/magento2-sentry", 3 | "description": "Magento 2 Logger for Sentry", 4 | "keywords": [ 5 | "PHP", 6 | "Magento", 7 | "Magento 2", 8 | "javascript", 9 | "Sentry", 10 | "Logger", 11 | "Logging", 12 | "Error", 13 | "Error Reporting", 14 | "Error Tracking", 15 | "Exception", 16 | "Exception Reporting", 17 | "Exception Tracking", 18 | "Session Replay", 19 | "Logrocket", 20 | "Tracing", 21 | "Performance" 22 | ], 23 | "type": "magento2-module", 24 | "license": "MIT", 25 | "require": { 26 | "php": ">=8.0", 27 | "sentry/sentry": "^4.13", 28 | "monolog/monolog": ">=2.7.0|^3.0", 29 | "magento/framework": ">=103.0.7", 30 | "magento/module-csp": "*", 31 | "nyholm/psr7": "^1.2", 32 | "magento/module-config": ">=101.2" 33 | }, 34 | "repositories": { 35 | "magento": { 36 | "type": "composer", 37 | "url": "https://repo-magento-mirror.fooman.co.nz/" 38 | } 39 | }, 40 | "authors": [ 41 | { 42 | "name": "Indy Koning", 43 | "email": "indy@justbetter.nl", 44 | "homepage": "https://justbetter.nl", 45 | "role": "Developer" 46 | }, 47 | { 48 | "name": "Rakhal Imming", 49 | "email": "rakhal@justbetter.nl", 50 | "homepage": "https://justbetter.nl", 51 | "role": "Developer" 52 | }, 53 | { 54 | "name": "Joseph Maxwell", 55 | "email": "joseph@swiftotter.com", 56 | "homepage": "https://swiftotter.com", 57 | "role": "Developer" 58 | } 59 | ], 60 | "autoload": { 61 | "psr-4": { "JustBetter\\Sentry\\": "" }, 62 | "files": [ "registration.php" ] 63 | }, 64 | "config": { 65 | "allow-plugins": { 66 | "php-http/discovery": true, 67 | "magento/composer-dependency-version-audit-plugin": true, 68 | "dealerdirect/phpcodesniffer-composer-installer": true 69 | } 70 | }, 71 | "scripts": { 72 | "analyse": "vendor/bin/phpstan analyse --memory-limit='1G'", 73 | "phpcs": "vendor/bin/phpcs --colors --standard=vendor/magento/magento-coding-standard/Magento2 -s --exclude=Generic.Files.LineLength --report=full,summary,gitblame --extensions=php,phtml --ignore=./vendor ./", 74 | "phpcbf": "vendor/bin/phpcbf --colors --standard=vendor/magento/magento-coding-standard/Magento2 --exclude=Generic.Files.LineLength --extensions=php,phtml --ignore=./vendor ./ || exit 0", 75 | "codestyle": [ 76 | "@phpcbf", 77 | "@phpcs" 78 | ] 79 | }, 80 | "require-dev": { 81 | "bitexpert/phpstan-magento": "^0.32.0", 82 | "magento/magento-coding-standard": "^34" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Plugin/CspModeConfigManagerPlugin.php: -------------------------------------------------------------------------------- 1 | getReportUri() !== null || !$this->config->isActive() || !$this->config->isEnableCspReportUrl()) { 39 | return $result; 40 | } 41 | 42 | $dsn = $this->config->getDSN(); 43 | if (!is_string($dsn) || empty($dsn)) { 44 | return $result; 45 | } 46 | 47 | // DSN 48 | // https://@.ingest..sentry.io/ 49 | // https://@example.com/ 50 | 51 | // security-url 52 | // https://.ingest..sentry.io/api//security/?sentry_key= 53 | // https://example.com/api//security/?sentry_key= 54 | 55 | $uriParsed = UriFactory::factory($dsn); 56 | 57 | $dsnPaths = explode('/', $uriParsed->getPath()); // the last one is the project-id 58 | $reportUri = sprintf('https://%s/api/%s/security', $uriParsed->getHost(), $dsnPaths[count($dsnPaths) - 1]); 59 | 60 | $params = [ 61 | 'sentry_key' => $uriParsed->getUserInfo(), 62 | 'sentry_release' => $this->version->getValue(), 63 | 'sentry_environment' => $this->config->getEnvironment(), 64 | ]; 65 | 66 | $params = array_filter($params); 67 | if ($params !== []) { 68 | $reportUri .= '&'.http_build_query($params); 69 | } 70 | 71 | return new ModeConfigured($result->isReportOnly(), $reportUri); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Controller/Adminhtml/Test/Sentry.php: -------------------------------------------------------------------------------- 1 | false]; 51 | 52 | $activeWithReason = $this->helperSentry->isActiveWithReason(); 53 | 54 | if ($activeWithReason['active']) { 55 | try { 56 | if ($this->helperSentry->isPhpTrackingEnabled()) { 57 | $this->monolog->addRecord(\Monolog\Logger::ALERT, 'TEST message from Magento 2', []); 58 | $result['status'] = true; 59 | $result['content'] = __('Check sentry.io which should hold an alert'); 60 | } else { 61 | $result['content'] = __('Php error tracking must be enabled for testing'); 62 | } 63 | } catch (\Exception $e) { 64 | $result['content'] = $e->getMessage(); 65 | $this->logger->critical($e); 66 | } 67 | } else { 68 | $result['content'] = implode(PHP_EOL, $activeWithReason['reasons']); 69 | } 70 | 71 | /** @var \Magento\Framework\App\Response\Http $response */ 72 | $response = $this->getResponse(); 73 | 74 | return $response->representJson( 75 | $this->jsonSerializer->serialize($result) 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Plugin/Profiling/EventManagerPlugin.php: -------------------------------------------------------------------------------- 1 | excludePatterns = array_merge([ 25 | '^model_load_', 26 | '_load_before$', 27 | '_load_after$', 28 | '_$', 29 | '^view_block_abstract_', 30 | '^core_layout_render_e', 31 | ], $excludePatterns); 32 | } 33 | 34 | /** 35 | * Method checks if the block is excluded for php profiling. 36 | * 37 | * @param string|null $eventName 38 | * 39 | * @return bool 40 | */ 41 | public function canTrace(?string $eventName): bool 42 | { 43 | if ($eventName === null) { 44 | return false; 45 | } 46 | 47 | foreach ($this->excludePatterns as $excludePattern) { 48 | if (preg_match('/'.$excludePattern.'/i', $eventName)) { 49 | return false; 50 | } 51 | } 52 | 53 | if ($this->config->getObservers(mb_strtolower($eventName)) === []) { 54 | return false; 55 | } 56 | 57 | return true; 58 | } 59 | 60 | /** 61 | * Method creates a Sentry span for php profiling to profile event handling. 62 | * 63 | * @param ManagerInterface $subject 64 | * @param callable $callable 65 | * @param string $eventName 66 | * @param array $data 67 | * 68 | * @return mixed 69 | */ 70 | public function aroundDispatch(ManagerInterface $subject, callable $callable, string $eventName, array $data = []): mixed 71 | { 72 | if (!$this->canTrace($eventName)) { 73 | return $callable($eventName, $data); 74 | } 75 | 76 | $context = SpanContext::make() 77 | ->setOp('event') 78 | ->setDescription($eventName) 79 | ->setData([ 80 | 'event.name' => $eventName, 81 | ]) 82 | ->setOrigin('auto.event'); 83 | 84 | $tracingDto = SentryPerformance::traceStart($context); 85 | 86 | try { 87 | return $callable($eventName, $data); 88 | } finally { 89 | SentryPerformance::traceEnd($tracingDto); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /.github/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)** 44 | 45 | To save time on codestyle feedback, please run 46 | - `composer install` 47 | - `composer run codestyle` 48 | - `composer run analyse` 49 | 50 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 51 | 52 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 53 | 54 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option. 55 | 56 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 57 | 58 | **Happy coding**! -------------------------------------------------------------------------------- /Logger/Handler/Sentry.php: -------------------------------------------------------------------------------- 1 | deploymentConfig->isAvailable() && $this->sentryHelper->isActive(); 38 | } 39 | 40 | /** 41 | * @inheritDoc 42 | */ 43 | public function handle(array $record): bool 44 | { 45 | if (!$this->isHandling($record)) { 46 | return false; 47 | } 48 | 49 | $this->sentryLog->send($record['message'], $record['level'], $record['context']); 50 | 51 | return false; 52 | } 53 | } 54 | } else { 55 | class Sentry extends AbstractHandler 56 | { 57 | /** 58 | * Construct. 59 | * 60 | * @param Data $sentryHelper 61 | * @param SentryLog $sentryLog 62 | * @param DeploymentConfig $deploymentConfig 63 | */ 64 | public function __construct( 65 | protected Data $sentryHelper, 66 | protected SentryLog $sentryLog, 67 | protected DeploymentConfig $deploymentConfig, 68 | ) { 69 | parent::__construct(); 70 | } 71 | 72 | /** 73 | * @inheritDoc 74 | */ 75 | public function isHandling(LogRecord $record): bool 76 | { 77 | return $this->deploymentConfig->isAvailable() && $this->sentryHelper->isActive(); 78 | } 79 | 80 | /** 81 | * @inheritDoc 82 | */ 83 | public function handle(LogRecord $record): bool 84 | { 85 | if (!$this->isHandling($record)) { 86 | return false; 87 | } 88 | 89 | $this->sentryLog->send($record['message'], $record['level'], $record['context']); 90 | 91 | return false; 92 | } 93 | } 94 | } 95 | // phpcs:enable Generic.Classes.DuplicateClassName,PSR2.Classes.ClassDeclaration,PSR1.Classes.ClassDeclaration.MultipleClasses 96 | -------------------------------------------------------------------------------- /Plugin/Profiling/ExchangePlugin.php: -------------------------------------------------------------------------------- 1 | getSpan(); 25 | if ($parentSpan === null) { 26 | return [$topic, $envelopes]; 27 | } 28 | 29 | $isMultipleEnvelopes = is_array($envelopes); 30 | $context = \Sentry\Tracing\SpanContext::make() 31 | ->setOp('queue.publish') 32 | ->setDescription($topic); 33 | 34 | $envelopes = array_map(function (EnvelopeInterface $envelope) use ($parentSpan, $context, $topic) { 35 | $properties = $envelope->getProperties(); 36 | $span = $parentSpan->startChild($context); 37 | \Sentry\SentrySdk::getCurrentHub()->setSpan($span); 38 | 39 | $body = json_decode($envelope->getBody(), true); 40 | $envelope = $this->setBody( 41 | $envelope, 42 | json_encode([ 43 | ...$body, 44 | 'sentry_trace' => \Sentry\getTraceparent(), 45 | 'sentry_baggage' => \Sentry\getBaggage(), 46 | ]) 47 | ); 48 | 49 | $span 50 | ->setData([ 51 | 'messaging.message.id' => $properties['message_id'] ?? null, 52 | 'messaging.destination.name' => $topic, 53 | 'messaging.message.body.size' => strlen($envelope->getBody()), 54 | ]) 55 | ->finish(); 56 | 57 | \Sentry\SentrySdk::getCurrentHub()->setSpan($parentSpan); 58 | 59 | return $envelope; 60 | }, $isMultipleEnvelopes ? $envelopes : [$envelopes]); 61 | 62 | $envelopes = $isMultipleEnvelopes ? $envelopes : $envelopes[0]; 63 | 64 | return [$topic, $envelopes]; 65 | } 66 | 67 | /** 68 | * Attempt to set the body to the private body variable. 69 | * 70 | * @param EnvelopeInterface $envelope 71 | * @param string $body 72 | * 73 | * @return EnvelopeInterface 74 | */ 75 | protected function setBody(EnvelopeInterface $envelope, string $body): EnvelopeInterface 76 | { 77 | $reflectedEnvelope = new \ReflectionObject($envelope); 78 | // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock.DetectedWhile 79 | while (!$reflectedEnvelope->hasProperty('body') && $reflectedEnvelope = $reflectedEnvelope->getParentClass()) { 80 | } 81 | 82 | if ($reflectedEnvelope && $reflectedEnvelope->hasProperty('body')) { 83 | $prop = $reflectedEnvelope->getProperty('body'); 84 | $prop->setAccessible(true); 85 | $prop->setValue($envelope, $body); 86 | } 87 | 88 | return $envelope; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Helper/Version.php: -------------------------------------------------------------------------------- 1 | deploymentConfig = $deploymentConfig ?: ObjectManager::getInstance()->get(DeploymentConfig::class); 44 | } 45 | 46 | /** 47 | * Retrieve deployment version of static files. 48 | * 49 | * @return string|null 50 | */ 51 | public function getValue(): ?string 52 | { 53 | if (!$this->cachedValue) { 54 | $this->cachedValue = $this->readValue($this->appState->getMode()); 55 | } 56 | 57 | return $this->cachedValue; 58 | } 59 | 60 | /** 61 | * Load or generate deployment version of static files depending on the application mode. 62 | * 63 | * @param string $appMode 64 | * 65 | * @return string|null 66 | */ 67 | protected function readValue($appMode): ?string 68 | { 69 | if ($version = $this->sentryHelper->getRelease()) { 70 | return $version; 71 | } 72 | 73 | $result = $this->versionStorage->load(); 74 | if (!$result) { 75 | if ($appMode == \Magento\Framework\App\State::MODE_PRODUCTION 76 | && !$this->deploymentConfig->getConfigData( 77 | ConfigOptionsListConstants::CONFIG_PATH_SCD_ON_DEMAND_IN_PRODUCTION 78 | ) 79 | ) { 80 | $this->getLogger()->critical('Can not load static content version.'); 81 | 82 | return null; 83 | } 84 | $result = $this->generateVersion(); 85 | $this->versionStorage->save((string) $result); 86 | } 87 | 88 | return $result; 89 | } 90 | 91 | /** 92 | * Generate version of static content. 93 | * 94 | * @return int 95 | */ 96 | private function generateVersion() 97 | { 98 | return time(); 99 | } 100 | 101 | /** 102 | * Get logger. 103 | * 104 | * @return LoggerInterface 105 | */ 106 | private function getLogger() 107 | { 108 | if ($this->logger == null) { 109 | $this->logger = \Magento\Framework\App\ObjectManager::getInstance() 110 | ->get(LoggerInterface::class); 111 | } 112 | 113 | return $this->logger; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Model/SentryCron.php: -------------------------------------------------------------------------------- 1 | data->isActive() || 39 | !$this->data->isCronMonitoringEnabled() || 40 | !array_reduce( 41 | $this->data->getTrackCrons(), 42 | fn ($trackCron, $expression) => $trackCron || ( 43 | preg_match('/^\/.*\/[imsu]*$/', $expression) ? 44 | preg_match($expression, $schedule->getJobCode()) : 45 | $schedule->getJobCode() === $expression 46 | ), 47 | false 48 | ) 49 | ) { 50 | return; 51 | } 52 | 53 | $status = $schedule->getStatus(); 54 | if (!in_array($status, [ 55 | Schedule::STATUS_RUNNING, 56 | Schedule::STATUS_SUCCESS, 57 | Schedule::STATUS_ERROR, 58 | ])) { 59 | return; 60 | } 61 | 62 | /** @var array|null $cronExpressionArr */ 63 | $cronExpressionArr = $schedule->getCronExprArr(); 64 | $monitorConfig = null; 65 | if (!empty($cronExpressionArr)) { 66 | $cronExpression = implode(' ', $cronExpressionArr); 67 | $monitorConfig = new MonitorConfig(MonitorSchedule::crontab($cronExpression)); 68 | } 69 | 70 | if ($status === Schedule::STATUS_RUNNING) { 71 | if (!isset($this->runningCheckins[$schedule->getId()])) { 72 | $this->startCheckin($schedule, $monitorConfig); 73 | } 74 | 75 | return; 76 | } else { 77 | $this->finishCheckin($schedule, $monitorConfig); 78 | } 79 | } 80 | 81 | /** 82 | * Start the check-in for a given schedule. 83 | * 84 | * @param Schedule $schedule 85 | * @param MonitorConfig|null $monitorConfig 86 | */ 87 | public function startCheckin(Schedule $schedule, ?MonitorConfig $monitorConfig = null): void 88 | { 89 | $this->runningCheckins[$schedule->getId()] = [ 90 | 'started_at' => microtime(true), 91 | 'check_in_id' => \Sentry\captureCheckIn( 92 | slug: $schedule->getJobCode(), 93 | status: CheckInStatus::inProgress(), 94 | monitorConfig: $monitorConfig, 95 | ), 96 | ]; 97 | } 98 | 99 | /** 100 | * Finish the check-in for a given schedule. 101 | * 102 | * @param Schedule $schedule 103 | * @param MonitorConfig|null $monitorConfig 104 | */ 105 | public function finishCheckin(Schedule $schedule, ?MonitorConfig $monitorConfig = null): void 106 | { 107 | if (!isset($this->runningCheckins[$schedule->getId()])) { 108 | return; 109 | } 110 | 111 | \Sentry\captureCheckIn( 112 | slug: $schedule->getJobCode(), 113 | status: $schedule->getStatus() === Schedule::STATUS_SUCCESS ? CheckInStatus::ok() : CheckInStatus::error(), 114 | duration: microtime(true) - $this->runningCheckins[$schedule->getId()]['started_at'], 115 | monitorConfig: $monitorConfig, 116 | checkInId: $this->runningCheckins[$schedule->getId()]['check_in_id'], 117 | ); 118 | 119 | unset($this->runningCheckins[$schedule->getId()]); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /etc/di.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | JustBetter\Sentry\Logger\Handler\Sentry\Proxy 23 | 24 | 25 | 26 | 27 | 29 | 30 | 31 | 32 | 33 | Magento\Customer\Model\Session\Proxy 34 | 35 | 36 | 37 | 38 | 39 | JustBetter\Sentry\Helper\Data\Proxy 40 | JustBetter\Sentry\Model\SentryLog\Proxy 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | JustBetter\Sentry\Model\Collector\SentryRelatedCspCollector\Proxy 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /Plugin/Profiling/Cache.php: -------------------------------------------------------------------------------- 1 | getSpan(); 25 | if ($parentSpan === null) { 26 | return parent::load($identifier); 27 | } 28 | 29 | $context = SpanContext::make() 30 | ->setOp('cache.get') 31 | ->setData([ 32 | 'cache.key' => $identifier, 33 | ]) 34 | ->setDescription((string) $identifier) 35 | ->setOrigin('auto.cache'); 36 | $span = $parentSpan->startChild($context); 37 | SentrySdk::getCurrentHub()->setSpan($span); 38 | 39 | $result = parent::load($identifier); 40 | 41 | if ($result === null || $result === false) { 42 | $span->setData([ 43 | 'cache.hit' => false, 44 | ]); 45 | } else { 46 | $span->setData([ 47 | 'cache.hit' => true, 48 | 'cache.item_size' => is_string($result) ? strlen($result) : null, 49 | ]); 50 | } 51 | 52 | $span->finish(); 53 | SentrySdk::getCurrentHub()->setSpan($parentSpan); 54 | 55 | return $result; 56 | } 57 | 58 | /** 59 | * Instrument the cache.put operation around the save. 60 | * 61 | * @param string $data 62 | * @param string $identifier 63 | * @param array $tags 64 | * @param int|bool|null $lifeTime 65 | * 66 | * @return bool 67 | */ 68 | public function save($data, $identifier, array $tags = [], $lifeTime = null) 69 | { 70 | $parentSpan = SentrySdk::getCurrentHub()->getSpan(); 71 | if ($parentSpan === null) { 72 | return parent::save($data, $identifier, $tags, $lifeTime); 73 | } 74 | 75 | $context = SpanContext::make() 76 | ->setOp('cache.put') 77 | ->setData([ 78 | 'cache.key' => $identifier, 79 | 'cache.tags' => $tags, 80 | 'cache.ttl' => $lifeTime, 81 | ]) 82 | ->setDescription($identifier) 83 | ->setOrigin('auto.cache'); 84 | 85 | $span = $parentSpan->startChild($context); 86 | SentrySdk::getCurrentHub()->setSpan($span); 87 | 88 | $result = parent::save($data, $identifier, $tags, $lifeTime); 89 | 90 | $span->finish(); 91 | SentrySdk::getCurrentHub()->setSpan($parentSpan); 92 | 93 | return $result; 94 | } 95 | 96 | /** 97 | * Instrument the cache.remove operation around the remove. 98 | * 99 | * @param string $identifier 100 | * 101 | * @return bool 102 | */ 103 | public function remove($identifier) 104 | { 105 | $parentSpan = SentrySdk::getCurrentHub()->getSpan(); 106 | if ($parentSpan === null) { 107 | return parent::remove($identifier); 108 | } 109 | 110 | $context = SpanContext::make() 111 | ->setOp('cache.remove') 112 | ->setData([ 113 | 'cache.key' => $identifier, 114 | ]) 115 | ->setDescription($identifier) 116 | ->setOrigin('auto.cache'); 117 | 118 | $span = $parentSpan->startChild($context); 119 | SentrySdk::getCurrentHub()->setSpan($span); 120 | 121 | $result = parent::remove($identifier); 122 | 123 | $span->finish(); 124 | SentrySdk::getCurrentHub()->setSpan($parentSpan); 125 | 126 | return $result; 127 | } 128 | 129 | /** 130 | * Instrument the cache.remove operation around the clean. 131 | * 132 | * @param string $mode 133 | * @param array $tags 134 | * 135 | * @return bool 136 | */ 137 | public function clean($mode = \Zend_Cache::CLEANING_MODE_ALL, array $tags = []) 138 | { 139 | $parentSpan = SentrySdk::getCurrentHub()->getSpan(); 140 | if ($parentSpan === null) { 141 | return parent::clean($mode, $tags); 142 | } 143 | 144 | $context = SpanContext::make() 145 | ->setOp('cache.remove') 146 | ->setData([ 147 | 'cache.mode' => $mode, 148 | 'cache.tags' => $tags, 149 | ]) 150 | ->setDescription($mode.' '.implode(',', $tags)) 151 | ->setOrigin('auto.cache'); 152 | 153 | $span = $parentSpan->startChild($context); 154 | SentrySdk::getCurrentHub()->setSpan($span); 155 | 156 | $result = parent::clean($mode, $tags); 157 | 158 | $span->finish(); 159 | SentrySdk::getCurrentHub()->setSpan($parentSpan); 160 | 161 | return $result; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /Test/Unit/Plugin/GlobalExceptionCatcherTest.php: -------------------------------------------------------------------------------- 1 | getSentryHelperMock(false); 25 | $helper->expects($this->never()) 26 | ->method('getDSN'); 27 | 28 | /** @var GlobalExceptionCatcher $plugin */ 29 | $plugin = (new ObjectManager($this))->getObject( 30 | GlobalExceptionCatcher::class, 31 | [ 32 | 'sentryHelper' => $helper, 33 | 'releaseIdentifier' => $this->getReleaseIdentifierMock(), 34 | 'sentryInteraction' => $this->getSentryInteractionMock(false, false), 35 | ] 36 | ); 37 | 38 | $called = false; 39 | $plugin->aroundLaunch( 40 | $this->getAppInterfaceMock(), 41 | function () use (&$called) { 42 | $called = true; 43 | } 44 | ); 45 | 46 | $this->assertTrue($called); 47 | } 48 | 49 | public function testInitializeIsCalledWithCorrectCredentials() 50 | { 51 | $helper = $this->getSentryHelperMock(true, 'test'); 52 | $helper->expects($this->once()) 53 | ->method('getDSN'); 54 | 55 | $helper->expects($this->once()) 56 | ->method('getEnvironment'); 57 | 58 | /** @var GlobalExceptionCatcher $plugin */ 59 | $plugin = (new ObjectManager($this))->getObject( 60 | GlobalExceptionCatcher::class, 61 | [ 62 | 'sentryHelper' => $helper, 63 | 'releaseIdentifier' => $this->getReleaseIdentifierMock(), 64 | 'sentryInteraction' => $this->getSentryInteractionMock(true, false), 65 | ] 66 | ); 67 | 68 | $called = false; 69 | $plugin->aroundLaunch( 70 | $this->getAppInterfaceMock(), 71 | function () use (&$called) { 72 | $called = true; 73 | } 74 | ); 75 | 76 | $this->assertTrue($called); 77 | } 78 | 79 | public function testCaptureExceptionIsCalled() 80 | { 81 | $helper = $this->getSentryHelperMock(true, 'test'); 82 | 83 | /** @var GlobalExceptionCatcher $plugin */ 84 | $plugin = (new ObjectManager($this))->getObject( 85 | GlobalExceptionCatcher::class, 86 | [ 87 | 'sentryHelper' => $helper, 88 | 'releaseIdentifier' => $this->getReleaseIdentifierMock(), 89 | 'sentryInteraction' => $this->getSentryInteractionMock(true, true), 90 | ] 91 | ); 92 | 93 | $called = false; 94 | $exceptionMessage = 'Big problem'; 95 | $this->expectExceptionMessage($exceptionMessage); 96 | 97 | $plugin->aroundLaunch( 98 | $this->getAppInterfaceMock(), 99 | function () use (&$called, $exceptionMessage) { 100 | $called = true; 101 | 102 | throw new \Exception($exceptionMessage); 103 | } 104 | ); 105 | 106 | $this->assertTrue($called); 107 | } 108 | 109 | private function getAppInterfaceMock() 110 | { 111 | return $this->getMockForAbstractClass(AppInterface::class); 112 | } 113 | 114 | private function getSentryInteractionMock(bool $expectsInitialize, bool $expectsCaptureException) 115 | { 116 | $mock = $this->createConfiguredMock( 117 | SentryInteraction::class, 118 | [ 119 | 'initialize' => '', 120 | 'captureException' => '', 121 | ] 122 | ); 123 | 124 | $mock->expects($expectsInitialize ? $this->atLeastOnce() : $this->never()) 125 | ->method('initialize'); 126 | 127 | $mock->expects($expectsCaptureException ? $this->atLeastOnce() : $this->never()) 128 | ->method('captureException'); 129 | 130 | return $mock; 131 | } 132 | 133 | private function getReleaseIdentifierMock() 134 | { 135 | $mock = $this->createConfiguredMock( 136 | ReleaseIdentifier::class, 137 | [ 138 | 'getReleaseId' => 1, 139 | ] 140 | ); 141 | 142 | return $mock; 143 | } 144 | 145 | private function getSentryHelperMock(bool $isActive = true, $environment = null) 146 | { 147 | $mock = $this->createConfiguredMock(Data::class, [ 148 | 'isActive' => $isActive, 149 | 'getDSN' => 'dsn', 150 | 'getEnvironment' => $environment, 151 | ]); 152 | 153 | $mock->expects($this->once()) 154 | ->method('isActive'); 155 | 156 | return $mock; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /Plugin/Profiling/QueuePlugin.php: -------------------------------------------------------------------------------- 1 | getProperties(); 34 | $body = json_decode($envelope->getBody(), true); 35 | if (!isset($body['sentry_trace']) && !isset($body['sentry_baggage'])) { 36 | return $envelope; 37 | } 38 | 39 | $this->parentSpan ??= \Sentry\SentrySdk::getCurrentHub()->getSpan(); 40 | 41 | $context = \Sentry\continueTrace( 42 | $body['sentry_trace'], 43 | $body['sentry_baggage'] 44 | ) 45 | ->setOp('queue.process') 46 | ->setName($properties['topic_name']); 47 | 48 | $this->transactions[$properties['message_id']] = \Sentry\startTransaction($context); 49 | $this->transactions[$properties['message_id']] 50 | ->setData([ 51 | 'messaging.message.id' => $properties['message_id'], 52 | 'messaging.destination.name' => $properties['topic_name'], 53 | 'messaging.queue.name' => $properties['queue_name'] ?? null, 54 | 'messaging.message.body.size' => strlen($envelope->getBody()), 55 | 'messaging.message.retry.count' => $properties['retries'] ?? null, 56 | ]); 57 | \Sentry\SentrySdk::getCurrentHub()->setSpan($this->transactions[$properties['message_id']]); 58 | 59 | unset($body['sentry_trace']); 60 | unset($body['sentry_baggage']); 61 | 62 | return $this->setBody($envelope, json_encode($body)); 63 | } 64 | 65 | /** 66 | * Finish transaction for failed job. 67 | * 68 | * @param QueueInterface $queue 69 | * @param EnvelopeInterface $envelope 70 | * @param bool $requeue 71 | * @param string $rejectionMessage 72 | * 73 | * @return array 74 | */ 75 | public function beforeReject(QueueInterface $queue, EnvelopeInterface $envelope, $requeue = true, $rejectionMessage = null): array 76 | { 77 | $properties = $envelope->getProperties(); 78 | $transaction = $this->transactions[$properties['message_id']] ?? null; 79 | if (!$transaction) { 80 | return [$envelope, $requeue, $rejectionMessage]; 81 | } 82 | 83 | $transaction->setStatus(\Sentry\Tracing\SpanStatus::internalError()); 84 | 85 | $transaction->finish(); 86 | unset($this->transactions[$properties['message_id']]); 87 | if ($this->parentSpan) { 88 | \Sentry\SentrySdk::getCurrentHub()->setSpan($this->parentSpan); 89 | } 90 | 91 | return [$envelope, $requeue, $rejectionMessage]; 92 | } 93 | 94 | /** 95 | * Finish transaction for successfully executed job. 96 | * 97 | * @param QueueInterface $queue 98 | * @param EnvelopeInterface $envelope 99 | * 100 | * @return array 101 | */ 102 | public function beforeAcknowledge(QueueInterface $queue, EnvelopeInterface $envelope): array 103 | { 104 | $properties = $envelope->getProperties(); 105 | $transaction = $this->transactions[$properties['message_id']] ?? null; 106 | if (!$transaction) { 107 | return [$envelope]; 108 | } 109 | 110 | $transaction->setStatus(\Sentry\Tracing\SpanStatus::ok()); 111 | 112 | $transaction->finish(); 113 | unset($this->transactions[$properties['message_id']]); 114 | if ($this->parentSpan) { 115 | \Sentry\SentrySdk::getCurrentHub()->setSpan($this->parentSpan); 116 | } 117 | 118 | return [$envelope]; 119 | } 120 | 121 | /** 122 | * Attempt to set the body to the private body variable. 123 | * 124 | * @param EnvelopeInterface $envelope 125 | * @param string $body 126 | * 127 | * @return EnvelopeInterface 128 | */ 129 | protected function setBody(EnvelopeInterface $envelope, string $body): EnvelopeInterface 130 | { 131 | $reflectedEnvelope = new \ReflectionObject($envelope); 132 | // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock.DetectedWhile 133 | while (!$reflectedEnvelope->hasProperty('body') && $reflectedEnvelope = $reflectedEnvelope->getParentClass()) { 134 | } 135 | 136 | if ($reflectedEnvelope && $reflectedEnvelope->hasProperty('body')) { 137 | $prop = $reflectedEnvelope->getProperty('body'); 138 | $prop->setAccessible(true); 139 | $prop->setValue($envelope, $body); 140 | } 141 | 142 | return $envelope; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /Block/SentryScript.php: -------------------------------------------------------------------------------- 1 | dataHelper->isActive() || !$this->dataHelper->showScriptTagInThisBlock($blockName)) { 43 | return false; 44 | } 45 | 46 | if ($this->useScriptTag()) { 47 | return true; 48 | } 49 | 50 | if ($this->isSpotlightEnabled()) { 51 | return true; 52 | } 53 | 54 | return false; 55 | } 56 | 57 | /** 58 | * Get the DSN of Sentry. 59 | * 60 | * @return string 61 | */ 62 | public function getDSN() 63 | { 64 | return (string) $this->dataHelper->getDSN(); 65 | } 66 | 67 | /** 68 | * Get the version of the JS-SDK of Sentry. 69 | * 70 | * @return string 71 | */ 72 | public function getJsSdkVersion() 73 | { 74 | return $this->dataHelper->getJsSdkVersion(); 75 | } 76 | 77 | /** 78 | * Get the current version of the Magento application. 79 | * 80 | * @return int|string 81 | */ 82 | public function getVersion() 83 | { 84 | return $this->version->getValue(); 85 | } 86 | 87 | /** 88 | * Get the current environment of Sentry. 89 | * 90 | * @return mixed 91 | */ 92 | public function getEnvironment() 93 | { 94 | return $this->dataHelper->getEnvironment(); 95 | } 96 | 97 | /** 98 | * Whether to enable sentry js tracking. 99 | */ 100 | public function useScriptTag(): bool 101 | { 102 | return $this->dataHelper->useScriptTag(); 103 | } 104 | 105 | /** 106 | * Whether to enable session replay. 107 | */ 108 | public function useSessionReplay(): bool 109 | { 110 | return $this->dataHelper->useSessionReplay(); 111 | } 112 | 113 | /** 114 | * Get the session replay sample rate. 115 | */ 116 | public function getReplaySessionSampleRate(): float 117 | { 118 | return $this->dataHelper->getReplaySessionSampleRate(); 119 | } 120 | 121 | /** 122 | * Get the session replay error sample rate. 123 | */ 124 | public function getReplayErrorSampleRate(): float 125 | { 126 | return $this->dataHelper->getReplayErrorSampleRate(); 127 | } 128 | 129 | /** 130 | * Whether to block media during replay. 131 | */ 132 | public function getReplayBlockMedia(): bool 133 | { 134 | return $this->dataHelper->getReplayBlockMedia(); 135 | } 136 | 137 | /** 138 | * Whether to show mask text. 139 | */ 140 | public function getReplayMaskText(): bool 141 | { 142 | return $this->dataHelper->getReplayMaskText(); 143 | } 144 | 145 | /** 146 | * If LogRocket should be used. 147 | * 148 | * @return bool 149 | */ 150 | public function useLogRocket() 151 | { 152 | return $this->dataHelper->useLogrocket(); 153 | } 154 | 155 | /** 156 | * If LogRocket identify should be used. 157 | * 158 | * @return bool 159 | */ 160 | public function useLogRocketIdentify() 161 | { 162 | return $this->dataHelper->useLogrocketIdentify(); 163 | } 164 | 165 | /** 166 | * Gets the LogRocket key. 167 | * 168 | * @return string 169 | */ 170 | public function getLogrocketKey() 171 | { 172 | return $this->dataHelper->getLogrocketKey(); 173 | } 174 | 175 | /** 176 | * Whether we should strip the static content version from the URL. 177 | * 178 | * @return bool 179 | */ 180 | public function stripStaticContentVersion() 181 | { 182 | return $this->dataHelper->stripStaticContentVersion(); 183 | } 184 | 185 | /** 186 | * Whether we should strip the store code from the URL. 187 | * 188 | * @return bool 189 | */ 190 | public function stripStoreCode() 191 | { 192 | return $this->dataHelper->stripStoreCode(); 193 | } 194 | 195 | /** 196 | * Get Store code. 197 | * 198 | * @return string 199 | */ 200 | public function getStoreCode() 201 | { 202 | return $this->_storeManager->getStore()->getCode(); 203 | } 204 | 205 | /** 206 | * Whether tracing is enabled. 207 | */ 208 | public function isTracingEnabled(): bool 209 | { 210 | return $this->dataHelper->isTracingEnabled(); 211 | } 212 | 213 | /** 214 | * Whether spotlight is enabled. 215 | */ 216 | public function isSpotlightEnabled(): bool 217 | { 218 | return $this->dataHelper->isSpotlightEnabled(); 219 | } 220 | 221 | /** 222 | * Get sample rate for tracing. 223 | */ 224 | public function getTracingSampleRate(): float 225 | { 226 | return $this->dataHelper->getTracingSampleRate(); 227 | } 228 | 229 | /** 230 | * Get a list of js errors to ignore. 231 | */ 232 | public function getIgnoreJsErrors(): string 233 | { 234 | return $this->json->serialize($this->dataHelper->getIgnoreJsErrors()); 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /Model/SentryLog.php: -------------------------------------------------------------------------------- 1 | data->collectModuleConfig(); 50 | $customTags = []; 51 | 52 | if ($config['enable_logs'] && $logLevel >= $config['logger_log_level']) { 53 | match ($logLevel) { 54 | Monolog::DEBUG => \Sentry\Logger()->debug($message, $context), 55 | Monolog::INFO => \Sentry\Logger()->info($message, $context), 56 | Monolog::WARNING => \Sentry\Logger()->warn($message, $context), 57 | Monolog::ERROR, MONOLOG::CRITICAL => \Sentry\Logger()->error($message, $context), 58 | Monolog::ALERT, MONOLOG::EMERGENCY => \Sentry\Logger()->fatal($message, $context), 59 | default => \Sentry\Logger()->info($message, $context) 60 | }; 61 | } 62 | 63 | if ($logLevel < (int) $config['log_level']) { 64 | return; 65 | } 66 | 67 | if (true === isset($context['custom_tags']) && false === empty($context['custom_tags'])) { 68 | $customTags = $context['custom_tags']; 69 | unset($context['custom_tags']); 70 | } 71 | 72 | \Sentry\configureScope( 73 | function (SentryScope $scope) use ($context, $customTags): void { 74 | $this->setTags($scope, $customTags); 75 | if (false === empty($context)) { 76 | $scope->setContext('Custom context', $context); 77 | } 78 | } 79 | ); 80 | 81 | $this->sentryInteraction->addUserContext(); 82 | 83 | if ($message instanceof \Throwable) { 84 | $lastEventId = \Sentry\captureException($message); 85 | } else { 86 | $lastEventId = \Sentry\captureMessage( 87 | $message, 88 | \Sentry\Severity::fromError($logLevel), 89 | $this->monologContextToSentryHint($context) 90 | ); 91 | } 92 | 93 | /// when using JS SDK you can use this for custom error page printing 94 | try { 95 | if (true === $this->canGetCustomerData()) { 96 | $this->customerSession->setSentryEventId($lastEventId); 97 | } 98 | } catch (SessionException $e) { 99 | return; 100 | } 101 | } 102 | 103 | /** 104 | * Turn the monolog context into a format Sentrys EventHint can deal with. 105 | * 106 | * @param array $context 107 | * 108 | * @return EventHint|null 109 | */ 110 | public function monologContextToSentryHint(array $context): ?EventHint 111 | { 112 | return EventHint::fromArray( 113 | [ 114 | 'exception' => ($context['exception'] ?? null) instanceof \Throwable ? $context['exception'] : null, 115 | 'mechanism' => ($context['mechanism'] ?? null) instanceof ExceptionMechanism ? $context['mechanism'] : null, 116 | 'stacktrace' => ($context['stacktrace'] ?? null) instanceof Stacktrace ? $context['stacktrace'] : null, 117 | 'extra' => array_filter( 118 | $context, 119 | fn ($key) => !in_array($key, ['exception', 'mechanism', 'stacktrace']), 120 | ARRAY_FILTER_USE_KEY 121 | ) ?: [], 122 | ] 123 | ); 124 | } 125 | 126 | /** 127 | * Check if we can retrieve customer data. 128 | * 129 | * @return bool 130 | */ 131 | private function canGetCustomerData() 132 | { 133 | try { 134 | return $this->appState->getAreaCode() === Area::AREA_FRONTEND; 135 | } catch (LocalizedException $ex) { 136 | return false; 137 | } 138 | } 139 | 140 | /** 141 | * Add additional tags to the scope. 142 | * 143 | * @param SentryScope $scope 144 | * @param array $customTags 145 | */ 146 | private function setTags(SentryScope $scope, $customTags): void 147 | { 148 | $store = $this->data->getStore(); 149 | 150 | $scope->setTag('mage_mode', $this->data->getAppState()); 151 | $scope->setTag('version', $this->data->getMagentoVersion()); 152 | $scope->setTag('website_id', (string) $store?->getWebsiteId()); 153 | $scope->setTag('store_id', (string) $store?->getId()); 154 | $scope->setTag('store_code', (string) $store?->getCode()); 155 | 156 | if (false === empty($customTags)) { 157 | foreach ($customTags as $tag => $value) { 158 | $scope->setTag($tag, $value); 159 | } 160 | } 161 | } 162 | 163 | /** 164 | * Send the logs to Sentry if there are any. 165 | */ 166 | public function __destruct() 167 | { 168 | \Sentry\Logger()->flush(); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /Plugin/GlobalExceptionCatcher.php: -------------------------------------------------------------------------------- 1 | globalCatcher( 61 | $subject, 62 | $proceed, 63 | ...$args 64 | ); 65 | } 66 | 67 | /** 68 | * Wrap command run, start watching for exceptions. 69 | * 70 | * @param Command $subject 71 | * @param callable $proceed 72 | * @param mixed $args 73 | * 74 | * @return int 75 | */ 76 | public function aroundRun(Command $subject, callable $proceed, ...$args) 77 | { 78 | return $this->globalCatcher( 79 | $subject, 80 | $proceed, 81 | ...$args 82 | ); 83 | } 84 | 85 | /** 86 | * Catch anything coming out of the proceed function. 87 | * 88 | * @param mixed $subject 89 | * @param callable $proceed 90 | * @param mixed $args 91 | * 92 | * @return mixed 93 | */ 94 | public function globalCatcher($subject, $proceed, ...$args) 95 | { 96 | if ($this->booted) { 97 | return $proceed(...$args); 98 | } 99 | 100 | $this->booted = true; 101 | 102 | if ((!$this->sentryHelper->isActive()) || (!$this->sentryHelper->isPhpTrackingEnabled())) { 103 | return $proceed(...$args); 104 | } 105 | 106 | $config = $this->prepareConfig(); 107 | 108 | $this->sentryInteraction->initialize(array_filter($config->getData())); 109 | $this->sentryPerformance->startTransaction($subject, ...$args); 110 | 111 | try { 112 | return $response = $proceed(...$args); 113 | } catch (Throwable $exception) { 114 | $this->sentryInteraction->captureException($exception); 115 | 116 | throw $exception; 117 | } finally { 118 | $this->sentryPerformance->finishTransaction($response ?? 500); 119 | } 120 | } 121 | 122 | /** 123 | * Prepare all the config passed to sentry. 124 | * 125 | * @return DataObject 126 | */ 127 | public function prepareConfig(): DataObject 128 | { 129 | /** @var DataObject $config */ 130 | $config = $this->dataObjectFactory->create(); 131 | $config->setData(array_intersect_key($this->sentryHelper->collectModuleConfig(), SentryHelper::NATIVE_SENTRY_CONFIG_KEYS)); 132 | 133 | $config->setSpotlight($this->sentryHelper->isSpotlightEnabled()); 134 | $config->setDsn($this->sentryHelper->getDSN()); 135 | if ($release = $this->releaseIdentifier->getReleaseId()) { 136 | $config->setRelease((string) $release); 137 | } 138 | 139 | if ($environment = $this->sentryHelper->getEnvironment()) { 140 | $config->setEnvironment($environment); 141 | } 142 | 143 | if ($prefixes = $this->sentryHelper->getPrefixes()) { 144 | $config->setPrefixes($prefixes); 145 | } 146 | 147 | $config->setBeforeBreadcrumb(function (\Sentry\Breadcrumb $breadcrumb): ?\Sentry\Breadcrumb { 148 | $data = $this->dataObjectFactory->create(); 149 | $data->setBreadcrumb($breadcrumb); 150 | $this->eventManager->dispatch('sentry_before_breadcrumb', [ 151 | 'sentry_breadcrumb' => $data, 152 | ]); 153 | 154 | return $data->getBreadcrumb(); 155 | }); 156 | 157 | $config->setBeforeSendTransaction(function (\Sentry\Event $transaction): ?\Sentry\Event { 158 | $data = $this->dataObjectFactory->create(); 159 | $data->setTransaction($transaction); 160 | $this->eventManager->dispatch('sentry_before_send_transaction', [ 161 | 'sentry_transaction' => $data, 162 | ]); 163 | 164 | return $data->getTransaction(); 165 | }); 166 | 167 | $config->setBeforeSendCheckIn(function (\Sentry\Event $checkIn): ?\Sentry\Event { 168 | $data = $this->dataObjectFactory->create(); 169 | $data->setCheckIn($checkIn); 170 | $this->eventManager->dispatch('sentry_before_send_check_in', [ 171 | 'sentry_check_in' => $data, 172 | ]); 173 | 174 | return $data->getCheckIn(); 175 | }); 176 | 177 | $config->setBeforeSendLog(function (\Sentry\Logs\Log $log): ?\Sentry\Logs\Log { 178 | $data = $this->dataObjectFactory->create(); 179 | $data->setLog($log); 180 | $this->eventManager->dispatch('sentry_before_send_log', [ 181 | 'sentry_log' => $data, 182 | ]); 183 | 184 | return $data->getLog(); 185 | }); 186 | 187 | $config->setBeforeSend(function (\Sentry\Event $event, ?\Sentry\EventHint $hint): ?\Sentry\Event { 188 | $data = $this->dataObjectFactory->create(); 189 | $data->setEvent($event); 190 | $data->setHint($hint); 191 | $this->eventManager->dispatch('sentry_before_send', [ 192 | 'sentry_event' => $data, 193 | ]); 194 | 195 | return $data->getEvent(); 196 | }); 197 | 198 | $disabledDefaultIntegrations = $this->sentryHelper->getDisabledDefaultIntegrations(); 199 | $config->setData('integrations', static fn (array $integrations) => array_filter( 200 | $integrations, 201 | static fn (IntegrationInterface $integration) => !in_array(get_class($integration), $disabledDefaultIntegrations) 202 | )); 203 | 204 | $config->setErrorTypes($this->sentryHelper->getErrorTypes()); 205 | 206 | if ($this->sentryHelper->isPerformanceTrackingEnabled()) { 207 | $config->setTracesSampleRate( 208 | php_sapi_name() === 'cli' ? 209 | $this->sentryHelper->getTracingSampleRateCli() : 210 | $this->sentryHelper->getTracingSampleRate() 211 | ); 212 | } else { 213 | $config->unsetTracesSampleRate(null); 214 | } 215 | 216 | $config->setInAppExclude([ 217 | ...($config->getInAppExclude() ?? []), 218 | 'cron.php', 219 | 'get.php', 220 | 'health_check.php', 221 | 'index.php', 222 | 'static.php', 223 | 'bin/magento', 224 | 'app/autoload.php', 225 | 'app/bootstrap.php', 226 | 'app/functions.php', 227 | ]); 228 | 229 | $this->eventManager->dispatch('sentry_before_init', [ 230 | 'config' => $config, 231 | ]); 232 | 233 | return $config; 234 | } 235 | 236 | /** 237 | * Ensure transactions are finished and cleaned up when unexpectedly exiting. 238 | */ 239 | public function __destruct() 240 | { 241 | $this->sentryPerformance->finishTransaction(); 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /Model/SentryInteraction.php: -------------------------------------------------------------------------------- 1 | setSdkIdentifier(static::SDK_IDENTIFIER); 63 | 64 | SentrySdk::init()->bindClient($client->getClient()); 65 | } 66 | 67 | /** 68 | * Check if we might be able to get user context. 69 | */ 70 | public function canGetUserContext(): bool 71 | { 72 | try { 73 | // @phpcs:ignore Generic.PHP.NoSilencedErrors 74 | return in_array(@$this->appState->getAreaCode(), [Area::AREA_ADMINHTML, Area::AREA_FRONTEND, Area::AREA_WEBAPI_REST, Area::AREA_WEBAPI_SOAP, Area::AREA_GRAPHQL]); 75 | } catch (LocalizedException $ex) { 76 | return false; 77 | } 78 | } 79 | 80 | /** 81 | * Get a class instance, but only if it is already available within the objectManager. 82 | * 83 | * @param string $class The classname of the type you want from the objectManager. 84 | */ 85 | public function getObjectIfInitialized($class): ?object 86 | { 87 | $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); 88 | $reflectionClass = new ReflectionClass($objectManager); 89 | $sharedInstances = $reflectionClass->getProperty('_sharedInstances'); 90 | $sharedInstances->setAccessible(true); 91 | $class = $this->omConfigInterface->getPreference($class); 92 | 93 | if (!array_key_exists(ltrim($class, '\\'), $sharedInstances->getValue($objectManager))) { 94 | return null; 95 | } 96 | 97 | return $objectManager->get($class); 98 | } 99 | 100 | /** 101 | * Attempt to get userContext from the objectManager, so we don't request it too early. 102 | */ 103 | public function getUserContext(): ?UserContextInterface 104 | { 105 | if ($this->userContext) { 106 | return $this->userContext; 107 | } 108 | 109 | return $this->userContext = $this->getObjectIfInitialized(UserContextInterface::class); 110 | } 111 | 112 | /** 113 | * Check if we might be able to get the user data. 114 | */ 115 | public function canGetUserData(): bool 116 | { 117 | try { 118 | // @phpcs:ignore Generic.PHP.NoSilencedErrors 119 | return in_array(@$this->appState->getAreaCode(), [Area::AREA_ADMINHTML, Area::AREA_FRONTEND]); 120 | } catch (LocalizedException $ex) { 121 | return false; 122 | } 123 | } 124 | 125 | /** 126 | * Attempt to get userdata from the current session. 127 | */ 128 | private function getSessionUserData(): array 129 | { 130 | if (!$this->canGetUserData()) { 131 | return []; 132 | } 133 | 134 | if ($this->appState->getAreaCode() === Area::AREA_ADMINHTML) { 135 | $adminSession = $this->getObjectIfInitialized(AdminSession::class); 136 | if ($adminSession === null) { 137 | return []; 138 | } 139 | 140 | if ($adminSession->isLoggedIn()) { 141 | return [ 142 | 'id' => $adminSession->getUser()->getId(), 143 | 'email' => $adminSession->getUser()->getEmail(), 144 | 'user_type' => UserContextInterface::USER_TYPE_ADMIN, 145 | ]; 146 | } 147 | } 148 | 149 | if ($this->appState->getAreaCode() === Area::AREA_FRONTEND) { 150 | $customerSession = $this->getObjectIfInitialized(CustomerSession::class); 151 | if ($customerSession === null) { 152 | return []; 153 | } 154 | 155 | if ($customerSession->isLoggedIn()) { 156 | return [ 157 | 'id' => $customerSession->getCustomer()->getId(), 158 | 'email' => $customerSession->getCustomer()->getEmail(), 159 | 'website_id' => $customerSession->getCustomer()->getWebsiteId(), 160 | 'store_id' => $customerSession->getCustomer()->getStoreId(), 161 | 'user_type' => UserContextInterface::USER_TYPE_CUSTOMER, 162 | ]; 163 | } 164 | } 165 | 166 | return []; 167 | } 168 | 169 | /** 170 | * Attempt to add the user context to the exception. 171 | */ 172 | public function addUserContext(): void 173 | { 174 | $userId = null; 175 | $userType = null; 176 | $userData = []; 177 | 178 | \Magento\Framework\Profiler::start('SENTRY::add_user_context'); 179 | 180 | try { 181 | $ip = $this->remoteAddress->getRemoteAddress(); 182 | if ($ip) { 183 | configureScope(function (Scope $scope) use ($ip) { 184 | $scope->setUser([ 185 | 'ip_address' => $ip, 186 | ]); 187 | }); 188 | } 189 | 190 | if ($this->canGetUserContext()) { 191 | $userId = $this->getUserContext()->getUserId(); 192 | if ($userId) { 193 | $userType = $this->getUserContext()->getUserType(); 194 | } 195 | } 196 | 197 | if (count($userData = $this->getSessionUserData())) { 198 | $userId = $userData['id'] ?? $userId; 199 | $userType = $userData['user_type'] ?? $userType; 200 | unset($userData['user_type']); 201 | } 202 | 203 | if (!$userId) { 204 | return; 205 | } 206 | 207 | configureScope(function (Scope $scope) use ($userType, $userId, $userData) { 208 | $scope->setUser([ 209 | 'id' => $userId, 210 | ...$userData, 211 | 'user_type' => match ($userType) { 212 | UserContextInterface::USER_TYPE_INTEGRATION => 'integration', 213 | UserContextInterface::USER_TYPE_ADMIN => 'admin', 214 | UserContextInterface::USER_TYPE_CUSTOMER => 'customer', 215 | UserContextInterface::USER_TYPE_GUEST => 'guest', 216 | default => 'unknown' 217 | }, 218 | ]); 219 | }); 220 | } catch (\Throwable $e) { 221 | // User context could not be found or added. 222 | \Magento\Framework\Profiler::stop('SENTRY::add_user_context'); 223 | 224 | return; 225 | } 226 | \Magento\Framework\Profiler::stop('SENTRY::add_user_context'); 227 | } 228 | 229 | /** 230 | * Capture passed exception. 231 | * 232 | * @param \Throwable $ex 233 | * 234 | * @return void 235 | */ 236 | public function captureException(\Throwable $ex): void 237 | { 238 | try { 239 | if (!$this->sentryHelper->shouldCaptureException($ex)) { 240 | return; 241 | } 242 | 243 | $this->addUserContext(); 244 | 245 | ob_start(); 246 | captureException($ex); 247 | ob_end_clean(); 248 | } catch (Throwable) { // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock.DetectedCatch 249 | } 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /Model/SentryPerformance.php: -------------------------------------------------------------------------------- 1 | transaction !== null) { 63 | // Do not start a transaction if one is already runnning. 64 | return; 65 | } 66 | 67 | if ($app instanceof Http) { 68 | $this->startHttpTransaction($app, ...$args); 69 | 70 | return; 71 | } 72 | if ($app instanceof Command) { 73 | $this->startCommandTransaction($app, ...$args); 74 | 75 | return; 76 | } 77 | } 78 | 79 | /** 80 | * Starts a new HTTP transaction. 81 | * 82 | * @param Http $app 83 | * 84 | * @return void 85 | */ 86 | public function startHttpTransaction(Http $app): void 87 | { 88 | $requestStartTime = $this->request->getServer('REQUEST_TIME_FLOAT', microtime(true)); 89 | 90 | $context = TransactionContext::fromHeaders( 91 | $this->request->getHeader('sentry-trace') ?: '', 92 | $this->request->getHeader('baggage') ?: '' 93 | ); 94 | 95 | $requestPath = '/'.ltrim($this->request->getRequestUri(), '/'); 96 | 97 | $context->setName($requestPath); 98 | $context->setSource(TransactionSource::url()); 99 | $context->setStartTimestamp($requestStartTime); 100 | 101 | $context->setData([ 102 | 'url' => $requestPath, 103 | 'method' => strtoupper($this->request->getMethod()), 104 | ]); 105 | 106 | // Start the transaction 107 | $transaction = startTransaction($context); 108 | 109 | // If this transaction is not sampled, don't set it either and stop doing work from this point on 110 | if (!$transaction->getSampled()) { 111 | return; 112 | } 113 | 114 | $transaction->setOrigin('auto.http.server'); 115 | 116 | $this->transaction = $transaction; 117 | SentrySdk::getCurrentHub()->setSpan($transaction); 118 | } 119 | 120 | /** 121 | * Starts a new Command transaction. 122 | * 123 | * @param Command $command 124 | * @param InputInterface $input 125 | * @param OutputInterface $output 126 | * 127 | * @return void 128 | */ 129 | public function startCommandTransaction(Command $command, InputInterface $input, OutputInterface $output): void 130 | { 131 | $requestStartTime = microtime(true); 132 | $context = TransactionContext::make(); 133 | $context->setName('bin/magento '.($input->__toString() ?: $command->getName())); 134 | $context->setSource(TransactionSource::task()); 135 | $context->setStartTimestamp($requestStartTime); 136 | 137 | $context->setData([ 138 | 'command' => $command->getName(), 139 | 'arguments' => $input->getArguments(), 140 | 'options' => $input->getOptions(), 141 | ]); 142 | 143 | // Start the transaction 144 | $transaction = startTransaction($context); 145 | 146 | // Do not sample long running tasks, individual jobs are sampled if the initiator was sampled. 147 | if (in_array($command->getName(), ['queue:consumers:start'])) { 148 | $transaction->setSampled(false); 149 | } 150 | 151 | // If this transaction is not sampled, don't set it either and stop doing work from this point on 152 | if (!$transaction->getSampled()) { 153 | return; 154 | } 155 | 156 | $this->transaction = $transaction; 157 | SentrySdk::getCurrentHub()->setSpan($transaction); 158 | } 159 | 160 | /** 161 | * Finish the transaction. this will send the transaction (and the profile) to Sentry. 162 | * 163 | * @param ResponseInterface|int|null $statusCode 164 | * 165 | * @throws LocalizedException 166 | * 167 | * @return void 168 | */ 169 | public function finishTransaction(ResponseInterface|int|null $statusCode = null): void 170 | { 171 | if ($this->transaction === null) { 172 | return; 173 | } 174 | 175 | try { 176 | $state = $this->objectManager->get(State::class); 177 | $areaCode = $state->getAreaCode(); 178 | } catch (LocalizedException $e) { 179 | // Default area is global. 180 | $areaCode = Area::AREA_GLOBAL; 181 | } 182 | 183 | if (in_array($areaCode, $this->helper->getPerformanceTrackingExcludedAreas())) { 184 | return; 185 | } 186 | 187 | if ($statusCode instanceof Response) { 188 | $statusCode = (int) $statusCode->getStatusCode(); 189 | } 190 | 191 | if (is_numeric($statusCode)) { 192 | $this->transaction->setHttpStatus($statusCode); 193 | } 194 | 195 | if (in_array($areaCode, [Area::AREA_FRONTEND, Area::AREA_ADMINHTML, Area::AREA_WEBAPI_REST, Area::AREA_WEBAPI_SOAP, Area::AREA_GRAPHQL])) { 196 | if (!empty($this->request->getFullActionName())) { 197 | $this->transaction->setName(strtoupper($this->request->getMethod()).' '.$this->request->getFullActionName('/')); 198 | } 199 | 200 | $this->transaction->setOp('http'); 201 | 202 | $this->transaction->setData(array_merge( 203 | $this->transaction->getData(), 204 | $this->request->__debugInfo(), 205 | [ 206 | 'module' => $this->request->getModuleName(), 207 | 'action' => $this->request->getFullActionName(), 208 | ] 209 | )); 210 | } else { 211 | $this->transaction->setOp($areaCode); 212 | } 213 | 214 | try { 215 | // Finish the transaction, this submits the transaction and it's span to Sentry 216 | $this->transaction->finish(); 217 | } catch (Throwable) { // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock.DetectedCatch 218 | } 219 | 220 | $this->transaction = null; 221 | } 222 | 223 | /** 224 | * Helper function to create a new span. returns a DTO which holds the important information about the span, and the span itself. 225 | * 226 | * @param SpanContext $context 227 | * 228 | * @return PerformanceTracingDto 229 | */ 230 | public static function traceStart(SpanContext $context): PerformanceTracingDto // phpcs:ignore Magento2.Functions.StaticFunction.StaticFunction 231 | { 232 | $scope = SentrySdk::getCurrentHub()->pushScope(); 233 | $span = null; 234 | 235 | $parentSpan = $scope->getSpan(); 236 | if ($parentSpan !== null && $parentSpan->getSampled()) { 237 | $span = $parentSpan->startChild($context); 238 | $scope->setSpan($span); 239 | } 240 | 241 | return new PerformanceTracingDto($scope, $parentSpan, $span); 242 | } 243 | 244 | /** 245 | * Method close the given span. a DTO object, which has been created by `::traceStart` must be passed. 246 | * 247 | * @param PerformanceTracingDto $context 248 | * 249 | * @return void 250 | */ 251 | public static function traceEnd(PerformanceTracingDto $context): void // phpcs:ignore Magento2.Functions.StaticFunction.StaticFunction 252 | { 253 | if ($context->getSpan()) { 254 | $context->getSpan()->finish(); 255 | $context->getScope()->setSpan($context->getParentSpan()); 256 | } 257 | SentrySdk::getCurrentHub()->popScope(); 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /etc/adminhtml/system.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | separator-top 9 | 10 | justbetter 11 | JustBetter_Sentry::configuration 12 | 13 | 14 | 15 | 16 | JustBetter\Sentry\Block\Adminhtml\System\Config\DeploymentConfigInfo 17 | 18 | 19 | 20 | Magento\Config\Model\Config\Source\Yesno 21 | 22 | 23 | 24 | Magento\Config\Model\Config\Source\Yesno 25 | 26 | 27 | 28 | Magento\Config\Model\Config\Source\Yesno 29 | 30 | 31 | 32 | JustBetter\Sentry\Model\Config\Source\ScriptTagPlacement 33 | 34 | 1 35 | 36 | 37 | 38 | 39 | Magento\Config\Model\Config\Source\Yesno 40 | 41 | 1 42 | 43 | 44 | 45 | 46 | 47 | 1 48 | 49 | 50 | 51 | 52 | 53 | 1 54 | 55 | 56 | 57 | 58 | Magento\Config\Model\Config\Source\Yesno 59 | 60 | 1 61 | 62 | 63 | 64 | 65 | Magento\Config\Model\Config\Source\Yesno 66 | 67 | 1 68 | 69 | 70 | 71 | 72 | Magento\Config\Model\Config\Source\Yesno 73 | 74 | 1 75 | 76 | 77 | 78 | 79 | Include user ID, name and email to identify logged in users in LogRocket 80 | Magento\Config\Model\Config\Source\Yesno 81 | 82 | 1 83 | 1 84 | 85 | 86 | 87 | Send Sentry test event 88 | justbetter_sentry/test/sentry 89 | JustBetter\Sentry\Block\Adminhtml\System\Config\Button 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | Magento\Config\Model\Config\Source\Yesno 98 | 99 | 100 | 101 | 102 | 1 103 | 104 | 105 | 106 | 107 | 108 | 1 109 | 110 | 111 | 112 | 113 | 114 | 1 115 | 116 | 117 | 118 | 119 | JustBetter\Sentry\Model\Config\Source\LogLevel 120 | 121 | 1 122 | 123 | 124 | 125 | 126 | Magento\Config\Model\Config\Source\Yesno 127 | 128 | 1 129 | 130 | 131 | 132 | 133 | validate-not-negative-number validate-number 134 | 135 | 1 136 | 137 | 138 | 139 | 140 | validate-not-negative-number validate-number 141 | 142 | 1 143 | 144 | 145 | 146 | 147 | validate-not-negative-number validate-number 148 | 149 | 1 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | Magento\Config\Model\Config\Source\Yesno 159 | 160 | 161 | 162 | 163 | Magento\Config\Model\Config\Source\Yesno 164 | 1 165 | 166 | 167 |
168 |
169 |
170 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Package banner 3 | 4 | 5 | 6 | # Magento 2 Sentry Logger 7 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/justbetter/magento2-sentry.svg?style=flat-square)](https://packagist.org/packages/justbetter/magento2-sentry) 8 | [![Total Downloads](https://img.shields.io/packagist/dt/justbetter/magento2-sentry.svg?style=flat-square)](https://packagist.org/packages/justbetter/magento2-sentry) 9 | ![Magento Support](https://img.shields.io/badge/magento-2.4-orange.svg?logo=magento&longCache=true&style=flat-square) 10 | [![PHPStan passing](https://img.shields.io/github/actions/workflow/status/justbetter/magento2-sentry/analyse.yml?label=PHPStan&style=flat-square)](https://github.com/justbetter/magento2-sentry/actions/workflows/analyse.yml) 11 | 12 | This Magento 2 module integrates [Sentry](https://github.com/getsentry/sentry-php) into magento 2. 13 | Depending on the log level configured in the backend of magento 2, notifications and errors can be sent to sentry. 14 | 15 | ## Features 16 | 17 | - Send exceptions and logs to Sentry 18 | - Show detailed context on thrown exceptions (Like Magento user/api consumer id) 19 | - Easily control which events get sent to Sentry 20 | - Backend and frontend error reporting 21 | - Performance sampling ([tracing](https://docs.sentry.io/platforms/php/tracing/) & [profiling](https://docs.sentry.io/platforms/php/profiling/)) 22 | - Database Queries (Detect N+1 queries) 23 | - Events 24 | - Template rendering 25 | - [Cache monitoring](https://docs.sentry.io/platforms/php/tracing/instrumentation/caches-module/) 26 | - [Queue monitoring](https://docs.sentry.io/platforms/php/tracing/instrumentation/queues-module/) 27 | - Cron Monitoring 28 | - Session replay 29 | - Logrocket support 30 | - Sentry feedback form after an error 31 | 32 | ## Installation 33 | - `composer require justbetter/magento2-sentry` 34 | - `bin/magento module:enable JustBetter_Sentry` 35 | - `bin/magento setup:upgrade` 36 | - `bin/magento setup:di:compile` 37 | - `bin/magento setup:static-content:deploy` 38 | 39 | ## Configuration 40 | For configuration with Adobe Cloud, [check below](#configuration-for-adobe-cloud). 41 | 42 | This module uses the [Magento Deployment Configuration](https://devdocs.magento.com/guides/v2.3/config-guide/config/config-php.html) for most it's configuration. This means that you need to add this array to your `app/etc/env.php`: 43 | 44 | ``` 45 | 'sentry' => [ 46 | 'dsn' => 'example.com', 47 | 'logrocket_key' => 'example/example', 48 | 'environment' => null, 49 | 'log_level' => \Monolog\Level::Warning, 50 | 'error_types' => E_ALL, 51 | 'ignore_exceptions' => [], 52 | 'mage_mode_development' => false, 53 | 'js_sdk_version' => \JustBetter\Sentry\Block\SentryScript::CURRENT_VERSION, 54 | 'tracing_enabled' => true, 55 | 'traces_sample_rate' => 0.5, 56 | 'disable_default_integrations' => [ 57 | \Sentry\Integration\ModulesIntegration::class, 58 | ], 59 | 'performance_tracking_enabled' => true, 60 | 'performance_tracking_excluded_areas' => [\Magento\Framework\App\Area::AREA_ADMINHTML, \Magento\Framework\App\Area::AREA_CRONTAB], 61 | 'profiles_sample_rate' => 0.5, 62 | 'ignore_js_errors' => [], 63 | 'enable_csp_report_url' => true, 64 | ] 65 | ``` 66 | 67 | Next to that there are some configuration options under Stores > Configuration > JustBetter > Sentry. 68 | 69 | ### Configuration values 70 | | Name | Default | Description | 71 | |---|---|---| 72 | | `dsn` | — | The DSN you got from Sentry for your project. You can find the DSN in the project settings under "Client Key (DSN)" | 73 | | `release` | the current deployed version in `deployed_version.txt` | Specify the current release version. Example with dynamic git hash: `trim(exec('git --git-dir ' . BP . '/.git' . ' log --pretty="%h" -n1 HEAD'))` | 74 | | `mage_mode_development` | `false` | If set to true, you will receive issues in Sentry even if Magento is running in develop mode. | 75 | | `environment` | — | Specify the environment under which the deployed version is running. Common values: production, staging, development. Helps differentiate errors between environments. | 76 | | `max_breadcrumbs` | `100` | This variable controls the total amount of breadcrumbs that should be captured. | 77 | | `attach_stacktrace` | `false` | When enabled, stack traces are automatically attached to all messages logged. Even if they are not exceptions. | 78 | | `prefixes` | [[BP](https://github.com/magento/magento2/blob/9a62604c5a7ab70db1386d307b0dbfe596611102/app/autoload.php#L16)] | A list of prefixes that should be stripped from the filenames of captured stacktraces to make them relative. | 79 | | `sample_rate` | `1.0` | Configures the sample rate for error events, in the range of 0.0 to 1.0. | 80 | | `ignore_exceptions` | `[]` | If the class being thrown matches any in this list, do not send it to Sentry, e.g., `[\Magento\Framework\Exception\NoSuchEntityException::class]` | 81 | | `error_types` | `E_ALL` | If the Exception is an instance of [ErrorException](https://www.php.net/manual/en/class.errorexception.php), send the error to Sentry if it matches the error reporting. Uses the same syntax as [Error Reporting](https://www.php.net/manual/en/function.error-reporting.php), e.g., `E_ERROR` | E_WARNING`. | 82 | | `log_level` | `\Monolog\Level::Warning` | Specify from which logging level on Sentry should get the [messages](https://docs.sentry.io/platforms/php/usage/#capturing-messages). | 83 | | `clean_stacktrace` | `true` | Whether unnecessary files (like Interceptor.php, Proxy.php, and Factory.php) should be removed from the stacktrace. (They will not be removed if they threw the error.) | 84 | | `tracing_enabled` | `false` | If set to true, tracing is enabled (bundle file is loaded automatically). | 85 | | `traces_sample_rate` | `0.2` | If tracing is enabled, set the sample rate. A number between 0 and 1, controlling the percentage chance a given transaction will be sent to Sentry. | 86 | | `traces_sample_rate_cli` | The value of `traces_sample_rate` | If tracing is enabled, set the sample rate for CLI. A number between 0 and 1, controlling the percentage chance a given transaction will be sent to Sentry. | 87 | | `profiles_sample_rate` | `0` (disabled) | if this option is larger than 0 (zero), the module will create a profile of the request. Please note that you have to install [Excimer](https://www.mediawiki.org/wiki/Excimer) on your server to use profiling. [Sentry documentation](https://docs.sentry.io/platforms/php/profiling/). You have to enable tracing too. | 88 | | `performance_tracking_enabled` | `false` | if performance tracking is enabled, a performance report gets generated for the request. | 89 | | `performance_tracking_excluded_areas` | `['adminhtml', 'crontab']` | if `performance_tracking_enabled` is enabled, we recommend to exclude the `adminhtml` & `crontab` area. | 90 | | `enable_logs` | `false` | This option enables the [logging integration](https://sentry.io/product/logs/), which allows the SDK to capture logs and send them to Sentry. | 91 | | `logger_log_level` | `\Monolog\Level::Notice` | If the logging integration is enabled, specify from which logging level the logger should log | 92 | | `js_sdk_version` | `\JustBetter\Sentry\Block\SentryScript::CURRENT_VERSION` | If set, loads the explicit version of the JavaScript SDK of Sentry. | 93 | | `ignore_js_errors` | `[]` | Array of JavaScript error messages which should not be sent to Sentry. (See also `ignoreErrors` in [Sentry documentation](https://docs.sentry.io/clients/javascript/config/)) | 94 | | `disable_default_integrations` | `[]` | Provide a list of FQCN of default integrations you do not want to use. [List of default integrations](https://github.com/getsentry/sentry-php/tree/master/src/Integration).| 95 | | `cron_monitoring_enabled` | `false` | Wether to enable [cron check ins](https://docs.sentry.io/platforms/php/crons/#upserting-cron-monitors) | 96 | | `track_crons` | `[]` | Cron handles of crons to track with cron monitoring, [Sentry only supports 6 check-ins per minute](https://docs.sentry.io/platforms/php/crons/#rate-limits) Magento does many more. | 97 | | `spotlight` | `false` | Enable [Spotlight](https://spotlightjs.com/) on the page | 98 | | `spotlight_url` | - | Override the [Sidecar url](https://spotlightjs.com/sidecar/) | 99 | | `enable_csp_report_url` | `false` | If set to true, the report-uri will be automatically added based on the DSN. | 100 | 101 | ### Configuration for Adobe Cloud 102 | Since Adobe Cloud doesn't allow you to add manually add content to the `env.php` file, the configuration can be done 103 | using the "Variables" in Adobe Commerce using the following variables: 104 | 105 | | Name | Type | 106 | |--------------------------------------------------|---------| 107 | | `CONFIG__SENTRY__ENVIRONMENT__ENABLED` | boolean | 108 | | `CONFIG__SENTRY__ENVIRONMENT__DSN` | string | 109 | | `CONFIG__SENTRY__ENVIRONMENT__MAGE_MODE_DEVELOPMENT` | string | 110 | | `CONFIG__SENTRY__ENVIRONMENT__ENVIRONMENT` | string | 111 | | `CONFIG__SENTRY__ENVIRONMENT__MAX_BREADCRUMBS` | integer | 112 | | `CONFIG__SENTRY__ENVIRONMENT__ATTACH_STACKTRACE` | boolean | 113 | | `CONFIG__SENTRY__ENVIRONMENT__SAMPLE_RATE` | float | 114 | | `CONFIG__SENTRY__ENVIRONMENT__IGNORE_EXCEPTIONS` | JSON array of classes | 115 | | `CONFIG__SENTRY__ENVIRONMENT__ERROR_TYPES` | integer | 116 | | `CONFIG__SENTRY__ENVIRONMENT__LOG_LEVEL` | integer | 117 | | `CONFIG__SENTRY__ENVIRONMENT__CLEAN_STACKTRACE` | boolean | 118 | | `CONFIG__SENTRY__ENVIRONMENT__TRACING_ENABLED` | boolean | 119 | | `CONFIG__SENTRY__ENVIRONMENT__TRACES_SAMPLE_RATE`| float | 120 | | `CONFIG__SENTRY__ENVIRONMENT__PROFILES_SAMPLE_RATE`| float | 121 | | `CONFIG__SENTRY__ENVIRONMENT__PERFORMANCE_TRACKING_ENABLED` | boolean | 122 | | `CONFIG__SENTRY__ENVIRONMENT__PERFORMANCE_TRACKING_EXCLUDED_AREAS` | boolean | 123 | | `CONFIG__SENTRY__ENVIRONMENT__ENABLE_LOGS` | boolean | 124 | | `CONFIG__SENTRY__ENVIRONMENT__LOGGER_LOG_LEVEL` | boolean | 125 | | `CONFIG__SENTRY__ENVIRONMENT__JS_SDK_VERSION` | string | 126 | | `CONFIG__SENTRY__ENVIRONMENT__IGNORE_JS_ERRORS` | JSON array of error messages | 127 | 128 | The following configuration settings can be overridden in the Magento admin. This is limited to ensure that changes to 129 | particular config settings can only be done on server level and can't be broken by changes in the admin. 130 | 131 | Please note, that it is not possible to use profiling within the Adobe Cloud. 132 | 133 | ## Optional error page configuration 134 | - Optional you can configure custom error pages in pub/errors. You can use the sentry feedback form and insert here the sentry log ID. The Sentry Log Id is captured in de customer session and can be retrieved in `processor.php`. 135 | 136 | ## Sending additional data to Sentry when logging errors 137 | - When calling any function from the [Psr\Log\LoggerInterface](https://github.com/php-fig/log/blob/master/src/LoggerInterface.php) you can pass any data to the parameter $context and it will be send to Sentry as 'Custom context'. 138 | 139 | ## Change / Filter events 140 | This module has an event called `sentry_before_send` that is dispatched before setting the config [before_send](https://docs.sentry.io/platforms/php/configuration/filtering/#using-platformidentifier-namebefore-send-). This provides the means to edit / filter events. You could for example add extra criteria to determine if the exception should be captured to Sentry. To prevent the Exception from being captured you can set the event to `null` or unset it completly. 141 | 142 | ```PHP 143 | public function execute(\Magento\Framework\Event\Observer $observer) 144 | { 145 | $observer->getEvent()->getSentryEvent()->unsEvent(); 146 | } 147 | ``` 148 | 149 | Example: https://github.com/justbetter/magento2-sentry-filter-events 150 | 151 | This same thing is the case for 152 | | | | 153 | |--------------------------------|-------------------------------------------------------------------------------------| 154 | | sentry_before_send | https://docs.sentry.io/platforms/php/configuration/options/#before_send | 155 | | sentry_before_send_transaction | https://docs.sentry.io/platforms/php/configuration/options/#before_send_transaction | 156 | | sentry_before_send_check_in | https://docs.sentry.io/platforms/php/configuration/options/#before_send_check_in | 157 | | sentry_before_breadcrumb | https://docs.sentry.io/platforms/php/configuration/options/#before_breadcrumb | 158 | | sentry_before_send_log | https://docs.sentry.io/platforms/php/configuration/options/#before_send_log | 159 | 160 | ## Compatibility 161 | The module is tested on Magento version 2.4.x with sentry sdk version 4.x. feel free to fork this project or make a pull request. 162 | 163 | ## Ideas, bugs or suggestions? 164 | Please create a [issue](https://github.com/justbetter/magento2-sentry/issues) or a [pull request](https://github.com/justbetter/magento2-sentry/pulls). 165 | 166 | ## Contributing 167 | Contributing? Awesome! Thank you for your help improving the module! 168 | 169 | Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. 170 | 171 | Most importantly: 172 | - When making a PR please add a description what you've added, and if relevant why. 173 | - To save time on codestyle feedback, please run 174 | - `composer install` 175 | - `composer run codestyle` 176 | - `composer run analyse` 177 | 178 | ## Security Vulnerabilities 179 | 180 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 181 | 182 | ## License 183 | 184 | The MIT License (MIT). Please see [License File](LICENSE) for more information. 185 | 186 | 187 | We’re a innovative development agency from The Netherlands. 188 | 189 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | ## [4.5.2] - 2025-11-27 3 | ### Fixed 4 | * Fixed undefined message properties on queues (https://github.com/justbetter/magento2-sentry/pull/221) 5 | * Fixed error thrown on new setup:install (https://github.com/justbetter/magento2-sentry/pull/223) 6 | * Fixed activating sentry with invalid DSN (https://github.com/justbetter/magento2-sentry/pull/222) 7 | ## [4.5.1] - 2025-11-11 8 | ### Added 9 | * Set Magento BP as default prefix 10 | ## [4.5.0] - 2025-11-04 11 | ### Added 12 | * Added Logger support (https://github.com/justbetter/magento2-sentry/pull/201) 13 | * Added support for manual release defnition (https://github.com/justbetter/magento2-sentry/pull/204) 14 | * Added custom SDK identifier to module (https://github.com/justbetter/magento2-sentry/pull/208) 15 | * Added Cache instrumentation (https://github.com/justbetter/magento2-sentry/pull/206) 16 | * Added Queue instrumentation (https://github.com/justbetter/magento2-sentry/pull/210) 17 | * add csp report-uri if none is configured (https://github.com/justbetter/magento2-sentry/pull/216) thanks to https://github.com/rommelfreddy 18 | ### Changed 19 | * Replaced deprecated \Monolog\Logger::Warning (https://github.com/justbetter/magento2-sentry/pull/214) thanks to https://github.com/GeKVe 20 | * use configured dsn-hostname for csp connect-src policy (https://github.com/justbetter/magento2-sentry/pull/215) thanks to https://github.com/rommelfreddy 21 | ## [4.4.0] - 2025-07-14 22 | ### Fixed 23 | * Catch zend db adapter errors when database is missing (https://github.com/justbetter/magento2-sentry/pull/193) 24 | * Fix logrocket key type 25 | ### Added 26 | * Add the ability to select job codes using regex (https://github.com/justbetter/magento2-sentry/pull/197) 27 | * Added support for [spotlight](https://spotlightjs.com/) (https://github.com/justbetter/magento2-sentry/pull/195) 28 | ## [4.3.0] - 2025-06-24 29 | ### Fixed 30 | * Catch The default website isnt defined when getting store (https://github.com/justbetter/magento2-sentry/pull/188) 31 | ### Added 32 | * Add sentry logger by hooking into monolog setHandlers (https://github.com/justbetter/magento2-sentry/pull/184) 33 | * Added support for cron check-ins (https://github.com/justbetter/magento2-sentry/pull/182) 34 | ## [4.2.0] - 2025-06-11 35 | ### Added 36 | * Add performance sampling by (https://github.com/justbetter/magento2-sentry/pull/178) thanks to https://github.com/indykoning, https://github.com/barryvdh and https://github.com/rommelfreddy 37 | * Automatically pass all supported sentry config (https://github.com/justbetter/magento2-sentry/pull/177) 38 | ## [4.1.0] - 2025-06-04 39 | ### Added 40 | * Clean up the Magento stacktrace before sending it to Sentry (https://github.com/justbetter/magento2-sentry/pull/171) 41 | * Pass config also relevant to sentry (https://github.com/justbetter/magento2-sentry/pull/172) 42 | ### Fixed 43 | * Gather ip from Magento instead of server variable (https://github.com/justbetter/magento2-sentry/pull/174) 44 | ## [4.0.1] - 2025-05-06 45 | ### Fixed 46 | * Load customerSession in SentryLog via proxy (Fixing https://github.com/justbetter/magento2-sentry/issues/160) (https://github.com/justbetter/magento2-sentry/pull/169) thanks to https://github.com/brosenberger 47 | ## [4.0.0] - 2025-04-23 48 | ### Added 49 | * Moved sentry logging to a monolog handler (https://github.com/justbetter/magento2-sentry/pull/165) 50 | * Added support for Magento 2.4.8 51 | * Added support for Monolog V3 52 | ### Changed 53 | * Dynamically add sentry and logrocket domains to CSP whitelist (https://github.com/justbetter/magento2-sentry/pull/156) thanks to https://github.com/brosenberger 54 | * Use secureRenderer for including sentry frontend js (https://github.com/justbetter/magento2-sentry/pull/157) thanks to https://github.com/brosenberger 55 | ## [3.9.0] - 2025-01-31 56 | ### Added 57 | * Raised PHPStan level to 5 (https://github.com/justbetter/magento2-sentry/pull/149) 58 | * Added user context to logging and errors when possible (https://github.com/justbetter/magento2-sentry/pull/133 & https://github.com/justbetter/magento2-sentry/pull/152) 59 | * Allow overriding some config per store (https://github.com/justbetter/magento2-sentry/pull/155) thanks to https://github.com/bruno-blackbird 60 | ## [3.8.0] - 2024-08-09 61 | ### Added 62 | * Added option to exclude default-integrations (https://github.com/justbetter/magento2-sentry/pull/146) thanks to https://github.com/rommelfreddy 63 | ### Removed 64 | * Removed promise-polyfill (https://github.com/justbetter/magento2-sentry/pull/142) thanks to https://github.com/rommelfreddy 65 | ## [3.7.2] - 2024-07-04 66 | ### Fixed 67 | * fix initialisation of replay integration (https://github.com/justbetter/magento2-sentry/pull/143) thanks to https://github.com/rommelfreddy 68 | ## [3.7.1] - 2024-06-25 69 | ### Fixed 70 | * Fix CSP after Polyfill changes (https://github.com/justbetter/magento2-sentry/pull/141) thanks to https://github.com/sprankhub 71 | ## [3.7.0] - 2024-06-25 72 | ### Fixed 73 | * Replace polyfill CDN with fastly (https://github.com/justbetter/magento2-sentry/pull/140) thanks to https://github.com/barryvdh 74 | ### Added 75 | * Enable INP when tracing is enabled (https://github.com/justbetter/magento2-sentry/pull/137) thanks to https://github.com/amenk 76 | ## [3.6.0] - 2024-03-29 77 | This release drops support for php 7.4 as it has been completely EOL for over a year. [https://www.php.net/supported-versions.php](https://www.php.net/supported-versions.php) 78 | ### Fixed 79 | * Check if Sentry is defined before running init (https://github.com/justbetter/magento2-sentry/pull/129) thanks to https://github.com/netzkollektiv 80 | ### Changed 81 | * Use property promotions (https://github.com/justbetter/magento2-sentry/pull/130) thanks to https://github.com/cirolosapio 82 | * Raised sentry/sdk version to 4.0+ 83 | ## [3.5.2] - 2024-03-18 84 | ### Fixed 85 | * Fix start errors without database connection (https://github.com/justbetter/magento2-sentry/pull/125) thanks to https://github.com/fredden 86 | ## [3.5.1] - 2023-12-21 87 | ### Fixed 88 | * Fix getting SCOPE_STORES from incorrect ScopeInterface (https://github.com/justbetter/magento2-sentry/pull/123) thanks to https://github.com/peterjaap 89 | * Improve extendibility by changing self to static (https://github.com/justbetter/magento2-sentry/pull/122) thanks to https://github.com/peterjaap 90 | ## [3.5.0] - 2023-12-20 91 | ### Added 92 | * Added support for configuration using System Configuration (https://github.com/justbetter/magento2-sentry/pull/121) thanks to https://github.com/ArjenMiedema 93 | ## [3.4.0] - 2023-06-14 94 | ### Added 95 | * Added `sentry_before_send` event (https://github.com/justbetter/magento2-sentry/pull/117) thanks to https://github.com/rbnmulder 96 | * Added support to ignore javascript errors (https://github.com/justbetter/magento2-sentry/pull/102) thanks to https://github.com/rommelfreddy 97 | ## [3.3.1] - 2023-03-15 98 | ### Fixed 99 | * Fixed PHP 8.2 deprecation errors (https://github.com/justbetter/magento2-sentry/pull/114) thanks to https://github.com/peterjaap 100 | ## [3.3.0] - 2023-03-01 101 | ### Added 102 | * Added Session Replay functionality 103 | ## [3.2.0] - 2022-07-07 104 | ### Fixed 105 | * Changed addAlert to addRecord for Test error (https://github.com/justbetter/magento2-sentry/pull/98) thanks to https://github.com/peterjaap 106 | ### Added 107 | * Send context data top Sentry as Custom Data (https://github.com/justbetter/magento2-sentry/pull/97) thanks to https://github.com/oneserv-heuser 108 | ## [3.1.0] - 2022-06-14 109 | ### Fixed 110 | * Update addRecord function for Monolog 2.7+ (https://github.com/justbetter/magento2-sentry/pull/92) thanks to https://github.com/torhoehn 111 | ## [3.0.2] - 2022-06-14 112 | ### Added 113 | * Added sentry_before_init event to GlobalExceptionCatcher (https://github.com/justbetter/magento2-sentry/pull/90) thanks to https://github.com/peterjaap 114 | ## [3.0.1] - 2022-05-10 115 | ### Fixed 116 | * Fix problems with logout during product edit in admin panel when Chrome DevTools is open (https://github.com/justbetter/magento2-sentry/pull/86) thanks to https://github.com/trungpq2711 117 | ## [3.0.0] - 2022-04-13 118 | Breaking: New version (2.0.0+) for Monolog will be required. 119 | ### Fixed 120 | * Fixed PHP 8.1 support with monolog 2.0.0+ (https://github.com/justbetter/magento2-sentry/pull/85) thanks to https://github.com/peterjaap 121 | ## [2.6.1] - 2022-04-13 122 | ### Changed 123 | * Updated constraints for monolog versions lower than 2.0.0 124 | ## [2.6.0] - 2021-04-08 125 | ### Added 126 | * Add option to filter the severity of ErrorExceptions being sent to Sentry 127 | * Add option to ignore specific Exception classes 128 | ## [2.5.4] - 2021-02-10 129 | ### Fixed 130 | * Fixed capital in dataHelper (https://github.com/justbetter/magento2-sentry/pull/74) thanks to https://github.com/peterjaap 131 | ## [2.5.3] - 2021-02-03 132 | ### Added 133 | * Add option to strip static content version and store code from urls that get sent to Sentry (https://github.com/justbetter/magento2-sentry/pull/73) thanks to https://github.com/peterjaap 134 | ## [2.5.2] - 2021-01-18 135 | ### Fixed 136 | * Fix monolog plugin still adding monolog as argument 137 | ## [2.5.1] - 2021-01-18 138 | ### Fixed 139 | * Merged PR - Use isset to check for custom_tags in context (https://github.com/justbetter/magento2-sentry/pull/71) thanks to https://github.com/matthiashamacher 140 | ## [2.5.0] - 2021-01-15 141 | ### Changed 142 | * Merged PR - Remove monolog class in function, add custom tag functionality (https://github.com/justbetter/magento2-sentry/pull/66) thanks to https://github.com/matthiashamacher 143 | This is a breaking change on SentryLog::send(), the function required monolog before, this has been removed 144 | ## [2.4.0] - 2020-12-07 145 | ### Added 146 | * Merged PR - Add csp_whitelist.xml (https://github.com/justbetter/magento2-sentry/pull/65) thanks to https://github.com/matthiashamacher 147 | ### Changed 148 | * Merged PR - Update Sentry version (https://github.com/justbetter/magento2-sentry/pull/64) thanks to https://github.com/matthiashamacher 149 | ## [2.3.2] - 2020-11-30 150 | ### Added 151 | * Merged PR - Adding environment to js integration (https://github.com/justbetter/magento2-sentry/pull/62) thanks to https://github.com/matthiashamacher 152 | ## [2.3.1] - 2020-07-31 153 | ### Added 154 | * Merged PR - Add option to enable or disable Php error tracking (https://github.com/justbetter/magento2-sentry/pull/59) thanks to https://github.com/t9toqwerty 155 | ## [2.3.0] - 2020-06-29 156 | ### Added 157 | * Merged PR - Added version tagging to JavaScript (https://github.com/justbetter/magento2-sentry/pull/54) thanks to https://github.com/JKetelaar 158 | * Added LogRocket support 159 | ### Changed 160 | * Better feedback if configuration prevents the module from running 161 | 162 | ## [2.2.3] - 2020-02-25 163 | ### Fixed 164 | * Merged PR - Release version should be string (https://github.com/justbetter/magento2-sentry/pull/49) thanks to https://github.com/DominicWatts 165 | 166 | ## [2.2.2] - 2020-02-11 167 | ### Changed 168 | * Merged PR - Change behavior to check of script tag can be used (https://github.com/justbetter/magento2-sentry/pull/48) thanks to https://github.com/adamj88 169 | * Updated changelog format to folow more closely to https://keepachangelog.com/ 170 | 171 | ## [2.2.1] - 2019-12-12 172 | ### Changed 173 | * Merged PR - fix for requirements sdk (https://github.com/justbetter/magento2-sentry/pull/42) thanks to https://github.com/jupiterhs 174 | 175 | ## [2.2.0] - 2019-12-12 176 | ### Changed 177 | * Merged PR (https://github.com/justbetter/magento2-sentry/pull/37) thanks to https://github.com/DominicWatts 178 | * Merged PR (https://github.com/justbetter/magento2-sentry/pull/38) thanks to https://github.com/matthiashamacher 179 | 180 | ## [2.1.0] - 2019-11-22 181 | ### Changed 182 | * Merged PR (https://github.com/justbetter/magento2-sentry/pull/33) thanks to https://github.com/DominicWatts 183 | * Merged PR (https://github.com/justbetter/magento2-sentry/pull/35) thanks to https://github.com/peterjaap 184 | * Merged PR (https://github.com/justbetter/magento2-sentry/pull/36) thanks to https://github.com/peterjaap 185 | * Refactor of proxy classes in di.xml 186 | ### Fixed 187 | * Fixed all PHPcs warnings and errors 188 | 189 | ## [2.0.0] - 2019-11-19 190 | ### Changed 191 | * Merged PR (https://github.com/justbetter/magento2-sentry/pull/29) thanks to https://github.com/michielgerritsen 192 | * Merged PR (https://github.com/justbetter/magento2-sentry/pull/26) thanks to https://github.com/JosephMaxwell 193 | * Merged PR (https://github.com/justbetter/magento2-sentry/pull/27) thanks to https://github.com/DominicWatts 194 | 195 | ## [0.8.0] - 2019-08-29 196 | ### Changed 197 | * Merged PR (https://github.com/justbetter/magento2-sentry/pull/24) thanks to https://github.com/kyriog 198 | * Merged PR (https://github.com/justbetter/magento2-sentry/pull/22) thanks to https://github.com/fredden 199 | * Merged PR (https://github.com/justbetter/magento2-sentry/pull/21) thanks to https://github.com/erikhansen 200 | 201 | ## [0.7.2] - 2019-06-19 202 | ### Changed 203 | * Reverted async attribute 204 | 205 | ## [0.7.1] - 2019-06-19 206 | ### Added 207 | * Added async and crossorigin attribute to script tags 208 | 209 | ## [0.7.0] - 2019-04-23 210 | ### Fixed 211 | * Merged pull request avoiding magento crash (https://github.com/justbetter/magento2-sentry/pull/19) 212 | 213 | ## [0.6.2] - 2019-04-05 214 | ### Fixed 215 | * Fixed issues useing referenceBlock when adding scripts (https://github.com/justbetter/magento2-sentry/issues/18) 216 | 217 | ## [0.6.1] - 2019-02-14 218 | ### Added 219 | * Added missing files of PR 13 (https://github.com/justbetter/magento2-sentry/pull/16) 220 | 221 | ## [0.6.0] - 2019-02-14 222 | ### Added 223 | * Added Sentry script tag to catch javascript errors (https://github.com/justbetter/magento2-sentry/pull/13) 224 | 225 | ## [0.5.1] - 2019-02-13 226 | ### Added 227 | * Support for setting environment (https://github.com/justbetter/magento2-sentry/pull/12) 228 | 229 | ## [0.5.0] - 2018-12-07 230 | ### Added 231 | * Send extra parameters to sentry (https://github.com/justbetter/magento2-sentry/issues/11) 232 | * Added Magento 2.3.x support & dropped 2.1.x support 233 | ### Fixed 234 | * Fixed area code not set or already set 235 | 236 | ## [0.4.2] - 2018-10-17 237 | ### Fixed 238 | * Bugfix with area code already set - removed area code from constructor with causes problems at random cases. (https://github.com/justbetter/magento2-sentry/issues/10) 239 | ### Changed 240 | * Removed info level - this log level is not useable in sentry. 241 | 242 | ## [0.4.1] - 2018-08-15 243 | ### Fixed 244 | * Removed plugin in di.xml to prevent fatal crash on storefrontend 245 | * Restored preference in di.xml 246 | 247 | ## [0.4.0] - 2018-08-10 248 | ### Added 249 | * Added user context 250 | * Added message context 251 | * Added extra magento store parameters 252 | ### Changed 253 | * Refactor of overwrite to plugin with monolog 254 | * Refactor of ExceptionCatcher to use same SentryLogger 255 | * Refactor a lot of code 256 | * PSR2 compliance 257 | ### Fixed 258 | * Bugfix area code not set 259 | 260 | ## [0.2.1] - 2018-04-30 261 | ### Changed 262 | * downgraded requirement monolog for magento 2.1.x 263 | 264 | ## [0.2.0] - 2018-04-30 265 | ### Added 266 | * Feature request to test sentry in development mode (https://github.com/justbetter/magento2-sentry/issues/4) 267 | * Added mage deploy mode to sentry in context_tags 268 | 269 | ## [0.1.0] - 2018-04-17 270 | ### Added 271 | * Feature request for sending test events (https://github.com/justbetter/magento2-sentry/issues/3) 272 | * Added ACL roles for editing sentry config 273 | 274 | ## [0.0.6] - 2018-03-25 275 | ### Fixed 276 | * Bugfix wrong file commit 277 | 278 | ## [0.0.5] - 2018-03-17 279 | ### Added 280 | * Initial commit module - basic working module 281 | -------------------------------------------------------------------------------- /Helper/Data.php: -------------------------------------------------------------------------------- 1 | ['type' => 'string'], 33 | 'release' => ['type' => 'string'], 34 | 'environment' => ['type' => 'string'], 35 | 'max_breadcrumbs' => ['type' => 'int'], 36 | 'attach_stacktrace' => ['type' => 'bool'], 37 | 'send_default_pii' => ['type' => 'bool'], 38 | 'server_name' => ['type' => 'string'], 39 | 'in_app_include' => ['type' => 'array'], 40 | 'in_app_exclude' => ['type' => 'array'], 41 | 'prefixes' => ['type' => 'array'], 42 | 'max_request_body_size' => ['type' => 'string'], 43 | 'max_value_length' => ['type' => 'int'], 44 | // https://docs.sentry.io/platforms/php/configuration/options/#error-monitoring-options 45 | 'sample_rate' => ['type' => 'float'], 46 | 'ignore_exceptions' => ['type' => 'array'], 47 | 'error_types' => ['type' => 'int'], 48 | 'context_lines' => ['type' => 'int'], 49 | // https://docs.sentry.io/platforms/php/configuration/options/#tracing-options 50 | 'traces_sample_rate' => ['type' => 'float'], 51 | 'ignore_transactions' => ['type' => 'array'], 52 | 'trace_propagation_targets' => ['type' => 'array'], 53 | // https://docs.sentry.io/platforms/php/profiling/#enabling-profiling 54 | 'profiles_sample_rate' => ['type' => 'float'], 55 | // https://docs.sentry.io/platforms/php/configuration/options/#logs-options 56 | 'enable_logs' => ['type' => 'bool'], 57 | // https://docs.sentry.io/platforms/php/configuration/options/#transport-options 58 | 'http_proxy' => ['type' => 'string'], 59 | 'http_connect_timeout' => ['type' => 'int'], 60 | 'http_timeout' => ['type' => 'int'], 61 | // https://spotlightjs.com/ 62 | 'spotlight' => ['type' => 'bool'], 63 | 'spotlight_url' => ['type' => 'string'], 64 | ]; 65 | 66 | /** 67 | * @var ScopeConfigInterface 68 | */ 69 | protected $scopeConfig; 70 | 71 | /** 72 | * @var array 73 | */ 74 | protected $config = []; 75 | 76 | /** 77 | * @var ?bool 78 | */ 79 | protected $isActive = null; 80 | 81 | /** 82 | * @var array 83 | */ 84 | protected $configKeys = [ 85 | ...self::NATIVE_SENTRY_CONFIG_KEYS, 86 | 'logrocket_key' => ['type' => 'string'], 87 | 'log_level' => ['type' => 'int'], 88 | 'logger_log_level' => ['type' => 'int', 'default' => 300 /* \Monolog\Level::Warning */], 89 | 'errorexception_reporting' => ['type' => 'int'], /* @deprecated by @see: error_types https://docs.sentry.io/platforms/php/configuration/options/#error_types */ 90 | 'mage_mode_development' => ['type' => 'bool'], 91 | 'js_sdk_version' => ['type' => 'string'], 92 | 'tracing_enabled' => ['type' => 'bool'], 93 | 'tracing_sample_rate' => ['type' => 'float'], /* @deprecated by @see: traces_sample_rate https://docs.sentry.io/platforms/php/configuration/options/#error_types */ 94 | 'traces_sample_rate_cli' => ['type' => 'float'], 95 | 'performance_tracking_enabled' => ['type' => 'bool'], 96 | 'performance_tracking_excluded_areas' => ['type' => 'array'], 97 | 'ignore_js_errors' => ['type' => 'array'], 98 | 'disable_default_integrations' => ['type' => 'array'], 99 | 'clean_stacktrace' => ['type' => 'bool'], 100 | 'cron_monitoring_enabled' => ['type' => 'bool'], 101 | 'track_crons' => ['type' => 'array'], 102 | 'enable_csp_report_url' => ['type' => 'bool'], 103 | ]; 104 | 105 | /** 106 | * Data constructor. 107 | * 108 | * @param Context $context 109 | * @param StoreManagerInterface $storeManager 110 | * @param State $appState 111 | * @param Json $serializer 112 | * @param ProductMetadataInterface $productMetadataInterface 113 | * @param DeploymentConfig $deploymentConfig 114 | */ 115 | public function __construct( 116 | Context $context, 117 | protected StoreManagerInterface $storeManager, 118 | protected State $appState, 119 | private Json $serializer, 120 | protected ProductMetadataInterface $productMetadataInterface, 121 | protected DeploymentConfig $deploymentConfig 122 | ) { 123 | $this->scopeConfig = $context->getScopeConfig(); 124 | $this->collectModuleConfig(); 125 | 126 | parent::__construct($context); 127 | } 128 | 129 | /** 130 | * Get the sentry DSN. 131 | * 132 | * @return mixed 133 | */ 134 | public function getDSN() 135 | { 136 | return $this->collectModuleConfig()['dsn']; 137 | } 138 | 139 | /** 140 | * Get the sentry release. 141 | * 142 | * @return string|null 143 | */ 144 | public function getRelease(): ?string 145 | { 146 | return $this->collectModuleConfig()['release']; 147 | } 148 | 149 | /** 150 | * Get the sentry prefixes. 151 | * 152 | * @return array 153 | */ 154 | public function getPrefixes(): array 155 | { 156 | $prefixes = $this->collectModuleConfig()['prefixes'] ?? []; 157 | if (defined('BP')) { 158 | $prefixes[] = BP; 159 | } 160 | 161 | return $prefixes; 162 | } 163 | 164 | /** 165 | * Whether cron monitoring (check-in events) is enabled. 166 | */ 167 | public function isCronMonitoringEnabled(): bool 168 | { 169 | return $this->collectModuleConfig()['cron_monitoring_enabled'] ?? false; 170 | } 171 | 172 | /** 173 | * Get a list of crons to track. 174 | */ 175 | public function getTrackCrons(): array 176 | { 177 | return $this->collectModuleConfig()['track_crons'] ?? []; 178 | } 179 | 180 | /** 181 | * Whether tracing is enabled. 182 | */ 183 | public function isTracingEnabled(): bool 184 | { 185 | return $this->collectModuleConfig()['tracing_enabled'] ?? false; 186 | } 187 | 188 | /** 189 | * Whether spotlight is enabled. 190 | * 191 | * Enabling spotlight will implicitly enable override production mode. 192 | */ 193 | public function isSpotlightEnabled(): bool 194 | { 195 | return ($this->collectModuleConfig()['spotlight'] ?? false) && !$this->isProductionMode(); 196 | } 197 | 198 | /** 199 | * Get sample rate for tracing. 200 | */ 201 | public function getTracingSampleRate(): float 202 | { 203 | return (float) ($this->collectModuleConfig()['traces_sample_rate'] ?? $this->collectModuleConfig()['tracing_sample_rate'] ?? 0.2); 204 | } 205 | 206 | /** 207 | * Get sample rate for tracing in CLI. 208 | */ 209 | public function getTracingSampleRateCli(): float 210 | { 211 | return (float) ($this->collectModuleConfig()['traces_sample_rate_cli'] ?? $this->getTracingSampleRate()); 212 | } 213 | 214 | /** 215 | * Get a list of integrations to disable. 216 | */ 217 | public function getDisabledDefaultIntegrations(): array 218 | { 219 | return $this->config['disable_default_integrations'] ?? []; 220 | } 221 | 222 | /** 223 | * Get list of js errors to ignore. 224 | * 225 | * @return array|null 226 | */ 227 | public function getIgnoreJsErrors() 228 | { 229 | return $this->collectModuleConfig()['ignore_js_errors']; 230 | } 231 | 232 | /** 233 | * Get the sdk version string. 234 | * 235 | * @return string the version of the js sdk of Sentry 236 | */ 237 | public function getJsSdkVersion(): string 238 | { 239 | return $this->collectModuleConfig()['js_sdk_version'] ?: SentryScript::CURRENT_VERSION; 240 | } 241 | 242 | /** 243 | * Get the current environment. 244 | * 245 | * @return mixed 246 | */ 247 | public function getEnvironment() 248 | { 249 | return $this->collectModuleConfig()['environment'] ?? 'default'; 250 | } 251 | 252 | /** 253 | * Retrieve config values. 254 | * 255 | * @param string $field 256 | * @param int|string|null $storeId 257 | * 258 | * @return mixed 259 | */ 260 | public function getConfigValue($field, $storeId = null) 261 | { 262 | return $this->scopeConfig->getValue( 263 | $field, 264 | ScopeInterface::SCOPE_STORE, 265 | $storeId 266 | ); 267 | } 268 | 269 | /** 270 | * Retrieve Sentry General config values. 271 | * 272 | * @param string $code 273 | * @param null $storeId 274 | * 275 | * @return mixed 276 | */ 277 | public function getGeneralConfig($code, $storeId = null) 278 | { 279 | return $this->getConfigValue(static::XML_PATH_SRS.$code, $storeId); 280 | } 281 | 282 | /** 283 | * Get the store id of the current store. 284 | * 285 | * @return int 286 | */ 287 | public function getStoreId(): int 288 | { 289 | return $this->getStore()?->getId() ?? 0; 290 | } 291 | 292 | /** 293 | * Gather all configuration. 294 | * 295 | * @return array 296 | */ 297 | public function collectModuleConfig(): array 298 | { 299 | $storeId = $this->getStoreId(); 300 | if (isset($this->config[$storeId]['enabled'])) { 301 | return $this->config[$storeId]; 302 | } 303 | 304 | try { 305 | $this->config[$storeId]['enabled'] = $this->scopeConfig->getValue('sentry/environment/enabled', ScopeInterface::SCOPE_STORE) 306 | ?? $this->deploymentConfig->get('sentry') !== null; 307 | } catch (TableNotFoundException|FileSystemException|RuntimeException|DomainException|Zend_Db_Adapter_Exception $e) { 308 | $this->config[$storeId]['enabled'] = $this->deploymentConfig->get('sentry') !== null; 309 | } 310 | 311 | foreach ($this->configKeys as $key => $config) { 312 | try { 313 | $value = $this->scopeConfig->getValue('sentry/environment/'.$key, ScopeInterface::SCOPE_STORE) 314 | ?? $this->deploymentConfig->get('sentry/'.$key); 315 | } catch (TableNotFoundException|FileSystemException|RuntimeException|DomainException|Zend_Db_Adapter_Exception $e) { 316 | $value = $this->deploymentConfig->get('sentry/'.$key); 317 | } 318 | 319 | $this->config[$storeId][$key] = $this->processConfigValue( 320 | $value, 321 | $config 322 | ); 323 | } 324 | 325 | return $this->config[$storeId]; 326 | } 327 | 328 | /** 329 | * Parse the config value to the type defined in the config. 330 | * 331 | * @param mixed $value 332 | * @param array $config 333 | * 334 | * @return mixed 335 | */ 336 | public function processConfigValue(mixed $value, array $config): mixed 337 | { 338 | if ($value === null) { 339 | return $config['default'] ?? null; 340 | } 341 | 342 | return match ($config['type']) { 343 | 'array' => is_array($value) ? $value : $this->serializer->unserialize($value), 344 | 'int' => (int) $value, 345 | 'float' => (float) $value, 346 | 'bool' => (bool) $value, 347 | 'string' => (string) $value, 348 | default => $value, 349 | }; 350 | } 351 | 352 | /** 353 | * Whether Sentry is active. 354 | * 355 | * @return bool 356 | */ 357 | public function isActive(): bool 358 | { 359 | return $this->isActive ??= $this->isActiveWithReason()['active']; 360 | } 361 | 362 | /** 363 | * Whether sentry is active, adding a reason why not. 364 | * 365 | * @return array 366 | */ 367 | public function isActiveWithReason(): array 368 | { 369 | $reasons = []; 370 | $config = $this->collectModuleConfig(); 371 | $emptyConfig = empty($config); 372 | $configEnabled = isset($config['enabled']) && $config['enabled']; 373 | $dsn = $this->getDSN(); 374 | $productionMode = ($this->isProductionMode() || $this->isOverwriteProductionMode()); 375 | 376 | if ($emptyConfig) { 377 | $reasons[] = __('Config is empty.'); 378 | } 379 | if (!$configEnabled) { 380 | $reasons[] = __('Module is not enabled in config.'); 381 | } 382 | if (!$dsn && !$this->isSpotlightEnabled()) { 383 | $reasons[] = __('DSN is empty.'); 384 | } 385 | if ($dsn) { 386 | try { 387 | \Sentry\Dsn::createFromString($dsn); 388 | } catch (\InvalidArgumentException $e) { 389 | $reasons[] = $e->getMessage(); 390 | } 391 | } 392 | if (!$productionMode) { 393 | $reasons[] = __('Not in production and development mode is false.'); 394 | } 395 | 396 | return count($reasons) > 0 ? ['active' => false, 'reasons' => $reasons] : ['active' => true]; 397 | } 398 | 399 | /** 400 | * Whether the application is in production. 401 | * 402 | * @return bool 403 | */ 404 | public function isProductionMode(): bool 405 | { 406 | return $this->appState->emulateAreaCode(Area::AREA_GLOBAL, [$this, 'getAppState']) === 'production'; 407 | } 408 | 409 | /** 410 | * Get the current mode. 411 | * 412 | * @return string 413 | */ 414 | public function getAppState(): string 415 | { 416 | return $this->appState->getMode(); 417 | } 418 | 419 | /** 420 | * Is sentry enabled on development mode? 421 | * 422 | * @return bool 423 | */ 424 | public function isOverwriteProductionMode(): bool 425 | { 426 | $config = $this->collectModuleConfig(); 427 | 428 | return (isset($config['mage_mode_development']) && $config['mage_mode_development']) || $this->isSpotlightEnabled(); 429 | } 430 | 431 | /** 432 | * Get the current magento version. 433 | * 434 | * @return string 435 | */ 436 | public function getMagentoVersion(): string 437 | { 438 | return $this->productMetadataInterface->getVersion(); 439 | } 440 | 441 | /** 442 | * Get the current store. 443 | */ 444 | public function getStore(): ?\Magento\Store\Api\Data\StoreInterface 445 | { 446 | try { 447 | return $this->storeManager->getStore(); 448 | } catch (DomainException|Zend_Db_Adapter_Exception|NoSuchEntityException $e) { 449 | // If the store is not available, return null 450 | return null; 451 | } 452 | } 453 | 454 | /** 455 | * Should the stacktrace get cleaned up? 456 | * 457 | * @return bool 458 | */ 459 | public function getCleanStacktrace(): bool 460 | { 461 | return $this->collectModuleConfig()['clean_stacktrace'] ?? true; 462 | } 463 | 464 | /** 465 | * Is php tracking enabled? 466 | * 467 | * @return bool 468 | */ 469 | public function isPhpTrackingEnabled(): bool 470 | { 471 | return $this->scopeConfig->isSetFlag(static::XML_PATH_SRS.'enable_php_tracking', ScopeInterface::SCOPE_STORE); 472 | } 473 | 474 | /** 475 | * Is php performance tracking enabled? 476 | * 477 | * @return bool 478 | */ 479 | public function isPerformanceTrackingEnabled(): bool 480 | { 481 | return $this->isTracingEnabled() && ($this->collectModuleConfig()['performance_tracking_enabled'] ?? false); 482 | } 483 | 484 | /** 485 | * Get excluded Magento areas which should be not profiled. 486 | * 487 | * @return string[] 488 | */ 489 | public function getPerformanceTrackingExcludedAreas(): array 490 | { 491 | return $this->collectModuleConfig()['performance_tracking_excluded_areas'] ?? [Area::AREA_ADMINHTML, Area::AREA_CRONTAB]; 492 | } 493 | 494 | /** 495 | * Is the script tag enabled? 496 | * 497 | * @return bool 498 | */ 499 | public function useScriptTag() 500 | { 501 | return $this->scopeConfig->isSetFlag(static::XML_PATH_SRS.'enable_script_tag', ScopeInterface::SCOPE_STORE); 502 | } 503 | 504 | /** 505 | * Whether to enable session replay. 506 | */ 507 | public function useSessionReplay(): bool 508 | { 509 | return $this->scopeConfig->isSetFlag(static::XML_PATH_SRS.'enable_session_replay', ScopeInterface::SCOPE_STORE); 510 | } 511 | 512 | /** 513 | * Get the session replay sample rate. 514 | */ 515 | public function getReplaySessionSampleRate(): float 516 | { 517 | return $this->getConfigValue(static::XML_PATH_SRS.'replay_session_sample_rate') ?? 0.1; 518 | } 519 | 520 | /** 521 | * Get the session replay error sample rate. 522 | */ 523 | public function getReplayErrorSampleRate(): float 524 | { 525 | return $this->getConfigValue(static::XML_PATH_SRS.'replay_error_sample_rate') ?? 1; 526 | } 527 | 528 | /** 529 | * Whether to block media during replay. 530 | */ 531 | public function getReplayBlockMedia(): bool 532 | { 533 | return $this->getConfigValue(static::XML_PATH_SRS.'replay_block_media') ?? true; 534 | } 535 | 536 | /** 537 | * Whether to show mask text. 538 | */ 539 | public function getReplayMaskText(): bool 540 | { 541 | return $this->getConfigValue(static::XML_PATH_SRS.'replay_mask_text') ?? true; 542 | } 543 | 544 | /** 545 | * Should we load the script tag in the current block? 546 | * 547 | * @param string $blockName 548 | * 549 | * @return bool 550 | */ 551 | public function showScriptTagInThisBlock($blockName): bool 552 | { 553 | $config = $this->getGeneralConfig('script_tag_placement'); 554 | 555 | if (!$config) { 556 | return false; 557 | } 558 | 559 | $name = 'sentry.'.$config; 560 | 561 | return $name === $blockName; 562 | } 563 | 564 | /** 565 | * Get logrocket key. 566 | * 567 | * @return mixed 568 | */ 569 | public function getLogrocketKey() 570 | { 571 | return $this->collectModuleConfig()['logrocket_key']; 572 | } 573 | 574 | /** 575 | * Whether to use logrocket. 576 | * 577 | * @return bool 578 | */ 579 | public function useLogrocket(): bool 580 | { 581 | return $this->scopeConfig->isSetFlag(static::XML_PATH_SRS.'use_logrocket') && 582 | isset($this->collectModuleConfig()['logrocket_key']) && 583 | $this->getLogrocketKey() !== null; 584 | } 585 | 586 | /** 587 | * Should logrocket identify. 588 | * 589 | * @return bool 590 | */ 591 | public function useLogrocketIdentify(): bool 592 | { 593 | return $this->scopeConfig->isSetFlag( 594 | static::XML_PATH_SRS.'logrocket_identify', 595 | ScopeInterface::SCOPE_STORE 596 | ); 597 | } 598 | 599 | /** 600 | * Whether to remove static content versioning from the url sent to sentry. 601 | * 602 | * @return bool 603 | */ 604 | public function stripStaticContentVersion(): bool 605 | { 606 | return $this->scopeConfig->isSetFlag( 607 | static::XML_PATH_SRS_ISSUE_GROUPING.'strip_static_content_version', 608 | ScopeInterface::SCOPE_STORE 609 | ); 610 | } 611 | 612 | /** 613 | * Whether to remove store code from the url sent to sentry. 614 | * 615 | * @return bool 616 | */ 617 | public function stripStoreCode(): bool 618 | { 619 | return $this->scopeConfig->isSetFlag( 620 | static::XML_PATH_SRS_ISSUE_GROUPING.'strip_store_code', 621 | ScopeInterface::SCOPE_STORE 622 | ); 623 | } 624 | 625 | /** 626 | * Get the ErrorException reporting level we will send at. 627 | * 628 | * @return int 629 | */ 630 | public function getErrorTypes(): int 631 | { 632 | return (int) ($this->collectModuleConfig()['error_types'] ?? $this->collectModuleConfig()['errorexception_reporting'] ?? error_reporting()); 633 | } 634 | 635 | /** 636 | * Get a list of exceptions we should never send to Sentry. 637 | * 638 | * @return array 639 | */ 640 | public function getIgnoreExceptions(): array 641 | { 642 | return $this->collectModuleConfig()['ignore_exceptions'] ?? []; 643 | } 644 | 645 | /** 646 | * Whether the report-uri directive in the CSP is enabled. 647 | * 648 | * @return bool 649 | */ 650 | public function isEnableCspReportUrl(): bool 651 | { 652 | return $this->collectModuleConfig()['enable_csp_report_url'] ?? false; 653 | } 654 | 655 | /** 656 | * Check whether we should capture the given exception based on severity and ignore exceptions. 657 | * 658 | * @param Throwable $ex 659 | * 660 | * @return bool 661 | */ 662 | public function shouldCaptureException(Throwable $ex): bool 663 | { 664 | if ($ex instanceof ErrorException && !($ex->getSeverity() & $this->getErrorTypes())) { 665 | return false; 666 | } 667 | 668 | if (in_array(get_class($ex), $this->getIgnoreExceptions())) { 669 | return false; 670 | } 671 | 672 | if ($ex->getPrevious()) { 673 | return $this->shouldCaptureException($ex->getPrevious()); 674 | } 675 | 676 | return true; 677 | } 678 | } 679 | --------------------------------------------------------------------------------