3
3
namespace Sentry \Laravel \Features ;
4
4
5
5
use Livewire \Component ;
6
+ use Livewire \EventBus ;
6
7
use Livewire \LivewireManager ;
7
8
use Livewire \Request ;
8
9
use Sentry \Breadcrumb ;
10
+ use Sentry \Laravel \Features \Concerns \TracksPushedScopesAndSpans ;
9
11
use Sentry \Laravel \Integration ;
10
12
use Sentry \SentrySdk ;
11
- use Sentry \Tracing \Span ;
12
13
use Sentry \Tracing \SpanContext ;
13
14
use Sentry \Tracing \TransactionSource ;
14
15
15
16
class LivewirePackageIntegration extends Feature
16
17
{
17
- private const FEATURE_KEY = 'livewire ' ;
18
-
19
- private const COMPONENT_SPAN_OP = 'ui.livewire.component ' ;
18
+ use TracksPushedScopesAndSpans;
20
19
21
- /** @var array<Span> */
22
- private $ spanStack = [];
20
+ private const FEATURE_KEY = 'livewire ' ;
23
21
24
22
public function isApplicable (): bool
25
23
{
@@ -32,11 +30,56 @@ public function isApplicable(): bool
32
30
}
33
31
34
32
public function onBoot (LivewireManager $ livewireManager ): void
33
+ {
34
+ if (class_exists (EventBus::class)) {
35
+ $ this ->registerLivewireThreeEventListeners ($ livewireManager );
36
+
37
+ return ;
38
+ }
39
+
40
+ $ this ->registerLivewireTwoEventListeners ($ livewireManager );
41
+ }
42
+
43
+ private function registerLivewireThreeEventListeners (LivewireManager $ livewireManager ): void
44
+ {
45
+ $ livewireManager ->listen ('mount ' , function (Component $ component , array $ data ) {
46
+ if ($ this ->isTracingFeatureEnabled (self ::FEATURE_KEY )) {
47
+ $ this ->handleComponentBoot ($ component );
48
+ }
49
+
50
+ if ($ this ->isBreadcrumbFeatureEnabled (self ::FEATURE_KEY )) {
51
+ $ this ->handleComponentMount ($ component , $ data );
52
+ }
53
+ });
54
+
55
+ $ livewireManager ->listen ('hydrate ' , function (Component $ component ) {
56
+ if ($ this ->isTracingFeatureEnabled (self ::FEATURE_KEY )) {
57
+ $ this ->handleComponentBoot ($ component );
58
+ }
59
+
60
+ if ($ this ->isBreadcrumbFeatureEnabled (self ::FEATURE_KEY )) {
61
+ $ this ->handleComponentHydrate ($ component );
62
+ }
63
+ });
64
+
65
+ if ($ this ->isTracingFeatureEnabled (self ::FEATURE_KEY )) {
66
+ $ livewireManager ->listen ('dehydrate ' , [$ this , 'handleComponentDehydrate ' ]);
67
+ }
68
+
69
+ if ($ this ->isBreadcrumbFeatureEnabled (self ::FEATURE_KEY )) {
70
+ $ livewireManager ->listen ('call ' , [$ this , 'handleComponentCall ' ]);
71
+ }
72
+ }
73
+
74
+ private function registerLivewireTwoEventListeners (LivewireManager $ livewireManager ): void
35
75
{
36
76
$ livewireManager ->listen ('component.booted ' , [$ this , 'handleComponentBooted ' ]);
37
77
38
78
if ($ this ->isTracingFeatureEnabled (self ::FEATURE_KEY )) {
39
- $ livewireManager ->listen ('component.boot ' , [$ this , 'handleComponentBoot ' ]);
79
+ $ livewireManager ->listen ('component.boot ' , function ($ component ) {
80
+ $ this ->handleComponentBoot ($ component );
81
+ });
82
+
40
83
$ livewireManager ->listen ('component.dehydrate ' , [$ this , 'handleComponentDehydrate ' ]);
41
84
}
42
85
@@ -45,23 +88,38 @@ public function onBoot(LivewireManager $livewireManager): void
45
88
}
46
89
}
47
90
48
- public function handleComponentBoot (Component $ component ): void
91
+ public function handleComponentCall (Component $ component , string $ method , array $ arguments ): void
92
+ {
93
+ Integration::addBreadcrumb (new Breadcrumb (
94
+ Breadcrumb::LEVEL_INFO ,
95
+ Breadcrumb::TYPE_DEFAULT ,
96
+ 'livewire ' ,
97
+ "Component call: {$ component ->getName ()}:: {$ method }" ,
98
+ $ this ->mapCallArgumentsToMethodParameters ($ component , $ method , $ arguments ) ?? ['arguments ' => $ arguments ]
99
+ ));
100
+ }
101
+
102
+ public function handleComponentBoot (Component $ component , ?string $ method = null ): void
49
103
{
50
- $ currentSpan = SentrySdk::getCurrentHub ()->getSpan ();
104
+ if ($ this ->isLivewireRequest ()) {
105
+ $ this ->updateTransactionName ($ component ->getName ());
106
+ }
51
107
52
- if ($ currentSpan === null ) {
108
+ $ parentSpan = SentrySdk::getCurrentHub ()->getSpan ();
109
+
110
+ if ($ parentSpan === null ) {
53
111
return ;
54
112
}
55
113
56
- $ this ->spanStack [] = $ currentSpan ;
57
-
58
114
$ context = new SpanContext ;
59
- $ context ->setOp (self ::COMPONENT_SPAN_OP );
60
- $ context ->setDescription ($ component ->getName ());
61
-
62
- $ componentSpan = $ currentSpan ->startChild ($ context );
63
-
64
- SentrySdk::getCurrentHub ()->setSpan ($ componentSpan );
115
+ $ context ->setOp ('ui.livewire.component ' );
116
+ $ context ->setDescription (
117
+ empty ($ method )
118
+ ? $ component ->getName ()
119
+ : "{$ component ->getName ()}:: {$ method }"
120
+ );
121
+
122
+ $ this ->pushSpan ($ parentSpan ->startChild ($ context ));
65
123
}
66
124
67
125
public function handleComponentMount (Component $ component , array $ data ): void
@@ -92,23 +150,28 @@ public function handleComponentBooted(Component $component, Request $request): v
92
150
}
93
151
94
152
if ($ this ->isTracingFeatureEnabled (self ::FEATURE_KEY )) {
95
- $ this ->updateTransactionName ($ component:: getName ());
153
+ $ this ->updateTransactionName ($ component-> getName ());
96
154
}
97
155
}
98
156
157
+ public function handleComponentHydrate (Component $ component ): void
158
+ {
159
+ Integration::addBreadcrumb (new Breadcrumb (
160
+ Breadcrumb::LEVEL_INFO ,
161
+ Breadcrumb::TYPE_DEFAULT ,
162
+ 'livewire ' ,
163
+ "Component hydrate: {$ component ->getName ()}" ,
164
+ $ component ->all ()
165
+ ));
166
+ }
167
+
99
168
public function handleComponentDehydrate (Component $ component ): void
100
169
{
101
- $ currentSpan = SentrySdk:: getCurrentHub ()-> getSpan ();
170
+ $ span = $ this -> maybePopSpan ();
102
171
103
- if ($ currentSpan === null || empty ( $ this -> spanStack ) ) {
104
- return ;
172
+ if ($ span !== null ) {
173
+ $ span -> finish () ;
105
174
}
106
-
107
- $ currentSpan ->finish ();
108
-
109
- $ previousSpan = array_pop ($ this ->spanStack );
110
-
111
- SentrySdk::getCurrentHub ()->setSpan ($ previousSpan );
112
175
}
113
176
114
177
private function updateTransactionName (string $ componentName ): void
@@ -137,10 +200,41 @@ private function isLivewireRequest(): bool
137
200
return false ;
138
201
}
139
202
140
- return $ request ->header ('x-livewire ' ) === ' true ' ;
203
+ return $ request ->hasHeader ('x-livewire ' );
141
204
} catch (\Throwable $ e ) {
142
205
// If the request cannot be resolved, it's probably not a Livewire request.
143
206
return false ;
144
207
}
145
208
}
209
+
210
+ private function mapCallArgumentsToMethodParameters (Component $ component , string $ method , array $ data ): ?array
211
+ {
212
+ // If the data is empty there is nothing to do and we can return early
213
+ // We also do a quick sanity check the method exists to prevent doing more expensive reflection to come to the same conclusion
214
+ if (empty ($ data ) || !method_exists ($ component , $ method )) {
215
+ return null ;
216
+ }
217
+
218
+ try {
219
+ $ reflection = new \ReflectionMethod ($ component , $ method );
220
+ $ parameters = [];
221
+
222
+ foreach ($ reflection ->getParameters () as $ parameter ) {
223
+ $ defaultValue = $ parameter ->isDefaultValueAvailable () ? $ parameter ->getDefaultValue () : '<missing> ' ;
224
+
225
+ $ parameters ["\${$ parameter ->getName ()}" ] = $ data [$ parameter ->getPosition ()] ?? $ defaultValue ;
226
+
227
+ unset($ data [$ parameter ->getPosition ()]);
228
+ }
229
+
230
+ if (!empty ($ data )) {
231
+ $ parameters ['additionalArguments ' ] = $ data ;
232
+ }
233
+
234
+ return $ parameters ;
235
+ } catch (\ReflectionException $ e ) {
236
+ // If reflection fails, fail the mapping instead of crashing
237
+ return null ;
238
+ }
239
+ }
146
240
}
0 commit comments