diff --git a/CHANGELOG.md b/CHANGELOG.md index cb03294..748541f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Added + +- Add a `Sentry` class to easily configure Sentry integration for both front-end and back-end ([#29](https://github.com/studiometa/wp-toolkit/pull/29)) + ## v2.0.1 - 2024.03.09 ### Fixed diff --git a/composer.json b/composer.json index 4525fbd..73f5073 100644 --- a/composer.json +++ b/composer.json @@ -7,6 +7,7 @@ "php": "^8.1", "monolog/monolog": "^2.9|^3.0", "psr/log": "^1.1", + "sentry/sentry": "^4.6", "studiometa/webpack-config": "^5.0", "symfony/http-foundation": "^6.4|^7.0", "symfony/yaml": "^6.4|^7.0", diff --git a/composer.lock b/composer.lock index 5ad034e..379e41f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4ae9d81b385c475f971ba5f532cf6190", + "content-hash": "83e8d763f067b8b9f1332f4fb1a9bcd4", "packages": [ { "name": "anahkiasen/html-object", @@ -52,6 +52,181 @@ }, "time": "2017-05-31T07:52:45+00:00" }, + { + "name": "guzzlehttp/psr7", + "version": "2.6.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/45b30f99ac27b5ca93cb4831afe16285f57b8221", + "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "^0.9", + "phpunit/phpunit": "^8.5.36 || ^9.6.15" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.6.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2023-12-03T20:05:35+00:00" + }, + { + "name": "jean85/pretty-package-versions", + "version": "2.0.6", + "source": { + "type": "git", + "url": "https://github.com/Jean85/pretty-package-versions.git", + "reference": "f9fdd29ad8e6d024f52678b570e5593759b550b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/f9fdd29ad8e6d024f52678b570e5593759b550b4", + "reference": "f9fdd29ad8e6d024f52678b570e5593759b550b4", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.0.0", + "php": "^7.1|^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "jean85/composer-provided-replaced-stub-package": "^1.0", + "phpstan/phpstan": "^1.4", + "phpunit/phpunit": "^7.5|^8.5|^9.4", + "vimeo/psalm": "^4.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Jean85\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alessandro Lai", + "email": "alessandro.lai85@gmail.com" + } + ], + "description": "A library to get pretty versions strings of installed dependencies", + "keywords": [ + "composer", + "package", + "release", + "versions" + ], + "support": { + "issues": "https://github.com/Jean85/pretty-package-versions/issues", + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.0.6" + }, + "time": "2024-03-08T09:58:59+00:00" + }, { "name": "monolog/monolog", "version": "2.9.1", @@ -154,6 +329,114 @@ ], "time": "2023-02-06T13:44:46+00:00" }, + { + "name": "psr/http-factory", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "e616d01114759c4c489f93b099585439f795fe35" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35", + "reference": "e616d01114759c4c489f93b099585439f795fe35", + "shasum": "" + }, + "require": { + "php": ">=7.0.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory/tree/1.0.2" + }, + "time": "2023-04-10T20:10:41+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, { "name": "psr/log", "version": "1.1.4", @@ -204,6 +487,139 @@ }, "time": "2021-05-03T11:20:27+00:00" }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "sentry/sentry", + "version": "4.6.1", + "source": { + "type": "git", + "url": "https://github.com/getsentry/sentry-php.git", + "reference": "5a94184175e5830b589bf923da8c9c3af2c0f409" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/5a94184175e5830b589bf923da8c9c3af2c0f409", + "reference": "5a94184175e5830b589bf923da8c9c3af2c0f409", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "guzzlehttp/psr7": "^1.8.4|^2.1.1", + "jean85/pretty-package-versions": "^1.5|^2.0.4", + "php": "^7.2|^8.0", + "psr/log": "^1.0|^2.0|^3.0", + "symfony/options-resolver": "^4.4.30|^5.0.11|^6.0|^7.0" + }, + "conflict": { + "raven/raven": "*" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.4", + "guzzlehttp/promises": "^1.0|^2.0", + "guzzlehttp/psr7": "^1.8.4|^2.1.1", + "monolog/monolog": "^1.6|^2.0|^3.0", + "phpbench/phpbench": "^1.0", + "phpstan/phpstan": "^1.3", + "phpunit/phpunit": "^8.5.14|^9.4", + "symfony/phpunit-bridge": "^5.2|^6.0|^7.0", + "vimeo/psalm": "^4.17" + }, + "suggest": { + "monolog/monolog": "Allow sending log messages to Sentry by using the included Monolog handler." + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Sentry\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sentry", + "email": "accounts@sentry.io" + } + ], + "description": "PHP SDK for Sentry (http://sentry.io)", + "homepage": "http://sentry.io", + "keywords": [ + "crash-reporting", + "crash-reports", + "error-handler", + "error-monitoring", + "log", + "logging", + "profiling", + "sentry", + "tracing" + ], + "support": { + "issues": "https://github.com/getsentry/sentry-php/issues", + "source": "https://github.com/getsentry/sentry-php/tree/4.6.1" + }, + "funding": [ + { + "url": "https://sentry.io/", + "type": "custom" + }, + { + "url": "https://sentry.io/pricing/", + "type": "custom" + } + ], + "time": "2024-03-08T08:18:09+00:00" + }, { "name": "studiometa/webpack-config", "version": "5.3.0", @@ -395,6 +811,73 @@ ], "time": "2024-02-08T15:01:18+00:00" }, + { + "name": "symfony/options-resolver", + "version": "v6.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "22301f0e7fdeaacc14318928612dee79be99860e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/22301f0e7fdeaacc14318928612dee79be99860e", + "reference": "22301f0e7fdeaacc14318928612dee79be99860e", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v6.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-08-08T10:16:24+00:00" + }, { "name": "symfony/polyfill-ctype", "version": "v1.29.0", diff --git a/src/Sentry/Config.php b/src/Sentry/Config.php new file mode 100644 index 0000000..6e92f21 --- /dev/null +++ b/src/Sentry/Config.php @@ -0,0 +1,34 @@ + $this->dsn, + 'environment' => $this->environment, + 'release' => $this->release, + 'traces_sample_rate' => $this->traces_sample_rate, + 'profiles_sample_rate' => $this->profiles_sample_rate, + ]; + } + + public function getJsConfig():string|false + { + $config = $this->toArray(); + unset($config['dsn']); + return json_encode($config); + } +} diff --git a/src/Sentry/Sentry.php b/src/Sentry/Sentry.php new file mode 100644 index 0000000..9ab87d5 --- /dev/null +++ b/src/Sentry/Sentry.php @@ -0,0 +1,89 @@ +name . '@' . $package->version; + } + } + + $sample_rate = (float) (env('SENTRY_SAMPLE_RATE') ?: 0); + $traces_sample_rate = (float) (env('SENTRY_TRACES_SAMPLE_RATE') ?: $sample_rate); + $profiles_sample_rate = (float) (env('SENTRY_PROFILES_SAMPLE_RATE') ?: $sample_rate); + + self::configure( + new Config( + dsn: env('SENTRY_DSN'), + js_loader_script: env('SENTRY_JS_LOADER_SCRIPT'), + environment: env('SENTRY_ENV') ?: env('APP_ENV'), + release: $release, + traces_sample_rate: $traces_sample_rate, + profiles_sample_rate: $profiles_sample_rate, + ), + ); + } + + private static function configure(Config $config): void + { + init_sentry($config->toArray()); + + early_add_action('init', function () use ($config) { + wp_enqueue_script('sentry-loader-script', $config->js_loader_script, []); + $js_config = $config->getJsConfig(); + $inline_script = "window.sentryOnLoad = () => { Sentry.init({$js_config}) }"; + wp_add_inline_script('sentry-loader-script', $inline_script, 'before'); + }); + + if ($config->traces_sample_rate > 0) { + // Setup Sentry performance + profiling + // Setup context for the full transaction + $transactionContext = new \Sentry\Tracing\TransactionContext(); + /** @var string */ + $transactionName = request()->server->get('REQUEST_URI', 'wp-cli'); + $transactionContext->setName($transactionName); + $transactionContext->setOp('http.server'); + + // Start the transaction + $transaction = \Sentry\startTransaction($transactionContext); + + // Set the current transaction as the current span so we can retrieve it later + \Sentry\SentrySdk::getCurrentHub()->setSpan($transaction); + + // Setup the context for the expensive operation span + $spanContext = new \Sentry\Tracing\SpanContext(); + $spanContext->setOp('wordpress'); + + // Start the span + $span = $transaction->startChild($spanContext); + + // Set the current span to the span we just started + \Sentry\SentrySdk::getCurrentHub()->setSpan($span); + + register_shutdown_function(function () use ($span, $transaction) { + // Finish the span + $span->finish(); + // Set the current span back to the transaction since we just finished the previous span + \Sentry\SentrySdk::getCurrentHub()->setSpan($transaction); + // Finish the transaction, this submits the transaction and it's span to Sentry + $transaction->finish(); + }); + } + } +} diff --git a/tests/Sentry/ConfigTest.php b/tests/Sentry/ConfigTest.php new file mode 100644 index 0000000..4805105 --- /dev/null +++ b/tests/Sentry/ConfigTest.php @@ -0,0 +1,46 @@ + 'dsn', + 'js_loader_script' => 'js_loader_script', + 'environment' => 'environment', + 'release' => 'release', + 'traces_sample_rate' => 0.0, + 'profiles_sample_rate' => 0.0, + ]; + + public function config() + { + return new Config( + dsn: $this->values['dsn'], + js_loader_script: $this->values['js_loader_script'], + environment: $this->values['environment'], + release: $this->values['release'], + traces_sample_rate: $this->values['traces_sample_rate'], + profiles_sample_rate: $this->values['profiles_sample_rate'], + ); + } + + public function test_it_has_a_working_to_array_method() + { + $expected = $this->values; + unset($expected['js_loader_script']); + $this->assertEqualsCanonicalizing($expected, $this->config()->toArray()); + } + + public function test_it_has_a_js_config_method() + { + $expected = $this->values; + unset($expected['js_loader_script']); + unset($expected['dsn']); + $js_config = json_decode($this->config()->getJsConfig(), true); + $this->assertEqualsCanonicalizing($expected, $js_config); + } +}