Skip to content

Commit 3b304aa

Browse files
stayallivecleptric
andauthored
Laravel Folio support (#738)
Co-authored-by: Michi Hoffmann <[email protected]>
1 parent d451fd3 commit 3b304aa

13 files changed

+257
-34
lines changed

.github/workflows/ci.yaml

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,55 @@ jobs:
1414
env:
1515
COMPOSER_NO_INTERACTION: 1
1616

17+
strategy:
18+
fail-fast: false
19+
matrix:
20+
php: [ "8.2", "8.1" ]
21+
packages:
22+
# All versions below only support PHP ^8.1 (Laravel requirement)
23+
- { laravel: ^10.0, testbench: ^8.0, phpunit: 9.6.* }
24+
25+
name: phpunit (PHP:${{ matrix.php }}, Laravel:${{ matrix.packages.laravel }})
26+
27+
steps:
28+
- name: Checkout code
29+
uses: actions/checkout@v3
30+
31+
- name: Setup PHP
32+
uses: shivammathur/setup-php@v2
33+
with:
34+
php-version: ${{ matrix.php }}
35+
coverage: pcov
36+
tools: composer:v2
37+
38+
- name: Install Composer dependencies
39+
run: |
40+
# friendsofphp/php-cs-fixer: No need for this package to run phpunit and it conflicts with older Laravel versions
41+
composer remove friendsofphp/php-cs-fixer --dev --no-interaction --no-update
42+
43+
# Require the correct versions we want to run phpunit for
44+
composer require \
45+
"laravel/framework:${{ matrix.packages.laravel }}" \
46+
"illuminate/support:${{ matrix.packages.laravel }}" \
47+
"phpunit/phpunit:${{ matrix.packages.phpunit }}" \
48+
"orchestra/testbench:${{ matrix.packages.testbench }}" \
49+
--no-interaction --no-update
50+
51+
# Actually run the composer installation
52+
composer install --no-interaction --prefer-dist --no-progress
53+
54+
- name: Run phpunit
55+
run: composer test:ci
56+
57+
- name: Upload code coverage
58+
uses: codecov/codecov-action@v3
59+
60+
phpunit-legacy:
61+
runs-on: ubuntu-latest
62+
timeout-minutes: 15
63+
env:
64+
COMPOSER_NO_INTERACTION: 1
65+
1766
strategy:
1867
fail-fast: false
1968
matrix:
@@ -28,33 +77,22 @@ jobs:
2877

2978
# All versions below only support PHP ^8.0 (Laravel requirement)
3079
- { laravel: ^9.0, testbench: ^7.0, phpunit: 9.5.* }
31-
32-
# All versions below only support PHP ^8.1 (Laravel requirement)
33-
- { laravel: ^10.0, testbench: ^8.0, phpunit: 9.6.* }
3480
exclude:
3581
- php: "7.2"
3682
packages: { laravel: ^8.0, testbench: ^6.0, phpunit: 9.3.* }
3783
- php: "7.2"
3884
packages: { laravel: ^9.0, testbench: ^7.0, phpunit: 9.5.* }
39-
- php: "7.2"
40-
packages: { laravel: ^10.0, testbench: ^8.0, phpunit: 9.6.* }
4185

4286
- php: "7.3"
4387
packages: { laravel: ^9.0, testbench: ^7.0, phpunit: 9.5.* }
44-
- php: "7.3"
45-
packages: { laravel: ^10.0, testbench: ^8.0, phpunit: 9.6.* }
4688

4789
- php: "7.4"
4890
packages: { laravel: ^9.0, testbench: ^7.0, phpunit: 9.5.* }
49-
- php: "7.4"
50-
packages: { laravel: ^10.0, testbench: ^8.0, phpunit: 9.6.* }
5191

5292
- php: "8.0"
5393
packages: { laravel: ^6.0, testbench: 4.7.*, phpunit: 8.4.* }
5494
- php: "8.0"
5595
packages: { laravel: ^7.0, testbench: 5.1.*, phpunit: 8.4.* }
56-
- php: "8.0"
57-
packages: { laravel: ^10.0, testbench: ^8.0, phpunit: 9.6.* }
5896

5997
- php: "8.1"
6098
packages: { laravel: ^6.0, testbench: 4.7.*, phpunit: 8.4.* }
@@ -81,8 +119,9 @@ jobs:
81119

82120
- name: Install Composer dependencies
83121
run: |
84-
# No need for this package to run phpunit and it conflicts with older Laravel versions
85-
composer remove friendsofphp/php-cs-fixer --dev --no-interaction --no-update
122+
# friendsofphp/php-cs-fixer: No need for this package to run phpunit and it conflicts with older Laravel versions
123+
# laravel/folio: Only supported on PHP 8.1 + Laravel 10.0 and above
124+
composer remove friendsofphp/php-cs-fixer laravel/folio --dev --no-interaction --no-update
86125
87126
# Require the correct versions we want to run phpunit for
88127
composer require \

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@
3939
"orchestra/testbench": "^4.7 | ^5.1 | ^6.0 | ^7.0 | ^8.0",
4040
"friendsofphp/php-cs-fixer": "^3.11",
4141
"mockery/mockery": "^1.3",
42-
"phpstan/phpstan": "^1.10"
42+
"phpstan/phpstan": "^1.10",
43+
"laravel/folio": "^1.0"
4344
},
4445
"autoload-dev": {
4546
"psr-4": {

src/Sentry/Laravel/EventHandler.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,14 @@ public function __call(string $method, array $arguments)
208208

209209
protected function routeMatchedHandler(RoutingEvents\RouteMatched $match): void
210210
{
211+
$routeAlias = $match->route->action['as'] ?? '';
212+
213+
// Ignore the route if it is the route for the Laravel Folio package
214+
// We handle that route separately in the FolioPackageIntegration
215+
if ($routeAlias === 'laravel-folio') {
216+
return;
217+
}
218+
211219
[$routeName] = Integration::extractNameAndSourceForRoute($match->route);
212220

213221
Integration::addBreadcrumb(new Breadcrumb(
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
namespace Sentry\Laravel\Features;
4+
5+
use Illuminate\Contracts\Events\Dispatcher;
6+
use Illuminate\Support\Str;
7+
use Laravel\Folio\Events\ViewMatched;
8+
use Laravel\Folio\Folio;
9+
use Laravel\Folio\MountPath;
10+
use Laravel\Folio\Pipeline\MatchedView;
11+
use Sentry\Breadcrumb;
12+
use Sentry\Laravel\Integration;
13+
use Sentry\SentrySdk;
14+
use Sentry\Tracing\TransactionSource;
15+
16+
class FolioPackageIntegration extends Feature
17+
{
18+
private const FEATURE_KEY = 'folio';
19+
20+
public function isApplicable(): bool
21+
{
22+
return class_exists(Folio::class);
23+
}
24+
25+
public function onBoot(Dispatcher $events): void
26+
{
27+
$events->listen(ViewMatched::class, [$this, 'handleViewMatched']);
28+
}
29+
30+
public function handleViewMatched(ViewMatched $matched): void
31+
{
32+
$routeName = $this->extractRouteForMatchedView($matched->matchedView, $matched->mountPath);
33+
34+
Integration::addBreadcrumb(new Breadcrumb(
35+
Breadcrumb::LEVEL_INFO,
36+
Breadcrumb::TYPE_NAVIGATION,
37+
'folio.route',
38+
$routeName
39+
));
40+
41+
Integration::setTransaction($routeName);
42+
43+
$transaction = SentrySdk::getCurrentHub()->getTransaction();
44+
45+
if ($transaction === null) {
46+
return;
47+
}
48+
49+
$transaction->setName($routeName);
50+
$transaction->getMetadata()->setSource(TransactionSource::route());
51+
}
52+
53+
private function extractRouteForMatchedView(MatchedView $matchedView, MountPath $mountPath): string
54+
{
55+
$path = Str::beforeLast('/' . ltrim($mountPath->baseUri . $matchedView->relativePath(), '/'), '.blade.php');
56+
57+
return Str::replace(['[', ']'], ['{', '}'], $path);
58+
}
59+
}

src/Sentry/Laravel/ServiceProvider.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ class ServiceProvider extends BaseServiceProvider
5858
Features\CacheIntegration::class,
5959
Features\QueueIntegration::class,
6060
Features\ConsoleIntegration::class,
61+
Features\FolioPackageIntegration::class,
6162
Features\Storage\Integration::class,
6263
Features\LivewirePackageIntegration::class,
6364
];

test/Sentry/Features/DatabaseIntegrationTest.php

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
use Illuminate\Support\Facades\DB;
77
use Sentry\Laravel\Tests\TestCase;
88
use Sentry\Tracing\Span;
9-
use Sentry\Tracing\TransactionContext;
109

1110
class DatabaseIntegrationTest extends TestCase
1211
{
@@ -88,12 +87,7 @@ public function testSpanIsCreatedForSqliteConnectionQuery(): void
8887

8988
private function executeQueryAndRetrieveSpan(string $query): Span
9089
{
91-
$hub = $this->getHubFromContainer();
92-
93-
$transaction = $hub->startTransaction(new TransactionContext);
94-
$transaction->initSpanRecorder();
95-
96-
$this->getCurrentScope()->setSpan($transaction);
90+
$transaction = $this->startTransaction();
9791

9892
$this->dispatchLaravelEvent(new QueryExecuted($query, [], 123, DB::connection()));
9993

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<?php
2+
3+
namespace Sentry\Features;
4+
5+
use Laravel\Folio\Folio;
6+
use Sentry\Laravel\Integration;
7+
use Illuminate\Config\Repository;
8+
use Sentry\Laravel\Tests\TestCase;
9+
use Illuminate\Database\Eloquent\Model;
10+
11+
class FolioPackageIntegrationTest extends TestCase
12+
{
13+
protected function setUp(): void
14+
{
15+
if (!class_exists(Folio::class)) {
16+
$this->markTestSkipped('Laravel Folio package is not installed.');
17+
}
18+
19+
parent::setUp();
20+
}
21+
22+
protected function defineRoutes($router): void
23+
{
24+
Folio::path(__DIR__ . '/../../stubs/folio')->uri('/folio');
25+
}
26+
27+
protected function defineEnvironment($app): void
28+
{
29+
parent::defineEnvironment($app);
30+
31+
tap($app['config'], static function (Repository $config) {
32+
// This is done to prevent noise from the database queries in the breadcrumbs
33+
$config->set('sentry.breadcrumbs.sql_queries', false);
34+
35+
$config->set('database.default', 'inmemory');
36+
$config->set('database.connections.inmemory', [
37+
'driver' => 'sqlite',
38+
'database' => ':memory:',
39+
]);
40+
});
41+
}
42+
43+
protected function defineDatabaseMigrations(): void
44+
{
45+
$this->loadLaravelMigrations();
46+
}
47+
48+
public function testFolioBreadcrumbIsRecorded(): void
49+
{
50+
$this->get('/folio');
51+
52+
$this->assertCount(1, $this->getCurrentBreadcrumbs());
53+
54+
$lastBreadcrumb = $this->getLastBreadcrumb();
55+
56+
$this->assertEquals('folio.route', $lastBreadcrumb->getCategory());
57+
$this->assertEquals('navigation', $lastBreadcrumb->getType());
58+
$this->assertEquals('/folio/index', $lastBreadcrumb->getMessage());
59+
}
60+
61+
public function testFolioRouteUpdatesIntegrationTransaction(): void
62+
{
63+
$this->get('/folio/post/123')->assertOk();
64+
65+
$this->assertEquals('/folio/post/{id}', Integration::getTransaction());
66+
}
67+
68+
public function testFolioRouteUpdatesPerformanceTransaction(): void
69+
{
70+
$transaction = $this->startTransaction();
71+
72+
$this->get('/folio/post/123')->assertOk();
73+
74+
$this->assertEquals('/folio/post/{id}', $transaction->getName());
75+
}
76+
77+
public function testFolioTransactionNameForRouteWithSingleSegmentParamater(): void
78+
{
79+
$this->get('/folio/post/123')->assertOk();
80+
81+
$this->assertEquals('/folio/post/{id}', Integration::getTransaction());
82+
}
83+
84+
public function testFolioTransactionNameForRouteWithMultipleSegmentParameter(): void
85+
{
86+
$this->get('/folio/posts/1/2/3')->assertOk();
87+
88+
$this->assertEquals('/folio/posts/{...ids}', Integration::getTransaction());
89+
}
90+
91+
public function testFolioTransactionNameForRouteWithRouteModelBoundSegmentParameter(): void
92+
{
93+
$user = FolioPackageIntegrationUserModel::create([
94+
'name' => 'John Doe',
95+
'email' => '[email protected]',
96+
'password' => 'secret',
97+
]);
98+
99+
$this->get("/folio/user/{$user->id}")->assertOk();
100+
101+
// This looks a little odd, but that is because we want to make the route model binding work in our tests
102+
// normally this would look like `/folio/user/{User}` instead, see: https://laravel.com/docs/10.x/folio#route-model-binding.
103+
$this->assertEquals('/folio/user/{.Sentry.Features.FolioPackageIntegrationUserModel}', Integration::getTransaction());
104+
}
105+
}
106+
107+
class FolioPackageIntegrationUserModel extends Model
108+
{
109+
protected $table = 'users';
110+
protected $guarded = false;
111+
}

test/Sentry/Features/StorageIntegrationTest.php

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
use Illuminate\Support\Facades\Storage;
66
use Sentry\Laravel\Features\Storage\Integration;
77
use Sentry\Laravel\Tests\TestCase;
8-
use Sentry\Tracing\TransactionContext;
98

109
class StorageIntegrationTest extends TestCase
1110
{
@@ -15,12 +14,7 @@ public function testCreatesSpansFor(): void
1514
'filesystems.disks' => Integration::configureDisks(config('filesystems.disks')),
1615
]);
1716

18-
$hub = $this->getHubFromContainer();
19-
20-
$transaction = $hub->startTransaction(new TransactionContext);
21-
$transaction->initSpanRecorder();
22-
23-
$this->getCurrentScope()->setSpan($transaction);
17+
$transaction = $this->startTransaction();
2418

2519
Storage::put('foo', 'bar');
2620
$fooContent = Storage::get('foo');
@@ -75,12 +69,7 @@ public function testDoesntCreateSpansWhenDisabled(): void
7569
'filesystems.disks' => Integration::configureDisks(config('filesystems.disks'), false),
7670
]);
7771

78-
$hub = $this->getHubFromContainer();
79-
80-
$transaction = $hub->startTransaction(new TransactionContext);
81-
$transaction->initSpanRecorder();
82-
83-
$this->getCurrentScope()->setSpan($transaction);
72+
$transaction = $this->startTransaction();
8473

8574
Storage::exists('foo');
8675

0 commit comments

Comments
 (0)