Skip to content

Commit

Permalink
Add support for Livewire 3 (#798)
Browse files Browse the repository at this point in the history
Co-authored-by: Alex Bouma <alex@bouma.me>
Co-authored-by: Michi Hoffmann <cleptric@users.noreply.github.com>
  • Loading branch information
3 people authored Nov 16, 2023
1 parent f132ad5 commit 229287c
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 55 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,9 @@ jobs:
- name: Install Composer dependencies
run: |
# friendsofphp/php-cs-fixer: No need for this package to run phpunit and it conflicts with older Laravel versions
# livewire/livewire: Only supported on Laravel 7.0 and above
# laravel/folio: Only supported on PHP 8.1 + Laravel 10.0 and above
composer remove friendsofphp/php-cs-fixer laravel/folio --dev --no-interaction --no-update
composer remove friendsofphp/php-cs-fixer livewire/livewire laravel/folio --dev --no-interaction --no-update
# Require the correct versions we want to run phpunit for
composer require \
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"require-dev": {
"phpunit/phpunit": "^8.4 | ^9.3 | ^10.4",
"laravel/framework": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0",
"livewire/livewire": "^2.0 | ^3.0",
"orchestra/testbench": "^4.7 | ^5.1 | ^6.0 | ^7.0 | ^8.0",
"friendsofphp/php-cs-fixer": "^3.11",
"mockery/mockery": "^1.3",
Expand Down
25 changes: 0 additions & 25 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -90,31 +90,6 @@ parameters:
count: 1
path: src/Sentry/Laravel/EventHandler.php

-
message: "#^Parameter \\$component of method Sentry\\\\Laravel\\\\Features\\\\LivewirePackageIntegration\\:\\:handleComponentBoot\\(\\) has invalid type Livewire\\\\Component\\.$#"
count: 1
path: src/Sentry/Laravel/Features/LivewirePackageIntegration.php

-
message: "#^Parameter \\$component of method Sentry\\\\Laravel\\\\Features\\\\LivewirePackageIntegration\\:\\:handleComponentBooted\\(\\) has invalid type Livewire\\\\Component\\.$#"
count: 1
path: src/Sentry/Laravel/Features/LivewirePackageIntegration.php

-
message: "#^Parameter \\$component of method Sentry\\\\Laravel\\\\Features\\\\LivewirePackageIntegration\\:\\:handleComponentDehydrate\\(\\) has invalid type Livewire\\\\Component\\.$#"
count: 1
path: src/Sentry/Laravel/Features/LivewirePackageIntegration.php

-
message: "#^Parameter \\$component of method Sentry\\\\Laravel\\\\Features\\\\LivewirePackageIntegration\\:\\:handleComponentMount\\(\\) has invalid type Livewire\\\\Component\\.$#"
count: 1
path: src/Sentry/Laravel/Features/LivewirePackageIntegration.php

-
message: "#^Parameter \\$livewireManager of method Sentry\\\\Laravel\\\\Features\\\\LivewirePackageIntegration\\:\\:onBoot\\(\\) has invalid type Livewire\\\\LivewireManager\\.$#"
count: 1
path: src/Sentry/Laravel/Features/LivewirePackageIntegration.php

-
message: "#^Parameter \\$request of method Sentry\\\\Laravel\\\\Features\\\\LivewirePackageIntegration\\:\\:handleComponentBooted\\(\\) has invalid type Livewire\\\\Request\\.$#"
count: 1
Expand Down
152 changes: 123 additions & 29 deletions src/Sentry/Laravel/Features/LivewirePackageIntegration.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,21 @@
namespace Sentry\Laravel\Features;

use Livewire\Component;
use Livewire\EventBus;
use Livewire\LivewireManager;
use Livewire\Request;
use Sentry\Breadcrumb;
use Sentry\Laravel\Features\Concerns\TracksPushedScopesAndSpans;
use Sentry\Laravel\Integration;
use Sentry\SentrySdk;
use Sentry\Tracing\Span;
use Sentry\Tracing\SpanContext;
use Sentry\Tracing\TransactionSource;

class LivewirePackageIntegration extends Feature
{
private const FEATURE_KEY = 'livewire';

private const COMPONENT_SPAN_OP = 'ui.livewire.component';
use TracksPushedScopesAndSpans;

/** @var array<Span> */
private $spanStack = [];
private const FEATURE_KEY = 'livewire';

public function isApplicable(): bool
{
Expand All @@ -32,11 +30,56 @@ public function isApplicable(): bool
}

public function onBoot(LivewireManager $livewireManager): void
{
if (class_exists(EventBus::class)) {
$this->registerLivewireThreeEventListeners($livewireManager);

return;
}

$this->registerLivewireTwoEventListeners($livewireManager);
}

private function registerLivewireThreeEventListeners(LivewireManager $livewireManager): void
{
$livewireManager->listen('mount', function (Component $component, array $data) {
if ($this->isTracingFeatureEnabled(self::FEATURE_KEY)) {
$this->handleComponentBoot($component);
}

if ($this->isBreadcrumbFeatureEnabled(self::FEATURE_KEY)) {
$this->handleComponentMount($component, $data);
}
});

$livewireManager->listen('hydrate', function (Component $component) {
if ($this->isTracingFeatureEnabled(self::FEATURE_KEY)) {
$this->handleComponentBoot($component);
}

if ($this->isBreadcrumbFeatureEnabled(self::FEATURE_KEY)) {
$this->handleComponentHydrate($component);
}
});

if ($this->isTracingFeatureEnabled(self::FEATURE_KEY)) {
$livewireManager->listen('dehydrate', [$this, 'handleComponentDehydrate']);
}

if ($this->isBreadcrumbFeatureEnabled(self::FEATURE_KEY)) {
$livewireManager->listen('call', [$this, 'handleComponentCall']);
}
}

private function registerLivewireTwoEventListeners(LivewireManager $livewireManager): void
{
$livewireManager->listen('component.booted', [$this, 'handleComponentBooted']);

if ($this->isTracingFeatureEnabled(self::FEATURE_KEY)) {
$livewireManager->listen('component.boot', [$this, 'handleComponentBoot']);
$livewireManager->listen('component.boot', function ($component) {
$this->handleComponentBoot($component);
});

$livewireManager->listen('component.dehydrate', [$this, 'handleComponentDehydrate']);
}

Expand All @@ -45,23 +88,38 @@ public function onBoot(LivewireManager $livewireManager): void
}
}

public function handleComponentBoot(Component $component): void
public function handleComponentCall(Component $component, string $method, array $arguments): void
{
Integration::addBreadcrumb(new Breadcrumb(
Breadcrumb::LEVEL_INFO,
Breadcrumb::TYPE_DEFAULT,
'livewire',
"Component call: {$component->getName()}::{$method}",
$this->mapCallArgumentsToMethodParameters($component, $method, $arguments) ?? ['arguments' => $arguments]
));
}

public function handleComponentBoot(Component $component, ?string $method = null): void
{
$currentSpan = SentrySdk::getCurrentHub()->getSpan();
if ($this->isLivewireRequest()) {
$this->updateTransactionName($component->getName());
}

if ($currentSpan === null) {
$parentSpan = SentrySdk::getCurrentHub()->getSpan();

if ($parentSpan === null) {
return;
}

$this->spanStack[] = $currentSpan;

$context = new SpanContext;
$context->setOp(self::COMPONENT_SPAN_OP);
$context->setDescription($component->getName());

$componentSpan = $currentSpan->startChild($context);

SentrySdk::getCurrentHub()->setSpan($componentSpan);
$context->setOp('ui.livewire.component');
$context->setDescription(
empty($method)
? $component->getName()
: "{$component->getName()}::{$method}"
);

$this->pushSpan($parentSpan->startChild($context));
}

public function handleComponentMount(Component $component, array $data): void
Expand Down Expand Up @@ -92,23 +150,28 @@ public function handleComponentBooted(Component $component, Request $request): v
}

if ($this->isTracingFeatureEnabled(self::FEATURE_KEY)) {
$this->updateTransactionName($component::getName());
$this->updateTransactionName($component->getName());
}
}

public function handleComponentHydrate(Component $component): void
{
Integration::addBreadcrumb(new Breadcrumb(
Breadcrumb::LEVEL_INFO,
Breadcrumb::TYPE_DEFAULT,
'livewire',
"Component hydrate: {$component->getName()}",
$component->all()
));
}

public function handleComponentDehydrate(Component $component): void
{
$currentSpan = SentrySdk::getCurrentHub()->getSpan();
$span = $this->maybePopSpan();

if ($currentSpan === null || empty($this->spanStack)) {
return;
if ($span !== null) {
$span->finish();
}

$currentSpan->finish();

$previousSpan = array_pop($this->spanStack);

SentrySdk::getCurrentHub()->setSpan($previousSpan);
}

private function updateTransactionName(string $componentName): void
Expand Down Expand Up @@ -137,10 +200,41 @@ private function isLivewireRequest(): bool
return false;
}

return $request->header('x-livewire') === 'true';
return $request->hasHeader('x-livewire');
} catch (\Throwable $e) {
// If the request cannot be resolved, it's probably not a Livewire request.
return false;
}
}

private function mapCallArgumentsToMethodParameters(Component $component, string $method, array $data): ?array
{
// If the data is empty there is nothing to do and we can return early
// We also do a quick sanity check the method exists to prevent doing more expensive reflection to come to the same conclusion
if (empty($data) || !method_exists($component, $method)) {
return null;
}

try {
$reflection = new \ReflectionMethod($component, $method);
$parameters = [];

foreach ($reflection->getParameters() as $parameter) {
$defaultValue = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : '<missing>';

$parameters["\${$parameter->getName()}"] = $data[$parameter->getPosition()] ?? $defaultValue;

unset($data[$parameter->getPosition()]);
}

if (!empty($data)) {
$parameters['additionalArguments'] = $data;
}

return $parameters;
} catch (\ReflectionException $e) {
// If reflection fails, fail the mapping instead of crashing
return null;
}
}
}

0 comments on commit 229287c

Please sign in to comment.