Skip to content

Commit d43b279

Browse files
authored
Improve output and DX for sentry:test and sentry:publish commands (#522)
1 parent e95bac5 commit d43b279

File tree

6 files changed

+377
-239
lines changed

6 files changed

+377
-239
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- Improve output and DX for `sentry:test` and `sentry:publish` commands (#522)
6+
57
## 2.9.0
68

79
- Add support for Laravel Octane (#495)
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
<?php
2+
3+
namespace Sentry\Laravel\Console;
4+
5+
use Exception;
6+
use Illuminate\Console\Command;
7+
use Illuminate\Support\Str;
8+
use Sentry\Dsn;
9+
use Sentry\Laravel\ServiceProvider;
10+
11+
class PublishCommand extends Command
12+
{
13+
/**
14+
* Laravel 5.0.x: The name and signature of the console command.
15+
*
16+
* @var string
17+
*/
18+
protected $name = 'sentry:publish';
19+
20+
/**
21+
* The name and signature of the console command.
22+
*
23+
* @var string
24+
*/
25+
protected $signature = 'sentry:publish {--dsn=} {--without-performance-monitoring} {--without-test}';
26+
27+
/**
28+
* The console command description.
29+
*
30+
* @var string
31+
*/
32+
protected $description = 'Publishes and configures the Sentry config.';
33+
34+
/**
35+
* Execute the console command.
36+
*
37+
* @return int
38+
*/
39+
public function handle(): int
40+
{
41+
$arg = [];
42+
$env = [];
43+
44+
$dsn = $this->option('dsn');
45+
46+
if (!empty($dsn) || !$this->isEnvKeySet('SENTRY_LARAVEL_DSN')) {
47+
if (empty($dsn)) {
48+
$dsnFromInput = $this->askForDsnInput();
49+
50+
if (empty($dsnFromInput)) {
51+
$this->error('Please provide a valid DSN using the `--dsn` option or setting `SENTRY_LARAVEL_DSN` in your `.env` file!');
52+
53+
return self::FAILURE;
54+
}
55+
56+
$dsn = $dsnFromInput;
57+
}
58+
59+
$env['SENTRY_LARAVEL_DSN'] = $dsn;
60+
$arg['--dsn'] = $dsn;
61+
}
62+
63+
$testCommandPrompt = 'Want to send a test event?';
64+
65+
if ($this->confirm('Enable Performance Monitoring?', !$this->option('without-performance-monitoring'))) {
66+
$testCommandPrompt = 'Want to send a test event & transaction?';
67+
68+
$env['SENTRY_TRACES_SAMPLE_RATE'] = '1.0';
69+
70+
$arg['--transaction'] = true;
71+
} elseif ($this->isEnvKeySet('SENTRY_TRACES_SAMPLE_RATE')) {
72+
$env['SENTRY_TRACES_SAMPLE_RATE'] = '0';
73+
}
74+
75+
if ($this->confirm($testCommandPrompt, !$this->option('without-test'))) {
76+
$testResult = $this->call('sentry:test', $arg);
77+
78+
if ($testResult === self::FAILURE) {
79+
return self::FAILURE;
80+
}
81+
}
82+
83+
$this->info('Publishing Sentry config...');
84+
$this->call('vendor:publish', ['--provider' => ServiceProvider::class]);
85+
86+
if (!$this->setEnvValues($env)) {
87+
return self::FAILURE;
88+
}
89+
90+
return self::SUCCESS;
91+
}
92+
93+
private function setEnvValues(array $values): bool
94+
{
95+
$envFilePath = app()->environmentFilePath();
96+
97+
$envFileContents = file_get_contents($envFilePath);
98+
99+
if (!$envFileContents) {
100+
$this->error('Could not read `.env` file!');
101+
102+
return false;
103+
}
104+
105+
if (count($values) > 0) {
106+
foreach ($values as $envKey => $envValue) {
107+
if ($this->isEnvKeySet($envKey, $envFileContents)) {
108+
$envFileContents = preg_replace("/^{$envKey}=.*?[\s$]/m", "{$envKey}={$envValue}\n", $envFileContents);
109+
110+
$this->info("Updated {$envKey} with new value in your `.env` file.");
111+
} else {
112+
$envFileContents .= "{$envKey}={$envValue}\n";
113+
114+
$this->info("Added {$envKey} to your `.env` file.");
115+
}
116+
}
117+
}
118+
119+
if (!file_put_contents($envFilePath, $envFileContents)) {
120+
$this->error('Updating the `.env` file failed!');
121+
122+
return false;
123+
}
124+
125+
return true;
126+
}
127+
128+
private function isEnvKeySet(string $envKey, ?string $envFileContents = null): bool
129+
{
130+
$envFileContents = $envFileContents ?? file_get_contents(app()->environmentFilePath());
131+
132+
return (bool)preg_match("/^{$envKey}=.*?[\s$]/m", $envFileContents);
133+
}
134+
135+
private function askForDsnInput(): string
136+
{
137+
if ($this->option('no-interaction')) {
138+
return '';
139+
}
140+
141+
while (true) {
142+
$this->info('');
143+
144+
$this->question('Please paste the DSN here');
145+
146+
$dsn = $this->ask('DSN');
147+
148+
// In case someone copies it with SENTRY_LARAVEL_DSN= or SENTRY_DSN=
149+
$dsn = Str::after($dsn, '=');
150+
151+
try {
152+
Dsn::createFromString($dsn);
153+
154+
return $dsn;
155+
} catch (Exception $e) {
156+
// Not a valid DSN do it again
157+
$this->error('The DSN is not valid, please make sure to paste a valid DSN!');
158+
}
159+
}
160+
}
161+
}
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
<?php
2+
3+
namespace Sentry\Laravel\Console;
4+
5+
use Exception;
6+
use Illuminate\Console\Command;
7+
use Illuminate\Support\Str;
8+
use Psr\Log\AbstractLogger;
9+
use Sentry\ClientBuilder;
10+
use Sentry\State\Hub;
11+
use Sentry\State\HubInterface;
12+
use Sentry\Tracing\SpanContext;
13+
use Sentry\Tracing\TransactionContext;
14+
15+
class TestCommand extends Command
16+
{
17+
/**
18+
* Laravel 5.0.x: The name and signature of the console command.
19+
*
20+
* @var string
21+
*/
22+
protected $name = 'sentry:test';
23+
24+
/**
25+
* The name and signature of the console command.
26+
*
27+
* @var string
28+
*/
29+
protected $signature = 'sentry:test {--transaction} {--dsn=}';
30+
31+
/**
32+
* The console command description.
33+
*
34+
* @var string
35+
*/
36+
protected $description = 'Generate a test event and send it to Sentry';
37+
38+
/**
39+
* Buffer of log messages generated by the Sentry SDK.
40+
*
41+
* @var array
42+
*/
43+
private $logMessages = [];
44+
45+
/**
46+
* Execute the console command.
47+
*
48+
* @return int
49+
*/
50+
public function handle(): int
51+
{
52+
// Maximize error reporting
53+
$old_error_reporting = error_reporting(E_ALL | E_STRICT);
54+
55+
$dsn = $this->option('dsn');
56+
57+
// If the DSN was not passed as option to the command we use the registered client to get the DSN from the Laravel config
58+
if ($dsn === null) {
59+
$dsnObject = app(HubInterface::class)->getClient()->getOptions()->getDsn();
60+
61+
if ($dsnObject !== null) {
62+
$dsn = (string)$dsnObject;
63+
64+
$this->info('DSN discovered from Laravel config or `.env` file!');
65+
}
66+
}
67+
68+
// No DSN found from the command line or config
69+
if (!$dsn) {
70+
$this->error('Could not discover DSN!');
71+
72+
$this->printDebugTips();
73+
74+
return self::FAILURE;
75+
}
76+
77+
try {
78+
$clientBuilder = ClientBuilder::create([
79+
'dsn' => $dsn,
80+
'traces_sample_rate' => 1.0,
81+
]);
82+
} catch (Exception $e) {
83+
$this->error($e->getMessage());
84+
85+
return self::FAILURE;
86+
}
87+
88+
// We set a logger so we can surface errors thrown internally by the SDK
89+
$clientBuilder->setLogger(new class($this) extends AbstractLogger {
90+
private $command;
91+
92+
public function __construct(TestCommand $command)
93+
{
94+
$this->command = $command;
95+
}
96+
97+
public function log($level, $message, array $context = []): void
98+
{
99+
if ($level === 'error') {
100+
$this->command->logMessageFromSDK($message);
101+
}
102+
}
103+
});
104+
105+
$hub = new Hub($clientBuilder->getClient());
106+
107+
$this->info('Sending test event...');
108+
109+
$exception = $this->generateTestException($this->name, ['foo' => 'bar']);
110+
111+
$eventId = $hub->captureException($exception);
112+
113+
if (!$eventId) {
114+
$this->error('There was an error sending the event.');
115+
116+
$this->printDebugTips();
117+
118+
return self::FAILURE;
119+
}
120+
121+
$this->info("Test event sent with ID: {$eventId}");
122+
123+
if ($this->option('transaction')) {
124+
$this->clearLogMessagesFromSDK();
125+
126+
$transactionContext = new TransactionContext();
127+
$transactionContext->setSampled(true);
128+
$transactionContext->setName('Sentry Test Transaction');
129+
$transactionContext->setOp('sentry.test');
130+
131+
$transaction = $hub->startTransaction($transactionContext);
132+
133+
$spanContext = new SpanContext();
134+
$spanContext->setOp('sentry.sent');
135+
136+
$span = $transaction->startChild($spanContext);
137+
138+
$this->info('Sending transaction...');
139+
140+
$span->finish();
141+
$transactionId = $transaction->finish();
142+
143+
if (!$transactionId) {
144+
$this->error('There was an error sending the transaction.');
145+
146+
$this->printDebugTips();
147+
148+
return self::FAILURE;
149+
}
150+
151+
$this->info("Transaction sent with ID: {$transactionId}");
152+
}
153+
154+
error_reporting($old_error_reporting);
155+
156+
return self::SUCCESS;
157+
}
158+
159+
/**
160+
* Generate a test exception to send to Sentry.
161+
*
162+
* @param $command
163+
* @param $arg
164+
*
165+
* @return \Exception
166+
*/
167+
protected function generateTestException($command, $arg): Exception
168+
{
169+
// Do something silly
170+
try {
171+
throw new Exception('This is a test exception sent from the Sentry Laravel SDK.');
172+
} catch (Exception $exception) {
173+
return $exception;
174+
}
175+
}
176+
177+
public function logMessageFromSDK(string $message): void
178+
{
179+
$this->logMessages[] = $message;
180+
}
181+
182+
private function clearLogMessagesFromSDK(): void
183+
{
184+
$this->logMessages = [];
185+
}
186+
187+
private function printDebugTips(): void
188+
{
189+
$emittedSDKErrors = false;
190+
$probablySSLError = false;
191+
192+
foreach ($this->logMessages as $logMessage) {
193+
$emittedSDKErrors = true;
194+
195+
$this->error("SDK: {$logMessage}");
196+
197+
if (Str::contains($logMessage, ['SSL certificate problem', 'certificate has expired'])) {
198+
$probablySSLError = true;
199+
}
200+
}
201+
202+
if ($probablySSLError) {
203+
$this->warn('The problem might be related to the Let\'s Encrypt root certificate that expired and your machine not having an up-to-date enough OpenSSL version or still having the expired root in your certificate authority store.');
204+
$this->warn('For more information you can check out this forum post from Let\'s Encrypt that contains helpful links on how to resolve this for your environment: https://community.letsencrypt.org/t/production-chain-changes/150739/4');
205+
} elseif ($emittedSDKErrors) {
206+
$this->error('Please check the error message from the SDK above for further hints about what went wrong.');
207+
} else {
208+
$this->error('Please check if your DSN is set properly in your `.env` as `SENTRY_LARAVEL_DSN` or in your config file `config/sentry.php`.');
209+
}
210+
}
211+
}

0 commit comments

Comments
 (0)