<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="/vendor/feed/atom.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-US">
                        <id>https://flareapp.io/feed</id>
                                <link href="https://flareapp.io/feed" rel="self"></link>
                                <title><![CDATA[flareapp.io]]></title>
                    
                                <subtitle></subtitle>
                                                    <updated>2026-09-17T00:00:00+00:00</updated>
                        <entry>
            <title><![CDATA[Introducing: Dark mode for Flare]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/introducing-dark-mode-for-flare" />
            <id>https://flareapp.io/introducing-dark-mode-for-flare</id>
            <author>
                <name><![CDATA[Dries]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Over the years we heard from many of you that you wanted a dark mode for Flare. We always put it at the bottom of our to-do list, because it's a big task to do right.</p>
<p>But we've finally done it! Making sure the colours of the dark palette fit the existing theme of the app, while keeping it accessible, wasn't easy.</p>
<p>You can turn it on under Account → Appearance, or straight from the menu behind your avatar. Pick Light, Dark, or Match my system.</p>
<h2>Some of the things we learnt</h2>
<h3>Shadows don't really work in dark mode</h3>
<p>We use shadows on some surfaces to separate them from the background, and this works well in a light theme. Once everything turns dark, it becomes a lot harder: black on black doesn't always work, and turning your black shadows white creates a glow effect. Not what you want.</p>
<p>Instead, we used light borders to recreate that separation from the background.</p>
<p><img src="https://assets.flareapp.io/production/blog/67b7450c63c93a85871895cf6515a054/5c7fb92c-0901-4cd7-9985-b3f3c1e6e241.png" alt="compare dark vs light" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/67b7450c63c93a85871895cf6515a054/5c7fb92c-0901-4cd7-9985-b3f3c1e6e241.png" width="610" height="460" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/67b7450c63c93a85871895cf6515a054/5c7fb92c-0901-4cd7-9985-b3f3c1e6e241.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=610/https://assets.flareapp.io/production/blog/67b7450c63c93a85871895cf6515a054/5c7fb92c-0901-4cd7-9985-b3f3c1e6e241.png 610w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<h3>Some of our gradients were unusable in dark mode</h3>
<p>We have a fun, glass-looking colour gradient that is mostly used on the flash messages you see in Flare. Because there is some opacity involved to achieve this effect, the gradients didn't work in dark mode at all.</p>
<p>To make them work in dark mode, we reversed them. We took the red of the text and used it as the background colour, and the more faded red of the light mode background became the text colour.</p>
<p><img src="https://assets.flareapp.io/production/blog/5a9ce0951f285b05e2596e55f8da28a2/7570fcc1-1a4b-4aaa-ac8e-7266dcdd0f73.png" alt="compare light gradients vs dark non-gradient" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/5a9ce0951f285b05e2596e55f8da28a2/7570fcc1-1a4b-4aaa-ac8e-7266dcdd0f73.png" width="610" height="460" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/5a9ce0951f285b05e2596e55f8da28a2/7570fcc1-1a4b-4aaa-ac8e-7266dcdd0f73.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=610/https://assets.flareapp.io/production/blog/5a9ce0951f285b05e2596e55f8da28a2/7570fcc1-1a4b-4aaa-ac8e-7266dcdd0f73.png 610w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<h3>The CSS variables were not ready</h3>
<p>We already had all our colours in CSS variables, so we assumed we were good to go. But we made a mistake: our variables were named after colours, not roles. A name like <code>--fl-purple-light</code> doesn't describe what it's used for.</p>
<p>So we introduced a semantic layer of tokens: <code>--canvas</code>, <code>--surface</code>, <code>--line</code>, <code>--fg-muted</code>, and so on. Once we had this, creating a new theme became very easy. The hard part was getting there: our components used the old colour names 1,323 times. We used AI for the first pass. Most of those were straight renames, like <code>text-fl-gray</code> becoming <code>text-fg-muted</code>. The rest needed a closer look, because one old colour was often doing two different jobs, like white being both a card background and the label on a purple button.</p>
<p>After our first pass, we found some hardcoded hex value stragglers here and there, which were easy to clean up. Next, we used Playwright to take screenshots of the same pages on the main branch and on our dark mode branch, and pixel-matched them to check that everything still looked the same in the light theme.</p>
<p>Finally, it was just a matter of creating a dark theme next to our light theme. Our components only ever use the token names, like <code>bg-surface</code> or <code>text-fg-muted</code>, so a dark theme is nothing more than a second set of values for the same tokens:</p>
<pre class="phiki language-css nord" data-language="css" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #616E88;">/*</span><span class="token" style="color: #616E88;"> Light theme, the default </span><span class="token" style="color: #616E88;">*/</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">:</span><span class="token" style="color: #8FBCBB;">root</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token">--canvas</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #88C0D0;">var</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #88C0D0;">--fl-gray-30</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token"> </span><span class="token" style="color: #616E88;">/*</span><span class="token" style="color: #616E88;"> #f3f4fd </span><span class="token" style="color: #616E88;">*/</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token">--surface</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #88C0D0;">var</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #88C0D0;">--fl-white</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token"> </span><span class="token" style="color: #616E88;">/*</span><span class="token" style="color: #616E88;"> #ffffff </span><span class="token" style="color: #616E88;">*/</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token">--line</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #88C0D0;">var</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #88C0D0;">--fl-gray-90</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token"> </span><span class="token" style="color: #616E88;">/*</span><span class="token" style="color: #616E88;"> #e2e8f0 </span><span class="token" style="color: #616E88;">*/</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token">--fg-muted</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #88C0D0;">var</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #88C0D0;">--fl-gray-540</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token"> </span><span class="token" style="color: #616E88;">/*</span><span class="token" style="color: #616E88;"> #64748b </span><span class="token" style="color: #616E88;">*/</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token">--elevation-card</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #B48EAD;">0</span><span class="token" style="color: #81A1C1;">px</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #B48EAD;">10</span><span class="token" style="color: #81A1C1;">px</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #B48EAD;">15</span><span class="token" style="color: #81A1C1;">px</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #B48EAD;">-3</span><span class="token" style="color: #81A1C1;">px</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #88C0D0;">rgba</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #B48EAD;">0</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #B48EAD;">0</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #B48EAD;">0</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #B48EAD;">0.05</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #616E88;">/*</span><span class="token" style="color: #616E88;"> Dark theme: the same tokens, different values </span><span class="token" style="color: #616E88;">*/</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #8FBCBB;">dark</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #D8DEE9;">color-scheme</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #81A1C1;">dark</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token">--canvas</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">#</span><span class="token" style="color: #B48EAD;">111219</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token">--surface</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">#</span><span class="token" style="color: #B48EAD;">191a24</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token">--line</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #88C0D0;">var</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #88C0D0;">--fl-gray-800</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token"> </span><span class="token" style="color: #616E88;">/*</span><span class="token" style="color: #616E88;"> #2c2e3c </span><span class="token" style="color: #616E88;">*/</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token">--fg-muted</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #88C0D0;">var</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #88C0D0;">--fl-gray-370</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token"> </span><span class="token" style="color: #616E88;">/*</span><span class="token" style="color: #616E88;"> #9599ad </span><span class="token" style="color: #616E88;">*/</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #616E88;">/*</span><span class="token" style="color: #616E88;"> Shadows disappear on dark grounds, so a 1px border takes over </span><span class="token" style="color: #616E88;">*/</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token">--elevation-card</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #B48EAD;">0</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #B48EAD;">0</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #B48EAD;">0</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #B48EAD;">1</span><span class="token" style="color: #81A1C1;">px</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #88C0D0;">var</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #88C0D0;">--line</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #616E88;">/*</span><span class="token" style="color: #616E88;"> Tailwind turns the tokens into utilities like bg-surface and shadow-card </span><span class="token" style="color: #616E88;">*/</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">@</span><span class="token" style="color: #81A1C1;">theme</span><span class="token"> inline</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token">--color-canvas: var(--canvas</span><span class="token">);
</span></span><span class="line"><span class="token">    </span><span class="token">--color-surface: var(--surface</span><span class="token">);
</span></span><span class="line"><span class="token">    </span><span class="token">--color-line: var(--line</span><span class="token">);
</span></span><span class="line"><span class="token">    </span><span class="token">--color-fg-muted: var(--fg-muted</span><span class="token">);
</span></span><span class="line"><span class="token">    </span><span class="token">--shadow-card: var(--elevation-card</span><span class="token">);
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>A component doesn't need to know which theme is active:</p>
<pre class="phiki language-tsx nord" data-language="tsx" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">&lt;</span><span class="token" style="color: #81A1C1;">div</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">className</span><span class="token" style="color: #81A1C1;">=</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">rounded-lg bg-surface p-4 shadow-card</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #81A1C1;">&gt;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">&lt;</span><span class="token" style="color: #81A1C1;">p</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">className</span><span class="token" style="color: #81A1C1;">=</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">text-fg-muted</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #81A1C1;">&gt;</span><span class="token">Looks right in both themes, without a single dark: class.</span><span class="token" style="color: #81A1C1;">&lt;/</span><span class="token" style="color: #81A1C1;">p</span><span class="token" style="color: #81A1C1;">&gt;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">&lt;/</span><span class="token" style="color: #81A1C1;">div</span><span class="token" style="color: #81A1C1;">&gt;</span><span class="token">
</span></span></code></pre>
<h2>Happy with the result</h2>
<p>It feels great to finally cross this one off our to-do list, and we learnt a lot along the way.</p>
<p>Give it a try under Account → Appearance, or from the menu behind your avatar. Spotted a screen that still looks off in dark mode? Let us know, and we'll fix it.</p>
]]>
            </summary>
                                    <updated>2026-09-17T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Automatic breadcrumbs recording and GDPR consent]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/automatic-breadcrumbs-recording-and-gdpr-consent" />
            <id>https://flareapp.io/automatic-breadcrumbs-recording-and-gdpr-consent</id>
            <author>
                <name><![CDATA[Dries]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Until now, the JavaScript client just reported errors and any glows you manually attached. In the newly released version (2.10.0), we have added automatic breadcrumb recording.</p>
<p>Alongside to automatic breadcrumbs, we also added a way to enable or disable the client from the callbacks of your GDPR or cookie banner libraries. The JS client does not set any cookies of its own. But when it reports an error, it reads data like the user-agent and the cookies in the browser, which you might want to gate behind a consent banner.</p>
<h2>What is recorded exactly?</h2>
<ul>
<li>Clicks, by element, so you can see which button someone pressed</li>
<li>Form changes, when a field is filled in and left</li>
<li>Fetch and XHR requests, with the method, the URL and the status that came back</li>
<li>Route changes, including the page the session started on</li>
</ul>
<p>Breadcrumbs give you a timeline of the events that led up to the error. Which button was clicked, which API call was fired, which field the user was typing in. The breadcrumbs are lean and contain only information that is helpful for debugging. We never record the text a user types into a form, and we redact sensitive values from URLs.</p>
<p>This is an example of the timeline for an exception during a checkout flow. You can follow the user's journey up until that point, which makes it easier for you to reproduce the exact scenario that led up to this error.</p>
<p><img src="https://assets.flareapp.io/production/blog/a8cf80d83ecd9ffacd5f8cb7529dab4d/5831e2ff-ea8e-45a3-8fb1-0b669e0bb247.png" alt="breadcrumb timeline" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/a8cf80d83ecd9ffacd5f8cb7529dab4d/5831e2ff-ea8e-45a3-8fb1-0b669e0bb247.png" width="1401" height="1211" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/a8cf80d83ecd9ffacd5f8cb7529dab4d/5831e2ff-ea8e-45a3-8fb1-0b669e0bb247.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/a8cf80d83ecd9ffacd5f8cb7529dab4d/5831e2ff-ea8e-45a3-8fb1-0b669e0bb247.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/a8cf80d83ecd9ffacd5f8cb7529dab4d/5831e2ff-ea8e-45a3-8fb1-0b669e0bb247.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1401/https://assets.flareapp.io/production/blog/a8cf80d83ecd9ffacd5f8cb7529dab4d/5831e2ff-ea8e-45a3-8fb1-0b669e0bb247.png 1401w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<h2>Turning it on</h2>
<p>Breadcrumbs are off by default. You can turn them on in your Flare config like this:</p>
<pre class="phiki language-javascript nord" data-language="javascript" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #D8DEE9;">flare</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #88C0D0;">configure</span><span class="token">(</span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #88C0D0;">enableBreadcrumbs</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #81A1C1;">true</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span></code></pre>
<p>By default we buffer the last 100 breadcrumb events. You can change this amount with <code>maxBreadcrumbs</code>.</p>
<p>For more information about automatic breadcrumb recording, head over to our <a href="/docs/javascript/data-collection/automatic-breadcrumbs">docs</a>.</p>
<h2>How to integrate the client with a cookie banner</h2>
<p>To integrate the JS client with a cookie banner library like Cookiebot you add these lines of code:</p>
<pre class="phiki language-javascript nord" data-language="javascript" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">import</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">flare</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">}</span><span class="token"> </span><span class="token" style="color: #81A1C1;">from</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">@flareapp/js</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #D8DEE9;">window</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #88C0D0;">addEventListener</span><span class="token">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">CookiebotOnAccept</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #D8DEE9;">flare</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #88C0D0;">light</span><span class="token">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">YOUR PROJECT KEY</span><span class="token" style="color: #ECEFF4;">'</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token"> </span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> start the client</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #D8DEE9;">flare</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #88C0D0;">setConsent</span><span class="token">(</span><span class="token" style="color: #81A1C1;">true</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token"> </span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> and allow sending</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #D8DEE9;">window</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #88C0D0;">addEventListener</span><span class="token">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">CookiebotOnDecline</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #D8DEE9;">flare</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #88C0D0;">setConsent</span><span class="token">(</span><span class="token" style="color: #81A1C1;">false</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token"> </span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> withdrawal: stop all sends, drop buffers</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span></code></pre>
<p>The client defaults the consent to <code>true</code> so we won't break any existing install. If you want to start the client with consent disabled, you can always do the following:</p>
<pre class="phiki language-javascript nord" data-language="javascript" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">import</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">flare</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">}</span><span class="token"> </span><span class="token" style="color: #81A1C1;">from</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">@flareapp/js</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #D8DEE9;">flare</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #88C0D0;">configure</span><span class="token">(</span><span class="token" style="color: #ECEFF4;">{</span><span class="token"> </span><span class="token" style="color: #88C0D0;">hasConsent</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #81A1C1;">false</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token"> </span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> consent off</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #D8DEE9;">flare</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #88C0D0;">light</span><span class="token">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">YOUR PROJECT KEY</span><span class="token" style="color: #ECEFF4;">'</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token"> </span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> start js client</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> nothing is sent to Flare, until this callback is called</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #D8DEE9;">window</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #88C0D0;">addEventListener</span><span class="token">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">CookiebotOnAccept</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">flare</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #88C0D0;">setConsent</span><span class="token">(</span><span class="token" style="color: #81A1C1;">true</span><span class="token">)</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> stop sending data when consent is revoked by the user</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #D8DEE9;">window</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #88C0D0;">addEventListener</span><span class="token">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">CookiebotOnDecline</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">flare</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #88C0D0;">setConsent</span><span class="token">(</span><span class="token" style="color: #81A1C1;">false</span><span class="token">)</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span></code></pre>
<p>For more information about the consent option, head over to the <a href="/docs/javascript/data-collection/gdpr-consent">docs</a>.</p>
]]>
            </summary>
                                    <updated>2026-08-27T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Every exception has a backstory: introducing previous exceptions in Flare]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/every-exception-has-a-backstory-introducing-previous-exceptions-in-flare" />
            <id>https://flareapp.io/every-exception-has-a-backstory-introducing-previous-exceptions-in-flare</id>
            <author>
                <name><![CDATA[Ruben]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Here at Flare we love building the best error tracker there is. One of the things nagging me personally for years is that we didn't support previous exceptions.</p>
<p>When you create an exception in PHP, you can pass along the one that caused it:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">try</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">importProduct</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">product</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token"> </span><span class="token" style="color: #81A1C1;">catch</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #8FBCBB;">Throwable</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">exception</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">throw</span><span class="token"> </span><span class="token" style="color: #81A1C1;">new</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">ProductImportFailed</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">        </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">Could not import product </span><span class="token" style="color: #81A1C1;">{</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">product</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">sku</span><span class="token" style="color: #81A1C1;">}</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">        </span><span class="token" style="color: #88C0D0;">previous</span><span class="token" style="color: #ECEFF4;">:</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">exception</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">    </span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>This lets you catch a low level exception and rethrow it with your own context. Laravel famously does the same thing, wrapping a <code>PDOException</code> in its own <code>QueryException</code> with some extra info about the query that just failed.</p>
<p>Up until now, Flare completely ignored these previous exceptions. And often that's fine, since the wrapping exception usually carries more useful information.</p>
<p>But here's the thing. When you wrap an exception, the stack trace you end up looking at starts at your <code>catch</code> block:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">App\Import\ProductImportFailed
</span></span><span class="line"><span class="token">Could not import product SKU-4471
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">#0 app/Import/ImportProducts.php:19
</span></span><span class="line"><span class="token">#1 app/Jobs/ImportFeedJob.php:24
</span></span><span class="line"><span class="token">#2 vendor/laravel/framework/src/.../CallQueuedHandler.php:47
</span></span></code></pre>
<p>You know which product broke. You have no idea why.</p>
<p>Every frame that led to the actual failure lives on the previous exception:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">TypeError
</span></span><span class="line"><span class="token">Money::__construct(): Argument #1 ($amount) must be of type int, string given
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">#0 app/Support/Money.php:31
</span></span><span class="line"><span class="token">#1 app/Import/ProductTransformer.php:58
</span></span><span class="line"><span class="token">#2 app/Import/ProductImporter.php:24
</span></span></code></pre>
<p>There it is. The feed started sending prices as strings.</p>
<p>That's why, starting from version 3.4 of our PHP and Laravel packages, we collect the whole chain and Flare displays it. The arrow next to the exception class lets you navigate between them, and the message and stack trace follow along:</p>
<p><img src="https://assets.flareapp.io/production/blog/7b46e6db3982a8a138d1c2ba257b5344/1703bd3d-7d43-47e6-8749-222d4684c8cf.png" alt="Navigating between previous exceptions in Flare" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/7b46e6db3982a8a138d1c2ba257b5344/1703bd3d-7d43-47e6-8749-222d4684c8cf.png" width="1692" height="1152" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/7b46e6db3982a8a138d1c2ba257b5344/1703bd3d-7d43-47e6-8749-222d4684c8cf.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/7b46e6db3982a8a138d1c2ba257b5344/1703bd3d-7d43-47e6-8749-222d4684c8cf.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/7b46e6db3982a8a138d1c2ba257b5344/1703bd3d-7d43-47e6-8749-222d4684c8cf.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/7b46e6db3982a8a138d1c2ba257b5344/1703bd3d-7d43-47e6-8749-222d4684c8cf.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>Previous exceptions are an essential part of errors and we're really happy Flare finally supports them.</p>
<p>We're working a lot on Flare these days. <a href="https://flareapp.io/blog/announcing-performance-monitoring-for-javascript-applications">Performance monitoring for JavaScript</a> has been added, <a href="https://flareapp.io/blog/flare-now-uses-oauth-for-mcp-clients-the-cli-and-the-api">connecting an MCP client to Flare is now a lot easier thanks to OAuth</a>, and <a href="https://flareapp.io/blog/bringing-flare-back-to-laravel-10-and-php-81">the Flare clients have been backported to older PHP and Laravel versions</a>.</p>
<p>Anything you're missing in Flare? Let us know at support@flareapp.io.</p>
<p>Happy debugging!</p>
]]>
            </summary>
                                    <updated>2026-08-27T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Why we removed AI solutions from Flare]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/why-we-removed-ai-solutions-from-flare" />
            <id>https://flareapp.io/why-we-removed-ai-solutions-from-flare</id>
            <author>
                <name><![CDATA[Alex]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>I've always thought Flare's AI generated solutions were one of the coolest things we built in the early AI days: a short suggested fix next to your error, written by an LLM from the stack trace and a snippet of the failing code. We shipped them in March 2023 and this April we removed them.</p>
<p>Frankly put they lasted a lot longer than they should've. In the era of agentic AI, these AI solutions felt ancient and on top of that, they were taking up valuable space in the UI. Nobody has complained since the removal, so it seems the hunch was right.</p>
<p>Let's quickly go over where the old AI solutions were failing, and what we're replacing them with: an MCP server that handles real triage, a CLI with an agent skill, and quick prompts to copy right in the UI.</p>
<h2>Why the AI solutions (often) sucked</h2>
<p>When we added solutions to Flare back in 2019, a fix was proposed by an algorithm in the PHP client and sent to the Flare API together with the error report. Some solutions even contained runnable code or commands: forgot to run a migration, and the error page offered to run <code>php artisan migrate</code> for you.</p>
<p>In 2023, a few weeks after OpenAI opened up the ChatGPT API, we added an AI solution resolver on top: <code>gpt-3.5-turbo</code> came up with solutions dynamically from the thrown error, a code snippet, and the stack trace. Back then, when we were all still copy-pasting code into a ChatGPT window, this was a genuinely cool feature. It had its annoyances too: the runnable solutions caused a minor RCE for apps with debug mode enabled in production, which turned out to be a lot of apps.</p>
<p>We upgraded the model a couple of times, ending on <code>gpt-4.1-mini</code>. Better prose every time, same problem: not enough context and that's the point of this post: fixing an error takes context, and that context lives with your coding agent and your repo, not on our servers. The feedback loop from error to fix should be as short as possible. So, like just about every developer product this year, we built two surfaces for agents, an MCP server and a CLI, plus a button that gets an error into them fast, and docs an agent can read.</p>
<h2>The MCP server handles real triage now</h2>
<p>Our MCP server started out as little more than error search. Today your agent can fetch occurrences with full stack traces, resolve, snooze, and assign errors, leave notes, pull performance data like slow routes and queries, trace requests and jobs, and search your logs. The investigation and the cleanup afterwards both happen without you ever switching back to Flare in the browser.</p>
<p>Connecting is one command:</p>
<pre class="phiki language-shellscript nord" data-language="shellscript" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #88C0D0;">claude</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">mcp</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">add</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">-</span><span class="token" style="color: #A3BE8C;">-transport</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">http</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">flare</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">https://flareapp.io/mcp</span><span class="token">
</span></span></code></pre>
<p>Your browser opens, you approve the connection, done. Connections are read-only by default and scoped to the teams and projects you choose. You decide per connection whether an agent can only look or also act. We wrote about the OAuth flow in detail <a href="https://flareapp.io/blog/flare-now-uses-oauth-for-mcp-clients-the-cli-and-the-api">last week</a>.</p>
<h2>A CLI and an agent skill</h2>
<p>Whether your agent should use the CLI or the MCP server is mostly personal preference. I've long preferred the CLI because it keeps my agent's context light, but lately the MCP server has been winning me over with how fast it is to hook up. The CLI covers the same ground as the MCP server:</p>
<pre class="phiki language-shellscript nord" data-language="shellscript" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #88C0D0;">composer</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">global</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">require</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">spatie/flare-cli</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">flare</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">login</span><span class="token">
</span></span></code></pre>
<p>There's also a published <a href="https://github.com/spatie/flare-cli/blob/main/skills/flare/SKILL.md">agent skill</a> that teaches your agent the CLI and the triage workflow. The <a href="https://flareapp.io/docs/flare/general/using-the-cli">docs</a> compare the skill and the MCP server so you can pick one instead of installing both.</p>
<h2>A Copy for AI button on every error</h2>
<p>Every error card now has a "Copy for AI" button. It puts a pre-made prompt on your clipboard in one of three formats. CLI and MCP are a short instruction telling your agent to investigate the error through that tool, with the error and project IDs filled in. Markdown is a full dump of the latest occurrence, stack trace, snippets, and context included, so it works with any AI tool that accepts pasted text, no Flare connection needed.</p>
<p><img src="https://assets.flareapp.io/production/blog/525b38b222653918105ea483495b82e2/9aef1782-a265-4921-8a63-e27da377c151.png" alt="The Copy for AI popover on an error card" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/525b38b222653918105ea483495b82e2/9aef1782-a265-4921-8a63-e27da377c151.png" width="1280" height="360" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/525b38b222653918105ea483495b82e2/9aef1782-a265-4921-8a63-e27da377c151.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/525b38b222653918105ea483495b82e2/9aef1782-a265-4921-8a63-e27da377c151.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/525b38b222653918105ea483495b82e2/9aef1782-a265-4921-8a63-e27da377c151.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1280/https://assets.flareapp.io/production/blog/525b38b222653918105ea483495b82e2/9aef1782-a265-4921-8a63-e27da377c151.png 1280w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>The button remembers which format you picked, so from the second error on it's a single click. Paste the prompt into Claude Code, Codex, or whatever tool is hyped up on Reddit this week and the agent pulls the occurrence and gets to work. This is the fix-an-error flow we wanted solutions to be, except the model can actually see your code.</p>
<h2>Docs your agent can read</h2>
<p>Good docs still matter, and these days agents read them too. Append <code>.md</code> to any Flare docs URL and you get the page as plain markdown, navigation included:</p>
<pre class="phiki language-shellscript nord" data-language="shellscript" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #88C0D0;">curl</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">https://flareapp.io/docs/flare/general/our-mcp-server.md</span><span class="token">
</span></span></code></pre>
<p>A markdown response burns far fewer tokens per page, and the good this is: your agent will discover this on its own.</p>
<p>We also try to stay on top of the emerging patterns AI agents use for service and content discovery: an MCP server card, an agent skills index, and an API catalog are all available under <code>/.well-known/</code>. We use Flare with AI every day ourselves, so when a new convention helps an agent find its way around, we want to support it early.</p>
<h2>A peek at what's cooking</h2>
<p>Agentic loops are hot! An AI agent that looks at Flare, tweaks some code, runs your sandboxed test suite (and maybe breaks out), and then checks whether the error is actually resolved, all in one session. We're experimenting with running those loops on our infrastructure, where a hosted agent picks up a new error, a performance regression, or a suspicious pattern in your logs and investigates it the way a developer would.</p>
<p>No promises on dates. Solutions will return in some form, built on all of the above, and we won't ship them until they're clearly better than what we removed.</p>
<h2>Try it</h2>
<p>The Copy for AI button is live on every error card. Connect your MCP client to <code>https://flareapp.io/mcp</code>, or install the CLI with <code>composer global require spatie/flare-cli</code>. The <a href="https://flareapp.io/docs/flare/general/our-mcp-server">MCP docs</a> and <a href="https://flareapp.io/docs/flare/general/using-the-cli">CLI docs</a> have the details. And if you were using AI solutions and disagree with the removal, please tell us. That feedback shapes what the next version of Flare will look like.</p>
]]>
            </summary>
                                    <updated>2026-08-11T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Bringing Flare back to Laravel 10 and PHP 8.1]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/bringing-flare-back-to-laravel-10-and-php-81" />
            <id>https://flareapp.io/bringing-flare-back-to-laravel-10-and-php-81</id>
            <author>
                <name><![CDATA[Ruben]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Here at Spatie, the creators of Flare and over 400 PHP and Laravel packages, we always try to push to the latest versions of PHP and Laravel. It's pretty much always a good idea.</p>
<p>For Flare we make an exception. We'd love to give all our customers the newest features like performance monitoring and logs, no matter which version they're on. So we decided to again start supporting Laravel 10 and PHP 8.1.</p>
<p>Because if you were still on Laravel 10, you were stuck on an older Flare. And especially with performance monitoring and logging, that means missing out on the best stuff we've built in recent months. That's something we don't want.</p>
<h2>A chain of dependencies</h2>
<p>This wasn't one version bump. Flare on Laravel is a small stack: <code>laravel-flare</code> sits on top of <code>flare-client-php</code> (the framework-agnostic core), <code>flare-daemon</code> and <code>laravel-error-share</code>. You can widen the <code>laravel-flare</code> version compatibility all you want, but if the packages underneath still demand Laravel 11, Composer just refuses.</p>
<p>So it had to move from the bottom up. First the core and <code>flare-daemon</code> reach down to PHP 8.1, then <code>laravel-error-share</code>, then finally <code>laravel-flare</code> itself.</p>
<h2>Why stop at PHP 8.1?</h2>
<p>PHP 8.1 is where we drew the line, and that's on purpose.</p>
<p>Enums landed in PHP 8.1, and we use them everywhere across the codebase. Going lower would mean losing them and replacing the enums with something clunkier. We'd do that with pain in the heart, and it would force a new major release on everyone, something we recently did with version three of the Flare packages and something we hope to avoid in the future.</p>
<p>Laravel 9 and lower didn't make the cut either, and here the reason is very practical. Testbench for Laravel 9 has no workbench, the little app we boot to run our integration tests against. And that integration test is the whole reason this port was even doable, but we come back to that later on.</p>
<h2>Letting AI do the tedious part</h2>
<p>There weren't a lot of changes we needed. For the framework agnostic client, it was merely changing some dependencies and we were done.</p>
<p>Downgrading the Laravel dependency was a bit more difficult:</p>
<ul>
<li>The store names were missing from cache events</li>
<li>Specific container hooks we've been using, like "registered", don't exist</li>
<li>The queueable trait wasn't invented yet</li>
<li>And a few smaller things</li>
</ul>
<p>Most of the fixes were small, but there were a lot of them, spread across source, tests, workbench and CI. The perfect work for an AI agent. Not because it's clever, but because it's a grind with a tight feedback loop.</p>
<p>And that loop is the fun part.</p>
<h2>Our massive integration test</h2>
<p>Flare has one integration test that does an absurd amount. It boots a real Laravel app, starts an HTTP server and a queue worker, fires requests, runs jobs, and asserts that the exact spans, logs and errors Flare should record actually show up. Hundreds of assertions across the whole framework in one run.</p>
<p>Which makes something like this almost easy. Point the same test suite at Laravel 10 and it immediately tells you every spot where behaviour drifts.</p>
<p>So the agent could just loop: run the suite, find out what failed, fix, repeat, across the whole version matrix until everything turned green.</p>
<p>So welcome to the future of Flare, even when you're still on an older PHP or Laravel version!</p>
]]>
            </summary>
                                    <updated>2026-07-03T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Your Laravel routes can carry metadata now, and Flare shows it]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/your-laravel-routes-can-carry-metadata-now-and-flare-shows-it" />
            <id>https://flareapp.io/your-laravel-routes-can-carry-metadata-now-and-flare-shows-it</id>
            <author>
                <name><![CDATA[Ruben]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Laravel just landed a neat little feature: <a href="https://github.com/laravel/framework/pull/60530">route metadata</a>. You can attach arbitrary metadata to a route and read it back later.</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #8FBCBB;">Route</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">get</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">/users</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #8FBCBB;">UserController</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #81A1C1;">class</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">index</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">metadata</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">head</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">title</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">Users</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span></code></pre>
<p>And then somewhere down the line:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">request</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">route</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">getMetadata</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">head.title</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token"> </span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> 'Users'</span><span class="token" style="color: #616E88;">
</span></span></code></pre>
<p>Cool, right? It's a clean way to attach metadata to a route without inventing your own conventions.</p>
<h2>So where does Flare come in?</h2>
<p>When something goes wrong on a request, that route's metadata is part of the story. Maybe you flagged the route as <code>internal</code>, tagged it with a feature flag, or added some specific logic based upon the route.</p>
<p>That's exactly the kind of context you want sitting next to a stack trace when you're trying to figure out what happened.</p>
<p>The newest <code>laravel-flare</code> client (3.1.0) sends this info along with errors. Flare picks it up and shows it for you:</p>
<p><img src="https://assets.flareapp.io/production/blog/c6227d3c038f4a4eb38b8d355815ef73/ee8f61f5-430e-42fc-8661-c77e0af97b40.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/c6227d3c038f4a4eb38b8d355815ef73/ee8f61f5-430e-42fc-8661-c77e0af97b40.png" width="3690" height="1390" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/c6227d3c038f4a4eb38b8d355815ef73/ee8f61f5-430e-42fc-8661-c77e0af97b40.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/c6227d3c038f4a4eb38b8d355815ef73/ee8f61f5-430e-42fc-8661-c77e0af97b40.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/c6227d3c038f4a4eb38b8d355815ef73/ee8f61f5-430e-42fc-8661-c77e0af97b40.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/c6227d3c038f4a4eb38b8d355815ef73/ee8f61f5-430e-42fc-8661-c77e0af97b40.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>Scalar values show up inline. Nested values get rendered as JSON so you can actually read them.</p>
<h2>It works for performance traces too</h2>
<p>The same metadata flows through to performance monitoring. Open up a trace, click into the request span, and the Route panel shows the metadata right alongside everything else:</p>
<p><img src="https://assets.flareapp.io/production/blog/e142665ebdede75a423faedcbbbc9bf9/540a6d26-73b1-45ef-a895-76dc0979e79c.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/e142665ebdede75a423faedcbbbc9bf9/540a6d26-73b1-45ef-a895-76dc0979e79c.png" width="880" height="1084" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/e142665ebdede75a423faedcbbbc9bf9/540a6d26-73b1-45ef-a895-76dc0979e79c.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/e142665ebdede75a423faedcbbbc9bf9/540a6d26-73b1-45ef-a895-76dc0979e79c.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=880/https://assets.flareapp.io/production/blog/e142665ebdede75a423faedcbbbc9bf9/540a6d26-73b1-45ef-a895-76dc0979e79c.png 880w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<h2>Getting it</h2>
<p>Make sure you're on the 3.1.0 version of the <code>laravel-flare</code> client, add some metadata to your routes, and you're done.</p>
<p>Happy debugging!</p>
]]>
            </summary>
                                    <updated>2026-06-30T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Create richer issues from your errors]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/create-richer-issues-from-your-errors" />
            <id>https://flareapp.io/create-richer-issues-from-your-errors</id>
            <author>
                <name><![CDATA[Ruben]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>When an error pops up in Flare, you can turn it into an issue on GitHub, GitLab or Linear with a single click. Handy, but until now that issue landed in your tracker a little naked. No assignee. No labels. Nobody knew who should pick it up.</p>
<p>So you'd create the issue in Flare, switch to GitHub or Linear, assign the right person, slap a <code>bug</code> label on it. Every single time.</p>
<p>That's a few clicks too many. Let's fix that.</p>
<h2>Pick assignees and labels while you create</h2>
<p>From today, when you create an issue from an error you can choose the assignee and labels right there in Flare. On Linear you can also pick the project the issue should land in.</p>
<p><img src="https://assets.flareapp.io/production/blog/f721cc72eba8f3385002f7cb7330f2e3/e7746467-597b-48cb-ae37-03f1d94f70c0.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/f721cc72eba8f3385002f7cb7330f2e3/e7746467-597b-48cb-ae37-03f1d94f70c0.png" width="1636" height="2032" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/f721cc72eba8f3385002f7cb7330f2e3/e7746467-597b-48cb-ae37-03f1d94f70c0.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/f721cc72eba8f3385002f7cb7330f2e3/e7746467-597b-48cb-ae37-03f1d94f70c0.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/f721cc72eba8f3385002f7cb7330f2e3/e7746467-597b-48cb-ae37-03f1d94f70c0.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/f721cc72eba8f3385002f7cb7330f2e3/e7746467-597b-48cb-ae37-03f1d94f70c0.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>The dropdowns are filled with the people, labels and projects from your connected issue tracker. So whatever you'd normally pick over there, you can now pick here.</p>
<h2>Set defaults per project</h2>
<p>In most projects the <code>bug</code> label will be the same, just like the assignee for the error. So you can set defaults that get used the next time an issue is created.</p>
<p>Of course you're totally free to overwrite these defaults when creating an issue.</p>
<p><img src="https://assets.flareapp.io/production/blog/38afc592a8243af4432c27d6a8f230c2/3d027c70-f68f-4cc4-9ca6-3bc977befdd6.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/38afc592a8243af4432c27d6a8f230c2/3d027c70-f68f-4cc4-9ca6-3bc977befdd6.png" width="2798" height="748" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/38afc592a8243af4432c27d6a8f230c2/3d027c70-f68f-4cc4-9ca6-3bc977befdd6.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/38afc592a8243af4432c27d6a8f230c2/3d027c70-f68f-4cc4-9ca6-3bc977befdd6.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/38afc592a8243af4432c27d6a8f230c2/3d027c70-f68f-4cc4-9ca6-3bc977befdd6.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/38afc592a8243af4432c27d6a8f230c2/3d027c70-f68f-4cc4-9ca6-3bc977befdd6.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>This works on GitHub, GitLab and Linear today. Due to some bigger Jira refactors we're working on, defaults didn't make it into this round, yet you can already provide values for custom fields. Defaults for Jira will be added later this year.</p>
<p>Let us know what you think. Happy debugging!</p>
]]>
            </summary>
                                    <updated>2026-06-23T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[One core, many clients: the new Flare JavaScript client architecture]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/one-core-many-clients-the-new-flare-javascript-client-architecture" />
            <id>https://flareapp.io/one-core-many-clients-the-new-flare-javascript-client-architecture</id>
            <author>
                <name><![CDATA[Dries]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>We recently reshaped the Flare JavaScript client from a single browser library and a few thin framework specific packages into a small family of packages built on a shared, platform-agnostic core. This post explains why we did it, what the core package exposes, how the browser and Node SDKs are built on top of it, why the React, Vue, and Svelte packages sit one level higher, and how anyone can use the same core to write a Flare JS client for a platform we do not ship ourselves.</p>
<p><img src="https://assets.flareapp.io/production/blog/0a6220b210eee7a3310263df99024a2a/ed4cdf73-9e9a-4c16-85aa-5221114d7f2a.png" alt="Flare JS clients architecture" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/0a6220b210eee7a3310263df99024a2a/ed4cdf73-9e9a-4c16-85aa-5221114d7f2a.png" width="3302" height="1987" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/0a6220b210eee7a3310263df99024a2a/ed4cdf73-9e9a-4c16-85aa-5221114d7f2a.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/0a6220b210eee7a3310263df99024a2a/ed4cdf73-9e9a-4c16-85aa-5221114d7f2a.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/0a6220b210eee7a3310263df99024a2a/ed4cdf73-9e9a-4c16-85aa-5221114d7f2a.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/0a6220b210eee7a3310263df99024a2a/ed4cdf73-9e9a-4c16-85aa-5221114d7f2a.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>The diagram captures the whole idea. Blue are core utilities that only client authors consume. Yellow are the libraries an end user installs. Green are the bundler plugins for sourcemap upload, which live on their own track and are out of scope for this post.</p>
<h2>The problem: one library, too many assumptions</h2>
<p>The original <code>@flareapp/js</code> did one job well: catch errors in a browser and report them to Flare. The trouble was that "in a browser" was baked into every layer. The error pipeline reached for <code>window</code> to attach listeners, for <code>fetch</code> to read source files, and for the DOM and cookies to collect context. There was a single global place to stash breadcrumbs, because a browser tab has a single user doing one thing at a time.</p>
<p>While this works for the browser, it doesn't in a Node environment. A server process has no <code>window</code>, reads source from disk rather than over HTTP, and serves many concurrent requests at once. Each of those requests wants its own breadcrumbs and its own context, and none of it should leak into the request running next to it.</p>
<p>We had two bad options. Fork the library and maintain two copies of the same error pipeline, or thread <code>typeof window === 'undefined'</code> checks through a codebase that should actually not care in what environment or on what platform it's running. Both options become unmaintainable rather quickly, and neither leaves room for a third or fourth platform. So we decided to split the reporting pipeline into a platform agnostic core package instead.</p>
<h2>The move: extract a platform-agnostic core</h2>
<p><code>@flareapp/core</code> now owns everything about reporting an error that does not depend on where the code runs:</p>
<ul>
<li>Configuration: the API key, version, stage, sampling rate, URL denylist, and the hook callbacks.</li>
<li>Report building: turning a thrown value into a Flare <code>Report</code>, including stack-trace assembly and code-snippet extraction.</li>
<li>Glows: the breadcrumb trail leading up to an error.</li>
<li>Hooks: the <code>beforeEvaluate</code> and <code>beforeSubmit</code> pipeline that lets you drop or rewrite a report before it leaves.</li>
<li>Sampling, denylisting, and the <code>Api</code> transport that POSTs the finished report.</li>
</ul>
<p>Core has zero platform dependencies. It never touches <code>window</code>, <code>fetch</code>, <code>fs</code>, or <code>process</code>. It defines the shape of the work and leaves the platform-specific parts as holes to be filled. The <code>Flare</code> class in core is the whole pipeline, and its constructor is the contract:</p>
<pre class="phiki language-typescript nord" data-language="typescript" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #88C0D0;">constructor</span><span class="token">(</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #D8DEE9;">public</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">api</span><span class="token">: </span><span class="token" style="color: #D8DEE9;">Api</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">new</span><span class="token"> </span><span class="token" style="color: #88C0D0;">Api</span><span class="token">(</span><span class="token">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #D8DEE9;">private</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">contextCollector</span><span class="token">: </span><span class="token" style="color: #88C0D0;">ContextCollector</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token">(</span><span class="token" style="color: #ECEFF4;">{</span><span class="token" style="color: #ECEFF4;">}</span><span class="token">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #D8DEE9;">private</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">fileReader</span><span class="token">: </span><span class="token" style="color: #D8DEE9;">FileReader</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">new</span><span class="token"> </span><span class="token" style="color: #88C0D0;">NullFileReader</span><span class="token">(</span><span class="token">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #D8DEE9;">private</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">scopeProvider</span><span class="token">: </span><span class="token" style="color: #D8DEE9;">ScopeProvider</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">new</span><span class="token"> </span><span class="token" style="color: #88C0D0;">GlobalScopeProvider</span><span class="token">(</span><span class="token">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>Those four parameters are the seams. Everything that differs between a browser and a server is managed by implementing a custom provider or collector specific to the platform.</p>
<h2>What core leaves out: the four seams</h2>
<p>This is the heart of the design. Core decides <em>what</em> happens when an error is reported. The consumer decides <em>how</em> the platform-specific steps are carried out, by passing implementations of three small interfaces plus a transport.</p>
<h3>1. <code>ContextCollector</code>: what context to gather</h3>
<pre class="phiki language-typescript nord" data-language="typescript" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">export</span><span class="token"> </span><span class="token" style="color: #81A1C1;">type</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">ContextCollector</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #D8DEE9;">config</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">Readonly</span><span class="token" style="color: #ECEFF4;">&lt;</span><span class="token" style="color: #8FBCBB;">Config</span><span class="token" style="color: #ECEFF4;">&gt;</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">Attributes</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span></code></pre>
<p>A plain function. Core calls it while building every report and merges the returned attributes into the payload. The browser version reads the URL, cookies, request headers, and referrer. The Node version reads the HTTP method, path, headers, and process info. Core does not know or care which.</p>
<h3>2. <code>FileReader</code>: how to read source for snippets</h3>
<pre class="phiki language-typescript nord" data-language="typescript" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">export</span><span class="token"> </span><span class="token" style="color: #81A1C1;">interface</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">FileReader</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #88C0D0;">read</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #D8DEE9;">url</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">string</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">Promise</span><span class="token" style="color: #ECEFF4;">&lt;</span><span class="token" style="color: #8FBCBB;">string</span><span class="token"> </span><span class="token" style="color: #81A1C1;">|</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">null</span><span class="token" style="color: #ECEFF4;">&gt;</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>To show the line of code where an error was thrown, core needs the source file. How you get that file is platform-specific, so core asks through one method and accepts <code>null</code> when the source is unavailable. The browser fetches it over HTTP. Node reads it from disk. The core package caches the result and extracts the relevant lines. (While Flare resolves the code on the server with sourcemaps, this file reader is handy to provide a fallback with minified code when the sourcemaps are not uploaded to Flare.)</p>
<h3>3. <code>ScopeProvider</code>: what "current scope" means</h3>
<pre class="phiki language-typescript nord" data-language="typescript" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">export</span><span class="token"> </span><span class="token" style="color: #81A1C1;">interface</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">ScopeProvider</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #88C0D0;">active</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">Scope</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>This is the seam that was really necessary for building a good Node client. A <code>Scope</code> holds the per-call mutable state: the glows, the pending custom attributes, and the current entry point. The core Flare class never stores that state directly. It always asks the provider for the active scope.</p>
<p>In the browser there is one scope for the page, so the default <code>GlobalScopeProvider</code> returns the same <code>Scope</code> every time. In Node, a single <code>Flare</code> instance serves many concurrent requests, and each one needs its own breadcrumbs. The Node scope provider, backed by <a href="https://nodejs.org/api/async_context.html"><code>AsyncLocalStorage</code></a>, hands each in-flight request its own scope, so context never bleeds between concurrent requests. Core stays oblivious: it runs the same pipeline and calls active(), with no concept of concurrency at all.</p>
<h3>4. The injected <code>Api</code>: transport</h3>
<p>The core package ships a simple <code>fetch</code>-based <code>Api</code> that POSTs the report. Because it is injected rather than hardcoded, a consumer can replace it: a Node process might want a different timeout, an offline-first client might queue reports, and so on.</p>
<h3>Plus one thing core cannot do for you: attach the listeners</h3>
<p>Core can build and send a report once you hand it an error. It cannot decide <em>when</em> an error happens, because catching uncaught errors is inherently platform-specific. Somebody has to call <code>window.addEventListener('error', ...)</code> or <code>process.on('uncaughtException', ...)</code>. That wiring lives in each client, not in core.</p>
<h2>How <code>@flareapp/js</code> implements core</h2>
<p>The browser client is thin. It subclasses core's <code>Flare</code>, fills the seams with browser implementations, and wires up the listeners:</p>
<pre class="phiki language-typescript nord" data-language="typescript" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> packages/js/src/index.ts</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">export</span><span class="token"> </span><span class="token" style="color: #81A1C1;">class</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">Flare</span><span class="token"> </span><span class="token" style="color: #81A1C1;">extends</span><span class="token"> </span><span class="token" style="color: #8FBCBB;font-weight: bold;">CoreFlare</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">constructor</span><span class="token" style="color: #ECEFF4;">(</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #D8DEE9;">api</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">Api</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">new</span><span class="token"> </span><span class="token" style="color: #88C0D0;">Api</span><span class="token">(</span><span class="token">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #D8DEE9;">contextCollector</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">ContextCollector</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">collectBrowser</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #D8DEE9;">fileReader</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">FileReader</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">new</span><span class="token"> </span><span class="token" style="color: #88C0D0;">FetchFileReader</span><span class="token">(</span><span class="token">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #D8DEE9;">scopeProvider</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">ScopeProvider</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">new</span><span class="token"> </span><span class="token" style="color: #88C0D0;">GlobalScopeProvider</span><span class="token">(</span><span class="token">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">super</span><span class="token">(</span><span class="token" style="color: #D8DEE9;">api</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">contextCollector</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">fileReader</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">scopeProvider</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #88C0D0;">setSdkInfo</span><span class="token">(</span><span class="token" style="color: #ECEFF4;">{</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">name</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">@flareapp/js</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">version</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">CLIENT_VERSION</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">export</span><span class="token"> </span><span class="token" style="color: #81A1C1;">const</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">flare</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">new</span><span class="token"> </span><span class="token" style="color: #88C0D0;">Flare</span><span class="token">(</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">if</span><span class="token"> </span><span class="token">(</span><span class="token" style="color: #81A1C1;">typeof</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">window</span><span class="token"> </span><span class="token" style="color: #81A1C1;">!==</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">undefined</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #81A1C1;">&amp;&amp;</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">window</span><span class="token">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">    </span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> @ts-expect-error attach to window</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #D8DEE9;">window</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #D8DEE9;">flare</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">flare</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #88C0D0;">catchWindowErrors</span><span class="token">(</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p><code>collectBrowser</code> is the <code>ContextCollector</code>. <code>FetchFileReader</code> is the <code>FileReader</code>. The global scope is fine because a tab has one user. <code>catchWindowErrors()</code> attaches the <code>error</code> and <code>unhandledrejection</code> listeners and routes both into the singleton. That is the entire browser-specific surface. Every line of the error pipeline lives in core.</p>
<h2>How <code>@flareapp/node</code> implements core</h2>
<p>The Node client follows the shame shape, but injects different implementations of the required drivers. This shows why the new approach is so strong and flexible:</p>
<ul>
<li><code>DiskFileReader</code> reads source from the filesystem instead of fetching it.</li>
<li><code>AsyncLocalStorageScopeProvider</code> gives each request its own <code>Scope</code>, and falls back to a shared scope when called outside any request. This is why two requests reporting errors at the same moment do not mix up their breadcrumbs.</li>
<li>A <code>ProcessHandlerManager</code> attaches <code>process.on('uncaughtException')</code> and <code>process.on('unhandledRejection')</code>, with configurable behaviour for whether to report and exit or report and continue. Before exiting, it calls <code>flare.flush()</code> to wait for in-flight reports to finish sending, so a crash does not drop the very report that explains it.</li>
</ul>
<p>The Node client also extends <code>Scope</code> into a <code>NodeScope</code> with extra buckets for the request (method, path, headers) and the user (id, email).</p>
<h2>How React, Vue, and Svelte ride on JS, not core</h2>
<p>Here is a distinction the diagram makes on purpose. React, Vue, and Svelte are not new platforms. They all run in the browser. So they do not implement core. They depend on <code>@flareapp/js</code> as a peer dependency and reuse its <code>flare</code> singleton:</p>
<pre class="phiki language-typescript nord" data-language="typescript" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">import</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">flare</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">}</span><span class="token"> </span><span class="token" style="color: #81A1C1;">from</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">@flareapp/js</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span></code></pre>
<p>What each framework package adds is small and framework-shaped: a way to catch the errors the framework swallows before they ever reach <code>window.onerror</code>.</p>
<ul>
<li><code>@flareapp/react</code> ships a <code>FlareErrorBoundary</code> component that hooks <code>getDerivedStateFromError</code> and <code>componentDidCatch</code>, capturing the React component stack and forwarding it to <code>flare</code>.</li>
<li><code>@flareapp/vue</code> ships a <code>flareVue</code> plugin that installs <code>app.config.errorHandler</code> and adds component-hierarchy and route context.</li>
<li><code>@flareapp/svelte</code> and <code>@flareapp/sveltekit</code> ship a boundary component and error hooks, plus prop serialisation.</li>
</ul>
<p>None of them re-implements stack-trace parsing, context collection, or transport. They catch a framework-specific error, add framework-specific context, and hand it to the same browser client every other browser page uses. That is why they sit a level below JS in the diagram, not next to it.</p>
<h2>Build your own client: a React Native client</h2>
<p>To showcase how easy it is to build a new client, we'll implement a minimal React Native client. React Native runs JavaScript with no DOM, catches errors through its own <code>ErrorUtils</code> global, and has a single app instance with one user. That maps cleanly onto the seams.</p>
<h3>Step 1: the context collector</h3>
<pre class="phiki language-typescript nord" data-language="typescript" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">import</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">Platform</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">}</span><span class="token"> </span><span class="token" style="color: #81A1C1;">from</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">react-native</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">import</span><span class="token"> </span><span class="token" style="color: #81A1C1;">type</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">ContextCollector</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">Attributes</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">}</span><span class="token"> </span><span class="token" style="color: #81A1C1;">from</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">@flareapp/core</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">export</span><span class="token"> </span><span class="token" style="color: #81A1C1;">const</span><span class="token"> </span><span class="token" style="color: #88C0D0;">collectReactNative</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">ContextCollector</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">Attributes</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token">(</span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">flare.entry_point.type</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">mobile</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">os.type</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">Platform</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #D8DEE9;">OS</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">os.version</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #88C0D0;">String</span><span class="token">(</span><span class="token" style="color: #D8DEE9;">Platform</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #D8DEE9;">Version</span><span class="token">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span></code></pre>
<p>A function in, attributes out. We tag the entry point as mobile and add a little device context. That is all core asks for.</p>
<h3>Step 2: the file reader</h3>
<p>The <code>FileReader</code> produces the code snippet around the failing line, and it is worth being precise about what that snippet is for. In the browser, <code>FetchFileReader</code> fetches the <em>minified</em> bundle the frame points at and sends that minified snippet along with the frame's line and column. It does not unminify anything on the client. The mapping back to your original source happens on Flare's servers: when a sourcemap is uploaded for that build, the backend remaps each frame's line and column and rebuilds the snippet from the sourcemap, discarding whatever minified snippet the client sent. The snippet the client produces is really a fallback, shown only when no sourcemap exists for that build.</p>
<p>That tells us what a React Native reader has to do, and what it can skip. In development Metro serves the bundle over HTTP, so we fetch it exactly like the browser does:</p>
<pre class="phiki language-typescript nord" data-language="typescript" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">import</span><span class="token"> </span><span class="token" style="color: #81A1C1;">type</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">FileReader</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">}</span><span class="token"> </span><span class="token" style="color: #81A1C1;">from</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">@flareapp/core</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">export</span><span class="token"> </span><span class="token" style="color: #81A1C1;">class</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">MetroFileReader</span><span class="token"> </span><span class="token" style="color: #81A1C1;">implements</span><span class="token"> </span><span class="token" style="color: #8FBCBB;font-weight: bold;">FileReader</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">async</span><span class="token"> </span><span class="token" style="color: #88C0D0;">read</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #D8DEE9;">url</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">string</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">Promise</span><span class="token" style="color: #ECEFF4;">&lt;</span><span class="token" style="color: #8FBCBB;">string</span><span class="token"> </span><span class="token" style="color: #81A1C1;">|</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">null</span><span class="token" style="color: #ECEFF4;">&gt;</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">if</span><span class="token"> </span><span class="token">(</span><span class="token" style="color: #81A1C1;">!</span><span class="token" style="color: #D8DEE9;">url</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #88C0D0;">startsWith</span><span class="token">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">http://</span><span class="token" style="color: #ECEFF4;">'</span><span class="token">)</span><span class="token"> </span><span class="token" style="color: #81A1C1;">&amp;&amp;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">!</span><span class="token" style="color: #D8DEE9;">url</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #88C0D0;">startsWith</span><span class="token">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">https://</span><span class="token" style="color: #ECEFF4;">'</span><span class="token">)</span><span class="token">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">return</span><span class="token"> </span><span class="token" style="color: #81A1C1;">null</span><span class="token" style="color: #81A1C1;">;</span><span class="token"> </span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> production bundle, leave it to Flare to resolve sourcemaps</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">try</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">const</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">response</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">await</span><span class="token"> </span><span class="token" style="color: #88C0D0;">fetch</span><span class="token">(</span><span class="token" style="color: #D8DEE9;">url</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">return</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">response</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #D8DEE9;">ok</span><span class="token"> </span><span class="token" style="color: #81A1C1;">?</span><span class="token"> </span><span class="token" style="color: #81A1C1;">await</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">response</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #88C0D0;">text</span><span class="token">(</span><span class="token">)</span><span class="token"> </span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #81A1C1;">null</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #ECEFF4;">}</span><span class="token"> </span><span class="token" style="color: #81A1C1;">catch</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">return</span><span class="token"> </span><span class="token" style="color: #81A1C1;">null</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>In a production build the bundle lives on the device as an AOT compiled HBC bytecode file, which is impossible to get a fallback snippet from and the frame URLs are not something we can fetch over HTTP, so <code>read()</code> returns <code>null</code>. That is fine, as long as you upload your sourcemaps to Flare, the backend maps each frame to the original file, line, and column from the line and column numbers alone, and reconstructs the original snippet itself.</p>
<h3>Step 3: the scope provider</h3>
<p>A mobile app has one user and one logical context, exactly like a browser tab. So we do not need to write a provider at all. We reuse the one core already ships:</p>
<pre class="phiki language-typescript nord" data-language="typescript" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">import</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">GlobalScopeProvider</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">}</span><span class="token"> </span><span class="token" style="color: #81A1C1;">from</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">@flareapp/core</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span></code></pre>
<p>You only implement the seams your platform actually needs. The per-request <code>AsyncLocalStorage</code> machinery is Node's concern, and a React Native author doesn't need to think about it.</p>
<h3>Step 4: assemble the client</h3>
<pre class="phiki language-typescript nord" data-language="typescript" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> packages/react-native/src/index.ts</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">import</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">Api</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">Flare</span><span class="token"> </span><span class="token" style="color: #81A1C1;">as</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">CoreFlare</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">GlobalScopeProvider</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">}</span><span class="token"> </span><span class="token" style="color: #81A1C1;">from</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">@flareapp/core</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">import</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">collectReactNative</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">}</span><span class="token"> </span><span class="token" style="color: #81A1C1;">from</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">./collectReactNative</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">import</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">MetroFileReader</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">}</span><span class="token"> </span><span class="token" style="color: #81A1C1;">from</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">./MetroFileReader</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">import</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">PACKAGE_VERSION</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">}</span><span class="token"> </span><span class="token" style="color: #81A1C1;">from</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">./env</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">export</span><span class="token"> </span><span class="token" style="color: #81A1C1;">class</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">Flare</span><span class="token"> </span><span class="token" style="color: #81A1C1;">extends</span><span class="token"> </span><span class="token" style="color: #8FBCBB;font-weight: bold;">CoreFlare</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">constructor</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">super</span><span class="token">(</span><span class="token">
</span></span><span class="line"><span class="token">          </span><span class="token" style="color: #81A1C1;">new</span><span class="token"> </span><span class="token" style="color: #88C0D0;">Api</span><span class="token">(</span><span class="token">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> 
</span></span><span class="line"><span class="token">          </span><span class="token" style="color: #D8DEE9;">collectReactNative</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> 
</span></span><span class="line"><span class="token">          </span><span class="token" style="color: #81A1C1;">new</span><span class="token"> </span><span class="token" style="color: #88C0D0;">MetroFileReader</span><span class="token">(</span><span class="token">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> 
</span></span><span class="line"><span class="token">          </span><span class="token" style="color: #81A1C1;">new</span><span class="token"> </span><span class="token" style="color: #88C0D0;">GlobalScopeProvider</span><span class="token">(</span><span class="token">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #88C0D0;">setSdkInfo</span><span class="token">(</span><span class="token" style="color: #ECEFF4;">{</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">name</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">@flareapp/react-native</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">version</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">PACKAGE_VERSION</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">export</span><span class="token"> </span><span class="token" style="color: #81A1C1;">const</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">flare</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">new</span><span class="token"> </span><span class="token" style="color: #88C0D0;">Flare</span><span class="token">(</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span></code></pre>
<h3>Step 5: catch the errors</h3>
<p>The only platform-specific wiring left is hooking React Native's global error handler and forwarding to the pipeline:</p>
<pre class="phiki language-typescript nord" data-language="typescript" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> packages/react-native/src/index.ts</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">const</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">previousHandler</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">ErrorUtils</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #88C0D0;">getGlobalHandler</span><span class="token">(</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #D8DEE9;">ErrorUtils</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #88C0D0;">setGlobalHandler</span><span class="token">(</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #D8DEE9;">error</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">isFatal</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #D8DEE9;">flare</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #88C0D0;">reportSilently</span><span class="token">(</span><span class="token" style="color: #D8DEE9;">error</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #88C0D0;">previousHandler</span><span class="token" style="color: #ECEFF4;">?.</span><span class="token">(</span><span class="token" style="color: #D8DEE9;">error</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">isFatal</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span></code></pre>
<p>There you have it, a working and completely new Flare client. Three small files, one of which is a single import, plus a handful of lines to attach the handler. Everything else, the stack-trace parsing, the glows, the hooks, the sampling, the transport, comes from core unchanged.</p>
<h2>Why this makes new clients cheap</h2>
<p>The payoff is that the expensive, fiddly, easy-to-get-wrong part of an error tracker, the reporting pipeline, is written once and shared. A new client is no longer a rewrite. It is a few implementations of small interfaces:</p>
<ul>
<li>A <code>ContextCollector</code> function that returns whatever context the platform can offer.</li>
<li>A <code>FileReader</code> that returns source when it can and <code>null</code> when it cannot.</li>
<li>A <code>ScopeProvider</code>, but only if "current scope" means something other than "one global scope", which it usually does not.</li>
<li>A bit of glue to catch the platform's uncaught errors.</li>
</ul>
<p>Each seam is independently understandable and independently testable. You can reason about the context collector without knowing anything about stack traces, and you can swap the file reader without touching the scope logic. That isolation is what keeps the browser and Node clients as thin as they are, and it is what makes many more clients possible.</p>
<p>We should be honest about the edges. Core today is deliberately lean, and it does not yet cover everything the other error trackers out there offer: automatic breadcrumb capture, built-in user identification across platforms, transport-level retry and batching, and report deduplication are all still on the roadmap. The architecture is what makes adding them attractive and easy: anything we build into core lands in every client at once, browser, Node, React, Vue, Svelte, and whatever we decide to build next.</p>
]]>
            </summary>
                                    <updated>2026-06-11T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Copy-pasteable SQL in your Laravel query exceptions]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/copy-pasteable-sql-in-your-laravel-query-exceptions" />
            <id>https://flareapp.io/copy-pasteable-sql-in-your-laravel-query-exceptions</id>
            <author>
                <name><![CDATA[Ruben]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Today we're shipping a small but lovely upgrade to Flare: every query exception now comes with a SQL statement you can actually copy and run.</p>
<p>Here's what query exceptions used to look like:</p>
<p><img src="https://assets.flareapp.io/production/blog/49baf3080d6fce32190784ca55117ea6/629c49ad-790e-40eb-b6e1-3f9bc41346c2.png" alt="Before" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/49baf3080d6fce32190784ca55117ea6/629c49ad-790e-40eb-b6e1-3f9bc41346c2.png" width="2010" height="552" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/49baf3080d6fce32190784ca55117ea6/629c49ad-790e-40eb-b6e1-3f9bc41346c2.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/49baf3080d6fce32190784ca55117ea6/629c49ad-790e-40eb-b6e1-3f9bc41346c2.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/49baf3080d6fce32190784ca55117ea6/629c49ad-790e-40eb-b6e1-3f9bc41346c2.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/49baf3080d6fce32190784ca55117ea6/629c49ad-790e-40eb-b6e1-3f9bc41346c2.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>Query exceptions in Flare have shown the executed SQL for years. Can you spot the problem? Those UUIDs are strings, so they need quotes. Paste this into your database client and you get a syntax error. Laravel builds the message that way on purpose (exception constructors have to stay cheap and safe), so the bindings go in raw.</p>
<p>We weren't happy with that. Flare is where you go to fix things fast, and a broken SQL string slows you down every time you hit one.</p>
<p>So we added a step in the newest <a href="https://github.com/spatie/laravel-flare/releases/tag/3.0.1">laravel-flare client</a>. Before the message is sent to Flare, we walk the bindings and format them properly:</p>
<p><img src="https://assets.flareapp.io/production/blog/ab261bd51158a14d046593853a6bb5af/9a738df2-6a59-4bb0-8e31-be0bfe4cd2fd.png" alt="After" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/ab261bd51158a14d046593853a6bb5af/9a738df2-6a59-4bb0-8e31-be0bfe4cd2fd.png" width="2012" height="546" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/ab261bd51158a14d046593853a6bb5af/9a738df2-6a59-4bb0-8e31-be0bfe4cd2fd.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/ab261bd51158a14d046593853a6bb5af/9a738df2-6a59-4bb0-8e31-be0bfe4cd2fd.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/ab261bd51158a14d046593853a6bb5af/9a738df2-6a59-4bb0-8e31-be0bfe4cd2fd.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/ab261bd51158a14d046593853a6bb5af/9a738df2-6a59-4bb0-8e31-be0bfe4cd2fd.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>Click, copy, paste, run. The way it should have always been.</p>
<p>Happy debugging!</p>
]]>
            </summary>
                                    <updated>2026-05-27T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Flare ❤️ Livewire]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/flare-livewire" />
            <id>https://flareapp.io/flare-livewire</id>
            <author>
                <name><![CDATA[Ruben]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>A lot of Laravel developers use Livewire to build their interactive frontends. Write some PHP, sprinkle in a few <code>wire:click</code> attributes, and things just work. It's great.</p>
<p>But when something goes wrong? That's where it gets painful. Stack trace frames point to hashed filenames in Livewire's compile cache. Components can be nested in other components, creating a complex hierarchy. You have no visibility into which component is running when, or how long it's taking. Your components are basically invisible to most debugging tools.</p>
<p>We built deep Livewire support into Flare. Let's take a look at what that gives you.</p>
<h2>Livewire components in your traces</h2>
<p>When you open a trace for a page that uses Livewire, you don't just see the HTTP request. You see every component that was involved, nested in the timeline exactly where it ran.</p>
<p>Let's say you have a page with a task board. The trace shows the full hierarchy: <code>TaskBoard</code> mounts, triggers three <code>TaskColumn</code> components, and each of those renders its own <code>TaskCard</code> children. Database queries show up nested under the component that ran them.</p>
<p><img src="https://assets.flareapp.io/production/blog/d01369a18b53d0888f86dafaaab4e6a4/28c0eebd-a304-4a8c-8204-19de4665dbf9.png" alt="Timeline view of a Livewire trace showing nested components and queries" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/d01369a18b53d0888f86dafaaab4e6a4/28c0eebd-a304-4a8c-8204-19de4665dbf9.png" width="1712" height="1340" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/d01369a18b53d0888f86dafaaab4e6a4/28c0eebd-a304-4a8c-8204-19de4665dbf9.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/d01369a18b53d0888f86dafaaab4e6a4/28c0eebd-a304-4a8c-8204-19de4665dbf9.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/d01369a18b53d0888f86dafaaab4e6a4/28c0eebd-a304-4a8c-8204-19de4665dbf9.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/d01369a18b53d0888f86dafaaab4e6a4/28c0eebd-a304-4a8c-8204-19de4665dbf9.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>Whether you use a regular Livewire component with a separate view, a class-only component with an inline render method, or a single-file component, Flare understands it. You get the same visibility and insights regardless of how you structure your components.</p>
<h2>Phase-by-phase breakdown</h2>
<p>Every Livewire component goes through a lifecycle: mounting, hydrating, calling methods, rendering, and dehydrating. Flare breaks this down for you.</p>
<p>Each phase gets its own span with timing. So you can immediately see whether a component is spending time in the mount phase, the render phase, or somewhere else entirely.</p>
<p><img src="https://assets.flareapp.io/production/blog/b3c9a8a0d87b91a32fab31d2d11705ba/1f2b7a89-ca17-491d-ae96-c9c50d0f2e9a.png" alt="Timeline view zoomed in on a single component showing mount, render, and dehydrate spans with timings" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/b3c9a8a0d87b91a32fab31d2d11705ba/1f2b7a89-ca17-491d-ae96-c9c50d0f2e9a.png" width="1736" height="386" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/b3c9a8a0d87b91a32fab31d2d11705ba/1f2b7a89-ca17-491d-ae96-c9c50d0f2e9a.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/b3c9a8a0d87b91a32fab31d2d11705ba/1f2b7a89-ca17-491d-ae96-c9c50d0f2e9a.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/b3c9a8a0d87b91a32fab31d2d11705ba/1f2b7a89-ca17-491d-ae96-c9c50d0f2e9a.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/b3c9a8a0d87b91a32fab31d2d11705ba/1f2b7a89-ca17-491d-ae96-c9c50d0f2e9a.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<h2>Method calls</h2>
<p>Back on our task board, when a user clicks a card to mark it done, the <code>TaskCard</code> component's <code>toggleStatus</code> method runs. Flare records that call as its own span, with the method name and the parameters (if any). As with any other span, you can see nested spans for database queries or other operations that happen during that method call.</p>
<p><img src="https://assets.flareapp.io/production/blog/936f9aaef7ea3dff2bd0b65d742a22d7/dffeffb6-b1b0-4e5d-8e2c-f66c410b0818.png" alt="Timeline view showing a toggleStatus() method call span with parameters and nested DB query spans" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/936f9aaef7ea3dff2bd0b65d742a22d7/dffeffb6-b1b0-4e5d-8e2c-f66c410b0818.png" width="870" height="696" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/936f9aaef7ea3dff2bd0b65d742a22d7/dffeffb6-b1b0-4e5d-8e2c-f66c410b0818.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/936f9aaef7ea3dff2bd0b65d742a22d7/dffeffb6-b1b0-4e5d-8e2c-f66c410b0818.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=870/https://assets.flareapp.io/production/blog/936f9aaef7ea3dff2bd0b65d742a22d7/dffeffb6-b1b0-4e5d-8e2c-f66c410b0818.png 870w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<h2>Aggregated components</h2>
<p>Flare also aggregates components across traces, treating them as their own top-level entity.</p>
<p><img src="https://assets.flareapp.io/production/blog/f51e49a418275a68e33e2e3b1d514178/b29b2d1e-8ba8-44e9-8e80-12781b93f326.png" alt="screenshot: component overview page showing list of components with average timings and trace counts" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/f51e49a418275a68e33e2e3b1d514178/b29b2d1e-8ba8-44e9-8e80-12781b93f326.png" width="3566" height="2212" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/f51e49a418275a68e33e2e3b1d514178/b29b2d1e-8ba8-44e9-8e80-12781b93f326.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/f51e49a418275a68e33e2e3b1d514178/b29b2d1e-8ba8-44e9-8e80-12781b93f326.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/f51e49a418275a68e33e2e3b1d514178/b29b2d1e-8ba8-44e9-8e80-12781b93f326.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/f51e49a418275a68e33e2e3b1d514178/b29b2d1e-8ba8-44e9-8e80-12781b93f326.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>You get an overview of all your Livewire components with their average phase times and the number of traces they appear in. If one component is consistently slow, it'll stand out immediately.</p>
<p><img src="https://assets.flareapp.io/production/blog/b465a6f936dc340accde0ea8825e551e/ab6380d4-dd30-48b0-a4f8-f68636e010ee.png" alt="screenshot: component detail page showing phase breakdown and timings" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/b465a6f936dc340accde0ea8825e551e/ab6380d4-dd30-48b0-a4f8-f68636e010ee.png" width="2602" height="1256" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/b465a6f936dc340accde0ea8825e551e/ab6380d4-dd30-48b0-a4f8-f68636e010ee.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/b465a6f936dc340accde0ea8825e551e/ab6380d4-dd30-48b0-a4f8-f68636e010ee.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/b465a6f936dc340accde0ea8825e551e/ab6380d4-dd30-48b0-a4f8-f68636e010ee.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/b465a6f936dc340accde0ea8825e551e/ab6380d4-dd30-48b0-a4f8-f68636e010ee.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>Flare links each component to its surroundings, in both directions. You can see what uses the component:</p>
<ul>
<li>Which routes render it</li>
<li>Which views include it</li>
<li>Which other Livewire components include it</li>
</ul>
<p>And what the component uses:</p>
<ul>
<li>Which queries it runs</li>
<li>Which views it renders</li>
<li>Which components it renders</li>
<li>And more</li>
</ul>
<p>These links are always relative to the component and show the time spent in each, along with how often nested or parent links were called. So you can quickly see if a component is slow because of the queries it runs, the views it renders, or the other components it includes.</p>
<p><img src="https://assets.flareapp.io/production/blog/ce810a1913c4cb3d95e74015840a84e7/35f286eb-39ee-49d9-acdc-5808fa0cdcd5.png" alt="screenshot: route detail page showing linked components and their timings" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/ce810a1913c4cb3d95e74015840a84e7/35f286eb-39ee-49d9-acdc-5808fa0cdcd5.png" width="2622" height="854" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/ce810a1913c4cb3d95e74015840a84e7/35f286eb-39ee-49d9-acdc-5808fa0cdcd5.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/ce810a1913c4cb3d95e74015840a84e7/35f286eb-39ee-49d9-acdc-5808fa0cdcd5.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/ce810a1913c4cb3d95e74015840a84e7/35f286eb-39ee-49d9-acdc-5808fa0cdcd5.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/ce810a1913c4cb3d95e74015840a84e7/35f286eb-39ee-49d9-acdc-5808fa0cdcd5.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<h2>Error tracking</h2>
<p>When an error occurs during a Livewire request, Flare shows you the original URL the user was on, which component was involved, its data and state, and which method was being called. All the context you need, right there.</p>
<p><img src="https://assets.flareapp.io/production/blog/ed8ddd3d4b89f8d356959d362a3f8d1f/4dbe8f0f-613c-418f-a7b0-5748527de878.png" alt="screenshot: error page showing Livewire context with component class, data state, and method call info" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/ed8ddd3d4b89f8d356959d362a3f8d1f/4dbe8f0f-613c-418f-a7b0-5748527de878.png" width="3498" height="1852" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/ed8ddd3d4b89f8d356959d362a3f8d1f/4dbe8f0f-613c-418f-a7b0-5748527de878.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/ed8ddd3d4b89f8d356959d362a3f8d1f/4dbe8f0f-613c-418f-a7b0-5748527de878.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/ed8ddd3d4b89f8d356959d362a3f8d1f/4dbe8f0f-613c-418f-a7b0-5748527de878.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/ed8ddd3d4b89f8d356959d362a3f8d1f/4dbe8f0f-613c-418f-a7b0-5748527de878.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<h3>Stack traces for single-file components</h3>
<p>Livewire v4 introduced single-file components where both the PHP class and template live in one Blade file. Neat! But internally, Livewire compiles these into hashed PHP files. So when an error happens, the stack trace points to something like <code>a4f8e2c1.php</code>. Not that helpful.</p>
<p>Flare maps these back to the original source file. Your stack trace points to the actual <code>.blade.php</code>, at the correct line number. No configuration needed.</p>
<p>Want to know more about how this works? Check out our deep dive into the internals of Livewire's single-file components and how we integrate with them: <a href="https://flareapp.io/blog/how-flare-handles-livewire-v4s-single-file-components">How Flare handles Livewire v4's single-file components</a>.</p>
<h2>The small things</h2>
<p>A few more details that we think matter:</p>
<ul>
<li><strong>Livewire's JavaScript assets are excluded from traces.</strong> Requests for <code>livewire.js</code> won't pollute your performance data.</li>
<li><strong>Noisy components can be ignored.</strong> If you have a component that runs a lot and isn't relevant to your performance monitoring, you can exclude it from being collected.</li>
<li><strong>Minimal component spans</strong> can be enabled. Instead of showing the mount, render, and dehydrate phases separately, you'll see just one span for the whole component. This is useful if you want to keep your traces simpler and don't care about the phase breakdown.</li>
<li><strong>Both Livewire v3 and v4 are supported.</strong> Different internal APIs, same experience.</li>
</ul>
<h2>Getting started</h2>
<p>All of this works out of the box. Install <code>spatie/laravel-flare</code>, and if Livewire is in your project, the integrations activate automatically.</p>
<p>We use Livewire every day. We made sure Flare understands it just as well as we do.</p>
<p>Happy debugging!</p>
]]>
            </summary>
                                    <updated>2026-04-30T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Integration testing our Laravel package with a real server and queue]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/integration-testing-our-laravel-package-with-a-real-server-and-queue" />
            <id>https://flareapp.io/integration-testing-our-laravel-package-with-a-real-server-and-queue</id>
            <author>
                <name><![CDATA[Ruben]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Flare hooks into a lot of Laravel's internals: HTTP requests, views, queries, cache operations, queued jobs, Livewire components, and more. For each of these, we collect spans and send them as traces. We also catch exceptions and send error reports.</p>
<p>We're currently building new versions of the Flare packages that will also collect logs next from your application. To verify that everything works end to end in a Laravel application, we needed integration tests that go beyond mocking. We needed a real HTTP server, a real queue worker, and real payloads being sent.</p>
<p>Unit testing individual recorders of spans, logs and errors is straightforward. Laravel even provides facades for testing Http and Queue calls from the framework. The problem with them, they all run in the application currently booted.</p>
<p>We also wanted to verify that a full HTTP request through the Laravel kernel produces exactly the payloads we expect. That a queued job dispatched from a route creates a trace linked back to the originating request. That a Livewire component mount exception generates both a trace and an error report.</p>
<p>You can't really mock your way through that using facades. You need an actual Laravel application running.</p>
<h2>Setting up a workbench application</h2>
<p>When building a Laravel package, you cannot go around the Orchestra Testbench package, it's the de facto standard for testing Laravel packages. While it does a great job for unit testing, it doesn't really support the kind of integration testing we needed out of the box.</p>
<p>Luckily testbench has this great feature called workbench. It lets you scaffold a full Laravel application inside your package that you can use for testing.</p>
<p>You can install it like this:</p>
<pre class="phiki language-shellscript nord" data-language="shellscript" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #88C0D0;">composer</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">require</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">-</span><span class="token" style="color: #A3BE8C;">-dev</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">orchestra/testbench</span><span class="token">
</span></span></code></pre>
<p>It will set up a <code>workbench/</code> directory in your package. You can customize this skeleton application and use it to run real HTTP requests against it.</p>
<p>With workbench, we basically run two commands before we run our tests: <code>vendor/bin/testbench workbench:build</code> and <code>vendor/bin/testbench serve</code>. The first one prepares the skeleton, the second one starts the HTTP server.</p>
<p>The configuration for workbench lives in <code>testbench.yaml</code>:</p>
<pre class="phiki language-yaml nord" data-language="yaml" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #8FBCBB;">l</span><span class="token" style="color: #8FBCBB;">aravel</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">@testbench</span><span class="token" style="color: #ECEFF4;">'</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #8FBCBB;">p</span><span class="token" style="color: #8FBCBB;">roviders</span><span class="token" style="color: #ECEFF4;">:</span><span class="token">
</span></span><span class="line"><span class="token">  </span><span class="token" style="color: #ECEFF4;">-</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">W</span><span class="token" style="color: #A3BE8C;">orkbench\App\Providers\WorkbenchServiceProvider</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #8FBCBB;">m</span><span class="token" style="color: #8FBCBB;">igrations</span><span class="token" style="color: #ECEFF4;">:</span><span class="token">
</span></span><span class="line"><span class="token">  </span><span class="token" style="color: #ECEFF4;">-</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">w</span><span class="token" style="color: #A3BE8C;">orkbench/database/migrations</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #8FBCBB;">w</span><span class="token" style="color: #8FBCBB;">orkbench</span><span class="token" style="color: #ECEFF4;">:</span><span class="token">
</span></span><span class="line"><span class="token">  </span><span class="token" style="color: #8FBCBB;">s</span><span class="token" style="color: #8FBCBB;">tart</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">/</span><span class="token" style="color: #ECEFF4;">'</span><span class="token">
</span></span><span class="line"><span class="token">  </span><span class="token" style="color: #8FBCBB;">d</span><span class="token" style="color: #8FBCBB;">iscovers</span><span class="token" style="color: #ECEFF4;">:</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #8FBCBB;">w</span><span class="token" style="color: #8FBCBB;">eb</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #81A1C1;">true</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #8FBCBB;">a</span><span class="token" style="color: #8FBCBB;">pi</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #81A1C1;">true</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #8FBCBB;">c</span><span class="token" style="color: #8FBCBB;">ommands</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #81A1C1;">true</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #8FBCBB;">c</span><span class="token" style="color: #8FBCBB;">omponents</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #81A1C1;">true</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #8FBCBB;">f</span><span class="token" style="color: #8FBCBB;">actories</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #81A1C1;">true</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #8FBCBB;">v</span><span class="token" style="color: #8FBCBB;">iews</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #81A1C1;">true</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #8FBCBB;">c</span><span class="token" style="color: #8FBCBB;">onfig</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #81A1C1;">true</span><span class="token">
</span></span><span class="line"><span class="token">  </span><span class="token" style="color: #8FBCBB;">b</span><span class="token" style="color: #8FBCBB;">uild</span><span class="token" style="color: #ECEFF4;">:</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">-</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">a</span><span class="token" style="color: #A3BE8C;">sset-publish</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">-</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">c</span><span class="token" style="color: #A3BE8C;">reate-sqlite-db</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">-</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">d</span><span class="token" style="color: #A3BE8C;">b-wipe</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">-</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">m</span><span class="token" style="color: #A3BE8C;">igrate-fresh</span><span class="token">
</span></span><span class="line"><span class="token">  </span><span class="token" style="color: #8FBCBB;">s</span><span class="token" style="color: #8FBCBB;">ync</span><span class="token" style="color: #ECEFF4;">:</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">-</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">f</span><span class="token" style="color: #8FBCBB;">rom</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">s</span><span class="token" style="color: #A3BE8C;">torage</span><span class="token">
</span></span><span class="line"><span class="token">      </span><span class="token" style="color: #8FBCBB;">t</span><span class="token" style="color: #8FBCBB;">o</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">w</span><span class="token" style="color: #A3BE8C;">orkbench/storage</span><span class="token">
</span></span><span class="line"><span class="token">      </span><span class="token" style="color: #8FBCBB;">r</span><span class="token" style="color: #8FBCBB;">everse</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #81A1C1;">true</span><span class="token">
</span></span></code></pre>
<p>The <code>build</code> section is important. It tells testbench what to do when you build the application. In our case: publish assets, create a SQLite database, wipe it, and run migrations fresh.</p>
<p>The <code>sync</code> section creates a symlink from the testbench skeleton's storage directory to <code>workbench/storage</code>. This becomes relevant later.</p>
<p>Now we can create a real Laravel application inside <code>workbench/</code> and customize it as we like. We can add routes, controllers, jobs, views, and more.</p>
<p>Our tests will make real HTTP requests to this server, which will trigger all the normal Laravel lifecycle events and Flare instrumentation.</p>
<p>The next challenge is how to capture these payloads in our tests so we can make assertions on them?</p>
<h2>Writing payloads to disk instead of sending over HTTP</h2>
<p>Normally, Flare sends data to our API over HTTP. That's not great for testing. We don't want to hit a real API.</p>
<p>Flare has a concept of a "sender" which is responsible for sending payloads to the API. By default, it uses an HTTP sender. But we can swap this out when testing.</p>
<p>So we created a <code>FileSender</code>:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">class</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">FileSender</span><span class="token"> </span><span class="token" style="color: #81A1C1;">implements</span><span class="token"> </span><span class="token" style="color: #8FBCBB;font-weight: bold;">Sender</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">post</span><span class="token" style="color: #ECEFF4;">(</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">string</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">endpoint</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">string</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">apiToken</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">array</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">payload</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #8FBCBB;">FlareEntityType</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">type</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">bool</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">test</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #8FBCBB;">Closure</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">callback</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #81A1C1;">void</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">id</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #88C0D0;">uniqid</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">        
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #88C0D0;">file_put_contents</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">            </span><span class="token" style="color: #88C0D0;">storage_path</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #81A1C1;">{</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">type</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">value</span><span class="token" style="color: #81A1C1;">}</span><span class="token" style="color: #A3BE8C;">_</span><span class="token" style="color: #81A1C1;">{</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">id</span><span class="token" style="color: #81A1C1;">}</span><span class="token" style="color: #A3BE8C;">.json</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">            </span><span class="token" style="color: #88C0D0;">json_encode</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">payload</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">        </span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>Instead of sending payloads over HTTP, it writes them as JSON files to the storage directory. The filename encodes the type (errors, traces, or logs) and a unique ID.</p>
<h2>The ExpectSentPayloads helper</h2>
<p>Now for the fun part. We built a test helper called <code>ExpectSentPayloads</code> that ties everything together:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">workspace</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">ExpectSentPayloads</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">get</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">/exception</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">workspace</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">assertSent</span><span class="token" style="color: #ECEFF4;">(</span><span class="token">reports</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #B48EAD;">1</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token">traces</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #B48EAD;">1</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">trace</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">workspace</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">lastTrace</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">expectLaravelRequestLifecycle</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">trace</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">expectSpan</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #8FBCBB;">SpanType</span><span class="token" style="color: #81A1C1;">::</span><span class="token">Request</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">expectAttribute</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">http.response.status_code</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #B48EAD;">500</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">workspace</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">lastReport</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">expectExceptionClass</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #8FBCBB;">Exception</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #81A1C1;">class</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">expectMessage</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">Test exception</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span></code></pre>
<p>Let's take a look at how it works internally.</p>
<p>When you call <code>ExpectSentPayloads::get('/exception')</code>, the constructor does three things:</p>
<ol>
<li><strong>Cleans up</strong> any leftover payload files from a previous test</li>
<li><strong>Makes an HTTP request</strong> to <code>http://127.0.0.1:8000/exception</code> using Laravel's HTTP client</li>
<li><strong>Reads the payload files</strong> that the server wrote via <code>FileSender</code></li>
</ol>
<p>After the request, we read all files from <code>workbench/storage</code> and categorize them by type:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">foreach</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #8FBCBB;">File</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">files</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">workSpacePath</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #81A1C1;">as</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">file</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">entityType</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">match</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">true</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #88C0D0;">str_starts_with</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">file</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">getFilename</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #8FBCBB;">FlareEntityType</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">Errors</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">value</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">FlareEntityType</span><span class="token" style="color: #81A1C1;">::</span><span class="token">Errors</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #88C0D0;">str_starts_with</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">file</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">getFilename</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #8FBCBB;">FlareEntityType</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">Traces</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">value</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">FlareEntityType</span><span class="token" style="color: #81A1C1;">::</span><span class="token">Traces</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #88C0D0;">str_starts_with</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">file</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">getFilename</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #8FBCBB;">FlareEntityType</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">Logs</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">value</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">FlareEntityType</span><span class="token" style="color: #81A1C1;">::</span><span class="token">Logs</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">default</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">null</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">    </span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> Parse and store each payload...</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>The files then get parsed and stored in the <code>ExpectSentPayloads</code> instance, which provides helper methods to make assertions on them.</p>
<h2>Waiting for async work</h2>
<p>Some tests dispatch queued jobs. The HTTP request returns immediately, but the job is processed asynchronously by the queue worker. We need to wait for the job to finish before reading the payload files.</p>
<p>In order to do that we only need to observe the <code>jobs</code> table in the SQLite database. When it's empty, we know all jobs have been processed.</p>
<p>We use a backoff strategy which eventually gives up after about 10 seconds, if something went wrong and the jobs are not processed:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">backoff</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">[</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #B48EAD;">500_000</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">   </span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> 500ms</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #B48EAD;">750_000</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">   </span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> 750ms</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #B48EAD;">1_000_000</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> 1s</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #B48EAD;">1_500_000</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> 1.5s</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #B48EAD;">2_500_000</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> 2.5s</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #B48EAD;">4_000_000</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> 4s</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">while</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">true</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #88C0D0;">usleep</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">backoff</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">currentBackoffIndex</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">if</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #8FBCBB;">DB</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">table</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">jobs</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">count</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #81A1C1;">===</span><span class="token"> </span><span class="token" style="color: #B48EAD;">0</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">return</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">currentBackoffIndex</span><span class="token" style="color: #81A1C1;">++</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>This behavior needs to be enabled on test case base since not all tests dispatch jobs and waiting for jobs to be processed when you don't need to is just extra time added to your tests.</p>
<h2>What an actual test looks like</h2>
<p>Here's a test that verifies a queued job creates the right traces with proper parent-child relationships:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #88C0D0;">it</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">can handle a queued job</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">    </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">workspace</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #8FBCBB;">ExpectSentPayloads</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">get</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">        </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">/trigger-job</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">        </span><span class="token" style="color: #88C0D0;">waitUntilAllJobsAreProcessed</span><span class="token" style="color: #ECEFF4;">:</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">true</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">    </span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">    </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">workspace</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">assertSent</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #88C0D0;">traces</span><span class="token" style="color: #ECEFF4;">:</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #B48EAD;">2</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">    </span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> First trace: the HTTP request that dispatched the job</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">    </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">httpTrace</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">workspace</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">trace</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #B48EAD;">0</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">        </span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">expectLaravelRequestLifecycle</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">    </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">requestSpan</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">httpTrace</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">expectSpan</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #8FBCBB;">SpanType</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">Request</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">        </span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">expectAttribute</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">http.response.status_code</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #B48EAD;">200</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">    </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">queuingSpan</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">httpTrace</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">expectSpan</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #8FBCBB;">LaravelSpanType</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">Queueing</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">        </span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">expectParentId</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">requestSpan</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">        </span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">expectAttribute</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">laravel.job.queue.connection_name</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">database</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">        </span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">expectAttribute</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">laravel.job.name</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #8FBCBB;">SuccesJob</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #81A1C1;">class</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">    </span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> Second trace: the job itself, linked back to the HTTP trace</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">    </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">jobTrace</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">workspace</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">trace</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #B48EAD;">1</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">    </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">jobTrace</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">expectSpan</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #8FBCBB;">LaravelSpanType</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">Job</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">        </span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">expectParentId</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">queuingSpan</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">        </span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">expectTraceId</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">queuingSpan</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">span</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">traceId</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">        </span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">expectAttribute</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">laravel.job.success</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">true</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span></code></pre>
<p>Two traces come out of this: one for the HTTP request, one for the job execution. The job trace's parent span ID points back to the queueing span in the HTTP trace. That's distributed tracing working end to end.</p>
<p>When a test dispatches a queued job, we end up with two trace files: one for the HTTP request and one for the job. Our tests need to know which is which. <code>trace(0)</code> should be the HTTP trace, <code>trace(1)</code> should be the job trace.</p>
<p>We can't rely on filenames for ordering since the IDs are random. Instead, we sort by inode number. On most filesystems, files created earlier get lower inode numbers. Since the HTTP trace is always written before the job trace, this gives us a consistent ordering.</p>
<p>In total we have about 65 integration tests like this, covering HTTP requests, exceptions, views, queries, transactions, cache operations, queued jobs (including chains, batches, retries, and failures), Livewire components, log messages, and context.</p>
<h2>Auto-restarting a crashed server</h2>
<p>Sometimes a test crashes the PHP built-in server or queue. That's actually something we want to test, especially at the moment when Laravel is terminating itself.</p>
<p>We tried getting the setup working where we started a fresh server and queue for each test. While this worked perfectly locally, it was very flaky in CI. Killing the server process between tests turned out to be tricky, and sometimes a server would linger in the background and cause port conflicts.</p>
<p>We instead opted for a single long-lived server that auto-restarts when it crashes. If a request fails to connect, we attempt to restart the server and retry the request:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">try</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">response</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">client</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">get</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">endpoint</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token"> </span><span class="token" style="color: #81A1C1;">catch</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #8FBCBB;">ConnectException</span><span class="token" style="color: #ECEFF4;">|</span><span class="token" style="color: #8FBCBB;">ConnectionException</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">e</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">if</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">!</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">restartServer</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">throw</span><span class="token"> </span><span class="token" style="color: #81A1C1;">new</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">Exception</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">Workbench server is not running.</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">    </span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> Retry the request</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">response</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">client</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">get</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">endpoint</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>While not the cleanest solution, it turned out to be the most stable in CI.</p>
<h2>Conclusion</h2>
<p>This setup lets us test the full Flare instrumentation pipeline end to end. Just a real server, a real queue worker, and real payloads written to disk.</p>
<p>By using this approach we were able to catch a lot of edge cases and verify that all the different pieces of Flare work together as expected in a real Laravel application.</p>
<p>We're working on the latest changes to these new Flare packages, and we're excited to share them with the world soon.</p>
<p>For now, you can check out the code in the <a href="https://github.com/spatie/laravel-flare/pull/30">pull request</a> of the spatie/laravel-flare package.</p>
]]>
            </summary>
                                    <updated>2026-04-22T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[A minimal "Last used" login option indicator with Alpine.js]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/minimal-last-used-login-option-indicator-alpinejs" />
            <id>https://flareapp.io/minimal-last-used-login-option-indicator-alpinejs</id>
            <author>
                <name><![CDATA[Alex]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>When you come back to Flare's login page, the sign-in option you used last time has a small "Last used" pill above it. Nothing dramatic, just a quiet nudge toward the option you've used before. I added it recently and was surprised how little code it took: around 25 lines of HTML and AlpineJS, most of which already existed to render the login page. No custom code for managing localstorage or cookies.</p>
<p>The whole thing is held together by Alpine's <a href="https://alpinejs.dev/plugins/persist"><code>$persist</code> plugin</a>. It wraps a reactive variable so its value survives page reloads by mirroring it into localStorage. You declare it inside an <code>x-data</code> block and then forget about it. Reading and writing to it look identical to any other Alpine state variable.</p>
<pre class="phiki language-html nord" data-language="html" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">&lt;</span><span class="token" style="color: #81A1C1;">div</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">x-data</span><span class="token" style="color: #ECEFF4;">=</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">{ count: $persist(0) }</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #81A1C1;">&gt;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">&lt;</span><span class="token" style="color: #81A1C1;">button</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">x-on:click</span><span class="token" style="color: #ECEFF4;">=</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">count++</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #81A1C1;">&gt;</span><span class="token">count: </span><span class="token" style="color: #81A1C1;">&lt;</span><span class="token" style="color: #81A1C1;">span</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">x-text</span><span class="token" style="color: #ECEFF4;">=</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">count</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #81A1C1;">&gt;</span><span class="token" style="color: #81A1C1;">&lt;/</span><span class="token" style="color: #81A1C1;">span</span><span class="token" style="color: #81A1C1;">&gt;</span><span class="token" style="color: #81A1C1;">&lt;/</span><span class="token" style="color: #81A1C1;">button</span><span class="token" style="color: #81A1C1;">&gt;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">&lt;/</span><span class="token" style="color: #81A1C1;">div</span><span class="token" style="color: #81A1C1;">&gt;</span><span class="token">
</span></span></code></pre>
<p>My first pass at the indicator pill was the direct version. One persisted variable, bound to both the TailwindCSS <code>ring</code> class and the pill's visibility. Click a button, variable updates, everything re-renders.</p>
<pre class="phiki language-html nord" data-language="html" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">&lt;</span><span class="token" style="color: #81A1C1;">div</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">x-data</span><span class="token" style="color: #ECEFF4;">=</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">{ last: $persist('').as('last-method') }</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #81A1C1;">&gt;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">&lt;</span><span class="token" style="color: #81A1C1;">button</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #8FBCBB;">x-on:click</span><span class="token" style="color: #ECEFF4;">=</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">last = 'google'</span><span class="token" style="color: #ECEFF4;">"</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #8FBCBB;">x-bind:class</span><span class="token" style="color: #ECEFF4;">=</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">{ 'ring': last === 'google' }</span><span class="token" style="color: #ECEFF4;">"</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">&gt;</span><span class="token">
</span></span><span class="line"><span class="token">        Sign in with Google
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">&lt;</span><span class="token" style="color: #81A1C1;">span</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">x-show</span><span class="token" style="color: #ECEFF4;">=</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">last === 'google'</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #81A1C1;">&gt;</span><span class="token">Last used</span><span class="token" style="color: #81A1C1;">&lt;/</span><span class="token" style="color: #81A1C1;">span</span><span class="token" style="color: #81A1C1;">&gt;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">&lt;/</span><span class="token" style="color: #81A1C1;">button</span><span class="token" style="color: #81A1C1;">&gt;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">&lt;/</span><span class="token" style="color: #81A1C1;">div</span><span class="token" style="color: #81A1C1;">&gt;</span><span class="token">
</span></span></code></pre>
<iframe height="300" style="width: 100%;" scrolling="no" title="simple persisting last used sign in button with alpinejs" src="https://codepen.io/alexvanderbist/embed/YPGJzQv?default-tab=result&amp;theme-id=light" frameborder="no" loading="lazy" allowtransparency="true">
  See the Pen <a href="https://codepen.io/alexvanderbist/pen/YPGJzQv">
  simple persisting last used sign in button with alpinejs</a> by Alex Vanderbist (<a href="https://codepen.io/alexvanderbist">@alexvanderbist</a>)
  on <a href="https://codepen.io">CodePen</a>.
</iframe>
<p>You might've already caught the most glaring issue here: the moment you click Google, the pill immediately activates on the Google button. That's not wrong exactly, it's just not what you want, especially for an external login flow that hasn't been completed yet. The pill is supposed to say "here's what you used <em>last</em> time".</p>
<p>The fix is to split the variable in two. Keep <code>$persist</code> as the write target, but take a one-time copy into a plain Alpine variable on <code>x-init</code>. The bindings read from the copied variable, which was frozen the moment the page loaded.</p>
<pre class="phiki language-html nord" data-language="html" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">&lt;</span><span class="token" style="color: #81A1C1;">div</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">x-data</span><span class="token" style="color: #ECEFF4;">=</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">{
</span></span><span class="line"><span class="token" style="color: #A3BE8C;">        recent: $persist('').as('last-method'),
</span></span><span class="line"><span class="token" style="color: #A3BE8C;">        displayed: '',
</span></span><span class="line"><span class="token" style="color: #A3BE8C;">    }</span><span class="token" style="color: #ECEFF4;">"</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #8FBCBB;">x-init</span><span class="token" style="color: #ECEFF4;">=</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">displayed = recent</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #81A1C1;">&gt;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">&lt;</span><span class="token" style="color: #81A1C1;">button</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #8FBCBB;">x-on:click</span><span class="token" style="color: #ECEFF4;">=</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">recent = 'google'</span><span class="token" style="color: #ECEFF4;">"</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #8FBCBB;">x-bind:class</span><span class="token" style="color: #ECEFF4;">=</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">{ 'ring': displayed === 'google' }</span><span class="token" style="color: #ECEFF4;">"</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">&gt;</span><span class="token">
</span></span><span class="line"><span class="token">        Sign in with Google
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">&lt;</span><span class="token" style="color: #81A1C1;">span</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">x-show</span><span class="token" style="color: #ECEFF4;">=</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">displayed === 'google'</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #81A1C1;">&gt;</span><span class="token">Last used</span><span class="token" style="color: #81A1C1;">&lt;/</span><span class="token" style="color: #81A1C1;">span</span><span class="token" style="color: #81A1C1;">&gt;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">&lt;/</span><span class="token" style="color: #81A1C1;">button</span><span class="token" style="color: #81A1C1;">&gt;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">&lt;/</span><span class="token" style="color: #81A1C1;">div</span><span class="token" style="color: #81A1C1;">&gt;</span><span class="token">
</span></span></code></pre>
<iframe height="300" style="width: 100%;" scrolling="no" title="persisting last used sign in button with alpinejs" src="https://codepen.io/alexvanderbist/embed/vEXVYjQ?default-tab=result&amp;theme-id=light" frameborder="no" loading="lazy" allowtransparency="true">
  See the Pen <a href="https://codepen.io/alexvanderbist/pen/vEXVYjQ">
  persisting last used sign in button with alpinejs</a> by Alex Vanderbist (<a href="https://codepen.io/alexvanderbist">@alexvanderbist</a>)
  on <a href="https://codepen.io">CodePen</a>.
</iframe>
<p>Now clicking "Sign in with Google" immediately updates localStorage (so it's the value you'll see next time) but it doesn't touch the <code>displayed</code> variable, so the pill stays where it was. On the next visit, <code>x-init</code> runs again, <code>displayed</code> gets the updated value, and the pill has moved.</p>
<p>The login page also has an email/password form below the social buttons, and I deliberately don't track it with a pill, because it feels like the defautl. But if they <em>used</em> to sign in with Google and then switched back to the email and password login flow, I don't want a stale pill on Google forever either. Thats why a single <code>x-on:submit</code> on the form clears the stored value.</p>
<pre class="phiki language-html nord" data-language="html" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">&lt;</span><span class="token" style="color: #81A1C1;">form</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">x-on:submit</span><span class="token" style="color: #ECEFF4;">=</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">recent = ''</span><span class="token" style="color: #ECEFF4;">"</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">method</span><span class="token" style="color: #ECEFF4;">=</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">POST</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #81A1C1;">&gt;</span><span class="token">
</span></span><span class="line"><span class="token">    ...
</span></span><span class="line"><span class="token" style="color: #81A1C1;">&lt;/</span><span class="token" style="color: #81A1C1;">form</span><span class="token" style="color: #81A1C1;">&gt;</span><span class="token">
</span></span></code></pre>
<p>I like when a touch like this doesn't need a session, an endpoint, or a new column on <code>users</code>. The browser already has localStorage, Alpine already has <code>$persist</code>, and the whole thing just feels elegant like this.</p>
]]>
            </summary>
                                    <updated>2026-04-09T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[How Flare handles Livewire v4's single file components]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/how-flare-handles-livewire-v4s-single-file-components" />
            <id>https://flareapp.io/how-flare-handles-livewire-v4s-single-file-components</id>
            <author>
                <name><![CDATA[Ruben]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>One of Livewire v4's features is the ability to create single file components. Instead of splitting your logic across a PHP class and a Blade view, you can now write everything in a single <code>.php</code> file.</p>
<p>It's a fantastic developer experience.</p>
<p>Until something breaks.</p>
<p>When an error occurs inside a single file component, the stack trace points to a compiled file with a hashed name like <code>ab1234cd.php</code>. That's not very helpful when you're trying to figure out what went wrong. We wanted Flare to show you the <em>actual</em> source file at the <em>actual</em> line number, just like it does for regular Livewire components.</p>
<p>Here's how we made that happen.</p>
<h2>The problem with compiled files</h2>
<p>Traditional Livewire components are straightforward for Flare to handle. There's a PHP class and a Blade view. We know how to map both back to their source files.</p>
<p>Single file components are different. Livewire compiles them into anonymous classes with hashed filenames and stores them in the cache directory. When an exception is thrown, the stack trace looks something like this:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">/storage/framework/cache/livewire/ab1234cd.php:42
</span></span></code></pre>
<p>That path tells you nothing about which component failed. And line 42 in the compiled output won't correspond to line 42 in your source file either.</p>
<h2>Mapping hashes back to source files</h2>
<p>During the compilation phase, Livewire creates a cache mapping component names to their compiled classes.</p>
<p>Flare reads that cache using reflection:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">factory</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">app</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">make</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">livewire.factory</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">reflection</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">new</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">ReflectionProperty</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">factory</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">resolvedComponentCache</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">resolvedComponents</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">reflection</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">getValue</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">factory</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span></code></pre>
<p>The map looks roughly like this:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #ECEFF4;">[</span><span class="token">                                                                                               
</span></span><span class="line"><span class="token">  </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">counter</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">class@anonymous</span><span class="token" style="color: #EBCB8B;">\x00</span><span class="token" style="color: #A3BE8C;">/storage/framework/cache/livewire/ab1234cd.php:12$0</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">]</span><span class="token"> 
</span></span></code></pre>
<p>We then loop over each resolved component. An single file component's class string contains a null byte followed by the compiled file path:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">foreach</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">resolvedComponents</span><span class="token"> </span><span class="token" style="color: #81A1C1;">as</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">name</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">class</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">compiledClassPath</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #88C0D0;">preg_replace</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'/</span><span class="token" style="color: #EBCB8B;">:</span><span class="token" style="color: #EBCB8B;">\d</span><span class="token" style="color: #81A1C1;">+</span><span class="token" style="color: #EBCB8B;">\$</span><span class="token" style="color: #EBCB8B;">.</span><span class="token" style="color: #81A1C1;">*</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #ECEFF4;">/'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #88C0D0;">substr</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">class</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #88C0D0;">strpos</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">class</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #EBCB8B;">\x00</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">+</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #B48EAD;">1</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>At this point we have the hash from the compiled path, but only the component name like <code>counter</code>. We don't know where the source file lives yet. So, we ask Livewire's <code>Finder</code> to resolve that:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">sourcePath</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">livewireComponentFinder</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">findSingleFileComponentFile</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">name</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">if</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #88C0D0;">preg_match</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'/</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #EBCB8B;">\/\\\\</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #EBCB8B;">(</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #EBCB8B;">a-f0-9</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #81A1C1;">+</span><span class="token" style="color: #EBCB8B;">)</span><span class="token" style="color: #EBCB8B;">\.</span><span class="token" style="color: #EBCB8B;">php</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #ECEFF4;">/'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">compiledClassPath</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">matches</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">compiledPathMap</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">matches</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #B48EAD;">1</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">]</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">sourcePath</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>The end result is a simple reverse map:</p>
<p><code>"ab1234cd" =&gt; "/resources/views/livewire/counter.php"</code></p>
<p>When Flare later encounters a hashed path in a stack trace, it checks this map and swaps in the real file path. The map is built lazily, only when an single file component is present in the current request. If you're not using single file components, none of this code runs.</p>
<h2>Fixing stack traces</h2>
<p>With the reverse map in place, we need to hook it into Flare's stack trace processing. The <code>LaravelStacktraceMapper</code> walks every frame in a stack trace and checks if it points to a compiled view:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">map</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">array</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">frames</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #81A1C1;">?</span><span class="token" style="color: #8FBCBB;">Throwable</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">throwable</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #81A1C1;">array</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">frames</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #88C0D0;">array_map</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">function</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #8FBCBB;">Frame</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">frame</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">        </span><span class="token" style="color: #81A1C1;">if</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">originalPath</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">viewFrameMapper</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">findCompiledView</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">frame</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">file</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">            </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">frame</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">file</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">originalPath</span><span class="token" style="color: #81A1C1;">;</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">            </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">frame</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">lineNumber</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">viewFrameMapper</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">getBladeLineNumber</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">                </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">frame</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">file</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">                </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">frame</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">lineNumber</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">            </span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">        </span><span class="token" style="color: #ECEFF4;">}</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">        </span><span class="token" style="color: #81A1C1;">return</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">frame</span><span class="token" style="color: #81A1C1;">;</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">frames</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">return</span><span class="token"> </span><span class="token" style="color: #81A1C1;">parent</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">map</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">frames</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">throwable</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>If a frame's file path matches a hash in our reverse map, we replace it with the real source path.</p>
<p>Next up is fixing the line number, which at this point is still the line number of the compiled Blade component.</p>
<p>Flare already knows how to map compiled Blade line numbers back to source line numbers. We use the same <code>getBladeLineNumber</code> logic that we've always used for regular Blade views. Since an single file component is just a Blade view under the hood, no new line number mapping was needed. We just had to get to the real file path first.</p>
<h2>Tagging traces correctly</h2>
<p>Flare doesn't just fix stack traces. It also records performance traces of your application. A trace is a timeline of everything that happens during a request, broken down into individual spans. Each Livewire component gets its own span, showing when it mounted, hydrated, and rendered.</p>
<p>When a component starts rendering, the <code>LivewireRecorder</code> checks whether it's a single file component and tags the span accordingly:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">isSingleFileComponent</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">livewireComponentFinder</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">isSingleFileComponent</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">component</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">attributes</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">[</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">livewire.component.name</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">component</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">livewire.component.single_file_component</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">isSingleFileComponent</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">if</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">!</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">isSingleFileComponent</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">attributes</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">livewire.component.class</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">]</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">class</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>For class-based components, Flare records the class name. For single file components, there is no meaningful class name, so we resolve the source file path from the component name and record that instead:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">if</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">componentState</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">isSingleFileComponent</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">&amp;&amp;</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">viewFile</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">livewireComponentFinder</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">findSingleFileComponentFile</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">component</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">getName</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">componentState</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">span</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">addAttribute</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">view.file</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">viewFile</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>The same happens for view spans. When the <code>ViewRecorder</code> detects that the current view belongs to a single file component, it replaces the compiled file path with the real source file and tags the span:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">singleFileComponentFile</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">livewireComponentFinder</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">findCurrentSingleFileComponentFile</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">if</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">singleFileComponentFile</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">file</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">singleFileComponentFile</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">attributes</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">view.is_livewire_single_file_component</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">]</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">true</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>This way, every span in your traces links back to the right file, whether it's a class-based component or an single file component.</p>
<h2>Built for Laravel</h2>
<p>Flare is <em>the</em> error tracker and performance monitoring platform  for Laravel. Every feature we build, including single file component support, is designed to work the way Laravel developers expect.</p>
<p>As the framework evolves, so does Flare. We're committed to being deeply integrated with the Laravel ecosystem, so you never have to wrestle with your tools to understand your application.</p>
]]>
            </summary>
                                    <updated>2026-04-08T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Track frontend errors back to the exact commit]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/track-frontend-errors-back-to-the-exact-commit" />
            <id>https://flareapp.io/track-frontend-errors-back-to-the-exact-commit</id>
            <author>
                <name><![CDATA[Dries]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>When Flare's JavaScript SDK reports an error, it includes a version identifier. This version ties the error to a specific sourcemap upload so we can show you clean, readable stacktraces instead of minified code.</p>
<p>By default, our Vite and Webpack plugins generate a random UUID for each build. That gets the job done, but when you're staring at an error in Flare, a version like <code>a3f8e2b1-7c4d-4e9f-b6a1-2d5f8c3e7a9b</code> doesn't tell you much.</p>
<p>There's a better option: use your git commit hash as the version identifier. Instead of a random string, you'll see something like <code>e4a7c3f</code> next to every error. Run <code>git log e4a7c3f</code> and you're looking at the exact code that was deployed when things went wrong.</p>
<p>Let's set it up.</p>
<h2>Getting the commit hash at build time</h2>
<p>Both our Vite and Webpack plugins run during your build process, so you can grab the current commit hash right there in your config file:</p>
<pre class="phiki language-javascript nord" data-language="javascript" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">import</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">execSync</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">}</span><span class="token"> </span><span class="token" style="color: #81A1C1;">from</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">child_process</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">const</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">commitHash</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #88C0D0;">execSync</span><span class="token">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">git rev-parse --short HEAD</span><span class="token" style="color: #ECEFF4;">'</span><span class="token">)</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #88C0D0;">toString</span><span class="token">(</span><span class="token">)</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #88C0D0;">trimEnd</span><span class="token">(</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span></code></pre>
<p>This gives you a short, 7-character hash like <code>e4a7c3f</code>. If you'd rather use the full 40-character hash, just drop the <code>--short</code> flag.</p>
<h2>Vite</h2>
<p>Pass the commit hash as the <code>version</code> option in your <code>vite.config.js</code>:</p>
<pre class="phiki language-javascript nord" data-language="javascript" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">import</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">defineConfig</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">}</span><span class="token"> </span><span class="token" style="color: #81A1C1;">from</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">vite</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">import</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">execSync</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">}</span><span class="token"> </span><span class="token" style="color: #81A1C1;">from</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">child_process</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">import</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">flareSourcemapUploader</span><span class="token"> </span><span class="token" style="color: #81A1C1;">from</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">@flareapp/vite</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">const</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">commitHash</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #88C0D0;">execSync</span><span class="token">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">git rev-parse --short HEAD</span><span class="token" style="color: #ECEFF4;">'</span><span class="token">)</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #88C0D0;">toString</span><span class="token">(</span><span class="token">)</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #88C0D0;">trimEnd</span><span class="token">(</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">export</span><span class="token"> </span><span class="token" style="color: #81A1C1;">default</span><span class="token"> </span><span class="token" style="color: #88C0D0;">defineConfig</span><span class="token">(</span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #88C0D0;">plugins</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token">[</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #88C0D0;">flareSourcemapUploader</span><span class="token">(</span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #88C0D0;">key</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">YOUR-FLARE-PROJECT-KEY</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #88C0D0;">version</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">commitHash</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token">]</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span></code></pre>
<p>Every build now tags its sourcemaps with the commit hash, and the Flare client automatically sends that same hash along with every error report.</p>
<h2>Webpack</h2>
<p>For Webpack, the option is called <code>versionId</code>:</p>
<pre class="phiki language-javascript nord" data-language="javascript" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">const</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">execSync</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">}</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #88C0D0;">require</span><span class="token">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">child_process</span><span class="token" style="color: #ECEFF4;">'</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">const</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">FlareWebpackPluginSourcemap</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #88C0D0;">require</span><span class="token">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">@flareapp/flare-webpack-plugin-sourcemap</span><span class="token" style="color: #ECEFF4;">'</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">const</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">commitHash</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #88C0D0;">execSync</span><span class="token">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">git rev-parse --short HEAD</span><span class="token" style="color: #ECEFF4;">'</span><span class="token">)</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #88C0D0;">toString</span><span class="token">(</span><span class="token">)</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #88C0D0;">trimEnd</span><span class="token">(</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #8FBCBB;">module</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #8FBCBB;">exports</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #88C0D0;">plugins</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token">[</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">new</span><span class="token"> </span><span class="token" style="color: #88C0D0;">FlareWebpackPluginSourcemap</span><span class="token">(</span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #88C0D0;">key</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">YOUR-FLARE-PROJECT-KEY</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #88C0D0;">versionId</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">commitHash</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token">]</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #88C0D0;">devtool</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">hidden-source-map</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span></code></pre>
<p>If you're using Laravel Mix, the same plugin works in your <code>webpack.mix.js</code>:</p>
<pre class="phiki language-javascript nord" data-language="javascript" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">const</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">execSync</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">}</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #88C0D0;">require</span><span class="token">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">child_process</span><span class="token" style="color: #ECEFF4;">'</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">const</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">FlareWebpackPluginSourcemap</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #88C0D0;">require</span><span class="token">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">@flareapp/flare-webpack-plugin-sourcemap</span><span class="token" style="color: #ECEFF4;">'</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">const</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">commitHash</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #88C0D0;">execSync</span><span class="token">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">git rev-parse --short HEAD</span><span class="token" style="color: #ECEFF4;">'</span><span class="token">)</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #88C0D0;">toString</span><span class="token">(</span><span class="token">)</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #88C0D0;">trimEnd</span><span class="token">(</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #D8DEE9;">mix</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #88C0D0;">webpackConfig</span><span class="token">(</span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #88C0D0;">plugins</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token">[</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">new</span><span class="token"> </span><span class="token" style="color: #88C0D0;">FlareWebpackPluginSourcemap</span><span class="token">(</span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">                </span><span class="token" style="color: #88C0D0;">key</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">YOUR-FLARE-PROJECT-KEY</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">                </span><span class="token" style="color: #88C0D0;">versionId</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">commitHash</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token">]</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">)</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #88C0D0;">sourceMaps</span><span class="token">(</span><span class="token" style="color: #81A1C1;">true</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">hidden-source-map</span><span class="token" style="color: #ECEFF4;">'</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span></code></pre>
<h2>Using CI environment variables</h2>
<p>Most CI providers already expose the commit hash as an environment variable, so you don't need to shell out to git at all:</p>
<pre class="phiki language-javascript nord" data-language="javascript" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">const</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">commitHash</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">process</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #D8DEE9;">env</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #D8DEE9;">GITHUB_SHA</span><span class="token" style="color: #ECEFF4;">?.</span><span class="token" style="color: #88C0D0;">slice</span><span class="token">(</span><span class="token" style="color: #B48EAD;">0</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #B48EAD;">7</span><span class="token">)</span><span class="token">   </span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> GitHub Actions</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">??</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">process</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #D8DEE9;">env</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #D8DEE9;">CI_COMMIT_SHORT_SHA</span><span class="token">                   </span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> GitLab CI</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">??</span><span class="token"> </span><span class="token" style="color: #88C0D0;">execSync</span><span class="token">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">git rev-parse --short HEAD</span><span class="token" style="color: #ECEFF4;">'</span><span class="token">)</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #88C0D0;">toString</span><span class="token">(</span><span class="token">)</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #88C0D0;">trimEnd</span><span class="token">(</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span></code></pre>
<p>This uses the CI-provided value when available and falls back to the git command for local builds.</p>
<h2>What this looks like in practice</h2>
<p>Once deployed, every JavaScript error in Flare shows the commit hash that produced the build. You see an error spiking after a deploy, check the version, and you're one <code>git show</code> away from reading the exact diff that caused it.</p>
<p>No more cross-referencing deploy logs or guessing based on timestamps. Just a direct link from error to commit.</p>
]]>
            </summary>
                                    <updated>2026-03-25T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[A unified error debug timeline]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/a-unified-error-debug-timeline" />
            <id>https://flareapp.io/a-unified-error-debug-timeline</id>
            <author>
                <name><![CDATA[Ruben]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>When you're debugging an error, context is everything. Knowing that a query failed is useful, but knowing that it failed right after a Redis cache miss, tells a completely different story.</p>
<p>Until now, the error page showed events grouped by type. You could look at queries, logs or glows, but you had to mentally piece together the order of operations yourself. The debug tab now shows all events on a single, chronological timeline.</p>
<p><img src="https://assets.flareapp.io/production/blog/8ae14b30fbcbf7e4b23b0f82909e6ccd/0d9e5e8d-59ea-43fd-90e6-f881b01166e1.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/8ae14b30fbcbf7e4b23b0f82909e6ccd/0d9e5e8d-59ea-43fd-90e6-f881b01166e1.png" width="3668" height="2790" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/8ae14b30fbcbf7e4b23b0f82909e6ccd/0d9e5e8d-59ea-43fd-90e6-f881b01166e1.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/8ae14b30fbcbf7e4b23b0f82909e6ccd/0d9e5e8d-59ea-43fd-90e6-f881b01166e1.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/8ae14b30fbcbf7e4b23b0f82909e6ccd/0d9e5e8d-59ea-43fd-90e6-f881b01166e1.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/8ae14b30fbcbf7e4b23b0f82909e6ccd/0d9e5e8d-59ea-43fd-90e6-f881b01166e1.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<h2>Adding more context</h2>
<p>Previously Flare could show you the following event types:</p>
<ul>
<li>Queries running on your database</li>
<li>Logs written by your application</li>
<li>Flare specific glows</li>
<li>Livewire components being rendered</li>
<li>The PHP command or job currently running</li>
</ul>
<p>We've extended this list with a few new types:</p>
<p><strong>HTTP client requests</strong> Outgoing HTTP calls your application makes are now visible. We show you what the response code was and the request method.</p>
<p><img src="https://assets.flareapp.io/production/blog/94e68b9d261b3c6ba6b0c69926b5acc2/11b306cd-71e2-46b6-b2f4-e1101c172a78.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/94e68b9d261b3c6ba6b0c69926b5acc2/11b306cd-71e2-46b6-b2f4-e1101c172a78.png" width="2748" height="186" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/94e68b9d261b3c6ba6b0c69926b5acc2/11b306cd-71e2-46b6-b2f4-e1101c172a78.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/94e68b9d261b3c6ba6b0c69926b5acc2/11b306cd-71e2-46b6-b2f4-e1101c172a78.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/94e68b9d261b3c6ba6b0c69926b5acc2/11b306cd-71e2-46b6-b2f4-e1101c172a78.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/94e68b9d261b3c6ba6b0c69926b5acc2/11b306cd-71e2-46b6-b2f4-e1101c172a78.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p><strong>Redis commands</strong> Whether it's a GET, SET, LPUSH, or EXPIRE, you'll know exactly what your application asked Redis to do and how long it took.</p>
<p><img src="https://assets.flareapp.io/production/blog/4ee89db4dca4bb44ed882bc283c043ad/d715908a-4855-4eb9-b86c-4620af31fc14.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/4ee89db4dca4bb44ed882bc283c043ad/d715908a-4855-4eb9-b86c-4620af31fc14.png" width="2762" height="188" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/4ee89db4dca4bb44ed882bc283c043ad/d715908a-4855-4eb9-b86c-4620af31fc14.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/4ee89db4dca4bb44ed882bc283c043ad/d715908a-4855-4eb9-b86c-4620af31fc14.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/4ee89db4dca4bb44ed882bc283c043ad/d715908a-4855-4eb9-b86c-4620af31fc14.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/4ee89db4dca4bb44ed882bc283c043ad/d715908a-4855-4eb9-b86c-4620af31fc14.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p><strong>Filesystem operations</strong> Reads, writes, deletes: we track what operation happened and which files were used in the process. If something goes wrong, the operation gets marked as "failed".</p>
<p><img src="https://assets.flareapp.io/production/blog/3e5d31720abc338ee767be0102968561/9edf39e9-2505-4836-ae93-5ca475b75844.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/3e5d31720abc338ee767be0102968561/9edf39e9-2505-4836-ae93-5ca475b75844.png" width="2764" height="214" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/3e5d31720abc338ee767be0102968561/9edf39e9-2505-4836-ae93-5ca475b75844.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/3e5d31720abc338ee767be0102968561/9edf39e9-2505-4836-ae93-5ca475b75844.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/3e5d31720abc338ee767be0102968561/9edf39e9-2505-4836-ae93-5ca475b75844.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/3e5d31720abc338ee767be0102968561/9edf39e9-2505-4836-ae93-5ca475b75844.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p><strong>Caching operations</strong> A cache hit or miss can make a world of difference in code paths taken. Information like this might be crucial for debugging issues.</p>
<p><img src="https://assets.flareapp.io/production/blog/240d75b06244af94098e2f8f26b668e6/90717558-6ceb-4862-9fed-55ea645c56c1.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/240d75b06244af94098e2f8f26b668e6/90717558-6ceb-4862-9fed-55ea645c56c1.png" width="2770" height="204" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/240d75b06244af94098e2f8f26b668e6/90717558-6ceb-4862-9fed-55ea645c56c1.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/240d75b06244af94098e2f8f26b668e6/90717558-6ceb-4862-9fed-55ea645c56c1.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/240d75b06244af94098e2f8f26b668e6/90717558-6ceb-4862-9fed-55ea645c56c1.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/240d75b06244af94098e2f8f26b668e6/90717558-6ceb-4862-9fed-55ea645c56c1.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<h2>How it works</h2>
<p>The newer versions of the Flare clients (spatie/laravel-flare and spatie/flare-client-php starting from v2) were completely rewritten to support our performance tracing feature.</p>
<p>As part of tracing, we started recording a lot more events within your application, so it made sense to also include these when an error happens.</p>
<p>You can configure which events are recorded when an error happens, during tracing, and how many items of each event type are stored. More information can be found in the documentation of our packages.</p>
]]>
            </summary>
                                    <updated>2026-03-11T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[New and improved settings screens]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/new-and-improved-settings-screens" />
            <id>https://flareapp.io/new-and-improved-settings-screens</id>
            <author>
                <name><![CDATA[Dries]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Your settings pages are the kind of thing you don't think about until something frustrates you. A form that doesn't behave the way you expect, navigation that makes you hunt for the right page, or inconsistencies that make you second-guess whether you're in the right place.</p>
<p>We've spent time improving the settings experience across Flare. Account, team and project settings all received a new lick of paint. Nothing groundbreaking, just the same kind of polish you find on performance monitoring.</p>
<h2>Consistent layout, everywhere</h2>
<p>Every settings page now follows the same structure: a sidebar for navigation, clearly grouped sections, and a predictable layout no matter where you are. Whether you're updating your profile, managing API tokens, or configuring notifications for a project, the experience feels the same.</p>
<p><img src="https://assets.flareapp.io/production/blog/ab522afea68017c40d539d90bbbf7a18/6028641c-613c-4e39-852c-f81f6bdf9736.png" alt="account-overview" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/ab522afea68017c40d539d90bbbf7a18/6028641c-613c-4e39-852c-f81f6bdf9736.png" width="4562" height="2424" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/ab522afea68017c40d539d90bbbf7a18/6028641c-613c-4e39-852c-f81f6bdf9736.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/ab522afea68017c40d539d90bbbf7a18/6028641c-613c-4e39-852c-f81f6bdf9736.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/ab522afea68017c40d539d90bbbf7a18/6028641c-613c-4e39-852c-f81f6bdf9736.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/ab522afea68017c40d539d90bbbf7a18/6028641c-613c-4e39-852c-f81f6bdf9736.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<h2>Improved forms and flows</h2>
<p>We've revisited the forms across settings to make them more intuitive. Fields are better organised, validation is clearer, and destructive actions like deleting your account or revoking a token are properly guarded with confirmation steps.</p>
<p><img src="https://assets.flareapp.io/production/blog/08444a0ed47453f319669ca6a565d776/a29cd716-c14c-4e03-bec7-fc9a00cc76f2.png" alt="confirm-dialog" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/08444a0ed47453f319669ca6a565d776/a29cd716-c14c-4e03-bec7-fc9a00cc76f2.png" width="1986" height="1378" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/08444a0ed47453f319669ca6a565d776/a29cd716-c14c-4e03-bec7-fc9a00cc76f2.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/08444a0ed47453f319669ca6a565d776/a29cd716-c14c-4e03-bec7-fc9a00cc76f2.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/08444a0ed47453f319669ca6a565d776/a29cd716-c14c-4e03-bec7-fc9a00cc76f2.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/08444a0ed47453f319669ca6a565d776/a29cd716-c14c-4e03-bec7-fc9a00cc76f2.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>Gone are the confusing toggle components on the security and notification pages.</p>
<p><img src="https://assets.flareapp.io/production/blog/f81f855c25aa187ef2ed8f25313514eb/c5e9532e-e6c3-4c6a-8614-d7cd1573ba82.png" alt="no-toggles" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/f81f855c25aa187ef2ed8f25313514eb/c5e9532e-e6c3-4c6a-8614-d7cd1573ba82.png" width="2454" height="634" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/f81f855c25aa187ef2ed8f25313514eb/c5e9532e-e6c3-4c6a-8614-d7cd1573ba82.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/f81f855c25aa187ef2ed8f25313514eb/c5e9532e-e6c3-4c6a-8614-d7cd1573ba82.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/f81f855c25aa187ef2ed8f25313514eb/c5e9532e-e6c3-4c6a-8614-d7cd1573ba82.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/f81f855c25aa187ef2ed8f25313514eb/c5e9532e-e6c3-4c6a-8614-d7cd1573ba82.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<h2>Better navigation</h2>
<p>Account settings and project settings each have their own navigation, so you always know exactly where you are.</p>
<p><img src="https://assets.flareapp.io/production/blog/6304df5442f8508358d413201bd60dd4/c5fc0cf4-21c6-42b9-b6cd-1def7c519254.png" alt="account-settings-sidebar" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/6304df5442f8508358d413201bd60dd4/c5fc0cf4-21c6-42b9-b6cd-1def7c519254.png" width="2200" height="1330" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/6304df5442f8508358d413201bd60dd4/c5fc0cf4-21c6-42b9-b6cd-1def7c519254.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/6304df5442f8508358d413201bd60dd4/c5fc0cf4-21c6-42b9-b6cd-1def7c519254.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/6304df5442f8508358d413201bd60dd4/c5fc0cf4-21c6-42b9-b6cd-1def7c519254.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/6304df5442f8508358d413201bd60dd4/c5fc0cf4-21c6-42b9-b6cd-1def7c519254.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p><img src="https://assets.flareapp.io/production/blog/eb668dd387060e0aef68f39085b7efa8/5565b344-1292-4419-a5ec-5c3e3297f61c.png" alt="team-settings-sidebar" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/eb668dd387060e0aef68f39085b7efa8/5565b344-1292-4419-a5ec-5c3e3297f61c.png" width="2150" height="1658" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/eb668dd387060e0aef68f39085b7efa8/5565b344-1292-4419-a5ec-5c3e3297f61c.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/eb668dd387060e0aef68f39085b7efa8/5565b344-1292-4419-a5ec-5c3e3297f61c.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/eb668dd387060e0aef68f39085b7efa8/5565b344-1292-4419-a5ec-5c3e3297f61c.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/eb668dd387060e0aef68f39085b7efa8/5565b344-1292-4419-a5ec-5c3e3297f61c.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<h2>Small details that add up</h2>
<p>Beyond the structural changes, we've cleaned up spacing, typography, and visual hierarchy throughout. Cards have more breathing room. Actions are easier to spot. The overall feel is calmer and more focused.</p>
<p>These are the kinds of changes that are hard to put into a changelog but easy to feel when you use the product.</p>
<p>Give the new settings a look next time you're in Flare. If you have feedback or spot something we missed, we'd love to hear from you.</p>
]]>
            </summary>
                                    <updated>2026-03-11T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[How we accidentally dropped half our traces: a tale of Cloudflare Workers and WAF]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/how-we-accidentally-dropped-half-our-traces-a-tale-of-cloudflare-workers-and-waf" />
            <id>https://flareapp.io/how-we-accidentally-dropped-half-our-traces-a-tale-of-cloudflare-workers-and-waf</id>
            <author>
                <name><![CDATA[Ruben]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Yesterday morning we noticed something worrying: our queueing servers were processing far fewer spans than usual. After a few hours of debugging, we traced the problem to an unexpected routing change inside Cloudflare. Here's what happened.</p>
<p>Flare receives spans as part of our <a href="https://flareapp.io/docs/flare/performance">tracing integration</a>. It shows you exactly what happens within your application. Which queries run, which API requests are made, which views get rendered, and a lot more.</p>
<p>We use Flare to monitor Flare, our staging environment watches over the production environment. That morning, the production <code>/v1/traces</code> endpoint was getting hit about 50% less than usual.</p>
<p>The day before, we had merged a massive PR: a complete rewrite of our trace ingestion logic. Naturally, that's where we started looking. But here's the strange part: 50% of our traces <em>were</em> still being processed correctly. The new code was working half the time. Strange. Very strange.</p>
<h2>Our ingestion pipeline</h2>
<p>To understand what went wrong, here's how trace ingestion works in Flare:</p>
<ol>
<li>A client sends a trace in OpenTelemetry format to <code>ingress.flareapp.io/v1/traces</code></li>
<li>A Cloudflare Worker handles the request. Think AWS Lambda, a function running at the edge. The Worker checks:
<ul>
<li>Is there an API key provided?</li>
<li>Is the API key valid?</li>
<li>Has the API key exceeded its usage quota?</li>
<li>Is the API key being rate limited for sending too much at once?</li>
<li>Is the trace format valid?</li>
</ul>
</li>
<li>If everything checks out, the Worker uploads the trace to R2 (Cloudflare's S3 alternative) and sends a notification to Flare with the filename</li>
<li>That notification is a request to <code>ingress.flareapp.io/api/cloudflare-traces</code>, handled by our load balancer and eventually a Laravel application that queues a job to process the file</li>
<li>A queue worker picks up the job, fetches the file from R2, processes the trace, and stores the spans in ClickHouse</li>
<li>Done, another trace successfully processed</li>
</ol>
<h2>What went wrong</h2>
<p>In step 4, the Cloudflare Worker sends a request to <code>ingress.flareapp.io/api/cloudflare-traces</code> to notify Flare about the new trace. We always assumed that since this request originates from a Worker within the same Cloudflare zone, it would be passed directly to our origin load balancer, bypassing the rest of Cloudflare's infrastructure.</p>
<p>That assumption turned out to be wrong.</p>
<p>Instead of going straight to our origin, Cloudflare routed these requests back through its entire stack, including the WAF (Web Application Firewall). We use Cloudflare's WAF extensively: the entire Flare website, app, and API sit behind it. We have rules to block abusive API keys and, crucially, rate limits.</p>
<p>So what happened is simple: the Worker's internal requests to Flare were being treated as regular external traffic. They hit our rate limits and got dropped. That's why roughly half the traces made it through, we were sitting right at the edge of the rate limit threshold.</p>
<p>The baffling part? This Worker setup has been running unchanged since we launched performance monitoring over a year ago. We have no idea why Cloudflare suddenly started routing these requests differently.</p>
<p>Looking back, we actually noticed a similar but smaller dip in spans around Valentine's weekend. At the time we shrugged it off, people probably had better plans than visiting websites on that weekend. It now looks like Cloudflare briefly rerouted requests that weekend too. Since everything recovered on Monday, nobody investigated.</p>
<h2>How we fixed it</h2>
<p>We applied two fixes.</p>
<p><strong>First</strong>, we changed the internal routing so the Worker notifies Flare through the <code>/v1/traces</code> instead of the <code>/api/cloudflare-traces</code> path. That's the path the worker is catching and we hoped it would start avoiding the WAF entirely. This immediately brought most traces back, but not all of them.</p>
<p><strong>Second</strong>, we added a WAF skip rule. Cloudflare lets you bypass WAF rules when a request originates from a Worker in a specific zone:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">(cf.worker.upstream_zone eq "flareapp.io")
</span></span></code></pre>
<p>We assumed this would be the default behavior. It's not. This rule isn't even supported by Cloudflare's visual rule builder, and there's barely any documentation mentioning it exists. But it works!</p>
<p>After both changes, trace ingestion was back to normal.</p>
<h2>Closing thoughts</h2>
<p>Despite this hiccup, Cloudflare Workers have been a great part of our infrastructure. They've kept Flare's servers safe from all kinds of malicious traffic for over a year, and they'll keep doing that. We're happy with what Cloudflare provides, even if the configuration can surprise you sometimes.</p>
<p>Up next: we're working on the next big thing for Flare, <strong>logging</strong>. While you can already send logs to Flare, we're going to take it to the next level. Stay tuned!</p>
]]>
            </summary>
                                    <updated>2026-02-25T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Require two-factor authentication for your team]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/require-two-factor-authentication-for-your-team" />
            <id>https://flareapp.io/require-two-factor-authentication-for-your-team</id>
            <author>
                <name><![CDATA[Alex]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Flare already contains stack traces, request data, and environment details for your application's errors. That's sensitive information — and it makes sense to protect access to it.</p>
<p>That's why we've added the ability for team admins to require two-factor authentication for all team members. Once enabled, every member of your team will need to set up 2FA before they can access any
projects or errors.</p>
<p>You'll find the new toggle in your team's Security settings. When you enable the requirement, a confirmation modal makes sure you're intentional about it. And of course, you'll need to have 2FA enabled on
your own account first before you can require it from others.</p>
<p>Team members who haven't set up 2FA yet will be redirected to their security settings with a clear message explaining what's needed. They won't be able to access anything in the team until they've
completed the setup.</p>
<p>If you're working with a larger team or handling production error data, this is an easy way to add an extra layer of protection to your Flare account.</p>
]]>
            </summary>
                                    <updated>2026-02-10T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Introducing Flare’s next big feature: performance monitoring]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/introducing-flares-next-big-feature-performance-monitoring" />
            <id>https://flareapp.io/introducing-flares-next-big-feature-performance-monitoring</id>
            <author>
                <name><![CDATA[Freek]]></name>
            </author>
            <summary type="html">
                <![CDATA[<h2>Introducing Flare's next big feature: performance monitoring</h2>
<p><a href="https://flareapp.io">Flare</a> was introduced on stage at Laracon EU 2019 as the first error tracker built for Laravel. Since then, my team at Spatie has been steadily improving it, adding integrations, better JS support, and lots of smaller quality-of-life updates.</p>
<p>I'm happy to share that our big new feature, <a href="https://flareapp.io/platform/performance-monitoring">Performance Monitoring</a>, is now live for everyone. After <a href="https://flareapp.io/blog/lessons-from-the-deep-end">quite the journey</a> and a lengthy beta, I can really say that Flare is now the all-in-one monitoring tool I've always wanted to use. We kept pricing the same, so you're getting two products (the best error tracker for Laravel apps + performance monitoring) for the price of one.</p>
<h3>Let's take a look at performance monitoring</h3>
<p><img src="https://assets.flareapp.io/production/blog/f72485b5a0bf81cd8e9ff874f21339d2/9cf93cd0-5016-44b7-88f1-b1798825ea62.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/f72485b5a0bf81cd8e9ff874f21339d2/9cf93cd0-5016-44b7-88f1-b1798825ea62.jpg" width="4492" height="3208" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/f72485b5a0bf81cd8e9ff874f21339d2/9cf93cd0-5016-44b7-88f1-b1798825ea62.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/f72485b5a0bf81cd8e9ff874f21339d2/9cf93cd0-5016-44b7-88f1-b1798825ea62.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/f72485b5a0bf81cd8e9ff874f21339d2/9cf93cd0-5016-44b7-88f1-b1798825ea62.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/f72485b5a0bf81cd8e9ff874f21339d2/9cf93cd0-5016-44b7-88f1-b1798825ea62.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>Performance monitoring gives you a complete view of what's happening in your Laravel app. The dashboard tracks everything: HTTP routes, queued jobs, Artisan commands, and database queries.</p>
<p>Each section shows you response or execution times with clear charts, making it easy to spot slow endpoints at a glance. In the screenshot above, you can see which routes are taking the longest (that login POST at 2,114ms might need some attention), how your background jobs are performing, and which database queries are slowing things down.</p>
<p>The "slowest 5%" metric is particularly useful: it helps you identify those edge cases where performance degrades, even when your averages look fine. You're not just getting numbers either; click through and you'll see the actual route names, controller methods, and SQL queries, so you know exactly where to start optimizing.</p>
<p>Let's click that api/users route.</p>
<p><img src="https://assets.flareapp.io/production/blog/9166ec5d258384678460c336a7b3a68c/a159b803-e6ee-4209-97fc-0570248827ec.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/9166ec5d258384678460c336a7b3a68c/a159b803-e6ee-4209-97fc-0570248827ec.jpg" width="4492" height="3208" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/9166ec5d258384678460c336a7b3a68c/a159b803-e6ee-4209-97fc-0570248827ec.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/9166ec5d258384678460c336a7b3a68c/a159b803-e6ee-4209-97fc-0570248827ec.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/9166ec5d258384678460c336a7b3a68c/a159b803-e6ee-4209-97fc-0570248827ec.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/9166ec5d258384678460c336a7b3a68c/a159b803-e6ee-4209-97fc-0570248827ec.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>The breakdown section is where things get really useful. You can see exactly which database queries ran during this request, how long each one took, and how often it appeared across your traces. In this example, that user bookings query showed up in 2,280 different requests—so you know it's a heavily-used query worth optimizing. You can also tab over to see external HTTP calls, commands, views, and even Livewire components that were part of the request.</p>
<p><img src="https://assets.flareapp.io/production/blog/8523edcb5ac0db7ce296be8f0c965bf6/376e4a2a-7723-4e23-b9fc-43dc148147f3.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/8523edcb5ac0db7ce296be8f0c965bf6/376e4a2a-7723-4e23-b9fc-43dc148147f3.jpg" width="4492" height="3208" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/8523edcb5ac0db7ce296be8f0c965bf6/376e4a2a-7723-4e23-b9fc-43dc148147f3.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/8523edcb5ac0db7ce296be8f0c965bf6/376e4a2a-7723-4e23-b9fc-43dc148147f3.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/8523edcb5ac0db7ce296be8f0c965bf6/376e4a2a-7723-4e23-b9fc-43dc148147f3.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/8523edcb5ac0db7ce296be8f0c965bf6/376e4a2a-7723-4e23-b9fc-43dc148147f3.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>Scroll down and you'll see the actual traces. Each one shows you the complete timeline of what happened during that request—from the initial Laravel bootstrap and routing, through middleware, down to every single database query that was executed.
All those queries are clickable. Click one and you'll see the full SQL, execution time, and all the context you need to understand what's happening.</p>
<p>Let's now take a look at jobs.</p>
<p><img src="https://assets.flareapp.io/production/blog/e6f12aeb933a7955e91f30eec211fcfd/517457f7-95be-48c8-aa32-43938a250ee1.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/e6f12aeb933a7955e91f30eec211fcfd/517457f7-95be-48c8-aa32-43938a250ee1.jpg" width="4492" height="3208" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/e6f12aeb933a7955e91f30eec211fcfd/517457f7-95be-48c8-aa32-43938a250ee1.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/e6f12aeb933a7955e91f30eec211fcfd/517457f7-95be-48c8-aa32-43938a250ee1.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/e6f12aeb933a7955e91f30eec211fcfd/517457f7-95be-48c8-aa32-43938a250ee1.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/e6f12aeb933a7955e91f30eec211fcfd/517457f7-95be-48c8-aa32-43938a250ee1.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>Jobs get the same treatment. You can see execution times for all your queued jobs, and spot the slow ones. Let's click one of them.</p>
<p><img src="https://assets.flareapp.io/production/blog/63455e33cc1287d2d55b5e17ed0eaefd/4cf52567-2c24-4a91-8d64-c8aa20b6cb17.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/63455e33cc1287d2d55b5e17ed0eaefd/4cf52567-2c24-4a91-8d64-c8aa20b6cb17.jpg" width="4492" height="3208" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/63455e33cc1287d2d55b5e17ed0eaefd/4cf52567-2c24-4a91-8d64-c8aa20b6cb17.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/63455e33cc1287d2d55b5e17ed0eaefd/4cf52567-2c24-4a91-8d64-c8aa20b6cb17.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/63455e33cc1287d2d55b5e17ed0eaefd/4cf52567-2c24-4a91-8d64-c8aa20b6cb17.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/63455e33cc1287d2d55b5e17ed0eaefd/4cf52567-2c24-4a91-8d64-c8aa20b6cb17.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>Click into any job and you'll see which routes triggered it, what queries it ran, and whether it called any external APIs or fired off other commands. The breakdown shows you everything that happened during execution, so if a job is taking longer than expected, you can pinpoint whether it's a slow query, an external API call, or something else entirely.</p>
<p>I hope you get the idea of what kind of info we can display. We have similar screens to display performance data around commands, queries, HTTP calls, views and Livewire Components.</p>
<p>To close off this section, let's look at the Livewire Components view, since we're proud that we could build this one. In this section you can look at the render time of each individual Livewire component. Here is the general graph for the <code>ShipPositionTracker</code> component.</p>
<p><img src="https://assets.flareapp.io/production/blog/72f871fae19f4b6de5c28c192f054110/d53e6f55-0dc9-4185-bb7f-37dd1a7f8214.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/72f871fae19f4b6de5c28c192f054110/d53e6f55-0dc9-4185-bb7f-37dd1a7f8214.jpg" width="3038" height="1428" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/72f871fae19f4b6de5c28c192f054110/d53e6f55-0dc9-4185-bb7f-37dd1a7f8214.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/72f871fae19f4b6de5c28c192f054110/d53e6f55-0dc9-4185-bb7f-37dd1a7f8214.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/72f871fae19f4b6de5c28c192f054110/d53e6f55-0dc9-4185-bb7f-37dd1a7f8214.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/72f871fae19f4b6de5c28c192f054110/d53e6f55-0dc9-4185-bb7f-37dd1a7f8214.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>On that page, when we scroll a bit down, you can see everything that happened in the request that rendered the component. We can see that the component itself executed a couple of queries and rendered a couple of views.</p>
<p><img src="https://assets.flareapp.io/production/blog/137be783e5fcba495adc63bdae0b166c/7c0561a2-0500-466c-b4ef-139d76d9ca66.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/137be783e5fcba495adc63bdae0b166c/7c0561a2-0500-466c-b4ef-139d76d9ca66.jpg" width="3034" height="2280" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/137be783e5fcba495adc63bdae0b166c/7c0561a2-0500-466c-b4ef-139d76d9ca66.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/137be783e5fcba495adc63bdae0b166c/7c0561a2-0500-466c-b4ef-139d76d9ca66.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/137be783e5fcba495adc63bdae0b166c/7c0561a2-0500-466c-b4ef-139d76d9ca66.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/137be783e5fcba495adc63bdae0b166c/7c0561a2-0500-466c-b4ef-139d76d9ca66.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<h2>In closing</h2>
<p>We think <a href="https://flareapp.io/platform/performance-monitoring">performance monitoring</a> is a very powerful addition to Flare. This isn't the only feature we added recently. We also have <a href="https://flareapp.io/docs/flare/general/our-mcp-server">an MCP server</a> that allows you to integrate Flare's error tracking capabilities directly into your AI-powered development workflows.</p>
<p>This isn't the finish line, and we'll continue improving Flare. In addition to applying general polish across our service, we're going to start working on the next big feature: making your application logs viewable and searchable from within the Flare UI.</p>
<p>You can try our proven exception tracker and new performance monitoring right now. Just <a href="https://flareapp.io/register">create your Flare account</a> (we offer a free 10-day trial) to get started.</p>
]]>
            </summary>
                                    <updated>2025-11-06T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Lessons from the deep end]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/lessons-from-the-deep-end" />
            <id>https://flareapp.io/lessons-from-the-deep-end</id>
            <author>
                <name><![CDATA[Alex]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Spatie has been part of the Laravel ecosystem for years, releasing over 300 packages, publishing many blog posts and courses, and growing with the community. We started building Flare in 2018 with 7 employees; today we’re 11, still balancing client work with our OSS projects and products like Flare, Mailcoach, and Ray.</p>
<p>20 months ago, we started building Performance Monitoring as Flare’s next big feature, never expecting Laravel’s rapid commercial growth to put us in direct competition with their own tools. This is our honest take on those 20 months went, how we’re adapting to this new reality, and where we’re heading next while staying true to who we are. A dive into the deep end, without knowing how far down it goes.</p>
<h2>Why we jumped</h2>
<p>Many other exception trackers at the time felt bloated and didn’t focus on Laravel. We started building Flare as an easy to use, and context-rich alternative built to support Laravel, the framework we have been using for years at that point. Flare has grown into a tool that we use and love here at Spatie every day. Using our own product meant that we were able to improve it over the years by adding features and context that we were missing. This made both Flare and Ignition (our standalone error page, which was the default in Laravel until 2024) to become more powerful and usable over time.</p>
<p>Early on, we had big ideas about expanding Flare beyond just collecting exceptions from failed requests. We are already collecting rich context for them, why not collect the same data for sampled requests, jobs and commands? With new tracing standards like Open Telemetry emerging from 2019, adding performance monitoring felt like a natural next step.</p>
<p>The turning point came at our booth at Laracon EU 2024. A first for Flare, and to add to the excitement, we were almost placed right next to Sentry. A real eye-opener was that not many attendees had actually heard of Flare or its affiliation with Spatie. This came as a bit of a surprise; apparently we had overestimated our recognition. On top of that, a lot of the conversations we had were more about monitoring and tracing integrations rather than error monitoring. This aligned with our own wishes and ideas for Flare and with our gut feeling validated, we felt highly motivated and full of ideas after the conference. Back at the office, we started working on a proof of concept for Performance Monitoring right away.</p>
<h2>Getting caught in a whirlpool</h2>
<p>My colleague Ruben spent just over two weeks extending the Flare client to collect trace data and cook up a nice dashboard with performance metrics for each route. At this point we were confident we could ship this thing by summer 2024.</p>
<p>Fast forward to February 2025 and nothing has shipped at all. Instead, we’re now moving infrastructure to Kubernetes, adding ClickHouse as a secondary time series database, adding additional ingress endpoints to monitoring data, introducing Open Telemetry support and basically rewriting our entire ingress flow for errors. This wasn’t just a new feature anymore, we were overhauling the complete product. We missed all our previous deadlines, and dedicated all of Flare’s development time to it. Features and improvements to Flare we planned in those 12 months were postponed, and we fell behind on the marketing efforts we had planned after Laracon EU.</p>
<p>Even with our years of experience in building projects for ourselves and for clients, we fell into many classic project management traps: scope creep, underestimating tasks, optimistic planning, losing momentum, perfection paralysis and the "while we're at it" syndrome of refactoring everything we touched. Beyond this, life also got in the way. Ruben, our lead back-end developer for Flare became a dad, and other colleagues moved on to new challenges or had to take a break from Flare. As our team’s energy waned, our momentum faded.</p>
<p>The reality of running a small agency adds another layer of complexity. Spatie still relies heavily on client work, and work on Flare was put on the back burner whenever client deadlines emerged. We always try to balance what our team wants to work on with what the company needs, having to shift focus far more often than we’d like. Ironically, after months wrestling with OTEL data structures and ClickHouse performance tuning, doing client work felt like a breath of fresh air.</p>
<p>At this point, we weren’t so sure anymore when and where we would actually land with Performance Monitoring.</p>
<h2>Unfamiliar waters</h2>
<p>A few months after Laracon EU 2024, things in the ecosystem we’ve been part of for years started changing quickly for us. In May 2024, a PR was made to the Laravel core removing Ignition as the framework’s default error page. September brought a $57M investment into Laravel by Accel, and November revealed Nightwatch as a first-party performance monitoring solution at Laracon AU.</p>
<p>While we always have maintained a good relationship with Laravel, we never had any commercial dealings with them. No secret handshakes, contracts or shared profits. Having Ignition as a default error page meant we could drive more leads to Flare, but also add useful features in the framework like sharing errors and the editor integration. This helped Flare to grow in the long term, despite having no direct commercial ties to Laravel.</p>
<p>With Ignition being gone, Laravel backing Sentry as their preferred error tracker, and launching Nightwatch, it felt like your big brother passed you in a race you only just realised was happening. Our commitment with Laravel as the open-source framework remains unchanged. However, the reality is that we are now working with (and competing against) Laravel being a commercial entity.</p>
<p>We get the push toward a more commercial focus, and competition is only natural. But to be building quietly while our competitor was showing off their product to a wide audience was frustrating. It’s hard to stay excited about a private beta when others are stealing the spotlight.</p>
<h2>To sink or to swim</h2>
<p>These announcements made us look inward and reconsider our plans for Flare. Is now even the right time to build and release performance monitoring, or is it now or never?</p>
<p>I remember difficult discussions we had with the entire Spatie team which had colleagues feeling disappointed both with the situation and the perspective of reconsidering Flare’s future. But at some point we decided to power through. It might’ve been the sunk cost fallacy or stubbornness that led to the decision to not back down, but I think it’s simply because we truly believe we’re building a great product.</p>
<p>Looking back, it's easy to view these developments as setbacks, but they helped us to build something better. While we didn't abandon our initial idea, these new circumstances caused us to make different decisions that improved both our performance monitoring and Flare as a whole:</p>
<ul>
<li>Refactoring everything to be OTEL compliant, believing this will become a key differentiator for Flare</li>
<li>Adding the same in-depth monitoring context to our error reporting data, making existing Flare features even better</li>
<li>Switching to ClickHouse to make performance monitoring more performant</li>
<li>Bringing Zuzana onto support to free up developer time for building</li>
<li>Starting to containerize our infrastructure to enable fully European servers</li>
<li>Most importantly: committing to keep investing time, money and energy into Flare</li>
</ul>
<p>We realize that we should’ve shipped sooner and kept the scope smaller (there are only a million blog posts with this conclusion). But at this point, we’re developing something valuable enough that deserves to exist alongside competing products like Nightwatch, or Sentry. The market is large enough for different approaches to monitoring, and with the $57M investment in Laravel, the ecosystem and community will keep growing.</p>
<h2>Swimming back to shore</h2>
<p>In the short term, we're working hard to make performance monitoring available to all our users. In the longer term, we are adapting to the new reality without abandoning Flare. By not trying to outcompete Laravel or match their resources, we can focus on doing what we do best and take a different approach.</p>
<p><strong>As a small team</strong>, we've eliminated the middleman entirely. Our engineers answer your Flare questions directly, help you integrate with complex legacy applications, and build features based on real conversations with users. When you need support, you're talking to the people who actually wrote the code (or these blog posts).</p>
<p><strong>As a European company</strong>, we bring an actual European perspective on data protection, GDPR compliance, and privacy. We can make decisions quickly based on what's right for our users, not what's commercially most interesting. As a team we also like having European alternatives to software and tools.</p>
<p><strong>As Laravel and OSS contributors</strong>, we've spent years plowing through the framework's internals and building Laravel-specific packages, integrations and projects. Our 300+ packages give us insights and ideas on what to integrate into Flare next and how to do it. And we're not scared of exploring better integrations with other (PHP) frameworks and products.</p>
<p>We continue to invest time and energy in Flare and our marketing efforts. Although we were too quiet in the past and relied too heavily on Laravel and Ignition for growth, we are now moving forward and telling our own story. If nobody knows you exist, it doesn't matter how great your tool is.</p>
<p>In line with adapting, here’s what’s next:</p>
<ul>
<li>
<p><strong>Ignition returns as open source</strong>: Losing Ignition as Laravel’s default error page cost us leads, but also our favourite error page. We’re bringing it back and expanding it with the same rich debugging context we collect for Performance Monitoring.</p>
</li>
<li>
<p><strong>Exploring an open source Flare</strong>: Open source lies at the heart of who we are at Spatie and of our success. Making Flare’s code public will reveal how we grew a “simple feature” into a complete product.</p>
</li>
<li>
<p><strong>Releasing Performance Monitoring for free</strong>: Performance Monitoring is currently in public beta and available to all users. Once released, it will be included for free in every plan. Combined with Flare’s existing error tracking, you're now essentially getting two powerful products for the price of one.</p>
</li>
</ul>
<p>The last 20 months have taught us that jumping in at the deep end isn't always about having perfect timing or ideal conditions. Sometimes, it's about having the conviction to keep swimming when the going gets tough.</p>
]]>
            </summary>
                                    <updated>2025-10-02T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Using ClickHouse at Flare to aggregate data]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/how-were-using-clickhouse-at-flare-aggregating-data" />
            <id>https://flareapp.io/how-were-using-clickhouse-at-flare-aggregating-data</id>
            <author>
                <name><![CDATA[Ruben]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>The word is out: we're working on performance monitoring, Flare's most significant feature yet. One of the challenges we've tackled in the previous months was storing all this performance data generated by our customers' applications.</p>
<p><img src="https://assets.flareapp.io/production/blog/b312da4df6565bd3bfe3a62c3c9d7c48/7509a829-a50b-40d0-8665-23c4d867e520.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/b312da4df6565bd3bfe3a62c3c9d7c48/7509a829-a50b-40d0-8665-23c4d867e520.png" width="3290" height="1180" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/b312da4df6565bd3bfe3a62c3c9d7c48/7509a829-a50b-40d0-8665-23c4d867e520.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/b312da4df6565bd3bfe3a62c3c9d7c48/7509a829-a50b-40d0-8665-23c4d867e520.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/b312da4df6565bd3bfe3a62c3c9d7c48/7509a829-a50b-40d0-8665-23c4d867e520.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/b312da4df6565bd3bfe3a62c3c9d7c48/7509a829-a50b-40d0-8665-23c4d867e520.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>An application has multiple entry points. Think of requests, jobs, or commands within those entry points. Many things happen within those entry points, like executing queries, rendering views, making external HTTP calls, etc. We call each unit where something happens (including the entry points) a span.</p>
<p>Spans have a start and end time (and thus a duration). They can be children of other spans, like a query happening during a request, and they can be running on different servers at different times, for example, when dispatching and running a job on separate servers.</p>
<p>Ultimately, we want to aggregate these spans into digestible pieces to show, for example, which routes are slow and which queries might be the reason.</p>
<p>To do that, we need to process massive amounts of data quickly. While more traditional databases like MySQL and PostgreSQL allow us to store and query data quickly, they aren't that useful when storing and querying time series data. That's where ClickHouse comes in. A column-store database allows us to digest and query lots of data blazingly fast.</p>
<p>One of ClickHouse's significant advantages is its unique table structures, like the AggregatingMergeTree, which allows us to store all sorts of statistics efficiently about the duration of spans aggregated per minute. Let's take a look at an example.</p>
<p>Say we have the following spans:</p>
<table>
<thead>
<tr>
<th align="left">Span</th>
<th align="left">Duration</th>
<th align="left">Time</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">A</td>
<td align="left">500ms</td>
<td align="left">12:55:13</td>
</tr>
<tr>
<td align="left">B</td>
<td align="left">200ms</td>
<td align="left">12:55:22</td>
</tr>
<tr>
<td align="left">C</td>
<td align="left">150ms</td>
<td align="left">12:56:10</td>
</tr>
<tr>
<td align="left">D</td>
<td align="left">200ms</td>
<td align="left">13:10:05</td>
</tr>
</tbody>
</table>
<p>Our aggregated spans table per minute now would look like this:</p>
<table>
<thead>
<tr>
<th align="left">Time</th>
<th align="left">Spans</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">12:55</td>
<td align="left">A, B</td>
</tr>
<tr>
<td align="left">12:56</td>
<td align="left">C</td>
</tr>
<tr>
<td align="left">13:10</td>
<td align="left">D</td>
</tr>
</tbody>
</table>
<p>Querying all the spans between 12:00 and 13:00 now only requires reading 2 rows instead of 3. That might not seem like a massive improvement, but if you're ingesting +100 spans per minute, that's much less data to read!</p>
<p>While it is technically possible to implement this functionality with traditional databases, it requires a lot of code, caching, and time. The cool thing is that ClickHouse provides this out of the box and even more!</p>
<p>Let's calculate the average duration of the spans in the table. In a more traditional database, we would add a column to the table containing the average duration:</p>
<table>
<thead>
<tr>
<th align="left">Time</th>
<th align="left">Spans</th>
<th align="left">Average</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">12:55</td>
<td align="left">A, B</td>
<td align="left">350ms</td>
</tr>
<tr>
<td align="left">12:56</td>
<td align="left">C</td>
<td align="left">150ms</td>
</tr>
<tr>
<td align="left">13:10</td>
<td align="left">D</td>
<td align="left">200ms</td>
</tr>
</tbody>
</table>
<p>If we want to query the average duration at 12:55, we get 350ms, which is correct. But what if we want to query the average duration for all spans between 12:00 and 13:00?</p>
<p>A naive approach would be summing the averages and dividing that number by two. But that's not correct:</p>
<ul>
<li>Naive average approach: (350 + 150) / 2 = 250</li>
<li>Correct average approach: (500+200+150) / 3 = 283</li>
</ul>
<p>So, to correctly calculate the average, we need the input of the average function instead of the outcome we stored in the database. ClickHouse solves this problem using AggregateFunctions; they allow you to call mathematical functions on data, but instead of storing the outcome, they store the input as a binary blob. When you need the outcome, call the mathematical function again, this time on the binary input data, and get a number as output.</p>
<p>If we would try to represent this in our demo table, it would look like this:</p>
<table>
<thead>
<tr>
<th align="left">Time</th>
<th align="left">Spans</th>
<th align="left">AggregateFunction(average)</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">12:55</td>
<td align="left">A, B</td>
<td align="left">500ms, 200ms</td>
</tr>
<tr>
<td align="left">12:56</td>
<td align="left">C</td>
<td align="left">150ms</td>
</tr>
<tr>
<td align="left">13:10</td>
<td align="left">D</td>
<td align="left">200ms</td>
</tr>
</tbody>
</table>
<h2>SQL side of things</h2>
<p>To implement such an operation, we first need a table with the spans we've received:</p>
<pre class="phiki language-sql nord" data-language="sql" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">CREATE</span><span class="token"> </span><span class="token" style="color: #81A1C1;">TABLE</span><span class="token"> </span><span class="token" style="color: #88C0D0;">spans</span><span class="token">
</span></span><span class="line"><span class="token">(
</span></span><span class="line"><span class="token">    span_id String,
</span></span><span class="line"><span class="token">    duration UInt64,
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">time</span><span class="token"> </span><span class="token" style="color: #81A1C1;">DateTime</span><span class="token">
</span></span><span class="line"><span class="token">)
</span></span><span class="line"><span class="token">ENGINE </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> MergeTree</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">ORDER BY</span><span class="token"> </span><span class="token" style="color: #81A1C1;">time</span><span class="token">;
</span></span></code></pre>
<p>This schema differs from the schemas we're used to from MySQL and Postgres. Let's quickly go through it!</p>
<ul>
<li>We create a table <code>spans</code> with three columns: <code>span_id</code>, <code>duration</code>, and <code>time</code></li>
<li>Each ClickHouse table requires an engine type. We're using a MergeTree engine, which behaves kinda like a more traditional SQL table</li>
<li>The <code>ORDER BY time</code> clause defines how ClickHouse will store and index data</li>
</ul>
<p>Let's create our <code>aggregated_spans</code> schema where we'll store the aggregate grouped by time:</p>
<pre class="phiki language-sql nord" data-language="sql" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">CREATE</span><span class="token"> </span><span class="token" style="color: #81A1C1;">TABLE</span><span class="token"> </span><span class="token" style="color: #88C0D0;">aggregated_spans</span><span class="token">
</span></span><span class="line"><span class="token">(
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">time</span><span class="token"> </span><span class="token" style="color: #81A1C1;">DateTime</span><span class="token">,
</span></span><span class="line"><span class="token">    spans AggregateFunction(groupArray, String),
</span></span><span class="line"><span class="token">    average AggregateFunction(avg, UInt64)
</span></span><span class="line"><span class="token">)
</span></span><span class="line"><span class="token">ENGINE </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> AggregatingMergeTree</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">ORDER BY</span><span class="token"> toStartOfMinute(</span><span class="token" style="color: #81A1C1;">time</span><span class="token">);
</span></span></code></pre>
<p>In this schema:</p>
<ul>
<li>We create a table <code>aggregated_spans</code> with three columns</li>
<li><code>time</code> is a plaintext column representing the date</li>
<li><code>spans</code> is an array of strings representing the span ID, but instead of storing the string itself, we store the input of a group function so that in the end, we can get an array of all the span IDs</li>
<li><code>average</code> stores the input of the average function, as we discussed earlier</li>
<li>Since we're aggregating data, we're using the AggregatingMergeTree table engine</li>
<li>The <code>ORDER BY time</code> clause again tells ClickHouse how to store and index data; it is also the column by which we'll aggregate the data. Since we want to do this on a minute basis, we're stripping the seconds for every datetime.</li>
</ul>
<p>We're using a materialized view to get data from the <code>spans</code> table into the <code>aggregated_spans</code> table. It can be compared to a trigger that listens to inserts on the <code>spans</code> table; when an insert happens, the materialized view query will be executed and insert rows in the <code>aggregated_spans</code> table. We create it as such:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">CREATE</span><span class="token"> </span><span class="token">MATERIALIZED</span><span class="token"> </span><span class="token">VIEW</span><span class="token"> </span><span class="token">aggregated_spans_mv</span><span class="token"> </span><span class="token">TO</span><span class="token"> </span><span class="token">aggregated_spans</span><span class="token"> </span><span class="token" style="color: #81A1C1;">AS</span><span class="token">
</span></span><span class="line"><span class="token">SELECT</span><span class="token"> 
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #88C0D0;">toStartOfMinute</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #88C0D0;">time</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #81A1C1;">AS</span><span class="token"> </span><span class="token">time</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #88C0D0;">groupArrayState</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #88C0D0;">span</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #81A1C1;">AS</span><span class="token"> </span><span class="token">spans</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #88C0D0;">avgState</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #88C0D0;">duration</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #81A1C1;">AS</span><span class="token"> </span><span class="token">avg_duration</span><span class="token">
</span></span><span class="line"><span class="token">FROM</span><span class="token"> </span><span class="token">spans</span><span class="token">
</span></span><span class="line"><span class="token">GROUP</span><span class="token"> </span><span class="token">BY</span><span class="token"> </span><span class="token">time</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span></code></pre>
<p>Did you notice the <code>groupArrayState</code> and <code>avgState</code> functions? Since we don't want to store the output of the <code>groupArray</code> or <code>avg</code> functions but their input, we suffix <code>state</code> to these functions to do that.</p>
<p>Inserting spans can be done just like you would expect:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">INSERT INTO spans VALUES 
</span></span><span class="line"><span class="token">('A', 500, '2024-03-14 12:55:13'),
</span></span><span class="line"><span class="token">('B', 200, '2024-03-14 12:55:22'),
</span></span><span class="line"><span class="token">('C', 150, '2024-03-14 12:56:10'),
</span></span><span class="line"><span class="token">('D', 200, '2024-03-14 13:10:05');
</span></span></code></pre>
<p>The materialized view will automatically act and insert 4 rows into <code>aggregated_spans</code>. If we would now query <code>aggregated_spans</code> like this:</p>
<pre class="phiki language-sql nord" data-language="sql" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">SELECT</span><span class="token"> </span><span class="token" style="color: #81A1C1;">*</span><span class="token"> </span><span class="token" style="color: #81A1C1;">from</span><span class="token"> aggregated_spans
</span></span></code></pre>
<p>Then we'll get the following rows:</p>
<table>
<thead>
<tr>
<th align="left">Time</th>
<th align="left">Spans</th>
<th align="left">Average</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">12:55</td>
<td align="left">BINARY</td>
<td align="left">BINARY</td>
</tr>
<tr>
<td align="left">12:56</td>
<td align="left">BINARY</td>
<td align="left">BINARY</td>
</tr>
<tr>
<td align="left">13:10</td>
<td align="left">BINARY</td>
<td align="left">BINARY</td>
</tr>
</tbody>
</table>
<p>As you can see, the first two rows are aggregated instead of having four rows within our initial data. The values of the span IDs and average are not yet what they should be since they're represented as a binary blob. Let's fix that by adjusting our query:</p>
<pre class="phiki language-sql nord" data-language="sql" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">SELECT</span><span class="token"> 
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">time</span><span class="token">, 
</span></span><span class="line"><span class="token">    groupArrayMerge(spans) </span><span class="token" style="color: #81A1C1;">AS</span><span class="token"> spans, 
</span></span><span class="line"><span class="token">    avgMerge(avg_duration) </span><span class="token" style="color: #81A1C1;">AS</span><span class="token"> avg_duration
</span></span><span class="line"><span class="token" style="color: #81A1C1;">FROM</span><span class="token"> aggregated_spans
</span></span><span class="line"><span class="token" style="color: #81A1C1;">GROUP BY</span><span class="token"> </span><span class="token" style="color: #81A1C1;">time</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">ORDER BY</span><span class="token"> </span><span class="token" style="color: #81A1C1;">time</span><span class="token">;
</span></span></code></pre>
<p>Let's quickly go through this query:</p>
<ul>
<li>We'll select the time on which we also group the rows to get the data on a minute basis</li>
<li>We run the <code>groupArray</code> and <code>avg</code> functions, but this time, we suffix them with <code>merge</code>, meaning we provide input values as a binary blob of values instead of raw numbers</li>
<li>In the end, everything will be sorted by the time</li>
</ul>
<p>The output now looks like this:</p>
<table>
<thead>
<tr>
<th align="left">Time</th>
<th align="left">Spans</th>
<th align="left">Average</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">12:55</td>
<td align="left">A,B</td>
<td align="left">350</td>
</tr>
<tr>
<td align="left">12:56</td>
<td align="left">C</td>
<td align="left">150</td>
</tr>
<tr>
<td align="left">13:10</td>
<td align="left">D</td>
<td align="left">200</td>
</tr>
</tbody>
</table>
<p>Looking great! As a last test, can we get the correct average if we combine rows together to achieve, for example, an average per hour? A query for such data would look like this:</p>
<pre class="phiki language-sql nord" data-language="sql" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">SELECT</span><span class="token"> 
</span></span><span class="line"><span class="token">    toStartOfHour(</span><span class="token" style="color: #81A1C1;">time</span><span class="token">) </span><span class="token" style="color: #81A1C1;">as</span><span class="token"> </span><span class="token" style="color: #81A1C1;">time</span><span class="token">, 
</span></span><span class="line"><span class="token">    groupArrayMerge(spans) </span><span class="token" style="color: #81A1C1;">AS</span><span class="token"> spans, 
</span></span><span class="line"><span class="token">    avgMerge(avg_duration) </span><span class="token" style="color: #81A1C1;">AS</span><span class="token"> avg_duration
</span></span><span class="line"><span class="token" style="color: #81A1C1;">FROM</span><span class="token"> aggregated_spans
</span></span><span class="line"><span class="token" style="color: #81A1C1;">GROUP BY</span><span class="token"> </span><span class="token" style="color: #81A1C1;">time</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">ORDER BY</span><span class="token"> </span><span class="token" style="color: #81A1C1;">time</span><span class="token">;
</span></span></code></pre>
<p>Notice that we'll now select <code>toStartOfHour(time)</code> instead of <code>time</code>. The results now look like this:</p>
<table>
<thead>
<tr>
<th align="left">Time</th>
<th align="left">Spans</th>
<th align="left">Average</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">12:00</td>
<td align="left">A,B,C</td>
<td align="left">283</td>
</tr>
<tr>
<td align="left">13:00</td>
<td align="left">D</td>
<td align="left">200</td>
</tr>
</tbody>
</table>
<p>Since we're calculating the functions on the spot based on the query, our average is now correct.</p>
<h2>Conclusion</h2>
<p>ClickHouse was essential to crunch massive amounts of data without breaking a sweat. Next time, we'll examine how we're running queries against ClickHouse in Laravel and how to run these queries concurrently in batches.</p>
<p>See ya!</p>
]]>
            </summary>
                                    <updated>2025-04-28T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Automating styles with data-slot]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/automating-styles-with-data-slot" />
            <id>https://flareapp.io/automating-styles-with-data-slot</id>
            <author>
                <name><![CDATA[Sébastien]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>As we work on Flare’s performance monitoring feature, we’ve adopted a method to streamline our component architecture, making our UI more adaptable and maintainable. We derived this approach from Adam Wathan, who gave an insightful presentation titled <a href="https://www.youtube.com/watch?v=MrzrSFbxW7M&amp;ab_channel=Laravel">"Designing a Component Library"</a> at Laracon US 2024. Although it’s been five months since the talk, there were some awesome takeaways. In his presentation, Adam showcased techniques for building flexible and maintainable UI components using Tailwind CSS. One of the key ideas he explored was the use of the <code>data-slot</code> attribute to improve component structure and reusability.</p>
<p>The <code>data-slot</code> attribute allows developers to define placeholders within components that can be dynamically filled with content, reducing the need for excessive props or complex conditional rendering. By selecting elements based on <code>data-slot</code>, we can inject content where needed while keeping components clean and flexible.</p>
<h3><strong>A Practical Example: Button Component with <code>data-slot</code></strong></h3>
<p>Let’s say we have a <strong>button component</strong> that simply renders its children. We assign a fixed height of 40 pixels to the button and set padding of 12 pixels on the left and right.</p>
<pre class="phiki language-tsx nord" data-language="tsx" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">export</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">Button</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">{</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">children</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">}</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">	</span><span class="token" style="color: #81A1C1;">return</span><span class="token"> </span><span class="token">(</span><span class="token">
</span></span><span class="line"><span class="token">		</span><span class="token" style="color: #81A1C1;">&lt;</span><span class="token" style="color: #81A1C1;">button</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">className</span><span class="token" style="color: #81A1C1;">=</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">flex items-center justify-center gap-3 rounded bg-purple-500 text-white text-semibold px-3 h-[40px]</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #81A1C1;">&gt;</span><span class="token">
</span></span><span class="line"><span class="token">			</span><span class="token" style="color: #81A1C1;">{</span><span class="token" style="color: #D8DEE9;">children</span><span class="token" style="color: #81A1C1;">}</span><span class="token">
</span></span><span class="line"><span class="token">		</span><span class="token" style="color: #81A1C1;">&lt;/</span><span class="token" style="color: #81A1C1;">button</span><span class="token" style="color: #81A1C1;">&gt;</span><span class="token">
</span></span><span class="line"><span class="token">	</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>Now, based on this <code>Button</code> component, we can create a button with both text and an icon by passing them as children:</p>
<pre class="phiki language-tsx nord" data-language="tsx" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">export</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">App</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">	</span><span class="token" style="color: #81A1C1;">return</span><span class="token"> </span><span class="token">(</span><span class="token">
</span></span><span class="line"><span class="token">		</span><span class="token" style="color: #81A1C1;">&lt;</span><span class="token" style="color: #8FBCBB;">Button</span><span class="token" style="color: #81A1C1;">&gt;</span><span class="token">
</span></span><span class="line"><span class="token">			Performance monitoring
</span></span><span class="line"><span class="token">			</span><span class="token" style="color: #81A1C1;">&lt;</span><span class="token" style="color: #8FBCBB;">Icon</span><span class="token"> </span><span class="token" style="color: #81A1C1;">/&gt;</span><span class="token">
</span></span><span class="line"><span class="token">		</span><span class="token" style="color: #81A1C1;">&lt;/</span><span class="token" style="color: #8FBCBB;">Button</span><span class="token" style="color: #81A1C1;">&gt;</span><span class="token">
</span></span><span class="line"><span class="token">	</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>But what if we want a button with just an icon and no text? Simply removing the text does the job, but now the button's dimensions become inconsistent. Its width and height vary because of the padding, and ideally, we want it to be a square when only an icon is present.</p>
<p>To fix this, we could use the <code>aspect-square</code> class, but how does the <code>Button</code> component know when to apply it?</p>
<h3><strong>The Old Approach: Using a Prop</strong></h3>
<p>Before discovering <code>data-slot</code>, we handled this scenario by adding an explicit <code>icon</code> prop to the <code>Button</code> component:</p>
<pre class="phiki language-tsx nord" data-language="tsx" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">export</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">App</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">	</span><span class="token" style="color: #81A1C1;">return</span><span class="token"> </span><span class="token">(</span><span class="token">
</span></span><span class="line"><span class="token">		</span><span class="token" style="color: #81A1C1;">&lt;</span><span class="token" style="color: #8FBCBB;">Button</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">icon</span><span class="token" style="color: #81A1C1;">&gt;</span><span class="token">
</span></span><span class="line"><span class="token">			</span><span class="token" style="color: #81A1C1;">&lt;</span><span class="token" style="color: #8FBCBB;">Icon</span><span class="token"> </span><span class="token" style="color: #81A1C1;">/&gt;</span><span class="token">
</span></span><span class="line"><span class="token">		</span><span class="token" style="color: #81A1C1;">&lt;/</span><span class="token" style="color: #8FBCBB;">Button</span><span class="token" style="color: #81A1C1;">&gt;</span><span class="token">
</span></span><span class="line"><span class="token">	</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>This worked, but it was tedious. Every time we used an icon-only button, we had to remember to pass the <code>icon</code> prop, something easy to forget, leading to inconsistencies. Ideally, the button should automatically adjust based on its children.</p>
<h2>The <code>data-slot</code> Solution</h2>
<p>Instead of adding a prop, we can mark the <code>Icon</code> component with <code>data-slot="icon"</code>:</p>
<pre class="phiki language-tsx nord" data-language="tsx" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">export</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">Icon</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">	</span><span class="token" style="color: #81A1C1;">return</span><span class="token"> </span><span class="token">(</span><span class="token">
</span></span><span class="line"><span class="token">		</span><span class="token" style="color: #81A1C1;">&lt;</span><span class="token" style="color: #81A1C1;">span</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">data-slot</span><span class="token" style="color: #81A1C1;">=</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">icon</span><span class="token" style="color: #ECEFF4;">"</span><span class="token"> </span><span class="token" style="color: #81A1C1;">/&gt;</span><span class="token">
</span></span><span class="line"><span class="token">	</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>Then, we modify the <code>Button</code> component to detect when it only contains an icon:</p>
<pre class="phiki language-tsx nord" data-language="tsx" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">export</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">Button</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">{</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">children</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">}</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">	</span><span class="token" style="color: #81A1C1;">return</span><span class="token"> </span><span class="token">(</span><span class="token">
</span></span><span class="line"><span class="token">		</span><span class="token" style="color: #81A1C1;">&lt;</span><span class="token" style="color: #81A1C1;">button</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">className</span><span class="token" style="color: #81A1C1;">=</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">flex items-center justify-center gap-3 rounded bg-purple-500 text-white text-semibold px-3 h-[40px] [&amp;:has(&gt;[data-slot=icon]:only-child)]:aspect-square</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #81A1C1;">&gt;</span><span class="token">
</span></span><span class="line"><span class="token">			</span><span class="token" style="color: #81A1C1;">{</span><span class="token" style="color: #D8DEE9;">children</span><span class="token" style="color: #81A1C1;">}</span><span class="token">
</span></span><span class="line"><span class="token">		</span><span class="token" style="color: #81A1C1;">&lt;/</span><span class="token" style="color: #81A1C1;">button</span><span class="token" style="color: #81A1C1;">&gt;</span><span class="token">
</span></span><span class="line"><span class="token">	</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>Now, the <code>aspect-square</code> class is <strong>automatically</strong> applied when the button contains only an <code>Icon</code> component!</p>
<h2>Breaking Down the Class Selector</h2>
<p>Let’s analyze this newly added class:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">[&amp;:has(&gt;[data-slot=icon]:only-child)]:aspect-square
</span></span></code></pre>
<p>This means:</p>
<ul>
<li><code>&amp;</code>: Selects the current element (the <code>button</code>).</li>
<li><code>:has()</code>: A pseudo-selector that checks if the parent has a specific child.</li>
<li><code>&gt;[data-slot=icon]:only-child</code>: Selects a direct child with the attribute <code>data-slot="icon"</code> that has no siblings.</li>
</ul>
<h3><strong>Extending the Solution: Handling Padding</strong></h3>
<p>We can go a step further and remove padding when the button has only an icon:</p>
<pre class="phiki language-tsx nord" data-language="tsx" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">export</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">Button</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">{</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">children</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">}</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">	</span><span class="token" style="color: #81A1C1;">return</span><span class="token"> </span><span class="token">(</span><span class="token">
</span></span><span class="line"><span class="token">		</span><span class="token" style="color: #81A1C1;">&lt;</span><span class="token" style="color: #81A1C1;">button</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">className</span><span class="token" style="color: #81A1C1;">=</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">flex items-center justify-center gap-3 rounded bg-purple-500 text-white text-semibold h-[40px] [&amp;:has(&gt;[data-slot=icon]:only-child)]:aspect-square [&amp;:not(:has(&gt;[data-slot=icon]:only-child))]:px-3</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #81A1C1;">&gt;</span><span class="token">
</span></span><span class="line"><span class="token">			</span><span class="token" style="color: #81A1C1;">{</span><span class="token" style="color: #D8DEE9;">children</span><span class="token" style="color: #81A1C1;">}</span><span class="token">
</span></span><span class="line"><span class="token">		</span><span class="token" style="color: #81A1C1;">&lt;/</span><span class="token" style="color: #81A1C1;">button</span><span class="token" style="color: #81A1C1;">&gt;</span><span class="token">
</span></span><span class="line"><span class="token">	</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>This means:</p>
<ul>
<li>If the button has only an icon, it becomes a square and loses its padding.</li>
<li>If the button has other children, it retains <code>px-3</code> (left and right padding).</li>
</ul>
<h2>Why does this approach rock?</h2>
<p>This approach eliminates the need for extra props, allowing the button to adapt automatically and reducing unnecessary API surface. The JSX remains cleaner since there’s no need to manually specify button types or styles. With CSS handling the logic, components become more declarative and easier to maintain.</p>
<h2>A Potential Downside: Class Complexity</h2>
<p>While this approach is powerful, it can result in long, hard-to-read class names. To improve readability, we use the <a href="https://www.npmjs.com/package/clsx"><code>clsx</code></a> library to split rules onto separate lines:</p>
<pre class="phiki language-tsx nord" data-language="tsx" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #88C0D0;">clsx</span><span class="token">(</span><span class="token">
</span></span><span class="line"><span class="token">	</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">flex items-center justify-center gap-3 h-[40px]</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">	</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">rounded bg-purple-500 text-white text-semibold</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">	</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">[&amp;:not(:has(&gt;[data-slot=icon]:only-child))]:px-3</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">	</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">[&amp;:has(&gt;[data-slot=icon]:only-child)]:aspect-square</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">)</span><span class="token">
</span></span></code></pre>
<p>This makes the styling more manageable while keeping the logic intact. Although, we'll admit, it can still get out of hand.</p>
<h2>Final thoughts</h2>
<p>We’ve used the <code>data-slot</code> attribute in several components to dynamically adjust spacing, layout, and behavior based on their children. The <code>:has()</code> selector has been a game-changer. Without it, this level of flexibility wouldn’t be possible.</p>
<p>This was a relatively simple use case, but Adam Wathan goes much deeper in his talk. He demonstrates how form components can adapt based on their children and even tackles nerve-racking dropdown component issues.</p>
<p>If you’re interested in learning more, we highly recommend watching <a href="https://www.youtube.com/watch?v=MrzrSFbxW7M&amp;ab_channel=Laravel">his talk</a>. Hopefully, this inspires you as much as it did for us!</p>
]]>
            </summary>
                                    <updated>2025-02-18T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Building a micro dependency container, because why not?]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/building-a-micro-container-because-why-not" />
            <id>https://flareapp.io/building-a-micro-container-because-why-not</id>
            <author>
                <name><![CDATA[Ruben]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>We're currently completely rewriting the Flare backend client packages that run on our clients' servers. The original clients were written more than six years ago, and after more than a hundred releases, countless new features, and many bug fixes, it was time to review the packages.</p>
<p>In this blog post, I'm going to concentrate on the PHP agnostic package. Our Laravel package requires this package, which was written for non-PHP environments like WordPress, Symfony, CraftCMS, or any other PHP application/framework.</p>
<p>In Laravel applications and packages, we can use the almighty dependency container. A container can build PHP objects with dependencies on other objects, create objects based upon bindings provided by the developer, store singletons, and much more!</p>
<p>In our framework-agnostic package, we've refrained from using the Laravel Container since that would add an extra dependency on Laravel, which is weird in a framework-agnostic package. But with the rewrite, we found out that a single container that holds all the package dependencies is quite handy because it makes code a lot more readable and maintainable.</p>
<p>Therefore, let's add a container to the package that isn't the Laravel container. Other packages provide containers, and they work marvelously. But this also introduces an extra dependency, which we don't want.</p>
<p>As an extra requirement, since a lot of code is shared between the Laravel package and the framework-agnostic PHP package, it would be useful to share the bindings for building objects between the packages so that we only need to write them once.</p>
<p>That brings us back to the Laravel container ... software development is complicated. There's one extra feature about the Laravel container we haven't discussed: PSR-11. It is an abstraction written by the PHP-FIG and tries to create a standard interface so that framework components from different vendors can communicate with each other. Laravel's container is implementing the PSR-11 interface, which looks like this:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">interface</span><span class="token"> </span><span class="token">ContainerInterface</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">get</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">string</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">id</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">has</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">string</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">id</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #81A1C1;">bool</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>As you can see, it requires two methods to be implemented on a container:</p>
<ul>
<li><strong>get</strong> a way to retrieve (most of the time) an object from the container</li>
<li><strong>has</strong> a way to check if a binding exists in the container</li>
</ul>
<p>What if we built our own micro container implementing the <code>ContainerInterface</code> in our PHP agnostic package? Then, we would have a container in the PHP agnostic package, and since our container implements the same PSR interface, we could swap our container with Laravel's container in the Laravel package.</p>
<p>We could then create a ServiceProvider like this in the PHP agnostic package:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">use</span><span class="token"> </span><span class="token">Illuminate</span><span class="token" style="color: #ECEFF4;">\</span><span class="token">Contracts</span><span class="token" style="color: #ECEFF4;">\</span><span class="token">Container</span><span class="token" style="color: #ECEFF4;">\</span><span class="token" style="color: #8FBCBB;">Container</span><span class="token"> </span><span class="token" style="color: #81A1C1;">as</span><span class="token"> </span><span class="token">IlluminateContainer</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">use</span><span class="token"> </span><span class="token">Spatie</span><span class="token" style="color: #ECEFF4;">\</span><span class="token">FlareClient</span><span class="token" style="color: #ECEFF4;">\</span><span class="token">Support</span><span class="token" style="color: #ECEFF4;">\</span><span class="token" style="color: #8FBCBB;">Container</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">class</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">FlareProvider</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">__construct</span><span class="token" style="color: #ECEFF4;">(</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">protected</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">Container</span><span class="token" style="color: #ECEFF4;">|</span><span class="token" style="color: #8FBCBB;">IlluminateContainer</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">container</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">register</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #81A1C1;">void</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">     
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">	</span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> register further bindings   </span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>In a framework-agnostic project, you'll initialize Flare like this:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #8FBCBB;">Flare</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">make</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">your-api-key</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span></code></pre>
<p>Which would internally look like this:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">static</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">make</span><span class="token" style="color: #ECEFF4;">(</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">string</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">apiToken</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #81A1C1;">self</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">container</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">Container</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">instance</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">provider</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">new</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">FlareProvider</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">container</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">provider</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">register</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>In Laravel, we'll then have the following ServiceProvider:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">class</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">FlareServiceProvider</span><span class="token"> </span><span class="token" style="color: #81A1C1;">extends</span><span class="token"> </span><span class="token" style="color: #8FBCBB;font-weight: bold;">ServiceProvider</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">protected</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">FlareProvider</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">provider</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">register</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #81A1C1;">void</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">provider</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">new</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">FlareProvider</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">            </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">app</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">        </span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">        
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">provider</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">register</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>Now, within both the Laravel and framework agnostic package, we'll be able to get dependencies from the container as such:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">container</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">get</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #8FBCBB;">ViewExceptionMapper</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #81A1C1;">class</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span></code></pre>
<h2>Binding things</h2>
<p>The PSR-11 interface has one problem: It defines whether a binding exists in a container and provides a way to retrieve it. What it doesn't provide is a way to add bindings.</p>
<p>This isn't a problem in the Laravel container since the container supports auto-wiring, which means that an object with dependencies can be automatically resolved by resolving its entire chain of dependencies.</p>
<p>In our homegrown micro container, we don't want to think about auto-wiring; while implementing such a feature isn't that complicated, it adds extra maintenance to a part of the package we didn't even want to build in the beginning.</p>
<p>As an alternative to auto wiring, we could define all our bindings manually in the code. While this initially might seem like a lot of work, since we're working with a package, there aren't that many dependencies, so it won't be that much work. Since the container will only be used by the package, we know that the set of dependencies won't grow unless we add them ourselves. This means we just need to remember to add the bindings, and we're done.</p>
<p>Lastly, auto-wiring is a cool feature, but it makes applications slower since a lot of reflection is required to determine how to construct objects and their dependencies. By manually defining bindings, we'll have the quickest container possible, which is great since we don't want our package to add too much of a performance penalty to our client's applications.</p>
<p>That leaves one question: How should manual bindings be defined? Since our ServiceProvider is shared with a Laravel and framework-agnostic container, let's do it the same way as Laravel.</p>
<p>A binding can be added as such:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">container</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">bind</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #8FBCBB;">ViewExceptionMapper</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #81A1C1;">class</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #81A1C1;">fn</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">=&gt;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">new</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">ViewExceptionMapper</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">	</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">container</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">get</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #8FBCBB;">ViewDataResolver</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #81A1C1;">class</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span></code></pre>
<p>A default binding will be recreated every time it is resolved from the container. In some cases, we want to create a binding and store the object in the container so that the same object is returned the next time the binding needs to be created.</p>
<p>Basically, a singleton thus:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">container</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">bind</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #8FBCBB;">ConfigStore</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #81A1C1;">class</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #81A1C1;">fn</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">=&gt;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">new</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">ConfigStore</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">	</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">config</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span></code></pre>
<h2>Building our container</h2>
<p>Let's take a look at the implementation of the container:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">use</span><span class="token"> </span><span class="token">Psr</span><span class="token" style="color: #ECEFF4;">\</span><span class="token">Container</span><span class="token" style="color: #ECEFF4;">\</span><span class="token" style="color: #8FBCBB;">ContainerInterface</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">class</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">Container</span><span class="token"> </span><span class="token" style="color: #81A1C1;">implements</span><span class="token"> </span><span class="token" style="color: #8FBCBB;font-weight: bold;">ContainerInterface</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #616E88;">/**</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token" style="color: #616E88;">     * @template T
</span></span><span class="line"><span class="token" style="color: #616E88;">     *
</span></span><span class="line"><span class="token" style="color: #616E88;">     * </span><span class="token" style="color: #81A1C1;">@param</span><span class="token" style="color: #616E88;"> </span><span class="token" style="color: #81A1C1;">array</span><span class="token" style="color: #616E88;">&lt;</span><span class="token" style="color: #8FBCBB;">class</span><span class="token" style="color: #616E88;">-</span><span class="token" style="color: #81A1C1;">string</span><span class="token" style="color: #616E88;">&lt;</span><span class="token" style="color: #8FBCBB;">T</span><span class="token" style="color: #616E88;">&gt;,</span><span class="token" style="color: #616E88;"> Closure(): T&gt; $definitions
</span></span><span class="line"><span class="token" style="color: #616E88;">     * </span><span class="token" style="color: #81A1C1;">@param</span><span class="token" style="color: #616E88;"> </span><span class="token" style="color: #81A1C1;">array</span><span class="token" style="color: #616E88;">&lt;</span><span class="token" style="color: #8FBCBB;">class</span><span class="token" style="color: #616E88;">-</span><span class="token" style="color: #81A1C1;">string</span><span class="token" style="color: #616E88;">&lt;</span><span class="token" style="color: #8FBCBB;">T</span><span class="token" style="color: #616E88;">&gt;,</span><span class="token" style="color: #616E88;"> Closure(): T&gt; $singletons
</span></span><span class="line"><span class="token" style="color: #616E88;">     * </span><span class="token" style="color: #81A1C1;">@param</span><span class="token" style="color: #616E88;"> </span><span class="token" style="color: #81A1C1;">array</span><span class="token" style="color: #616E88;">&lt;</span><span class="token" style="color: #8FBCBB;">class</span><span class="token" style="color: #616E88;">-</span><span class="token" style="color: #81A1C1;">string</span><span class="token" style="color: #616E88;">&lt;</span><span class="token" style="color: #8FBCBB;">T</span><span class="token" style="color: #616E88;">&gt;,</span><span class="token" style="color: #616E88;"> T&gt; $initializedSingletons
</span></span><span class="line"><span class="token" style="color: #616E88;">     </span><span class="token" style="color: #616E88;">*/</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">__construct</span><span class="token" style="color: #ECEFF4;">(</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">protected</span><span class="token"> </span><span class="token" style="color: #81A1C1;">array</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">definitions</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">protected</span><span class="token"> </span><span class="token" style="color: #81A1C1;">array</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">singletons</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">protected</span><span class="token"> </span><span class="token" style="color: #81A1C1;">array</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">initializedSingletons</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #616E88;">/**</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token" style="color: #616E88;">     * @template T
</span></span><span class="line"><span class="token" style="color: #616E88;">     *
</span></span><span class="line"><span class="token" style="color: #616E88;">     * </span><span class="token" style="color: #81A1C1;">@param</span><span class="token" style="color: #616E88;"> </span><span class="token" style="color: #8FBCBB;">class</span><span class="token" style="color: #616E88;">-</span><span class="token" style="color: #81A1C1;">string</span><span class="token" style="color: #616E88;">&lt;</span><span class="token" style="color: #8FBCBB;">T</span><span class="token" style="color: #616E88;">&gt;</span><span class="token" style="color: #616E88;"> $class
</span></span><span class="line"><span class="token" style="color: #616E88;">     * </span><span class="token" style="color: #81A1C1;">@param</span><span class="token" style="color: #616E88;"> </span><span class="token" style="color: #81A1C1;">null</span><span class="token" style="color: #ECEFF4;">|</span><span class="token" style="color: #8FBCBB;">Closure</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #616E88;">:</span><span class="token" style="color: #8FBCBB;">T</span><span class="token" style="color: #616E88;"> $builder
</span></span><span class="line"><span class="token" style="color: #616E88;">     </span><span class="token" style="color: #616E88;">*/</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">singleton</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">string</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">class</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #81A1C1;">?</span><span class="token" style="color: #8FBCBB;">Closure</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">builder</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">null</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #81A1C1;">void</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">singletons</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">class</span><span class="token" style="color: #ECEFF4;">]</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">builder</span><span class="token"> </span><span class="token" style="color: #81A1C1;">??</span><span class="token"> </span><span class="token" style="color: #81A1C1;">fn</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">=&gt;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">new</span><span class="token"> </span><span class="token">$</span><span class="token" style="color: #81A1C1;">class</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #616E88;">/**</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token" style="color: #616E88;">     * @template T
</span></span><span class="line"><span class="token" style="color: #616E88;">     *
</span></span><span class="line"><span class="token" style="color: #616E88;">     * </span><span class="token" style="color: #81A1C1;">@param</span><span class="token" style="color: #616E88;"> </span><span class="token" style="color: #8FBCBB;">class</span><span class="token" style="color: #616E88;">-</span><span class="token" style="color: #81A1C1;">string</span><span class="token" style="color: #616E88;">&lt;</span><span class="token" style="color: #8FBCBB;">T</span><span class="token" style="color: #616E88;">&gt;</span><span class="token" style="color: #616E88;"> $class
</span></span><span class="line"><span class="token" style="color: #616E88;">     * </span><span class="token" style="color: #81A1C1;">@param</span><span class="token" style="color: #616E88;"> </span><span class="token" style="color: #81A1C1;">null</span><span class="token" style="color: #ECEFF4;">|</span><span class="token" style="color: #8FBCBB;">Closure</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #616E88;">:</span><span class="token" style="color: #8FBCBB;">T</span><span class="token" style="color: #616E88;"> $builder
</span></span><span class="line"><span class="token" style="color: #616E88;">     </span><span class="token" style="color: #616E88;">*/</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">bind</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">string</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">class</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #81A1C1;">?</span><span class="token" style="color: #8FBCBB;">Closure</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">builder</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">null</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #81A1C1;">void</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">definitions</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">class</span><span class="token" style="color: #ECEFF4;">]</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token">  </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">builder</span><span class="token"> </span><span class="token" style="color: #81A1C1;">??</span><span class="token"> </span><span class="token" style="color: #81A1C1;">fn</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">=&gt;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">new</span><span class="token"> </span><span class="token">$</span><span class="token" style="color: #81A1C1;">class</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #616E88;">/**</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token" style="color: #616E88;">     * @template T
</span></span><span class="line"><span class="token" style="color: #616E88;">     *
</span></span><span class="line"><span class="token" style="color: #616E88;">     * </span><span class="token" style="color: #81A1C1;">@param</span><span class="token" style="color: #616E88;"> </span><span class="token" style="color: #8FBCBB;">class</span><span class="token" style="color: #616E88;">-</span><span class="token" style="color: #81A1C1;">string</span><span class="token" style="color: #616E88;">&lt;</span><span class="token" style="color: #8FBCBB;">T</span><span class="token" style="color: #616E88;">&gt;</span><span class="token" style="color: #616E88;"> $id
</span></span><span class="line"><span class="token" style="color: #616E88;">     *
</span></span><span class="line"><span class="token" style="color: #616E88;">     * </span><span class="token" style="color: #81A1C1;">@return</span><span class="token" style="color: #616E88;"> </span><span class="token" style="color: #8FBCBB;">T</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token" style="color: #616E88;">     </span><span class="token" style="color: #616E88;">*/</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">get</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">string</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">id</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">if</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #88C0D0;">array_key_exists</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">id</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">initializedSingletons</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">return</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">initializedSingletons</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">id</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">if</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #88C0D0;">array_key_exists</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">id</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">singletons</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">return</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">initializedSingletons</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">id</span><span class="token" style="color: #ECEFF4;">]</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">singletons</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">id</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">if</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #88C0D0;">array_key_exists</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">id</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">definitions</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">return</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">definitions</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">id</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">throw</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">ContainerEntryNotFoundException</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">make</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">id</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">has</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">string</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">id</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #81A1C1;">bool</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">return</span><span class="token"> </span><span class="token" style="color: #88C0D0;">array_key_exists</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">id</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">definitions</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #81A1C1;">||</span><span class="token"> </span><span class="token" style="color: #88C0D0;">array_key_exists</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">id</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">singletons</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>That's probably the smallest PHP container out there, but it works!</p>
<p>Since a container is only resolved once per application, it is an excellent case for the singleton pattern. This allows us to access the container in places where passing the initially created container object is cumbersome.</p>
<p>We can quickly add support for a singleton like this:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">class</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">Container</span><span class="token"> </span><span class="token" style="color: #81A1C1;">implements</span><span class="token"> </span><span class="token" style="color: #8FBCBB;font-weight: bold;">ContainerInterface</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">private</span><span class="token"> </span><span class="token" style="color: #81A1C1;">static</span><span class="token"> </span><span class="token" style="color: #81A1C1;">self</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">instance</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">static</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">instance</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #81A1C1;">self</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">return</span><span class="token"> </span><span class="token" style="color: #81A1C1;">self</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">instance</span><span class="token"> </span><span class="token" style="color: #81A1C1;">??=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">new</span><span class="token"> </span><span class="token" style="color: #81A1C1;">self</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #616E88;">/**</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token" style="color: #616E88;">     * @template T
</span></span><span class="line"><span class="token" style="color: #616E88;">     *
</span></span><span class="line"><span class="token" style="color: #616E88;">     * </span><span class="token" style="color: #81A1C1;">@param</span><span class="token" style="color: #616E88;"> </span><span class="token" style="color: #81A1C1;">array</span><span class="token" style="color: #616E88;">&lt;</span><span class="token" style="color: #8FBCBB;">class</span><span class="token" style="color: #616E88;">-</span><span class="token" style="color: #81A1C1;">string</span><span class="token" style="color: #616E88;">&lt;</span><span class="token" style="color: #8FBCBB;">T</span><span class="token" style="color: #616E88;">&gt;,</span><span class="token" style="color: #616E88;"> Closure(): T&gt; $definitions
</span></span><span class="line"><span class="token" style="color: #616E88;">     * </span><span class="token" style="color: #81A1C1;">@param</span><span class="token" style="color: #616E88;"> </span><span class="token" style="color: #81A1C1;">array</span><span class="token" style="color: #616E88;">&lt;</span><span class="token" style="color: #8FBCBB;">class</span><span class="token" style="color: #616E88;">-</span><span class="token" style="color: #81A1C1;">string</span><span class="token" style="color: #616E88;">&lt;</span><span class="token" style="color: #8FBCBB;">T</span><span class="token" style="color: #616E88;">&gt;,</span><span class="token" style="color: #616E88;"> Closure(): T&gt; $singletons
</span></span><span class="line"><span class="token" style="color: #616E88;">     * </span><span class="token" style="color: #81A1C1;">@param</span><span class="token" style="color: #616E88;"> </span><span class="token" style="color: #81A1C1;">array</span><span class="token" style="color: #616E88;">&lt;</span><span class="token" style="color: #8FBCBB;">class</span><span class="token" style="color: #616E88;">-</span><span class="token" style="color: #81A1C1;">string</span><span class="token" style="color: #616E88;">&lt;</span><span class="token" style="color: #8FBCBB;">T</span><span class="token" style="color: #616E88;">&gt;,</span><span class="token" style="color: #616E88;"> T&gt; $initializedSingletons
</span></span><span class="line"><span class="token" style="color: #616E88;">     </span><span class="token" style="color: #616E88;">*/</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">protected</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">__construct</span><span class="token" style="color: #ECEFF4;">(</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">protected</span><span class="token"> </span><span class="token" style="color: #81A1C1;">array</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">definitions</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">protected</span><span class="token"> </span><span class="token" style="color: #81A1C1;">array</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">singletons</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">protected</span><span class="token"> </span><span class="token" style="color: #81A1C1;">array</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">initializedSingletons</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">    
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">    </span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> The rest of the container code</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<h2>Define agnostic, extend in Laravel</h2>
<p>What if we created a binding in the framework-agnostic version of the package and then needed to redefine this binding in the Laravel package?</p>
<p>Well, that won't be a problem since the latest binding always takes precedence over the earlier defined bindings.</p>
<p>We can illustrate this with the following piece of code:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">container</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">bind</span><span class="token" style="color: #ECEFF4;">(</span><span class="token">
</span></span><span class="line"><span class="token">	</span><span class="token" style="color: #8FBCBB;">ViewExceptionMapper</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #81A1C1;">class</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> 
</span></span><span class="line"><span class="token">	</span><span class="token" style="color: #81A1C1;">fn</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">=&gt;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">new</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">ViewExceptionMapper</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">		</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">container</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">get</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #8FBCBB;">ViewDataResolver</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #81A1C1;">class</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">	</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">container</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">bind</span><span class="token" style="color: #ECEFF4;">(</span><span class="token">
</span></span><span class="line"><span class="token">	</span><span class="token" style="color: #8FBCBB;">ViewExceptionMapper</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #81A1C1;">class</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #81A1C1;">fn</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">=&gt;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">new</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">LaravelViewExceptionMapper</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">		</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">container</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">get</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #8FBCBB;">ViewDataResolver</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #81A1C1;">class</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">	</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">container</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">get</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #8FBCBB;">ViewExceptionMapper</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #81A1C1;">class</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token"> </span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> returns LaravelViewExceptionMapper</span><span class="token" style="color: #616E88;">
</span></span></code></pre>
<p>This works great! But sometimes, we already have a binding in the framework-agnostic package and want to keep it as is but transform it just a bit to make it work with the Laravel package.</p>
<p>An example of such an object is the <code>Resource</code> object, a key-value store for the current resource (aka server, OS, network, etc.) on which the code is running.</p>
<p>In the framework agnostic package, we'll define the binding as such:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">container</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">singleton</span><span class="token" style="color: #ECEFF4;">(</span><span class="token">
</span></span><span class="line"><span class="token">	</span><span class="token" style="color: #8FBCBB;">Resource</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #81A1C1;">class</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> 
</span></span><span class="line"><span class="token">	</span><span class="token" style="color: #81A1C1;">fn</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">=&gt;</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">Resource</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">create</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">addAttribute</span><span class="token" style="color: #ECEFF4;">(</span><span class="token">
</span></span><span class="line"><span class="token">		</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">host.ip</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">		</span><span class="token" style="color: #88C0D0;">gethostbyname</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #88C0D0;">gethostname</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">	</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span></code></pre>
<p>Then, in the Laravel version of the package, we also want to include the current version of Laravel the code is running on; this can be done by using a native function called <code>extend</code>:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">container</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">extend</span><span class="token" style="color: #ECEFF4;">(</span><span class="token">
</span></span><span class="line"><span class="token">	</span><span class="token" style="color: #8FBCBB;">Resource</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #81A1C1;">class</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">	</span><span class="token" style="color: #81A1C1;">fn</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #8FBCBB;">Resource</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">resource</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">=&gt;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">resource</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">addAttribute</span><span class="token" style="color: #ECEFF4;">(</span><span class="token">
</span></span><span class="line"><span class="token">		</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">laravel.version</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">		</span><span class="token" style="color: #88C0D0;">app</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">version</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span><span class="line"><span class="token">	</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span></code></pre>
<p>The next time the Laravel container resolves the <code>Resource</code> class, not only <code>host.ip</code> but also <code>laravel.version</code> will be added. That's neat!</p>
<h2>In the end</h2>
<p>This micro container will appear in the next major version of the Flare PHP client. As Spatie, we have been considering turning this into a package (of course, we have 😅). This isn't the plan at the moment since it is just a tiny, non-complicated piece of code to make the package work.</p>
<p>But as a young Canadian pop idol once said, never say never.</p>
]]>
            </summary>
                                    <updated>2025-01-20T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Customizing error grouping]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/customizing-error-grouping" />
            <id>https://flareapp.io/customizing-error-grouping</id>
            <author>
                <name><![CDATA[Ruben]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>For Flare, we've spent a lot of time optimizing our grouping algorithm. This algorithm groups occurrences of errors or exceptions together into one error.</p>
<p>Without such an algorithm, it would be impossible to use Flare since every time your application throws an exception or error, a new error is created in Flare, making oversight impossible.</p>
<p>We're quite proud of our algorithm since it works for 99% of the cases, but sometimes, in our support channel, we get the question if it would be possible to have a little more control over how errors are grouped.</p>
<p>We've implemented such a feature recently!</p>
<p>For example, let's say you're using an HTTP client to contact an external service, which returns a response with an error message when something is going wrong.</p>
<p>You're probably not implementing an HTTP client yourself, Guzzle or Laravel's HTTP client are perfect options for making HTTP requests without having to think about the rabbit hole of problems that might arise when sending HTTP requests.</p>
<p>The HTTP client package will make the request and throw an exception when something is wrong. The problem is that Flare's grouping algorithm will group all these exceptions even if the message differs.</p>
<p>That's for a reason. While exception messages might often look the same, as soon as a uuid, integer, email, or other user-specific information is present within the message, it would create a completely different error item within Flare (if we would group based upon the exception message).</p>
<p>When Flare processes an error, we examine its stack trace and a few other properties and intelligently group it.</p>
<p>Now, there are cases where we want to group by exception message, as mentioned above. This is possible starting today when using the latest version of the <a href="https://github.com/spatie/laravel-flare">laravel-flare</a> or <a href="https://github.com/spatie/flare-client-php">flare-client-php</a> package.</p>
<p>In the Laravel package, you'll find a new config value:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">overridden_groupings</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">[</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;">        Illuminate\Http\Client\ConnectionException::class =&gt; Spatie\FlareClient\Enums\OverriddenGrouping::ExceptionMessageAndClass,</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span></code></pre>
<p>In this array, you can define exception classes that should be grouped based on:</p>
<ul>
<li>ExceptionClass: group all exceptions with the same exception class together</li>
<li>ExceptionMessage: group all exceptions with the same message together</li>
<li>ExceptionClassAndMessage: group all exceptions with the same message and exception class together</li>
</ul>
<p>In the framework agnostic Flare client, you can add overrides as such:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">flare</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">overrideGrouping</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #8FBCBB;">SomeExceptionClass</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #81A1C1;">class</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">OverriddenGrouping</span><span class="token" style="color: #81A1C1;">::</span><span class="token">ExceptionMessageAndClass</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span></code></pre>
<p>We're constantly innovating Flare to improve the developer experience, and we'll share our biggest update within a few weeks. See you then!</p>
]]>
            </summary>
                                    <updated>2025-01-06T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Perfecting Flare: one annoyance at a time]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/perfecting-flare-one-annoyance-at-a-time" />
            <id>https://flareapp.io/perfecting-flare-one-annoyance-at-a-time</id>
            <author>
                <name><![CDATA[Alex]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Over the next few months, we're committed to fixing bugs - not just in your code, but in our app too.  We’ve got a bunch of improvements lined up to make Flare even better! Any issues you are having with Flare? Let us know on support@flareapp.io</p>
<h2>This week's special: better number formatting</h2>
<p>Something that's been bothering some of us for a while, is Flare's number formatting on the project overview. It feels like decoding the Matrix when looking at unformatted labels like <code>3249870 occurrences</code>.</p>
<p>So today, we've given our number formatting a much-needed makeover:</p>
<p><img src="https://assets.flareapp.io/production/blog/69a8b61295dc710f226c4968de2b5ad2/def67022-a3d2-446c-9d08-55e9e41daaed.jpg" alt="Flare screenshot showing formatted numbers" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/69a8b61295dc710f226c4968de2b5ad2/def67022-a3d2-446c-9d08-55e9e41daaed.jpg" width="2048" height="1024" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/69a8b61295dc710f226c4968de2b5ad2/def67022-a3d2-446c-9d08-55e9e41daaed.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/69a8b61295dc710f226c4968de2b5ad2/def67022-a3d2-446c-9d08-55e9e41daaed.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/69a8b61295dc710f226c4968de2b5ad2/def67022-a3d2-446c-9d08-55e9e41daaed.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/69a8b61295dc710f226c4968de2b5ad2/def67022-a3d2-446c-9d08-55e9e41daaed.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>Same data, just more pleasing to look at. Hopefully this small change makes your Flare experience better! Stay tuned as this is just the beginning of our quest to make Flare even better.</p>
]]>
            </summary>
                                    <updated>2024-10-18T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Browser extension errors begone!]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/browser-extension-error-noise-begone" />
            <id>https://flareapp.io/browser-extension-error-noise-begone</id>
            <author>
                <name><![CDATA[Sebastian]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Our team has received a lot of feedback about errors caused by browser extensions flooding projects. We have listened! In the latest version of our JavaScript client, errors triggered by browser extensions will not be reported anymore.</p>
<p>Our JavaScript client sends all errors to Flare. When the error is processing on our servers, we'll check if it's is likely caused by a browser extension. If it is, we won't report it and it won't count towards your monthly usage.</p>
<p>Update to our latest package version rid of those pesky errors, we'll take care of it from there!</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">npm install "@flareapp/js@1.1.0"
</span></span></code></pre>
<p>If you <em>do</em> want to see errors caused by extensions, you can opt in to reporting them by configuring the client.</p>
<pre class="phiki language-javascript nord" data-language="javascript" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #D8DEE9;">flare</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #88C0D0;">light</span><span class="token">(</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #D8DEE9;">flare</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #88C0D0;">configure</span><span class="token">(</span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">  </span><span class="token" style="color: #88C0D0;">reportBrowserExtensionErrors</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #81A1C1;">true</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span></code></pre>
<p>There's no way for a browser to tell us whether an error is triggered by an extension or not. We partially rely on magic (aka regex) to detect them. If you still notice a bunch of errors that shouldn't be there: let us know! We love data we can use to improve our product.</p>
]]>
            </summary>
                                    <updated>2024-10-10T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Using 1password for Laravel environment variables]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/using-1password-for-laravel-environment-variables" />
            <id>https://flareapp.io/using-1password-for-laravel-environment-variables</id>
            <author>
                <name><![CDATA[Alex]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Let's face it: secret management isn't the most thrilling part of development, but it's crucial. Even though it's very tempting to manage environment variables and secrets by <code>ssh</code>ing into a server and typing<code>vi .env</code>, better, and more secure alternatives are available. Let's look at some options, why we chose the 1Password CLI and how to set it up.</p>
<h2>Why should I care?</h2>
<p><img src="https://assets.flareapp.io/production/blog/0d707cdcaa089a8a749f4487ff2606af/bd5650ca-e61a-41bc-9b98-f845d6e9ca2b.jpg" alt="Secret secrets are no fun, secret secrets hurt someone" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/0d707cdcaa089a8a749f4487ff2606af/bd5650ca-e61a-41bc-9b98-f845d6e9ca2b.jpg" width="1080" height="1054" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/0d707cdcaa089a8a749f4487ff2606af/bd5650ca-e61a-41bc-9b98-f845d6e9ca2b.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/0d707cdcaa089a8a749f4487ff2606af/bd5650ca-e61a-41bc-9b98-f845d6e9ca2b.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/0d707cdcaa089a8a749f4487ff2606af/bd5650ca-e61a-41bc-9b98-f845d6e9ca2b.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1080/https://assets.flareapp.io/production/blog/0d707cdcaa089a8a749f4487ff2606af/bd5650ca-e61a-41bc-9b98-f845d6e9ca2b.jpg 1080w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>Secret management goes beyond just keeping things secure - it's also about access control and avoiding accidents. Not everyone needs (all) the keys to the kingdom. You might want to give your junior dev access to the staging environment, but keep the production credentials under stricter control. In corporate speak this is called "implementing the principle of least privilege for effective risk mitigation in secret management", but we'll call it common sense.</p>
<h2>The right tool for the right job</h2>
<p>Before jumping right to the HashiCorp Vaults of this world, you might want to consider these more accessible options for your small to medium size Laravel application:</p>
<table>
<thead>
<tr>
<th>Product</th>
<th>Git Compatibility</th>
<th>Granular User Access</th>
<th>Encryption</th>
</tr>
</thead>
<tbody>
<tr>
<td>artisan env:encrypt</td>
<td>❌</td>
<td>❌</td>
<td>✔️</td>
</tr>
<tr>
<td>git-secret</td>
<td>❌</td>
<td>✔️</td>
<td>✔️</td>
</tr>
<tr>
<td>1Password CLI</td>
<td>✔️</td>
<td>✔️</td>
<td>✔️</td>
</tr>
</tbody>
</table>
<p>As you can see, <code>env:encrypt</code> and <code>git-secret</code> are both effective tools for securely storing your environment variables. However, they save the encrypted secrets as binary files, which makes merging them with Git impossible. By using the 1Password CLI, we can manage <code>.env</code> files for all our application's environments without any of the downsides.</p>
<h2>1Password to rule them all</h2>
<p><img src="https://assets.flareapp.io/production/blog/a62dcb9c4939ccabfe15a1c6427651bc/13bee56c-78ab-440c-b0d1-9ad7cf41ec6f.jpg" alt="My precious 1Password" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/a62dcb9c4939ccabfe15a1c6427651bc/13bee56c-78ab-440c-b0d1-9ad7cf41ec6f.jpg" width="2560" height="1400" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/a62dcb9c4939ccabfe15a1c6427651bc/13bee56c-78ab-440c-b0d1-9ad7cf41ec6f.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/a62dcb9c4939ccabfe15a1c6427651bc/13bee56c-78ab-440c-b0d1-9ad7cf41ec6f.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/a62dcb9c4939ccabfe15a1c6427651bc/13bee56c-78ab-440c-b0d1-9ad7cf41ec6f.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/a62dcb9c4939ccabfe15a1c6427651bc/13bee56c-78ab-440c-b0d1-9ad7cf41ec6f.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>For as long as I can remember, we’ve relied on 1Password to securely share passwords, secrets, and API keys with the Spatie team. It feels like a logical progression to also use their developer tools for integrating those very secrets into our deployments. Did I mention there's excellent <a href="https://plugins.jetbrains.com/plugin/19698-1password">JetBrains</a> and <a href="https://marketplace.visualstudio.com/items?itemName=1Password.op-vscode">VS Code</a> plugins as well, that make working with 1Password references in your config and .env files even easier?</p>
<h2>Generating <code>.env</code> files with 1Password</h2>
<p>1Password's CLI, <code>op</code>, has a couple of useful commands. We'll be using <code>op inject</code> to inject some secrets from 1Password entries into a template file.</p>
<p>The <code>.env.template</code> file:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">APP_KEY=op://Flare/Environment/APP_KEY
</span></span><span class="line"><span class="token"># and other variables...
</span></span></code></pre>
<p>Executing this command will output a new <code>.env</code> file with the 1Password references replaces with their actual values:</p>
<pre class="phiki language-shellscript nord" data-language="shellscript" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #88C0D0;">op</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">inject</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">-</span><span class="token" style="color: #A3BE8C;">i</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">.env.template</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">-</span><span class="token" style="color: #A3BE8C;">o</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">.env</span><span class="token">
</span></span></code></pre>
<p>The outputted <code>.env</code> file looks like this:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">APP_KEY=123abc
</span></span><span class="line"><span class="token"># and other variables...
</span></span></code></pre>
<p>Simple, yet effective.</p>
<h2>One template, many environments</h2>
<p>Typically, you'll have different environment variables for your staging and production environment. Maybe you even want to share a set of local environment variables with your team to make local development easier.</p>
<p>For this, we'll create separate entries in our 1Password vault for each of these environments. Be sure to include the environment name in the entry title, as we'll use that to link it to the <code>APP_ENV</code>.</p>
<p><img src="https://assets.flareapp.io/production/blog/0bab1a22f790a35b29dd1c587cd0d35e/77e205f6-895f-42ba-9922-9fed24e5c1e2.jpg" alt="Screenshot of 1Password showing stagin-env and production-env entries" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/0bab1a22f790a35b29dd1c587cd0d35e/77e205f6-895f-42ba-9922-9fed24e5c1e2.jpg" width="1920" height="900" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/0bab1a22f790a35b29dd1c587cd0d35e/77e205f6-895f-42ba-9922-9fed24e5c1e2.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/0bab1a22f790a35b29dd1c587cd0d35e/77e205f6-895f-42ba-9922-9fed24e5c1e2.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/0bab1a22f790a35b29dd1c587cd0d35e/77e205f6-895f-42ba-9922-9fed24e5c1e2.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/0bab1a22f790a35b29dd1c587cd0d35e/77e205f6-895f-42ba-9922-9fed24e5c1e2.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<blockquote>
<p>[!TIP]
Keep the set of variables consistent across environments. The <code>op</code> CLI will complain if you use a 1Password reference that doesn't exist in your 1Password entry.</p>
</blockquote>
<p>Next, you can use the <code>$APP_ENV</code> variable in your 1Password references to access the correct entries (or vaults):</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token"># The $APP_ENV will be replaced with "staging" or "production"
</span></span><span class="line"><span class="token">APP_KEY=op://Flare/$APP_ENV-env/APP_KEY
</span></span></code></pre>
<p>By setting the <code>APP_ENV</code> before running the <code>op inject</code> command, we can generate an <code>.env</code> file using a specific 1Password entry, for a specific environment:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">APP_ENV=staging op inject -i .env.template -o .env
</span></span></code></pre>
<blockquote>
<p>[!WARNING]
Instead of creating separate entries in 1Password for each environment, you should create different Vaults. This approach allows you to manage access to each environment's secrets separately.</p>
</blockquote>
<h2>Integrating 1Password in your deploy process</h2>
<p>Depending on your deployment process, there's two options to consider when integrating the 1Password CLI:</p>
<ul>
<li>generating the <code>.env</code> directly on your production server(s)</li>
<li>generating the <code>.env</code> file on your local machine (or CI) and copying it to the production server(s)</li>
</ul>
<p>We prefer the first option, this way we never have to store any secrets on your local machine or CI.</p>
<p>To access the 1Password vault on your production or staging servers, you'll need to <a href="https://developer.1password.com/docs/service-accounts/get-started/#create-a-service-account">create a 1Password service account</a>. The <code>op</code> CLI and service account also need to be configured on your production or staging servers. It's best to only configure this service account during the deployment process. This way the <code>op</code> CLI is useless for any bad actor that might gain direct access to the server.</p>
<p>In Laravel Envoy this looks like this:</p>
<pre class="phiki language-blade nord" data-language="blade" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">@setup</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> Get the service account token using the local 1Password CLI</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">opServiceAccount</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #88C0D0;">shell_exec</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">op read "op://Flare/service-account/credential"</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">environment</span><span class="token"> ??</span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">staging</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">@endsetup</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">...
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">@task</span><span class="token">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">updateEnvironment</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">on</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">environment</span><span class="token" style="color: #ECEFF4;">]</span><span class="token">)</span><span class="token">
</span></span><span class="line"><span class="token"># Configure the service account only for this shell session
</span></span><span class="line"><span class="token">export OP_SERVICE_ACCOUNT_TOKEN=</span><span class="token" style="color: #81A1C1;">{{</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">opServiceAccount</span><span class="token"> </span><span class="token" style="color: #81A1C1;">}}</span><span class="token">  
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">// Set the APP_ENV based on the $environment and generate the .env
</span></span><span class="line"><span class="token">APP_ENV=</span><span class="token" style="color: #81A1C1;">{{</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">environment</span><span class="token"> </span><span class="token" style="color: #81A1C1;">}}</span><span class="token"> op inject -i .env.1pass -o .env
</span></span><span class="line"><span class="token" style="color: #81A1C1;">@endtask</span><span class="token">
</span></span></code></pre>
<p>In the <code>@setup</code> block, we're loading the service account credentials from our local <code>op</code> CLI. Once we're ready to generate the <code>.env</code> file on our remote server, the <code>updateEnvironment</code> task will configure the service account token and run the <code>op inject</code> command for the current environment.</p>
<p>If you're using GitHub actions to build or deploy your application, there's this <a href="https://github.com/1Password/install-cli-action">excellent GitHub action</a> to install, configure and use the <code>op</code> CLI in your GH actions.</p>
<h2>Wrapping up</h2>
<p>And there you have it - using the 1Password CLI for your Laravel <code>.env</code> files makes secret management less of a headache and more secure.</p>
]]>
            </summary>
                                    <updated>2024-09-30T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[When everything fails, you can always trust the Laravel rescue helper]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/when-everything-fails-you-can-always-trust-the-laravel-rescue-helper" />
            <id>https://flareapp.io/when-everything-fails-you-can-always-trust-the-laravel-rescue-helper</id>
            <author>
                <name><![CDATA[Ruben]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Your application probably has a number of minor and more significant bugs. Some bugs will destroy your application, making it impossible to use, while others can be ignored.</p>
<p>The problem? When something goes wrong, we love to throw an exception; it is so easy:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">throw</span><span class="token"> </span><span class="token" style="color: #81A1C1;">new</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">Exception</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">This thing went wrong, I'm done here it is up to the next person to handle this</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span></code></pre>
<p>But that exception doesn't tell you anything about the severity of what happened. It could be a TypeError where you used a string for a function argument while a complex object with multiple data properties is required. It could also be that you're sending an email notification to someone whose email address no longer exists, so the request failed.</p>
<p>In that first case, your application breaks, and that's probably best; we're trying to do something in our application with data that is not valid for the operation. While the second example shouldn't break the application, we'll want to remove that email address from our lists so that we stop sending to it next time.</p>
<h2>Try/Catch to the ... rescue</h2>
<p>Luckily, this problem can be quickly solved by using a try/catch statement:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">try</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #88C0D0;">sendNotification</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">email</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">contents</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token"> </span><span class="token" style="color: #81A1C1;">catch</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #8FBCBB;">EmailDoesNotExist</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #88C0D0;">removeEmail</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">email</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>Now, what if we want to track these <code>EmailDoesNotExist</code> exceptions? As said earlier, code always has bugs, and it could be that something is broken in this piece of code, and the email address is not being removed.</p>
<p>It would be handy to send these exceptions to an error tracker like Flare, which is the best error tracker for Laravel!</p>
<h2>Laravel error handling to the ... rescue</h2>
<p>We can change our try-catch statement like this:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">try</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #88C0D0;">sendNotification</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">email</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">contents</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token"> </span><span class="token" style="color: #81A1C1;">catch</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #8FBCBB;">EmailDoesNotExist</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">exception</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #88C0D0;">report</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">exception</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #88C0D0;">removeEmail</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">email</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>The exception will be reported through Laravel's error handling system and eventually sent to a chosen error tracker, but your application will keep running like nothing happened.</p>
<p>A cool feature of Flare is that we mark these kinds of exceptions as handled with an extra label:</p>
<p><img src="https://assets.flareapp.io/production/blog/7e5bf91c4f1c29e5a9dc2d6db3cbc7cb/c6be7a32-c2aa-4c1a-b73d-524f6976e565.png" alt="A handled error" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/7e5bf91c4f1c29e5a9dc2d6db3cbc7cb/c6be7a32-c2aa-4c1a-b73d-524f6976e565.png" width="2366" height="432" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/7e5bf91c4f1c29e5a9dc2d6db3cbc7cb/c6be7a32-c2aa-4c1a-b73d-524f6976e565.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/7e5bf91c4f1c29e5a9dc2d6db3cbc7cb/c6be7a32-c2aa-4c1a-b73d-524f6976e565.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/7e5bf91c4f1c29e5a9dc2d6db3cbc7cb/c6be7a32-c2aa-4c1a-b73d-524f6976e565.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/7e5bf91c4f1c29e5a9dc2d6db3cbc7cb/c6be7a32-c2aa-4c1a-b73d-524f6976e565.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>This label makes it easy to see if you should start panicking if an error is happening a bazillion times or if it is just business as usual.</p>
<p>But ... we're working with Laravel. Can we rewrite these lines of code to some of that polished beautiful Laravel code?</p>
<h2>Laravel rescue to the ...</h2>
<p>Laravel provides a rescue helper that mimics the try/catch statement we've written earlier. It works like this:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #88C0D0;">rescue</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">    </span><span class="token" style="color: #81A1C1;">fn</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #ECEFF4;">=&gt;</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #88C0D0;">sendNotification</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">email</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">contents</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">    </span><span class="token" style="color: #81A1C1;">fn</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #ECEFF4;">=&gt;</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #88C0D0;">removeEmail</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">email</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span></code></pre>
<p>You've got to love the beauty of those four lines of code!</p>
<p>Do you want to decide if you're going to report the exception? You can add a third closure like this:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">rescue(
</span></span><span class="line"><span class="token">    fn() =&gt; sendNotification($email, $contents),
</span></span><span class="line"><span class="token">    fn() =&gt; removeEmail($email),
</span></span><span class="line"><span class="token">    fn(EmailDoesNotExist $exception) =&gt; str_contains($exception-&gt;getMessage(), "@spatie.be")
</span></span><span class="line"><span class="token">);
</span></span></code></pre>
<h2>Conclusion</h2>
<p>Laravel has many of these tiny helpers. The rescue helper is probably one of my favorites. They allow you to write more readable code even faster!</p>
]]>
            </summary>
                                    <updated>2024-08-23T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Check your Laravel Horizon failed jobs from Flare]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/check-your-laravel-horizon-failed-jobs-from-flare" />
            <id>https://flareapp.io/check-your-laravel-horizon-failed-jobs-from-flare</id>
            <author>
                <name><![CDATA[Ruben]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Jobs in queues can fail—that's a reality! The good thing about jobs is that you can retry them because they have their own state.</p>
<p>In Flare, we've already been tracking failed jobs for quite some time; we can show you the job's initial state and where it went wrong.</p>
<p>We've added a new feature when you're using Laravel Horizon as a queue. Laravel Horizon provides a visual dashboard showing you all sorts of info about the queue, its failed jobs, and the ability to retry failed jobs. From now on, you can link your Flare project with the Laravel Horizon dashboard.</p>
<p>Every error triggered by a job in Laravel Horizon now links you to the failed job in the horizon dashboard. Allowing you to see even more info about the failed job and retry it:</p>
<p><img src="https://assets.flareapp.io/production/blog/393ff3e280b325f0a7319ed9f02031d2/0adf12d8-e1d9-47d3-bc5b-fdbd1a7d7764.png" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/393ff3e280b325f0a7319ed9f02031d2/0adf12d8-e1d9-47d3-bc5b-fdbd1a7d7764.png" width="2512" height="524" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/393ff3e280b325f0a7319ed9f02031d2/0adf12d8-e1d9-47d3-bc5b-fdbd1a7d7764.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/393ff3e280b325f0a7319ed9f02031d2/0adf12d8-e1d9-47d3-bc5b-fdbd1a7d7764.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/393ff3e280b325f0a7319ed9f02031d2/0adf12d8-e1d9-47d3-bc5b-fdbd1a7d7764.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/393ff3e280b325f0a7319ed9f02031d2/0adf12d8-e1d9-47d3-bc5b-fdbd1a7d7764.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>You can read more about configuring this feature in our <a href="/docs/integration/laravel-customizations/laravel-horizon">docs</a>.</p>
]]>
            </summary>
                                    <updated>2024-08-13T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Laravel's report helper: a must for error handling]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/laravels-report-helper-a-must-for-error-handling" />
            <id>https://flareapp.io/laravels-report-helper-a-must-for-error-handling</id>
            <author>
                <name><![CDATA[Alex]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>For any application, handling errors gracefully is crucial for maintaining a smooth user experience. While PHP's try-catch statements are great at catching known exceptions and preventing them from crashing your application, they also make exceptions go unnoticed. What if you also want to log and report an exception without bringing your application to a screeching halt? That's where Laravel's <code>report</code> helper steps into the spotlight.</p>
<h2>The Problem: Handling Known Failures</h2>
<p>Let's start with a common scenario: making an API call to an unreliable service. You know the request might fail, but you don't want it to bring down your entire application. Here's how you might handle something like this:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">getData</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">Response</span><span class="token" style="color: #ECEFF4;">|</span><span class="token" style="color: #81A1C1;">null</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">	</span><span class="token" style="color: #81A1C1;">try</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">	    </span><span class="token" style="color: #81A1C1;">return</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">Http</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">get</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">https://crowdstrike.com/unreliable-api</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">	</span><span class="token" style="color: #ECEFF4;">}</span><span class="token"> </span><span class="token" style="color: #81A1C1;">catch</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #8FBCBB;">RequestException</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">exception</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">	    </span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> We "gracefully" handle the error by returning null and not crashing</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">	    </span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> but how do we let ourselves know this happened?</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">	    </span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> Log the error? Send a notification to Slack? No, we'll use report!</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token">	
</span></span><span class="line"><span class="token">	    </span><span class="token" style="color: #81A1C1;">return</span><span class="token"> </span><span class="token" style="color: #81A1C1;">null</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">	</span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>In this example, we're catching the exception and returning <code>null</code> to prevent the application from crashing. We'll then handle the <code>null</code> value by informing the end user that something went wrong. But how do we let ourselves, the developers, know? How do you keep track of handled exceptions?</p>
<h2>Enter the <code>report</code> Helper</h2>
<p>Before we dive into the <code>report</code> helper, it's helpful to understand Laravel's default error handling behavior. By default, Laravel automatically logs unhandled errors and exceptions. If you're using an error tracking solution like Flare, these errors will also get sent to Flare. However, when you handle an exception in a try-catch block, Laravel doesn't automatically log or report it – that's where the <code>report</code> helper comes in.</p>
<p>The <code>report</code> helper is Laravel's solution for sending an error or exception you've handled to your logs and error tracking service, without halting the execution. It's perfect for situations where you want to catch and handle an error but still want to be notified about it.</p>
<p>Here's how it works:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #ECEFF4;">`</span><span class="token" style="color: #A3BE8C;">report(</span><span class="token" style="color: #A3BE8C;">$</span><span class="token" style="color: #81A1C1;">exception</span><span class="token" style="color: #A3BE8C;">);</span><span class="token" style="color: #ECEFF4;">`</span><span class="token">
</span></span></code></pre>
<p>The beauty of <code>report</code> is that it doesn't throw the exception or stop your code - it simply reports it and moves on.</p>
<p>In Flare, these reported exceptions appear like any other exception, distinguished by a "handled" label. This label indicates the exception was handled without crashing the application.</p>
<p><img src="https://assets.flareapp.io/production/blog/3f7bc7f54876201310d66ec45ad75c42/c0f5ec3f-5b37-4e3a-8e3e-035af92e8867.png" alt="A handled error report in Flare" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/3f7bc7f54876201310d66ec45ad75c42/c0f5ec3f-5b37-4e3a-8e3e-035af92e8867.png" width="1746" height="636" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/3f7bc7f54876201310d66ec45ad75c42/c0f5ec3f-5b37-4e3a-8e3e-035af92e8867.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/3f7bc7f54876201310d66ec45ad75c42/c0f5ec3f-5b37-4e3a-8e3e-035af92e8867.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/3f7bc7f54876201310d66ec45ad75c42/c0f5ec3f-5b37-4e3a-8e3e-035af92e8867.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/3f7bc7f54876201310d66ec45ad75c42/c0f5ec3f-5b37-4e3a-8e3e-035af92e8867.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<h2>Beyond Exceptions: Reporting Custom Messages</h2>
<p>The <code>report</code> helper isn't limited to exceptions. You can also use it to report custom error messages:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #88C0D0;">report</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">API call failed: unable to fetch user data</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span></code></pre>
<p>Behind the scenes, Laravel will automatically create and report an exception for this message. This handy shortcut allows you to quickly provide more context about the edge-case, making debugging easier down the line.</p>
<h2>Conditional Reporting</h2>
<p>Finally, it's worth mentioning that, in true Laravel fashion, two additional helpers exists for conditionally <code>report</code>ing an exception: <code>report_if</code> and <code>report_unless</code>.</p>
<h2>A Real-World Example</h2>
<p>Let's revisit our initial example and see how we can use these helpers to implement a strategy of graceful degradation:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">fetchUserData</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">userId</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">try</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">response</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">Http</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">get</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">example.com/api/users/</span><span class="token" style="color: #81A1C1;">{</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">userId</span><span class="token" style="color: #81A1C1;">}</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">        
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">if</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">response</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">successful</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">return</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">response</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">json</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #88C0D0;">report_unless</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">response</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">notFound</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">API returned unexpected status: </span><span class="token" style="color: #81A1C1;">{</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">response</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">status</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">}</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">        
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">return</span><span class="token"> </span><span class="token" style="color: #81A1C1;">null</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token"> </span><span class="token" style="color: #81A1C1;">catch</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #8FBCBB;">HttpException</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">exception</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #88C0D0;">report</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">exception</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">        
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">return</span><span class="token"> </span><span class="token" style="color: #81A1C1;">null</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>In this enhanced version:</p>
<ol>
<li>We're still catching and reporting any <code>RequestException</code> that might occur.</li>
<li>We're using <code>report_unless</code> to report unexpected status codes, but not 404s (which might be expected in some cases).</li>
<li>In both error cases, we're returning <code>null</code>, allowing the application to continue running without the user data.</li>
</ol>
<p>This approach allows your application to continue functioning even when parts of it fail, while ensuring you're always informed about these failures.</p>
<h2>Conclusion</h2>
<p>Laravel's <code>report</code> helper and its variants are invaluable for error handling. They allow us to keep track of handled errors and edge cases in our applications without disrupting the user experience. Errors are always inevitable but with Laravel's reporting helpers and Flare, you're well-equipped to manage them effectively.</p>
]]>
            </summary>
                                    <updated>2024-08-13T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Handled exceptions]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/handled-exceptions" />
            <id>https://flareapp.io/handled-exceptions</id>
            <author>
                <name><![CDATA[Ruben]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Exceptions will break your application, and your users will not be able to complete what they were doing in the first place. That's how I looked at exceptions a few years ago, and it even stopped me from using them for quite some time.</p>
<p>But this is not always the case. For example, in a Laravel application, the validator will throw an exception when your request validation fails. This exception does not break the application; it is cached by Laravel and converted into a response to the user indicating which fields contain invalid values.</p>
<p>Any exception can be caught like this:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">try</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">	</span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> Do something that can fail</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">catch</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #8FBCBB;">Throwable</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">t</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">	</span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> Handle what failed</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>This is great because your application keeps running, and the user won't notice something crashes. The thing is, you probably want to know that something crashed.</p>
<p>An example of such a scenario here at Flare is generating <a href="https://flareapp.io/blog/flare-and-ignition-now-offer-ai-powered-solutions">AI solutions</a>. We use an external service for this and send some information about the exception to this service. In return, we receive a solution we can show you.</p>
<p>Sadly, we do not always receive a response. Sometimes, the AI can't figure out an answer in the time window we specified, and thus, an exception is thrown. Flare will keep working as is since we catch those exceptions, but what if we want to keep track of these responses? Or, even worse, what if something breaks and we stop generating AI solutions? Since we catch all exceptions, we wouldn't know.</p>
<p>The solution is pretty simple: instead of ignoring the exception, let's report it to Flare as such (in a Laravel context):</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">try</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">	</span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> Contact our AI gods</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">catch</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #8FBCBB;">Throwable</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">t</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">	</span><span class="token" style="color: #88C0D0;">report</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">t</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>Everything will keep working as it was, but now the exception will be stored in Flare, and we can start monitoring it there!</p>
<p>Since the beginning of Flare, this has already been possible, but there hasn't been a visual difference between handled and unhandled exceptions until now!</p>
<p>When you install the latest version of ignition and report an exception, the exception will look like this in Flare:</p>
<p><img src="https://assets.flareapp.io/production/blog/463b747be8e42399029cd474574067f9/dbb305ab-cb2c-4853-bdab-e466225dd9b3.png" alt="handled exception" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/463b747be8e42399029cd474574067f9/dbb305ab-cb2c-4853-bdab-e466225dd9b3.png" width="2366" height="432" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/463b747be8e42399029cd474574067f9/dbb305ab-cb2c-4853-bdab-e466225dd9b3.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/463b747be8e42399029cd474574067f9/dbb305ab-cb2c-4853-bdab-e466225dd9b3.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/463b747be8e42399029cd474574067f9/dbb305ab-cb2c-4853-bdab-e466225dd9b3.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/463b747be8e42399029cd474574067f9/dbb305ab-cb2c-4853-bdab-e466225dd9b3.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<h2>How did we do it</h2>
<p>In Laravel, there's only one way to report handled exceptions: the <code>report</code> method. The problem is that Laravel doesn't distinguish between manually reported exceptions and exceptions cached by the Laravel exception handler (exceptions that crash your application).</p>
<p>How can we determine whether the exception was handled or not? Backtracing!</p>
<p>If an exception is reported to Flare, it was triggered somewhere and followed a path through the application to the code that sends it to Flare. If we can somehow find that the <code>report</code> method was used within that path, the exception was handled!</p>
<p>That path is a backtrace; do not confuse it with the exception stacktrace, which will show which path leads to the exception. The backtrace will tell us what happened after the exception was thrown.</p>
<p>Creating a backtrace can be done using our <a href="https://github.com/spatie/backtrace">spatie/backtrace</a> package:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">frames</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">Backtrace</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">create</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">limit</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #B48EAD;">40</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">frames</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span></code></pre>
<p>Now, we run over each frame and check the following:</p>
<ul>
<li>Is the frame method name equal to <code>report</code>?</li>
<li>Is the frame a class call and not a function call?</li>
<li>Are there any frames after this frame?</li>
</ul>
<p>In this case, it probably means we'll be in the <code>Illuminate\Foundation\Exceptions\Handler</code> class; we do not check if the frame class equals this since you can define your handler.</p>
<p>We now start checking the next frame, which is called the <code>Handler</code> class:</p>
<ul>
<li>Is the next frame a function call?</li>
<li>Is the next frame function call also named <code>report</code>?</li>
</ul>
<p>We'll mark the exception as handled if all these questions are confirmed for a frame within the backtrace.</p>
<h2>PHP integration</h2>
<p>We'll mark exceptions as handled automatically in Laravel. When using another framework or just plain PHP, you'll need to do this yourself as such:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">try</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">    </span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> Code that might throw an exception</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token"> </span><span class="token" style="color: #81A1C1;">catch</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #8FBCBB;">Exception</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">exception</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">flare</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">reportHandled</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">exception</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
]]>
            </summary>
                                    <updated>2024-07-05T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[WeakMaps a hidden gem in PHP]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/weakmaps-a-hidden-gem-in-php" />
            <id>https://flareapp.io/weakmaps-a-hidden-gem-in-php</id>
            <author>
                <name><![CDATA[Ruben]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>We're working on a prominent new feature for Flare in the coming months. I hope to share more information about it in the coming weeks, but for now, we're going to keep it a secret.</p>
<p>We're rewriting the internals of some of the Flare packages for this feature, which led to a problem. It goes like this: we have an item that we store in an array with similar items like this:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #616E88;">/**</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token" style="color: #616E88;"> * @template T
</span></span><span class="line"><span class="token" style="color: #616E88;"> </span><span class="token" style="color: #616E88;">*/</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">class</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">Store</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #616E88;">/**</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token" style="color: #616E88;">     * </span><span class="token" style="color: #81A1C1;">@param</span><span class="token" style="color: #616E88;"> </span><span class="token" style="color: #81A1C1;">array</span><span class="token" style="color: #616E88;">&lt;</span><span class="token" style="color: #8FBCBB;">T</span><span class="token" style="color: #616E88;">&gt;</span><span class="token" style="color: #616E88;"> $items
</span></span><span class="line"><span class="token" style="color: #616E88;">     </span><span class="token" style="color: #616E88;">*/</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">__construct</span><span class="token" style="color: #ECEFF4;">(</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">protected</span><span class="token"> </span><span class="token" style="color: #81A1C1;">array</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">items</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">]</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #616E88;">/**</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token" style="color: #616E88;">     * </span><span class="token" style="color: #81A1C1;">@param</span><span class="token" style="color: #616E88;"> </span><span class="token" style="color: #8FBCBB;">T</span><span class="token" style="color: #616E88;"> $item
</span></span><span class="line"><span class="token" style="color: #616E88;">     </span><span class="token" style="color: #616E88;">*/</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">addItem</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">mixed</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">item</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #81A1C1;">void</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">items</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">]</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">item</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>There's nothing too fancy right now. The <code>Store</code> should keep a configured number of items, so we drop the first item added when we add a new item and the limit has been reached. We are always more interested in the items added at the process's end.</p>
<p>Such a thing can be easily implemented as such:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #616E88;">/**</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token" style="color: #616E88;"> * @template T
</span></span><span class="line"><span class="token" style="color: #616E88;"> </span><span class="token" style="color: #616E88;">*/</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">class</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">Store</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #616E88;">/**</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token" style="color: #616E88;">     * </span><span class="token" style="color: #81A1C1;">@param</span><span class="token" style="color: #616E88;"> </span><span class="token" style="color: #81A1C1;">array</span><span class="token" style="color: #616E88;">&lt;</span><span class="token" style="color: #8FBCBB;">T</span><span class="token" style="color: #616E88;">&gt;</span><span class="token" style="color: #616E88;"> $items
</span></span><span class="line"><span class="token" style="color: #616E88;">     </span><span class="token" style="color: #616E88;">*/</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">__construct</span><span class="token" style="color: #ECEFF4;">(</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">protected</span><span class="token"> </span><span class="token" style="color: #81A1C1;">array</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">items</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">protected</span><span class="token"> </span><span class="token" style="color: #81A1C1;">int</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">maxEntries</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #B48EAD;">200</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #616E88;">/**</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token" style="color: #616E88;">     * </span><span class="token" style="color: #81A1C1;">@param</span><span class="token" style="color: #616E88;"> </span><span class="token" style="color: #8FBCBB;">T</span><span class="token" style="color: #616E88;"> $item
</span></span><span class="line"><span class="token" style="color: #616E88;">     </span><span class="token" style="color: #616E88;">*/</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">addItem</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">mixed</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">item</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #81A1C1;">void</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">items</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">]</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">item</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">if</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #88C0D0;">count</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">items</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #81A1C1;">&gt;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">maxEntries</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #88C0D0;">array_shift</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">items</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>Now starts the complexity. We define a completely new structure called a <code>Node</code>. A <code>Node</code> can have children, which are again <code>Nodes</code>. We're basically creating a tree structure.</p>
<p>Here's the thing: our <code>Node</code> also has an array of items, the identical items we used in our <code>Store</code> earlier:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #616E88;">/**</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token" style="color: #616E88;"> * @template T
</span></span><span class="line"><span class="token" style="color: #616E88;"> </span><span class="token" style="color: #616E88;">*/</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">class</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">Node</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #616E88;">/**</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token" style="color: #616E88;">     * </span><span class="token" style="color: #81A1C1;">@param</span><span class="token" style="color: #616E88;"> </span><span class="token" style="color: #81A1C1;">array</span><span class="token" style="color: #616E88;">&lt;</span><span class="token" style="color: #8FBCBB;">Node</span><span class="token" style="color: #616E88;">&gt;</span><span class="token" style="color: #616E88;"> $children
</span></span><span class="line"><span class="token" style="color: #616E88;">     * </span><span class="token" style="color: #81A1C1;">@param</span><span class="token" style="color: #616E88;"> </span><span class="token" style="color: #81A1C1;">array</span><span class="token" style="color: #616E88;">&lt;</span><span class="token" style="color: #8FBCBB;">T</span><span class="token" style="color: #616E88;">&gt;</span><span class="token" style="color: #616E88;"> $items
</span></span><span class="line"><span class="token" style="color: #616E88;">     </span><span class="token" style="color: #616E88;">*/</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">__construct</span><span class="token" style="color: #ECEFF4;">(</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">readonly</span><span class="token"> </span><span class="token" style="color: #81A1C1;">string</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">id</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">array</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">children</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">array</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">items</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>We update our requirements so that when adding an item to the store, we'll also add that item to a node. To find the node, we use a <code>MagicNodeFinder</code> (something I just made up to keep this article a bit clearer). The <code>MagicNodeFinder</code> will just return a node where the item should be stored, but it is dependent on the time it runs, so it will return different nodes based on the time it has been called.</p>
<p>Our <code>Store</code> now looks like this:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #616E88;">/**</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token" style="color: #616E88;"> * @template T
</span></span><span class="line"><span class="token" style="color: #616E88;"> </span><span class="token" style="color: #616E88;">*/</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">class</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">Store</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #616E88;">/**</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token" style="color: #616E88;">     * </span><span class="token" style="color: #81A1C1;">@param</span><span class="token" style="color: #616E88;"> </span><span class="token" style="color: #81A1C1;">array</span><span class="token" style="color: #616E88;">&lt;</span><span class="token" style="color: #8FBCBB;">T</span><span class="token" style="color: #616E88;">&gt;</span><span class="token" style="color: #616E88;"> $items
</span></span><span class="line"><span class="token" style="color: #616E88;">     </span><span class="token" style="color: #616E88;">*/</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">__construct</span><span class="token" style="color: #ECEFF4;">(</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">protected</span><span class="token"> </span><span class="token" style="color: #81A1C1;">array</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">items</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">protected</span><span class="token"> </span><span class="token" style="color: #81A1C1;">int</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">maxEntries</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #B48EAD;">200</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #616E88;">/**</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token" style="color: #616E88;">     * </span><span class="token" style="color: #81A1C1;">@param</span><span class="token" style="color: #616E88;"> </span><span class="token" style="color: #8FBCBB;">T</span><span class="token" style="color: #616E88;"> $item
</span></span><span class="line"><span class="token" style="color: #616E88;">     </span><span class="token" style="color: #616E88;">*/</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">addItem</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">mixed</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">item</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #81A1C1;">void</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">node</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">new</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">MagicNodeFinder</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">execute</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">items</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">]</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">item</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">node</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">items</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">]</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">item</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">if</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #88C0D0;">count</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">items</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #81A1C1;">&gt;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">maxEntries</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #88C0D0;">array_shift</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">items</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>We're almost there. We need to update the item removal code so that items can be removed from the node when the item limit is reached.</p>
<p>Although this might look easy, it isn't since <code>MagicNodeFinder</code> cannot be trusted to return the correct node where the item is stored.</p>
<p>There's no way to know in which node the item is stored, and even if we knew the node, we'd have to traverse the tree every time we need to remove an item, resulting in wasted computing time.</p>
<h2>Introducing the WeakMap</h2>
<p>In PHP 8.0, WeakMaps were added to the language. Weakmaps are maps that hold objects as keys but without raising the reference count for that object. Thus, whenever an object stored in the WeakMap is garbage collected(it doesn't exist anymore because it went out of scope or was unset), it is also immediately removed from the map.</p>
<p>That all sounds difficult; let's take a look at an example. We refactor our <code>Node</code> class like this:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #616E88;">/**</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token" style="color: #616E88;"> * @template T
</span></span><span class="line"><span class="token" style="color: #616E88;"> </span><span class="token" style="color: #616E88;">*/</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">class</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">Node</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #616E88;">/**</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token" style="color: #616E88;">     * </span><span class="token" style="color: #81A1C1;">@param</span><span class="token" style="color: #616E88;"> </span><span class="token" style="color: #81A1C1;">string</span><span class="token" style="color: #616E88;"> $id
</span></span><span class="line"><span class="token" style="color: #616E88;">     * </span><span class="token" style="color: #81A1C1;">@param</span><span class="token" style="color: #616E88;"> </span><span class="token" style="color: #81A1C1;">array</span><span class="token" style="color: #616E88;">&lt;</span><span class="token" style="color: #8FBCBB;">Node</span><span class="token" style="color: #616E88;">&gt;</span><span class="token" style="color: #616E88;"> $children
</span></span><span class="line"><span class="token" style="color: #616E88;">     * </span><span class="token" style="color: #81A1C1;">@param</span><span class="token" style="color: #616E88;"> </span><span class="token" style="color: #8FBCBB;">WeakMap</span><span class="token" style="color: #616E88;">&lt;</span><span class="token" style="color: #8FBCBB;">T</span><span class="token" style="color: #616E88;">,</span><span class="token" style="color: #616E88;"> null&gt; $items
</span></span><span class="line"><span class="token" style="color: #616E88;">     </span><span class="token" style="color: #616E88;">*/</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">__construct</span><span class="token" style="color: #ECEFF4;">(</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">readonly</span><span class="token"> </span><span class="token" style="color: #81A1C1;">string</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">id</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">array</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">children</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">WeakMap</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">items</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">new</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">WeakMap</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>Let's add an item to our node:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">$item = new Item('Some info here'); 
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">$node = new Node(42);
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">$node-&gt;items[$item] = null;
</span></span></code></pre>
<p>We count the number of items in the WeakMap:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">node</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">items</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">count</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token"> </span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> 1</span><span class="token" style="color: #616E88;">
</span></span></code></pre>
<p>If we garbage collect our item by unsetting it:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">unset</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">item</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span></code></pre>
<p>What happens to the WeakMap's count?</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">node</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">items</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">count</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token"> </span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> 0</span><span class="token" style="color: #616E88;">
</span></span></code></pre>
<p>Cool! The item was automatically removed from the map by garbage collecting the item.</p>
<h2>Back to our <code>Store</code></h2>
<p>Let's see how we can use this <code>Weakmap</code> within our <code>Store</code>. The <code>addItem</code> method of our <code>Store</code> looks like this when using a <code>WeakMap</code> in <code>Node</code>:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #616E88;">/**</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token" style="color: #616E88;"> * </span><span class="token" style="color: #81A1C1;">@param</span><span class="token" style="color: #616E88;"> </span><span class="token" style="color: #8FBCBB;">T</span><span class="token" style="color: #616E88;"> $item
</span></span><span class="line"><span class="token" style="color: #616E88;"> </span><span class="token" style="color: #616E88;">*/</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">addItem</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">mixed</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">item</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #81A1C1;">void</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">node</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">new</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">MagicNodeFinder</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">execute</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">items</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">]</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">item</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">node</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">items</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">item</span><span class="token" style="color: #ECEFF4;">]</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">null</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">if</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #88C0D0;">count</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">items</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #81A1C1;">&gt;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">maxEntries</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #88C0D0;">array_shift</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">items</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>By shifting the items array, the object will, due to scoping rules, be unset when the end of the method is reached. Thus, it will also be removed from the WeakMap, which is excellent! We're done; no extra changes are required here!</p>
<h2>Conclusions</h2>
<p>WeakMap was a feature added to PHP that I thought I would never use. Boy, was I wrong! It made our code a lot more performant and easier to read.</p>
<p>We have great things in the pipeline and would love to show them in the next months. See you soon!</p>
]]>
            </summary>
                                    <updated>2024-06-26T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Prioritizing exceptions]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/prioritizing-exceptions" />
            <id>https://flareapp.io/prioritizing-exceptions</id>
            <author>
                <name><![CDATA[Ruben]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>If I'm not mistaken, it was Shakespeare who quoted:</p>
<blockquote>
<p>"Verily, in the realm of errors, not all missteps bear equal weight, nor warrant the same swift hand. Some, mere trifles, may wait, while others, of grave import, demand urgent priority."</p>
</blockquote>
<p>That's not a real quote 😅 but the quote has a point: sometimes, you just want to have an overview of the errors in your application that need to be fixed now!</p>
<p>That's where prioritization comes into place, a new feature we've added to Flare. For each error, you can now decide which priority it needs:</p>
<p><img src="https://assets.flareapp.io/production/blog/a551aa1de42127c08349de967f70710f/b282f808-f7a1-4ff8-843c-05942bd72add.png" alt="select a priority" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/a551aa1de42127c08349de967f70710f/b282f808-f7a1-4ff8-843c-05942bd72add.png" width="710" height="736" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/a551aa1de42127c08349de967f70710f/b282f808-f7a1-4ff8-843c-05942bd72add.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=710/https://assets.flareapp.io/production/blog/a551aa1de42127c08349de967f70710f/b282f808-f7a1-4ff8-843c-05942bd72add.png 710w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>We've defined four levels of priority to select from:</p>
<ul>
<li>Info: not really an error, just some extra info from within your system</li>
<li>Low: something to pick up later on when you've got time for it</li>
<li>Medium: a bug that needs to be taken care off</li>
<li>High: drop everything you're working on; this issue should be resolved immediately!</li>
</ul>
<p>When a priority is changed, we'll keep track of it in the activity overview:</p>
<p><img src="https://assets.flareapp.io/production/blog/db26705f742b87ad017eb0cfbdb96323/4a36d987-43e3-48f0-b6eb-eb122650c240.png" alt="priority in the activity overview" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/db26705f742b87ad017eb0cfbdb96323/4a36d987-43e3-48f0-b6eb-eb122650c240.png" width="974" height="1188" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/db26705f742b87ad017eb0cfbdb96323/4a36d987-43e3-48f0-b6eb-eb122650c240.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/db26705f742b87ad017eb0cfbdb96323/4a36d987-43e3-48f0-b6eb-eb122650c240.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=974/https://assets.flareapp.io/production/blog/db26705f742b87ad017eb0cfbdb96323/4a36d987-43e3-48f0-b6eb-eb122650c240.png 974w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>Lastly, it is possible to filter errors in a project based on their priority:</p>
<p><img src="https://assets.flareapp.io/production/blog/f121ec37dc51ed1a162839eb0dc225f2/f49b57db-0056-4a80-bcd5-9c7a16fac56b.png" alt="filter based upon priority" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/f121ec37dc51ed1a162839eb0dc225f2/f49b57db-0056-4a80-bcd5-9c7a16fac56b.png" width="1110" height="486" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/f121ec37dc51ed1a162839eb0dc225f2/f49b57db-0056-4a80-bcd5-9c7a16fac56b.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/f121ec37dc51ed1a162839eb0dc225f2/f49b57db-0056-4a80-bcd5-9c7a16fac56b.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/f121ec37dc51ed1a162839eb0dc225f2/f49b57db-0056-4a80-bcd5-9c7a16fac56b.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1110/https://assets.flareapp.io/production/blog/f121ec37dc51ed1a162839eb0dc225f2/f49b57db-0056-4a80-bcd5-9c7a16fac56b.png 1110w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>Happy debugging!</p>
]]>
            </summary>
                                    <updated>2024-06-21T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Running Ignition on five years of Laravel]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/running-ignition-on-five-years-of-laravel" />
            <id>https://flareapp.io/running-ignition-on-five-years-of-laravel</id>
            <author>
                <name><![CDATA[Ruben]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Here at Flare, we're trying to make the best Laravel error tracker possible, so we support as many Laravel versions as possible.</p>
<p>Flare and thus Ignition run on Laravel versions 6 (released in 2019) up to Laravel 11 (when writing the most recent version). Ignition is the default error page in Laravel 6, and we've completely rewritten it with Laravel 9 and added a new design.</p>
<p>The problem? When making changes to Flare or packages like <a href="https://github.com/spatie/backtrace">spatie/backtrace</a>, which powers the stack traces used by Ignition, how can we check that everything works when something changes?</p>
<p>Of course, we have continuous integration testing for this; the Ignition packages are tested with each commit and PR to ensure nothing breaks. Eventually, these packages will end up in a Laravel application for our Flare customers and everyone using Laravel. How can we make sure that they work with our install instructions? What if we add some new visual features to Ignition? How do we check for each Laravel version that the feature works? In these cases, we can wait for CI, but quickly checking something is just so handy!</p>
<h2>A nasty small bug</h2>
<p>We created a unique structure to set up Ignition and Flare in Laravel versions 6 to 11. The reason we did this was the following bug:</p>
<p><img src="https://assets.flareapp.io/production/blog/66c8eb04ebbd67a6e1d0493fd040da36/c99526f1-0640-41f4-ab38-0653cfe4eecf.png" alt="A bug" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/66c8eb04ebbd67a6e1d0493fd040da36/c99526f1-0640-41f4-ab38-0653cfe4eecf.png" width="2848" height="2318" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/66c8eb04ebbd67a6e1d0493fd040da36/c99526f1-0640-41f4-ab38-0653cfe4eecf.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/66c8eb04ebbd67a6e1d0493fd040da36/c99526f1-0640-41f4-ab38-0653cfe4eecf.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/66c8eb04ebbd67a6e1d0493fd040da36/c99526f1-0640-41f4-ab38-0653cfe4eecf.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/66c8eb04ebbd67a6e1d0493fd040da36/c99526f1-0640-41f4-ab38-0653cfe4eecf.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>Notice that the serializable closure is defined twice: once where it belongs in the code view and once as the filename, causing problems in Ignition. In this case, it is a small closure not causing too much trouble. But with bigger closures, Ignition becomes unusable, and Flare doesn't know what to do with the long filename.</p>
<p>This error can only be triggered using a serializable closure, a solution from Laravel that allows closures in jobs to be serialized. Simply said, the serializable closure package will keep a copy of the closure's code as a string and its state. Then, when unserialized, the code will be executed by including a stream of the string of the code (PHP magic at its best!).</p>
<p>Since we're not executing a file with code but a stream, there is no filename, and thus, the stream of code is used as a filename. Our code is floating in thin air between files. This is weird stuff, indeed.</p>
<p>The fix is to set the filename to <code>laravel-serializable-closure://function()</code>, and we also clean up the code example:</p>
<p><img src="https://assets.flareapp.io/production/blog/fb06525373332349fc1f16b31cf34663/efdeb54a-1440-41d5-8151-23711c5fc549.png" alt="Fix of the bug" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/fb06525373332349fc1f16b31cf34663/efdeb54a-1440-41d5-8151-23711c5fc549.png" width="2814" height="2316" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/fb06525373332349fc1f16b31cf34663/efdeb54a-1440-41d5-8151-23711c5fc549.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/fb06525373332349fc1f16b31cf34663/efdeb54a-1440-41d5-8151-23711c5fc549.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/fb06525373332349fc1f16b31cf34663/efdeb54a-1440-41d5-8151-23711c5fc549.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/fb06525373332349fc1f16b31cf34663/efdeb54a-1440-41d5-8151-23711c5fc549.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>But how do we check if this works for all Laravel versions since this is a more visual bug?</p>
<h2>Building a test app(s) repository</h2>
<p>We've got an internal repository called <code>flare-test-apps</code> containing a Vue, React, PHP, and, of course, a Laravel app. We removed that last one and replaced it with 6 new Laravel apps, which require versions 6 through 11.</p>
<p><img src="https://assets.flareapp.io/production/blog/79884a8a47ac868bec3fbcd714797568/feb7779e-066c-450f-840b-7d5a34d820a7.png" alt="A lot of laravel dirs" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/79884a8a47ac868bec3fbcd714797568/feb7779e-066c-450f-840b-7d5a34d820a7.png" width="690" height="320" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/79884a8a47ac868bec3fbcd714797568/feb7779e-066c-450f-840b-7d5a34d820a7.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=690/https://assets.flareapp.io/production/blog/79884a8a47ac868bec3fbcd714797568/feb7779e-066c-450f-840b-7d5a34d820a7.png 690w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>The next thing to do was to get them working simultaneously. Laravel 6 requires a minimum PHP version of 8.0, while Laravel 11 requires at least PHP 8.2. Luckily, we've got an excellent tool for this: Laravel Valet!</p>
<p>Laravel Valet is not only a server that can serve your local Laravel application but also can run CLI commands. The cool thing about Laravel Valet is that it can isolate sites, meaning that we can set a specific PHP version that will be used to serve the site and also run CLI commands.</p>
<p>To set this up, we loop using a bash script over all the <code>laravel-*</code> directories (where * is the Laravel version):</p>
<pre class="phiki language-shellscript nord" data-language="shellscript" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #616E88;">#!</span><span class="token" style="color: #616E88;">/bin/bash
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">for</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">dir</span><span class="token"> </span><span class="token" style="color: #81A1C1;">in</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">l</span><span class="token" style="color: #A3BE8C;">a</span><span class="token" style="color: #A3BE8C;">r</span><span class="token" style="color: #A3BE8C;">a</span><span class="token" style="color: #A3BE8C;">v</span><span class="token" style="color: #A3BE8C;">e</span><span class="token" style="color: #A3BE8C;">l</span><span class="token" style="color: #A3BE8C;">-</span><span class="token" style="color: #A3BE8C;">*</span><span class="token" style="color: #81A1C1;">;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">do</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">if</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">[[</span><span class="token"> </span><span class="token" style="color: #81A1C1;">-d</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #D8DEE9;">$</span><span class="token" style="color: #D8DEE9;">dir</span><span class="token" style="color: #ECEFF4;">"</span><span class="token"> </span><span class="token" style="color: #81A1C1;">&amp;&amp;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">{</span><span class="token" style="color: #D8DEE9;">dir</span><span class="token" style="color: #81A1C1;">#</span><span class="token" style="color: #D8DEE9;">laravel-</span><span class="token" style="color: #81A1C1;">}</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=~</span><span class="token"> ^</span><span class="token" style="color: #ECEFF4;">[</span><span class="token">0-9</span><span class="token" style="color: #ECEFF4;">]</span><span class="token">+$ </span><span class="token" style="color: #ECEFF4;">]]</span><span class="token" style="color: #81A1C1;">;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">then</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #88C0D0;">cd</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #D8DEE9;">$</span><span class="token" style="color: #D8DEE9;">dir</span><span class="token" style="color: #ECEFF4;">"</span><span class="token">
</span></span><span class="line"><span class="token">        
</span></span><span class="line"><span class="token">	      </span><span class="token" style="color: #D8DEE9;">php_requirement</span><span class="token" style="color: #81A1C1;">=</span><span class="token" style="color: #ECEFF4;">$(</span><span class="token" style="color: #88C0D0;">grep</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">-</span><span class="token" style="color: #A3BE8C;">o</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">"php": "[^"]*</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">composer.json</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #D8DEE9;">php_versions</span><span class="token" style="color: #81A1C1;">=</span><span class="token" style="color: #ECEFF4;">$(</span><span class="token" style="color: #88C0D0;">echo</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">$</span><span class="token" style="color: #D8DEE9;">php_requirement</span><span class="token"> </span><span class="token" style="color: #81A1C1;">|</span><span class="token"> </span><span class="token" style="color: #88C0D0;">tr</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">-</span><span class="token" style="color: #A3BE8C;">dc</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">0-9.|\n</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span><span class="line"><span class="token">        
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">if</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">[[</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">$</span><span class="token" style="color: #D8DEE9;">php_versions</span><span class="token"> </span><span class="token" style="color: #81A1C1;">==</span><span class="token"> </span><span class="token" style="color: #81A1C1;">*</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">|</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #81A1C1;">*</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">]]</span><span class="token" style="color: #81A1C1;">;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">then</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #D8DEE9;">IFS</span><span class="token" style="color: #81A1C1;">=</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">|</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #88C0D0;">read</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">-</span><span class="token" style="color: #A3BE8C;">ra</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">versions</span><span class="token"> </span><span class="token" style="color: #81A1C1;">&lt;&lt;&lt;</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #D8DEE9;">$</span><span class="token" style="color: #D8DEE9;">php_versions</span><span class="token" style="color: #ECEFF4;">"</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #D8DEE9;">php_version</span><span class="token" style="color: #81A1C1;">=</span><span class="token" style="color: #ECEFF4;">$(</span><span class="token" style="color: #88C0D0;">printf</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">%s\n</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">{</span><span class="token" style="color: #D8DEE9;">versions</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #81A1C1;">@</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #81A1C1;">}</span><span class="token" style="color: #ECEFF4;">"</span><span class="token"> </span><span class="token" style="color: #81A1C1;">|</span><span class="token"> </span><span class="token" style="color: #88C0D0;">sort</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">-</span><span class="token" style="color: #A3BE8C;">V</span><span class="token"> </span><span class="token" style="color: #81A1C1;">|</span><span class="token"> </span><span class="token" style="color: #88C0D0;">tail</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">-</span><span class="token" style="color: #A3BE8C;">n</span><span class="token"> </span><span class="token" style="color: #B48EAD;">1</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">else</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #D8DEE9;">php_version</span><span class="token" style="color: #81A1C1;">=</span><span class="token" style="color: #D8DEE9;">$</span><span class="token" style="color: #D8DEE9;">php_versions</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">fi</span><span class="token">
</span></span><span class="line"><span class="token">        
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #88C0D0;">echo</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">Linking </span><span class="token" style="color: #D8DEE9;">$</span><span class="token" style="color: #D8DEE9;">dir</span><span class="token" style="color: #A3BE8C;"> with PHP version </span><span class="token" style="color: #D8DEE9;">$</span><span class="token" style="color: #D8DEE9;">php_version</span><span class="token" style="color: #ECEFF4;">"</span><span class="token">
</span></span><span class="line"><span class="token">        
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #88C0D0;">valet</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">link</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">$</span><span class="token" style="color: #D8DEE9;">dir</span><span class="token" style="color: #A3BE8C;">.flare-test-apps</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #D8DEE9;">isolated_sites</span><span class="token" style="color: #81A1C1;">=</span><span class="token" style="color: #ECEFF4;">$(</span><span class="token" style="color: #88C0D0;">valet</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">isolated</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span><span class="line"><span class="token">        
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">if</span><span class="token"> </span><span class="token" style="color: #81A1C1;">!</span><span class="token"> </span><span class="token" style="color: #88C0D0;">echo</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #D8DEE9;">$</span><span class="token" style="color: #D8DEE9;">isolated_sites</span><span class="token" style="color: #ECEFF4;">"</span><span class="token"> </span><span class="token" style="color: #81A1C1;">|</span><span class="token"> </span><span class="token" style="color: #88C0D0;">grep</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">-</span><span class="token" style="color: #A3BE8C;">q</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #D8DEE9;">$</span><span class="token" style="color: #D8DEE9;">dir</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #81A1C1;">;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">then</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #88C0D0;">echo</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">Isolating site </span><span class="token" style="color: #D8DEE9;">$</span><span class="token" style="color: #D8DEE9;">dir</span><span class="token" style="color: #ECEFF4;">"</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #88C0D0;">valet</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">isolate</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">php@</span><span class="token" style="color: #D8DEE9;">$</span><span class="token" style="color: #D8DEE9;">php_version</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">-</span><span class="token" style="color: #A3BE8C;">-site=</span><span class="token" style="color: #D8DEE9;">$</span><span class="token" style="color: #D8DEE9;">dir</span><span class="token" style="color: #A3BE8C;">.flare-test-apps</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">fi</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #88C0D0;">valet</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">composer</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">update</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">fi</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #88C0D0;">cd</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">..</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">done</span><span class="token">
</span></span></code></pre>
<p>I must admit, chatGPT was quite helpful in writing this BASH code. Let's go quickly over it:</p>
<ul>
<li>We loop over each laravel-* directory</li>
<li>We go into the directory</li>
<li>Check which php version was required by Composer
<ul>
<li>This could be <code>^7.4|^8.0</code>. We'll always take the highest version to reduce the amount of PHP versions required on our computers</li>
</ul>
</li>
<li>Then we'll tell Laravel valet that the subdomain laravel-*.flare-test-apps.test should serve the current directory by using the <code>valet link command</code></li>
<li>We'll check if the site is isolated at the moment by checking the <code>valet isolated</code> command if the current directory is isolated
<ul>
<li>If not, we'll isolate it using the PHP version found earlier and the <code>valet isolate</code> command</li>
</ul>
</li>
<li>We run <code>valet composer update</code>, which will run composer update in the directory</li>
<li>And we return back to the root dir so that we can visit other laravel-* directories</li>
</ul>
<p>From now on, we'll have a working version of all the required Laravel versions. You can check this by going to http://laravel-6.flare-test-apps.test or http://laravel-10.flare-test-apps.test.</p>
<h2>Adding common routes</h2>
<p>To check our serializable closure bug, we'll need a route and controller to trigger it. The code is simple, but copying and pasting it into every Laravel version directory would be stupid.</p>
<p>Since we at Spatie (the creators of Flare) are masters at creating packages, let us create yet another one! Albeit one that will never be published on Packagist, we call it laravel-shared. It will contain the route required for testing.</p>
<p>The package will live in the laravel-shared directory, and its composer file looks like this:</p>
<pre class="phiki language-json nord" data-language="json" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #8FBCBB;">name</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">spatie/laravel-shared</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #8FBCBB;">require</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #8FBCBB;">php</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">^7.4|^8.0</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #8FBCBB;">illuminate/contracts</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">^6.0|^7.0|^8.0|^9.0|^10.0|^11.0</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #8FBCBB;">laravel/serializable-closure</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">^1.3</span><span class="token" style="color: #ECEFF4;">"</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #8FBCBB;">autoload</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #8FBCBB;">psr-4</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #8FBCBB;">Spatie</span><span class="token" style="color: #EBCB8B;">\\</span><span class="token" style="color: #8FBCBB;">LaravelShared</span><span class="token" style="color: #EBCB8B;">\\</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">src</span><span class="token" style="color: #ECEFF4;">"</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #8FBCBB;">extra</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #8FBCBB;">laravel</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #8FBCBB;">providers</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">[</span><span class="token">
</span></span><span class="line"><span class="token">                </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">Spatie</span><span class="token" style="color: #EBCB8B;">\\</span><span class="token" style="color: #A3BE8C;">LaravelShared</span><span class="token" style="color: #EBCB8B;">\\</span><span class="token" style="color: #A3BE8C;">LaravelSharedServiceProvider</span><span class="token" style="color: #ECEFF4;">"</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #ECEFF4;">]</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #8FBCBB;">minimum-stability</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">stable</span><span class="token" style="color: #ECEFF4;">"</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>Nothing too fancy, we register a <code>LaravelSharedServiceProvider</code> looking like this:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">&lt;</span><span class="token" style="color: #81A1C1;">?</span><span class="token">php</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">namespace</span><span class="token"> </span><span class="token">Spatie</span><span class="token" style="color: #ECEFF4;">\</span><span class="token">LaravelShared</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">use</span><span class="token"> </span><span class="token">Illuminate</span><span class="token" style="color: #ECEFF4;">\</span><span class="token">Support</span><span class="token" style="color: #ECEFF4;">\</span><span class="token">Facades</span><span class="token" style="color: #ECEFF4;">\</span><span class="token" style="color: #8FBCBB;">Route</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">use</span><span class="token"> </span><span class="token">Illuminate</span><span class="token" style="color: #ECEFF4;">\</span><span class="token">Support</span><span class="token" style="color: #ECEFF4;">\</span><span class="token" style="color: #8FBCBB;">ServiceProvider</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">use</span><span class="token"> </span><span class="token">Spatie</span><span class="token" style="color: #ECEFF4;">\</span><span class="token">LaravelShared</span><span class="token" style="color: #ECEFF4;">\</span><span class="token">Controllers</span><span class="token" style="color: #ECEFF4;">\</span><span class="token" style="color: #8FBCBB;">FlareController</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">class</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">LaravelSharedServiceProvider</span><span class="token"> </span><span class="token" style="color: #81A1C1;">extends</span><span class="token"> </span><span class="token" style="color: #8FBCBB;font-weight: bold;">ServiceProvider</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">boot</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #8FBCBB;">Route</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">get</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">serializeable-closure</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #8FBCBB;">FlareController</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #81A1C1;">class</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">serializeableClosure</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>The service provider will register a route (like you would do in <code>routes/web.php</code>) to a controller where the bug will be triggered.</p>
<p>Within every Laravel application, we add a dependency for the <code>laravel-shared</code> package as such:</p>
<pre class="phiki language-json nord" data-language="json" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">require</span><span class="token" style="color: #ECEFF4;">"</span><span class="token"> : </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #8FBCBB;">php</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">^8.2</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #8FBCBB;">laravel/framework</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">^11.0</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #8FBCBB;">laravel/tinker</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">^2.9</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #8FBCBB;">spatie/laravel-shared</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">@dev</span><span class="token" style="color: #ECEFF4;">"</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">,
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">repositories</span><span class="token" style="color: #ECEFF4;">"</span><span class="token"> : </span><span class="token" style="color: #ECEFF4;">[</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #8FBCBB;">type</span><span class="token" style="color: #ECEFF4;">"</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">path</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #8FBCBB;">URL</span><span class="token" style="color: #ECEFF4;">"</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">../laravel-shared</span><span class="token" style="color: #ECEFF4;">"</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">]</span><span class="token">,
</span></span></code></pre>
<p>And that's it! But how can we now check this new route by typing in 6 URLs? We're developers, let's automate this by reusing the BASH script we wrote before:</p>
<pre class="phiki language-shellscript nord" data-language="shellscript" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #616E88;">#!</span><span class="token" style="color: #616E88;">/bin/bash
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">if</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">[</span><span class="token"> </span><span class="token" style="color: #81A1C1;">-z</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #D8DEE9;">$</span><span class="token" style="color: #D8DEE9;">1</span><span class="token" style="color: #ECEFF4;">"</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #81A1C1;">;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">then</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #88C0D0;">echo</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">No command provided, the following commands can be used:</span><span class="token" style="color: #ECEFF4;">"</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #88C0D0;">echo</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">  setup: Setup all Laravel applications</span><span class="token" style="color: #ECEFF4;">"</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #88C0D0;">echo</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">  open: Open a URL on all Laravel applications</span><span class="token" style="color: #ECEFF4;">"</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #88C0D0;">exit</span><span class="token"> </span><span class="token" style="color: #B48EAD;">1</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">fi</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">setup_laravel</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #616E88;">#</span><span class="token" style="color: #616E88;"> Our previous code
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">for</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">dir</span><span class="token"> </span><span class="token" style="color: #81A1C1;">in</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">l</span><span class="token" style="color: #A3BE8C;">a</span><span class="token" style="color: #A3BE8C;">r</span><span class="token" style="color: #A3BE8C;">a</span><span class="token" style="color: #A3BE8C;">v</span><span class="token" style="color: #A3BE8C;">e</span><span class="token" style="color: #A3BE8C;">l</span><span class="token" style="color: #A3BE8C;">-</span><span class="token" style="color: #A3BE8C;">*</span><span class="token" style="color: #81A1C1;">;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">do</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">if</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">[[</span><span class="token"> </span><span class="token" style="color: #81A1C1;">-d</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #D8DEE9;">$</span><span class="token" style="color: #D8DEE9;">dir</span><span class="token" style="color: #ECEFF4;">"</span><span class="token"> </span><span class="token" style="color: #81A1C1;">&amp;&amp;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">{</span><span class="token" style="color: #D8DEE9;">dir</span><span class="token" style="color: #81A1C1;">#</span><span class="token" style="color: #D8DEE9;">laravel-</span><span class="token" style="color: #81A1C1;">}</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=~</span><span class="token"> ^</span><span class="token" style="color: #ECEFF4;">[</span><span class="token">0-9</span><span class="token" style="color: #ECEFF4;">]</span><span class="token">+$ </span><span class="token" style="color: #ECEFF4;">]]</span><span class="token" style="color: #81A1C1;">;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">then</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #88C0D0;">cd</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #D8DEE9;">$</span><span class="token" style="color: #D8DEE9;">dir</span><span class="token" style="color: #ECEFF4;">"</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #88C0D0;">echo</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">Working in </span><span class="token" style="color: #D8DEE9;">$</span><span class="token" style="color: #D8DEE9;">dir</span><span class="token" style="color: #ECEFF4;">"</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">if</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">[</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #D8DEE9;">$</span><span class="token" style="color: #D8DEE9;">1</span><span class="token" style="color: #ECEFF4;">"</span><span class="token"> </span><span class="token" style="color: #81A1C1;">==</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">setup</span><span class="token" style="color: #ECEFF4;">"</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #81A1C1;">;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">then</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #88C0D0;">setup_laravel</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">fi</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">if</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">[</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #D8DEE9;">$</span><span class="token" style="color: #D8DEE9;">1</span><span class="token" style="color: #ECEFF4;">"</span><span class="token"> </span><span class="token" style="color: #81A1C1;">==</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">open</span><span class="token" style="color: #ECEFF4;">"</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #81A1C1;">;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">then</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #88C0D0;">open</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">-</span><span class="token" style="color: #A3BE8C;">a</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">Safari</span><span class="token" style="color: #ECEFF4;">"</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">http://</span><span class="token" style="color: #D8DEE9;">$</span><span class="token" style="color: #D8DEE9;">dir</span><span class="token" style="color: #A3BE8C;">.flare-test-apps.test/</span><span class="token" style="color: #D8DEE9;">$</span><span class="token" style="color: #D8DEE9;">2</span><span class="token" style="color: #ECEFF4;">"</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">fi</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #88C0D0;">cd</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">..</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">fi</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">done</span><span class="token">
</span></span></code></pre>
<p>Now, when running our script like this:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">./laravel.sh open serializable-closure
</span></span></code></pre>
<p>Six browser tabs will open, each with a different Laravel version calling the route.</p>
<h2>Adding Flare support</h2>
<p>Wouldn't it be cool to test whether the Flare client works on each Laravel version by triggering <code>php artisan flare:test</code>?</p>
<p>To enable Flare in a Laravel application basically, three steps are required:</p>
<ul>
<li>Add a Flare key to the <code>flare.key</code> config entry (in regular applications, we'll check the <code>FLARE_KEY</code> environment variable for this)</li>
<li>Add a Flare logging channel to the <code>logging.channels</code> config entry</li>
<li>Update the <code>logging.channels.stack.channels</code> config entry also to include the logging channel we've added</li>
</ul>
<p>We can update the <code>LaravelSharedServiceProvider</code> in our <code>laravel-shared</code> package as such:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">&lt;</span><span class="token" style="color: #81A1C1;">?</span><span class="token">php</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">namespace</span><span class="token"> </span><span class="token">Spatie</span><span class="token" style="color: #ECEFF4;">\</span><span class="token">LaravelShared</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">use</span><span class="token"> </span><span class="token">Illuminate</span><span class="token" style="color: #ECEFF4;">\</span><span class="token">Support</span><span class="token" style="color: #ECEFF4;">\</span><span class="token">Facades</span><span class="token" style="color: #ECEFF4;">\</span><span class="token" style="color: #8FBCBB;">Route</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">use</span><span class="token"> </span><span class="token">Illuminate</span><span class="token" style="color: #ECEFF4;">\</span><span class="token">Support</span><span class="token" style="color: #ECEFF4;">\</span><span class="token" style="color: #8FBCBB;">ServiceProvider</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">use</span><span class="token"> </span><span class="token">Spatie</span><span class="token" style="color: #ECEFF4;">\</span><span class="token">LaravelShared</span><span class="token" style="color: #ECEFF4;">\</span><span class="token">Controllers</span><span class="token" style="color: #ECEFF4;">\</span><span class="token" style="color: #8FBCBB;">FlareController</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">class</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">LaravelSharedServiceProvider</span><span class="token"> </span><span class="token" style="color: #81A1C1;">extends</span><span class="token"> </span><span class="token" style="color: #8FBCBB;font-weight: bold;">ServiceProvider</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">boot</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #8FBCBB;">Route</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">get</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">serializeable-closure</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #8FBCBB;">FlareController</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #81A1C1;">class</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">serializeableClosure</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">register</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">flareKey</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">some-flare-api-key-here</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">if</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">flareKey</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #88C0D0;">config</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">set</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">flare.key</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">flareKey</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #88C0D0;">config</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">set</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">logging.channels.flare</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">[</span><span class="token">
</span></span><span class="line"><span class="token">                </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">driver</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">flare</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #88C0D0;">config</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">set</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">logging.channels.stack.channels</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">single</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">flare</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>This approach will work for the later versions of the Ignition client, but before Laravel 9, the Flare package (which will use the key to send a test) gets registered before <code>laravel-shared</code>. This means our update to the config will come too late, and the key will be set to <code>null</code>.</p>
<p>We've fixed this by adding the following lines to <code>app/bootstrap.php</code> in those versions:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">app</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">resolving</span><span class="token" style="color: #ECEFF4;">(</span><span class="token">Illuminate</span><span class="token" style="color: #ECEFF4;">\</span><span class="token">Foundation</span><span class="token" style="color: #ECEFF4;">\</span><span class="token">Bootstrap</span><span class="token" style="color: #ECEFF4;">\</span><span class="token" style="color: #8FBCBB;">RegisterProviders</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #81A1C1;">class</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #81A1C1;">use</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">app</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">app</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">register</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #8FBCBB;">LaravelSharedServiceProvider</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #81A1C1;">class</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span></code></pre>
<p>This line of code will cause Laravel to register the <code>LaravelSharedServiceProvider</code> before resolving all other service providers so that the key is set correctly.</p>
<p>Now we're ready to test the <code>php artisan flare:test</code> command on all the Laravel versions, but we're lazy developers. Let's update our bash script again so that we can run artisan commands:</p>
<pre class="phiki language-shellscript nord" data-language="shellscript" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #616E88;">#!</span><span class="token" style="color: #616E88;">/bin/bash
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">setup_laravel</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #616E88;">#</span><span class="token" style="color: #616E88;"> Our previous code
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #D8DEE9;">command</span><span class="token" style="color: #81A1C1;">=</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #D8DEE9;">$</span><span class="token" style="color: #D8DEE9;">1</span><span class="token" style="color: #ECEFF4;">"</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">shift</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">if</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">[</span><span class="token"> </span><span class="token" style="color: #81A1C1;">-z</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #D8DEE9;">$</span><span class="token" style="color: #D8DEE9;">command</span><span class="token" style="color: #ECEFF4;">"</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #81A1C1;">;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">then</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #88C0D0;">echo</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">No command provided, the following commands can be used:</span><span class="token" style="color: #ECEFF4;">"</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #88C0D0;">echo</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">  setup: Setup all Laravel applications</span><span class="token" style="color: #ECEFF4;">"</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #88C0D0;">echo</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">  composer: Run a composer command on all Laravel applications</span><span class="token" style="color: #ECEFF4;">"</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #88C0D0;">echo</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">  artisan: Run an artisan command on all Laravel applications</span><span class="token" style="color: #ECEFF4;">"</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #88C0D0;">echo</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">  open: Open a URL on all Laravel applications</span><span class="token" style="color: #ECEFF4;">"</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #88C0D0;">exit</span><span class="token"> </span><span class="token" style="color: #B48EAD;">1</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">fi</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">for</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">dir</span><span class="token"> </span><span class="token" style="color: #81A1C1;">in</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">l</span><span class="token" style="color: #A3BE8C;">a</span><span class="token" style="color: #A3BE8C;">r</span><span class="token" style="color: #A3BE8C;">a</span><span class="token" style="color: #A3BE8C;">v</span><span class="token" style="color: #A3BE8C;">e</span><span class="token" style="color: #A3BE8C;">l</span><span class="token" style="color: #A3BE8C;">-</span><span class="token" style="color: #A3BE8C;">*</span><span class="token" style="color: #81A1C1;">;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">do</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">if</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">[[</span><span class="token"> </span><span class="token" style="color: #81A1C1;">-d</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #D8DEE9;">$</span><span class="token" style="color: #D8DEE9;">dir</span><span class="token" style="color: #ECEFF4;">"</span><span class="token"> </span><span class="token" style="color: #81A1C1;">&amp;&amp;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">{</span><span class="token" style="color: #D8DEE9;">dir</span><span class="token" style="color: #81A1C1;">#</span><span class="token" style="color: #D8DEE9;">laravel-</span><span class="token" style="color: #81A1C1;">}</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=~</span><span class="token"> ^</span><span class="token" style="color: #ECEFF4;">[</span><span class="token">0-9</span><span class="token" style="color: #ECEFF4;">]</span><span class="token">+$ </span><span class="token" style="color: #ECEFF4;">]]</span><span class="token" style="color: #81A1C1;">;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">then</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #ECEFF4;">(</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #88C0D0;">cd</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #D8DEE9;">$</span><span class="token" style="color: #D8DEE9;">dir</span><span class="token" style="color: #ECEFF4;">"</span><span class="token"> </span><span class="token" style="color: #81A1C1;">|</span><span class="token" style="color: #81A1C1;">|</span><span class="token"> </span><span class="token" style="color: #88C0D0;">exit</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #88C0D0;">echo</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">Working in </span><span class="token" style="color: #D8DEE9;">$</span><span class="token" style="color: #D8DEE9;">dir</span><span class="token" style="color: #ECEFF4;">"</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">if</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">[</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #D8DEE9;">$</span><span class="token" style="color: #D8DEE9;">command</span><span class="token" style="color: #ECEFF4;">"</span><span class="token"> </span><span class="token" style="color: #81A1C1;">==</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">setup</span><span class="token" style="color: #ECEFF4;">"</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #81A1C1;">;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">then</span><span class="token">
</span></span><span class="line"><span class="token">                </span><span class="token" style="color: #88C0D0;">setup_laravel</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">elif</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">[</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #D8DEE9;">$</span><span class="token" style="color: #D8DEE9;">command</span><span class="token" style="color: #ECEFF4;">"</span><span class="token"> </span><span class="token" style="color: #81A1C1;">==</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">composer</span><span class="token" style="color: #ECEFF4;">"</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #81A1C1;">;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">then</span><span class="token">
</span></span><span class="line"><span class="token">                </span><span class="token" style="color: #88C0D0;">valet</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">composer</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #D8DEE9;">$</span><span class="token" style="color: #D8DEE9;">@</span><span class="token" style="color: #ECEFF4;">"</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">elif</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">[</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #D8DEE9;">$</span><span class="token" style="color: #D8DEE9;">command</span><span class="token" style="color: #ECEFF4;">"</span><span class="token"> </span><span class="token" style="color: #81A1C1;">==</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">artisan</span><span class="token" style="color: #ECEFF4;">"</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #81A1C1;">;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">then</span><span class="token">
</span></span><span class="line"><span class="token">                </span><span class="token" style="color: #88C0D0;">valet</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">php</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">artisan</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #D8DEE9;">$</span><span class="token" style="color: #D8DEE9;">@</span><span class="token" style="color: #ECEFF4;">"</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">elif</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">[</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #D8DEE9;">$</span><span class="token" style="color: #D8DEE9;">command</span><span class="token" style="color: #ECEFF4;">"</span><span class="token"> </span><span class="token" style="color: #81A1C1;">==</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">open</span><span class="token" style="color: #ECEFF4;">"</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #81A1C1;">;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">then</span><span class="token">
</span></span><span class="line"><span class="token">                </span><span class="token" style="color: #88C0D0;">open</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">-</span><span class="token" style="color: #A3BE8C;">a</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">Safari</span><span class="token" style="color: #ECEFF4;">"</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">http://</span><span class="token" style="color: #D8DEE9;">$</span><span class="token" style="color: #D8DEE9;">dir</span><span class="token" style="color: #A3BE8C;">.flare-test-apps.test/</span><span class="token" style="color: #D8DEE9;">$</span><span class="token" style="color: #D8DEE9;">@</span><span class="token" style="color: #ECEFF4;">"</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">else</span><span class="token">
</span></span><span class="line"><span class="token">                </span><span class="token" style="color: #88C0D0;">echo</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">Invalid command: </span><span class="token" style="color: #D8DEE9;">$</span><span class="token" style="color: #D8DEE9;">command</span><span class="token" style="color: #ECEFF4;">"</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">fi</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">fi</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">done</span><span class="token">
</span></span></code></pre>
<p>Now, it is possible to do the following:</p>
<pre class="phiki language-shellscript nord" data-language="shellscript" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #88C0D0;">./laravel.sh</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">artisan</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">flare:test</span><span class="token">
</span></span></code></pre>
<p>Which will (thanks to Laravel Valet) run <code>php artisan flare:test</code> on all our Laravel versions.</p>
<h2>In the end</h2>
<p>We're constantly making changes to Flare to make it even better! This tiny bug fix, which will only occur on Laravel projects again, shows why Flare is the best error tracker for Laravel.</p>
]]>
            </summary>
                                    <updated>2024-06-10T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Introducing Laravel Error Share]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/introducing-laravel-error-share" />
            <id>https://flareapp.io/introducing-laravel-error-share</id>
            <author>
                <name><![CDATA[Freek]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Earlier this month, Laravel changed the default error page from Ignition to their home-baked basic version.</p>
<p>Today, we are releasing a new package called <a href="https://github.com/spatie/laravel-error-share">spatie/laravel-error-share</a>, which brings one of Ignition's core features to the new error page: the ability to share errors.</p>
<p>In this blog post, we'd like to tell you all about it.</p>
<h2>Why you would share an error</h2>
<p>Imagine that you're working on a project locally and bump into an exception. You try to figure out the problem, but you're unable to find a solution. In that case, you might want to ask a colleague for help.</p>
<p>Instead of immediately starting screen sharing with your colleague (or letting your colleague roll their chair to your desk if you work in an office), it might be handy to just send a link to your colleague so that he or she can see that exception in detail.</p>
<h2>Using Laravel Error Share</h2>
<p>After you install <a href="https://github.com/spatie/laravel-error-share">the Laravel Error Share Page</a>, a new "Share" button will be displayed on the default Laravel error page. It looks like this.</p>
<p><img src="https://assets.flareapp.io/production/blog/4adb3082ad5dd08d9338a07a6c579cd8/0ee68ae8-dbb9-4eb5-9ab4-9c326214c7f7.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/4adb3082ad5dd08d9338a07a6c579cd8/0ee68ae8-dbb9-4eb5-9ab4-9c326214c7f7.png" width="2698" height="1208" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/4adb3082ad5dd08d9338a07a6c579cd8/0ee68ae8-dbb9-4eb5-9ab4-9c326214c7f7.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/4adb3082ad5dd08d9338a07a6c579cd8/0ee68ae8-dbb9-4eb5-9ab4-9c326214c7f7.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/4adb3082ad5dd08d9338a07a6c579cd8/0ee68ae8-dbb9-4eb5-9ab4-9c326214c7f7.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/4adb3082ad5dd08d9338a07a6c579cd8/0ee68ae8-dbb9-4eb5-9ab4-9c326214c7f7.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>When you press that button, a dropdown menu will open, allowing you to choose which information to share.</p>
<p><img src="https://assets.flareapp.io/production/blog/a6a77651ca866f289a812b8f163e7178/b8c6f608-3db5-47b2-aebf-a9a2f3fd3f5e.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/a6a77651ca866f289a812b8f163e7178/b8c6f608-3db5-47b2-aebf-a9a2f3fd3f5e.png" width="904" height="682" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/a6a77651ca866f289a812b8f163e7178/b8c6f608-3db5-47b2-aebf-a9a2f3fd3f5e.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/a6a77651ca866f289a812b8f163e7178/b8c6f608-3db5-47b2-aebf-a9a2f3fd3f5e.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=904/https://assets.flareapp.io/production/blog/a6a77651ca866f289a812b8f163e7178/b8c6f608-3db5-47b2-aebf-a9a2f3fd3f5e.png 904w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>Of course, like the old Ignition page, we support dark mode as well.</p>
<p><img src="https://assets.flareapp.io/production/blog/d06b9d7f1ba445874f9c62288b092f43/b8763dd1-2d56-4380-ae3f-a4bcef92baa8.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/d06b9d7f1ba445874f9c62288b092f43/b8763dd1-2d56-4380-ae3f-a4bcef92baa8.png" width="812" height="744" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/d06b9d7f1ba445874f9c62288b092f43/b8763dd1-2d56-4380-ae3f-a4bcef92baa8.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/d06b9d7f1ba445874f9c62288b092f43/b8763dd1-2d56-4380-ae3f-a4bcef92baa8.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=812/https://assets.flareapp.io/production/blog/d06b9d7f1ba445874f9c62288b092f43/b8763dd1-2d56-4380-ae3f-a4bcef92baa8.png 812w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>When you press the create share button, all information about your exception will be sent to <a href="https://flareapp.io">Flare</a>, and a publicly accessible but hard-to-guess URL will be generated. On that URL, all information about your exception will be displayed.</p>
<p><img src="https://assets.flareapp.io/production/blog/0f799f57249f6609f8298b6d23ca87ee/9bcba018-ba8f-488e-acdd-89ef5324ac1f.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/0f799f57249f6609f8298b6d23ca87ee/9bcba018-ba8f-488e-acdd-89ef5324ac1f.png" width="3090" height="2176" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/0f799f57249f6609f8298b6d23ca87ee/9bcba018-ba8f-488e-acdd-89ef5324ac1f.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/0f799f57249f6609f8298b6d23ca87ee/9bcba018-ba8f-488e-acdd-89ef5324ac1f.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/0f799f57249f6609f8298b6d23ca87ee/9bcba018-ba8f-488e-acdd-89ef5324ac1f.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/0f799f57249f6609f8298b6d23ca87ee/9bcba018-ba8f-488e-acdd-89ef5324ac1f.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>When you scroll down you see even more information.</p>
<p><img src="https://assets.flareapp.io/production/blog/81cd3b0fa7a10bbffdfc5c28be0403cc/6171256e-9262-4144-a7f6-477d969b9670.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/81cd3b0fa7a10bbffdfc5c28be0403cc/6171256e-9262-4144-a7f6-477d969b9670.png" width="3090" height="2176" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/81cd3b0fa7a10bbffdfc5c28be0403cc/6171256e-9262-4144-a7f6-477d969b9670.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/81cd3b0fa7a10bbffdfc5c28be0403cc/6171256e-9262-4144-a7f6-477d969b9670.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/81cd3b0fa7a10bbffdfc5c28be0403cc/6171256e-9262-4144-a7f6-477d969b9670.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/81cd3b0fa7a10bbffdfc5c28be0403cc/6171256e-9262-4144-a7f6-477d969b9670.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>You can send that URL to the person(s) you'd like to help you fix the exception. You could also add these URLs to GitHub issues or share them in Slack; they have no expiration date.</p>
<h2>Is this secure?</h2>
<p>When using Flare's sharing feature, you can choose what exception data and context to include in the shared error page. The following three sections are available for sharing and correspond to the following sections:</p>
<ul>
<li>Stack: Provides a detailed stack trace that shows the sequence of method calls leading to the error, including file paths and arguments.</li>
<li>Context: Offers relevant contextual information about the error, such as request payload, headers, and routing details.</li>
<li>Debug: Displays additional debugging information like dump output, SQL queries with bindings</li>
</ul>
<p>Generally, the data included in the Laravel error page and its shared errors is safe to share with colleagues.</p>
<p>However, as always, common sense applies. It is important to note that the data may contain sensitive information, such as database credentials, API keys, or other secrets. Therefore, it is recommended to review the data visible in the error page before sharing it with others. Be especially careful when dealing with production data locally.</p>
<h2>How to remove your exception</h2>
<p>To maintain privacy and control over shared error pages, Flare offers a simple method for removing shares. When sharing an error, an ownership cookie is automatically set in your browser. To delete the shared error, simply visit the shared error from the same browser and look for the "Delete Share" button.</p>
<p><img src="https://assets.flareapp.io/production/blog/6b0b6b96183e77f53909fb14fce85edf/0bf69b52-a561-43e1-8fd1-d7add12dee0a.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/6b0b6b96183e77f53909fb14fce85edf/0bf69b52-a561-43e1-8fd1-d7add12dee0a.png" width="1242" height="876" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/6b0b6b96183e77f53909fb14fce85edf/0bf69b52-a561-43e1-8fd1-d7add12dee0a.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/6b0b6b96183e77f53909fb14fce85edf/0bf69b52-a561-43e1-8fd1-d7add12dee0a.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/6b0b6b96183e77f53909fb14fce85edf/0bf69b52-a561-43e1-8fd1-d7add12dee0a.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1242/https://assets.flareapp.io/production/blog/6b0b6b96183e77f53909fb14fce85edf/0bf69b52-a561-43e1-8fd1-d7add12dee0a.png 1242w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>In the rare event that the "Delete Share" button is not visible or you encounter any issues, you can contact <a href="https://flareapp.io">Flare</a> 's dedicated support team at support@flareapp.io and include the share URL you would like to have removed.</p>
<h2>In closing</h2>
<p>Sharing an error is very handy when you need some help from others to fix a local problem. Using our <a href="https://github.com/spatie/laravel-error-share">spatie/laravel-error-share</a> it is easy to make your exception available to others.</p>
<p>This service is provided for free by <a href="https://flareapp.io">Flare</a>, the best exception tracker for Laravel apps. Flare is built by all of us at Spatie, you might already know us by <a href="https://spatie.be/open-source">our extensive collection of open source packages</a>.</p>
]]>
            </summary>
                                    <updated>2024-06-06T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Creating issues is now easier than ever]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/creating-issues-is-now-easier-than-ever" />
            <id>https://flareapp.io/creating-issues-is-now-easier-than-ever</id>
            <author>
                <name><![CDATA[Ruben]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>We love to add small features to Flare. This week's feature allows you to create issues way faster!</p>
<p>If you've integrated your Flare project with GitHub, GitLab, Jira, or Linear (yes, that list is growing rapidly), you can create issues for your errors directly within Flare.</p>
<p>As an added benefit, after you create the issue, it will be automatically linked with the error in Flare so you can keep track of it when using Flare.</p>
<p>In the past, we provided the option to write a title and a description for each issue. The only issue? Sometimes, you just want to add an issue quickly without the hassle of typing a lot.</p>
<p>We've got you covered! From now on, you can click on the "Use details from error" link to prefill some info about the error:</p>
<p><img src="https://assets.flareapp.io/production/blog/7b9657e3027b9aafbd027297a82cb807/98ea7122-6920-47f9-a13d-bc0afdcd8848.png" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/7b9657e3027b9aafbd027297a82cb807/98ea7122-6920-47f9-a13d-bc0afdcd8848.png" width="1230" height="2176" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/7b9657e3027b9aafbd027297a82cb807/98ea7122-6920-47f9-a13d-bc0afdcd8848.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/7b9657e3027b9aafbd027297a82cb807/98ea7122-6920-47f9-a13d-bc0afdcd8848.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/7b9657e3027b9aafbd027297a82cb807/98ea7122-6920-47f9-a13d-bc0afdcd8848.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1230/https://assets.flareapp.io/production/blog/7b9657e3027b9aafbd027297a82cb807/98ea7122-6920-47f9-a13d-bc0afdcd8848.png 1230w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>Happy debugging!</p>
]]>
            </summary>
                                    <updated>2024-05-28T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Assigning a user to an error]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/assigning-a-user-to-an-error" />
            <id>https://flareapp.io/assigning-a-user-to-an-error</id>
            <author>
                <name><![CDATA[Niels]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>In popular project and source control systems, like GitHub, you can assign a person who should take care of a particular issue.</p>
<p>Now you can do this at Flare as well. In our latest release we've added support for assigning an error to a user of your team.</p>
<p>Let's dive into a quick demonstration of how you can achieve this:</p>
<video class="aspect-video" controls="" loop="" autoplay="" muted="">
  <source src="https://assets.flareapp.io/production/blog/452ff8dcca3afb192731d7c71957f125/1d9826f1-3d32-49ab-b627-396d8fa9b33a.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>
<h3>Filtering errors</h3>
<p>To navigate through errors, you can now filter them based on the assigned users. While typing, a list of users who have been assigned is shown, with the total number of assigned errors to this user.</p>
<video class="aspect-video" controls="" loop="" autoplay="" muted="">
  <source src="https://assets.flareapp.io/production/blog/3359e63ac690a1517d389fcd28d8e6ec/38f08719-6f00-4984-89d7-b5568fe1ea39.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>
<p>Additionally, there's a filter available to display all unassigned errors.</p>
<video class="aspect-video" controls="" loop="" autoplay="" muted="">
  <source src="https://assets.flareapp.io/production/blog/90fd437b467a797e77ee31b16c4c096c/ad056b1e-f5a7-49e5-965d-25141501148a.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>
<h3>Customize your settings</h3>
<p>Don't want to be notified when assigned or unassigned? You can tweak your notification setting too.</p>
<p><img src="https://assets.flareapp.io/production/blog/fab46c2704a937af38377513fd8a1b68/fa692308-52b7-492b-916b-bcaa0175869e.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/fab46c2704a937af38377513fd8a1b68/fa692308-52b7-492b-916b-bcaa0175869e.png" width="1208" height="1610" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/fab46c2704a937af38377513fd8a1b68/fa692308-52b7-492b-916b-bcaa0175869e.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/fab46c2704a937af38377513fd8a1b68/fa692308-52b7-492b-916b-bcaa0175869e.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/fab46c2704a937af38377513fd8a1b68/fa692308-52b7-492b-916b-bcaa0175869e.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1208/https://assets.flareapp.io/production/blog/fab46c2704a937af38377513fd8a1b68/fa692308-52b7-492b-916b-bcaa0175869e.png 1208w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
]]>
            </summary>
                                    <updated>2024-05-14T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Rethinking deploys at Flare]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/rethinking-deploys-at-flare" />
            <id>https://flareapp.io/rethinking-deploys-at-flare</id>
            <author>
                <name><![CDATA[Alex]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Flare has long relied on locally running Laravel Envoy for our app deployments. However, as the application and infrastructure evolves, so do the challenges. This article documents some of our endeavors to come up with a more robust solution in 2024 while keeping our deploys as simple as possible.</p>
<h2>Our current deployment process</h2>
<p>At Flare, we've been long time fans of Laravel Envoy. This task runner allows you to use Blade syntax for running bash scripts locally or on your servers over SSH. This makes it a great tool for deploying your application. For Flare, we've used Envoy for years to perform atomic deployments, asynchronously SSH into our production servers, install and build dependencies and finally perform an <a href="https://www.netlify.com/blog/2021/02/23/terminology-explained-atomic-and-immutable-deploys/">atomic deployment</a>. This strategy is based on a <a href="https://serversforhackers.com/c/deploying-with-envoy-cast">blog post on Servers for Hackers</a> from back in 2015.</p>
<h2>A couple drawbacks</h2>
<p>Even though we've tweaked and changed a lot in the past 9 years, this approach has a couple fundamental drawbacks:</p>
<ul>
<li>because the deploy runs separately on every server, it's as slow as the slowest server</li>
<li>while usually not a problem, the results of composer install or the front-end build might end up being different on every server (for example due to different OS or cached dependencies)</li>
<li>disk space on every server needs to be monitored to avoid having a bunch of old releases taking up too much space</li>
<li>credentials for private NPM registries and Composer repositories need to be available on every server</li>
<li>every developer needs SSH access to every server</li>
<li>Envoy requires quite some additional "boilerplate" code to set-up zero-downtime, atomic deployments</li>
</ul>
<p>Some of these issues have been (partially) fixed by building the front-end assets locally and using Airdrop to distribute build to our servers, but ultimately we're looking for a faster, more resilient, more 2024-ready solution.</p>
<h2>Some requirements and ideas</h2>
<p>We've explored modern deployment solutions such as serverless and containerization in the past. However, for Flare's needs we prefer a more traditional deploy script that is transparent, simple to understand and easy to run. However, we will re-evaluate some key parts of our deploy script to address some of its aforementioned downsides.</p>
<h3>Use Deployer with recipes instead of Envoy</h3>
<p>Envoy is great for small websites with one or two servers. It has support for running tasks on multiple server, but deploying Flare from multiple branches to multiple servers with multiple environments (staging/production) required excessive boilerplate code.</p>
<p>Instead, we've started looking at <a href="https://deployer.org/">Deployer</a>. It's been around for a long time and provides similar features to Laravel Envoy. Additionally, it includes recipes for zero-downtime and atomic Laravel deployments out of the box. This eliminates the need for much of the boilerplate code in our current deployment script.</p>
<h3>Deploy on GitHub Actions instead of locally</h3>
<p>We've been running deployments from our local machines for years. Deploying straight from our local terminals has been pretty useful to keep the deploy process visible and the feedback loop short. While this worked great with a short deploy script and one or two developers, this is now becoming an issue. The biggest downside is that every developer needs to have access to all resources required to perform a deploy (e.g. SSH access, <code>.env</code> files, access to S3 for builds, etc).</p>
<p>Instead, we'll be switching to GitHub Actions as a CI/CD service. This way there's a single service that's responsible for accessing secrets and production services. This also enables us to further automate our release process.</p>
<h3>Centralised build and distribute using rsync</h3>
<p>One of the biggest annoyances in our current approach is that the deploy/build code is executed separately on every server. This means that if we have 5 servers, we'll run tasks like <code>composer install</code> and <code>yarn run production</code> once on every server. This doesn't just waste CPU cycles but also puts unnecessary pressure on production servers. The go-to solution would be to run build front-end assets on a separate CI/CD build server and copy the build to each server using something like Airdrop. However, we decided to take it one step further by preparing and building theapplication in a GitHub Actions runtime and using <a href="https://linux.die.net/man/1/rsync">rsync</a> to copy the <em>entire</em> application directory to every server, not just the required build files.</p>
<h3>Use Airdrop to only build while necessary</h3>
<p>We're already using Airdrop in our current deploy script and we love it. GitHub CI/CD pipelines offer numerous ways to cache build files but Airdrop takes it one step further by allowing us to cache the entire build and defining a set of rules to only invalidate this cached build if any of the relevant files changed. This would also be possible in a (lengthy?) bash script, but our goal is to simplify the deploy script, not complicate it further. It also integrates nicely with GitHub Action's cache store, making it possible to cache our front-end build right next to our deploy server, speeding things up quite a bit.</p>
<h2>Wrapping things up</h2>
<p>We have already started implementing and testing these changes and the first results are promising. Besides the benefits mentioned above, the deployment time has also reduced from 8-10 minutes on an M1 MacBook Pro to slightly over 3 minutes using the default GitHub Actions runner.</p>
<p>Finally, while working on this new deployment approach, we came across another excellent blogpost talking about atomic Laravel deploys using Deployer and GitHub Actions. If you're deploying to a single server, it's worth checking out "<a href="https://christalks.dev/post/deploying-a-laravel-application-with-deployer-and-github-actions-718ece72">Deploying a Laravel Application with Deployer and GitHub Actions</a>" by Chris Page. He goes into more detail about atomic releases and provides some practical code examples to get things set-up.</p>
]]>
            </summary>
                                    <updated>2024-04-24T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[How we stopped a DDoS attack at Laracon]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/how-we-stopped-a-ddos-attack-while-at-a-conference" />
            <id>https://flareapp.io/how-we-stopped-a-ddos-attack-while-at-a-conference</id>
            <author>
                <name><![CDATA[Ruben]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>It is the first day of Laracon EU 2024. Flare was sponsoring the confererence for the first time and we were present with almost the whole Spatie team to promote it. The clock ticks 10:30, and inside the Bimhuis auditorium, Kévin Dunglas talk about how he built the awesome FrankenPHP server is almost over.</p>
<p>The doors of the auditorium open, and people come out towards us to talk about Flare or win a Playstation 5 / Lamborghini* at our booth. And suddenly, a message from Rias, who's still working at the office in Antwerp, pops up in Slack:</p>
<blockquote>
<p>@Freek @Alex @Ruben Flare down? #uptime looks like something DDoS 😬</p>
</blockquote>
<p>Oh no! We've been working towards this conference for weeks: preparing a sales talk (something we've never done; we're developers, not salespeople), crafting banners, picking t-shirts, and ordering a bunch of stickers.</p>
<p>This was our moment, the moment for Flare to shine, and it was down. With Alex, I rushed behind our promo banner, grabbed our MacBooks, and quickly formed a team with Rias at the office and Mathias from the befriend <a href="https://ohdear.app">Oh Dear</a> booth next to us. We started investigating the problem.</p>
<p><img src="https://assets.flareapp.io/production/blog/50272fc29f6f99af7637f87393821b8a/d87123cc-ab14-4fe4-9cd5-0471a738f186.jpg" alt="Fixing Flare" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/50272fc29f6f99af7637f87393821b8a/d87123cc-ab14-4fe4-9cd5-0471a738f186.jpg" width="2242" height="1488" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/50272fc29f6f99af7637f87393821b8a/d87123cc-ab14-4fe4-9cd5-0471a738f186.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/50272fc29f6f99af7637f87393821b8a/d87123cc-ab14-4fe4-9cd5-0471a738f186.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/50272fc29f6f99af7637f87393821b8a/d87123cc-ab14-4fe4-9cd5-0471a738f186.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/50272fc29f6f99af7637f87393821b8a/d87123cc-ab14-4fe4-9cd5-0471a738f186.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p><em>Taylor and Freek talking about Flare, while Ruben and Mathias try to fix Flare</em></p>
<p>At that moment, we received a 200x increase in requests compared to a regular day. WTF. Those requests were all sent to our servers, which spun up PHP-FPM processes as quickly as possible to handle the requests. Unfortunately, our server couldn't handle such a load, resulting in downtime.</p>
<p>The first idea that popped into our heads was to spin up more web servers or scale our existing servers so that they could manage the load. However, we quickly realized that it was impossible to manage a load like this by adding extra servers.</p>
<p>While the rest of our team was talking to the conference attendees, that Flare is THE Laravel error tracker, with a small but dedicated team always listening to our clients to create the best error tracker ever. That error tracker was now down for half an hour.</p>
<p>Then it hit us: CloudFlare! We had CloudFlare in front of Flare all the time. Cloudflare has an "I'm under attack" switch, which basically adds a JavaScript challenge to each request coming into Flare. You, as a human, can skip this request by clicking the famous "I'm not a robot" checkbox. Robots are less lucky and can't surpass the challenge.</p>
<p>We hit that "I'm under attack switch," and WOW! Immediately, we saw a reduction in the number of requests hitting the Flare servers. CloudFlare was blocking more than 99% of the requests at that time. We restarted our FPM services, and Flare was back online!</p>
<p>Now was the time to relax ... we were thinking. The great thing about the "I'm under attack" switch is that it immediately stops all non-human requests to a server. The problem with the switch is that it stops all non-human requests to a server. Flare is an error-tracking service. Many people send us errors from their servers. These requests, albeit non-human, are valid requests. By stopping the DDoS, we stopped the ingestion of errors.</p>
<p>Flare exists of two groups of servers, first we have our web servers they allow you to browse to the Flare application. The attack was targeted at these servers, specifically the login endpoint.</p>
<p>Another set of servers are the ingest servers. Ignition and the Flare client send errors from our clients' applications to these servers. There's no human interaction, and these servers were not under attack.</p>
<p>The ingestion servers kept working during the attack because a DDoS attack cannot attack them because the routes are protected by Cloudflare workers (kinda like Lambda functions). The workers check if a request has a valid API key and if the team connected to that API key has enough quota to send us its errors. When an API key does not exist, is invalid, or exceeds its quota, the request will be dropped.</p>
<p>That's why we kept processing errors sent to us during the attack. Our ingestion servers purred like kittens, and they didn't notice anything going on at the web servers.</p>
<p>Back to CloudFlare, the "I'm under attack" mode stopped the DDoS attack on our web servers, but it also stopped legitimate requests made to our ingestion servers. Luckily, the CloudFlare challenge can be configured, and after a few minutes, we managed to exclude it from our ingestion servers.</p>
<p>For the next two hours, the attacker kept sending the same ridiculous amount of requests to our servers. But Flare didn't break a sweat—we were back online!</p>
<p>We found an email addressed to us in our support system. The attacker wanted a certain amount of ransom money. After the payment, he would stop the attack. A few Google searches later, we found other small companies on Twitter (ehm X) that the same attacker targeted. We're glad we didn't pay.</p>
<p>That was one hell of a ride, our first DDoS attack. To let things run smoother the next time, we've updated our internal documentation so that the next time a DDoS attack happens, everyone on our team can configure CloudFlare to stop blocking the correct requests.</p>
<p>To all the other small SAAS companies like us, ensure you have a service like CloudFlare between your servers and the web. It is extremely helpful in these kinds of situations.</p>
<p>* It was a toy Lamborghini, still cool, though 😎</p>
]]>
            </summary>
                                    <updated>2024-03-27T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Migrating from MySQL to Postgres using Laravel's query builder]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/migrating-from-mysql-to-postgres-using-laravels-query-builder" />
            <id>https://flareapp.io/migrating-from-mysql-to-postgres-using-laravels-query-builder</id>
            <author>
                <name><![CDATA[Rias]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Migrating from MySQL to Postgres is not an easy feat in general; there are numerous packages &amp; scripts to accomplish this, like <a href="https://pgloader.io/">Pgloader</a> and <a href="https://pgloader.io/">Nmig</a>.</p>
<p>For us, the best way to do this was using Laravel's query builder, since it is agnostic to the database layer and allows us to transform any data as needed in a language we know (PHP) and a framework we know and love.</p>
<h2>Why?</h2>
<p>First off, a small note on why we moved from MySQL to Postgres since that's probably on everyone's mind reading this.</p>
<p>The main reason is that we were having trouble searching full text to work properly in MySQL for our use case. Our tests and subsequent migration to Postgres made our full-text search more reliable and faster.</p>
<p>We could use a different service or technology for our searching, but in the spirit of <a href="https://mcfunley.com/choose-boring-technology">boring technology</a>, Postgres does its job perfectly.</p>
<p>Looking toward the future, Postgres also offers many exciting opportunities thanks to its extensibility, for example, extensions like <a href="https://www.timescale.com/">Timescale</a>.</p>
<h2>Preparing our database config</h2>
<p>Our source database is MySQL; since this is our default, you could keep the database configuration as-is for the MySQL connection. However, we prefer to create a new one called <code>source</code>.</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">source</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">[</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">driver</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">mysql</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">url</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #88C0D0;">env</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">SOURCE_DATABASE_URL</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">host</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #88C0D0;">env</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">SOURCE_MYSQL_DB_HOST</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">127.0.0.1</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">port</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #88C0D0;">env</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">SOURCE_MYSQL_DB_PORT</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">3306</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">database</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #88C0D0;">env</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">SOURCE_MYSQL_DB_DATABASE</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">forge</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">username</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #88C0D0;">env</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">SOURCE_MYSQL_DB_USERNAME</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">forge</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">password</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #88C0D0;">env</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">SOURCE_MYSQL_DB_PASSWORD</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">...</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span></code></pre>
<p>Make sure to configure the necessary environment keys starting with <code>SOURCE_</code></p>
<p>The next thing to do is to configure the <code>destination</code> connection in the <code>config/database.php</code> file to connect to the target database using the <code>pgsql</code> driver:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">destination</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">[</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">driver</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">pgsql</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">url</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #88C0D0;">env</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">DATABASE_URL</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">host</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #88C0D0;">env</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">DESTINATION_DB_HOST</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">127.0.0.1</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">port</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #88C0D0;">env</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">DESTINATION_DB_PORT</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">5432</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">database</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #88C0D0;">env</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">DESTINATION_DB_DATABASE</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">forge</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">username</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #88C0D0;">env</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">DESTINATION_DB_USERNAME</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">forge</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">password</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #88C0D0;">env</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">DESTINATION_DB_PASSWORD</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">...</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span></code></pre>
<h2>Creating the tables in Postgres</h2>
<p>Laravel makes this extremely straightforward for us here, as you can run the migrations against the target database and have all the tables created using the correct field types.</p>
<pre class="phiki language-shellscript nord" data-language="shellscript" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #88C0D0;">php</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">artisan</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">migrate</span><span class="token"> </span><span class="token" style="color: #A3BE8C;">-</span><span class="token" style="color: #A3BE8C;">-database=destination</span><span class="token">
</span></span></code></pre>
<h2>Creating a command to backfill the data</h2>
<p>Since we're migrating tables with a lot of data, we need a way to sync the majority of the data before the migration. This way, we can minimize the downtime needed to execute a last-minute sync and switch over the database connection.</p>
<p>Laravel's query builder makes retrieving the data from the source MySQL database and inserting it into the destination Postgres database straightforward, as the query builder will change its schema language depending on the database driver.</p>
<p>Below you can find a simplified command from the one we used that retrieves the necessary data in a performant way and inserts it into the target database.</p>
<p>The command accepts two parameters: the <code>table</code> it needs to migrate and a <code>--from</code> parameter that lets you skip until a certain id. This makes the command easily restartable without needing to loop over thousands of rows that have been processed already.</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">class</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">MigrateDatabaseCommand</span><span class="token"> </span><span class="token" style="color: #81A1C1;">extends</span><span class="token"> </span><span class="token" style="color: #8FBCBB;font-weight: bold;">Command</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">protected</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">signature</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">app:migrate-database {table} {--from=}</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">handle</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">table</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">argument</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">table</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">getOutput</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">info</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">Importing table </span><span class="token" style="color: #81A1C1;">{</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">table</span><span class="token" style="color: #81A1C1;">}</span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">total</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">DB</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">connection</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">source</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">table</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">table</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">when</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">option</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">from</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #81A1C1;">fn</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">query</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">=&gt;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">query</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">where</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">id</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">&gt;</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">option</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">from</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">count</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">getOutput</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">progressStart</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">total</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #8FBCBB;">DB</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">connection</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">source</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">table</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">table</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">when</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">option</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">from</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #81A1C1;">fn</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">query</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">=&gt;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">query</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">where</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">id</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">&gt;</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">option</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">from</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">select</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">id</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">chunkById</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #B48EAD;">1_000</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">sourceRows</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #81A1C1;">use</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">table</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">                </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">sourceIds</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">Arr</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">pluck</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">sourceRows</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">id</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">                </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">destinationRows</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">DB</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">connection</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">destination</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">table</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">table</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">whereIn</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">id</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">sourceIds</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">select</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">id</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">get</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">toArray</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">                </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">missingIds</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #88C0D0;">array_diff</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">sourceIds</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #8FBCBB;">Arr</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">pluck</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">destinationRows</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">id</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">                </span><span class="token" style="color: #81A1C1;">if</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #88C0D0;">count</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">missingIds</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">                    </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">missing</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">DB</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">connection</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">source</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">table</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">table</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">whereIn</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">id</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">missingIds</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">get</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">                    </span><span class="token" style="color: #8FBCBB;">DB</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">connection</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">destination</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">table</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">table</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">insert</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">missing</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">                </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">                </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">getOutput</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">progressAdvance</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">option</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">chunk</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #ECEFF4;">}</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">getOutput</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">progressFinish</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>This command loops over every ID inside the source database and checks which ones are already present in the destination database, if there are any missing IDs, the rows will be inserted into the destination database.</p>
<p>These extra checks ensure the command can be re-run at any time, and only missing rows will be fetched and inserted into the new database.</p>
<h2>A couple of "gotcha's"</h2>
<h3>Rows that are updated or deleted between syncs</h3>
<p>We have a unique situation where most tables could be wiped and imported into the destination database in seconds. These tables were, in this case, synced at the time of migration.</p>
<p>The other tables, with millions of rows, are rows inside an "append only" type table that is not updated and is cleaned up after a time period. In this example, we do not have any checks on rows that would need updating or exist in the destination but no longer in the source, for example, when a user deletes some rows.</p>
<p>If your project needs this kind of functionality, then be sure to build it in your command.</p>
<h3>Column lengths</h3>
<p>Make sure your column lengths are defined large enough in Postgres for the existing (and future) data you have. MySQL does not complain and truncates your data. However, Postgres is more strict in this regard and throws an exception when inserting too long data for a column.</p>
<h3>Update any custom queries</h3>
<p>If you're not using the Laravel query builder for some queries, this usually means you're using some functionality specific to the database driver. Make sure to update these queries!</p>
]]>
            </summary>
                                    <updated>2024-03-06T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Enhancing error reporting with custom context and glows]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/enhancing-error-reporting-with-custom-context-and-glows" />
            <id>https://flareapp.io/enhancing-error-reporting-with-custom-context-and-glows</id>
            <author>
                <name><![CDATA[Zuzana]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>When it comes to debugging errors in your Laravel applications, Flare already collects several Laravel and user-specific information to send along with the error. However, an additional layer of customization is available that can provide even deeper insights into your application's state when the error or the exception arises: custom context.</p>
<h2>Adding custom context</h2>
<p>Implementing custom context in Flare is relatively straightforward. You can set context items using the provided Flare facade within your Laravel application in the service provider or an event:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">use</span><span class="token"> </span><span class="token">Spatie</span><span class="token" style="color: #ECEFF4;">\</span><span class="token">LaravelIgnition</span><span class="token" style="color: #ECEFF4;">\</span><span class="token">Facades</span><span class="token" style="color: #ECEFF4;">\</span><span class="token" style="color: #8FBCBB;">Flare</span><span class="token" style="color: #81A1C1;">;</span><span class="token"> 
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #8FBCBB;">Flare</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">context</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">Tenant</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">My-Tenant-Identifier</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span></code></pre>
<p>Once set, the next time an exception happens, the custom value will be in the error's context tab on the Flare dashboard.</p>
<h2>Adding a group of context items</h2>
<p>But what if you have multiple items of custom context to include? Flare allows you to group related context items under a common label. Here's how you can create custom context groups:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">use</span><span class="token"> </span><span class="token">Spatie</span><span class="token" style="color: #ECEFF4;">\</span><span class="token">LaravelIgnition</span><span class="token" style="color: #ECEFF4;">\</span><span class="token">Facades</span><span class="token" style="color: #ECEFF4;">\</span><span class="token" style="color: #8FBCBB;">Flare</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #8FBCBB;">Flare</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">group</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">Custom information</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">[</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">key</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">value</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">another key</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">another value</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span></code></pre>
<h2>Adding a glow</h2>
<p>While adding context is already very useful, we can take error reporting even further. To understand what happened in your application before each error, it can be helpful to log short statements, also known as <code>glows</code>. Glows would be especially useful to log events that might happen multiple times during the request, for example, when calling a payment provider API.</p>
<p>You can set up glows in your Laravel's service provider:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">use</span><span class="token"> </span><span class="token">Spatie</span><span class="token" style="color: #ECEFF4;">\</span><span class="token">LaravelIgnition</span><span class="token" style="color: #ECEFF4;">\</span><span class="token">Facades</span><span class="token" style="color: #ECEFF4;">\</span><span class="token" style="color: #8FBCBB;">Flare</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">use</span><span class="token"> </span><span class="token">Spatie</span><span class="token" style="color: #ECEFF4;">\</span><span class="token">FlareClient</span><span class="token" style="color: #ECEFF4;">\</span><span class="token">Enums</span><span class="token" style="color: #ECEFF4;">\</span><span class="token" style="color: #8FBCBB;">MessageLevels</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #8FBCBB;">Flare</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">glow</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">This is a message from glow!</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">MessageLevels</span><span class="token" style="color: #81A1C1;">::</span><span class="token">DEBUG</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #88C0D0;">func_get_args</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span></code></pre>
<p>Once enabled, glows can be found in chronological order in the Debug tab of the error on the Flare dashboard.</p>
<p>Flare already excels at capturing essential data for error diagnosis, but leveraging custom context and glows allows you to debug the errors even better. If that sounds like something you want to explore, you can try Flare for free! New customers get a 10-day trial, and no credit card is required to sign up. Check out our&nbsp;<a href="https://flareapp.io/docs">documentation</a>&nbsp;for more information.</p>
]]>
            </summary>
                                    <updated>2024-02-18T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[How we built our GitLab integration]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/how-we-built-our-gitlab-integration" />
            <id>https://flareapp.io/how-we-built-our-gitlab-integration</id>
            <author>
                <name><![CDATA[Ruben]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>A few days ago, we added a GitLab integration. In this blog post, we'll talk about the road to that integration and our struggles.</p>
<p><img src="https://assets.flareapp.io/production/blog/9943471c5fe9f991cd017604a8c0265d/23b34fa7-32b1-4e2a-b71a-4c40dc8230fb.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/9943471c5fe9f991cd017604a8c0265d/23b34fa7-32b1-4e2a-b71a-4c40dc8230fb.png" width="530" height="516" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/9943471c5fe9f991cd017604a8c0265d/23b34fa7-32b1-4e2a-b71a-4c40dc8230fb.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=530/https://assets.flareapp.io/production/blog/9943471c5fe9f991cd017604a8c0265d/23b34fa7-32b1-4e2a-b71a-4c40dc8230fb.png 530w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>Frequent readers of our blog might have read <a href="https://flareapp.io/blog/building-a-flexible-yet-abstract-external-integrations-structure">Building a flexible yet abstract external integrations Structure</a>. In that technical blog post, we describe how we revamped our internal structures to allow us creating integrations like GitHub, Jira, and GitLab in days rather than weeks. If you haven't read it, be sure to read it first.</p>
<p>We started building these new structures with the Jira integration and used them again when we converted our existing GitHub integration. To ensure these structures were as flexible as we wanted them to be. It was required to battle-test them!</p>
<p>Could we set up a new Flare integration from scratch in two days? That's where GitLab comes into the picture. Could we add a GitLab integration like we did on GitHub and Jira without breaking a sweat?</p>
<p>At Spatie, we're using GitHub. We had no experience with GitLab, but they kinda follow the same conventions as GitHub. Issues are issues, and a repository is called a project, a team is a group, and, oh yeah, you can nest groups.</p>
<p>The most complex part of the GitLab integration was that GitLab exists in two flavors, like GitHub, you can use the cloud version, where you create an account, and you're ready to go. GitLab also offers the ability to be self-hosted; before you can create an account, you'll need to set up a GitLab instance on your server, and after that, you can create an account.</p>
<p>This self-hosted option added extra challenges for us. Because we cannot, like with GitHub and Jira, create an application for Flare on GitLab with which we can connect. Instead, a user should create its own application with which Flare can connect when using a self-hosted GitLab instance.</p>
<p>We provide an application for the cloud version, which makes connecting Flare with the cloud version of GitLab as complex as clicking one button, which is nice.</p>
<p>When installing a new GitLab connection into Flare, you will be asked to use the Cloud or self-hosted version, allowing you to choose your personal favorite.</p>
<h2>Implementing</h2>
<p>We spend most of our time finding out how GitLab works, reading the GitLab API documentation, and writing front-end code.</p>
<p>Implementing the backend code was straightforward; the integration structures we discussed earlier worked like a charm. Let's go through a few of them:</p>
<h3>IntegrationIssueData</h3>
<p>The <code>IntegrationIssueData</code> object which represents an individual issue :</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">class</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">GitLabIntegrationIssueData</span><span class="token"> </span><span class="token" style="color: #81A1C1;">extends</span><span class="token"> </span><span class="token" style="color: #8FBCBB;font-weight: bold;">IntegrationIssueData</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">__construct</span><span class="token" style="color: #ECEFF4;">(</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">int</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">id</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">int</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">number</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">string</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">title</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">?</span><span class="token" style="color: #81A1C1;">string</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">description</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">GitLabIssueState</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">state</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">GitLabIssueType</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">type</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">    
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">getId</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #81A1C1;">int</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">return</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">id</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">static</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">createFromApiPayload</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">array</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">payload</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #81A1C1;">self</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">return</span><span class="token"> </span><span class="token" style="color: #81A1C1;">new</span><span class="token"> </span><span class="token" style="color: #81A1C1;">self</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">            </span><span class="token" style="color: #88C0D0;">id</span><span class="token" style="color: #ECEFF4;">:</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">payload</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">iid</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">            </span><span class="token" style="color: #88C0D0;">number</span><span class="token" style="color: #ECEFF4;">:</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">payload</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">iid</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">            </span><span class="token" style="color: #88C0D0;">title</span><span class="token" style="color: #ECEFF4;">:</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">payload</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">title</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">            </span><span class="token" style="color: #88C0D0;">description</span><span class="token" style="color: #ECEFF4;">:</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">payload</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">description</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">            </span><span class="token" style="color: #88C0D0;">state</span><span class="token" style="color: #ECEFF4;">:</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #8FBCBB;">GitLabIssueState</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">from</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">payload</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">state</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">            </span><span class="token" style="color: #88C0D0;">type</span><span class="token" style="color: #ECEFF4;">:</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #8FBCBB;">GitLabIssueType</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">from</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">payload</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">issue_type</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">        </span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>It is still looking great. We can keep track of all the issue properties a GitLab issue has.</p>
<p>Thanks to the <code>createFromApiPayload</code> method, a GitLab API object is automatically mapped to our structure (no more iid's), keeping it simple for other developers in our team to work with the data without any knowledge of the GitLab internal structure.</p>
<h3>IntegrationIssueDriver</h3>
<p>The <code>IntegrationIssueDriver</code> is the heart of our integration system. It looks (redacted) like this:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">class</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">GitLabInstalledIntegrationDriver</span><span class="token"> </span><span class="token" style="color: #81A1C1;">implements</span><span class="token"> </span><span class="token" style="color: #8FBCBB;font-weight: bold;">IssueIntegrationDriver</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">getIssue</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #8FBCBB;">ProjectIntegration</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">projectIntegration</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #81A1C1;">int</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">id</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">GitLabIntegrationIssueData</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">config</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">ensureProjectIntegrationConfigData</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">projectIntegration</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">response</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">resolveConnector</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">projectIntegration</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">integrationConnection</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">send</span><span class="token" style="color: #ECEFF4;">(</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">new</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">GetGitLabIssueRequest</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">config</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">project_id</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">id</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">return</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">GitLabIntegrationIssueData</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">createFromApiPayload</span><span class="token" style="color: #ECEFF4;">(</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">response</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">json</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">getIssueByUrl</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #8FBCBB;">ProjectIntegration</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">projectIntegration</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #81A1C1;">string</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">url</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">IntegrationIssueData</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #88C0D0;">preg_match_all</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'/</span><span class="token" style="color: #EBCB8B;">\/</span><span class="token" style="color: #EBCB8B;">issues</span><span class="token" style="color: #EBCB8B;">\/</span><span class="token" style="color: #EBCB8B;">(?&lt;id&gt;</span><span class="token" style="color: #EBCB8B;">\d</span><span class="token" style="color: #81A1C1;">+</span><span class="token" style="color: #EBCB8B;">)</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #ECEFF4;">/m'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">url</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">matches</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #81A1C1;">PREG_SET_ORDER</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">id</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">matches</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #B48EAD;">0</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">id</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">]</span><span class="token"> </span><span class="token" style="color: #81A1C1;">??</span><span class="token"> </span><span class="token" style="color: #81A1C1;">null</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">if</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">!</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">id</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">throw</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">CouldNotMatchUrlToIntegrationIssue</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">create</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">url</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">try</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">return</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">getIssue</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">projectIntegration</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">id</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #ECEFF4;">}</span><span class="token"> </span><span class="token" style="color: #81A1C1;">catch</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #8FBCBB;">NotFoundException</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">throw</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">CouldNotMatchUrlToIntegrationIssue</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">create</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">url</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>There are many more methods in the connector than this, but to keep things more readable, we will show these two methods.</p>
<p>The <code>getIssue</code> method returns an <code>IntegrationIssueData</code> object for an issue tied to a Flare project. It makes an API call to GitLab and uses <a href="https://github.com/spatie/laravel-data">laravel-data</a> to map the response to a data object.</p>
<p>See that <code>resolveConnector</code> method? We use the excellent <a href="https://docs.saloon.dev/">Saloon</a> package to have an API abstraction layer between Flare and GitLab. It handles everything from authorization with OAuth, sending requests, receiving responses, and allowing us to use fakes for testing the API. Saloon is excellent. If you haven't, you should start using it now!</p>
<p>The <code>getIssueByUrl</code> method we saw in the previous blog post tries to find an issue by URL and returns an <code>IntegrationIssueData</code> object. After using regex magic, we use the <code>getIssue</code> method to fetch the issue.</p>
<p>We can even abstract more here. In the GitHub, Jira, and thus GitLab connectors, this method always looks the same: use a regex to extract an id and then call the <code>getIssue</code> method with that id returning a data object.</p>
<p>It would require us to transform <code>IssueIntegrationDriver</code> from an interface into an abstract class and add the requirement to implement <code>resolveIssueUrlRegex</code>. Coding this will take little time.</p>
<p>So why didn't we do it? Could our subsequent integration need more complex code to resolve the ID? We don't know it at the moment, but trying to abstract everything away will, in the end, make things a lot harder to implement. A bit of code duplication is not bad in these cases.</p>
<p>When we've implemented more than five integrations, and the code is the same, then that's the moment we'll refactor.</p>
<h2>Webhooks</h2>
<p>In the previous post, we had a long section about Webhooks. External platforms like Jira and GitHub send them to us, allowing Flare to react to changes on these platforms.</p>
<p>At the moment, we do not ingest webhooks from GitLab. That's because, unlike those other platforms, there's no option to subscribe to them automatically.</p>
<p>Webhooks do exist in GitLab, but a user must manually register them. For our GitLab self-hosted customers, this could be an extra step in the wizard when an app is created in their GitLab instance.</p>
<p>For our GitLab cloud customers, the installation process has become much more complex than clicking a button; they now should register webhooks themselves.</p>
<p>We could add the webhooks ourselves since we have access to the API, but adding things to a GitLab instance that isn't ours feels intrusive.</p>
<p>Even worse, somebody could remove those webhooks (intentionally or unintentionally), and the two-way sync would stop working. To do it "right" we need to check around certain intervals if the webhooks still exist and if they are configured correctly.</p>
<p>This complicated webhook setup is why we're not implementing a two-way GitLab sync for now. We do love to listen to our users. Let us know if you want to see a two-way sync in GitLab, which might make the integration a bit more intrusive.</p>
<p>Luckily, we've built the <code>IssueIntegrationDriver</code> so that all the code related to webhooks is defined in another interface called <code>WebhookIntegrationDriver</code>, making it possible to add a new integration to Flare without webhooks support.</p>
<h2>Conclusion</h2>
<p>Our GitLab integration started as an experiment to see if our code was flexible enough to support upcoming integrations. I can tell you it is! We implemented the whole integration in two days!</p>
<p>We love working on Flare and have a lot of new features in the pipeline, heck, maybe even a GitLab two-way sync 😉</p>
]]>
            </summary>
                                    <updated>2023-12-06T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[You can now give team members access to specific projects]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/you-can-now-give-team-members-access-to-specific-projects" />
            <id>https://flareapp.io/you-can-now-give-team-members-access-to-specific-projects</id>
            <author>
                <name><![CDATA[Freek]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>On most projects, you're probably not working alone. That's why Flare has been team-based from day one. Everybody invited to your team on Flare can see all the projects you have in your account.</p>
<p>For small teams, this mostly works fine. When you have a bigger team, people often don't need access to all projects in your Flare account but only a few specific ones they are working on.</p>
<p>That's why we've now added the ability to give team members access to specific projects. In the team settings, you can specify which projects each of your team members should be able to see.</p>
<p><img src="https://assets.flareapp.io/production/blog/1a4fc5b92de5aa0976187dbcb4a16752/1d9a486b-ff1d-4bed-afd6-7c64f3ac5d5e.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/1a4fc5b92de5aa0976187dbcb4a16752/1d9a486b-ff1d-4bed-afd6-7c64f3ac5d5e.jpg" width="2928" height="1692" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/1a4fc5b92de5aa0976187dbcb4a16752/1d9a486b-ff1d-4bed-afd6-7c64f3ac5d5e.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/1a4fc5b92de5aa0976187dbcb4a16752/1d9a486b-ff1d-4bed-afd6-7c64f3ac5d5e.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/1a4fc5b92de5aa0976187dbcb4a16752/1d9a486b-ff1d-4bed-afd6-7c64f3ac5d5e.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/1a4fc5b92de5aa0976187dbcb4a16752/1d9a486b-ff1d-4bed-afd6-7c64f3ac5d5e.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>Remember that Flare, as an error tracker, contains potentially sensitive information. So it's also good practice to only expose that information to people who really need to see it.</p>
]]>
            </summary>
                                    <updated>2023-11-09T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Leveraging Cloudflare Workers for Edge API Authentication]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/leveraging-cloudflare-workers-for-edge-api-authentication" />
            <id>https://flareapp.io/leveraging-cloudflare-workers-for-edge-api-authentication</id>
            <author>
                <name><![CDATA[Alex]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Currently, there is at least a <a href="https://www.cloudping.co/grid">±70ms latency between the EU and US data centers</a>, which is unlikely to improve significantly due to the limitations imposed by the speed of light and other annoying physics-based factors. The internet could be a lot faster if we took all the people and servers and put them on the same continent but that brings other issues (where would I get my free healthcare?). Fortunately, edge computing allows you to bring your web app closer to your customers without having to organise a mass relocation.</p>
<p>Cloudflare and AWS can leverage their existing network and datacenters all over the world to bring your application to edge locations, as close to your users as possible. In this article, we'll explore how Cloudflare Workers can be used to deploy a small serverless function that performs API authentication on the edge, providing an effective solution to drop unauthorised and invalid requests in a high-traffic environment like Flare.</p>
<h2>Why perform API auth on edge at all?</h2>
<p>Flare receives a lot of invalid requests. About 40% of all incoming API requests contain invalid, expired or no API keys. In an ideal world, 100% of our CPU time (and budget) should go to handling valid requests from paying customers and not to rejecting unauthorised requests. By placing a cheap and fast edge function in front of our API, we can reject 40% of incoming traffic before it ever hits our ingress servers.</p>
<p>Keep in mind that we’re authenticating the incoming API requests on our own servers as well. Performing authentication twice may appear counter-intuitive, but the actual API endpoints remain publicly accessible if you manage to bypass the edge function and access the API directly. Additionally, keeping the authentication in the application code keeps the application nice and simple to run and debug locally.</p>
<h2>Choosing Cloudflare Workers</h2>
<p>After careful consideration (read: a panicked evening during a suspected DDoS attack) we decided on using Cloudflare’s excellent (and free) DDoS protection and WAF. When the time came to investigate serverless functions for the aforementioned API auth, we quickly decided on Cloudflare Workers too. They proved to be a good choice with instant cold starts, reasonable pricing, global availability (thanks to their edge-based nature), and easy integration with existing infrastructure on Cloudflare.</p>
<h2>Let’s write a basic authentication Worker</h2>
<p>Let’s put the theory into practice. Like the name suggests, an edge function is not much more than a function. The code for a very basic Cloudflare Worker looks like this:</p>
<pre class="phiki language-javascript nord" data-language="javascript" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">export</span><span class="token"> </span><span class="token" style="color: #81A1C1;">default</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">async</span><span class="token"> </span><span class="token" style="color: #88C0D0;">fetch</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #D8DEE9;">request</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token">Request</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token">Promise</span><span class="token" style="color: #ECEFF4;">&lt;</span><span class="token">Response</span><span class="token" style="color: #ECEFF4;">&gt;</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">return</span><span class="token"> </span><span class="token" style="color: #81A1C1;">new</span><span class="token"> </span><span class="token" style="color: #88C0D0;">Response</span><span class="token">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">Hello world!</span><span class="token" style="color: #ECEFF4;">'</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span></code></pre>
<p>To authenticate the API request here, we should check the API key in the <code>request.headers</code>. If everything checks out we can use the <code>fetch</code> API to send the request to our actual API endpoint:</p>
<pre class="phiki language-javascript nord" data-language="javascript" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">export</span><span class="token"> </span><span class="token" style="color: #81A1C1;">default</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">async</span><span class="token"> </span><span class="token" style="color: #88C0D0;">fetch</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #D8DEE9;">request</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token">Request</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token">Promise</span><span class="token" style="color: #ECEFF4;">&lt;</span><span class="token">Response</span><span class="token" style="color: #ECEFF4;">&gt;</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">const</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">apiKey</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">request</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #D8DEE9;">headers</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #88C0D0;">get</span><span class="token">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">X-Api-Key</span><span class="token" style="color: #ECEFF4;">'</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">		</span><span class="token" style="color: #81A1C1;">if</span><span class="token"> </span><span class="token">(</span><span class="token" style="color: #81A1C1;">!</span><span class="token"> </span><span class="token" style="color: #88C0D0;">authenticate</span><span class="token">(</span><span class="token" style="color: #D8DEE9;">apiKey</span><span class="token">)</span><span class="token">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">			</span><span class="token" style="color: #81A1C1;">return</span><span class="token"> </span><span class="token" style="color: #81A1C1;">new</span><span class="token"> </span><span class="token" style="color: #88C0D0;">Response</span><span class="token">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">Invalid API key</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token"> </span><span class="token" style="color: #88C0D0;">status</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #B48EAD;">403</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">		</span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">        </span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> forward the `request` to example.com/api</span><span class="token">
</span></span><span class="line"><span class="token">		</span><span class="token" style="color: #81A1C1;">return</span><span class="token"> </span><span class="token" style="color: #88C0D0;">fetch</span><span class="token">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">https://example.com/api</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">request</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span></code></pre>
<h2>Cloudflare Workers KV</h2>
<p>To authenticate the <code>apiKey</code>, it might be tempting to connect to your MySQL database and check the appropriate tables. However, because we’re executing this code on the edge, the database might be many milliseconds away, resulting in slow queries! Additionally, opening a many new database connection for all incoming API requests would undermine the purpose of having API authentication on the edge anyways.</p>
<p>Instead of hitting the database directly, we can leverage Cloudflare Workers KV, a low-latency and globally synced key-value store. It’s also accessible from the Cloudflare API, allowing us to sync a list of all active API keys to Workers KV, directly from a CRON job in our application. Accessing the Workers KV store from our Cloudflare Worker is made really easy with <a href="https://developers.cloudflare.com/kv/learning/kv-bindings/">KV bindings</a>:</p>
<pre class="phiki language-javascript nord" data-language="javascript" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">export</span><span class="token"> </span><span class="token" style="color: #81A1C1;">default</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">	</span><span class="token" style="color: #81A1C1;">async</span><span class="token"> </span><span class="token" style="color: #88C0D0;">fetch</span><span class="token" style="color: #ECEFF4;">(</span><span class="token">
</span></span><span class="line"><span class="token">		</span><span class="token" style="color: #D8DEE9;">request</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token">Request</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">		</span><span class="token" style="color: #D8DEE9;">env</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token">Env</span><span class="token">
</span></span><span class="line"><span class="token">	</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token">Promise</span><span class="token" style="color: #ECEFF4;">&lt;</span><span class="token">Response</span><span class="token" style="color: #ECEFF4;">&gt;</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">        </span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> env.ApiKeys is bound to the KV store:</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">const</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">apiKeys</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">await</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">env</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #D8DEE9;">ApiKeys</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #88C0D0;">get</span><span class="token" style="color: #ECEFF4;">&lt;</span><span class="token" style="color: #81A1C1;">string</span><span class="token">[</span><span class="token">]</span><span class="token" style="color: #ECEFF4;">&gt;</span><span class="token">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">api_keys</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token" style="color: #88C0D0;">type</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">json</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">}</span><span class="token">)</span><span class="token"> </span><span class="token" style="color: #81A1C1;">||</span><span class="token"> </span><span class="token">[</span><span class="token">]</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">const</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">apiKey</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">request</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #D8DEE9;">headers</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #88C0D0;">get</span><span class="token">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">X-Api-Key</span><span class="token" style="color: #ECEFF4;">'</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">if</span><span class="token"> </span><span class="token">(</span><span class="token" style="color: #D8DEE9;">apiKeys</span><span class="token" style="color: #ECEFF4;">.</span><span class="token" style="color: #88C0D0;">includes</span><span class="token">(</span><span class="token" style="color: #D8DEE9;">apiKey</span><span class="token">)</span><span class="token">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">return</span><span class="token"> </span><span class="token" style="color: #81A1C1;">new</span><span class="token"> </span><span class="token" style="color: #88C0D0;">Response</span><span class="token">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">Invalid API key</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token" style="color: #88C0D0;">status</span><span class="token" style="color: #ECEFF4;">:</span><span class="token"> </span><span class="token" style="color: #B48EAD;">403</span><span class="token" style="color: #ECEFF4;">}</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">return</span><span class="token"> </span><span class="token" style="color: #88C0D0;">fetch</span><span class="token">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">https://example.com/api</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #D8DEE9;">request</span><span class="token">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span></code></pre>
<p>Additionally, Cloudflare Worker cache can be used to decrease KV costs and latency even more.</p>
<h2>Getting ready for production</h2>
<p>Of course, there’s more to it than just these 20 lines of code. For Flare, we have additional code in place to <a href="https://developers.cloudflare.com/workers/examples/cors-header-proxy/">handle CORS requests</a>, <a href="https://developers.cloudflare.com/workers/examples/cache-using-fetch/">cache API keys using Worker cache</a>, and perform extra validation.</p>
<p>To deploy our Cloudflare Worker, we can use Cloudflare’s <code>wrangler</code> CLI. Using a simple config file and the <code>wrangler</code> command, it’s really easy to deploy to multiple environments with different environment variables, run the function locally, or tail the production logs. The config file also defines the <code>routes</code> that the Worker function will be active on. This allows us to essentially use the Worker as a middleware for an existing route.</p>
<h2>I swear this is not a sales pitch</h2>
<p>Cloudflare deserves more credit for everything they offer in their free tier. With DDoS protection, a basic WAF, and an abundance of free Worker function invocations (they're practically throwing them at us), load on our ingress servers has drastically reduced. Additionally, we now have the ability to more easily manage and route traffic through code, should we feel the need.</p>
]]>
            </summary>
                                    <updated>2023-11-05T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Building a flexible yet abstract external integrations Structure]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/building-a-flexible-yet-abstract-external-integrations-structure" />
            <id>https://flareapp.io/building-a-flexible-yet-abstract-external-integrations-structure</id>
            <author>
                <name><![CDATA[Ruben]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Here we are. Flare is better than ever! We've been adding features week after week. Yet, one significant request on our <a href="https://github.com/spatie/flareapp.io-roadmap/discussions">roadmap</a> remains: Jira! It is the most upvoted request on our roadmap, which is something we can't ignore.</p>
<p>We already had a GitHub integration. It allows you to <a href="/docs/flare/general/github-integration">link GitHub</a> issues and PRs to Flare errors. What's cool is that you can automatically close an issue when an error is resolved. The architecture behind this was built over two years ago, and we thought we had built it so that we could plug in another issue-tracking system like Jira without any problems. Oh boy, were we wrong about that.</p>
<p><img src="https://assets.flareapp.io/production/blog/21bbeaa006c17997126636722cae8a18/e52ebd3a-973a-463b-a173-716a01b77603.jpg" alt="github example" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/21bbeaa006c17997126636722cae8a18/e52ebd3a-973a-463b-a173-716a01b77603.jpg" width="1674" height="658" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/21bbeaa006c17997126636722cae8a18/e52ebd3a-973a-463b-a173-716a01b77603.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/21bbeaa006c17997126636722cae8a18/e52ebd3a-973a-463b-a173-716a01b77603.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/21bbeaa006c17997126636722cae8a18/e52ebd3a-973a-463b-a173-716a01b77603.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/21bbeaa006c17997126636722cae8a18/e52ebd3a-973a-463b-a173-716a01b77603.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>So, building the Jira integration opened up an opportunity to do it better, and that's just what we did. We built a flexible structure that abstracts much of the code shared among integrations, allowing us to add new integrations quickly. We'll go through three examples where we've abstracted a lot of code while still making it possible to implement completely different integrations.</p>
<h2>Requirements</h2>
<p>At its simplest, an issue tracker is an external service with a few groups (projects might be a better term here, but this clashed too much within our code base with Flare projects). For GitHub, a group is a repository; for Jira, this is a project. Each group has issues within them. These issues have a title and a body where the issue is summarized. Most of the time, these issues also have comments with a body.</p>
<h2>Issue Data</h2>
<p>We want to link a Flare error with an issue in an integration. So, somewhere within our database table, we need to store this link. A possible schema will probably look like this:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">id
</span></span><span class="line"><span class="token">error_id
</span></span><span class="line"><span class="token">integration_type
</span></span><span class="line"><span class="token">integration_id
</span></span></code></pre>
<p>Because we want to show some information about linked issues on an error, we need a data structure for them:</p>
<p>For Jira, the structure would look like this:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">class</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">JiraIntegrationIssueData</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">__construct</span><span class="token" style="color: #ECEFF4;">(</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">int</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">id</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">string</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">key</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">string</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">summary</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">?</span><span class="token" style="color: #81A1C1;">string</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">description</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">string</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">status</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">?</span><span class="token" style="color: #81A1C1;">string</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">reporter</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">?</span><span class="token" style="color: #81A1C1;">string</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">assignee</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">?</span><span class="token" style="color: #81A1C1;">string</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">creator</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">    
</span></span></code></pre>
<p>For GitHub, it would look like this:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">class</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">GitHubIntegrationIssueData</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">__construct</span><span class="token" style="color: #ECEFF4;">(</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">int</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">id</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">int</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">number</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">string</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">title</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">?</span><span class="token" style="color: #81A1C1;">string</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">body</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">GitHubIssueType</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">type</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">GitHubIssueStatus</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">status</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #616E88;">/**</span><span class="token" style="color: #616E88;"> </span><span class="token" style="color: #81A1C1;">@var</span><span class="token" style="color: #616E88;"> </span><span class="token" style="color: #81A1C1;">string</span><span class="token" style="color: #81A1C1;">[]</span><span class="token" style="color: #616E88;"> </span><span class="token" style="color: #616E88;">*/</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">array</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">assignees</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">    
</span></span></code></pre>
<p>Apart from the id, these structures are entirely different. We could create separate tables for them and then use a polymorphic relation to these structures. But that means we need to create two more extra tables + two more extra models just for this metadata. At the same time, these data objects contain all the structured data.</p>
<p>Even worse, because we need to link a Flare project to an integration group, we'll need some extra metadata here, so add two tables and two models. To finish it off, a team is linked with an integration instance. For example, you link your Flare team with a GitHub organization or account. Again, we need to store data about this; we need to store some configuration (like the name of the GitHub organization user) and secrets (API keys). This means we'll need to add another four tables and four models to make the Jira and GitHub integrations work.</p>
<p>Adding an extra integration would require four tables and models when we use this approach. We plan to add more integrations, so we would need twenty tables and models to store data when we have five integrations. That's an awful lot of extra models and tables!</p>
<p>Luckily, there's another way. Let's update our schema and add a JSON field called data:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">id
</span></span><span class="line"><span class="token">error_id
</span></span><span class="line"><span class="token">integration_type
</span></span><span class="line"><span class="token">integration_id
</span></span><span class="line"><span class="token">data
</span></span></code></pre>
<p>We could try to cast the <code>JiraIntegrationIssueData</code> and <code>GitHubIntegrationIssueData</code> objects ourselves because we need the functionality to cast them to a JSON payload and back into an object again. Luckily, we don't need to write code for this because we can use the excellent <a href="https://github.com/spatie/laravel-data">laravel-data</a> package to do this for us.</p>
<p>Letting our classes <code>JiraIntegrationIssueData</code> and <code>GitHubIntegrationIssueData</code> extend from data adds a lot of functionality. What's most useful for us is that we can call <code>$someDataObject-&gt;toJson()</code> on our data objects, and they will be automatically cast into a JSON string. Using <code>SomeIntegrationIssueData::from($json)</code> then allows us to create a data object again from JSON, neat!</p>
<p>But we're not done here. We still need a cast for our <code>ErrorIntegrationIssue</code> model representing the link between an error and an integration issue. The data field of the model can be <code>JiraIntegrationIssueData</code>, <code>GitHubIntegrationIssueData</code>, or even some completely new object type that we'll build for a new integration in the future.</p>
<p>Luckily, laravel-data saves us again here. We create an abstract <code>IntegrationIssueData</code> class like this:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">abstract</span><span class="token"> </span><span class="token" style="color: #81A1C1;">class</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">IntegrationIssueData</span><span class="token"> </span><span class="token" style="color: #81A1C1;">extends</span><span class="token"> </span><span class="token" style="color: #8FBCBB;font-weight: bold;">Data</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">getId</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #81A1C1;">int</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>The data classes then would look like this:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">class</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">JiraIntegrationIssueData</span><span class="token"> </span><span class="token" style="color: #81A1C1;">extends</span><span class="token"> </span><span class="token" style="color: #8FBCBB;font-weight: bold;">IntegrationIssueData</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">__construct</span><span class="token" style="color: #ECEFF4;">(</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">        </span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> Properties here</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">getId</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #81A1C1;">int</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">return</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">id</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> and</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">class</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">GitHubIntegrationIssueData</span><span class="token"> </span><span class="token" style="color: #81A1C1;">extends</span><span class="token"> </span><span class="token" style="color: #8FBCBB;font-weight: bold;">IntegrationIssueData</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">__construct</span><span class="token" style="color: #ECEFF4;">(</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">        </span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> Properties here</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">getId</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #81A1C1;">int</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">return</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">id</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>When we create a new link as such:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">gitHubIntegrationIssueData</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">new</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">GitHubIntegrationIssueData</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">...</span><span class="token" style="color: #81A1C1;">.</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">errorIntegrationIssue</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">ErrorIntegrationIssue</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">create</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">[</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">error_id</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">error</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">id</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">integration_type</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">IntegrationType</span><span class="token" style="color: #81A1C1;">::</span><span class="token">GitHub</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">integration_id</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">gitHubIntegrationIssueData</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">id</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">data</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">gitHubIntegrationIssueData</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span></code></pre>
<p>And retrieve somewhere later in our code, we can do the following:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">errorIntegrationIssue</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">data</span><span class="token" style="color: #81A1C1;">;</span><span class="token"> </span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> a GitHubIntegrationIssueData instance</span><span class="token" style="color: #616E88;">
</span></span></code></pre>
<p>How does this magic work? We've added the following cast to <code>ErrorIntegrationIssue</code>:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">class</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">ErrorIntegrationIssue</span><span class="token"> </span><span class="token" style="color: #81A1C1;">extends</span><span class="token"> </span><span class="token" style="color: #8FBCBB;font-weight: bold;">Model</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">protected</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">casts</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">[</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">data</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">IntegrationIssueData</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #81A1C1;">class</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">    </span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> other model stuff</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>Since <code>IntegrationIssueData</code> is abstract, laravel-data knows that each object being saved to the database extends from that abstract data object. So, we also store the class name next to the data from the data object. When retrieving the model and thus data object again from the database, laravel-data will create a new object based on that stored class name and with the stored data.</p>
<h3>Considerations</h3>
<p>The database people are probably already screaming; these data structures all deserve their own table with their own fields. And most of the time, I would agree with them, but not in this case. We don't need to query these data objects; they are only shown on an error page. We can fetch them by looking for issues linked to the error by checking the <code>error_id</code> field.</p>
<p>In one case, we need to query a field from the data object where a webhook comes in, for example, because the issue's title changed. In this case, we need to know that the issue is from a specific integration (this would most certainly mean that we need to check the class name of the data object to determine if it's Jira or GitHub) and its id so that we can update the issue data object.</p>
<p>That's why we have the <code>integration_type</code> and <code>integration_id</code> fields on <code>ErrorIntegrationIssue</code>. When a webhook comes in, we can use these (indexed) fields to quickly look up the issue and then update the data object.</p>
<h2>Communicating with APIs</h2>
<p>Let's look at another part of our application where we tried to create a flexible abstract structure. A new feature we're adding with these integrations is linking a Flare error with an issue using a URL. On an error, you'll have a text field in which you can put a URL to a GitHub or Jira issue, and by clicking a "link" button, these issues will be magically linked.</p>
<p>The process behind this has three steps:</p>
<ol>
<li>
<p>Find the issue based on a URL</p>
</li>
<li>
<p>Add a new entry to <code>ErrorIntegrationIssue</code> with the data from the integration issue</p>
</li>
<li>
<p>Add a comment to the issue that we've linked to it</p>
</li>
</ol>
<p>We chose a driver-based design for this. We created an interface called <code>IssueIntegrationDriver</code>, which looks like this:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">interface</span><span class="token"> </span><span class="token">IssueIntegrationDriver</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">getType</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">IntegrationType</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">getIssueByUrl</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #8FBCBB;">Project</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">project</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #81A1C1;">string</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">url</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">IntegrationIssueData</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">    
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">createIssueComment</span><span class="token" style="color: #ECEFF4;">(</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #8FBCBB;">Project</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">project</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">int</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">issueId</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">string</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">body</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #81A1C1;">void</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>Each integration will have its driver, so we have a <code>GitHubIntegrationDriver</code> and a <code>JiraIntegrationDriver</code>. The <code>getIssueByUrl</code> method will first parse the string URL given and try to find the issue id (for a GitHub URL like https://github.com/spatie/flareapp.io/issues/481, this would be 481). Then, we'll use the provided Flare project and the Flare team linked to it to fetch the required data for the API (the repository, GitHub owner of the repository, and API tokens) and call the GitHub API to fetch the issue.</p>
<p>Next, a <code>GitHubIntegrationIssueData</code> object is created from the API issue, which saves an <code>ErrorIntegrationIssue</code> model. Lastly, we call <code>createIssueComment</code> and use the URL to the Flare error as a body.</p>
<p>All of this looks like this action:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">class</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">LinkIntegrationIssueWithErrorAction</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">execute</span><span class="token" style="color: #ECEFF4;">(</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #8FBCBB;">Project</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">project</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #8FBCBB;">Error</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">error</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">string</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">url</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">ErrorIntegrationIssue</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">driver</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">project</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">getIssueDriver</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">data</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">driver</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">getIssueByUrl</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">project</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">url</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">        
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">errorIntegrationIssue</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">ErrorIntegrationIssue</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">create</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">[</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">error_id</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">error</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">id</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">integration_type</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">data</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">getType</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">integration_id</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">data</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">getId</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">data</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">data</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">flareUrl</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">ErrorLinksData</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">from</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">error</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">show</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">driver</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">createIssueComment</span><span class="token" style="color: #ECEFF4;">(</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">project</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">errorIntegrationIssue</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">integration_id</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #ECEFF4;">"</span><span class="token" style="color: #A3BE8C;">Issue was linked with Flare error: [</span><span class="token" style="color: #81A1C1;">{</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">error</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">exception_message</span><span class="token" style="color: #81A1C1;">}</span><span class="token" style="color: #A3BE8C;">](</span><span class="token" style="color: #81A1C1;">{</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">flareUrl</span><span class="token" style="color: #81A1C1;">}</span><span class="token" style="color: #A3BE8C;">)</span><span class="token" style="color: #ECEFF4;">"</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">return</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">errorIntegrationIssue</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>This action is tied to a controller, which first checks if there's an issue in the integration for the URL; otherwise, a validation error is shown. The beauty of this approach is that we're done here. This code should not be changed when we want to add another integration. We simply implement the interface, and that's it. The feature works!</p>
<h3>Considerations</h3>
<p>Again, there are a few considerations to be made. First, the drivers will become large when we count line numbers. Our GitHub driver is 360 lines, and our Jira driver is 380. They will grow bigger in the future when we add even more features.</p>
<p>Secondly, we briefly discussed the abstract data structures beside <code>IntegrationIssueData</code>. These are <code>ProjectIntegrationConfigData</code> on a project and two structures on a Flare team called: <code>IntegrationConnectionSecretsData</code> and <code>IntegrationConnectionConfigData</code>. A <code>Project</code> comes in with <code>ProjectIntegrationConfigData</code> in the interface methods. We don't know if it's <code>GitHubProjectIntegrationConfigData</code> or <code>JiraProjectIntegrationConfigData</code>.</p>
<p>This means that for a lot of methods, we'll have the following code:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">createIssueComment</span><span class="token" style="color: #ECEFF4;">(</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #8FBCBB;">Project</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">project</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">int</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">issueId</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">string</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">body</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #81A1C1;">void</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">if</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">!</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">project</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">config</span><span class="token"> </span><span class="token" style="color: #81A1C1;">instanceof</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">JiraProjectIntegrationConfigData</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">throw</span><span class="token"> </span><span class="token" style="color: #81A1C1;">new</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">Exception</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">Invalid config</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">    
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">    </span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> the API calling stuff</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>This code is required to ensure that the correct instances are passed to the methods and adds IDE completion, which is the most significant benefit. It gets even worse when we also need to check <code>IntegrationConnectionSecretsData</code> and <code>IntegrationConnectionConfigData</code>, sometimes adding a whopping three extra checks making our method less readable.</p>
<p>To make the code a bit more readable, we've added some helper methods like this:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">private</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">ensureProjectConfigData</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #8FBCBB;">Project</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">project</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">JiraProjectIntegrationConfigData</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">if</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">!</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">project</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">config</span><span class="token"> </span><span class="token" style="color: #81A1C1;">instanceof</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">JiraProjectIntegrationConfigData</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">throw</span><span class="token"> </span><span class="token" style="color: #81A1C1;">new</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">Exception</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">Invalid config</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">return</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">project</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">config</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>Making the method a bit more readable:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">createIssueComment</span><span class="token" style="color: #ECEFF4;">(</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #8FBCBB;">Project</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">project</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">int</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">issueId</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">string</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">body</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #81A1C1;">void</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">config</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">ensureProjectConfigData</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">project</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">    </span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> the API calling stuff</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>I don't think these two disadvantages weigh up against all the "free" functionality we get by using this abstraction. Adding a new integration is as easy as implementing the interface for the integration, and we're done.</p>
<h2>Processing Webhooks</h2>
<p>Lastly, webhooks. These differ significantly between integrations; they have other methods for verifying the webhook is sent from the integration itself and not some bad actor. The payload of an issue created webhook looks different between each integration. Also, when a webhook is sent, it can differ depending on the integration.</p>
<p>But is this really true? In the end, we can see that there is a common ground for all the integrations; each of these integrations sends an event in these cases:</p>
<ul>
<li>
<p>group created/updated/deleted</p>
</li>
<li>
<p>issue created/updated/deleted</p>
</li>
<li>
<p>issue comment created/updated/deleted</p>
</li>
</ul>
<p>Can we also make abstractions here?</p>
<p>We create a specific webhook controller for each integration. Since these integrations all have specific verification systems, we can't go around this. What we can do is create a generalized <code>IntegrationWebhook</code> model. It keeps track of a few things:</p>
<ul>
<li>
<p>Which connection to an integration on a Flare team does the webhook belong to</p>
</li>
<li>
<p>Possibly the project the webhook is connected to (not relevant to groups)</p>
</li>
<li>
<p>A unique webhook id so that a webhook is only processed once</p>
</li>
<li>
<p>The payload of the webhook request</p>
</li>
<li>
<p>The headers of the webhook request</p>
</li>
</ul>
<p>A webhook controller for GitHub now looks like this:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">class</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">GitHubWebhookController</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">__invoke</span><span class="token" style="color: #ECEFF4;">(</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #8FBCBB;">Request</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">request</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #8FBCBB;">IngestIntegrationWebhookAction</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">ingestIntegrationWebhookAction</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">if</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">isSignatureValid</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">request</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #81A1C1;">===</span><span class="token"> </span><span class="token" style="color: #81A1C1;">false</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">return</span><span class="token"> </span><span class="token" style="color: #88C0D0;">response</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">json</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">message</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">Invalid signature</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">Response</span><span class="token" style="color: #81A1C1;">::</span><span class="token">HTTP_FORBIDDEN</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">payload</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">request</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">all</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">connection</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">IntegrationConnection</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">query</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">where</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">type</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">IntegrationConnectionType</span><span class="token" style="color: #81A1C1;">::</span><span class="token">GitHub</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">where</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">identifier</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">payload</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">installation</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">id</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">first</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">if</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">!</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">connection</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">return</span><span class="token"> </span><span class="token" style="color: #88C0D0;">response</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">json</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">message</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">Connection not found</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #B48EAD;">200</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">message</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">ingestIntegrationWebhookAction</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">execute</span><span class="token" style="color: #ECEFF4;">(</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">connection</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">request</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">request</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">headers</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">get</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">X-GitHub-Delivery</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">payload</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">repository</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">name</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">]</span><span class="token"> </span><span class="token" style="color: #81A1C1;">??</span><span class="token"> </span><span class="token" style="color: #81A1C1;">null</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">return</span><span class="token"> </span><span class="token" style="color: #88C0D0;">response</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">json</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">message</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=&gt;</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">message</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">private</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">isSignatureValid</span><span class="token" style="color: #ECEFF4;">(</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #8FBCBB;">Request</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">request</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #81A1C1;">bool</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">        </span><span class="token" style="color: #616E88;">//</span><span class="token" style="color: #616E88;"> Some code to check the request</span><span class="token" style="color: #616E88;">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>The <code>IngestIntegrationWebhookAction</code> has four parameters:</p>
<ul>
<li>
<p><strong>connection</strong> the connection on the team with the integration (stores all API secrets and config)</p>
</li>
<li>
<p><strong>request</strong> a Laravel request object</p>
</li>
<li>
<p><strong>webhookId</strong> a unique id provided by GitHub so that we don't process the webhook twice</p>
</li>
<li>
<p><strong>integrationGroupId</strong> is a nullable id to a group we use to find the project. In the case of GitHub, this is the repository name</p>
</li>
</ul>
<p>The <code>IngestIntegrationWebhookAction</code> will now create the <code>IntegrationWebhook</code> model or stop if we already have an <code>IntegrationWebhook</code> model with the webhookId due to an earlier webhook being sent.</p>
<p>It tries to find a related project when <code>integrationGroupId</code> is provided and will trigger a job where the webhook will be handled so that the whole request to this controller is quick so that we can send a 200 response to the integration in a matter of milliseconds.</p>
<p>Within the job, the specific <code>IssueIntegrationDriver</code> for the integration based upon the connection is retrieved, and <code>getWebhookHandler</code> is called. This method will return an <code>IntegrationWebhookHandler</code> again an interface so that we'll have a <code>JiraIntegrationWebhookHandler</code> and <code>GitHubIntegrationWebhookHandler</code> and even more when we implement more integrations. These handlers have two tasks: trigger events that will interact with <code>ErrorIntegrationIssue</code> and ensure our system stays in sync with the integration.</p>
<p>When do we need to keep staying in sync? For example, we save the repository's name on the Flare project with the GitHub integration. But, a repository name can be changed in the GitHub UI. So when that happens, all the API requests using the old repository name will fail. That's why we're reacting to the GitHub repository <code>renamed</code> event and update the project. These actions are not required with Jira, where projects are identified by an id that cannot change.</p>
<p>On the other hand, we can still abstract away some code. When an issue is created or updated, it doesn't matter if it happened within GitHub or Jira. We only need to know when the issue was created or updated and with which data. That's why we can often send out an <code>IntegrationEvent</code>, which will be handled by our <code>IntegrationEventHandler</code>. If an issue is created or updated, we will trigger an <code>IntegrationIssueUpdatedOrCreated</code> event.</p>
<p>A simplified version of the <code>GitHubIntegrationWebhookHandler</code> would look like this:</p>
<pre class="phiki language-php nord" data-language="php" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token" style="color: #81A1C1;">class</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">GitHubIntegrationWebhookHandler</span><span class="token"> </span><span class="token" style="color: #81A1C1;">implements</span><span class="token"> </span><span class="token" style="color: #8FBCBB;font-weight: bold;">IntegrationWebhookHandler</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">public</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">handle</span><span class="token" style="color: #ECEFF4;">(</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #8FBCBB;">IntegrationConnection</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">integrationConnection</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">?</span><span class="token" style="color: #8FBCBB;">Project</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">project</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #8FBCBB;">IntegrationWebhook</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">webhook</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">array</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">payload</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #8FBCBB;">HeaderBag</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">headers</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #81A1C1;">void</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">event</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">headers</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">get</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">X-GitHub-Event</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">action</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">payload</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">action</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">if</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">event</span><span class="token"> </span><span class="token" style="color: #81A1C1;">===</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">repository</span><span class="token" style="color: #ECEFF4;">'</span><span class="token"> </span><span class="token" style="color: #81A1C1;">&amp;&amp;</span><span class="token"> </span><span class="token" style="color: #88C0D0;">in_array</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">action</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">opened</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">edited</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">closed</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">reopened</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">assigned</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">unassigned</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token"> </span><span class="token" style="color: #88C0D0;">handleRepositoryRenamed</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">integrationConnection</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">payload</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">headers</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">action</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">return</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">if</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">project</span><span class="token"> </span><span class="token" style="color: #81A1C1;">===</span><span class="token"> </span><span class="token" style="color: #81A1C1;">null</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">throw</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">WebhookNotMatched</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">noProjectFound</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">payload</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">headers</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">if</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #88C0D0;">in_array</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">event</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">issues</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">pull_request</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">            </span><span class="token" style="color: #81A1C1;">&amp;&amp;</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #88C0D0;">in_array</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">action</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">opened</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">edited</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">closed</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">reopened</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">assigned</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;"> </span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">unassigned</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">        </span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #81A1C1;">this</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token"> </span><span class="token" style="color: #88C0D0;">handleIssueUpdatedOrCreated</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">integrationConnection</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">project</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">payload</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">headers</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">action</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">return</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">throw</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">WebhookNotMatched</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">invalidEvent</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">payload</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">headers</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">private</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">handleRepositoryRenamed</span><span class="token" style="color: #ECEFF4;">(</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #8FBCBB;">IntegrationConnection</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">integrationConnection</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">array</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">payload</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">IntegrationGroupUpdated</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #8FBCBB;">Project</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">query</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">where</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">integration_connection_id</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">integrationConnection</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">id</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">where</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">integration_group_id</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">,</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">payload</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">changes</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">repository</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">name</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">from</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">)</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">each</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #8FBCBB;">Project</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">project</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #81A1C1;">use</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">payload</span><span class="token" style="color: #ECEFF4;">)</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">                </span><span class="token" style="color: #616E88;">/**</span><span class="token" style="color: #616E88;"> </span><span class="token" style="color: #81A1C1;">@var</span><span class="token" style="color: #616E88;"> </span><span class="token" style="color: #8FBCBB;">GitHubProjectIntegrationConfigData</span><span class="token" style="color: #616E88;"> $config </span><span class="token" style="color: #616E88;">*/</span><span class="token">
</span></span><span class="line"><span class="token">                </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">config</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">project</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">config</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">                </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">config</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">repository</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">payload</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">repository</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">name</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">                </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">project</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">config</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">config</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">                </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">project</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">integration_group_id</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">payload</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">repository</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #ECEFF4;">[</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #A3BE8C;">name</span><span class="token" style="color: #ECEFF4;">'</span><span class="token" style="color: #ECEFF4;">]</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">                </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">project</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #88C0D0;">save</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">            </span><span class="token" style="color: #ECEFF4;">}</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">return</span><span class="token"> </span><span class="token" style="color: #81A1C1;">new</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">IntegrationGroupUpdated</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">            </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">integrationConnection</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">            </span><span class="token" style="color: #8FBCBB;">GitHubIntegrationGroupData</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">createFromWebhook</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">payload</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">        </span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #81A1C1;">private</span><span class="token"> </span><span class="token" style="color: #81A1C1;">function</span><span class="token"> </span><span class="token" style="color: #88C0D0;">handleIssueUpdatedOrCreated</span><span class="token" style="color: #ECEFF4;">(</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #8FBCBB;">IntegrationConnection</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">integrationConnection</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #8FBCBB;">Project</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">project</span><span class="token" style="color: #ECEFF4;">,</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">array</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">payload</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">:</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">IntegrationIssueUpdatedOrCreated</span><span class="token"> </span><span class="token" style="color: #ECEFF4;">{</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #616E88;">/**</span><span class="token" style="color: #616E88;"> </span><span class="token" style="color: #81A1C1;">@var</span><span class="token" style="color: #616E88;"> </span><span class="token" style="color: #8FBCBB;">GitHubProjectIntegrationConfigData</span><span class="token" style="color: #616E88;"> $config </span><span class="token" style="color: #616E88;">*/</span><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">config</span><span class="token"> </span><span class="token" style="color: #81A1C1;">=</span><span class="token"> </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">project</span><span class="token" style="color: #81A1C1;">-&gt;</span><span class="token" style="color: #D8DEE9;">config</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        </span><span class="token" style="color: #81A1C1;">return</span><span class="token"> </span><span class="token" style="color: #81A1C1;">new</span><span class="token"> </span><span class="token" style="color: #8FBCBB;">IntegrationIssueUpdatedOrCreated</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">            </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">integrationConnection</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">            </span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">project</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">            </span><span class="token" style="color: #8FBCBB;">GitHubIntegrationIssueData</span><span class="token" style="color: #81A1C1;">::</span><span class="token" style="color: #88C0D0;">createFromWebhook</span><span class="token" style="color: #ECEFF4;">(</span><span class="token" style="color: #81A1C1;">$</span><span class="token" style="color: #D8DEE9;">payload</span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #ECEFF4;">,</span><span class="token" style="color: #88C0D0;">
</span></span><span class="line"><span class="token" style="color: #88C0D0;">        </span><span class="token" style="color: #ECEFF4;">)</span><span class="token" style="color: #81A1C1;">;</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span><span class="line"><span class="token" style="color: #ECEFF4;">}</span><span class="token">
</span></span></code></pre>
<p>Lastly, let's quickly look at the <code>IntegrationEventHandler</code>. It will listen to events and react accordingly. For example, when a <code>handleIntegrationGroupDeleted</code>, we'll delete all <code>ErrorIssueIntegration</code> models for that group since it no longer exists. When an <code>IntegrationIssueUpdatedOrCreated</code> is triggered, we start looking for existing <code>ErrorIssueIntegration</code> models and update them for that issue. If there is a link to a Flare error in the created or updated issue title or body, we'll link them together.</p>
<p>We react to even more events in the <code>IntegrationEventHandler</code>, which means we don't have to write this code for each integration, which is excellent! Fixing issues in this handler will also fix issues with each integration instead of fixing each individually.</p>
<h3>Considerations</h3>
<p>Interfaces are great but can be restrictive, becoming a real pain. Our approach works perfectly for Jira and GitHub, but another future integration can require some extra properties, which the current interfaces don't provide. This can be a problem, but we're probably reasonably safe because we're working with a specific set of events and operations (all based on issues). And if changes need to be made in the future, we'll find a way to make it work for all our integrations.</p>
<h2>Closing</h2>
<p>We're almost ready to release this. We've already implemented the Jira integration into this new system and moved the existing GitHub integration. Now we're in the phase of documenting it all and making sure the fronted code works and looks great!</p>
<p>As a bonus, I'm delighted to announce another new feature to Flare. We wanted to add another integration since we wanted to test how waterproof this whole system was. We managed to add GitLab within two days to Flare. So, we'll launch our complete integration revamp soon with Jira, GitHub, and GitLab.</p>
<p>Are there any other integrations with issue trackers you're interested in? Make an issue on our <a href="https://github.com/spatie/flareapp.io-roadmap/discussions">roadmap</a>.</p>
]]>
            </summary>
                                    <updated>2023-09-27T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Why error tracking is crucial for your application]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/why-error-tracking-is-crucial-for-your-application" />
            <id>https://flareapp.io/why-error-tracking-is-crucial-for-your-application</id>
            <author>
                <name><![CDATA[Christoph]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Errors will inevitably occur in your application. There is no way around it. If you notice them while working locally, you can fix them immediately. However, it gets more tricky if your application is already live. In production, you are no longer the only person triggering errors and the most crucial errors might go unnoticed. This is where error tracking software like Flare comes in handy.</p>
<h2>Why</h2>
<p>Before utilizing a new tool, it's always important to ask yourself, "Why do I need this?". So, first, let's discuss the significance of error tracking.</p>
<p><img src="https://assets.flareapp.io/production/blog/f97c33038001eaf151c8b042abd06207/99b05cc4-b2e6-4ef9-a35b-b8febd6eb872.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/f97c33038001eaf151c8b042abd06207/99b05cc4-b2e6-4ef9-a35b-b8febd6eb872.png" width="2542" height="856" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/f97c33038001eaf151c8b042abd06207/99b05cc4-b2e6-4ef9-a35b-b8febd6eb872.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/f97c33038001eaf151c8b042abd06207/99b05cc4-b2e6-4ef9-a35b-b8febd6eb872.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/f97c33038001eaf151c8b042abd06207/99b05cc4-b2e6-4ef9-a35b-b8febd6eb872.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/f97c33038001eaf151c8b042abd06207/99b05cc4-b2e6-4ef9-a35b-b8febd6eb872.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>When an error occurs in production, you will notice the errors in your log files, or a user will tell you about it, right? So why do you need another tool for that? There are two main reasons. First, you'll want to know about these issues as soon as possible: what happened, why did it happen, and do you need to fix it immediately? These are questions you'll want answered in the first couple of minutes after something goes wrong. You don't want to wait for your users to report these issues because chances are, they never will.</p>
<p>The best case scenario is that you get notified about an error that occurred, and you're able to fix it and contact the affected user before they contact you. As a user, I would love that, and it's only possible if you know about your errors and their context sooner rather than later.</p>
<p>That's why we need bug trackers. A bug tracker will tell you about the error no matter who triggers it. It will also show you an overview of occurred errors and more information about them that will help you fix them. Having all production errors displayed in a user-friendly UI makes them easier to handle than searching through log files or depending on users.</p>
<h3>A real life example</h3>
<p>Christoph sells his online courses over a little custom platform. One day, a customer gets in touch about an issue while trying to purchase one of his courses. It turns out Christoph's payment provider changed their webhook calls, which led to an error that went unnoticed.</p>
<p>Unfortunately, this mystery customer was one of many to experience this issue. There have been several before her who didn't mention or report the problem. Without even knowing it, Christoph lost several interested customers because he wasn't aware of the errors in his checkout flow.</p>
<h2>How</h2>
<h3>Introducing Flare, the error tracking tool for Laravel</h3>
<p>Several excellent bug trackers are already available for PHP and JavaScript. Established solutions like Bugsnag, Sentry, and Rollbar offer reliable error tracking. However, Flare stands out due to its tailored Laravel integration. Let's explore some features that make Flare stand out from other bug trackers and discuss how its Laravel integration works wonders for Artisans like you.</p>
<p>Let's start with your <strong>local development environment</strong>. Other error trackers typically only report production errors. However, Flare's PHP client has a beautiful error page to make local development much more enjoyable.</p>
<p><img src="https://assets.flareapp.io/production/blog/f119076e5baaacb479fe21e3ec836ae1/9c2e9594-2496-409d-97a1-725a3af36cc8.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/f119076e5baaacb479fe21e3ec836ae1/9c2e9594-2496-409d-97a1-725a3af36cc8.png" width="3290" height="1510" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/f119076e5baaacb479fe21e3ec836ae1/9c2e9594-2496-409d-97a1-725a3af36cc8.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/f119076e5baaacb479fe21e3ec836ae1/9c2e9594-2496-409d-97a1-725a3af36cc8.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/f119076e5baaacb479fe21e3ec836ae1/9c2e9594-2496-409d-97a1-725a3af36cc8.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/f119076e5baaacb479fe21e3ec836ae1/9c2e9594-2496-409d-97a1-725a3af36cc8.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>Apart from displaying the exception, error message, and stacktrace in a clean and modern UI, it's also packed with additional context. Executed SQL queries and timings are listed, request parameters are revealed, and view data is displayed. Additionally, Ignition has a free-forever share feature to share an error with colleagues in Slack or GitHub quickly. It also offers dark mode, editor links, relevant documentation links, and AI solutions.</p>
<p>If you're using Laravel, you might've noticed that Ignition comes pre-installed in the framework as the default error page, making it even easier to configure an API key and start tracking production errors.</p>
<p>You can think of Flare as the Ignition error page in production. In fact, you'll see the Ignition interface is an important part of the Flare UI:</p>
<p><img src="https://assets.flareapp.io/production/blog/80a412ee80344a4d934c33bfbf893137/145006d7-ab02-4f67-a0b6-345a8e8e6b5c.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/80a412ee80344a4d934c33bfbf893137/145006d7-ab02-4f67-a0b6-345a8e8e6b5c.png" width="1920" height="861" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/80a412ee80344a4d934c33bfbf893137/145006d7-ab02-4f67-a0b6-345a8e8e6b5c.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/80a412ee80344a4d934c33bfbf893137/145006d7-ab02-4f67-a0b6-345a8e8e6b5c.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/80a412ee80344a4d934c33bfbf893137/145006d7-ab02-4f67-a0b6-345a8e8e6b5c.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/80a412ee80344a4d934c33bfbf893137/145006d7-ab02-4f67-a0b6-345a8e8e6b5c.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>Flare takes error handling to the next level. It doesn't just list all reported errors across all your users, but it also provides extensive insights necessary to debug problems. Aside from error tracking, Flare also has a great GitHub integration, extensive notification settings, and project and team organizing options.</p>
<h3>Getting Started with Flare</h3>
<p>Enabling Flare in a Laravel project couldn't be more effortless. It's a 3-step process, and you can get started with a 10-day trial for free. There is no need to provide credit card details.</p>
<p>Once you have an account, let's create a new project. A project holds all the errors for one application's environment. Please give it a name and select the appropriate environment and technology. Next, you will see instructions on how to connect your application to this new project:</p>
<p><img src="https://assets.flareapp.io/production/blog/87361d161400882cadfe9a616f6b5d33/f0115f90-8ded-4b81-b4f0-81bb60cd87b4.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/87361d161400882cadfe9a616f6b5d33/f0115f90-8ded-4b81-b4f0-81bb60cd87b4.jpg" width="3510" height="1318" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/87361d161400882cadfe9a616f6b5d33/f0115f90-8ded-4b81-b4f0-81bb60cd87b4.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/87361d161400882cadfe9a616f6b5d33/f0115f90-8ded-4b81-b4f0-81bb60cd87b4.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/87361d161400882cadfe9a616f6b5d33/f0115f90-8ded-4b81-b4f0-81bb60cd87b4.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/87361d161400882cadfe9a616f6b5d33/f0115f90-8ded-4b81-b4f0-81bb60cd87b4.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>It boils down to:</p>
<ol>
<li>
<p>Install Ignition, the Flare client, and pretty error page</p>
</li>
<li>
<p>Configure your API key</p>
</li>
<li>
<p>Configure Laravel to send errors to Flare</p>
</li>
</ol>
<p>Afterward, you can use the "php artisan flare:test" command to test the integration. If set up correctly, it will send a test exception to Flare, which will show up in your project seconds later.</p>
<p>Congratulations, you are now tracking your application's errors. This was the most critical step toward a better error-handling future.</p>
<h3>Developer Experience</h3>
<p>Design and UI are essential to give your users a great experience. In the same way, good documentation and a well-coded client package are essential to give developers a great developer experience (DX).</p>
<p>Fortunately, the Spatie team working on Flare has several years and 300 open-source projects of experience creating software with fantastic developer experience.</p>
<p>Another example of good DX are the various filters and insights available for errors. Every time we use Flare to debug a problem, we look for insights or filters that would make the process easier:</p>
<p><img src="https://assets.flareapp.io/production/blog/0a0dab44f2805ec1c53cafdd6666ca46/bbccd702-1727-4d7c-aeea-36703c3fe5fd.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/0a0dab44f2805ec1c53cafdd6666ca46/bbccd702-1727-4d7c-aeea-36703c3fe5fd.png" width="3288" height="1448" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/0a0dab44f2805ec1c53cafdd6666ca46/bbccd702-1727-4d7c-aeea-36703c3fe5fd.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/0a0dab44f2805ec1c53cafdd6666ca46/bbccd702-1727-4d7c-aeea-36703c3fe5fd.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/0a0dab44f2805ec1c53cafdd6666ca46/bbccd702-1727-4d7c-aeea-36703c3fe5fd.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/0a0dab44f2805ec1c53cafdd6666ca46/bbccd702-1727-4d7c-aeea-36703c3fe5fd.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<h3>Introducing solutions</h3>
<p>Ignition and Flare can recognize and fix several commonly occurring errors. If you've ever lost an hour debugging a missing semi-colon or a typo, you'll be glad to learn Flare will automatically pick up on this and suggest a fix. When debugging locally using Ignition, you can even execute these solutions directly from the error page and solve the issue without ever leaving the browser.</p>
<p>Your browser does not support the video tag.</p>
<p>Recently, we've also added opt-in AI solutions using OpenAI's APIs. Every Flare subscription comes with a free daily quota of AI solutions. The future is now 🤖.</p>
<p><img src="https://assets.flareapp.io/production/blog/c7365795e227354d3b7ea471f30e2c81/58eb980a-1f55-4645-8f90-b8895f554757.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/c7365795e227354d3b7ea471f30e2c81/58eb980a-1f55-4645-8f90-b8895f554757.png" width="2996" height="618" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/c7365795e227354d3b7ea471f30e2c81/58eb980a-1f55-4645-8f90-b8895f554757.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/c7365795e227354d3b7ea471f30e2c81/58eb980a-1f55-4645-8f90-b8895f554757.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/c7365795e227354d3b7ea471f30e2c81/58eb980a-1f55-4645-8f90-b8895f554757.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/c7365795e227354d3b7ea471f30e2c81/58eb980a-1f55-4645-8f90-b8895f554757.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<h3>GitHub integration</h3>
<p>Flare includes a stunning GitHub integration. Once enabled, you can link a Flare project to a GitHub repository. This way, you can link incoming errors to GitHub issues and have the issue automatically close when the error is resolved.</p>
<p><img src="https://assets.flareapp.io/production/blog/f45599da5698af6f33196af88533314c/93aacb55-564f-4dd0-b909-9f41418f9340.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/f45599da5698af6f33196af88533314c/93aacb55-564f-4dd0-b909-9f41418f9340.png" width="1692" height="724" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/f45599da5698af6f33196af88533314c/93aacb55-564f-4dd0-b909-9f41418f9340.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/f45599da5698af6f33196af88533314c/93aacb55-564f-4dd0-b909-9f41418f9340.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/f45599da5698af6f33196af88533314c/93aacb55-564f-4dd0-b909-9f41418f9340.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/f45599da5698af6f33196af88533314c/93aacb55-564f-4dd0-b909-9f41418f9340.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>The GitHub integration also works the other way around. If a Flare URL is detected in a GitHub issue, we'll automatically link the issue to the relevant Flare error. If you work with GitHub a lot, this greatly enhances your workflow and opens up new possibilities for project management.</p>
<p>Do you love project management, but don't like GitHub? We're also working on a Jira integration, which should land in the coming few weeks.</p>
<h3>Identifying users</h3>
<p>When a severe error occurs in your application, you might want to notify the affected users proactively. However, it is usually challenging to identify the affected users from endless log files or traditional error trackers. That's why Flare has you covered by automatically detecting the logged-in user and providing exportable insights about affected users for each error.</p>
<p><img src="https://assets.flareapp.io/production/blog/d77a6424b4d6d85a78c0716405af1f92/07c1236e-fa92-4491-99a3-b710c9558204.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/d77a6424b4d6d85a78c0716405af1f92/07c1236e-fa92-4491-99a3-b710c9558204.png" width="2112" height="724" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/d77a6424b4d6d85a78c0716405af1f92/07c1236e-fa92-4491-99a3-b710c9558204.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/d77a6424b4d6d85a78c0716405af1f92/07c1236e-fa92-4491-99a3-b710c9558204.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/d77a6424b4d6d85a78c0716405af1f92/07c1236e-fa92-4491-99a3-b710c9558204.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/d77a6424b4d6d85a78c0716405af1f92/07c1236e-fa92-4491-99a3-b710c9558204.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>Additionally, the insight of affected users makes it even easier to find a user to impersonate locally to debug the problem.</p>
<h3>Advanced notifications settings</h3>
<p>We already mentioned how important it is to be notified of new errors in your application. Flare has an extensive notification system that lets you customize notifications to your personal needs and the needs of your team.</p>
<p>First, it provides several different notification channels out of the box: mail, Slack, SMS, Telegram, Discord, and Microsoft Teams. Just pick what suits you best. Need something specific? The webhook channel allows for endless customizability.</p>
<p>For each channel, you can customize which notifications you want to receive. For example, it's easy to configure a global notification channel to send a notification about the first occurrence of every error to a Slack channel. But if this error happens the tenth time, you may need to get someone out of bed by texting them.</p>
<p><img src="https://assets.flareapp.io/production/blog/b0b97c271f76caab617b5b46f78fad63/dec88349-524f-441e-a64a-8cbf025c8fa0.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/b0b97c271f76caab617b5b46f78fad63/dec88349-524f-441e-a64a-8cbf025c8fa0.png" width="2300" height="1886" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/b0b97c271f76caab617b5b46f78fad63/dec88349-524f-441e-a64a-8cbf025c8fa0.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/b0b97c271f76caab617b5b46f78fad63/dec88349-524f-441e-a64a-8cbf025c8fa0.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/b0b97c271f76caab617b5b46f78fad63/dec88349-524f-441e-a64a-8cbf025c8fa0.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/b0b97c271f76caab617b5b46f78fad63/dec88349-524f-441e-a64a-8cbf025c8fa0.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>Customizing notifications to your personal or your team's needs is vital. Having advanced options lets you ensure no one misses a notification for that critical project and no one gets overwhelmed with notification overload.</p>
<h2>Conclusion</h2>
<p>Bug tracking is a crucial part of building and maintaining any application. Not getting notified about occurring errors can cost you a lot of time and money. Flare is here to help. You don't have to worry about possible problems anymore because Flare will notify you immediately, and its specific focus on Laravel will make debugging even more straightforward.</p>
<p>It is never too late to start tracking errors. <a href="https://flareapp.io/register">Give Flare a try now</a>.</p>
]]>
            </summary>
                                    <updated>2023-09-11T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[New teams will now see an example project]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/new-teams-will-now-see-an-example-project" />
            <id>https://flareapp.io/new-teams-will-now-see-an-example-project</id>
            <author>
                <name><![CDATA[Freek]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>When people first create their Flare account (or create a new team within their account), they are greeted with instructions on how to send a first error to Flare. They would only see what an error in Flare looked like once they'd sent one themselves.</p>
<p>For people new to Flare, we wanted to make it easier to immediately see what errors look like and showcase some of the cool things we do when rendering particular errors. That's why new accounts and teams will now see an example project added to their account.</p>
<p><img src="https://assets.flareapp.io/production/blog/c1ffb070735edc83eaa6c9de84f561a1/b9e35615-7eae-4d21-8810-ea3c7ee1f7bf.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/c1ffb070735edc83eaa6c9de84f561a1/b9e35615-7eae-4d21-8810-ea3c7ee1f7bf.jpg" width="1792" height="350" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/c1ffb070735edc83eaa6c9de84f561a1/b9e35615-7eae-4d21-8810-ea3c7ee1f7bf.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/c1ffb070735edc83eaa6c9de84f561a1/b9e35615-7eae-4d21-8810-ea3c7ee1f7bf.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/c1ffb070735edc83eaa6c9de84f561a1/b9e35615-7eae-4d21-8810-ea3c7ee1f7bf.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/c1ffb070735edc83eaa6c9de84f561a1/b9e35615-7eae-4d21-8810-ea3c7ee1f7bf.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>This project contains some nice example errors, like this one where we have code highlighting for the SQL query.</p>
<p><img src="https://assets.flareapp.io/production/blog/cfe51d11772b01a024845cd09eb27370/b2b3792c-5316-4bdb-93da-0fbca4afe893.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/cfe51d11772b01a024845cd09eb27370/b2b3792c-5316-4bdb-93da-0fbca4afe893.jpg" width="1818" height="732" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/cfe51d11772b01a024845cd09eb27370/b2b3792c-5316-4bdb-93da-0fbca4afe893.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/cfe51d11772b01a024845cd09eb27370/b2b3792c-5316-4bdb-93da-0fbca4afe893.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/cfe51d11772b01a024845cd09eb27370/b2b3792c-5316-4bdb-93da-0fbca4afe893.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/cfe51d11772b01a024845cd09eb27370/b2b3792c-5316-4bdb-93da-0fbca4afe893.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>There's also an example error where we show the list of URLs where it occurred (all fictional URLs for this example).</p>
<p><img src="https://assets.flareapp.io/production/blog/af3f237f806a6424f8d3de09d01d7b82/00be27de-04bb-4cef-9a1d-499290b77532.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/af3f237f806a6424f8d3de09d01d7b82/00be27de-04bb-4cef-9a1d-499290b77532.jpg" width="1692" height="1472" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/af3f237f806a6424f8d3de09d01d7b82/00be27de-04bb-4cef-9a1d-499290b77532.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/af3f237f806a6424f8d3de09d01d7b82/00be27de-04bb-4cef-9a1d-499290b77532.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/af3f237f806a6424f8d3de09d01d7b82/00be27de-04bb-4cef-9a1d-499290b77532.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/af3f237f806a6424f8d3de09d01d7b82/00be27de-04bb-4cef-9a1d-499290b77532.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>And, of course, we also show an error that uses one of our defining features: solutions.</p>
<p><img src="https://assets.flareapp.io/production/blog/4cab4ce94f3ad42fd7f8c30e996eea42/887e38e1-3522-4e06-a4ca-934217a11da1.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/4cab4ce94f3ad42fd7f8c30e996eea42/887e38e1-3522-4e06-a4ca-934217a11da1.jpg" width="2016" height="750" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/4cab4ce94f3ad42fd7f8c30e996eea42/887e38e1-3522-4e06-a4ca-934217a11da1.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/4cab4ce94f3ad42fd7f8c30e996eea42/887e38e1-3522-4e06-a4ca-934217a11da1.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/4cab4ce94f3ad42fd7f8c30e996eea42/887e38e1-3522-4e06-a4ca-934217a11da1.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/4cab4ce94f3ad42fd7f8c30e996eea42/887e38e1-3522-4e06-a4ca-934217a11da1.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>We hope this example project makes it easier for people to see that Flare can beautifully display all errors.</p>
]]>
            </summary>
                                    <updated>2023-08-24T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Unveiling error notes and the activity feed]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/unveiling-error-notes-and-the-activity-feed" />
            <id>https://flareapp.io/unveiling-error-notes-and-the-activity-feed</id>
            <author>
                <name><![CDATA[Ruben]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Today we're introducing the error activity feed. It shows you what happened with an error during its lifetime within Flare. When it was resolved, snoozed or reoccurred.</p>
<p><img src="https://assets.flareapp.io/production/blog/6983060a354c2b2f2e1409184ef81b3d/07331df1-62f8-42f0-9e09-374304b99d62.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/6983060a354c2b2f2e1409184ef81b3d/07331df1-62f8-42f0-9e09-374304b99d62.png" width="756" height="442" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/6983060a354c2b2f2e1409184ef81b3d/07331df1-62f8-42f0-9e09-374304b99d62.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=756/https://assets.flareapp.io/production/blog/6983060a354c2b2f2e1409184ef81b3d/07331df1-62f8-42f0-9e09-374304b99d62.png 756w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>As developers, we know that two heads are better than one, especially when it comes to resolving complex issues. Enter Flare notes your secret weapon for seamless collaboration during error resolution.</p>
<p>On every error in Flare you can now leave notes directly. Sharing insights and hypotheses with your team in real-time. Team members can jump in, add their thoughts, and engage in discussions effortlessly.</p>
<p>You'll find the new notes section by clicking "show activity" on an error.</p>
<p><img src="https://assets.flareapp.io/production/blog/af49eb0915837f26d8cca131d1170b77/7abe02b4-1865-44b7-bbea-972d87a7c68c.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/af49eb0915837f26d8cca131d1170b77/7abe02b4-1865-44b7-bbea-972d87a7c68c.png" width="754" height="994" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/af49eb0915837f26d8cca131d1170b77/7abe02b4-1865-44b7-bbea-972d87a7c68c.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=754/https://assets.flareapp.io/production/blog/af49eb0915837f26d8cca131d1170b77/7abe02b4-1865-44b7-bbea-972d87a7c68c.png 754w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>From now on when you're executing an action on an error (resolving, unresolving, snoozing and unsnoozing) then Flare will show you a new dialog where you can now leave a note behind with the reason for executing that action.</p>
<p><img src="https://assets.flareapp.io/production/blog/36f5da4feab67c2de465a20b0c30c109/9f41f5e2-389e-47e9-ac2b-9baa052c2f0c.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/36f5da4feab67c2de465a20b0c30c109/9f41f5e2-389e-47e9-ac2b-9baa052c2f0c.png" width="1344" height="1104" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/36f5da4feab67c2de465a20b0c30c109/9f41f5e2-389e-47e9-ac2b-9baa052c2f0c.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/36f5da4feab67c2de465a20b0c30c109/9f41f5e2-389e-47e9-ac2b-9baa052c2f0c.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/36f5da4feab67c2de465a20b0c30c109/9f41f5e2-389e-47e9-ac2b-9baa052c2f0c.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1344/https://assets.flareapp.io/production/blog/36f5da4feab67c2de465a20b0c30c109/9f41f5e2-389e-47e9-ac2b-9baa052c2f0c.png 1344w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>These notes will all be shown on the activity feed which will provide more information on the why certains actions happened.</p>
<p>Happy error hunting!</p>
]]>
            </summary>
                                    <updated>2023-08-11T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Web animation wizardry]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/web-animation-wizardry" />
            <id>https://flareapp.io/web-animation-wizardry</id>
            <author>
                <name><![CDATA[Sam]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Picture this: a bunch of developers, most of them back-enders, are given a Figma design file to turn into the <a href="https://www.flareapp.io">new homepage for Flare</a>. We set out to build our brand-new marketing site with a sprinkle of animations. Here is how we crafted them.</p>
<blockquote>
<p>If you have not seen the new homepage, check it out before reading this. Try scrolling slowly and quickly to catch all of the little details. It's worth it, I promise.</p>
</blockquote>
<h2>Sticky navbar</h2>
<p>The design files had a different state for the navigation menu for when the page is not scrolled yet and when it is scrolled. To transition between them, we used a custom hook to determine when the page is scrolled to transition the width of a wrapper div from 100% to 0%. To prevent the menu from actually going to 0% we set a <code>min-width: max-content</code>, which scales down the container to the size of the actual content.
– Don't you love CSS?</p>
<h2>Error card carousel</h2>
<p>We went through a lot of iterations on this one, but I feel like we landed on a nice and easy 3-step solution.</p>
<h3>1. Positioning</h3>
<p>Each card is positioned at the same position by using a single-cell grid layout. This prevents layout shifts because the wrapper will be sized based on the largest card.</p>
<p>Tailwind classed to achieve a single-cell grid layout:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">&lt;div class="grid grid-cols-1 grid-rows-1"&gt;
</span></span><span class="line"><span class="token">		&lt;article class="row-start-1 col-start-1"&gt;Card 1&lt;/article&gt;
</span></span><span class="line"><span class="token">		&lt;article class="row-start-1 col-start-1"&gt;Card 2&lt;/article&gt;
</span></span><span class="line"><span class="token">		&lt;!-- ... --&gt;
</span></span><span class="line"><span class="token">&lt;/div&gt;
</span></span></code></pre>
<h3>2. Offset</h3>
<p>CSS variables are used to make the offsets configurable.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">.cardstack {
</span></span><span class="line"><span class="token">  --card-offset: 2rem;
</span></span><span class="line"><span class="token">  --scale-factor: 0.07;
</span></span><span class="line"><span class="token">  --opacity-factor: 0.23;
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>Each card is scaled and translated based on its index in the stack.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">.cardstack &gt; article {
</span></span><span class="line"><span class="token">  will-change: transform, opacity;
</span></span><span class="line"><span class="token">  transform: translateY(calc(var(--index) * -1 * var(--card-offset)))
</span></span><span class="line"><span class="token">    scale(calc(1 - var(--index) * var(--scale-factor)));
</span></span><span class="line"><span class="token">  opacity: calc(1 - var(--index) * var(--opacity-factor));
</span></span><span class="line"><span class="token">  transition: transform 0.5s ease-in-out, opacity 0.5s ease-in-out;
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>The index can be tracked through the <code>:nth-child()</code> selector, combined with a preprocessor like sass this can be done with a simple for loop.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">@for $i from 1 through 5 {
</span></span><span class="line"><span class="token">  .cardstack &gt; article:nth-child(#{6 - $i}) {
</span></span><span class="line"><span class="token">    --index: #{$i};
</span></span><span class="line"><span class="token">  }
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>Alternatively, you can write it out by hand:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">.cardstack &gt; article:nth-child(1) { --index: 5 } /* backmost card */
</span></span><span class="line"><span class="token">.cardstack &gt; article:nth-child(2) { --index: 4 }
</span></span><span class="line"><span class="token">.cardstack &gt; article:nth-child(3) { --index: 3 }
</span></span><span class="line"><span class="token">.cardstack &gt; article:nth-child(4) { --index: 2 }
</span></span><span class="line"><span class="token">.cardstack &gt; article:nth-child(5) { --index: 1 }
</span></span><span class="line"><span class="token">.cardstack &gt; article:nth-child(6) { --index: 0 } /* topmost card */
</span></span></code></pre>
<h3>3. Rotate the stack</h3>
<p>The topmost card (last in the list) is added to the back of the stack (top of the list). The trick here is to make the React component key follow each card. This prevents the cards from unmounting and allows the cards to transition smoothly.</p>
<h2>Epic screenshot</h2>
<p>Arguably the most epic transition on this page is the screenshot reveal.
This animation can be broken down into four steps.</p>
<h3>1. Track the scroll position</h3>
<p><a href="https://www.framer.com/motion/">Framer-motion</a> provides a <code>useScroll</code> hook that can track the vertical scroll progress of the screenshot in the viewport. Setting the <code>offset</code> option to <code>["start start", "start end"]</code> maps the scroll position from the start of entering the viewport to when the element is entirely inside of the viewport.</p>
<h3>2. Smoothly map the progress to transformations</h3>
<p>Framer Motion handles binding the translation and rotation of the element to the vertical scroll progress from step 1.
First, we use the <code>useTransform</code> hook to map the vertical scroll progress from an input range to an output range. Then the <code>useSpring</code> hook handles smoothly snapping the values to the actual scroll position.</p>
<h3>3. Transform in 3D</h3>
<p>To achieve the final effect, we set the css <code>perspective</code> property to move the user (you) away from the z0 plane to give the 3D-positioned element perspective. The transform origin is set to the bottom of the element to make it smoothly transition from moving towards z0. Now the screenshot is ready for z-translation and x-rotation.</p>
<h3>4. Blur</h3>
<p>The same Framer Motion hooks bind the blur value to the element. The input range is shortened to make it readable earlier in the animation.</p>
<h2>Retrospective</h2>
<p>Getting these animations to perform well for a marketing site was challenging. On our way, we ran into layout shifts, browser inconsistencies regarding positioning and scroll tracking, performance problems due to background blurs and complex SVGs, and many more papercuts that slowed us down.
We hoped Framer Motion would be a drop-in fix for all of these problems, but using regular CSS ended up being easier to develop and debug.
Ultimately, we came up with solutions for these challenges and learned a lot. I hope you learned something too.</p>
]]>
            </summary>
                                    <updated>2023-07-31T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[You can now log in using your Google or GitHub account]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/you-can-now-log-in-using-your-google-or-github-account" />
            <id>https://flareapp.io/you-can-now-log-in-using-your-google-or-github-account</id>
            <author>
                <name><![CDATA[Rias]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>You can now register and log in to Flare using your Google or GitHub account.</p>
<p>You'll see these two new buttons on the register and login page.</p>
<p><img src="https://assets.flareapp.io/production/blog/5dff5a9a628dbde9afa96c31ba671a16/7fa9c5f8-ac02-4783-9eb2-d54597859b71.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/5dff5a9a628dbde9afa96c31ba671a16/7fa9c5f8-ac02-4783-9eb2-d54597859b71.jpg" width="1518" height="832" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/5dff5a9a628dbde9afa96c31ba671a16/7fa9c5f8-ac02-4783-9eb2-d54597859b71.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/5dff5a9a628dbde9afa96c31ba671a16/7fa9c5f8-ac02-4783-9eb2-d54597859b71.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/5dff5a9a628dbde9afa96c31ba671a16/7fa9c5f8-ac02-4783-9eb2-d54597859b71.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1518/https://assets.flareapp.io/production/blog/5dff5a9a628dbde9afa96c31ba671a16/7fa9c5f8-ac02-4783-9eb2-d54597859b71.jpg 1518w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>You can log in to Flare using these buttons with a single click. This is way faster than the traditional email / password flow (where we'll also need to verify your email).</p>
<p>In this blog post, I'd like to show you how this feature works behind the scenes. Spoiler: <a href="https://laravel.com/docs/10.x/socialite">Socialite</a> makes this a breeze.</p>
<h2>Implementing social login in a Laravel app</h2>
<p>Google and GitHub offer login functionalities through what is called the OAuth flow. This flow will redirect the user from Flare to a login page on Google/GitHub, and after the user has logged in there, Google/GitHub will redirect the user back to Flare, which contains information about the logged-in user. That's the simplified version.</p>
<p><img src="https://assets.flareapp.io/production/blog/17a211daacc102aad643171fe7f84450/46f5c854-cf6c-4317-bef4-60d7e9ddac59.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/17a211daacc102aad643171fe7f84450/46f5c854-cf6c-4317-bef4-60d7e9ddac59.jpg" width="1088" height="604" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/17a211daacc102aad643171fe7f84450/46f5c854-cf6c-4317-bef4-60d7e9ddac59.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/17a211daacc102aad643171fe7f84450/46f5c854-cf6c-4317-bef4-60d7e9ddac59.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/17a211daacc102aad643171fe7f84450/46f5c854-cf6c-4317-bef4-60d7e9ddac59.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1088/https://assets.flareapp.io/production/blog/17a211daacc102aad643171fe7f84450/46f5c854-cf6c-4317-bef4-60d7e9ddac59.jpg 1088w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>Behind the scenes, Flare is a Laravel app. Let's review the steps we took to add a social login to Flare.</p>
<p>Both Google and GitHub will send ids and tokens around the user the will be logged in. We added some columns in our database to hold that info.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">use Illuminate\Database\Migrations\Migration;
</span></span><span class="line"><span class="token">use Illuminate\Database\Schema\Blueprint;
</span></span><span class="line"><span class="token">use Illuminate\Support\Facades\Schema;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">return new class () extends Migration {
</span></span><span class="line"><span class="token">    public function up()
</span></span><span class="line"><span class="token">    {
</span></span><span class="line"><span class="token">        Schema::table('users', function (Blueprint $table) {
</span></span><span class="line"><span class="token">            $table-&gt;after('remember_token', function (Blueprint $table) {
</span></span><span class="line"><span class="token">                $table-&gt;string('github_id')-&gt;nullable()-&gt;index();
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">                $table-&gt;string('google_id')-&gt;nullable()-&gt;index();
</span></span><span class="line"><span class="token">            });
</span></span><span class="line"><span class="token">        });
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">};
</span></span></code></pre>
<p>Laravel has a great first-party package called <a href="https://laravel.com/docs/10.x/socialite">Socialite</a> that can handle logins using the OAuth flow.</p>
<p>You can install it using Composer:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">composer require laravel/socialite
</span></span></code></pre>
<p>Next up, you should create an OAuth application at Google and/or GitHub that will provide you with a <code>client_id</code> and <code>client_secret.</code> Explaining how you can create this OAuth application is beyond the scope of this post, but you'll probably find it quickly with some Googling.</p>
<p>In the <code>config/services.php</code> file, we added these keys.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">return [
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">		// other values...	
</span></span><span class="line"><span class="token">		
</span></span><span class="line"><span class="token">    'github' =&gt; [
</span></span><span class="line"><span class="token">        'client_id' =&gt; env('GITHUB_CLIENT_ID'),
</span></span><span class="line"><span class="token">        'client_secret' =&gt; env('GITHUB_CLIENT_SECRET'),
</span></span><span class="line"><span class="token">        'redirect' =&gt; env('APP_URL') . '/auth/callback/github',
</span></span><span class="line"><span class="token">    ],
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    'google' =&gt; [
</span></span><span class="line"><span class="token">        'client_id' =&gt; env('GOOGLE_CLIENT_ID'),
</span></span><span class="line"><span class="token">        'client_secret' =&gt; env('GOOGLE_CLIENT_SECRET'),
</span></span><span class="line"><span class="token">        'redirect' =&gt; env('APP_URL') . '/auth/callback/google',
</span></span><span class="line"><span class="token">    ],
</span></span><span class="line"><span class="token">];
</span></span></code></pre>
<p>Of course, you should also set the client id and secret as <code>.env</code> variables; use the names mentioned in the config file above.</p>
<p>Next, let's add two routes to handle the social login.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">use App\Http\Auth\SocialiteController;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">// in a routes file
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">Route::get('auth/redirect/{driver}', [SocialiteController::class, 'redirect'])-&gt;name('socialite');
</span></span><span class="line"><span class="token">Route::get('auth/callback/{driver}', [SocialiteController::class, 'callback']);
</span></span></code></pre>
<p>When a user visits the first route, the user will be redirected to Google/GitHub. The second route will be hit when Google/GitHub redirects back to Flare. If the request contains the correct information, we'll log in to the user.</p>
<p>Let's take a look at the implementation of the
<code>SocialiteController</code> that handles requests to both these routes.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">namespace App\Http\Auth;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">use App\Domain\Team\Actions\FindOrCreateUserForSocialiteAction;
</span></span><span class="line"><span class="token">use App\Domain\Team\Enums\SocialiteDriver;
</span></span><span class="line"><span class="token">use Illuminate\Http\RedirectResponse;
</span></span><span class="line"><span class="token">use Illuminate\Support\Facades\Auth;
</span></span><span class="line"><span class="token">use Laravel\Socialite\Facades\Socialite;
</span></span><span class="line"><span class="token">use Laravel\Socialite\Two\InvalidStateException;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">class SocialiteController
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">    public function redirect(string $driver): RedirectResponse
</span></span><span class="line"><span class="token">    {
</span></span><span class="line"><span class="token">        abort_if(!in_array($driver, ['github', 'google']), 404);
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        /** @var \Laravel\Socialite\Two\GoogleProvider|\Laravel\Socialite\Two\GithubProvider $socialite */
</span></span><span class="line"><span class="token">        $socialite = Socialite::driver($driver);
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        return match ($driver) {
</span></span><span class="line"><span class="token">            'github' =&gt; $socialite-&gt;scopes(['read:user', 'user:email'])-&gt;redirect(),
</span></span><span class="line"><span class="token">            'google' =&gt; $socialite-&gt;redirect(),
</span></span><span class="line"><span class="token">        };
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">    
</span></span><span class="line"><span class="token">    public function callback(string $driver): RedirectResponse
</span></span><span class="line"><span class="token">    {
</span></span><span class="line"><span class="token">	     // implementation removed for brevity...
</span></span><span class="line"><span class="token">     }
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>So in the <code>redirect</code> function of the controller, we will redirect to Google/GitHub. Socialite will build up the correct URL. Nice!</p>
<p>Next, let's look at the implementation of <code>callback</code>, which will be called when Google/GitHub redirects back.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">&lt;?php
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">namespace App\Http\Front\Controllers;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">use App\Domain\Team\Actions\CreateUserAction;
</span></span><span class="line"><span class="token">use App\Domain\Team\Models\User;
</span></span><span class="line"><span class="token">use Illuminate\Database\Eloquent\Builder;
</span></span><span class="line"><span class="token">use Illuminate\Http\RedirectResponse;
</span></span><span class="line"><span class="token">use Illuminate\Support\Facades\Auth;
</span></span><span class="line"><span class="token">use Illuminate\Support\Str;
</span></span><span class="line"><span class="token">use Laravel\Socialite\Contracts\User as SocialiteUser;
</span></span><span class="line"><span class="token">use Laravel\Socialite\Facades\Socialite;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">class SocialiteController
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">    public function redirect(string $driver): RedirectResponse
</span></span><span class="line"><span class="token">    {
</span></span><span class="line"><span class="token">				// removed for brevity        
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    public function callback(string $driver): RedirectResponse
</span></span><span class="line"><span class="token">    {
</span></span><span class="line"><span class="token">        abort_if(! in_array($driver, ['github', 'google']), 404);
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        $socialiteUser = Socialite::driver($driver)-&gt;user();
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        $user = $this-&gt;findUserByKeyOrEmail($driver, $socialiteUser);
</span></span><span class="line"><span class="token">        $user ??= $this-&gt;createUser($driver, $socialiteUser);
</span></span><span class="line"><span class="token">        $this-&gt;updateUser($driver, $user, $socialiteUser);
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        Auth::login($user, remember: true);
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        return redirect()-&gt;route('projects.index');
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    // other functions removed for brevity, well get 
</span></span><span class="line"><span class="token">    // to those.
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>In the above code, you see that we're going to get to the information that Google/GitHub provided by calling <code>Socialite::driver($driver)-&gt;user()</code>. This object contains the email and 3rd party user id of the user at the external service. We will use that information to look up the user with the same email or 3rd party user id in our local data pass. If such a user doesn't exist, well create it. After that, we'll log in the user and redirect to the <code>projects.index</code> route where all Flare projects for that user are shown.</p>
<p>To finish off, let's look at the implementations of the <code>createUser</code> and <code>updateUser</code> functions.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">&lt;?php
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">namespace App\Http\Front\Controllers;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">use App\Domain\Team\Actions\CreateUserAction;
</span></span><span class="line"><span class="token">use App\Domain\Team\Models\User;
</span></span><span class="line"><span class="token">use Illuminate\Database\Eloquent\Builder;
</span></span><span class="line"><span class="token">use Illuminate\Http\RedirectResponse;
</span></span><span class="line"><span class="token">use Illuminate\Support\Facades\Auth;
</span></span><span class="line"><span class="token">use Illuminate\Support\Str;
</span></span><span class="line"><span class="token">use Laravel\Socialite\Contracts\User as SocialiteUser;
</span></span><span class="line"><span class="token">use Laravel\Socialite\Facades\Socialite;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">class SocialiteController
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">    // other functions
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    protected function findUserByKeyOrEmail(string $driver, SocialiteUser $user): ?User
</span></span><span class="line"><span class="token">    {
</span></span><span class="line"><span class="token">        return User::where("{$driver}_id", $user-&gt;getId())
</span></span><span class="line"><span class="token">            -&gt;orWhere(function (Builder $query) use ($user) {
</span></span><span class="line"><span class="token">                $query-&gt;where('email', $user-&gt;getEmail())-&gt;whereNotNull('email_verified_at');
</span></span><span class="line"><span class="token">            })
</span></span><span class="line"><span class="token">            -&gt;first();
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    protected function createUser(string $driver, SocialiteUser $user): User
</span></span><span class="line"><span class="token">    {
</span></span><span class="line"><span class="token">        return app(CreateUserAction::class)-&gt;execute(
</span></span><span class="line"><span class="token">            name: $user-&gt;getName(),
</span></span><span class="line"><span class="token">            email: $user-&gt;getEmail(),
</span></span><span class="line"><span class="token">            password: Str::random(64),
</span></span><span class="line"><span class="token">            attributes: [
</span></span><span class="line"><span class="token">                "{$driver}_id" =&gt; $user-&gt;getId(),
</span></span><span class="line"><span class="token">                'email_verified_at' =&gt; now(),
</span></span><span class="line"><span class="token">            ]
</span></span><span class="line"><span class="token">        );
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    protected function updateUser(string $driver, User $user, SocialiteUser $socialiteUser): bool
</span></span><span class="line"><span class="token">    {
</span></span><span class="line"><span class="token">        $user-&gt;fill([
</span></span><span class="line"><span class="token">            "{$driver}_id" =&gt; $socialiteUser-&gt;getId(),
</span></span><span class="line"><span class="token">        ]);
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        if (! $user-&gt;email_verified_at &amp;&amp; $user-&gt;email === $socialiteUser-&gt;getEmail()) {
</span></span><span class="line"><span class="token">            $user-&gt;email_verified_at = now();
</span></span><span class="line"><span class="token">        }
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        return $user-&gt;save();
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>The above code should be pretty straightforward.</p>
<h2>In closing</h2>
<p>In this post, you learned that adding a social login in a Laravel app is pretty easy. <a href="https://laravel.com/docs/10.x/socialite">Socialite</a> does the heavy lifting for you.</p>
<p>In our sister service, Oh Dear, we see that about half of the log ins and registrations now happen via Google. People seem to like that they can now log in using a single click. Adding this to your app might be worthwhile as well.</p>
]]>
            </summary>
                                    <updated>2023-07-24T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Managing production environment variables for Laravel deployments]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/managing-production-environment-variables-for-laravel-deployments" />
            <id>https://flareapp.io/managing-production-environment-variables-for-laravel-deployments</id>
            <author>
                <name><![CDATA[Alex]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>"Secret secrets are no fun, secret secrets hurt someone" unless managed right in your Laravel deployment! For Flare, we explored alternative solutions to the classic <code>.env</code> files deployed with every Laravel application. We looked at deploying the <code>.env</code> file using Ansible, using config servers, the <code>env:encrypt</code> command, and eventually <a href="https://sobolevn.me/git-secret/">git-secret</a>, to keep our production environment variables safe and sound.</p>
<p>Environment variables and <code>.env</code> files have been widely used to manage Laravel apps' application secrets. However, these approaches come with their own set of challenges. In this blog post, we will explore the limitations of traditional methods and discuss alternative solutions for managing environment variables securely.</p>
<h2>The limitations of .env and why we need something better</h2>
<p>While <code>.env</code> files and real environment variables are commonly used, they present some challenges in terms of security and manageability:</p>
<p>First, the <code>.env</code> file is full of sensitive information. It's something you want to avoid committing to a GitHub repo (not even a private one). In an ideal scenario, it's not stored as a plain text file but kept encrypted and only decrypted in memory where needed.</p>
<p>Additionally, managing access between different team members can also be challenging. Once you share access to a server or the location of the <code>.env</code> file with a new colleague, there's no way back. In an ideal scenario, we'd have a way to view and manage which team members can access each secret.</p>
<p>Another requirement to consider is versioning. A <code>.env</code> file only kept on the server doesn't keep a history, making it impossible to roll back mistakes.</p>
<p>Finally, deploying changes to the <code>.env</code> file on a single server is easy. It's manual labour, but it's a 5-minute job. However, the task becomes more challenging when more servers are involved, or worse, Kubernetes.</p>
<h2>Alternative Solutions</h2>
<p>To overcome these limitations, let's explore a few alternative solutions for securely managing environment variables.</p>
<h3>Solution 1: Using Ansible for syncing <code>.env</code> files</h3>
<p>Ansible is an open-source project that provides a simple way to automate various configuration and infrastructure deployments. For example, it's trivial to sync a configuration file to multiple servers using Ansible. And that's precisely what we used to sync our <code>.env</code> file to all Flare servers simultaneously.</p>
<p>The downside is that this still involves giving people access to the Ansible server, manually updating your application's plain text <code>.env</code> file, and running an Ansible playbook to deploy the updated <code>.env</code> to all servers. While it worked well enough for years, it would be nice to manage and deploy <code>.env</code> secrets closer to home. For example, in the repo itself...</p>
<h3>Solution 2: Laravel's <code>env:encrypt</code></h3>
<p>Starting from Laravel 9, the <code>env:encrypt</code> command can be used to encrypt the <code>.env</code> file using in a way that the encrypted file can be committed to the GitHub repo. The <code>.env.encrypted</code> can be deployed and decrypted to all production servers. Because the encrypted <code>.env</code> file is stored in the GitHub repo, it is also versioned. This approach checks a lot of boxes, but it still has one single drawback: access is managed through a single encryption key that needs to be shared and protected. This means there is no way to revoke access from a single user.</p>
<h3>Solution 3: Secret management products</h3>
<p>The various secret management products are arguably the best alternative for <code>.env</code>. For the big three, we have AWS Parameter Store, Google Secrets Manager, and Azure Key Vault (why do all of these products have different names for the same concept?). Another honorable mention is HashiCorp Vault. These solutions are made specifically for this problem and provide a ton of extra advantages:</p>
<ul>
<li>
<p>Automatic secret rotation: config servers can rotate secrets automatically, reducing the risk of compromised credentials.</p>
</li>
<li>
<p>Granular access control allows you to give specific users access to <em>individual</em> secrets.</p>
</li>
<li>
<p>Integration possibilities: Config servers typically don't write to a <code>.env</code> file. Instead, they'll decrypt the secrets to memory at runtime, avoiding the various risks of a plain text <code>.env</code> file</p>
</li>
<li>
<p>Built-in audition solutions</p>
</li>
<li>
<p>Real-time updates (depending on the application)</p>
</li>
</ul>
<p>While this seems like the ideal solution, it is another service to manage or pay for, and for now, it feels a bit too heavy for our use case.</p>
<h3>Solution 4: git-secret - What we ended up using at Flare</h3>
<p><a href="https://sobolevn.me/git-secret/">git-secret</a> is built to solve the exact problem of managing secrets close to the git repository. Working very similar to Laravel's own <code>env:encrypt</code> command, its most significant advantage is that access is managed through a GPG keyring instead of a single shared encryption key. This means we can give and revoke access to each team member through their public key.</p>
<p>Additionally, it checks all the other boxes:</p>
<ul>
<li>
<p>Secrets are stored directly in the repository, making managing and tracking changes easy.</p>
</li>
<li>
<p>No additional infrastructure or services are required.</p>
</li>
<li>
<p>Built right into existing tooling: We already use git everywhere during development and deployment.</p>
</li>
<li>
<p>Deploys stay super simple: one extra command is all it takes to deploy the encrypted <code>.env</code> file.</p>
</li>
</ul>
<p>Let's switch from theory to practice and look at how we set up git-secret &amp; add the public key for the first user.</p>
<h3>Setting up git-secret</h3>
<ol>
<li>
<p>Install git-secret on your local machine and servers. Because git-secret is an extension to the Git CLI, it must be installed separately. Installation instructions can be found <a href="https://sobolevn.me/git-secret/installation">here</a>.</p>
</li>
<li>
<p>Create a new GPG key pair using the <code>gpg --gen-key</code> command.</p>
</li>
<li>
<p>Export the public key for your newly created GPG key pair: <code>gpg --armor --export your.email@address.com &gt; public-key.gpg</code></p>
</li>
<li>
<p>Initialize git-secret by running the <code>git secret init</code> command in your app's repository. The relevant sensitive files will automatically be added to your <code>.gitignore</code>, but checking either way is a good idea.</p>
</li>
<li>
<p>Use <code>git secret add .env.production</code> to add the <code>.env.production</code> to this repo's list of encrypted files. Again, the relevant sensitive files will automatically be added to your <code>.gitignore</code>.</p>
</li>
<li>
<p>Run <code>git secret hide -d' to encrypt the </code>.env<code>file. Because the</code>-d' flag was used, the original file will also be deleted.</p>
</li>
<li>
<p>Share the secret with the user's newly created public key using <code>git secret tell &lt;you.email@address.com&gt;</code></p>
</li>
<li>
<p>You can now test decrypting the encrypted secret using <code>git secret reveal</code>. If all goes well, the <code>.env.production</code> file should show back up.</p>
</li>
</ol>
<p>After you're ready to deploy your application to production, <code>git secret hide -d</code>, commit, and deploy to production. On the production server (or as the last step in the deploy script), we can now decrypt the <code>.env</code> file using the same <code>git secret reveal</code> command.</p>
<p>Like any good tool, many extra features, options, and configurations exist. You can read about it in the <a href="https://sobolevn.me/git-secret/#commands">git-secret docs</a>.</p>
<p><strong>Some notes on running git-secret in production</strong></p>
<p>The encrypted secrets are binary files. This makes it impossible (or annoyingly hard) to merge them in PRs or other branching actions. That's why it's essential to only commit changes to the encrypted <code>.env</code> files on a single branch and to push them to the remote repo immediately. This way, we can avoid all merge conflicts with these binary files.</p>
<p>Another good tip is to set up a separate Git repository to manage the public keys of your team. Using <code>gpg --import &lt;key&gt;</code>, you can quickly import these keys into your GPG keychain when necessary.</p>
<h3>Conclusion</h3>
<p>Managing environment variables securely is crucial for Laravel apps. While traditional methods like .env files have limitations, alternative solutions provide better security and manageability. This post discussed solutions like the various secret management products, Laravel's own <code>env:encrypt</code> command, Ansible, and git-secret.</p>
<p>For Flare and our team, git-secret emerged as a comprehensive solution, offering user-specific access control and seamless integration in our existing development and deployment flow.</p>
]]>
            </summary>
                                    <updated>2023-07-17T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Flare 2.0 has been launched!]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/flare-20-has-been-launched" />
            <id>https://flareapp.io/flare-20-has-been-launched</id>
            <author>
                <name><![CDATA[Sebastian]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>These past months, we've been busy redesigning our entire service. We're very proud to launch it today. Long-time users will immediately notice that both our marketing website and application have a new coat of paint.</p>
<p>We didn't only update the looks. We've also added some new features and improved others in the process.This blog post will look at Flare 2.0's history, features, and used technology. Let's dive in!</p>
<p>Instead of reading this blog post, you can see it all by yourself by <a href="/register">starting a free 10-day trial</a> now! Should you want to start using Flare after 10 days, we now offer <a href="/pricing">a very affordable hobby plan</a>.   You can help us spread the good news by retweeting our <a href="https://twitter.com/freekmurze/status/1667084817362452480">launch tweet</a>.</p>
<h2>What is Flare?</h2>
<p>In most cases, you start developing an app locally on your computer. At some point, the finished application is deployed to your production server, ready to use for the world!</p>
<p>Most applications are developed locally. On your computer, debugging can be done without hassle. You can place some <code>dd</code>, <code>dump</code> <a href="https://myray.app">or </a><a href="https://myray.app"><code>ray</code></a> statements within the application, or use xDebug. When an error occurs locally, an error message will be shown locally. In Laravel applications, errors are shown via Ignition, Laravel's default error page, which we created as well. More on that later. Here's how Ignition looks like for a common error in Laravel.</p>
<p><img src="https://assets.flareapp.io/production/blog/0a6c0358e1756592f34c7944eed9aab6/fdb9d162-6774-4099-97cc-5895b75b9371.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/0a6c0358e1756592f34c7944eed9aab6/fdb9d162-6774-4099-97cc-5895b75b9371.jpg" width="2940" height="1286" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/0a6c0358e1756592f34c7944eed9aab6/fdb9d162-6774-4099-97cc-5895b75b9371.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/0a6c0358e1756592f34c7944eed9aab6/fdb9d162-6774-4099-97cc-5895b75b9371.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/0a6c0358e1756592f34c7944eed9aab6/fdb9d162-6774-4099-97cc-5895b75b9371.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/0a6c0358e1756592f34c7944eed9aab6/fdb9d162-6774-4099-97cc-5895b75b9371.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>When deploying your application in production, adding <code>dd</code>'s can break your whole application, and running Xdebug isn't possible. Ignition is also disabled on production because you don't want to show the inner workings of your code, possible environment variables, queries, and so much more to the users visiting your app.</p>
<p>What if you could keep Ignition on production but not show it to your users? That's where Flare comes in. Instead of showing the error in the browser of your application's user, you can let your application send it to Flare. In Flare, you can see exactly what went wrong, and this information will help you fix the issue. Here's what the error above looks like in Flare.</p>
<p><img src="https://assets.flareapp.io/production/blog/ae7716dabde4c725fe6ae50ae603dff2/f226bdbf-f5d0-4c77-9cf8-43c018a134a0.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/ae7716dabde4c725fe6ae50ae603dff2/f226bdbf-f5d0-4c77-9cf8-43c018a134a0.jpg" width="2956" height="1982" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/ae7716dabde4c725fe6ae50ae603dff2/f226bdbf-f5d0-4c77-9cf8-43c018a134a0.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/ae7716dabde4c725fe6ae50ae603dff2/f226bdbf-f5d0-4c77-9cf8-43c018a134a0.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/ae7716dabde4c725fe6ae50ae603dff2/f226bdbf-f5d0-4c77-9cf8-43c018a134a0.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/ae7716dabde4c725fe6ae50ae603dff2/f226bdbf-f5d0-4c77-9cf8-43c018a134a0.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>Flare will also send you a notification (via Mail, Slack, Telegram, webhooks...) when an error happens. This way, you can be aware of and fix the problem even if your user doesn't contact you.</p>
<p>In Flare, you get all of the niceties Ignition provides, like the queries which ran, possible solutions for the problem, job context if the exception happened on the queue, LiveWire data passed to your components, and so much more!</p>
<p>You can link exceptions with GitHub issues and track who in your team is working on the issue, and if it has already been solved. Our graphs provide insight into which applications have the most errors and which should be fixed first. You can even see which users had the error and contact them.</p>
<p>If you have a JavaScript frontend (or even a backend application), then Flare will be an ideal companion. We support the major frameworks like Vue and React or vanilla JS if that's your thing. You can upload sourcemaps that make your obfuscated code make sense again.</p>
<h2>Four years of Flare history</h2>
<p>Let's take a look at how Flare initially came to be. The first commit on the Flare repository was pushed on the 12th of February 2019, more than four years ago! Oh, how time flies.</p>
<p><img src="https://assets.flareapp.io/production/blog/859b24109adedb03987ffa60e2ef3e50/004619ef-86a5-460c-964a-02cc1cd71318.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/859b24109adedb03987ffa60e2ef3e50/004619ef-86a5-460c-964a-02cc1cd71318.png" width="838" height="110" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/859b24109adedb03987ffa60e2ef3e50/004619ef-86a5-460c-964a-02cc1cd71318.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/859b24109adedb03987ffa60e2ef3e50/004619ef-86a5-460c-964a-02cc1cd71318.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=838/https://assets.flareapp.io/production/blog/859b24109adedb03987ffa60e2ef3e50/004619ef-86a5-460c-964a-02cc1cd71318.png 838w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>The commit message "fire torpedoes" was a wink at the working title of our product: "Photon". Before launching our product, we decided that Photon sounded too aggressive, and after a long process, we landed on the name Flare. Notice that both the names Photon and Flare revolve around "Light", which is the theme behind the early naming scheme of Laravel (Illuminate, Spark, Nova, …). In the early days of Flare, Marcel Pociot was also involved in building Flare.</p>
<p>During the development process, we tried new technologies that weren't fully released yet. We were one of the first users of <a href="https://vapor.laravel.com">Laravel Vapor</a>, the best serverless platform for Laravel apps. We also were very early adopters of Inertia.js, which we loved immediately. Our colleague Seb even coded the Inertia.js React adapter, which we still use today!</p>
<p>Our project deadline was set for Laracon EU 2019 in August. Within six months, we built not only the Flare application from scratch but Ignition as well. Ignition would go on to become the default error page in Laravel.</p>
<p>At Laracon EU 2019, Flare was <a href="https://www.youtube.com/watch?v=RP1mU4gfNeQ&amp;t=965s">shown</a>  in true Apple Keynote fashion to the public. Back at the Spatie office, all of us not attending Laracon were prepping the release and ensuring everything went live when the keynote was over. Everything went smoothly, and from that point in time, Flare was live!</p>
<p><img src="https://assets.flareapp.io/production/blog/f4e3cc516e6e54f829e09117e2a523f8/109d7b7d-339d-449f-a6fc-d6a7ff981de7.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/f4e3cc516e6e54f829e09117e2a523f8/109d7b7d-339d-449f-a6fc-d6a7ff981de7.jpg" width="1999" height="1125" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/f4e3cc516e6e54f829e09117e2a523f8/109d7b7d-339d-449f-a6fc-d6a7ff981de7.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/f4e3cc516e6e54f829e09117e2a523f8/109d7b7d-339d-449f-a6fc-d6a7ff981de7.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/f4e3cc516e6e54f829e09117e2a523f8/109d7b7d-339d-449f-a6fc-d6a7ff981de7.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/f4e3cc516e6e54f829e09117e2a523f8/109d7b7d-339d-449f-a6fc-d6a7ff981de7.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>In the next months and years, we didn't sit still and added a lot of new functionality to Flare. Here are some highlights:</p>
<ul>
<li>
<p><a href="https://flareapp.io/blog/23-introducing-our-github-integration">An integration with GitHub</a></p>
</li>
<li>
<p><a href="https://flareapp.io/blog/6-introducing-javascript-error-tracking">The JS error tracking which didn't exist when Flare launched</a></p>
</li>
<li>
<p><a href="https://flareapp.io/blog/13-mail-notifications-now-allow-to-snooze-and-resolve-errors">Better notifications with actions like resolving the error</a></p>
</li>
<li>
<p><a href="https://flareapp.io/blog/14-introducing-the-monthly-error-report-mail">Monthly error report emails</a></p>
</li>
<li>
<p><a href="https://flareapp.io/blog/22-meet-the-new-projects-overview-with-error-trends-and-favourites">A new projects overview with trends and favorites</a></p>
</li>
<li>
<p><a href="https://flareapp.io/blog/25-hello-there-new-api">An API</a></p>
</li>
<li>
<p><a href="https://flareapp.io/blog/9-flare-can-now-notify-you-via-discord-and-microsoft-teams">New notifications channels</a></p>
</li>
<li>
<p><a href="https://flareapp.io/blog/38-flares-new-job-tab-knows-all-about-your-failed-jobs">More info about your failed jobs</a></p>
</li>
<li>
<p><a href="https://flareapp.io/blog/42-better-support-for-livewire-in-flare-and-ignition">Context from LieWire components</a></p>
</li>
<li>
<p><a href="https://flareapp.io/blog/27-retry-requests-using-curl">Curl commands to retry requests</a></p>
</li>
<li>
<p><a href="https://flareapp.io/blog/36-building-a-better-search-with-monaco-and-amcharts">A better error search + occurrence graph for a project</a></p>
</li>
<li>
<p>And so much more ...</p>
</li>
</ul>
<p>Since Flare became so stable, we had some time to think about the next big step, and that's when we started working on the redesign, Flare v2.</p>
<h2>Why redesign Flare?</h2>
<p>When Flare was launched, we were delighted with Ignition becoming the default exception page in Laravel. We always intended to have a unified experience for handling exception messages. Laravel users should feel right at home in Flare, as they already know Ignition visually.</p>
<p>Of course, design trends change over time. Early last year, we decided to work on a refresh of how Ignition looks. It was released around one year ago. Our initial plan was to launch the redesign for Flare around the same time, but unfortunately, we couldn't pull this off. You might think that Spatie is a big firm with many people, but we're quite a small team. We also perform client work, and our full schedule didn't allow us to complete the Flare redesign.</p>
<p>Instead of shipping something half-baked, we made the difficult decision to postpone the redesign.</p>
<p>When we had time to spend on Flare again, we decided to speed things up by letting the good people at <a href="https://digitalwithyou.com">DWY</a> develop a new design for the marketing website from scratch. Dieter and Steve of DWY did a terrific job by providing a Sketch file from which we could cut the whole website.</p>
<p>With DWY having redesigned the marketing website, our app, which was still in its old style, looked way off. We decided to take hints from the marketing website to redesign our app. We didn't redesign it in one go but did several iterations. In these screenshots, you can see our app UI evolve.</p>
<p><img src="https://assets.flareapp.io/production/blog/27497ae788ba27e60c72aeda278deae6/4ff5d634-f9dd-409b-9812-970aaff6f945.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/27497ae788ba27e60c72aeda278deae6/4ff5d634-f9dd-409b-9812-970aaff6f945.png" width="1999" height="1355" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/27497ae788ba27e60c72aeda278deae6/4ff5d634-f9dd-409b-9812-970aaff6f945.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/27497ae788ba27e60c72aeda278deae6/4ff5d634-f9dd-409b-9812-970aaff6f945.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/27497ae788ba27e60c72aeda278deae6/4ff5d634-f9dd-409b-9812-970aaff6f945.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/27497ae788ba27e60c72aeda278deae6/4ff5d634-f9dd-409b-9812-970aaff6f945.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p><img src="https://assets.flareapp.io/production/blog/42793fb22189dd289bac3432b2219c38/9b43922e-a6f7-4ecf-8c07-99aa69a76138.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/42793fb22189dd289bac3432b2219c38/9b43922e-a6f7-4ecf-8c07-99aa69a76138.png" width="1999" height="1104" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/42793fb22189dd289bac3432b2219c38/9b43922e-a6f7-4ecf-8c07-99aa69a76138.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/42793fb22189dd289bac3432b2219c38/9b43922e-a6f7-4ecf-8c07-99aa69a76138.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/42793fb22189dd289bac3432b2219c38/9b43922e-a6f7-4ecf-8c07-99aa69a76138.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/42793fb22189dd289bac3432b2219c38/9b43922e-a6f7-4ecf-8c07-99aa69a76138.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p><img src="https://assets.flareapp.io/production/blog/f536189d8f068c187c010864e7a7c115/20de1332-1f4e-4618-8f79-2e58a608c9de.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/f536189d8f068c187c010864e7a7c115/20de1332-1f4e-4618-8f79-2e58a608c9de.png" width="1999" height="1176" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/f536189d8f068c187c010864e7a7c115/20de1332-1f4e-4618-8f79-2e58a608c9de.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/f536189d8f068c187c010864e7a7c115/20de1332-1f4e-4618-8f79-2e58a608c9de.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/f536189d8f068c187c010864e7a7c115/20de1332-1f4e-4618-8f79-2e58a608c9de.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/f536189d8f068c187c010864e7a7c115/20de1332-1f4e-4618-8f79-2e58a608c9de.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<h2>What's new in Flare</h2>
<p>We worked intensively with the whole team on this redesign and added some new functionality next to the new design. Let's go through it!</p>
<h3>New onboarding</h3>
<p>There's now a new onboarding experience. When you log in for the first time, you're invited to create a new project.</p>
<p><img src="https://assets.flareapp.io/production/blog/171350c77dd824c66c559d3ea53d1ea6/419d02bd-b2e4-49c5-b1f8-20a61d8f9305.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/171350c77dd824c66c559d3ea53d1ea6/419d02bd-b2e4-49c5-b1f8-20a61d8f9305.jpg" width="2988" height="1154" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/171350c77dd824c66c559d3ea53d1ea6/419d02bd-b2e4-49c5-b1f8-20a61d8f9305.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/171350c77dd824c66c559d3ea53d1ea6/419d02bd-b2e4-49c5-b1f8-20a61d8f9305.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/171350c77dd824c66c559d3ea53d1ea6/419d02bd-b2e4-49c5-b1f8-20a61d8f9305.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/171350c77dd824c66c559d3ea53d1ea6/419d02bd-b2e4-49c5-b1f8-20a61d8f9305.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>Here, you can create your very first project. Let's go ahead a pick Laravel as the used technology.</p>
<p><img src="https://assets.flareapp.io/production/blog/170c5fa426f79664e0d678cb261f9654/b1177d61-b5ad-4688-b313-b2995dfe917a.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/170c5fa426f79664e0d678cb261f9654/b1177d61-b5ad-4688-b313-b2995dfe917a.jpg" width="1910" height="1278" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/170c5fa426f79664e0d678cb261f9654/b1177d61-b5ad-4688-b313-b2995dfe917a.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/170c5fa426f79664e0d678cb261f9654/b1177d61-b5ad-4688-b313-b2995dfe917a.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/170c5fa426f79664e0d678cb261f9654/b1177d61-b5ad-4688-b313-b2995dfe917a.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/170c5fa426f79664e0d678cb261f9654/b1177d61-b5ad-4688-b313-b2995dfe917a.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>Next, we show detailed instructions on how to configure your Laravel app. You now get instructions tailored for the Laravel version that you use.</p>
<p><img src="https://assets.flareapp.io/production/blog/9822656f1915358230f667f48461349a/ced5d0b7-4c37-4544-8985-c168298fa1d3.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/9822656f1915358230f667f48461349a/ced5d0b7-4c37-4544-8985-c168298fa1d3.jpg" width="2990" height="2906" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/9822656f1915358230f667f48461349a/ced5d0b7-4c37-4544-8985-c168298fa1d3.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/9822656f1915358230f667f48461349a/ced5d0b7-4c37-4544-8985-c168298fa1d3.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/9822656f1915358230f667f48461349a/ced5d0b7-4c37-4544-8985-c168298fa1d3.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/9822656f1915358230f667f48461349a/ced5d0b7-4c37-4544-8985-c168298fa1d3.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>In one the final steps of the installation process, we added some real-time feedback to show you when we successfully received the first error sent by your app.</p>
<p><img src="https://assets.flareapp.io/production/blog/b4cd3790c136aa82193c6fa30c680f89/99b082d5-c9d5-4ae7-8540-f3763f104b30.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/b4cd3790c136aa82193c6fa30c680f89/99b082d5-c9d5-4ae7-8540-f3763f104b30.jpg" width="1368" height="322" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/b4cd3790c136aa82193c6fa30c680f89/99b082d5-c9d5-4ae7-8540-f3763f104b30.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/b4cd3790c136aa82193c6fa30c680f89/99b082d5-c9d5-4ae7-8540-f3763f104b30.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/b4cd3790c136aa82193c6fa30c680f89/99b082d5-c9d5-4ae7-8540-f3763f104b30.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1368/https://assets.flareapp.io/production/blog/b4cd3790c136aa82193c6fa30c680f89/99b082d5-c9d5-4ae7-8540-f3763f104b30.jpg 1368w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>For JavaScript projects, we also offer similar specific instructions for the framework and tools you use</p>
<p><img src="https://assets.flareapp.io/production/blog/9cae7061c7cb3dcda18f51ebbd82d8b9/0ceeba37-e49d-4e98-8238-4a6a552fa468.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/9cae7061c7cb3dcda18f51ebbd82d8b9/0ceeba37-e49d-4e98-8238-4a6a552fa468.jpg" width="2316" height="1678" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/9cae7061c7cb3dcda18f51ebbd82d8b9/0ceeba37-e49d-4e98-8238-4a6a552fa468.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/9cae7061c7cb3dcda18f51ebbd82d8b9/0ceeba37-e49d-4e98-8238-4a6a552fa468.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/9cae7061c7cb3dcda18f51ebbd82d8b9/0ceeba37-e49d-4e98-8238-4a6a552fa468.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/9cae7061c7cb3dcda18f51ebbd82d8b9/0ceeba37-e49d-4e98-8238-4a6a552fa468.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<h3>A new dashboard</h3>
<p>We've completely rebuilt the dashboard from scratch. You'll immediately notice that we added a big error graph and the most important metrics for each project.</p>
<p><img src="https://assets.flareapp.io/production/blog/54b9747415b0f026788adbc2969884cd/66127736-915d-4b1d-86ab-b2c05a955e37.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/54b9747415b0f026788adbc2969884cd/66127736-915d-4b1d-86ab-b2c05a955e37.jpg" width="2960" height="2732" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/54b9747415b0f026788adbc2969884cd/66127736-915d-4b1d-86ab-b2c05a955e37.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/54b9747415b0f026788adbc2969884cd/66127736-915d-4b1d-86ab-b2c05a955e37.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/54b9747415b0f026788adbc2969884cd/66127736-915d-4b1d-86ab-b2c05a955e37.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/54b9747415b0f026788adbc2969884cd/66127736-915d-4b1d-86ab-b2c05a955e37.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>Fear not; if you rather have a simple list of all your projects, you can pick that list view at the top.</p>
<p><img src="https://assets.flareapp.io/production/blog/083da5805f75606026fa78ed1096f8fc/7f6d6fe7-0380-43e5-a049-5eeea2db4bd6.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/083da5805f75606026fa78ed1096f8fc/7f6d6fe7-0380-43e5-a049-5eeea2db4bd6.jpg" width="2978" height="2326" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/083da5805f75606026fa78ed1096f8fc/7f6d6fe7-0380-43e5-a049-5eeea2db4bd6.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/083da5805f75606026fa78ed1096f8fc/7f6d6fe7-0380-43e5-a049-5eeea2db4bd6.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/083da5805f75606026fa78ed1096f8fc/7f6d6fe7-0380-43e5-a049-5eeea2db4bd6.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/083da5805f75606026fa78ed1096f8fc/7f6d6fe7-0380-43e5-a049-5eeea2db4bd6.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>As you can see above, we've added a visual distinction between your projects' different stages and languages. A project can be set as production, staging, development, or local, each with its own color.</p>
<h3>The new error list</h3>
<p>When you open up one of our projects, you'll see all the errors for that project. The error list is completely revamped.</p>
<p><img src="https://assets.flareapp.io/production/blog/5d6bc64bd4b7f52916d156defc495afe/a735b05b-ee8d-4345-b4f8-2273ec519b5a.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/5d6bc64bd4b7f52916d156defc495afe/a735b05b-ee8d-4345-b4f8-2273ec519b5a.jpg" width="2980" height="2096" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/5d6bc64bd4b7f52916d156defc495afe/a735b05b-ee8d-4345-b4f8-2273ec519b5a.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/5d6bc64bd4b7f52916d156defc495afe/a735b05b-ee8d-4345-b4f8-2273ec519b5a.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/5d6bc64bd4b7f52916d156defc495afe/a735b05b-ee8d-4345-b4f8-2273ec519b5a.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/5d6bc64bd4b7f52916d156defc495afe/a735b05b-ee8d-4345-b4f8-2273ec519b5a.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>On top, you can see a graph showing the total of errors received at a time for that project. You can drag a period on that graph, and in the list, we'll only show the errors that occurred in that period.</p>
<p><img src="https://assets.flareapp.io/production/blog/4d0443e4087f56212d79ac009c67f4ef/76178b0b-8d0a-49ea-94fb-a67dbf1eaa3a.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/4d0443e4087f56212d79ac009c67f4ef/76178b0b-8d0a-49ea-94fb-a67dbf1eaa3a.jpg" width="2974" height="1658" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/4d0443e4087f56212d79ac009c67f4ef/76178b0b-8d0a-49ea-94fb-a67dbf1eaa3a.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/4d0443e4087f56212d79ac009c67f4ef/76178b0b-8d0a-49ea-94fb-a67dbf1eaa3a.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/4d0443e4087f56212d79ac009c67f4ef/76178b0b-8d0a-49ea-94fb-a67dbf1eaa3a.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/4d0443e4087f56212d79ac009c67f4ef/76178b0b-8d0a-49ea-94fb-a67dbf1eaa3a.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>You'll notice that we now also show graphs for all individual errors, so you better understand when and how often they occurred. When you hover one of the bars, you'll see the exact timeframe and error count.</p>
<p><img src="https://assets.flareapp.io/production/blog/52b233dccced429e762f97c72fec45be/c215c3a5-dd67-44bd-b4be-577a4fb085e0.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/52b233dccced429e762f97c72fec45be/c215c3a5-dd67-44bd-b4be-577a4fb085e0.jpg" width="2914" height="440" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/52b233dccced429e762f97c72fec45be/c215c3a5-dd67-44bd-b4be-577a4fb085e0.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/52b233dccced429e762f97c72fec45be/c215c3a5-dd67-44bd-b4be-577a4fb085e0.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/52b233dccced429e762f97c72fec45be/c215c3a5-dd67-44bd-b4be-577a4fb085e0.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/52b233dccced429e762f97c72fec45be/c215c3a5-dd67-44bd-b4be-577a4fb085e0.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>On an error card, you'll notice that we add relevant, and sometimes Laravel-specific, info about that error. If your error occurred in a web request, we show the controller.</p>
<p><img src="https://assets.flareapp.io/production/blog/d2ae3e244c9dc716cf6ad9efe14b60c5/06445b03-fda1-40e3-880f-9bb253288cda.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/d2ae3e244c9dc716cf6ad9efe14b60c5/06445b03-fda1-40e3-880f-9bb253288cda.jpg" width="1538" height="706" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/d2ae3e244c9dc716cf6ad9efe14b60c5/06445b03-fda1-40e3-880f-9bb253288cda.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/d2ae3e244c9dc716cf6ad9efe14b60c5/06445b03-fda1-40e3-880f-9bb253288cda.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/d2ae3e244c9dc716cf6ad9efe14b60c5/06445b03-fda1-40e3-880f-9bb253288cda.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/d2ae3e244c9dc716cf6ad9efe14b60c5/06445b03-fda1-40e3-880f-9bb253288cda.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>If it happened in an Artisan command, we show that command.</p>
<p><img src="https://assets.flareapp.io/production/blog/603a166aa472f13b784e257e6acad7d0/d2de3f2e-857a-4b8c-9256-da7fdd687646.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/603a166aa472f13b784e257e6acad7d0/d2de3f2e-857a-4b8c-9256-da7fdd687646.jpg" width="1528" height="604" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/603a166aa472f13b784e257e6acad7d0/d2de3f2e-857a-4b8c-9256-da7fdd687646.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/603a166aa472f13b784e257e6acad7d0/d2de3f2e-857a-4b8c-9256-da7fdd687646.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/603a166aa472f13b784e257e6acad7d0/d2de3f2e-857a-4b8c-9256-da7fdd687646.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1528/https://assets.flareapp.io/production/blog/603a166aa472f13b784e257e6acad7d0/d2de3f2e-857a-4b8c-9256-da7fdd687646.jpg 1528w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>On the error cards, you'll find buttons to mark the error as resolved and snooze notifications. You can't see this visually, but we're using optimistic mutations when you use one of the buttons. That means when you click the button, you'll immediately get visual feedback that the error is resolved/snoozed; we don't wait for a positive response code from our internal API. This will make our UI feel lightning-fast.</p>
<p><img src="https://assets.flareapp.io/production/blog/f10dcd1514f6c098fac6d7fb2520f989/cab99410-1414-422a-b473-bd3a0b96e899.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/f10dcd1514f6c098fac6d7fb2520f989/cab99410-1414-422a-b473-bd3a0b96e899.jpg" width="2948" height="716" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/f10dcd1514f6c098fac6d7fb2520f989/cab99410-1414-422a-b473-bd3a0b96e899.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/f10dcd1514f6c098fac6d7fb2520f989/cab99410-1414-422a-b473-bd3a0b96e899.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/f10dcd1514f6c098fac6d7fb2520f989/cab99410-1414-422a-b473-bd3a0b96e899.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/f10dcd1514f6c098fac6d7fb2520f989/cab99410-1414-422a-b473-bd3a0b96e899.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<h3>The revamped error screen</h3>
<p>Next, let's look at the screen you'll see when clicking on one of the errors in the project's error list. It's arguably the most important screen of Flare: here's what the error details screen looks like.</p>
<p><img src="https://assets.flareapp.io/production/blog/fd6f1866e093a8f21e470095244dc2d0/de9a0c6a-39b4-4d13-9662-d4b74cd18c35.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/fd6f1866e093a8f21e470095244dc2d0/de9a0c6a-39b4-4d13-9662-d4b74cd18c35.jpg" width="2986" height="1036" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/fd6f1866e093a8f21e470095244dc2d0/de9a0c6a-39b4-4d13-9662-d4b74cd18c35.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/fd6f1866e093a8f21e470095244dc2d0/de9a0c6a-39b4-4d13-9662-d4b74cd18c35.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/fd6f1866e093a8f21e470095244dc2d0/de9a0c6a-39b4-4d13-9662-d4b74cd18c35.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/fd6f1866e093a8f21e470095244dc2d0/de9a0c6a-39b4-4d13-9662-d4b74cd18c35.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>You can see that it's completely revamped. We start by showing the same error card with all details that you saw in the list.</p>
<p>When scrolling down, you'll see the stack trace of the error. It should feel very familiar, as it's rendered by the same component that also renders Ignition, the default Laravel error page.</p>
<p><img src="https://assets.flareapp.io/production/blog/aab148a2109ac54f6b9a0948f765808a/3ba4a9cc-711d-4789-808c-8ec452beb920.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/aab148a2109ac54f6b9a0948f765808a/3ba4a9cc-711d-4789-808c-8ec452beb920.jpg" width="2956" height="1760" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/aab148a2109ac54f6b9a0948f765808a/3ba4a9cc-711d-4789-808c-8ec452beb920.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/aab148a2109ac54f6b9a0948f765808a/3ba4a9cc-711d-4789-808c-8ec452beb920.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/aab148a2109ac54f6b9a0948f765808a/3ba4a9cc-711d-4789-808c-8ec452beb920.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/aab148a2109ac54f6b9a0948f765808a/3ba4a9cc-711d-4789-808c-8ec452beb920.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>When scrolling down some more, you'll see all the app-specific details, also shown in Ignition—a true treasure trove of information.</p>
<p><img src="https://assets.flareapp.io/production/blog/eb280148754fcfd48ff56a5c38dcc887/342eea04-1416-430f-bc6f-4e8a97f49e1d.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/eb280148754fcfd48ff56a5c38dcc887/342eea04-1416-430f-bc6f-4e8a97f49e1d.jpg" width="2980" height="1506" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/eb280148754fcfd48ff56a5c38dcc887/342eea04-1416-430f-bc6f-4e8a97f49e1d.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/eb280148754fcfd48ff56a5c38dcc887/342eea04-1416-430f-bc6f-4e8a97f49e1d.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/eb280148754fcfd48ff56a5c38dcc887/342eea04-1416-430f-bc6f-4e8a97f49e1d.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/eb280148754fcfd48ff56a5c38dcc887/342eea04-1416-430f-bc6f-4e8a97f49e1d.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<h4>Error insights</h4>
<p>Scroll back to the top of an error, and you'll see one of the most significant new features: error insights.</p>
<p><img src="https://assets.flareapp.io/production/blog/aa893f3007fb3239e4df6a43559e0e51/c982224e-7548-43c7-abd7-02feffcbddd6.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/aa893f3007fb3239e4df6a43559e0e51/c982224e-7548-43c7-abd7-02feffcbddd6.jpg" width="2982" height="952" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/aa893f3007fb3239e4df6a43559e0e51/c982224e-7548-43c7-abd7-02feffcbddd6.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/aa893f3007fb3239e4df6a43559e0e51/c982224e-7548-43c7-abd7-02feffcbddd6.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/aa893f3007fb3239e4df6a43559e0e51/c982224e-7548-43c7-abd7-02feffcbddd6.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/aa893f3007fb3239e4df6a43559e0e51/c982224e-7548-43c7-abd7-02feffcbddd6.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>Using these insights, you can quickly see all the places where an error occurred and which of your users encountered it.</p>
<p>The URL insight shows a list of URLs where this particular URL was seen.</p>
<p><img src="https://assets.flareapp.io/production/blog/689ae55d629f52272f7a52570421728a/cb02a015-9aad-4093-9c5f-9c85a66c9eeb.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/689ae55d629f52272f7a52570421728a/cb02a015-9aad-4093-9c5f-9c85a66c9eeb.jpg" width="2978" height="1288" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/689ae55d629f52272f7a52570421728a/cb02a015-9aad-4093-9c5f-9c85a66c9eeb.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/689ae55d629f52272f7a52570421728a/cb02a015-9aad-4093-9c5f-9c85a66c9eeb.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/689ae55d629f52272f7a52570421728a/cb02a015-9aad-4093-9c5f-9c85a66c9eeb.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/689ae55d629f52272f7a52570421728a/cb02a015-9aad-4093-9c5f-9c85a66c9eeb.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>Each insight has a "Download CSV" button in the top right corner that allows you to download a list with results.</p>
<p>Let's take a look at a few other insights. Here are the jobs an error occurred in:</p>
<p><img src="https://assets.flareapp.io/production/blog/9a6d886d3287f895c61fcbf0c210e5de/339d79f1-fc77-42d2-afdf-18d09f4302c4.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/9a6d886d3287f895c61fcbf0c210e5de/339d79f1-fc77-42d2-afdf-18d09f4302c4.jpg" width="2970" height="1294" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/9a6d886d3287f895c61fcbf0c210e5de/339d79f1-fc77-42d2-afdf-18d09f4302c4.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/9a6d886d3287f895c61fcbf0c210e5de/339d79f1-fc77-42d2-afdf-18d09f4302c4.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/9a6d886d3287f895c61fcbf0c210e5de/339d79f1-fc77-42d2-afdf-18d09f4302c4.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/9a6d886d3287f895c61fcbf0c210e5de/339d79f1-fc77-42d2-afdf-18d09f4302c4.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>We think the users insight, which shows the list of users that encountered the error, will be very helpful. Here's a screenshot with fake, seeded email addresses.</p>
<p><img src="https://assets.flareapp.io/production/blog/b4181b1f48842acac9cb23c914fba858/1058a9e2-ebf5-4cae-b841-1bcf99cf67a3.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/b4181b1f48842acac9cb23c914fba858/1058a9e2-ebf5-4cae-b841-1bcf99cf67a3.jpg" width="2972" height="1388" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/b4181b1f48842acac9cb23c914fba858/1058a9e2-ebf5-4cae-b841-1bcf99cf67a3.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/b4181b1f48842acac9cb23c914fba858/1058a9e2-ebf5-4cae-b841-1bcf99cf67a3.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/b4181b1f48842acac9cb23c914fba858/1058a9e2-ebf5-4cae-b841-1bcf99cf67a3.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/b4181b1f48842acac9cb23c914fba858/1058a9e2-ebf5-4cae-b841-1bcf99cf67a3.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<h3>AI Solutions</h3>
<p>Flare already had solutions, which gave you some insight into the error and what might be the problem. We're now extending this list with AI solutions; these solutions will be generated using a GPT model.</p>
<p>Here's an error what occurred because there were too many connections to MySQL. The AI suggests some possible fixes and includes links to relevant documenation.</p>
<p><img src="https://assets.flareapp.io/production/blog/58731bde446883cfbac783cb5b1132db/673593e8-88c5-4d86-a2b1-e549819dcdb1.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/58731bde446883cfbac783cb5b1132db/673593e8-88c5-4d86-a2b1-e549819dcdb1.jpg" width="3164" height="1858" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/58731bde446883cfbac783cb5b1132db/673593e8-88c5-4d86-a2b1-e549819dcdb1.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/58731bde446883cfbac783cb5b1132db/673593e8-88c5-4d86-a2b1-e549819dcdb1.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/58731bde446883cfbac783cb5b1132db/673593e8-88c5-4d86-a2b1-e549819dcdb1.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/58731bde446883cfbac783cb5b1132db/673593e8-88c5-4d86-a2b1-e549819dcdb1.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>We're very proud of this feature and can learn more about how it works in <a href="https://flareapp.io/blog/54-flare-and-ignition-now-offer-ai-powered-solutions">the separate blog post on AI solutions</a>.</p>
<h3>Project switcher</h3>
<p>An interface element that has got more common these years is a switcher. In our redesign, we added a simple project switcher you can invoke with Cmd+K on Mac or Ctrl+K on Windows, allowing you to jump quickly to another project.</p>
<p><img src="https://assets.flareapp.io/production/blog/408b8b297cb58bb66bf6a6a957c75045/beb2b30b-f4a1-4815-955a-e324b7683e66.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/408b8b297cb58bb66bf6a6a957c75045/beb2b30b-f4a1-4815-955a-e324b7683e66.jpg" width="2018" height="1138" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/408b8b297cb58bb66bf6a6a957c75045/beb2b30b-f4a1-4815-955a-e324b7683e66.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/408b8b297cb58bb66bf6a6a957c75045/beb2b30b-f4a1-4815-955a-e324b7683e66.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/408b8b297cb58bb66bf6a6a957c75045/beb2b30b-f4a1-4815-955a-e324b7683e66.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/408b8b297cb58bb66bf6a6a957c75045/beb2b30b-f4a1-4815-955a-e324b7683e66.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>No surprises there.</p>
<h3>Simplified team management</h3>
<p>In the previous version of Flare, you would have an overview of all the projects for all the teams you belonged to. Internally, this made grouping and sorting complicated.</p>
<p>Since only 2% of our users have more than one team, we decided to simplify this.</p>
<p>From now on, you'll see the projects for the team you're working in. Switching to another team can be done by clicking on your user avatar.</p>
<p><img src="https://assets.flareapp.io/production/blog/77184b7341cf0dcc61167dd460b3e0dc/27babb56-b0a9-43cf-8311-5765150af4de.jpg" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/77184b7341cf0dcc61167dd460b3e0dc/27babb56-b0a9-43cf-8311-5765150af4de.jpg" width="796" height="1060" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/77184b7341cf0dcc61167dd460b3e0dc/27babb56-b0a9-43cf-8311-5765150af4de.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/77184b7341cf0dcc61167dd460b3e0dc/27babb56-b0a9-43cf-8311-5765150af4de.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=796/https://assets.flareapp.io/production/blog/77184b7341cf0dcc61167dd460b3e0dc/27babb56-b0a9-43cf-8311-5765150af4de.jpg 796w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<h2>Technologies used</h2>
<p>Flare is a love letter to the Laravel community. Behind the scenes, Flare is powered by the latest version of Laravel and PHP (at the time of writing, L10 and PHP 8.2). Of course, inside our codebase, we use <a href="https://spatie.be/open-source">a gazillion of our own packages</a>.</p>
<p>Our payments are handled by <a href="https://www.google.com/search?client=safari&amp;rls=en&amp;q=laravel+cashier&amp;ie=UTF-8&amp;oe=UTF-8">Laravel Cashier</a> and <a href="https://stripe.com">Stripe</a>.</p>
<p>Our error ingress point is managed by <a href="https://www.cloudflare.com">Cloudflare</a>. We use that service for authentication and spike protection.</p>
<p>The front end of our app is built us React, TypeScript, and InertiaJS.</p>
<p>Our admin panel, which is used to quickly look up customer info and impersonate users, is powered by Filament.</p>
<p>Though we started on <a href="https://vapor.laravel.com">Laravel Vapor</a>, it was more cost-effective for us to use regular servers. These servers are hosted on Digital Ocean and provided by <a href="https://forge.laravel.com">Laravel Forge</a>.</p>
<h2>In closing</h2>
<p>We have put a lot of love into the Flare redesign, and we hope it shows. Of course, this is not the end of development on Flare. With the redesign launched, we can now concentrate on adding more features to Flare that are on our roadmap.</p>
<p>One of the bigger things we'll concentrate on is something not visible to our users. We've experimented with <a href="https://www.singlestore.com">SingleStore</a> and are pretty impressed by the performance improvements over MySQL it offers. We're going to experiment if we can improve the response times for Flare by making the switch to SingleStore. We'll be sure to write a few blog posts on that process as well.</p>
<p>Now is the perfect time to try out Flare. We support both PHP and JS projects. For Laravel apps, we've gone the extra mile: we recognize Livewire errors, for an error in a queue job, we display information about that queue, view errors will contain the decompiled Blade view, ... You can <a href="https://flareapp.io">register here</a> for a free ten-day trial; no credit card is required.</p>
]]>
            </summary>
                                    <updated>2023-06-08T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Sensitive parameters in PHP 8.2]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/sensitive-parameters-in-php-82" />
            <id>https://flareapp.io/sensitive-parameters-in-php-82</id>
            <author>
                <name><![CDATA[Ruben]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>When debugging errors, we as developers have a few tools available: an exception of a particular class, a message, a status code, and a stack trace. The stack trace is a report that provides information about the sequence of function calls that led to an error or an exception. It shows the order in which functions were called.</p>
<p>We name each call to a function/method within a stack trace a frame. It has lots of useful info like the file name, line number, function name, class (sometimes), and arguments that were used to make the call. Having arguments within a stack trace can be dangerous. For example, let's say you have a function like this:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">function getPayments(string $apiKey): array 
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">    throw new Exception('Could not fetch payments');
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>Usually, you would have some application code here, fetching payments through an external API, but to simplify things, let's go straight to the case where we throw an exception because the payments couldn't be fetched.</p>
<p>You can call this function as such:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">getPayments('dj8gd4sl05db32xjch7989dsxws');
</span></span></code></pre>
<p>This function call, of course, fails miserably, providing you with the following exception message:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">Fatal error: Uncaught Exception: Could not fetch payments in /in/UoFa1:6
</span></span><span class="line"><span class="token">Stack trace:
</span></span><span class="line"><span class="token">#0 /in/UoFa1(32): getPayments('dj8gd4sl05db32x...')
</span></span><span class="line"><span class="token">#1 {main}
</span></span><span class="line"><span class="token">  thrown in /in/UoFa1 on line 6
</span></span><span class="line"><span class="token">&lt;br/&gt;&lt;i&gt;Process exited with code &lt;b title="Generic Error"&gt;255&lt;/b&gt;.&lt;/i&gt;
</span></span></code></pre>
<p>Take a look at the third line:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">#0 /index.php(34): getPayments('dj8gd4sl05db32x...')
</span></span></code></pre>
<p>That's our API key in plain sight, which we want to avoid at all costs!</p>
<p>Above, we saw the stringified version of the stack trace. When we catch the exception, we can take a look at a more normalized array version:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">try {
</span></span><span class="line"><span class="token">    getPayments('dj8gd4sl05db32xjch7989dsxws');
</span></span><span class="line"><span class="token">} catch (Exception $e) {
</span></span><span class="line"><span class="token">    var_dump($e-&gt;getTrace());
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>Catching the exception gives us the following stack trace:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">array(1) {
</span></span><span class="line"><span class="token">  [0]=&gt;
</span></span><span class="line"><span class="token">  array(4) {
</span></span><span class="line"><span class="token">    ["file"]=&gt;
</span></span><span class="line"><span class="token">    string(9) "/index.php"
</span></span><span class="line"><span class="token">    ["line"]=&gt;
</span></span><span class="line"><span class="token">    int(34)
</span></span><span class="line"><span class="token">    ["function"]=&gt;
</span></span><span class="line"><span class="token">    string(11) "getPayments"
</span></span><span class="line"><span class="token">    ["args"]=&gt;
</span></span><span class="line"><span class="token">    array(1) {
</span></span><span class="line"><span class="token">      [0]=&gt;
</span></span><span class="line"><span class="token">      string(27) "dj8gd4sl05db32xjch7989dsxws"
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">  }
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>As you can see, we're passing one argument to the <code>getPayments</code> function, which can be helpful when debugging. But this information can also end up in the wrong hands when displayed in logs or error pages like Ignition.</p>
<p>That's the reason why we refrained from showing these parameters in Ignition:</p>
<p><img src="https://assets.flareapp.io/production/blog/066296c7628a91a4a45446c266ae7559/7f7792a1-2b66-4de1-9c4f-470f8e6fe7da.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/066296c7628a91a4a45446c266ae7559/7f7792a1-2b66-4de1-9c4f-470f8e6fe7da.png" width="2994" height="2334" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/066296c7628a91a4a45446c266ae7559/7f7792a1-2b66-4de1-9c4f-470f8e6fe7da.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/066296c7628a91a4a45446c266ae7559/7f7792a1-2b66-4de1-9c4f-470f8e6fe7da.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/066296c7628a91a4a45446c266ae7559/7f7792a1-2b66-4de1-9c4f-470f8e6fe7da.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/066296c7628a91a4a45446c266ae7559/7f7792a1-2b66-4de1-9c4f-470f8e6fe7da.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>And thus, due to the tight coupling of Flare and Ignition, these parameters won't end up in Flare.</p>
<p>But this all might change due to a recent addition to PHP 8.2.</p>
<h2>Sensitive Parameters</h2>
<p>Starting from PHP 8.2, some parameters can now be tagged as 'sensitive' using an attribute, meaning a parameter will be hidden from a stack trace.</p>
<p>Let's rewrite our <code>getPayments</code> function as follows:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">function getPayments(
</span></span><span class="line"><span class="token">    #[SensitiveParameter]
</span></span><span class="line"><span class="token">    string $apiKey
</span></span><span class="line"><span class="token">): array {
</span></span><span class="line"><span class="token">    throw new Exception('Could not fetch payments');
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>Now when we execute the function, we get the following:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">Fatal error: Uncaught Exception: Could not fetch payments in /in/Ko3d8:13
</span></span><span class="line"><span class="token">Stack trace:
</span></span><span class="line"><span class="token">#0 /in/Ko3d8(32): getPayments(Object(SensitiveParameterValue))
</span></span><span class="line"><span class="token">#1 {main}
</span></span><span class="line"><span class="token">  thrown in /in/Ko3d8 on line 13
</span></span><span class="line"><span class="token">&lt;br/&gt;&lt;i&gt;Process exited with code &lt;b title="Generic Error"&gt;255&lt;/b&gt;.&lt;/i&gt;
</span></span></code></pre>
<p>The API key is replaced with <code>Object(SensitiveParameterValue)</code>, hiding the original value! Let's see how the stack trace looks:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">array(1) {
</span></span><span class="line"><span class="token">  [0]=&gt;
</span></span><span class="line"><span class="token">  array(4) {
</span></span><span class="line"><span class="token">    ["file"]=&gt;
</span></span><span class="line"><span class="token">    string(9) "/in/cv0er"
</span></span><span class="line"><span class="token">    ["line"]=&gt;
</span></span><span class="line"><span class="token">    int(33)
</span></span><span class="line"><span class="token">    ["function"]=&gt;
</span></span><span class="line"><span class="token">    string(11) "getPayments"
</span></span><span class="line"><span class="token">    ["args"]=&gt;
</span></span><span class="line"><span class="token">    array(1) {
</span></span><span class="line"><span class="token">      [0]=&gt;
</span></span><span class="line"><span class="token">      object(SensitiveParameterValue)#2 (0) {
</span></span><span class="line"><span class="token">      }
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">  }
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>Again the API key is gone from traces, but the value still exists. The original string value is replaced with an object <code>SensitiveParameterValue</code>, which wraps the original value. We can see this by retrieving the original value as such:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">try {
</span></span><span class="line"><span class="token">    getPayments('dj8gd4sl05db32xjch7989dsxws');
</span></span><span class="line"><span class="token">} catch (Exception $e) {
</span></span><span class="line"><span class="token">    var_dump($e-&gt;getTrace()[0]['args'][0]-&gt;getValue()); // dj8gd4sl05db32xj...
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<h2>Classes</h2>
<p>The example mentioned above, used functions to make things easy to understand, but you can perfectly tag parameters as sensitive in a class:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">class PaymentApi
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">    public function getPayments(
</span></span><span class="line"><span class="token">    	 int $tenantId,
</span></span><span class="line"><span class="token">        #[SensitiveParameter]
</span></span><span class="line"><span class="token">        string $apiKey
</span></span><span class="line"><span class="token">    ): string {
</span></span><span class="line"><span class="token">        throw new Exception('Could not fetch payments');
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>Running this:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">try {
</span></span><span class="line"><span class="token">    $api = new PaymentApi();
</span></span><span class="line"><span class="token">    $api-&gt;getPayments(314, 'dj8gd4sl05db32xjch7989dsxws');
</span></span><span class="line"><span class="token">} catch (Exception $e) {
</span></span><span class="line"><span class="token">    var_dump($e-&gt;getTrace());
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>It gives us the following stack trace:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">array(1) {
</span></span><span class="line"><span class="token">  [0]=&gt;
</span></span><span class="line"><span class="token">  array(6) {
</span></span><span class="line"><span class="token">    ["file"]=&gt;
</span></span><span class="line"><span class="token">    string(90) "/index.php"
</span></span><span class="line"><span class="token">    ["line"]=&gt;
</span></span><span class="line"><span class="token">    int(36)
</span></span><span class="line"><span class="token">    ["function"]=&gt;
</span></span><span class="line"><span class="token">    string(11) "getPayments"
</span></span><span class="line"><span class="token">    ["class"]=&gt;
</span></span><span class="line"><span class="token">    string(10) "PaymentApi"
</span></span><span class="line"><span class="token">    ["type"]=&gt;
</span></span><span class="line"><span class="token">    string(2) "-&gt;"
</span></span><span class="line"><span class="token">    ["args"]=&gt;
</span></span><span class="line"><span class="token">    array(2) {
</span></span><span class="line"><span class="token">      [0]=&gt;
</span></span><span class="line"><span class="token">      int(314)
</span></span><span class="line"><span class="token">      [1]=&gt;
</span></span><span class="line"><span class="token">      object(SensitiveParameterValue)#3 (0) {
</span></span><span class="line"><span class="token">      }
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">  }
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>As you can see, the <code>tenantId</code> is a non-sensitive parameter, so its value still appears in the stack trace. However, <code>apiKey</code>, the sensitive parameter value, has been wrapped into a <code>SensitiveParameterValue</code> object.</p>
<h2>Flare</h2>
<p>As mentioned, we don't support stack trace arguments in Ignition for security reasons, and this new sensitive parameter feature might let us rethink this. On our roadmap, I've created a new <a href="https://github.com/spatie/flareapp.io-roadmap/discussions/71">feature request</a>, allowing arguments to be shown in Ignition and Flare stack traces. Give it a thumbs up if you want to see this implemented.</p>
]]>
            </summary>
                                    <updated>2023-06-05T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Caching Inertia's SSR responses]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/caching-inertias-ssr-responses" />
            <id>https://flareapp.io/caching-inertias-ssr-responses</id>
            <author>
                <name><![CDATA[Alex]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p><a href="https://inertiajs.com/">Inertia</a> has been widespread among Laravel developers for a few years due to its seamless integration of front-end frameworks like Vue.js and React. However, the recent addition of SSR support in Inertia opens up new possibilities for improving performance and SEO. To further enhance the performance of SSR-rendered responses, we can leverage our very own <a href="https://github.com/spatie/laravel-responsecache">spatie/laravel-responsecache</a> package, a powerful HTTP caching solution for Laravel applications. In this post, we will explore how to integrate and configure the package to cache SSR responses generated by Inertia. We'll also look at some interesting edge cases we discovered when integrating this in Flare 2.0.</p>
<h2>SSR in Inertia: A Quick Introduction</h2>
<p>Server-side rendering (SSR) is a technique that allows the server to generate and deliver fully rendered HTML pages to the browser instead of relying on the browser to execute megabytes of JavaScript to render the content.</p>
<p>In the context of Inertia, this is achieved by passing the Inertia props from Laravel to a Node.js SSR process. This way, we can utilize the server's full performance to quickly pre-render the component's HTML. On the client side, React knows how to handle this SSR response and "re-hydrate" it to add the required interactivity for the React components. While this may sound complicated, Inertia makes integrating SSR easier than ever, even in existing applications (like Flare!).</p>
<p>This entire SSR flow results in faster load times, improved performance, and the ability to use React components in an SEO-friendly way (as the fully rendered HTML content is always readily available for indexing by search engines).</p>
<h2>(Re)-introducing spatie/laravel-responsecache</h2>
<p><code>spatie/laravel-responsecache</code> is a versatile HTTP caching package. While it may be nothing new in 2023, combining it with SSR could be this package's most exciting use case since its release. Inertia with SSR is already pretty fast, but in-memory caching will always be faster.</p>
<p>Apart from easy response caching, the package provides flexible caching strategies, cache invalidation mechanisms, and integration with various parts of your Laravel application. We can now efficiently cache Inertia's SSR responses using some of the above features initially designed for regular HTTP requests, resulting in lower server load and faster subsequent page loads for our users.</p>
<p>Let's take a quick look at how we configured <code>spatie/laravel-responsecache</code> to work with Inertia's SSR features:</p>
<h2>Installation and Configuration</h2>
<p>Installing the response cache package is as easy as installing any other package in Laravel:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token"># use composer to install the package
</span></span><span class="line"><span class="token">composer require spatie/laravel-responsecache
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token"># publish the config file
</span></span><span class="line"><span class="token">php artisan vendor:publish --provider="Spatie\ResponseCache\ResponseCacheServiceProvider"
</span></span></code></pre>
<p>Finally, add the <code>CacheResponse::class</code> middleware to the (Inertia) routes you want cached:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">use \Spatie\ResponseCache\Middlewares\CacheResponse::class;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">Route::get('my-awesome-homepage', HomeController::class)-&gt;middleware(CacheResponse::class);
</span></span></code></pre>
<p>And just like that, you've got basic HTTP response caching in Laravel. However, looking at the <code>laravel-responsecache</code> header in your browser's developer tools, you might notice that Inertia responses are not being cached. That's because Inertia requests are made using Axios (AJAX), and the response cache package avoids caching any AJAX requests.</p>
<h2>A custom cache profile for Inertia requests</h2>
<p>By default, <code>spatie/laravel-responsecache</code> comes configured with the <code>CacheAllSuccessfulGetRequests</code> cache profile. This profile caches (you guessed it!) all successful GET requests. However, less evident from reading the class name, it avoids caching AJAX requests. Because Inertia uses AJAX to request new pages, we need to customize the cache profile to ensure caching of these requests.</p>
<p>We'll do this by creating a new <code>InertiaResponseCacheProfile</code> class and configuring that in the <code>cache_profile</code> option of the  <code>config/responsecache.php</code> config:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">'cache_profile' =&gt; \App\Support\ResponseCache\ResponseCacheProfile::class,
</span></span></code></pre>
<p>The custom cache profile will work almost identically to the original <code>CacheAllSuccessfulGetRequests</code> profile. Here's what it looks like:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">use Illuminate\Http\Request;
</span></span><span class="line"><span class="token">use Spatie\ResponseCache\CacheProfiles\CacheAllSuccessfulGetRequests;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">class InertiaResponseCacheProfile extends CacheAllSuccessfulGetRequests
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">    public function shouldCacheRequest(Request $request): bool
</span></span><span class="line"><span class="token">    {
</span></span><span class="line"><span class="token">        if ($request-&gt;ajax() &amp;&amp; $request-&gt;isMethod('get')) {
</span></span><span class="line"><span class="token">            // Cache Inertia (= AJAX) GET requests!
</span></span><span class="line"><span class="token">            return true;
</span></span><span class="line"><span class="token">        }
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        if ($this-&gt;isRunningInConsole()) {
</span></span><span class="line"><span class="token">            return false;
</span></span><span class="line"><span class="token">        }
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        return $request-&gt;isMethod('get');
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>Testing this new cache profile with Inertia routes will quickly surface its biggest flaw: Inertia will return a normal HTML response on the first request to a particular endpoint and JSON responses on subsequent requests <em>to the same endpoint</em>. The important thing to note is that the response cache will not consider the response's content type. This means that whatever HTML or JSON response is returned will be cached, resulting in situations where HTML is returned when JSON is expected or vice versa.</p>
<h2>Making the response cache aware of content-type</h2>
<p>To deal with Inertia's HTML and JSON responses on the same endpoint, we must make the response cache aware of the content type. We can do this by using a custom hasher class. You can think of the hasher class as a cache key generator: it should generate a unique hash for unique requests (e.g., different content types) but the same hash for similar requests (e.g., <code>GET /?a=1&amp;b=2</code> and <code>GET /?b=2&amp;a=1</code>).</p>
<p>Let's create a custom <code>InertiaResponseCacheHasher</code> hasher class for generating a unique hash that is not only based on the request URL and method but also the request's <code>content-type</code> header:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">use Illuminate\Http\Request;
</span></span><span class="line"><span class="token">use Illuminate\Hashing\DefaultHasher;
</span></span><span class="line"><span class="token">use App\Actions\ResolveCurrencyForCustomerAction;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">class InertiaResponseCacheHasher extends DefaultHasher
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">    public function getHashFor(Request $request): string
</span></span><span class="line"><span class="token">    {
</span></span><span class="line"><span class="token">        $baseHash = parent::getHashFor($request);
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        $contentType = $request-&gt;getContentTypeFormat();
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        return "{$baseHash}-{$contentType}";
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>In this custom response cache hasher, we extend the <code>DefaultHasher</code> provided by the package and override the <code>getHashFor</code> method. We append the <code>content-type</code> to the base hash to maintain a different response cache for each content type.</p>
<p>If you provide different responses based on request parameters, this would be the ideal location to configure it. For instance, Flare's pricing page displays prices in either euros or dollars, depending on the IP location. By incorporating the resolved currency in the response cache key, we can prevent mistakenly serving the incorrect currency to the incorrect location.</p>
<h2>Conclusion</h2>
<p>Combining <code>spatie/laravel-responsecache</code> with Inertia can significantly enhance performance and reduce initial load times. Additionally, it enables us to fully utilize Inertia's SSR support with minimal server load and no SEO penalty. In Flare, we can combine the above to serve our marketing site and documentation as quickly as possible and ensure that it gets indexed by search engines.</p>
]]>
            </summary>
                                    <updated>2023-05-22T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Flare 2.0 is right around the corner]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/flare-20-is-right-around-the-corner" />
            <id>https://flareapp.io/flare-20-is-right-around-the-corner</id>
            <author>
                <name><![CDATA[Spatie]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Flare was launched almost four years ago. In human life, that's a short time, but for an online service, that's a lifetime.</p>
<p>Over the years, we had quite some ideas on how to improve the user experience for Flare. You'll be happy to know that for the past few months, our team has been working hard on Flare 2.0.</p>
<p>Flare 2.0 features:</p>
<ul>
<li>
<p>a major redesign both of the marketing page and the app</p>
</li>
<li>
<p>opt-in AI solutions, which auto-generate solutions based on all the information we have on your error</p>
</li>
<li>
<p>error insights, which give you a clear overview of which jobs, commands, ... errors happened for which users</p>
</li>
<li>
<p>a new onboarding experience that makes it much easier to get started using Flare</p>
</li>
<li>
<p>an avalanche of little improvements and refinements</p>
</li>
</ul>
<p>By rethinking and streamlining the whole process, we hope to have created lots of improvements you'd like. The launch of the Flare 2.0 is planned for early June.</p>
<h2>Get access to Flare 2.0 beta</h2>
<p>We're currently running a closed beta of Flare 2.0. If you want to help us test the new design and features, <a href="mailto:support@flareapp.io">send us a mail</a>.</p>
<p>Your feedback will help us polish everything so all our users have a great first-day experience.</p>
]]>
            </summary>
                                    <updated>2023-05-15T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Changing your larger-than-average MySQL table]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/changing-your-larger-than-average-mysql-table" />
            <id>https://flareapp.io/changing-your-larger-than-average-mysql-table</id>
            <author>
                <name><![CDATA[Ruben]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>We're still working on our redesign and are close to release. Today we started tackling an issue where the performance of the error page was too slow for us. In the end, we needed to change the structure of the error occurrences table, which is a lot harder than it seems.</p>
<p>In the redesign of Flare, we've added a new feature called insights which gives you a quick overview of some statistics like which endpoints triggered the error, which users, which application versions, and so on.</p>
<p><img src="https://assets.flareapp.io/production/blog/a064c084ae99dfcc711c4f14542944ac/7cbf9446-122b-4e32-8dde-38d0155e8876.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/a064c084ae99dfcc711c4f14542944ac/7cbf9446-122b-4e32-8dde-38d0155e8876.png" width="3058" height="600" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/a064c084ae99dfcc711c4f14542944ac/7cbf9446-122b-4e32-8dde-38d0155e8876.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/a064c084ae99dfcc711c4f14542944ac/7cbf9446-122b-4e32-8dde-38d0155e8876.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/a064c084ae99dfcc711c4f14542944ac/7cbf9446-122b-4e32-8dde-38d0155e8876.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/a064c084ae99dfcc711c4f14542944ac/7cbf9446-122b-4e32-8dde-38d0155e8876.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>The counts for these insights are always live calculated, which means queries like this:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">SELECT
</span></span><span class="line"><span class="token">	count(DISTINCT entry_point) AS aggregate
</span></span><span class="line"><span class="token">FROM
</span></span><span class="line"><span class="token">	`error_occurrences`
</span></span><span class="line"><span class="token">WHERE
</span></span><span class="line"><span class="token">	`error_occurrences`.`error_id` = ?
</span></span><span class="line"><span class="token">	AND `error_occurrences`.`error_id` IS NOT NULL
</span></span><span class="line"><span class="token">	AND `entry_point_type` = 'web';
</span></span></code></pre>
<p>Each error occurrence has an <code>entry_point_type</code> (web, queue, command) and an <code>entry_point</code> (a URL, job class, or command). These queries were quite performant on our seeded data, but in production, we have an error that has accumulated 66k occurrences. That's the point where things start to become slow.</p>
<p>Luckily, there's an easy solution to this, indexes! So we've added an index to <code>entry_point_type</code> and saw a performance boost. Later on, we tried adding an index to <code>entry_point</code>, but this exception popped up:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">BLOB/TEXT column 'entry_point' used in key specification without a key length
</span></span></code></pre>
<p>Wait! Are we using a text type column for <code>entry_point</code>? That's a bit crazy. These columns are stored differently than varchars in MySQL, which makes them a lot slower + indexing them has some limitations. Let's fix this using a varchar column, but how many characters should it be?</p>
<p>The first thing we checked was how many characters are required on average. A quick query that counted the number of occurrences for each <code>entry_point</code> length gave us the following:</p>
<p><img src="https://assets.flareapp.io/production/blog/9bdb2f9e6ceec257e71a52a0913cc2b6/bb5da7a8-b65d-4420-a070-c30ebc95aa5c.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/9bdb2f9e6ceec257e71a52a0913cc2b6/bb5da7a8-b65d-4420-a070-c30ebc95aa5c.png" width="2894" height="1210" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/9bdb2f9e6ceec257e71a52a0913cc2b6/bb5da7a8-b65d-4420-a070-c30ebc95aa5c.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/9bdb2f9e6ceec257e71a52a0913cc2b6/bb5da7a8-b65d-4420-a070-c30ebc95aa5c.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/9bdb2f9e6ceec257e71a52a0913cc2b6/bb5da7a8-b65d-4420-a070-c30ebc95aa5c.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/9bdb2f9e6ceec257e71a52a0913cc2b6/bb5da7a8-b65d-4420-a070-c30ebc95aa5c.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>We're pretty safe with 255 characters because almost all entry points for occurrences are around that length. Because a URL is somewhat limited to 2048 characters and varchars smartly assign space (it only takes the space required for a value), we took 2048 characters for this column.</p>
<p>Now changing our column can be done as such:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">ALTER TABLE `flare`.`error_occurrences`
</span></span><span class="line"><span class="token">CHANGE `entry_point` `entry_point` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL;
</span></span></code></pre>
<p>The thing is, this is a large table at the moment. There are 10531985 it.. 10532202 ite.. 10532278 items. You get the point. A lot of items, and the table is constantly growing.</p>
<p>By default, MySQL will make a copy of this table, apply the changes to that table, remove the old table, and use the newly created one instead. This process is extremely slow, and it blocks a lot of statements, completely shutting down Flare. We should avoid this at all costs.</p>
<h2>The trick with the extra column</h2>
<p>There are a few solutions to this problem. In our case, we did the following: we first created a new column called <code>entry_point_2</code> with the correct type:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">ALTER TABLE `flare`.`error_occurrences` 
</span></span><span class="line"><span class="token">ADD COLUMN `entry_point_2` varchar(2048) NULL, ALGORITHM=INSTANT;
</span></span></code></pre>
<p>The MySQL instant algorithm will instantly add the column (it still takes some time, but no locks are required). The problem? When we ran this query, MySQL started copying our table, which was the thing we were trying to avoid.</p>
<p>After some research work in the MySQL documentation, we discovered that these operations will still use the copy algorithm if a fulltext index exists on the table, even when that index isn't used in the operation.</p>
<p>So the easy fix was to drop the fulltext index temporarily:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">ALTER TABLE `flare`.`error_occurrences` 
</span></span><span class="line"><span class="token">DROP INDEX `error_message_fulltext`;
</span></span></code></pre>
<p>Great, we can now add an extra column without too much hassle.</p>
<h2>Moving data</h2>
<p>Next, we move the data from <code>entry_point</code> to <code>entry_point_2</code>. This operation is going to take some time. The cool thing is we can already ensure that newly created occurrences immediately fill <code>entry_point_2</code>.</p>
<p>Doing such a thing would require some changes to the code, then deploying that code, later changing the code, deploying that code again ... too much complexity if you ask me.</p>
<p>MySQL has a feature called triggers, which are small hooks running before or after inserting, updating, or deleting a row. Since we only add error occurrences, an insert trigger was enough to make sure each new occurrence has its <code>entry_point_2</code> set:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">CREATE TRIGGER `flare`.`error_occurrences`
</span></span><span class="line"><span class="token">	BEFORE INSERT ON `error_occurrences`
</span></span><span class="line"><span class="token">	FOR EACH ROW
</span></span><span class="line"><span class="token">	BEGIN
</span></span><span class="line"><span class="token">	SET NEW.`entry_point_2` = NEW.`entry_point`;
</span></span><span class="line"><span class="token">END
</span></span></code></pre>
<p>Now we can update all the other entries within the table which are missing a value for <code>entry_point_2</code>:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">UPDATE
</span></span><span class="line"><span class="token">	error_occurrences
</span></span><span class="line"><span class="token">SET
</span></span><span class="line"><span class="token">	entry_point_2 = entry_point
</span></span><span class="line"><span class="token">WHERE
</span></span><span class="line"><span class="token">	entry_point IS NOT NULL
</span></span><span class="line"><span class="token">	AND entry_point_2 IS NULL
</span></span></code></pre>
<p>There are probably more performant ways to this (it took a whopping 20 minutes to execute this query), but it doesn't add locks to our table and is easy to write.</p>
<h2>Setting everything right</h2>
<p>So we're almost there. We've got identical columns, <code>entry_point</code> of type text and <code>entry_point_2</code> of type varchar(2048). We're going to rename these columns so that:</p>
<ul>
<li>
<p><code>entry_point</code> -&gt; <code>old_entry_point</code></p>
</li>
<li>
<p><code>entry_point_2</code> -&gt; <code>entry_point</code></p>
</li>
</ul>
<p>We can do this instantly like this:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">ALTER TABLE  `flare`.`error_occurrences` 
</span></span><span class="line"><span class="token">RENAME COLUMN entry_point TO old_entry_point, 
</span></span><span class="line"><span class="token">RENAME COLUMN entry_point_2 TO entry_point;
</span></span></code></pre>
<p>We remove the trigger we've created:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">DROP TRIGGER `flare`.`error_occurrences`;
</span></span></code></pre>
<p>And in the end, altogether remove the <code>old_entry_point</code> column:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">ALTER TABLE `flare`.`error_occurrences` 
</span></span><span class="line"><span class="token">DROP COLUMN `old_entry_point`, ALGORITHM=INSTANT;
</span></span></code></pre>
<p>Now we can create an index on the newly created <code>entry_point</code> column:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">ALTER TABLE `flare`.`error_occurrences`
</span></span><span class="line"><span class="token">ADD INDEX `error_occurrences_entry_point_index` (`entry_point`(255)) USING BTREE;
</span></span></code></pre>
<p>And, of course, we also need to create the fulltext index we removed earlier. The thing is, we can only add such an index when nothing is written to the table. To fix this, we temporarily paused Laravel Horizon:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">php artisan horizon:pause
</span></span></code></pre>
<p>Our queues are now paused so that nothing gets written to this table. We don't lose anything thanks to Horizon because the jobs we've added still exist, and they can be executed as soon as we restart Horizon again.</p>
<p>Now we can add the fulltext index as such:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">ALTER TABLE `flare`.`error_occurrences`
</span></span><span class="line"><span class="token">ADD FULLTEXT INDEX `error_message_fulltext` (`exception_message`);
</span></span></code></pre>
<p>And restart horizon:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">php artisan horizon:continue
</span></span></code></pre>
<h2>Conclusion</h2>
<p>A minor fix turned out to be more complicated than expected, but the result is what counts. And our page now got a lot faster. We'll start inviting beta testers soon. Interested? Contact us at support@flareapp.io.</p>
]]>
            </summary>
                                    <updated>2023-05-07T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Fetching data to render complex graphs in Flare]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/fetching-data-to-render-complex-graphs-in-flare" />
            <id>https://flareapp.io/fetching-data-to-render-complex-graphs-in-flare</id>
            <author>
                <name><![CDATA[Ruben]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>When tracking errors within your application, a visual indication of when errors are happing is essential in the debugging experience we're trying to provide.</p>
<p>Flare already had graphs on errors happening per project. Here's how that looks like:</p>
<p><img src="https://assets.flareapp.io/production/blog/12bce72ab82c5ac5028af19291a5f9b4/9d8d9196-04e8-42fe-b828-41f409264dba.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/12bce72ab82c5ac5028af19291a5f9b4/9d8d9196-04e8-42fe-b828-41f409264dba.png" width="3064" height="1042" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/12bce72ab82c5ac5028af19291a5f9b4/9d8d9196-04e8-42fe-b828-41f409264dba.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/12bce72ab82c5ac5028af19291a5f9b4/9d8d9196-04e8-42fe-b828-41f409264dba.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/12bce72ab82c5ac5028af19291a5f9b4/9d8d9196-04e8-42fe-b828-41f409264dba.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/12bce72ab82c5ac5028af19291a5f9b4/9d8d9196-04e8-42fe-b828-41f409264dba.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>Now, we've also added graphs per error. So you can easily see when a particular error start occurring, when it was spiking, and more. Here how such a graph looks like.</p>
<p><img src="https://assets.flareapp.io/production/blog/6622ae63c612b8797962afcc7a4e5dcc/4a129b0e-dce8-4fbe-b38c-3b1b7ed74603.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/6622ae63c612b8797962afcc7a4e5dcc/4a129b0e-dce8-4fbe-b38c-3b1b7ed74603.png" width="3176" height="1994" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/6622ae63c612b8797962afcc7a4e5dcc/4a129b0e-dce8-4fbe-b38c-3b1b7ed74603.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/6622ae63c612b8797962afcc7a4e5dcc/4a129b0e-dce8-4fbe-b38c-3b1b7ed74603.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/6622ae63c612b8797962afcc7a4e5dcc/4a129b0e-dce8-4fbe-b38c-3b1b7ed74603.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/6622ae63c612b8797962afcc7a4e5dcc/4a129b0e-dce8-4fbe-b38c-3b1b7ed74603.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>These graphs count the number of occurrences of each error in a project. Sometimes we have five occurrences for an error, but there are situations where we have 40k occurrences. Our database already does a terrific job of crunching all this data. But this still can take a while.</p>
<p>We wanted to keep Flare fast and, more importantly, have a single approach to how we did these calculations and smartly cached them.</p>
<h2>Requirements</h2>
<p>We have four points in our application where we show graphs:</p>
<ul>
<li>
<p>On the projects view, we show a graph of occurrences per project per day</p>
</li>
<li>
<p>On the project errors view, we show a graph of all occurrences for the project per minute scoped by the current search query</p>
</li>
<li>
<p>Also, on the project errors view, for each error, we show a graph of occurrences per day</p>
</li>
<li>
<p>On an error-specific page, the graph we mentioned above is also shown</p>
</li>
</ul>
<p>Let's see what we need:</p>
<ul>
<li>
<p>We should be able to get graphs for a single or multiple items</p>
</li>
<li>
<p>Graphs are per day, sometimes per minute</p>
</li>
<li>
<p>A graph can be changed by external parameters (like the search query)</p>
</li>
<li>
<p>We want everything blazingly fast ⚡️</p>
</li>
</ul>
<h2>Caching</h2>
<p>The best way to solve this is by caching graphs and using the cached values on the following requests. The only problem: Flare is a real-time product. You can have a graph of zero errors for the whole month, and suddenly due to some bug within your code, that graph could have hundreds of occurrences.</p>
<p>We don't want to wait for the next day to show you that these occurrences came in. You want to see these graphs in real time.</p>
<p>That's why we're using a combination of caching and live data. For all the graphs, we cache the data of the retention period you've got in your subscription except the current day. For the current day, we'll do live data crunching to calculate how many occurrences have passed by.</p>
<p>Such caching will work since new errors will only come in today, which is queried by the live statistics.</p>
<p>A complete cache bust should happen when an error is deleted, resolved, or unresolved because these actions are destructive to our data from the past. We'll need to regenerate them again on the subsequent request.</p>
<h2>The implementation</h2>
<p>We've constructed an abstract base class, <code>MultiOccurrenceStats</code>, from which three classes extend:</p>
<ul>
<li>
<p><code>ErrorOccurrenceStatsPerDay</code> for the error view and the project errors view</p>
</li>
<li>
<p><code>ProjectErrorOccurrenceStatsPerMinute</code> for the project errors view</p>
</li>
<li>
<p><code>ProjectErrorOccurrenceStatsPerDay</code> for the projects view</p>
</li>
</ul>
<p>The base class has a <code>fetch</code> method, allowing us to fetch data for multiple subjects(errors or projects). We return an array for each subject with as key a UNIX timestamp and as value the number of occurrences around that timestamp:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">/**
</span></span><span class="line"><span class="token"> * @param  int[]  $subjectIds
</span></span><span class="line"><span class="token"> * @return array&lt;int, array&lt;int, int&gt;&gt;
</span></span><span class="line"><span class="token"> */
</span></span><span class="line"><span class="token">public function fetch(
</span></span><span class="line"><span class="token">    array $subjectIds,
</span></span><span class="line"><span class="token">    CarbonImmutable $start,
</span></span><span class="line"><span class="token">    CarbonImmutable $end,
</span></span><span class="line"><span class="token">): array {
</span></span><span class="line"><span class="token">    // ... 
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>Each subclass should also implement a few methods:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">//provides the key to storing statistics for a subject
</span></span><span class="line"><span class="token">abstract protected function cacheKey(int $subjectId): string;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">// The time the graph is cached
</span></span><span class="line"><span class="token">abstract protected function cacheTtl(): int;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">// A query to fetch the statistics
</span></span><span class="line"><span class="token">abstract protected function performQuery(): Builder;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">// The subject id in the query above. Please continue reading to know why we need this
</span></span><span class="line"><span class="token">abstract protected function querySubjectIdColumn(): string;
</span></span></code></pre>
<p>Now we can call the <code>fetch</code> method with</p>
<ul>
<li>on a single subject like the project:</li>
</ul>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">fetch([$project-&gt;id], $project-&gt;retentionStart(), $project-&gt;retentionEnd());
</span></span></code></pre>
<ul>
<li>for multiple subjects like the errors in a project:</li>
</ul>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">fetch($subjectIds, $start, $end);
</span></span></code></pre>
<p>How is it implemented? First, we map our subject ids to the keys within our cache:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">$cacheKeys = array_map(
</span></span><span class="line"><span class="token">    fn (int $subjectId) =&gt; $this-&gt;cacheKey($subjectId),
</span></span><span class="line"><span class="token">    $subjectIds
</span></span><span class="line"><span class="token">);
</span></span></code></pre>
<p>We split the retention period into a cacheable and non-cacheable period:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">[$cachablePeriod, $nonCacheablePeriod] = $this-&gt;splitPeriodInCacheableAndNonCacheable($retentionPeriod);
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">// calls:
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">function splitPeriodInCacheableAndNonCacheable(StatsPeriod $retentionPeriod): array
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">    return [
</span></span><span class="line"><span class="token">        new StatsPeriod(
</span></span><span class="line"><span class="token">            $retentionPeriod-&gt;start-&gt;startOfDay(),
</span></span><span class="line"><span class="token">            CarbonImmutable::yesterday()-&gt;endOfDay(),
</span></span><span class="line"><span class="token">        ),
</span></span><span class="line"><span class="token">        new StatsPeriod(
</span></span><span class="line"><span class="token">            CarbonImmutable::now()-&gt;startOfDay(),
</span></span><span class="line"><span class="token">            $retentionPeriod-&gt;end-&gt;endOfDay(),
</span></span><span class="line"><span class="token">        ),
</span></span><span class="line"><span class="token">    ];
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>Now we want to check what's been cached and what isn't. We store a <code>CachedStats</code> object within our cache:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">class CachedStats
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">    /**
</span></span><span class="line"><span class="token">     * @param  array&lt;int, int&gt;  $stats
</span></span><span class="line"><span class="token">     */
</span></span><span class="line"><span class="token">    public function __construct(
</span></span><span class="line"><span class="token">        public StatsPeriod $period,
</span></span><span class="line"><span class="token">        public array $stats,
</span></span><span class="line"><span class="token">    ) {
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>This object indicates whether cached graphs exist for a specific subject and for which period these were cached. Then, we will split our subjects into two categories: subjects for which a correct cached period exists and subjects without cached graphs.</p>
<p>Notice the <code>cache()-&gt;many()</code> method. It queries all cache keys at once, which is a bit faster. Keys that do not exist will return <code>null</code>.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">/** @var \Illuminate\Support\Collection&lt;int, null&gt; $nonCachedSubjects */
</span></span><span class="line"><span class="token">$nonCachedSubjects = collect();
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">/** @var \Illuminate\Support\Collection&lt;int, \App\Domain\Project\Stats\CachedStats&gt; $cachedSubjects */
</span></span><span class="line"><span class="token">$cachedSubjects = collect();
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">foreach (array_values(cache()-&gt;many($cacheKeys)) as $i =&gt; $cached) {
</span></span><span class="line"><span class="token">    $subjectId = $subjectIds[$i];
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    if (
</span></span><span class="line"><span class="token">        $cached === null
</span></span><span class="line"><span class="token">        || ! $cached instanceof CachedStats
</span></span><span class="line"><span class="token">        || ! $cached-&gt;period-&gt;equals($cachablePeriod)
</span></span><span class="line"><span class="token">    ) {
</span></span><span class="line"><span class="token">        $nonCachedSubjects[$subjectId] = null;
</span></span><span class="line"><span class="token">    } else {
</span></span><span class="line"><span class="token">        $cachedSubjects[$subjectId] = $cached;
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>From this point, the individual classes which extend from this base class become important. Fetching the data for graphs is different depending on what's exactly needed. From now on, we'll implement the stats for errors on the projects' errors page.</p>
<p>This is the base query required for these stats:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">DB::table('error_occurrences')
</span></span><span class="line"><span class="token">    -&gt;selectSub('error_occurrences.error_id', 'subject_id')
</span></span><span class="line"><span class="token">    -&gt;selectRaw('CAST(UNIX_TIMESTAMP(DATE_FORMAT(received_at, "%Y-%m-%d 00:00:00")) * 1000 AS UNSIGNED) AS timestamp, count(*) as count')
</span></span><span class="line"><span class="token">    -&gt;join('errors', 'error_occurrences.error_id', 'errors.id')
</span></span><span class="line"><span class="token">    -&gt;where('errors.status', Status::Open);
</span></span></code></pre>
<p>This query is missing some parts. We're not grouping anything or scoping the query onto our subjects. Within the <code>fetch</code> method, another part is added:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">$query = $this-&gt;performQuery()
</span></span><span class="line"><span class="token">    -&gt;groupBy('timestamp', 'subject_id')
</span></span><span class="line"><span class="token">    -&gt;where(fn (Builder $builder) =&gt; $builder
</span></span><span class="line"><span class="token">        -&gt;when($nonCachedSubjects-&gt;isNotEmpty(), fn (Builder $builder) =&gt; $builder
</span></span><span class="line"><span class="token">            -&gt;where(fn (Builder $builder) =&gt; $builder
</span></span><span class="line"><span class="token">                -&gt;whereIn($this-&gt;querySubjectIdColumn(), $nonCachedSubjects-&gt;keys())
</span></span><span class="line"><span class="token">                -&gt;whereBetween('received_at', [$retentionPeriod-&gt;start, $retentionPeriod-&gt;end])
</span></span><span class="line"><span class="token">            )
</span></span><span class="line"><span class="token">        )
</span></span><span class="line"><span class="token">        -&gt;when($cachedSubjects-&gt;isNotEmpty(), fn (Builder $builder) =&gt; $builder
</span></span><span class="line"><span class="token">            -&gt;orWhere(fn (Builder $builder) =&gt; $builder
</span></span><span class="line"><span class="token">                -&gt;whereIn($this-&gt;querySubjectIdColumn(), $cachedSubjects-&gt;keys())
</span></span><span class="line"><span class="token">                -&gt;whereBetween('received_at', [$nonCacheablePeriod-&gt;start, $nonCacheablePeriod-&gt;end])
</span></span><span class="line"><span class="token">            )
</span></span><span class="line"><span class="token">        )
</span></span><span class="line"><span class="token">    )
</span></span><span class="line"><span class="token">    -&gt;orderBy('timestamp');
</span></span></code></pre>
<p>These extra statements will:</p>
<ul>
<li>
<p>Group counted occurrences by their timestamp</p>
</li>
<li>
<p>Select for the non-cached subjects the whole retention period</p>
</li>
<li>
<p>Select the cached subjects for one day: today</p>
</li>
<li>
<p>Order the results by their timestamp</p>
</li>
</ul>
<p>Notice the <code>$this-&gt;querySubjectIdColumn()</code> call we discussed earlier. It selects occurrences based on the subject and will, in this case, be <code>error_occurrences.error_id</code>.</p>
<p>This massive query is now executed, and we group the results in a Laravel Collection by its subject:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">$stats = $query-&gt;get()
</span></span><span class="line"><span class="token">	-&gt;groupBy('subject_id')
</span></span><span class="line"><span class="token">	-&gt;map(fn (Collection $subjectStats) =&gt; $subjectStats-&gt;mapWithKeys(
</span></span><span class="line"><span class="token">	    fn (object $row) =&gt; [$row-&gt;timestamp =&gt; $row-&gt;count]
</span></span><span class="line"><span class="token">	)-&gt;all());
</span></span></code></pre>
<p>At this point, our flow splits into two based on whether a subject has cached values.</p>
<h3>The non-cached subjects</h3>
<p>We will first cache the freshly calculated counts so they can be reused in the next request. We'll start by filtering all the stats we've queried and only use stats before today which have not yet been cached:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">$cacheableStats = $stats
</span></span><span class="line"><span class="token">    -&gt;filter(fn (array $stats, int $subjectId) =&gt; $nonCachedSubjects-&gt;has($subjectId))
</span></span><span class="line"><span class="token">    -&gt;map(
</span></span><span class="line"><span class="token">        fn (array $stats) =&gt; $this-&gt;filterStatsForPeriod($cachablePeriod, $stats)
</span></span><span class="line"><span class="token">    );
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">// Calls:
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">/**
</span></span><span class="line"><span class="token"> * @return array&lt;int, int&gt;
</span></span><span class="line"><span class="token"> */
</span></span><span class="line"><span class="token">private function filterStatsForPeriod(
</span></span><span class="line"><span class="token">    StatsPeriod $period,
</span></span><span class="line"><span class="token">    array $stats
</span></span><span class="line"><span class="token">): array {
</span></span><span class="line"><span class="token">    $min = $period-&gt;start-&gt;getPreciseTimestamp(3);
</span></span><span class="line"><span class="token">    $max = $period-&gt;end-&gt;getPreciseTimestamp(3);
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    $filtered = [];
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    foreach ($stats as $timestamp =&gt; $count) {
</span></span><span class="line"><span class="token">        if ($min &lt;= $timestamp &amp;&amp; $timestamp &lt; $max) {
</span></span><span class="line"><span class="token">            $filtered[$timestamp] = $count;
</span></span><span class="line"><span class="token">        }
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    return $filtered;
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>Then we're going to cache these stats if we have any:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">$cacheValues = $cacheableStats-&gt;mapWithKeys(fn (array $stats, int $subjectId) =&gt; [
</span></span><span class="line"><span class="token">    $this-&gt;cacheKey($subjectId) =&gt; new CachedStats($cachablePeriod, $stats),
</span></span><span class="line"><span class="token">]);
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">if ($cacheValues-&gt;isNotEmpty()) {
</span></span><span class="line"><span class="token">    cache()-&gt;putMany($cacheValues-&gt;all(), $this-&gt;cacheTtl());
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>In the end, we put all the stats we've queried within an array per subject:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">$nonCachedStats = $nonCachedSubjects-&gt;map(
</span></span><span class="line"><span class="token">    fn (null $null, int $subjectId) =&gt; $stats-&gt;get($subjectId, [])
</span></span><span class="line"><span class="token">)-&gt;all();
</span></span></code></pre>
<h3>The cached subjects</h3>
<p>For the cached subjects, we're going to take the cached stats and append the freshly added stats:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">$cachedStats = $cachedSubjects-&gt;map(function (CachedStats $cachedStats, int $subjectId) use ($stats) {
</span></span><span class="line"><span class="token">    if (! $stats-&gt;has($subjectId)) {
</span></span><span class="line"><span class="token">        return $cachedStats-&gt;stats;
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    return $stats-&gt;get($subjectId) + $cachedStats-&gt;stats;
</span></span><span class="line"><span class="token">})-&gt;all();
</span></span></code></pre>
<p>That + within the code might look weird. Why not use <code>array_merge()</code>? In this case, the problem with <code>array_merge</code> is that it removes the keys, which are our timestamps. Crucial information we want to keep.</p>
<p>The + operation between arrays won't lose these keys but is sometimes considered dangerous because when a key overlaps, the left-hand side value is used. That's why we've put the most recent data on the left-hand side of this expression. Should something go wrong, we'll always have the most up-to-date data.</p>
<p>The latest line of the <code>fetch</code> method looks like this:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">return $nonCachedStats + $cachedStats;
</span></span></code></pre>
<p>We've successfully and efficiently calculated our stats!</p>
<h2>Future improvements</h2>
<p>We'll use this version for the following weeks. Some improvements can be made, though:</p>
<ul>
<li>
<p>Make the caching interval larger for minute stats. We could cache till an hour ago instead of the start of the day, thus reducing the query</p>
</li>
<li>
<p>Instead of requiring the exact cache period (start of retention period until yesterday), we could accept each cached period and then add the missing data in between when we're querying the most recent data</p>
</li>
<li>
<p>Make more use of lazy collections since we're working with a lot of data</p>
</li>
<li>
<p>When using arrays, pass them through with references. Which should eliminate some memory copies</p>
</li>
<li>
<p>Probably other stuff we can't think of right now 😀</p>
</li>
</ul>
<h2>In the end</h2>
<p>The Flare redesign is coming together. We hope to start inviting beta testers soon. Want to join this group? Send a mail to info@flareapp.io.</p>
<p>Now is the perfect time to try out Flare. You can <a href="https://flareapp.io">register here</a> for a free ten-day trial; no credit card is required.</p>
]]>
            </summary>
                                    <updated>2023-04-18T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Optimizing Flare]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/optimizing-flare" />
            <id>https://flareapp.io/optimizing-flare</id>
            <author>
                <name><![CDATA[Ruben]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Our whole team is currently working hard on the next version of Flare. We're completely redesigning the application and website, and it is an immense effort, but it will undoubtedly pay off in the future.</p>
<p>In this rewrite of Flare, we decided to move from Laravel resources to our own Laravel data package for sending data to the front end. Doing this not only adds the benefit of a fully typed response, but we also get TypeScript definitions for our resources without extra effort.</p>
<p>Once all resources were manually converted into data objects, we were excited to check if everything worked fine. Though we didn't hit on any exceptions or errors, some application pages felt extremely slow, too slow to be useful for our clients.</p>
<p>We quickly found that much of the response time was spent on the data objects. We started by optimizing the data objects using the lazy functionality, which allows loading certain parts of the data later on when the initial page is already sent. It made the pages faster, but we didn't want to stop there since we're completely redesigning the whole application.</p>
<p>The laravel-data package is fantastic to work with, but it also adds a lot of complexity when outputting data. In this blog post, we will look at how we've improved the performance of the package and, thus, the complete Flare application.</p>
<h2>New tools to play with</h2>
<p>To get started, I will introduce you to three tools you can use within your toolset to research performance problems: Xdebug, PHPBench, and QCacheGrind.</p>
<p>Xdebug is a PHP extension that provides debugging and profiling capabilities. It is most famous for its complicated configuration, which may slow down your whole application. Luckily, Xdebug version 3 made extensive progress on this part, and setting up Xdebug is now relatively easy.</p>
<p>PHPBench is a benchmarking tool for PHP that can help you measure the performance of your code. It allows you to create test cases and run them multiple times to get a notion of the speed of your code.</p>
<p>Lastly, QCacheGrind is an application allowing you to look at the profiles generated by Xdebug visually. QCacheGrind is available for MacOS and Windows. If you're on Linux, look at KCachegrind, which is almost the same.</p>
<h2>Getting a baseline</h2>
<p>To start, we need to be able to test if the changes we make to our code have a positive or negative impact on the performance. The best way to do this is by creating a few benchmarks using PHPBench. These benchmarks will be run multiple times, and the average time this take can be used as a metric to see if we've made a difference by updating the code.</p>
<p>PHPBench works a lot like PHPUnit. You create a few benchmark files (like tests) with benchmark cases (like test cases). The PHPBench command will then execute these files like the PHPUnit command would. Only it executes the cases multiple times and tracks how long it takes.</p>
<p>We get started as usual by installing PHPBench:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">composer require phpbench/phpbench --dev
</span></span></code></pre>
<p>Next, we create a <code>phpbench.json</code> file which configures PHPbench:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">     "$schema":"./vendor/phpbench/phpbench/phpbench.schema.json",
</span></span><span class="line"><span class="token">     "runner.bootstrap": "vendor/autoload.php",
</span></span><span class="line"><span class="token">     "runner.path" : "benchmarks"
</span></span><span class="line"><span class="token"> }
</span></span></code></pre>
<p>The <code>runner.path</code> option defines the directory where our benchmarks are located, so we create a new directory for them:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">mkdir benchmarks
</span></span></code></pre>
<p>The last thing to do is create the benchmark. We call this file <code>ExampleBench.php</code>. Notice the <code>Bench</code> suffix. Using this suffix is essential because, otherwise, PHPBench won't execute the file. Within the <code>ExampleBench.php</code>, we add an <code>ExampleBench </code> class:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">class ExampleBench
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">     
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>Let's create a first benchmark:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">use PhpBench\Attributes\Iterations;
</span></span><span class="line"><span class="token">use PhpBench\Attributes\Revs;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">class ExampleBench
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">    #[Revs(500), Iterations(5)]
</span></span><span class="line"><span class="token">    public function benchPow()
</span></span><span class="line"><span class="token">    {
</span></span><span class="line"><span class="token">		pow(2, 10);
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>As you can see, we will benchmark how long it takes to calculate 2^10. The benchmark case we've added is called <code>benchPow</code>. Notice the <code>bench</code> keyword prefixed, which is required for PHPBench to find the benchmark case.</p>
<p>We've also added two attributes to the method:</p>
<p>Revs, short for revolutions, refers to the number of times the benchmark is executed consecutively within a single time measurement. The more revolutions, the more accurate the result. In this case, we will calculate 2^10 a whopping 500 times!</p>
<p>In an ideal world, we're done and can start measuring. But running these revolutions multiple times is safer and more correct to ensure our measurements are stable and don't differ too much.</p>
<p>We have five iterations of 500 revolutions so that the pow function will be executed for 5 * 500 = 2500 times.</p>
<p>Now it is time to run PHPBench:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">vendor/bin/phpbench run  --report=default
</span></span></code></pre>
<p>And we get the following result:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">PHPBench (1.2.9) running benchmarks... #standwithukraine
</span></span><span class="line"><span class="token">with configuration file: /Users/ruben/Spatie/laravel-data/phpbench.json
</span></span><span class="line"><span class="token">with PHP version 8.2.3, xdebug ✔, opcache ❌
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">\ExampleBench
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    benchPow................................I4 - Mo0.038μs (±3.33%)
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">Subjects: 1, Assertions: 0, Failures: 0, Errors: 0
</span></span><span class="line"><span class="token">+------+--------------+----------+-----+------+------------+----------+--------------+----------------+
</span></span><span class="line"><span class="token">| iter | benchmark    | subject  | set | revs | mem_peak   | time_avg | comp_z_value | comp_deviation |
</span></span><span class="line"><span class="token">+------+--------------+----------+-----+------+------------+----------+--------------+----------------+
</span></span><span class="line"><span class="token">| 0    | ExampleBench | benchPow |     | 500  | 1,812,376b | 0.036μs  | -1.58σ       | -5.26%         |
</span></span><span class="line"><span class="token">| 1    | ExampleBench | benchPow |     | 500  | 1,812,376b | 0.038μs  | +0.00σ       | +0.00%         |
</span></span><span class="line"><span class="token">| 2    | ExampleBench | benchPow |     | 500  | 1,812,376b | 0.038μs  | +0.00σ       | +0.00%         |
</span></span><span class="line"><span class="token">| 3    | ExampleBench | benchPow |     | 500  | 1,812,376b | 0.038μs  | +0.00σ       | +0.00%         |
</span></span><span class="line"><span class="token">| 4    | ExampleBench | benchPow |     | 500  | 1,812,376b | 0.040μs  | +1.58σ       | +5.26%         |
</span></span><span class="line"><span class="token">+------+--------------+----------+-----+------+------------+----------+--------------+----------------+
</span></span></code></pre>
<p>It looks like 0.038μs is the best average we have. Let's see what's the fastest:</p>
<ol>
<li>
<p>2^20</p>
</li>
<li>
<p>2^10 * 2^10</p>
</li>
</ol>
<p>Our benchmark now will look like this:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">class ExampleBench
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">    #[Revs(500), Iterations(5)]
</span></span><span class="line"><span class="token">    public function benchPow()
</span></span><span class="line"><span class="token">    {
</span></span><span class="line"><span class="token">        pow(2, 20);
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    #[Revs(500), Iterations(5)]
</span></span><span class="line"><span class="token">    public function benchPowPow()
</span></span><span class="line"><span class="token">    {
</span></span><span class="line"><span class="token">        pow(2, 10) * pow(2, 10);
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>Running PHPBench gives the following results:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">PHPBench (1.2.9) running benchmarks... #standwithukraine
</span></span><span class="line"><span class="token">with configuration file: /Users/ruben/Spatie/laravel-data/phpbench.json
</span></span><span class="line"><span class="token">with PHP version 8.2.3, xdebug ✔, opcache ❌
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">\ExampleBench
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    benchPow................................I4 - Mo0.039μs (±3.78%)
</span></span><span class="line"><span class="token">    benchPowPow.............................I4 - Mo0.060μs (±11.25%)
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">Subjects: 2, Assertions: 0, Failures: 0, Errors: 0
</span></span><span class="line"><span class="token">+------+--------------+-------------+-----+------+------------+----------+--------------+----------------+
</span></span><span class="line"><span class="token">| iter | benchmark    | subject     | set | revs | mem_peak   | time_avg | comp_z_value | comp_deviation |
</span></span><span class="line"><span class="token">+------+--------------+-------------+-----+------+------------+----------+--------------+----------------+
</span></span><span class="line"><span class="token">| 0    | ExampleBench | benchPow    |     | 500  | 1,812,376b | 0.040μs  | +0.27σ       | +1.01%         |
</span></span><span class="line"><span class="token">| 1    | ExampleBench | benchPow    |     | 500  | 1,812,376b | 0.042μs  | +1.60σ       | +6.06%         |
</span></span><span class="line"><span class="token">| 2    | ExampleBench | benchPow    |     | 500  | 1,812,376b | 0.038μs  | -1.07σ       | -4.04%         |
</span></span><span class="line"><span class="token">| 3    | ExampleBench | benchPow    |     | 500  | 1,812,376b | 0.038μs  | -1.07σ       | -4.04%         |
</span></span><span class="line"><span class="token">| 4    | ExampleBench | benchPow    |     | 500  | 1,812,376b | 0.040μs  | +0.27σ       | +1.01%         |
</span></span><span class="line"><span class="token">| 0    | ExampleBench | benchPowPow |     | 500  | 1,812,376b | 0.076μs  | +1.47σ       | +16.56%        |
</span></span><span class="line"><span class="token">| 1    | ExampleBench | benchPowPow |     | 500  | 1,812,376b | 0.072μs  | +0.93σ       | +10.43%        |
</span></span><span class="line"><span class="token">| 2    | ExampleBench | benchPowPow |     | 500  | 1,812,376b | 0.060μs  | -0.71σ       | -7.98%         |
</span></span><span class="line"><span class="token">| 3    | ExampleBench | benchPowPow |     | 500  | 1,812,376b | 0.060μs  | -0.71σ       | -7.98%         |
</span></span><span class="line"><span class="token">| 4    | ExampleBench | benchPowPow |     | 500  | 1,812,376b | 0.058μs  | -0.98σ       | -11.04%        |
</span></span><span class="line"><span class="token">+------+--------------+-------------+-----+------+------------+----------+--------------+----------------+
</span></span></code></pre>
<p>So basically, it costs 50% more time to calculate two <code>pow(2, 10)</code> functions than one <code>pow(2, 20)</code>, that's quite useful!</p>
<p>Now back to laravel-data, we want to know how fast we can create a data object and how fast we can transform it into a JSON object. We can describe this as follows:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">class DataBench
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">	#[Revs(500), Iterations(2)]
</span></span><span class="line"><span class="token">    public function benchDataCreation()
</span></span><span class="line"><span class="token">    {
</span></span><span class="line"><span class="token">        MultiNestedData::from([
</span></span><span class="line"><span class="token">            'nested' =&gt; ['simple' =&gt; 'Hello'],
</span></span><span class="line"><span class="token">            'nestedCollection' =&gt; [
</span></span><span class="line"><span class="token">                ['simple' =&gt; 'Flare'],
</span></span><span class="line"><span class="token">                ['simple' =&gt; 'is'],
</span></span><span class="line"><span class="token">                ['simple' =&gt; 'awesome'],
</span></span><span class="line"><span class="token">            ],
</span></span><span class="line"><span class="token">        ]);
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    #[Revs(500), Iterations(2)]
</span></span><span class="line"><span class="token">    public function benchDataTransformation()
</span></span><span class="line"><span class="token">    {
</span></span><span class="line"><span class="token">        $data = new MultiNestedData(
</span></span><span class="line"><span class="token">            new NestedData(new SimpleData('Hello')),
</span></span><span class="line"><span class="token">            new DataCollection(NestedData::class, [
</span></span><span class="line"><span class="token">                new NestedData(new SimpleData('Flare')),
</span></span><span class="line"><span class="token">                new NestedData(new SimpleData('is')),
</span></span><span class="line"><span class="token">                new NestedData(new SimpleData('awesome')),
</span></span><span class="line"><span class="token">            ])
</span></span><span class="line"><span class="token">        );
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        $data-&gt;toArray();
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    #[Revs(500), Iterations(2)]
</span></span><span class="line"><span class="token">    public function benchDataCollectionCreation()
</span></span><span class="line"><span class="token">    {
</span></span><span class="line"><span class="token">        $collection = Collection::times(
</span></span><span class="line"><span class="token">            15,
</span></span><span class="line"><span class="token">            fn() =&gt; [
</span></span><span class="line"><span class="token">                'nested' =&gt; ['simple' =&gt; 'Hello'],
</span></span><span class="line"><span class="token">                'nestedCollection' =&gt; [
</span></span><span class="line"><span class="token">                    ['simple' =&gt; 'Flare'],
</span></span><span class="line"><span class="token">                    ['simple' =&gt; 'is'],
</span></span><span class="line"><span class="token">                    ['simple' =&gt; 'awesome'],
</span></span><span class="line"><span class="token">                ],
</span></span><span class="line"><span class="token">            ]
</span></span><span class="line"><span class="token">        )-&gt;all();
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        MultiNestedData::collection($collection);
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    #[Revs(500), Iterations(2)]
</span></span><span class="line"><span class="token">    public function benchDataCollectionTransformation()
</span></span><span class="line"><span class="token">    {
</span></span><span class="line"><span class="token">        $collection = Collection::times(
</span></span><span class="line"><span class="token">            15,
</span></span><span class="line"><span class="token">            fn() =&gt; new MultiNestedData(
</span></span><span class="line"><span class="token">                new NestedData(new SimpleData('Hello')),
</span></span><span class="line"><span class="token">                new DataCollection(NestedData::class, [
</span></span><span class="line"><span class="token">                    new NestedData(new SimpleData('Flare')),
</span></span><span class="line"><span class="token">                    new NestedData(new SimpleData('is')),
</span></span><span class="line"><span class="token">                    new NestedData(new SimpleData('awesome')),
</span></span><span class="line"><span class="token">                ])
</span></span><span class="line"><span class="token">            )
</span></span><span class="line"><span class="token">        )-&gt;all();
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        $collection = MultiNestedData::collection($collection);
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        $collection-&gt;toArray();
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>In the first benchmark, we're creating a data object using arrays. In the second one, we created an object as efficiently as possible by manually defining it and then transforming it to JSON.</p>
<p>We also added two cases where we do the same, but instead of using data objects, we benchmark using data collections of multiple data objects.</p>
<p>Now running PHPBench will fail because the laravel-data package depends on some Laravel functionality. Luckily Laravel provides excellent testing infrastructure, which we can use within our benchmark. We do this by using the <code>CreatesApplication</code> trait, which is also present in the base Laravel test case:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">use Tests\CreatesApplication;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">class DataBench
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">    use CreatesApplication;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    public function __construct()
</span></span><span class="line"><span class="token">    {
</span></span><span class="line"><span class="token">        $this-&gt;createApplication();
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">    
</span></span><span class="line"><span class="token">    // The benchmarks
</span></span><span class="line"><span class="token">}   
</span></span></code></pre>
<p>We need another update because we're benchmarking a laravel package, not a laravel application. This means we must use the <code>CreatesApplication</code> trait from the <code>orchestra/testbench</code> package, which is used to test Laravel packages. And we also need to specify the laravel-data service provider to boot up the package:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">use Orchestra\Testbench\Concerns\CreatesApplication;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">class DataBench
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">    use CreatesApplication;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    public function __construct()
</span></span><span class="line"><span class="token">    {
</span></span><span class="line"><span class="token">        $this-&gt;createApplication();
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    protected function getPackageProviders($app)
</span></span><span class="line"><span class="token">    {
</span></span><span class="line"><span class="token">        return [
</span></span><span class="line"><span class="token">            LaravelDataServiceProvider::class,
</span></span><span class="line"><span class="token">        ];
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">    
</span></span><span class="line"><span class="token">    // The benchmarks
</span></span><span class="line"><span class="token">} 
</span></span></code></pre>
<p>Great, we now have some benchmarks ready to measure the speed of our code!</p>
<h2>Getting started with Xdebug</h2>
<p>Next up, we need to profile our code. When we're profiling our code, we will run it a bit differently than like would typically. When profiling the PHP process will write down each function call we make and also keep track of how long it takes to run the function. Ultimately, all this information will be written into a file we can analyze.</p>
<p>To get this working, we need to install and enable Xdebug. You can find the instructions <a href="https://xdebug.org/docs/install">here</a>.</p>
<p>For me, on a Mac, it was as simple as this:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">sudo pecl install xdebug
</span></span></code></pre>
<p>We also need to update or create the Xdebug .ini file:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">[xdebug]
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">xdebug.mode=profile
</span></span><span class="line"><span class="token">xdebug.start_with_request=yes
</span></span></code></pre>
<p>On my Mac, I've put this file here: <code>nano /opt/homebrew/etc/php/8.2/conf.d/xdebug.ini</code>.</p>
<p>We configure two options:</p>
<ul>
<li>
<p><code>xdebug.mode=profile</code> to enable profiling in Xdebug.</p>
</li>
<li>
<p><code>xdebug.start_with_request=yes</code> to start profiling with PHPBench. When you're done with profiling, don't forget to set this to <code>off</code> or <code>trigger</code>. Otherwise, all your PHP applications will take ages to load.</p>
</li>
</ul>
<p>We have our benchmarks, and we have our profiler. Now we only need to combine those two, and then we can discover why our code is so slow!</p>
<h2>Profiling the code</h2>
<p>We will rerun our benchmarks, but this time we will also profile the benchmarks. We can do this with the following command:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">vendor/bin/phpbench xdebug:profile
</span></span></code></pre>
<p>Which gives the following output:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">PHPBench (1.2.9) running benchmarks... #standwithukraine
</span></span><span class="line"><span class="token">with configuration file: /Users/ruben/Spatie/laravel-data/phpbench.json
</span></span><span class="line"><span class="token">with PHP version 8.2.3, xdebug ✔, opcache ❌
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">\DataBench
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    benchDataCreation.......................I0 - Mo10.762ms (±0.00%)
</span></span><span class="line"><span class="token">    benchDataTransformation.................I0 - Mo1.095ms (±0.00%)
</span></span><span class="line"><span class="token">    benchDataCollectionCreation.............I0 - Mo159.879ms (±0.00%)
</span></span><span class="line"><span class="token">    benchDataCollectionTransformation.......I0 - Mo13.952ms (±0.00%)
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">Subjects: 4, Assertions: 0, Failures: 0, Errors: 0
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">4 profile(s) generated:
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">/Users/ruben/Spatie/laravel-data/.phpbench/xdebug-profile/3fcb020036c8d7e8efdcddd4dbd66b92.cachegrind.gz
</span></span><span class="line"><span class="token">/Users/ruben/Spatie/laravel-data/.phpbench/xdebug-profile/8deba1dcce573e1bf818772ac7a5ace0.cachegrind.gz
</span></span><span class="line"><span class="token">/Users/ruben/Spatie/laravel-data/.phpbench/xdebug-profile/06d049dacb2a7a3809e069c6c8289e02.cachegrind.gz
</span></span><span class="line"><span class="token">/Users/ruben/Spatie/laravel-data/.phpbench/xdebug-profile/41c9fe618b93431786fff90b1d624b82.cachegrind.gz
</span></span></code></pre>
<p>Running the benchmark suite now takes way longer than before. In the end, we have a new directory with our profiles:</p>
<ul>
<li>
<p>.phpbench/xdebug-profile/3fcb020036c8d7e8efdcddd4dbd66b92.cachegrind.gz</p>
</li>
<li>
<p>.phpbench/xdebug-profile/8deba1dcce573e1bf818772ac7a5ace0.cachegrind.gz</p>
</li>
<li>
<p>.phpbench/xdebug-profile/06d049dacb2a7a3809e069c6c8289e02.cachegrind.gz</p>
</li>
<li>
<p>.phpbench/xdebug-profile/41c9fe618b93431786fff90b1d624b82.cachegrind.gz</p>
</li>
</ul>
<p>Each file corresponds to a benchmark case within your benchmark suite. The best way to find out which file belongs to which benchmark is to look at the execution order of benchmark cases and match them with the files in the order they were created.</p>
<p>At the moment, all files are compressed. Let us decompress them:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">gzip --decompress .phpbench/xdebug-profile/*
</span></span></code></pre>
<h2>Analyzing</h2>
<p>It is finally time to find out why our code is slow. This is also the most complicated step in the process, and there is no guide to quickly finding a bottleneck. You should have a good understanding of your code and be able to see strange behavior within profiles. Some things that might stand out:</p>
<ul>
<li>
<p>functions that shouldn't be executed</p>
</li>
<li>
<p>functions that are executed too much</p>
</li>
<li>
<p>functions that take longer than expected</p>
</li>
</ul>
<p>Let's open a profile where we create a collection of data objects:</p>
<p><img src="https://assets.flareapp.io/production/blog/2faf2784d6e8724ef756e4b7083c3ddf/54287423-ae4b-41dd-a7f3-0ae8c73372af.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/2faf2784d6e8724ef756e4b7083c3ddf/54287423-ae4b-41dd-a7f3-0ae8c73372af.png" width="4832" height="2856" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/2faf2784d6e8724ef756e4b7083c3ddf/54287423-ae4b-41dd-a7f3-0ae8c73372af.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/2faf2784d6e8724ef756e4b7083c3ddf/54287423-ae4b-41dd-a7f3-0ae8c73372af.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/2faf2784d6e8724ef756e4b7083c3ddf/54287423-ae4b-41dd-a7f3-0ae8c73372af.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/2faf2784d6e8724ef756e4b7083c3ddf/54287423-ae4b-41dd-a7f3-0ae8c73372af.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>First, we're going to configure QCacheGrind a bit more. Go to:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">QCacheGrind -&gt; Preferences -&gt; Source Annotations
</span></span></code></pre>
<p>And add the path where your code is situated. This allows QCacheGrind to give some hints with code examples of which code is slow.</p>
<p><img src="https://assets.flareapp.io/production/blog/62af8bea6d93237c980c8221fd9e622b/6603ac3f-8657-4ced-b8ef-28b92d8322ea.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/62af8bea6d93237c980c8221fd9e622b/6603ac3f-8657-4ced-b8ef-28b92d8322ea.png" width="2060" height="1146" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/62af8bea6d93237c980c8221fd9e622b/6603ac3f-8657-4ced-b8ef-28b92d8322ea.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/62af8bea6d93237c980c8221fd9e622b/6603ac3f-8657-4ced-b8ef-28b92d8322ea.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/62af8bea6d93237c980c8221fd9e622b/6603ac3f-8657-4ced-b8ef-28b92d8322ea.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/62af8bea6d93237c980c8221fd9e622b/6603ac3f-8657-4ced-b8ef-28b92d8322ea.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>The application has three panels:</p>
<ul>
<li>
<p>On the left: the functions which were called sorted by the lengthiest call</p>
</li>
<li>
<p>On the right-top: the callers of a selected function</p>
</li>
<li>
<p>On the right-bottom: for a selected function, the functios called</p>
</li>
</ul>
<p>On the left side, we see all the functions called. The Self metric shows how long it took to call a function, the Incl. metric shows how long the function and all the functions it called took. So the PHPBench function, for example, takes 99.93% of the time Incl. but it spends only 0.01% of the time within the function. All the other time is used to call other functions, our benchmarks with data creation.</p>
<p>Immediately we see something bizarre. We spend 24.34% of the time in <code>Illuminate\Container\Container-&gt;resolve()</code>. Let's click on that function and see what's happening:</p>
<p><img src="https://assets.flareapp.io/production/blog/06ecd5e0ecbb893f07941a1cd17ffa5a/43f56fb0-d0c1-4dce-950f-0381cf658266.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/06ecd5e0ecbb893f07941a1cd17ffa5a/43f56fb0-d0c1-4dce-950f-0381cf658266.png" width="4832" height="2856" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/06ecd5e0ecbb893f07941a1cd17ffa5a/43f56fb0-d0c1-4dce-950f-0381cf658266.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/06ecd5e0ecbb893f07941a1cd17ffa5a/43f56fb0-d0c1-4dce-950f-0381cf658266.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/06ecd5e0ecbb893f07941a1cd17ffa5a/43f56fb0-d0c1-4dce-950f-0381cf658266.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/06ecd5e0ecbb893f07941a1cd17ffa5a/43f56fb0-d0c1-4dce-950f-0381cf658266.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>This function has been called by <code>Illuminate\Foundation\Application-&gt;resolve()</code>. Let's click on that one. We see <code>Illuminate\Container\Container-&gt;make()</code> as the function, usually calling the <code>resolve</code> function. We keep on clicking on the most called functions until we enter the <code>helpers.php</code> file:</p>
<p><img src="https://assets.flareapp.io/production/blog/42f53047f830e486cd6211244049c1fd/3953040e-8135-4e28-a641-038e00eac8cd.png" alt="" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/42f53047f830e486cd6211244049c1fd/3953040e-8135-4e28-a641-038e00eac8cd.png" width="2474" height="1238" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/42f53047f830e486cd6211244049c1fd/3953040e-8135-4e28-a641-038e00eac8cd.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/42f53047f830e486cd6211244049c1fd/3953040e-8135-4e28-a641-038e00eac8cd.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/42f53047f830e486cd6211244049c1fd/3953040e-8135-4e28-a641-038e00eac8cd.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/42f53047f830e486cd6211244049c1fd/3953040e-8135-4e28-a641-038e00eac8cd.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>From this data, we can see that we're resolving a lot from the Laravel container:</p>
<ul>
<li>
<p><code>Data::from</code> 67500 items</p>
</li>
<li>
<p><code>DataPipeline in a closure (line 75)</code> -&gt; 225000 items</p>
</li>
<li>
<p><code>DataPipeline in a closure (line 69)</code> -&gt; 187500 items</p>
</li>
</ul>
<p>What's exactly happening here? Let's take a look at the <code>DataPipeline</code> code:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">public function execute(): Collection
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">    /** @var \Spatie\LaravelData\Normalizers\Normalizer[] $normalizers */
</span></span><span class="line"><span class="token">    $normalizers = array_map(
</span></span><span class="line"><span class="token">        fn (string|Normalizer $normalizer) =&gt; is_string($normalizer) ? app($normalizer) : $normalizer,
</span></span><span class="line"><span class="token">        $this-&gt;normalizers
</span></span><span class="line"><span class="token">    );
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    /** @var \Spatie\LaravelData\DataPipes\DataPipe[] $pipes */
</span></span><span class="line"><span class="token">    $pipes = array_map(
</span></span><span class="line"><span class="token">        fn (string|DataPipe $pipe) =&gt; is_string($pipe) ? app($pipe) : $pipe,
</span></span><span class="line"><span class="token">        $this-&gt;pipes
</span></span><span class="line"><span class="token">    );
</span></span><span class="line"><span class="token">    
</span></span><span class="line"><span class="token">    // Other code, hidden to keep things a bit easier to read
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>This code doesn't look like a performance problem. Yes, we're creating these normalizers and pipes from the container, but as long as this method isn't being executed that many times there won't be a problem in the end. This process should happen somewhere.</p>
<p>Let's find out from where this code is called:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">public function execute(string $class, mixed ...$payloads): BaseData
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">    $properties = new Collection();
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    foreach ($payloads as $payload) {
</span></span><span class="line"><span class="token">        /** @var BaseData $class */
</span></span><span class="line"><span class="token">        $pipeline = $class::pipeline();
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        foreach ($class::normalizers() as $normalizer) {
</span></span><span class="line"><span class="token">            $pipeline-&gt;normalizer($normalizer);
</span></span><span class="line"><span class="token">        }
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        foreach ($pipeline-&gt;using($payload)-&gt;execute() as $key =&gt; $value) {
</span></span><span class="line"><span class="token">            $properties[$key] = $value;
</span></span><span class="line"><span class="token">        }
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">    
</span></span><span class="line"><span class="token">    // Other code, hidden to keep things a bit easier to read
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>That could be better. We create the pipeline for each data object we're constructing, and thus, we resolve all the pipes and normalizers from the container each time. Every time again, this is not required as the pipeline is statically constructed for a data object and thus the same for all objects.</p>
<p>For a few objects, this will be fine. But since we have collections of the same objects (sometimes more than 500), we hit a huge performance problem.</p>
<p>We now cache these pipelines in version 3.2 of laravel-data and add some other performance improvements. This should do a lot for our performance! Finally, let's rerun the benchmarks.</p>
<p>Before performance optimizations:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">benchData...............................I1 - Mo293.376μs (±0.79%)
</span></span><span class="line"><span class="token">benchDataCollection.....................I1 - Mo5.619ms (±0.48%)
</span></span></code></pre>
<p>After performance optimizations:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">benchData...............................I1 - Mo125.731μs (±0.57%)
</span></span><span class="line"><span class="token">benchDataCollection.....................I1 - Mo2.111ms (±0.82%)
</span></span></code></pre>
<p>For plain data objects, we've improved performance by 57%, and for collections of data, by 62%. That's huge!</p>
<h2>In the end</h2>
<p>With a few hours of work, we drastically reduced the performance penalty on Flare. Profiling the most critical parts of your application can be valuable, but it is not that easy!</p>
<p>We keep redesigning and refactoring Flare for the next weeks and hope to launch soon! You can find a preview of our new design <a href="https://flareapp.io/blog/48-a-preview-of-our-upcoming-redesign">here</a>. Are you interested in beta testing the new version of Flare? Please send us an email at support@flareapp.io.</p>
]]>
            </summary>
                                    <updated>2023-03-27T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[A preview of our upcoming redesign]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/a-preview-of-our-upcoming-redesign" />
            <id>https://flareapp.io/a-preview-of-our-upcoming-redesign</id>
            <author>
                <name><![CDATA[Spatie]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Flare was launched almost four years ago. In human life, that's not too long, but for a service, that's a lifetime. Over the years, we had quite some ideas on how to improve the user experience for Flare. We hope you'll be happy to know that for the past few months, our team has been working hard on a total redesign of the service. By rethinking and streamlining the whole process, we hope to have created lots of improvements you'd like. Launch is planned in <strong>May</strong>.</p>
<p><strong>Homepage</strong><br>Our complete marketing site has a fresh new coat of paint. We changed the dark purple background to a lighter variant.</p>
<p><img src="https://assets.flareapp.io/production/blog/f6d0e395cfb842e5b6a6211491ee83fc/ade24780-0745-491e-b4a7-61fd58d14276.jpg" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/f6d0e395cfb842e5b6a6211491ee83fc/ade24780-0745-491e-b4a7-61fd58d14276.jpg" width="2976" height="2074" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/f6d0e395cfb842e5b6a6211491ee83fc/ade24780-0745-491e-b4a7-61fd58d14276.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/f6d0e395cfb842e5b6a6211491ee83fc/ade24780-0745-491e-b4a7-61fd58d14276.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/f6d0e395cfb842e5b6a6211491ee83fc/ade24780-0745-491e-b4a7-61fd58d14276.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/f6d0e395cfb842e5b6a6211491ee83fc/ade24780-0745-491e-b4a7-61fd58d14276.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p><strong>Docs</strong><br>We have rewritten the docs to be updated with the upcoming changes.</p>
<p><img src="https://assets.flareapp.io/production/blog/ea453845f4ef579d938ee042d0ea998e/a9d6e3eb-a0fb-4ada-b838-50d35112976b.jpg" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/ea453845f4ef579d938ee042d0ea998e/a9d6e3eb-a0fb-4ada-b838-50d35112976b.jpg" width="2976" height="2312" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/ea453845f4ef579d938ee042d0ea998e/a9d6e3eb-a0fb-4ada-b838-50d35112976b.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/ea453845f4ef579d938ee042d0ea998e/a9d6e3eb-a0fb-4ada-b838-50d35112976b.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/ea453845f4ef579d938ee042d0ea998e/a9d6e3eb-a0fb-4ada-b838-50d35112976b.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/ea453845f4ef579d938ee042d0ea998e/a9d6e3eb-a0fb-4ada-b838-50d35112976b.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p><strong>Ignition</strong><br>The Ignition landing page has been updated and Ignition itself has also been updated to align with the latest Laravel 10 style.</p>
<p><img src="https://assets.flareapp.io/production/blog/f4965e8d8a59629dcc565930886fb58f/8ef7bb90-048b-4bea-ae95-cc0160ae3dec.jpg" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/f4965e8d8a59629dcc565930886fb58f/8ef7bb90-048b-4bea-ae95-cc0160ae3dec.jpg" width="3096" height="1674" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/f4965e8d8a59629dcc565930886fb58f/8ef7bb90-048b-4bea-ae95-cc0160ae3dec.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/f4965e8d8a59629dcc565930886fb58f/8ef7bb90-048b-4bea-ae95-cc0160ae3dec.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/f4965e8d8a59629dcc565930886fb58f/8ef7bb90-048b-4bea-ae95-cc0160ae3dec.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/f4965e8d8a59629dcc565930886fb58f/8ef7bb90-048b-4bea-ae95-cc0160ae3dec.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p><strong>Platform</strong><br>We didn't only redesign the marketing pages, but the entire application. Here are a few screens.</p>
<p><img src="https://assets.flareapp.io/production/blog/025a881f177743dcba5d94744a923a77/71622f46-ba5d-411c-a9eb-3024bdec4f5f.jpg" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/025a881f177743dcba5d94744a923a77/71622f46-ba5d-411c-a9eb-3024bdec4f5f.jpg" width="2900" height="1514" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/025a881f177743dcba5d94744a923a77/71622f46-ba5d-411c-a9eb-3024bdec4f5f.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/025a881f177743dcba5d94744a923a77/71622f46-ba5d-411c-a9eb-3024bdec4f5f.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/025a881f177743dcba5d94744a923a77/71622f46-ba5d-411c-a9eb-3024bdec4f5f.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/025a881f177743dcba5d94744a923a77/71622f46-ba5d-411c-a9eb-3024bdec4f5f.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>Here's what a list of projects looks like.</p>
<p><img src="https://assets.flareapp.io/production/blog/2e42c4cdc224817193389a770602cfec/f45a0e94-4ecc-4cad-ada0-6d098d376ee0.jpg" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/2e42c4cdc224817193389a770602cfec/f45a0e94-4ecc-4cad-ada0-6d098d376ee0.jpg" width="2440" height="940" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/2e42c4cdc224817193389a770602cfec/f45a0e94-4ecc-4cad-ada0-6d098d376ee0.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/2e42c4cdc224817193389a770602cfec/f45a0e94-4ecc-4cad-ada0-6d098d376ee0.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/2e42c4cdc224817193389a770602cfec/f45a0e94-4ecc-4cad-ada0-6d098d376ee0.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/2e42c4cdc224817193389a770602cfec/f45a0e94-4ecc-4cad-ada0-6d098d376ee0.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>When creating a project, you'll get improved installation instructions.</p>
<p><img src="https://assets.flareapp.io/production/blog/7e1520eda8ad6138580e67cd3d3a9225/e0e49ef6-54a2-46e4-9609-931ee3164696.jpg" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/7e1520eda8ad6138580e67cd3d3a9225/e0e49ef6-54a2-46e4-9609-931ee3164696.jpg" width="3086" height="1876" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/7e1520eda8ad6138580e67cd3d3a9225/e0e49ef6-54a2-46e4-9609-931ee3164696.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/7e1520eda8ad6138580e67cd3d3a9225/e0e49ef6-54a2-46e4-9609-931ee3164696.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/7e1520eda8ad6138580e67cd3d3a9225/e0e49ef6-54a2-46e4-9609-931ee3164696.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/7e1520eda8ad6138580e67cd3d3a9225/e0e49ef6-54a2-46e4-9609-931ee3164696.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>The error detail page has been built from the ground up.</p>
<p><img src="https://assets.flareapp.io/production/blog/7d332389e3852cba1fb779d0b0383402/c2d20b03-849b-41b3-a9e9-ec26b3078653.jpg" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/7d332389e3852cba1fb779d0b0383402/c2d20b03-849b-41b3-a9e9-ec26b3078653.jpg" width="3108" height="2254" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/7d332389e3852cba1fb779d0b0383402/c2d20b03-849b-41b3-a9e9-ec26b3078653.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/7d332389e3852cba1fb779d0b0383402/c2d20b03-849b-41b3-a9e9-ec26b3078653.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/7d332389e3852cba1fb779d0b0383402/c2d20b03-849b-41b3-a9e9-ec26b3078653.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/7d332389e3852cba1fb779d0b0383402/c2d20b03-849b-41b3-a9e9-ec26b3078653.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>From the error details, you can quickly jump to the new insights pages. You can see a list of URLs where the error occurred or a list of users who might have seen this error. In this case, it was only me, phew!</p>
<p><img src="https://assets.flareapp.io/production/blog/30a6dac0e341a6213413145c60f91c89/375ae273-a256-4fbc-8b4d-656b11b90521.jpg" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/30a6dac0e341a6213413145c60f91c89/375ae273-a256-4fbc-8b4d-656b11b90521.jpg" width="3104" height="1176" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/30a6dac0e341a6213413145c60f91c89/375ae273-a256-4fbc-8b4d-656b11b90521.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/30a6dac0e341a6213413145c60f91c89/375ae273-a256-4fbc-8b4d-656b11b90521.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/30a6dac0e341a6213413145c60f91c89/375ae273-a256-4fbc-8b4d-656b11b90521.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/30a6dac0e341a6213413145c60f91c89/375ae273-a256-4fbc-8b4d-656b11b90521.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<h2>In closing</h2>
<p>We hope that you like this preview. Only a few screens are shown, but we're giving our entire UI this treatment. We're not there yet, and more work has to be done, but we're confident we'll launch the redesign in May.</p>
<p>This redesign will also be the starting point of new features we'll add after launch.</p>
<p>Flare is one of the best error trackers for Laravel and PHP projects. We display the most important aspects of issues that happened in your production environment in our beautiful UI. For most issues, Flare will suggest solutions and related documentation. This way, you can fix your issue in no time.</p>
<p>Now is the perfect time to try out Flare. You can <a href="https://flareapp.io">register here</a> for a free ten-day trial; no credit card is required.</p>
]]>
            </summary>
                                    <updated>2023-03-08T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Fixing nested validation in Laravel]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/fixing-nested-validation-in-laravel" />
            <id>https://flareapp.io/fixing-nested-validation-in-laravel</id>
            <author>
                <name><![CDATA[Ruben]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Since the early days, Laravel includes an excellent validator class. It allows you to check if the input provided to your application is valid according a set of rules and follows a specific format. Laravel's validator works on any arbitrary array of data but it's typically used for request validation. You can check if an email is valid, a password is confirmed, a file is a pdf, a string is not longer than a certain amount of characters, and so much more.</p>
<p>Validating input in Laravel is incredibly powerful and quick to set up. It's always been one of our favorite Laravel features. But, a few strange edge cases can be found where the validator isn't working as expected. When developing a Flare feature, we encountered one of these oddities and found a nice way to work around it.</p>
<p>Suppose that we're trying to validate a basic array with a nested <code>team</code> property:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">[
</span></span><span class="line"><span class="token">	'name' =&gt; 'Ruben Van Assche',
</span></span><span class="line"><span class="token">	'team' =&gt; [
</span></span><span class="line"><span class="token">		'id' =&gt; 1,
</span></span><span class="line"><span class="token">		'role' =&gt; 'engineer'
</span></span><span class="line"><span class="token">	]
</span></span><span class="line"><span class="token">]
</span></span></code></pre>
<p><em>(The example above is abbreviated for better readability and clarity)</em></p>
<p>We could now write a <a href="https://laravel.com/docs/master/validation#form-request-validation"><code>FormRequest</code></a> to validate this data automatically or manually validate the array using the <code>Validator</code> facade in the controller. To keep things simple, we'll stick to using the validator class directly:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">$rules =  [
</span></span><span class="line"><span class="token">    'name' =&gt; ['required', 'string'],
</span></span><span class="line"><span class="token">    'team' =&gt; ['required', 'array'],
</span></span><span class="line"><span class="token">    'team.id' =&gt; ['required', Rule::exists('teams', 'id')],
</span></span><span class="line"><span class="token">    'team.role' =&gt; ['required', Rule::in(['engineer', 'project_manager', 'boss'])],
</span></span><span class="line"><span class="token">];
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">$validator = Validator::make($data, $rules);
</span></span></code></pre>
<p>We can now check if our data array (or "payload") is valid:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">$validator-&gt;passes(); // Returns true, data is valid
</span></span><span class="line"><span class="token">$validator-&gt;messages(); // An empty message bag, great!
</span></span></code></pre>
<p>When passing an invalid payload array as our input, the validator will fail and provide us with a couple of validation messages:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">$data = [
</span></span><span class="line"><span class="token">	'name' =&gt; 'Ruben Van Assche',
</span></span><span class="line"><span class="token">	'team' =&gt; [
</span></span><span class="line"><span class="token">		'id' =&gt; 2023,
</span></span><span class="line"><span class="token">		'role' =&gt; 'ceo'
</span></span><span class="line"><span class="token">	]
</span></span><span class="line"><span class="token">];
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">$validator = Validator::make($data, $rules);
</span></span><span class="line"><span class="token">$validator-&gt;passes(); // returns false!
</span></span></code></pre>
<p>The error bag contains the following validation messages:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">[
</span></span><span class="line"><span class="token">    "team.id" =&gt; ["The selected team.id is invalid.]
</span></span><span class="line"><span class="token">    "team.role" =&gt; ["The selected team.role is invalid."]
</span></span><span class="line"><span class="token">]
</span></span></code></pre>
<p>Great, so up until now, everything is working as expected. We can validate both root level properties and nested properties in the <code>team</code> array.</p>
<p>Let's make things more complicated by allowing the <code>team</code> property to be <code>nullable</code>. Maybe your application allows for users without teams, so the team property can also be <code>null</code>.</p>
<p>Let's update our rules by adding the <code>nullable</code> rule:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">[
</span></span><span class="line"><span class="token">	'name' =&gt; ['required', 'string'],
</span></span><span class="line"><span class="token">	'team' =&gt; ['nullable', 'array'],
</span></span><span class="line"><span class="token">	'team.id' =&gt; ['required', Rule::exists('teams', 'id')],
</span></span><span class="line"><span class="token">	'team.role' =&gt; ['required', Rule::in(['engineer', 'project_manager', 'boss'])],
</span></span><span class="line"><span class="token">]
</span></span></code></pre>
<p>When testing our two previous example payload again, everything still works as expected: the valid example payload passes and the second example contains errors so the validator fails. However, let's now test our new feature where we don't have a team at all:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">[
</span></span><span class="line"><span class="token">	'name' =&gt; 'Ruben Van Assche',
</span></span><span class="line"><span class="token">]
</span></span></code></pre>
<p>The <code>team</code> property is <code>nullable</code> so this payload should not give us validation errors. Let's validate:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">[
</span></span><span class="line"><span class="token">    "team.id" =&gt; ["The selected team.id is invalid.]
</span></span><span class="line"><span class="token">    "team.role" =&gt; ["The selected team.role is invalid."]
</span></span><span class="line"><span class="token">]
</span></span></code></pre>
<p><em>Wait what?!</em> For some reason, the validator ran the <code>team.id</code> and <code>team.role</code> rules even though we wrote a rule that the entire <code>team</code> array is allowed be null, so what happened here?</p>
<p>The validator did its job correctly, albeit a bit bizarre. We wrote three rules for the <code>team</code> array. The first one tells the validator that <code>team</code> should be an array but can also be null. This rule succeeds with our last example payload. The other two rules require a value to be in a specific format. And these fail as the value is always <code>required</code>.</p>
<p>Laravel's validator is built in a way where each rule exists on its own. This means the two <code>team.*</code> rules have no context of the first rule indicating that the <code>team</code> array can be null. So they get executed as they would typically, resulting in two error messages.</p>
<p>How can we fix this?</p>
<h2>Nullable all the way</h2>
<p>We could rewrite our rules like this:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">[
</span></span><span class="line"><span class="token">    'name' =&gt; ['required', 'string'],
</span></span><span class="line"><span class="token">    'team' =&gt; ['nullable', 'array'],
</span></span><span class="line"><span class="token">    'team.id' =&gt; ['nullable', Rule::exists('teams', 'id')],
</span></span><span class="line"><span class="token">    'team.role' =&gt; ['nullable', Rule::in(['engineer', 'project_manager', 'boss'])],
</span></span><span class="line"><span class="token">]
</span></span></code></pre>
<p>Now when validating the following:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">[
</span></span><span class="line"><span class="token">	'name' =&gt; 'Ruben Van Assche',
</span></span><span class="line"><span class="token">]
</span></span></code></pre>
<p>Great! No validation messages, but what if we provide a payload like this:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">[
</span></span><span class="line"><span class="token">	'name' =&gt; 'Ruben Van Assche',
</span></span><span class="line"><span class="token">	'team' =&gt; [
</span></span><span class="line"><span class="token">	    'id' =&gt; 1,
</span></span><span class="line"><span class="token">	]
</span></span><span class="line"><span class="token">]
</span></span></code></pre>
<p>Also no validation messages, but we expect one since a team always needs an id and role. There are better approaches to writing rules like this.</p>
<h2>Better requiring</h2>
<p>We could rewrite our require rules smarter by explicitly telling when a specific value is required:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">[
</span></span><span class="line"><span class="token">	'name' =&gt; ['required', 'string'],
</span></span><span class="line"><span class="token">	'team' =&gt; ['nullable', 'array'],
</span></span><span class="line"><span class="token">	'team.id' =&gt; ['required_with:team', Rule::exists('teams', 'id')],
</span></span><span class="line"><span class="token">	'team.role' =&gt; ['required_with:team', Rule::in(['engineer', 'project_manager', 'boss'])],
</span></span><span class="line"><span class="token">]
</span></span></code></pre>
<p>When validating:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">[
</span></span><span class="line"><span class="token">	'name' =&gt; 'Ruben Van Assche',
</span></span><span class="line"><span class="token">]
</span></span></code></pre>
<p>No messages. Up to the next one:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">[
</span></span><span class="line"><span class="token">	'name' =&gt; 'Ruben Van Assche',
</span></span><span class="line"><span class="token">	'team' =&gt; [
</span></span><span class="line"><span class="token">	    'id' =&gt; 1,
</span></span><span class="line"><span class="token">	]
</span></span><span class="line"><span class="token">]
</span></span></code></pre>
<p>We now get the following message:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">[
</span></span><span class="line"><span class="token">	"team.role" =&gt; ["The team.role field is required when team is present."]
</span></span><span class="line"><span class="token">]
</span></span></code></pre>
<p>Marvelous, we succeeded in writing the correct rules! But from now on, we must always use the <code>required_with:team</code> rule instead of the <code>required</code> rule and remember this for new properties to be added in the future.</p>
<p>We also need to be careful. If someone decides to change the name of the team property, then all our rules need to be updated. We only have two rules now, but as an application grows, these numbers tend to rise rapidly, and a mistake is quickly made.</p>
<p>Lastly, what if we also want to nest another array within the team array like the payload bellow, where we also want to be able to (optionally) disable or enable extra features per team:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">[
</span></span><span class="line"><span class="token">    'name' =&gt; 'Ruben Van Assche',
</span></span><span class="line"><span class="token">    'team' =&gt; [
</span></span><span class="line"><span class="token">        'id' =&gt; 1,
</span></span><span class="line"><span class="token">        'role' =&gt; 'engineer',
</span></span><span class="line"><span class="token">        'features' =&gt; [
</span></span><span class="line"><span class="token">            'github' =&gt; true,
</span></span><span class="line"><span class="token">            'jira' =&gt; false,
</span></span><span class="line"><span class="token">        ]
</span></span><span class="line"><span class="token">    ]
</span></span><span class="line"><span class="token">]
</span></span></code></pre>
<p>And to make things more complicated, you should also be able to provide no features (in this case, defaults could be used):</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">[
</span></span><span class="line"><span class="token">    'name' =&gt; 'Ruben Van Assche',
</span></span><span class="line"><span class="token">    'team' =&gt; [
</span></span><span class="line"><span class="token">        'id' =&gt; 1,
</span></span><span class="line"><span class="token">        'role' =&gt; 'engineer',
</span></span><span class="line"><span class="token">    ]
</span></span><span class="line"><span class="token">]
</span></span></code></pre>
<p>These were the best rules I could come up with:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">[
</span></span><span class="line"><span class="token">    'name' =&gt; ['required', 'string'],
</span></span><span class="line"><span class="token">    'team' =&gt; ['nullable', 'array'],
</span></span><span class="line"><span class="token">    'team.id' =&gt; ['required_with:team', Rule::exists('teams', 'id')],
</span></span><span class="line"><span class="token">    'team.role' =&gt; ['required_with:team', Rule::in(['engineer', 'project_manager', 'boss'])],
</span></span><span class="line"><span class="token">    'team.features' =&gt; ['nullable', 'required_with:team', 'array'],
</span></span><span class="line"><span class="token">    'team.features.github' =&gt; ['required_with:team.features', 'boolean'],
</span></span><span class="line"><span class="token">    'team.features.jira' =&gt; ['required_with:team.features', 'boolean'],
</span></span><span class="line"><span class="token">];
</span></span></code></pre>
<p>But these are not 100% watertight since the latest payload we'v constructed without the features array will result in the following validation message:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">[
</span></span><span class="line"><span class="token">	"team.features" =&gt; [
</span></span><span class="line"><span class="token">	  "The team.features field is required when team is present."
</span></span><span class="line"><span class="token">	]
</span></span><span class="line"><span class="token">]
</span></span></code></pre>
<p>The <code>required_with</code> rules can fix our problem but have a few disadvantages. Is there another way?</p>
<h2>Fixing the issue once and for all</h2>
<p>In Flare, we don't write validation rules ourselves. One of our packages called <a href="https://github.com/spatie/laravel-data">laravel-data</a> automatically generates them.</p>
<p>Let's revisit our previous example, but we now structure our data using data objects. Which has as an added benefit a stricter type structure we can use in the backend. We stop using untyped arrays with data and have objects with typed properties.</p>
<p>The laravel-data package can also generate TypeScript definitions based upon the types within our data objects, so our frontend code is now also typed exactly like our backend. Cool!</p>
<p>These are the data objects we've created based on the rules we decided on earlier:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">class UserData extends Data
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">    public function __construct(
</span></span><span class="line"><span class="token">        public string $name,
</span></span><span class="line"><span class="token">        public ?TeamData $team,
</span></span><span class="line"><span class="token">    ) {
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">}
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">class TeamData extends Data
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">    public function __construct(
</span></span><span class="line"><span class="token">        #[Exists('teams', 'id')]
</span></span><span class="line"><span class="token">        public int $id,
</span></span><span class="line"><span class="token">        #[In('engineer', 'project_manager', 'boss')]
</span></span><span class="line"><span class="token">        public string $role,
</span></span><span class="line"><span class="token">        public ?TeamFeaturesData $features,
</span></span><span class="line"><span class="token">    ) {
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">}
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">class TeamFeaturesData extends Data
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">    public function __construct(
</span></span><span class="line"><span class="token">        public bool $github,
</span></span><span class="line"><span class="token">        public bool $jira,
</span></span><span class="line"><span class="token">    ) {
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>Within our controller, we inject the <code>User</code> data object as follows:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">class UpdateUserController
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">    public function __invoke(
</span></span><span class="line"><span class="token">        User $user,
</span></span><span class="line"><span class="token">        UserData $data,
</span></span><span class="line"><span class="token">        UpdateOrCreateUserAction $updateOrCreateUserAction
</span></span><span class="line"><span class="token">    )
</span></span><span class="line"><span class="token">    {
</span></span><span class="line"><span class="token">        $updateOrCreateUserAction($data, $user);
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        flash('User updated!');
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        return redirect()-&gt;back();
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>As you can see, we also inject the user model we're updating and the action (a class with business logic) to perform the update within the database.</p>
<p>Due to the nature of laravel-data, when injected in a controller, the data object will validate the request input before creating itself. Let's take a look at the previous inputs we provided to the validator:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">[
</span></span><span class="line"><span class="token">    'name' =&gt; 'Ruben Van Assche',
</span></span><span class="line"><span class="token">];
</span></span></code></pre>
<p>No validation messages 👍</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">[
</span></span><span class="line"><span class="token">    'name' =&gt; 'Ruben Van Assche',
</span></span><span class="line"><span class="token">    'team' =&gt; [
</span></span><span class="line"><span class="token">        'id' =&gt; 1,
</span></span><span class="line"><span class="token">        'role' =&gt; 'engineer',
</span></span><span class="line"><span class="token">    ],
</span></span><span class="line"><span class="token">]
</span></span></code></pre>
<p>Also no validation messages 🥳</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">[
</span></span><span class="line"><span class="token">    'name' =&gt; 'Ruben Van Assche',
</span></span><span class="line"><span class="token">    'team' =&gt; [
</span></span><span class="line"><span class="token">        'id' =&gt; 1,
</span></span><span class="line"><span class="token">        'role' =&gt; 'engineer',
</span></span><span class="line"><span class="token">        'features' =&gt; [
</span></span><span class="line"><span class="token">            'github' =&gt; true,
</span></span><span class="line"><span class="token">            'jira' =&gt; false,
</span></span><span class="line"><span class="token">        ],
</span></span><span class="line"><span class="token">    ],
</span></span><span class="line"><span class="token">]
</span></span></code></pre>
<p>No validation messages. We fixed our initial problem! 🎉</p>
<p>At this point in time, you start to wonder, is the validation working? Let's try this one out:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">[
</span></span><span class="line"><span class="token">    'name' =&gt; 'Ruben Van Assche',
</span></span><span class="line"><span class="token">    'team' =&gt; [
</span></span><span class="line"><span class="token">        'id' =&gt; 1,
</span></span><span class="line"><span class="token">    ],
</span></span><span class="line"><span class="token">]
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">// Validation messages:
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">[
</span></span><span class="line"><span class="token">    "team.role" =&gt; ["The team.role field is required when team is present."]
</span></span><span class="line"><span class="token">]
</span></span></code></pre>
<p>It looks like everything is still validated as expected.</p>
<h2>Internals</h2>
<p>Let's go quickly over the internals of laravel-data. Until a few weeks ago, laravel-data worked precisely like our first approach by making all rules nullable in nested arrays. Though working, there were better solutions than this. So we completely rewrote the validation logic for laravel-data's third version.</p>
<p>Instead of generating rules before the payload is provided, laravel-data has a sort of JIT rule generator. It will generate rules based on the definition in the data class and the payload passed in.</p>
<p>For example, when we have a data object like <code>UserData</code>, all non-nested data property rules will be generated. The nested <code>TeamData</code> is nullable, so it will only start doing the same and thus generate rules for itself when we find a key <code>team</code> within the input payload that is not null.</p>
<p>If you're more interested in the internal workings of the validation rule generation, then take a look over <a href="https://github.com/spatie/laravel-data/blob/main/src/Resolvers/DataValidationRulesResolver.php">here</a>, where we generate validation rules for data objects just in time.</p>
<h2>Closing</h2>
<p>Laravel's validator is incredible, but sometimes it needs some help. Expect more articles like this on the Flare blog in the coming months and a new coat of paint for our website and application. Want a sneak peek? Mail us at support@flareapp.io.</p>
]]>
            </summary>
                                    <updated>2023-02-27T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[You might not need `useRef` for that]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/you-might-not-need-useref-for-that" />
            <id>https://flareapp.io/you-might-not-need-useref-for-that</id>
            <author>
                <name><![CDATA[Sam]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>According to the React maintainers, React developers <a href="https://beta.reactjs.org/learn/you-might-not-need-an-effect">reach for the </a><a href="https://beta.reactjs.org/learn/you-might-not-need-an-effect"><code>useEffect</code></a><a href="https://beta.reactjs.org/learn/you-might-not-need-an-effect"> hook too quickly</a>.  It is far from the only hook with many naive usages. Why won't we go through my favorite example of an incorrect usage for <code>useRef</code>?</p>
<h2>What is the <code>useRef</code> hook?</h2>
<p>In React, the data used for rendering is immutable. A change in a piece of state is committed through a setter or reducer.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">const Component = () =&gt; {
</span></span><span class="line"><span class="token">	const [name, setName] = useState&lt;string&gt;('');
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">	return (
</span></span><span class="line"><span class="token">		&lt;input
</span></span><span class="line"><span class="token">		  type="text"
</span></span><span class="line"><span class="token">		  value={name}
</span></span><span class="line"><span class="token">		  onChange={e =&gt; setName(e.target.value)}
</span></span><span class="line"><span class="token">		/&gt;
</span></span><span class="line"><span class="token">	);
</span></span><span class="line"><span class="token">};
</span></span></code></pre>
<p>These changes are observable to execute effects, invalidate memoization and rerender your component.</p>
<p><code>useRef</code> initializes and gives you access to a mutable variable across executions of your component or hook.
Mutable because on each render, <code>useRef</code> will return the same <code>RefObject</code> to which you can store data.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">const Component = () =&gt; {
</span></span><span class="line"><span class="token">	const buttonPresses = useRef&lt;number&gt;(0);
</span></span><span class="line"><span class="token">	return &lt;button onClick={() =&gt; buttonPresses.current++}/&gt;;
</span></span><span class="line"><span class="token">};
</span></span></code></pre>
<p>They do not trigger all the cool stuff that the observable state does.</p>
<p>A powerful and straightforward way to look at React is that it wants us to <a href="https://twitter.com/RyanCarniato/status/1353801411331416070">keep our state and UI in sync</a>.</p>
<h2>Where and when can you use <code>useRef</code>?</h2>
<p><code>useRef</code> is not just an escape hatch but can be used to read and write data in event handlers and effects. Remember that changing the ref won't trigger the <code>useEffect</code> through its dependency array.</p>
<p>It's not unreasonable to think that you can only pass a <code>RefObject</code> to a jsx <code>ref</code> attribute.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">const container = useRef&lt;HTMLDivElement&gt;(null);
</span></span><span class="line"><span class="token">return &lt;div ref={container}&gt;{children}&lt;/div&gt;;
</span></span></code></pre>
<p>Note that we initialise <code>useRef</code> with <code>null</code>.  <code>container.current</code> is reset to <code>null</code> whenever the node unmounts. Therefore <code>container.current</code> will only ever be an <code>HTMLDivElement</code> or <code>null</code>. It is important for typesafety and generally considered good practice to always pass something to the <code>useRef</code> initializer.</p>
<p>You should use this pattern whenever you want to access your DOM nodes inside <code>useEffect</code> or event handlers.</p>
<p>For example, when we want a click on the <code>::backdrop</code> of a <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog"><code>&lt;dialog&gt;</code></a> to close it.
In the <code>onClick</code> event handler, we first check if the click target is the dialog element.
Then we can use the native <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/close"><code>dialog.close()</code></a> API to close it.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">const Dialog = ({ children, state }) =&gt; {
</span></span><span class="line"><span class="token">	const dialogRef = useRef&lt;HTMLDialogElement&gt;(null);
</span></span><span class="line"><span class="token">	
</span></span><span class="line"><span class="token">	const handleClick = (e) =&gt; {
</span></span><span class="line"><span class="token">		if (e.target === dialogRef.current) dialogRef.current.close()
</span></span><span class="line"><span class="token">	}
</span></span><span class="line"><span class="token">	
</span></span><span class="line"><span class="token">	return (
</span></span><span class="line"><span class="token">		&lt;dialog ref={dialogRef} onClick={handleClick}&gt;
</span></span><span class="line"><span class="token">			{children}
</span></span><span class="line"><span class="token">		&lt;/dialog&gt;
</span></span><span class="line"><span class="token">	);
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<h2>Where it breaks down</h2>
<p>When I explained you could use mutable refs inside of <code>useEffect</code>, and you can assign a DOM node to that mutable ref through the ref attribute, you might try to add and remove event listeners inside of <code>useEffect</code>.</p>
<p>This pattern was deployed inside <a href="https://auto-animate.formkit.com">formkit's auto-animate</a>. I discovered this when I checked why their react hook was only sometimes working for me.</p>
<p>This is a slightly simplified version of the <a href="https://github.com/formkit/auto-animate/blob/master/src/react/index.ts">bundled hook</a>:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">import { useEffect, useRef, RefObject } from 'react'
</span></span><span class="line"><span class="token">import autoAnimate, { AutoAnimateOptions } from '../index'
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">/**
</span></span><span class="line"><span class="token">* AutoAnimate hook for adding dead-simple transitions and animations to react.
</span></span><span class="line"><span class="token">* @param options - Auto animate options
</span></span><span class="line"><span class="token">* @returns
</span></span><span class="line"><span class="token">*/
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">export function useAutoAnimate&lt;T extends Element&gt;(
</span></span><span class="line"><span class="token">	options: Partial&lt;AutoAnimateOptions&gt; = {}
</span></span><span class="line"><span class="token">): [RefObject&lt;T&gt;] {
</span></span><span class="line"><span class="token">	const element = useRef&lt;T&gt;(null)
</span></span><span class="line"><span class="token">	
</span></span><span class="line"><span class="token">	useEffect(() =&gt; {
</span></span><span class="line"><span class="token">		if (element.current instanceof HTMLElement)
</span></span><span class="line"><span class="token">			autoAnimate(element.current, options)
</span></span><span class="line"><span class="token">	}, [element])
</span></span><span class="line"><span class="token">	
</span></span><span class="line"><span class="token">	return [element]
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>And this is how I consumed the <code>useAutoAnimate</code> hook.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">import { useAutoAnimate } from '@formkit/auto-animate/react'
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">const Component = () =&gt; {
</span></span><span class="line"><span class="token">	const [container] = useAutoAnimate&lt;HTMLUlElement&gt;();
</span></span><span class="line"><span class="token">	const [items, error] = useSwr(/* swr config */);
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">	return (
</span></span><span class="line"><span class="token">		&lt;section&gt;
</span></span><span class="line"><span class="token">		{!items &amp;&amp; !error &amp;&amp; (&lt;spinner /&gt;)}
</span></span><span class="line"><span class="token">		{items &amp;&amp; (
</span></span><span class="line"><span class="token">			&lt;ul ref={container}&gt;
</span></span><span class="line"><span class="token">				{items.map(/* item renderer */)}
</span></span><span class="line"><span class="token">			&lt;/ul&gt;
</span></span><span class="line"><span class="token">		)}	
</span></span><span class="line"><span class="token">		&lt;/section&gt;
</span></span><span class="line"><span class="token">	)
</span></span><span class="line"><span class="token">};
</span></span></code></pre>
<p>Did you spot the problem?
This fragment has the unobservable reference <code>element</code> inside the dependency array of a <code>useEffect</code>, which does not work. This <code>useEffect</code> will only execute after the initial render. If the initial render has <code>items</code>, the ref will hold the <code>&lt;ul&gt;</code> DOM node.</p>
<p>Due to waiting for data to fetch, <code>element</code> hasn't received the DOM node and fails to register the autoAnimate.</p>
<h2>Callback refs to the rescue</h2>
<p>Let us figure out what the jsx <code>ref</code> attribute accepts as value.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">interface RefObject&lt;T&gt; { 
</span></span><span class="line"><span class="token">	readonly current: T | null;  
</span></span><span class="line"><span class="token">}  
</span></span><span class="line"><span class="token">type RefCallback&lt;T&gt; = (instance: T | null): void;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">type Ref&lt;T&gt; = RefCallback&lt;T&gt; | RefObject&lt;T&gt; | null;
</span></span></code></pre>
<p>From the React docs, we know that <code>RefCallback</code> receives the DOM node when being rendered and <code>null</code> on unmount. With this knowledge, we can rewrite the hook to accept DOM updates.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">import { useCallback } from 'react';  
</span></span><span class="line"><span class="token">import autoAnimate, { AutoAnimateOptions } from '@formkit/auto-animate';  
</span></span><span class="line"><span class="token">  
</span></span><span class="line"><span class="token">type Options = Partial&lt;AutoAnimateOptions&gt;;  
</span></span><span class="line"><span class="token">  
</span></span><span class="line"><span class="token">const useAutoAnimate = &lt;T extends HTMLElement&gt;(options?: Options): ((element: T | null) =&gt; void) =&gt; {  
</span></span><span class="line"><span class="token">    return useCallback(  
</span></span><span class="line"><span class="token">        (element: T | null) =&gt; {  
</span></span><span class="line"><span class="token">            if (!element) return;  
</span></span><span class="line"><span class="token">            autoAnimate(element, options);  
</span></span><span class="line"><span class="token">        },  
</span></span><span class="line"><span class="token">        [options]  
</span></span><span class="line"><span class="token">    );  
</span></span><span class="line"><span class="token">};
</span></span></code></pre>
<h2>An example with ResizeObserver</h2>
<p>I was building a new pagination interaction for Flare's redesign, and a <code>ResizeObserver</code> came into play. This example shows that <code>useRef</code> is useful in other ways than accessing the DOM and how registering and cleaning up DOM observers/listeners can be accomplished using a callback ref.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">const Component = () =&gt; {
</span></span><span class="line"><span class="token">	const [size, setSize] = useState&lt;ResizeObserverSize | null&gt;(null);
</span></span><span class="line"><span class="token">	
</span></span><span class="line"><span class="token">	const observer = useRef&lt;ResizeObserver&gt;(new ResizeObserver((entries) =&gt; {
</span></span><span class="line"><span class="token">		setSize(entries[0].borderBoxSize);
</span></span><span class="line"><span class="token">	}));
</span></span><span class="line"><span class="token">	
</span></span><span class="line"><span class="token">	const registerResizeObserver = useCallback(instance =&gt; {  
</span></span><span class="line"><span class="token">	    if (instance) return observer.current.observe(instance);  
</span></span><span class="line"><span class="token">	    
</span></span><span class="line"><span class="token">	    observer.current.disconnect();   
</span></span><span class="line"><span class="token">	}, []);
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">	return (
</span></span><span class="line"><span class="token">		&lt;ul ref={registerResizeObserver}&gt;
</span></span><span class="line"><span class="token">			...
</span></span><span class="line"><span class="token">		&lt;/ul&gt;
</span></span><span class="line"><span class="token">	);
</span></span><span class="line"><span class="token">};
</span></span></code></pre>
<p>Note that by defining the <code>ResizeObserver</code>'s callback inside of <code>Component</code> it can access state setters but cannot access the updated state because it created a closure on initialisation.</p>
<h2>Read more</h2>
<p>The React team is hard at work on building better documentation so that we can focus on writing better React code.
<a href="https://beta.reactjs.org/learn/referencing-values-with-refs">referencing values with refs</a> goes into more detail on how to use <code>useRef</code> safely.
The challenges at the bottom of <a href="https://beta.reactjs.org/learn/manipulating-the-dom-with-refs">manipulating the dom with refs</a> can build up your understanding of using DOM apis in react.</p>
<p>The old <a href="https://reactjs.org/docs/refs-and-the-dom.html#callback-refs">callback refs</a> documentation outlines the idea of using a function for more fine-grained control over when refs are set and unset. The caveats of which can be solved using the <code>useCallback</code> hook.</p>
]]>
            </summary>
                                    <updated>2022-09-02T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[How we improved Flare in 2021]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/how-we-improved-flare-in-2021" />
            <id>https://flareapp.io/how-we-improved-flare-in-2021</id>
            <author>
                <name><![CDATA[Rias]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>The main goal of Flare is to be the best error tracking for Laravel applications. When we launched Flare, we offered a lot of niceties like decompiled Blade views, solutions suggestions, and much more.</p>
<p>Work on a SaaS application is never done. There are always things to improve. And because the Laravel ecosystem is constantly moving, we have to adapt Flare. Here's a rundown of all the things things we improved in Flare in the past year.</p>
<h2>Making Livewire a first-class citizen</h2>
<p><a href="https://laravel-livewire.com">Livewire</a> has gained a lot of popularity these past years. What was initially viewed as a fun experiment with Blade views is now a valid architecture for building production worthy applications.</p>
<p>In Flare, we now recognise exceptions coming from Livewire components. For those exceptions, we display a Livewire tab.</p>
<p><img src="https://assets.flareapp.io/production/blog/74d32105aead3aeafc536172b1844c98/b3df25c5-ed82-4e4c-b28f-cfbe0f682a77.png" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/74d32105aead3aeafc536172b1844c98/b3df25c5-ed82-4e4c-b28f-cfbe0f682a77.png" width="2680" height="2456" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/74d32105aead3aeafc536172b1844c98/b3df25c5-ed82-4e4c-b28f-cfbe0f682a77.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/74d32105aead3aeafc536172b1844c98/b3df25c5-ed82-4e4c-b28f-cfbe0f682a77.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/74d32105aead3aeafc536172b1844c98/b3df25c5-ed82-4e4c-b28f-cfbe0f682a77.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/74d32105aead3aeafc536172b1844c98/b3df25c5-ed82-4e4c-b28f-cfbe0f682a77.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>That tab displays the entire state of the Livewire component in which the exception occurred.</p>
<p><img src="https://assets.flareapp.io/production/blog/45373c8664840b3565be7faf062dd0aa/3bf25d05-1786-4bca-9c0b-072d848ad2f0.png" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/45373c8664840b3565be7faf062dd0aa/3bf25d05-1786-4bca-9c0b-072d848ad2f0.png" width="2586" height="1992" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/45373c8664840b3565be7faf062dd0aa/3bf25d05-1786-4bca-9c0b-072d848ad2f0.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/45373c8664840b3565be7faf062dd0aa/3bf25d05-1786-4bca-9c0b-072d848ad2f0.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/45373c8664840b3565be7faf062dd0aa/3bf25d05-1786-4bca-9c0b-072d848ad2f0.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/45373c8664840b3565be7faf062dd0aa/3bf25d05-1786-4bca-9c0b-072d848ad2f0.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>When you make a typo in calling a method on a Livewire component, we'll also suggest the correct method. Of course, this works for properties as well.</p>
<p><img src="https://assets.flareapp.io/production/blog/d3afe1a36ade6cf256ba57067eb46cc1/e928eb01-e552-474c-8ed0-c994f4fcbe59.png" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/d3afe1a36ade6cf256ba57067eb46cc1/e928eb01-e552-474c-8ed0-c994f4fcbe59.png" width="2462" height="814" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/d3afe1a36ade6cf256ba57067eb46cc1/e928eb01-e552-474c-8ed0-c994f4fcbe59.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/d3afe1a36ade6cf256ba57067eb46cc1/e928eb01-e552-474c-8ed0-c994f4fcbe59.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/d3afe1a36ade6cf256ba57067eb46cc1/e928eb01-e552-474c-8ed0-c994f4fcbe59.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/d3afe1a36ade6cf256ba57067eb46cc1/e928eb01-e552-474c-8ed0-c994f4fcbe59.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<h2>Showing detailed information for Laravel jobs</h2>
<p>When an error occurs inside a job, we now show all properties of that job, and this will potentially help you fix the problem faster.</p>
<p><img src="https://assets.flareapp.io/production/blog/f12084fc7e690e38ea446c865b1f79cc/7414e17d-c6a1-4cbf-b459-10bbb8289773.png" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/f12084fc7e690e38ea446c865b1f79cc/7414e17d-c6a1-4cbf-b459-10bbb8289773.png" width="1388" height="1802" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/f12084fc7e690e38ea446c865b1f79cc/7414e17d-c6a1-4cbf-b459-10bbb8289773.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/f12084fc7e690e38ea446c865b1f79cc/7414e17d-c6a1-4cbf-b459-10bbb8289773.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/f12084fc7e690e38ea446c865b1f79cc/7414e17d-c6a1-4cbf-b459-10bbb8289773.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1388/https://assets.flareapp.io/production/blog/f12084fc7e690e38ea446c865b1f79cc/7414e17d-c6a1-4cbf-b459-10bbb8289773.png 1388w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p><a href="https://flareapp.io/blog/38-flares-new-job-tab-knows-all-about-your-failed-jobs">Read more in this blog post</a></p>
<h2>Vastly improved error search</h2>
<p>When Flare was launched, you had to search for errors in a basic  GitHub like search field that didn't have any autocompletion. That search now has autocompletion, which makes it much more powerful.</p>
<p><img src="https://assets.flareapp.io/production/blog/aeaeae5708cb84383b5d6869e28d60c2/25ab7cfe-076e-4b4d-9f93-61dc997f3059.gif" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/aeaeae5708cb84383b5d6869e28d60c2/25ab7cfe-076e-4b4d-9f93-61dc997f3059.gif" width="2000" height="1274"></p>
<p>Another improvement that we made was showing a nice graph with the error counts of all the errors.</p>
<p><img src="https://assets.flareapp.io/production/blog/aeaeae5708cb84383b5d6869e28d60c2/25ab7cfe-076e-4b4d-9f93-61dc997f3059.gif" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/aeaeae5708cb84383b5d6869e28d60c2/25ab7cfe-076e-4b4d-9f93-61dc997f3059.gif" width="2000" height="1274"></p>
<p><a href="https://flareapp.io/blog/36-building-a-better-search-with-monaco-and-amcharts">Read more in this blog post</a></p>
<h2>Integrating with GitHub Issues</h2>
<p>Errors on Flare can now be associated with issues on GitHub and vice versa. This integration allows to:</p>
<ul>
<li>
<p>create a GitHub issue directly on a Flare error</p>
</li>
<li>
<p>associate a GitHub issue with a Flare error by mentioning a Flare URL in the GitHub issue</p>
</li>
<li>
<p>automatically resolve an error on Flare when you close the GitHub issue</p>
</li>
<li>
<p>automatically close a GitHub issue when you resolve an error in Flare</p>
</li>
</ul>
<p><img src="https://assets.flareapp.io/production/blog/8903e1143440feebe4bd40c117da2379/9a0b70c6-8cd4-474a-97b1-36c98aa93c4e.png" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/8903e1143440feebe4bd40c117da2379/9a0b70c6-8cd4-474a-97b1-36c98aa93c4e.png" width="1102" height="960" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/8903e1143440feebe4bd40c117da2379/9a0b70c6-8cd4-474a-97b1-36c98aa93c4e.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/8903e1143440feebe4bd40c117da2379/9a0b70c6-8cd4-474a-97b1-36c98aa93c4e.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/8903e1143440feebe4bd40c117da2379/9a0b70c6-8cd4-474a-97b1-36c98aa93c4e.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1102/https://assets.flareapp.io/production/blog/8903e1143440feebe4bd40c117da2379/9a0b70c6-8cd4-474a-97b1-36c98aa93c4e.png 1102w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p><a href="https://flareapp.io/blog/23-introducing-our-github-integration">Read more in this blog post</a></p>
<h2>Notifications via Discord, Microsoft Teams and Telegram</h2>
<p>A key feature of our service is that we'll notify you when errors occur. Each development team has their favourite way of getting notified. When enough of our users request a specific notification channel, we add it to Flare.</p>
<p>This year we added support for Discord, Microsoft Team and Telegram.</p>
<p>Here's how the notification looks like in Discord.</p>
<p><img src="https://assets.flareapp.io/production/blog/01f6858a6ba19dd2e411b36fac114e99/c16f4127-00a6-404c-9d95-a7323342a0d0.png" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/01f6858a6ba19dd2e411b36fac114e99/c16f4127-00a6-404c-9d95-a7323342a0d0.png" width="1198" height="1034" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/01f6858a6ba19dd2e411b36fac114e99/c16f4127-00a6-404c-9d95-a7323342a0d0.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/01f6858a6ba19dd2e411b36fac114e99/c16f4127-00a6-404c-9d95-a7323342a0d0.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/01f6858a6ba19dd2e411b36fac114e99/c16f4127-00a6-404c-9d95-a7323342a0d0.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1198/https://assets.flareapp.io/production/blog/01f6858a6ba19dd2e411b36fac114e99/c16f4127-00a6-404c-9d95-a7323342a0d0.png 1198w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>And here's how's it looks like when using Microsoft Teams.</p>
<p><img src="https://assets.flareapp.io/production/blog/fedc8c629af0fd684f654cbe6e7a2269/f9a5b69c-450a-4215-8c81-a8d105029e46.png" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/fedc8c629af0fd684f654cbe6e7a2269/f9a5b69c-450a-4215-8c81-a8d105029e46.png" width="1818" height="1096" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/fedc8c629af0fd684f654cbe6e7a2269/f9a5b69c-450a-4215-8c81-a8d105029e46.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/fedc8c629af0fd684f654cbe6e7a2269/f9a5b69c-450a-4215-8c81-a8d105029e46.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/fedc8c629af0fd684f654cbe6e7a2269/f9a5b69c-450a-4215-8c81-a8d105029e46.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/fedc8c629af0fd684f654cbe6e7a2269/f9a5b69c-450a-4215-8c81-a8d105029e46.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>For Telegram, we went one step further and made the notifications interactive. This allows you to resolve or snooze a notification and even creating a GitHub issue right from Telegram.</p>
<p><img src="https://assets.flareapp.io/production/blog/69a8b890d81191adacd5b54e10a3f22d/40d404cf-88f8-45b5-98ae-0ad433fbc81f.png" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/69a8b890d81191adacd5b54e10a3f22d/40d404cf-88f8-45b5-98ae-0ad433fbc81f.png" width="2406" height="1330" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/69a8b890d81191adacd5b54e10a3f22d/40d404cf-88f8-45b5-98ae-0ad433fbc81f.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/69a8b890d81191adacd5b54e10a3f22d/40d404cf-88f8-45b5-98ae-0ad433fbc81f.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/69a8b890d81191adacd5b54e10a3f22d/40d404cf-88f8-45b5-98ae-0ad433fbc81f.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/69a8b890d81191adacd5b54e10a3f22d/40d404cf-88f8-45b5-98ae-0ad433fbc81f.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>More info in these <a href="https://flareapp.io/blog/9-flare-can-now-notify-you-via-discord-and-microsoft-teams">two</a> <a href="https://flareapp.io/blog/37-introducing-our-new-telegram-integration">blogposts</a>.</p>
<h2>Interactive mail notifications</h2>
<p>We've made our mail notifications more powerful. You can resolve, snooze, or create a GitHub issue right from a mail.</p>
<p><img src="https://assets.flareapp.io/production/blog/663e7413bf9a6564b40599d7ed66a42a/c6a75a7b-2d13-4404-a7b5-35e4b2aef652.png" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/663e7413bf9a6564b40599d7ed66a42a/c6a75a7b-2d13-4404-a7b5-35e4b2aef652.png" width="1350" height="1776" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/663e7413bf9a6564b40599d7ed66a42a/c6a75a7b-2d13-4404-a7b5-35e4b2aef652.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/663e7413bf9a6564b40599d7ed66a42a/c6a75a7b-2d13-4404-a7b5-35e4b2aef652.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/663e7413bf9a6564b40599d7ed66a42a/c6a75a7b-2d13-4404-a7b5-35e4b2aef652.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1350/https://assets.flareapp.io/production/blog/663e7413bf9a6564b40599d7ed66a42a/c6a75a7b-2d13-4404-a7b5-35e4b2aef652.png 1350w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p><a href="https://flareapp.io/blog/13-mail-notifications-now-allow-to-snooze-and-resolve-errors">Read more in this blog post</a></p>
<h2>Showing error trends and favourites at the project overview</h2>
<p>You can now mark a project as a favourite at the project overview. These projects will be displayed at the top of the list in a card showing extra details.</p>
<p><img src="https://assets.flareapp.io/production/blog/cf309275865b9740724defc9980a41c9/a1b53446-1688-40fc-99f8-6dbf50f5b2e7.png" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/cf309275865b9740724defc9980a41c9/a1b53446-1688-40fc-99f8-6dbf50f5b2e7.png" width="2528" height="1812" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/cf309275865b9740724defc9980a41c9/a1b53446-1688-40fc-99f8-6dbf50f5b2e7.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/cf309275865b9740724defc9980a41c9/a1b53446-1688-40fc-99f8-6dbf50f5b2e7.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/cf309275865b9740724defc9980a41c9/a1b53446-1688-40fc-99f8-6dbf50f5b2e7.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/cf309275865b9740724defc9980a41c9/a1b53446-1688-40fc-99f8-6dbf50f5b2e7.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>For each project in the list, we also show a little trend graph that immediately shows if there are more errors coming in as usual.</p>
<p><a href="https://flareapp.io/blog/22-meet-the-new-projects-overview-with-error-trends-and-favourites">Read more in this blog post</a></p>
<h2>Sending a monthly error report mail</h2>
<p>At the end of each month, we now email you a summary of all projects and our latest feature announcements.</p>
<p><img src="https://assets.flareapp.io/production/blog/c038c63befbb16de3e82a8456f7a8bd6/56c7f3c8-0fcc-432c-b5aa-5c0fe2a73bee.png" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/c038c63befbb16de3e82a8456f7a8bd6/56c7f3c8-0fcc-432c-b5aa-5c0fe2a73bee.png" width="1308" height="1656" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/c038c63befbb16de3e82a8456f7a8bd6/56c7f3c8-0fcc-432c-b5aa-5c0fe2a73bee.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/c038c63befbb16de3e82a8456f7a8bd6/56c7f3c8-0fcc-432c-b5aa-5c0fe2a73bee.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/c038c63befbb16de3e82a8456f7a8bd6/56c7f3c8-0fcc-432c-b5aa-5c0fe2a73bee.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1308/https://assets.flareapp.io/production/blog/c038c63befbb16de3e82a8456f7a8bd6/56c7f3c8-0fcc-432c-b5aa-5c0fe2a73bee.png 1308w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p><a href="https://flareapp.io/blog/14-introducing-the-monthly-error-report-mail">Read more in this blog post</a></p>
<h2>Handling customer feedback using a support bubble form</h2>
<p>We highly value the feedback and feature requests we get from our users, and that's why we want to make it as easy as possible to communicate with us.</p>
<p>When you log into Flare, you'll now see the support bubble on the bottom right of every page. Click the bubble to open the support form that can be used to submit a bug report or a suggestion. It is as simple as that.</p>
<p><img src="https://assets.flareapp.io/production/blog/dbe62537216757a71102ae73eb3d5950/f2929eca-f0f5-4881-952a-1f3572d97e39.gif" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/dbe62537216757a71102ae73eb3d5950/f2929eca-f0f5-4881-952a-1f3572d97e39.gif" width="450" height="572"></p>
<p>Under the hood, this uses <a href="https://github.com/spatie/laravel-support-bubble">our laravel-support-bubble package</a>.</p>
<h2>Interacting with Flare via the API</h2>
<p>Behind the scenes, Flare always had an API for people that requested access. We've polished and <a href="https://flareapp.io/docs/general/using-the-api">documented</a> that API and made it possible for every user to use it.</p>
<p><img src="https://assets.flareapp.io/production/blog/9a1084dfdbeadc4e175293e981fb84aa/4fb0215d-8aef-43a8-a441-81b78031f58a.png" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/9a1084dfdbeadc4e175293e981fb84aa/4fb0215d-8aef-43a8-a441-81b78031f58a.png" width="2494" height="1400" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/9a1084dfdbeadc4e175293e981fb84aa/4fb0215d-8aef-43a8-a441-81b78031f58a.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/9a1084dfdbeadc4e175293e981fb84aa/4fb0215d-8aef-43a8-a441-81b78031f58a.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/9a1084dfdbeadc4e175293e981fb84aa/4fb0215d-8aef-43a8-a441-81b78031f58a.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/9a1084dfdbeadc4e175293e981fb84aa/4fb0215d-8aef-43a8-a441-81b78031f58a.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<h2>Miscellaneous UI improvements</h2>
<p>You can now easily see if an error came from the web, CLI, or queue on an error card.</p>
<p><img src="https://assets.flareapp.io/production/blog/014b19ad81385d91a8ae6d6807707454/783f17aa-495e-44d3-9761-09696ef542b7.png" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/014b19ad81385d91a8ae6d6807707454/783f17aa-495e-44d3-9761-09696ef542b7.png" width="1614" height="504" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/014b19ad81385d91a8ae6d6807707454/783f17aa-495e-44d3-9761-09696ef542b7.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/014b19ad81385d91a8ae6d6807707454/783f17aa-495e-44d3-9761-09696ef542b7.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/014b19ad81385d91a8ae6d6807707454/783f17aa-495e-44d3-9761-09696ef542b7.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/014b19ad81385d91a8ae6d6807707454/783f17aa-495e-44d3-9761-09696ef542b7.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>We now show who resolved an error and via which channel.</p>
<p><img src="https://assets.flareapp.io/production/blog/225f02b24afe324fc8d2e83c79cb7180/18798c2f-2ea1-457b-af43-7fb5ed8fe552.png" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/225f02b24afe324fc8d2e83c79cb7180/18798c2f-2ea1-457b-af43-7fb5ed8fe552.png" width="1630" height="552" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/225f02b24afe324fc8d2e83c79cb7180/18798c2f-2ea1-457b-af43-7fb5ed8fe552.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/225f02b24afe324fc8d2e83c79cb7180/18798c2f-2ea1-457b-af43-7fb5ed8fe552.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/225f02b24afe324fc8d2e83c79cb7180/18798c2f-2ea1-457b-af43-7fb5ed8fe552.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/225f02b24afe324fc8d2e83c79cb7180/18798c2f-2ea1-457b-af43-7fb5ed8fe552.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>On the request tab, you'll now see the curl command that you can use to retry the request.</p>
<p><img src="https://assets.flareapp.io/production/blog/d1e37eb0700d4ba64c4c1e72f86ea39e/7404103f-def3-4e69-9892-8badb5a48027.jpg" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/d1e37eb0700d4ba64c4c1e72f86ea39e/7404103f-def3-4e69-9892-8badb5a48027.jpg" width="2048" height="1015" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/d1e37eb0700d4ba64c4c1e72f86ea39e/7404103f-def3-4e69-9892-8badb5a48027.jpg 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/d1e37eb0700d4ba64c4c1e72f86ea39e/7404103f-def3-4e69-9892-8badb5a48027.jpg 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/d1e37eb0700d4ba64c4c1e72f86ea39e/7404103f-def3-4e69-9892-8badb5a48027.jpg 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/d1e37eb0700d4ba64c4c1e72f86ea39e/7404103f-def3-4e69-9892-8badb5a48027.jpg 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>On the debug tab, rendering of the query bindings has been improved.</p>
<p><img src="https://assets.flareapp.io/production/blog/7ac1e859fc51d71f5e83f6deadd8fa60/94c11b2e-7475-4742-8aa8-3588db8dde50.png" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/7ac1e859fc51d71f5e83f6deadd8fa60/94c11b2e-7475-4742-8aa8-3588db8dde50.png" width="2844" height="1298" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/7ac1e859fc51d71f5e83f6deadd8fa60/94c11b2e-7475-4742-8aa8-3588db8dde50.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/7ac1e859fc51d71f5e83f6deadd8fa60/94c11b2e-7475-4742-8aa8-3588db8dde50.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/7ac1e859fc51d71f5e83f6deadd8fa60/94c11b2e-7475-4742-8aa8-3588db8dde50.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/7ac1e859fc51d71f5e83f6deadd8fa60/94c11b2e-7475-4742-8aa8-3588db8dde50.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p><a href="https://flareapp.io/docs/flare/general/spike-protection">Spike protection</a> will prevent all events of your plan from being used in a small timespan. You can now see the periods when Spike protection was active.</p>
<p><img src="https://assets.flareapp.io/production/blog/7e29210f23d31860ef435fdacb2456d4/c493be9c-a7ff-45ad-8047-884438ae277b.png" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/7e29210f23d31860ef435fdacb2456d4/c493be9c-a7ff-45ad-8047-884438ae277b.png" width="2480" height="1604" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/7e29210f23d31860ef435fdacb2456d4/c493be9c-a7ff-45ad-8047-884438ae277b.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/7e29210f23d31860ef435fdacb2456d4/c493be9c-a7ff-45ad-8047-884438ae277b.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/7e29210f23d31860ef435fdacb2456d4/c493be9c-a7ff-45ad-8047-884438ae277b.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/7e29210f23d31860ef435fdacb2456d4/c493be9c-a7ff-45ad-8047-884438ae277b.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>We've added a "select all" link to select all errors in a project. This way, you could snooze, resolve, or delete all project errors in one go.</p>
<p><img src="https://assets.flareapp.io/production/blog/de175d915f34c8dc432a3b0ed8877718/403b43bc-5e57-43be-84df-d7ed82f86455.png" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/de175d915f34c8dc432a3b0ed8877718/403b43bc-5e57-43be-84df-d7ed82f86455.png" width="2494" height="772" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/de175d915f34c8dc432a3b0ed8877718/403b43bc-5e57-43be-84df-d7ed82f86455.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/de175d915f34c8dc432a3b0ed8877718/403b43bc-5e57-43be-84df-d7ed82f86455.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/de175d915f34c8dc432a3b0ed8877718/403b43bc-5e57-43be-84df-d7ed82f86455.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/de175d915f34c8dc432a3b0ed8877718/403b43bc-5e57-43be-84df-d7ed82f86455.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>Our billing portal got a new coat of paint. Behind the scenes, we're <a href="https://flareapp.io/blog/29-migrating-our-billing-portal-to-the-latest-version-of-laravel-spark">using</a> the latest version of Laravel Spark.</p>
<p><img src="https://assets.flareapp.io/production/blog/b85a68e925407a115ffe855d710f2175/eed91742-e703-4d0a-aa93-6f8e2492a7f3.png" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/b85a68e925407a115ffe855d710f2175/eed91742-e703-4d0a-aa93-6f8e2492a7f3.png" width="2782" height="2592" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/b85a68e925407a115ffe855d710f2175/eed91742-e703-4d0a-aa93-6f8e2492a7f3.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/b85a68e925407a115ffe855d710f2175/eed91742-e703-4d0a-aa93-6f8e2492a7f3.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/b85a68e925407a115ffe855d710f2175/eed91742-e703-4d0a-aa93-6f8e2492a7f3.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/b85a68e925407a115ffe855d710f2175/eed91742-e703-4d0a-aa93-6f8e2492a7f3.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>It was long overdue, but we finally made our documentation searchable. Code highlighting of the code snippets looks better now that we render them <a href="https://flareapp.io/blog/33-better-code-block-highlighting-on-our-blog-and-docs">using Shiki</a>.</p>
<p><img src="https://assets.flareapp.io/production/blog/b4cb9e04876ceeaf132f847a13d8eb49/b5065f69-0e3f-4518-97ef-f38c287ddefb.png" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/b4cb9e04876ceeaf132f847a13d8eb49/b5065f69-0e3f-4518-97ef-f38c287ddefb.png" width="1700" height="1304" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/b4cb9e04876ceeaf132f847a13d8eb49/b5065f69-0e3f-4518-97ef-f38c287ddefb.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/b4cb9e04876ceeaf132f847a13d8eb49/b5065f69-0e3f-4518-97ef-f38c287ddefb.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/b4cb9e04876ceeaf132f847a13d8eb49/b5065f69-0e3f-4518-97ef-f38c287ddefb.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/b4cb9e04876ceeaf132f847a13d8eb49/b5065f69-0e3f-4518-97ef-f38c287ddefb.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<h2>Removing inactive users and teams automatically</h2>
<p>Like many other SaaS applications, you can try out Flare using a trial period. Of course, not everybody that tries out Flare will convert, and after the trial period is over, some people will not use the service anymore.</p>
<h2>Snoozing notifications per application version</h2>
<p>We've introduced the concept of an application version in Flare. And you can now snooze errors for a specific version. This way, you will get notified if the same error occurs again in a new version of your app.</p>
<p><img src="https://assets.flareapp.io/production/blog/7bb55147293f4223193e686b5aa670c8/ff706408-2a2d-42f2-a876-c10aeb1beb95.png" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/7bb55147293f4223193e686b5aa670c8/ff706408-2a2d-42f2-a876-c10aeb1beb95.png" width="2534" height="642" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/7bb55147293f4223193e686b5aa670c8/ff706408-2a2d-42f2-a876-c10aeb1beb95.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/7bb55147293f4223193e686b5aa670c8/ff706408-2a2d-42f2-a876-c10aeb1beb95.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/7bb55147293f4223193e686b5aa670c8/ff706408-2a2d-42f2-a876-c10aeb1beb95.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/7bb55147293f4223193e686b5aa670c8/ff706408-2a2d-42f2-a876-c10aeb1beb95.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p><a href="https://flareapp.io/blog/11-snooze-notifications-per-application-version">Read more in this post</a></p>
<h2>In closing</h2>
<p>We're very proud of the improvements we've made to Flare in the past 12 months. In 2022, we'll continue listening to the feedback from our users. We can already share that we will revamp our entire UI, and work on it is well underway. We can't share a screenshot of the new Flare UI yet, but it will take hints of the new layout we're implementing for Ignition.</p>
<p>Here's how the new version of Ignition will look like.</p>
<p><img src="https://assets.flareapp.io/production/blog/1c18b147c44cab04dca62a7955cb4da2/f19ceb3e-cac1-4fda-a0df-437bf5742d15.png" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/1c18b147c44cab04dca62a7955cb4da2/f19ceb3e-cac1-4fda-a0df-437bf5742d15.png" width="3000" height="3156" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/1c18b147c44cab04dca62a7955cb4da2/f19ceb3e-cac1-4fda-a0df-437bf5742d15.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/1c18b147c44cab04dca62a7955cb4da2/f19ceb3e-cac1-4fda-a0df-437bf5742d15.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/1c18b147c44cab04dca62a7955cb4da2/f19ceb3e-cac1-4fda-a0df-437bf5742d15.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/1c18b147c44cab04dca62a7955cb4da2/f19ceb3e-cac1-4fda-a0df-437bf5742d15.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>You can see a preview in <a href="https://twitter.com/flareappio/status/1460560582382428162">this Twitter thread</a>!</p>
<p>If you have any suggestions to make Flare better, let us know by <a href="mailto:support@flareapp.io">mailing us</a> or posting your suggestion in <a href="https://github.com/spatie/flareapp.io-roadmap/discussions">our roadmap repo on GitHub</a>. We're listening!</p>
]]>
            </summary>
                                    <updated>2021-12-15T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Better support for Livewire in Flare and Ignition]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/better-support-for-livewire-in-flare-and-ignition" />
            <id>https://flareapp.io/better-support-for-livewire-in-flare-and-ignition</id>
            <author>
                <name><![CDATA[Ruben]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Livewire is quickly gaining traction in the Laravel ecosystem. It allows you to write dynamic web applications without ever even having to touch JavaScript! That's why we've decided to bring first-party support for Livewire components to Flare and Ignition.</p>
<p>Starting from Ignition v2.17 and above, if something goes wrong within your Livewire component's lifecycle, the accompanying error page includes a new tab that looks like this:</p>
<p><img src="https://assets.flareapp.io/production/blog/694ee52c7d9ad4da74eb6deb3596dd73/b2058f1e-2831-44fb-b8af-bfa2286960ce.png" alt="Livewire screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/694ee52c7d9ad4da74eb6deb3596dd73/b2058f1e-2831-44fb-b8af-bfa2286960ce.png" width="2524" height="1754" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/694ee52c7d9ad4da74eb6deb3596dd73/b2058f1e-2831-44fb-b8af-bfa2286960ce.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/694ee52c7d9ad4da74eb6deb3596dd73/b2058f1e-2831-44fb-b8af-bfa2286960ce.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/694ee52c7d9ad4da74eb6deb3596dd73/b2058f1e-2831-44fb-b8af-bfa2286960ce.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/694ee52c7d9ad4da74eb6deb3596dd73/b2058f1e-2831-44fb-b8af-bfa2286960ce.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>This new tab will show up on your local Ignition error page as well as in Flare's reports. It show which component was being rendered when the error occurred, what updates were triggered right before (property updated, method called or event send) and the data used to render the component.</p>
<p>We've also added a couple new solutions to Ignition specifically for Livewire. For example, a typo in a property name will trigger a solution that tries to suggest the corrent name:</p>
<p><img src="https://assets.flareapp.io/production/blog/89982d107938aad709e7f89ff1ab9652/fd78944e-c740-4b16-be27-9713efcca0e9.png" alt="Livewire property solution screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/89982d107938aad709e7f89ff1ab9652/fd78944e-c740-4b16-be27-9713efcca0e9.png" width="2472" height="834" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/89982d107938aad709e7f89ff1ab9652/fd78944e-c740-4b16-be27-9713efcca0e9.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/89982d107938aad709e7f89ff1ab9652/fd78944e-c740-4b16-be27-9713efcca0e9.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/89982d107938aad709e7f89ff1ab9652/fd78944e-c740-4b16-be27-9713efcca0e9.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/89982d107938aad709e7f89ff1ab9652/fd78944e-c740-4b16-be27-9713efcca0e9.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>The solution is displayed when a method with a typo is wrongfully called in a Livewire component:</p>
<p><img src="https://assets.flareapp.io/production/blog/8cbef3abbf08007ab97dba172c123ebd/bc0adae4-206b-44a0-b65f-c94768b61b9d.png" alt="Livewire method solution screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/8cbef3abbf08007ab97dba172c123ebd/bc0adae4-206b-44a0-b65f-c94768b61b9d.png" width="2462" height="814" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/8cbef3abbf08007ab97dba172c123ebd/bc0adae4-206b-44a0-b65f-c94768b61b9d.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/8cbef3abbf08007ab97dba172c123ebd/bc0adae4-206b-44a0-b65f-c94768b61b9d.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/8cbef3abbf08007ab97dba172c123ebd/bc0adae4-206b-44a0-b65f-c94768b61b9d.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/8cbef3abbf08007ab97dba172c123ebd/bc0adae4-206b-44a0-b65f-c94768b61b9d.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>Whether an exception is thrown in a Livewire component, Inertia response or a regular Blade view. Flare and Ignition will try to make it as smooth as possible to show you what happened!</p>
]]>
            </summary>
                                    <updated>2021-11-29T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Grouping SQL errors]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/grouping-sql-errors" />
            <id>https://flareapp.io/grouping-sql-errors</id>
            <author>
                <name><![CDATA[Ruben]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Flare gets a lot of errors every day. To keep a good overview, we try to group them as best as possible because you don't want to end with thousands of errors in your Flare project when there's only one bug within your codebase.</p>
<p>For most errors, grouping is relatively easy. We look at the top frame within the call stack where the error occurred and keep track of the class, method and line where the error occurred. When Flare receives another error, we again look at this top frame and group it by these parameters.</p>
<p>This method works pretty well for most exceptions thrown within PHP. But there are cases where this approach isn't giving the best results.</p>
<h2>SQL Exceptions</h2>
<p>A Laravel application will throw a <code>QueryException</code> whenever something goes wrong with your database. When we would group by top frame, all database errors would be grouped into a single item. That's something we don't want since those SQL errors could be entirely different.</p>
<p>We could not use the top frame of the call stack. But go deeper in the stack until we find a frame within your application. But what then if an external package caused the exception? It could be that we never reach an application frame. Also, the deeper we go through the call stack, the more general the path of execution becomes. We could be grouping errors that have nothing to do with each other.</p>
<p>Flare uses an alternative solution. We do not group by top frame in cases of a <code>QueryException</code> but by exception class and message. Now, these two errors become two different items within Flare:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">SQLSTATE[42S22]: Column not found: 1054 Unknown column 'titl' in 'field list' (SQL: SELECT * FROM `posts` WHERE `titl` = 'flare groupings')
</span></span></code></pre>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">SQLSTATE[42S22]: Column not found: 1054 Unknown column 'publishe' in 'field list' (SQL: UPDATE `posts` SET `publishe` = 1 WHERE `id` = 1)
</span></span></code></pre>
<p>Great! But what happens to the grouping when we run this piece of code, and it fails?</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">public function publish(Post $post, bool $published): void
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">	$post-&gt;update(['publishe' =&gt; $published]);
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>The answer isn't that clear. It depends since a post can have multiple id's and <code>published</code> can be <code>0</code> or <code>1</code>. The following two exceptions could be thrown when this piece of code fails:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">SQLSTATE[42S22]: Column not found: 1054 Unknown column 'publishe' in 'field list' (SQL: UPDATE `posts` SET `publishe` = 0 WHERE `id` = 1)
</span></span></code></pre>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">SQLSTATE[42S22]: Column not found: 1054 Unknown column 'publishe' in 'field list' (SQL: UPDATE `posts` SET `publishe` = 1 WHERE `id` = 1)
</span></span></code></pre>
<p>This will cause Flare to create two entries for errors that should be grouped since they represent the same bug. We want to avoid these scenarios at all costs!</p>
<p>Let's take a look at the two queries:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">UPDATE `posts` SET `publishe` = 0 WHERE `id` = 1
</span></span></code></pre>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">UPDATE `posts` SET `publishe` = 1 WHERE `id` = 1
</span></span></code></pre>
<p>When we would use prepared statements with bindings where we sent the values separate from the query to the database server then, those queries would look like this:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">UPDATE `posts` SET `publishe` = ? WHERE `id` = ?
</span></span></code></pre>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">UPDATE `posts` SET `publishe` = ? WHERE `id` = ?
</span></span></code></pre>
<p>Great! The two queries are identical. We can now group using the exception class: <code>QueryException</code> and this query with bindings. Sadly enough, often prepared statements are entirely ignored, so we cannot use them.</p>
<p>But what if we could strip all values from these queries to look like queries with bindings?</p>
<h2>Parsing queries</h2>
<p>SQL queries are complicated, we've seen some simple queries earlier, but they can become quite complex. That's why we'll use a <a href="https://github.com/greenlion/PHP-SQL-Parser">SQL parser</a> to parse the queries into tokens so we know what should be replaced and whatnot.</p>
<p>The first thing we need to do is parse the query:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">(new PHPSQLParser($query))-&gt;parsed;
</span></span></code></pre>
<p>Such a parsed query looks like this:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">array:3 [
</span></span><span class="line"><span class="token">  "UPDATE" =&gt; array:1 [
</span></span><span class="line"><span class="token">    0 =&gt; array:10 [
</span></span><span class="line"><span class="token">      "expr_type" =&gt; "table"
</span></span><span class="line"><span class="token">      "table" =&gt; "`posts`"
</span></span><span class="line"><span class="token">      "no_quotes" =&gt; array:2 [
</span></span><span class="line"><span class="token">        "delim" =&gt; false
</span></span><span class="line"><span class="token">        "parts" =&gt; array:1 [
</span></span><span class="line"><span class="token">          0 =&gt; "posts"
</span></span><span class="line"><span class="token">        ]
</span></span><span class="line"><span class="token">      ]
</span></span><span class="line"><span class="token">      "alias" =&gt; false
</span></span><span class="line"><span class="token">      "hints" =&gt; false
</span></span><span class="line"><span class="token">      "join_type" =&gt; "JOIN"
</span></span><span class="line"><span class="token">      "ref_type" =&gt; false
</span></span><span class="line"><span class="token">      "ref_clause" =&gt; false
</span></span><span class="line"><span class="token">      "base_expr" =&gt; "`posts`"
</span></span><span class="line"><span class="token">      "sub_tree" =&gt; false
</span></span><span class="line"><span class="token">    ]
</span></span><span class="line"><span class="token">  ]
</span></span><span class="line"><span class="token">  "SET" =&gt; array:1 [
</span></span><span class="line"><span class="token">    0 =&gt; array:3 [
</span></span><span class="line"><span class="token">      "expr_type" =&gt; "expression"
</span></span><span class="line"><span class="token">      "base_expr" =&gt; "`publishe` = 0"
</span></span><span class="line"><span class="token">      "sub_tree" =&gt; array:3 [
</span></span><span class="line"><span class="token">        0 =&gt; array:4 [
</span></span><span class="line"><span class="token">          "expr_type" =&gt; "colref"
</span></span><span class="line"><span class="token">          "base_expr" =&gt; "`publishe`"
</span></span><span class="line"><span class="token">          "no_quotes" =&gt; array:2 [
</span></span><span class="line"><span class="token">            "delim" =&gt; false
</span></span><span class="line"><span class="token">            "parts" =&gt; array:1 [
</span></span><span class="line"><span class="token">              0 =&gt; "publishe"
</span></span><span class="line"><span class="token">            ]
</span></span><span class="line"><span class="token">          ]
</span></span><span class="line"><span class="token">          "sub_tree" =&gt; false
</span></span><span class="line"><span class="token">        ]
</span></span><span class="line"><span class="token">        1 =&gt; array:3 [
</span></span><span class="line"><span class="token">          "expr_type" =&gt; "operator"
</span></span><span class="line"><span class="token">          "base_expr" =&gt; "="
</span></span><span class="line"><span class="token">          "sub_tree" =&gt; false
</span></span><span class="line"><span class="token">        ]
</span></span><span class="line"><span class="token">        2 =&gt; array:3 [
</span></span><span class="line"><span class="token">          "expr_type" =&gt; "const"
</span></span><span class="line"><span class="token">          "base_expr" =&gt; "0"
</span></span><span class="line"><span class="token">          "sub_tree" =&gt; false
</span></span><span class="line"><span class="token">        ]
</span></span><span class="line"><span class="token">      ]
</span></span><span class="line"><span class="token">    ]
</span></span><span class="line"><span class="token">  ]
</span></span><span class="line"><span class="token">  "WHERE" =&gt; array:3 [
</span></span><span class="line"><span class="token">    0 =&gt; array:4 [
</span></span><span class="line"><span class="token">      "expr_type" =&gt; "colref"
</span></span><span class="line"><span class="token">      "base_expr" =&gt; "`id`"
</span></span><span class="line"><span class="token">      "no_quotes" =&gt; array:2 [
</span></span><span class="line"><span class="token">        "delim" =&gt; false
</span></span><span class="line"><span class="token">        "parts" =&gt; array:1 [
</span></span><span class="line"><span class="token">          0 =&gt; "id"
</span></span><span class="line"><span class="token">        ]
</span></span><span class="line"><span class="token">      ]
</span></span><span class="line"><span class="token">      "sub_tree" =&gt; false
</span></span><span class="line"><span class="token">    ]
</span></span><span class="line"><span class="token">    1 =&gt; array:3 [
</span></span><span class="line"><span class="token">      "expr_type" =&gt; "operator"
</span></span><span class="line"><span class="token">      "base_expr" =&gt; "="
</span></span><span class="line"><span class="token">      "sub_tree" =&gt; false
</span></span><span class="line"><span class="token">    ]
</span></span><span class="line"><span class="token">    2 =&gt; array:3 [
</span></span><span class="line"><span class="token">      "expr_type" =&gt; "const"
</span></span><span class="line"><span class="token">      "base_expr" =&gt; "1"
</span></span><span class="line"><span class="token">      "sub_tree" =&gt; false
</span></span><span class="line"><span class="token">    ]
</span></span><span class="line"><span class="token">  ]
</span></span><span class="line"><span class="token">]
</span></span></code></pre>
<p>We only want to replace the constants within the query, so we update each token where the <code>expr_type</code> is <code>const</code> to not use a value like <code>1</code> or <code>0</code> but to use a <code>?</code> just like with the bindings in prepared statements:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">array:3 [
</span></span><span class="line"><span class="token">  "expr_type" =&gt; "const"
</span></span><span class="line"><span class="token">  "base_expr" =&gt; "1"
</span></span><span class="line"><span class="token">  "sub_tree" =&gt; false
</span></span><span class="line"><span class="token">]
</span></span></code></pre>
<p>Becomes:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">array:3 [
</span></span><span class="line"><span class="token">  "expr_type" =&gt; "const"
</span></span><span class="line"><span class="token">  "base_expr" =&gt; "?"
</span></span><span class="line"><span class="token">  "sub_tree" =&gt; false
</span></span><span class="line"><span class="token">]
</span></span></code></pre>
<p>The cool thing with this parser package is that we can convert these tokens back to a SQL query:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">(new PHPSQLCreator())-&gt;create($parsed);
</span></span></code></pre>
<p>And we're done! Our query now looks like this:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">UPDATE `posts` SET `publishe` = ? WHERE `id` = ?
</span></span></code></pre>
<p>Within Flare, the algorithm of stripping the token tree is a bit more complex than we show here, but the gist is the same. Take some raw values and replace them with question marks.</p>
<h2>More exotic queries</h2>
<p>But this is not where we end, although the parser is quite good at parsing the wildest queries. Sometimes it just cannot understand what's happening. For example:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">UPDATE users SET uuid = 89637150-f807-11ea-aee4-51fe58f2b2b3
</span></span></code></pre>
<p>In some database configurations, this query is valid. And our parser will only strip the first part of the UUID because it thinks the <code>-</code> signs are operators, and the other parts of the UUID are references to columns within your table:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">UPDATE users SET uuid = ? - f807 - 11ea - aee4 - 51fe58f2b2b3
</span></span></code></pre>
<p>The same thing happens with dates:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">UPDATE users SET date = ? - ? - 2020T17:24:34.012345Z
</span></span></code></pre>
<p>That's why, before we parse the query, we preprocess it by stripping out some well-known types of strings like dates, emails, UUIDs:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">preg_replace(
</span></span><span class="line"><span class="token">	[
</span></span><span class="line"><span class="token">		'/\S+@\S+\.\S+/', // emails
</span></span><span class="line"><span class="token">		'/\d{2,4}-\d{2}-\d{2,4}[ T]?(\d{2}:\d{2}:\d{2}([+-]\d{2}:\d{2})?(\.\d{6}Z)?)?/', // dates
</span></span><span class="line"><span class="token">		'/[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}/', // uuids
</span></span><span class="line"><span class="token">	],
</span></span><span class="line"><span class="token">	['?', '?', '?'],
</span></span><span class="line"><span class="token">	$query
</span></span><span class="line"><span class="token">);
</span></span></code></pre>
<p>Preprocessing makes the queries a bit more readable for our parser, which improves our final result drastically!</p>
<h2>Connection issues</h2>
<p>A <code>QueryException</code> always looks like this:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">SQLSTATE[42S22]: Column not found: 1054 Unknown column 'publishe' in 'field list' (SQL: UPDATE `posts` SET `publishe` = 0 WHERE `id` = 1)
</span></span></code></pre>
<p>Did you notice the <code>SQLSTATE[42S22]</code> part? It is a <a href="https://en.wikipedia.org/wiki/SQLSTATE">code</a> telling you what went wrong with your database. For example, a connection issue in a MySql database has the following code: [HY000][2002] looks familiar?</p>
<p>Within Flare, we'll also take a look at these codes. When we notice the error is caused by connection issues and not a bug within your code, then we group all these errors since your database server was probably just a few moments down.</p>
<h2>Impossible queries</h2>
<p>Let's take a look at a final example:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">SELECT * FROM invoices WHERE number = 123ABC
</span></span></code></pre>
<p>Such a query is a difficult one. The parser doesn't know if <code>123ABC</code> is a constant or a reference to a column within your database table.</p>
<p>In the latter case, we don't want to replace the column name with a <code>?</code> since these are not values that change between exceptions.</p>
<p>In such cases, we do not strip the value and keep that part of the query.</p>
]]>
            </summary>
                                    <updated>2021-11-21T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Flare's new "job" tab knows all about your failed jobs]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/flares-new-job-tab-knows-all-about-your-failed-jobs" />
            <id>https://flareapp.io/flares-new-job-tab-knows-all-about-your-failed-jobs</id>
            <author>
                <name><![CDATA[Ruben]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Today we've deployed our latest set of features! This includes a new "job" tab with a ton of extra context for failed jobs:</p>
<p><img src="https://assets.flareapp.io/production/blog/7d6245b12f7c8ea0352bdebf3b93dfdd/f7d453a7-c6c0-48ce-adac-ea3c44674da0.png" alt="job context screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/7d6245b12f7c8ea0352bdebf3b93dfdd/f7d453a7-c6c0-48ce-adac-ea3c44674da0.png" width="1388" height="1802" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/7d6245b12f7c8ea0352bdebf3b93dfdd/f7d453a7-c6c0-48ce-adac-ea3c44674da0.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/7d6245b12f7c8ea0352bdebf3b93dfdd/f7d453a7-c6c0-48ce-adac-ea3c44674da0.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/7d6245b12f7c8ea0352bdebf3b93dfdd/f7d453a7-c6c0-48ce-adac-ea3c44674da0.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1388/https://assets.flareapp.io/production/blog/7d6245b12f7c8ea0352bdebf3b93dfdd/f7d453a7-c6c0-48ce-adac-ea3c44674da0.png 1388w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>Ignition's job tab will display information about the failed job's class, its properties, connection, queue, tags, chained jobs and a lot more. This is all powered by Ignition version 2.16.0, which was released earlier today.</p>
<h2>How does this work?</h2>
<p>Under the hood, Ignition registers a couple "recorders" that hook into your Laravel application. For example, the <code>DumpRecorder</code> will capture and send <code>dump</code> statements to Flare as extra context when an exception takes place. There's also the <code>LogRecorder</code> and <code>QueryRecorder</code> that will... you guessed it: capture and send logs and queries to Flare.</p>
<p>So starting today, Ignition version 2.15.0 and above also includes a <code>JobRecorder</code>. The <code>JobRecorder</code> registers itself in your application and waits for a <code>JobExceptionOccurred</code> event:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">public function register(): self
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">    $this-&gt;app['events']-&gt;listen(JobExceptionOccurred::class, [$this, 'record']);
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    return $this;
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>Laravel sends out a <code>JobExceptionOccurred</code> event each time an exception is thrown within a job. Once the event is captured, we can find the failed queue <code>Job</code> in its <code>$event-&gt;job</code> property. The <code>Job</code> object also contains some basic information about the queue and connection that it failed on:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">[
</span></span><span class="line"><span class="token">    'name' =&gt; $this-&gt;job-&gt;resolveName(),
</span></span><span class="line"><span class="token">    'connection' =&gt; $this-&gt;job-&gt;getConnectionName(),
</span></span><span class="line"><span class="token">    'queue' =&gt; $this-&gt;job-&gt;getQueue(),
</span></span><span class="line"><span class="token">]
</span></span></code></pre>
<p>Other properties like the job's id, when it was pushed onto the queue, the tags associated with it and its actual parameters are all contained in the serialized payload.</p>
<p>Since these jobs need to be executed in a different worker process (e.g. on Laravel Horizon or using Amazon SQS), they've been serialized like this:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">O:21:\"App\\Jobs\\TestFlareJob\":11:{s:27:\"\u0000App\\Jobs\\TestFlareJob\u0000name\";s:1:\"a\";s:3:\"job\";N;s:10:\"connection\";N;s:5:\"queue\";N;s:15:\"chainConnection\";N;s:10:\"chainQueue\";N;s:19:\"chainCatchCallbacks\";N;s:5:\"delay\";N;s:11:\"afterCommit\";N;s:10:\"middleware\";a:0:{}s:7:\"chained\";a:2:{i:0;s:276:\"O:21:\"App\\Jobs\\TestFlareJob\":11:{s:27:\"\u0000App\\Jobs\\TestFlareJob\u0000name\";s:1:\"b\";s:3:\"job\";N;s:10:\"connection\";N;s:5:\"queue\";N;s:15:\"chainConnection\";N;s:10:\"chainQueue\";N;s:19:\"chainCatchCallbacks\";N;s:5:\"delay\";N;s:11:\"afterCommit\";N;s:10:\"middleware\";a:0:{}s:7:\"chained\";a:0:{}}\";i:1;s:276:\"O:21:\"App\\Jobs\\TestFlareJob\":11:{s:27:\"\u0000App\\Jobs\\TestFlareJob\u0000name\";s:1:\"c\";s:3:\"job\";N;s:10:\"connection\";N;s:5:\"queue\";N;s:15:\"chainConnection\";N;s:10:\"chainQueue\";N;s:19:\"chainCatchCallbacks\";N;s:5:\"delay\";N;s:11:\"afterCommit\";N;s:10:\"middleware\";a:0:{}s:7:\"chained\";a:0:{}}\";}}
</span></span></code></pre>
<p>The get to the original data we need to deserialize this string. This will yield the original job object. We can then use class reflection to read its properties to an associative array:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">$properties = collect((new ReflectionClass($command))-&gt;getProperties())
</span></span><span class="line"><span class="token">    -&gt;mapWithKeys(function (ReflectionProperty $property) use ($command) {
</span></span><span class="line"><span class="token">        $property-&gt;setAccessible(true);
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        return [$property-&gt;name =&gt; $property-&gt;getValue($command)];
</span></span><span class="line"><span class="token">    });
</span></span></code></pre>
<p>That's it! This <code>$properties</code> array will be added as context to the Flare report and will soon show up in your Flare reports!</p>
]]>
            </summary>
                                    <updated>2021-11-01T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Building a better search with Monaco and amCharts]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/building-a-better-search-with-monaco-and-amcharts" />
            <id>https://flareapp.io/building-a-better-search-with-monaco-and-amcharts</id>
            <author>
                <name><![CDATA[Alex]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>A couple of months ago we refactored a considerable part of Flare's search to better utilise ElasticSearch's amazing search capabilities. We introduced better filters with autocompletion and fuzzy matching. Using our <a href="https://github.com/spatie/elasticsearch-search-string-parser">elasticsearch-search-string-parser</a> package we also created a custom search language to query ElasticSearch.</p>
<p>Inspired by GitHub's issue and PR search, we decided to keep the UI as simple as possible: a plain text field with a simple dropdown for basic autocompletion. You can read all about <a href="https://flareapp.io/blog/28-refactoring-our-search-capabilities-using-elasticsearch-and-two-new-spatie-packages">this blog post</a>.</p>
<p>Even though we were pretty happy with the results, we felt that we could do better. Let's <code>git checkout search-v2</code> and see what's going on.</p>
<h2>A better date range selector</h2>
<p>For a start, using a plaintext query to specify a timeframe is not very fun. Queries like <code>is:unresolved received_at:&gt;2021-08-01 received_at:&lt;=2021-08-31</code> are a pain to read –let alone write– especially without syntax highlighting.</p>
<p>We quickly decided to replace the <code>received_at</code> filters with a fancy UI component we had been eyeing for a while: <a href="https://www.amcharts.com/demos/line-chart-with-scroll-and-zoom/">amCharts' zoomable/scrollable charts</a> and their <a href="https://www.amcharts.com/docs/v4/tutorials/plugin-range-selector/">range selector plugin</a>.</p>
<p><img src="https://assets.flareapp.io/production/blog/6a521383bf547ca5b7738af6b9120608/34d5e1a3-5ce0-421c-83b0-38fad0688faa.gif" alt="Time range selector in action" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/6a521383bf547ca5b7738af6b9120608/34d5e1a3-5ce0-421c-83b0-38fad0688faa.gif" width="2000" height="1126"></p>
<p>This doesn't only solve the time range selector problem, but also provides a neat graph with the number of occurrences for each error matched by the search query. It also comes with some unique features like being able to handle 43.000 data points (one per minute for 30 days) and automatic grouping per day, hour or 10-minute interval, based on the zoom-level.</p>
<p>Resolving the chart data points was a bit tricky. We really wanted to filter the charts data using the ElasticSearch query. However, the occurrence counts and timestamps are <strong>only</strong> stored in the MySQL database. As a solution we ended up querying ElasticSearch for only the error IDs and nothing else. This kept the query short and the response data small. We then query MySQL for the actual occurrence counts per minute for only those error IDs.</p>
<p>Depending on the complexity of the search query, this whole process takes somewhere around 450ms. Because each query is pretty unique (different time frames, different search query, errors coming in all the time) we're not attempting to cache the graph data at all. In theory we could hash all these variables together and cache the graph data, but the cache hit percentage would be too low to make it worth it. That doesn't mean we can't manually cache some of the most frequent queries. For example, the occurrence graph data for the default search query (<code>is:unresolved is:unsnoozed</code>) is fetched entirely from cache without every hitting the ElasticSearch server.</p>
<h2>A better search field</h2>
<p><img src="https://assets.flareapp.io/production/blog/3cc884047faa8e1aae547c4803cfaf93/71bc758e-df5c-47b9-89c0-f370f7121883.gif" alt="Time range selector in action" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/3cc884047faa8e1aae547c4803cfaf93/71bc758e-df5c-47b9-89c0-f370f7121883.gif" width="2000" height="1274"></p>
<p>At this point we had a cool looking occurrence graph next to a very plain search field. Inspired by <a href="https://sourcegraph.com/search">Sourcegraph's search</a> and <a href="https://github.com/nhabuiduc/react-filter-box">this cool React package</a> we decided to try to compress the entire <a href="https://github.com/microsoft/monaco-editor">Monaco editor</a> (the open-source editor that powers VS Code) down to a single line and use it as an input field. As the Monaco editor is super extensible we can easily provide context-aware autocompletion, syntax highlighting and other goodies. Let's dive into the details.</p>
<h3>Installing Monaco</h3>
<p>The Monaco editor is no lightweight package. It comes in at 3MB (ungzipped) and includes a Webpack loader plugin to help shave off some of those unnecessary kilobytes. However, to avoid having to deal with Webpack and it's infinite complaints about our bundle, we decided to go with a <a href="https://github.com/suren-atoyan/monaco-react">suren-atoyan/monaco-react</a>. As the name suggests, this is a React wrapper around the Monaco editor. More importantly: it includes its own <code>monaco-loader</code> that works out of the box and without having to deal with Webpack.</p>
<h3>Monaco on a single line</h3>
<p>We can't simply set the editor's height to <code>1em</code> to turn it into a single line input field. There are a couple gotchas that need to be taken care of as well.</p>
<p>For starters, pressing the enter key in most code editors will insert a newline. It's really hard to get rid of that behaviour in Monaco without hacking into its internal event listeners and keybindinds (believe me, we tried). The easy way around is to hook into the editor's <code>onChange</code> event and literally replace every newline character with an empty string: <code>value.replace(/[\n\r]/g, '')</code>.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">&lt;Editor
</span></span><span class="line"><span class="token">    height="16px"
</span></span><span class="line"><span class="token">    value={value}
</span></span><span class="line"><span class="token">    onChange={value =&gt; setValue(value.replace(/[\n\r]/g, ''))}
</span></span><span class="line"><span class="token">    options={options}
</span></span><span class="line"><span class="token">/&gt;
</span></span></code></pre>
<p>While this is a somewhat unconventional solution, it still works with no performance impact or fanthom newlines.</p>
<p>Apart from setting the editor height to <code>16px</code> (= single line-height) and removing any newlines, we're passing in some <code>options</code> too. This options object contains a lot of the same keys you might recognise from VS Code's settings. For example, we had to disable line numbers using <code>lineNumbers: 'off'</code>. We also hid the default minimap, disabled all rulers, unregistered all keybinds and disabled the context menu. The devil really is in the details. After all those changes, our <code>&lt;SingleLineMonacoEditor /&gt;</code> component finally rendered a blank 16px high rectangle that can be typed in. Perfect!</p>
<h3>Adding a custom language for search</h3>
<p>To be able to highlight custom syntax, you need to be able to parse the custom syntax. Or so we thought. In practice it's really a lot easier. The Monaco editor only expects a <code>tokensProvider</code> that can convert a document into an array of tokens. Even easier: it comes with a token provider called <code>Monarch</code> out of the box and it honestly feels like cheating. Monarch is configured using <em>only</em> JSON and <a href="https://microsoft.github.io/monaco-editor/monarch.html">the Monarch config for highlighting all JavaScript code is only 150 lines (switch to the JavaScript sample)</a>.</p>
<p>For Flare's query language, we don't even need most of Monarch's features. We'll only provide an array of <code>keywords</code> (each search filter is a keyword) and a rule to apply a <code>filter</code> tag to keyword tokens. Take a look at the literal 13 lines of code for Flare's tokeniser:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">    keywords: filter.map(filter =&gt; filter.keyword),
</span></span><span class="line"><span class="token">    tokenizer: {
</span></span><span class="line"><span class="token">        root: [
</span></span><span class="line"><span class="token">            [
</span></span><span class="line"><span class="token">                /\w+/, { cases: { 
</span></span><span class="line"><span class="token">	                '@keywords': 'directive', 
</span></span><span class="line"><span class="token">	                '@default': 'identifier' 
</span></span><span class="line"><span class="token">				}},
</span></span><span class="line"><span class="token">            ],
</span></span><span class="line"><span class="token">        ],
</span></span><span class="line"><span class="token">    },
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>In the tokeniser above, we're once again taking the easy route. The <code>\w+</code> regex will first match every "word" in the code. The tokeniser will then look at each of those words and determine if it's one of the <code>@keywords</code> we specified earlier. If it is, we're applying the <code>directive</code> type to the token. If it's not, we'll fall back to the <code>@default</code> case and apply the <code>identifier</code> type.</p>
<p>Finally, VS Code or Monaco are able to style these keywords using the configured a color scheme. We slapped some Flare purple on the default VS Code color scheme, registered it and called it a day:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">monaco.editor.defineTheme('flare-light', {
</span></span><span class="line"><span class="token">    base: 'vs',
</span></span><span class="line"><span class="token">    inherit: true,
</span></span><span class="line"><span class="token">    rules: [{ token: 'directive', foreground: '#7900f5' }],
</span></span><span class="line"><span class="token">    colors: { foreground: '#332f51' },
</span></span><span class="line"><span class="token">});
</span></span></code></pre>
<h3>Autocompletion and code suggestions</h3>
<p>Getting suggestions from an external API to show up at the right places in the editor was probably the hardest part of this entire setup. Once again we feared having to dive into the world of tokenisers and abstract syntax trees to provide suggestions based on the current token and its context. Luckily, Monaco doesn't really care about any of that stuff and just expects us to return a <code>Array&lt;string&gt;</code> with suggestions based on the cursor position. Here's a quick example that will suggest <code>foo</code> and <code>bar</code> anywhere in the editor:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">monaco.langagues.registerCompletionItemProvider('flare-query-language', {
</span></span><span class="line"><span class="token">    provideCompletionItems: (model, position) =&gt; {
</span></span><span class="line"><span class="token">	    suggestions: ['foo', 'bar']
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">})
</span></span></code></pre>
<p>For Flare specifically, we're only able to suggest either filter keywords (e.g. <code>class</code>, <code>version</code>, ...) or filter values. The filter keyword suggestions are all a hardcoded array but the filter values need to be queried from ElasticSearch using our back-end API. We'll use the following regex to determine whether our cursor is currently in a filter keyword or in a filter value:</p>
<p><code>/\b(?&lt;filter&gt;[^\s:]+):?(?&lt;value&gt;\S+)?/g </code></p>
<p>If you don't speak regex, this just means to match any <code>key:value</code> occurrence in the query. You can also see the <code>:</code> and <code>value</code> parts are optional. This is because we also want to match the start of a new filter statement when the user is still typing. We can than then loop over all matches and compare the regex <code>match.index</code> and <code>match.length</code> to the cursor's <code>position</code> to determine if we're in a filter keyword or a filter value and make suggestions accordingly.</p>
<p>As mentioned before: if we're autocompleting a filter keyword we can simply return the a hardcoded array of filter keywords. For suggesting filter values, we need to execute and asynchronous API call. Luckily, Monaco supports returning a <code>Promise&lt;CompletionList&gt;</code> from the <code>provideCompletions</code> method. In pseudo-code, all of that looks like this:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">const filters = ['version', 'class', 'type', 'is'];
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">async function provideCompletions(code position) {
</span></span><span class="line"><span class="token"> 	// If we're autocompleting filter values, make an API request
</span></span><span class="line"><span class="token">    if (isFilterValue(code, position)) {
</span></span><span class="line"><span class="token">        const suggestions = await fetchSuggestions(query, position);
</span></span><span class="line"><span class="token">        
</span></span><span class="line"><span class="token">        return {
</span></span><span class="line"><span class="token">            suggestions,
</span></span><span class="line"><span class="token">        };
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">  	// We're autocompleting a new filter or random text, return all available filters
</span></span><span class="line"><span class="token">    return {
</span></span><span class="line"><span class="token">        suggestions: filters,
</span></span><span class="line"><span class="token">    };
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>Finally, using some black magic and a stroke of luck we also managed to add a debounce in there without upsetting React's delicate rendering cycle.</p>
<h3>Bonus round: tooltips on hover</h3>
<p>Most of Flare's filters are pretty self explanatory and we initially weren't planning to show tooltips. Despite that, we figured that we're already downloading 3MB of Monaco editor including tooltip support so we might as well use it. /s</p>
<p>Once again, Monaco makes it really easy to hook in a custom <code>hoverProvider</code>. Just like the suggestion provider, the hover provider is given the entire document and the current mouse position as if it were a cursor. This means hovering over the 5th character in the document will pass <code>5</code> for the position. It then expects us to return the contents of a tooltip, if we want to show one.</p>
<p>We can re-use the <code>filter:value</code> regex from the suggestion provider above to once again determine if we're hovering over a filter keyword or a value. We'll then provide tooltips for just the filter keywords:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">function provideHover(code, position) {
</span></span><span class="line"><span class="token">	// Use regex to extract the filter keyword or value the cursor is in:
</span></span><span class="line"><span class="token">	const currentWord = getCurrentWord(code, position);
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">	if (word.type !== 'keyword') {
</span></span><span class="line"><span class="token">		return null;
</span></span><span class="line"><span class="token">	}
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    return {
</span></span><span class="line"><span class="token">        range: new Monaco.Range(1, word.start, 1, word.end),
</span></span><span class="line"><span class="token">        contents: [
</span></span><span class="line"><span class="token">            { value: `**${word.value}**` },
</span></span><span class="line"><span class="token">            { value: keywords[word.value]?.description }
</span></span><span class="line"><span class="token">        ],
</span></span><span class="line"><span class="token">    };
</span></span><span class="line"><span class="token">},
</span></span></code></pre>
<h3>Lazy loading the input field</h3>
<p>Apart from the single-line Monaco configuration and the custom language definition, we're using a couple other tweaks to make the experience as smooth as possible. One of these tweaks is the loading indicator – or rather, lack thereof.</p>
<p>The <a href="https://github.com/suren-atoyan/monaco-react">monaco-react</a> package already show a <code>loading</code> placeholder that'll show a <code>Loading...</code> label by default. Instead of that label, we can also pass in a React component like a fully working input field styled to look exactly like the editor:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">&lt;Editor
</span></span><span class="line"><span class="token">    value={value}
</span></span><span class="line"><span class="token">    onChange={handeOnChange}
</span></span><span class="line"><span class="token">		// ...
</span></span><span class="line"><span class="token">    loading={&lt;InputField value={value} onChange={value =&gt; handleOnChange(value)} /&gt;}
</span></span><span class="line"><span class="token">/&gt;
</span></span></code></pre>
<p>The <code>onChange</code> callback can even be hooked up to capture any changes to the input field <strong>before</strong> the Monaco editor even loads. Once loaded it can then transition seamlessly into the full-blown editor.</p>
<h2>So you spent 2 months installing a graph component and copying VS Code?</h2>
<p>Yes! And we're pretty proud of it too. In the coming weeks we'll also bring this occurrence graph and the new search field to the error occurrence detail page. We're also looking at what other data we can index to provide useful search filters. Feel free to start a free 10-day trial (no credit card required) to play around with the new search features and be sure to let us know your feedback and ideas for interesting filters!</p>
]]>
            </summary>
                                    <updated>2021-09-30T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Linking to Flare errors on your error page]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/linking-to-flare-errors-on-your-error-page" />
            <id>https://flareapp.io/linking-to-flare-errors-on-your-error-page</id>
            <author>
                <name><![CDATA[Alex]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>We've launched a new feature that will make it easier for users of your app to report specific errors.</p>
<p>When an error occurs in a web request, Laravel will show this error page by default.</p>
<p><img src="https://assets.flareapp.io/production/blog/9ed408ab57b7db86831413a79f1f7933/aa204e96-9176-4b38-95cd-d66b10ad882b.png" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/9ed408ab57b7db86831413a79f1f7933/aa204e96-9176-4b38-95cd-d66b10ad882b.png" width="1826" height="1188" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/9ed408ab57b7db86831413a79f1f7933/aa204e96-9176-4b38-95cd-d66b10ad882b.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/9ed408ab57b7db86831413a79f1f7933/aa204e96-9176-4b38-95cd-d66b10ad882b.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/9ed408ab57b7db86831413a79f1f7933/aa204e96-9176-4b38-95cd-d66b10ad882b.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/9ed408ab57b7db86831413a79f1f7933/aa204e96-9176-4b38-95cd-d66b10ad882b.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>If a user sees this page and wants to report this error to you, the user usually only reports the URL and the time the error was seen.</p>
<p>To let your users pinpoint the exact error they saw, you can display the UUID of the error sent to Flare.</p>
<p>If you haven't already done so, you <a href="https://laravel.com/docs/8.x/errors#custom-http-error-pages">can publish Laravel's default error pages</a> with this command.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">php artisan vendor:publish --tag=laravel-errors
</span></span></code></pre>
<p>Typically, you would alter the <code>resources/views/errors/500.blade.php</code> to display the UUID and optionally a URL of the latest error sent to Flare.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">@extends('errors::minimal')
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">@section('title', __('Server Error'))
</span></span><span class="line"><span class="token">@section('code', '500')
</span></span><span class="line"><span class="token">@section('message')
</span></span><span class="line"><span class="token">    Server error
</span></span><span class="line"><span class="token">   &lt;div&gt;
</span></span><span class="line"><span class="token">       &lt;a href="{{ Flare::sentReports()-&gt;latestUrl() }}"&gt;
</span></span><span class="line"><span class="token">           {{ Flare::sentReports()-&gt;latestUuid() }}
</span></span><span class="line"><span class="token">       &lt;/a&gt;
</span></span><span class="line"><span class="token">   &lt;/div&gt;
</span></span><span class="line"><span class="token">@endsection
</span></span></code></pre>
<p>This is how that would look like in the browser.</p>
<p><img src="https://assets.flareapp.io/production/blog/b6f6f91250a2acc3b7e2dd5b26f96686/78688ee4-1838-47bf-a9e4-6e7ef5941181.png" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/b6f6f91250a2acc3b7e2dd5b26f96686/78688ee4-1838-47bf-a9e4-6e7ef5941181.png" width="1826" height="1188" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/b6f6f91250a2acc3b7e2dd5b26f96686/78688ee4-1838-47bf-a9e4-6e7ef5941181.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/b6f6f91250a2acc3b7e2dd5b26f96686/78688ee4-1838-47bf-a9e4-6e7ef5941181.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/b6f6f91250a2acc3b7e2dd5b26f96686/78688ee4-1838-47bf-a9e4-6e7ef5941181.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/b6f6f91250a2acc3b7e2dd5b26f96686/78688ee4-1838-47bf-a9e4-6e7ef5941181.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>That link returned by <code>Flare::sentReports()-&gt;latestUrl()</code> isn't publicly accessible, the page is only visible to Flare users that have access to the project on Flare.</p>
<p>In certain cases, multiple errors can be reported to Flare in a single request. To get a hold of the UUIDs of all sent errors, you can call <code>Flare::sentReports()-&gt;uuids()</code>. You can get links to all sent errors with <code>Flare::sentReports()-&gt;urls()</code>.</p>
<p>We hope that you like this nice new feature.</p>
]]>
            </summary>
                                    <updated>2021-09-13T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Handling customer feedback using a support bubble form]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/handling-customer-feedback-using-a-support-bubble-form" />
            <id>https://flareapp.io/handling-customer-feedback-using-a-support-bubble-form</id>
            <author>
                <name><![CDATA[Freek]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Like most pieces of software, Flare contains bugs. We try to catch those via automated/manual testing, and static analysis. But sometimes, one of those pesky bugs makes it to production where it stays unnoticed, and waiting to make a jump on an unexpecting user.</p>
<p>When this happens, we want our users to have an easy route to report such a bug. Of course we hope mosts of customers send us a mail, describing the issue to us and some users do exactly this. But we feel that it's human nature that if you want to make someone do something, you have to make it as easy as possible for them.</p>
<p>That's why many websites display a small widget - often called a chat or support bubble - where users can quickly report a bug, ask a quick question if something is not clear or even make suggestions for new features.</p>
<p>Last week, we decided to add such a support bubble to Flare as well. As an added bonus, we decided to open-source it too. That way you can add it to your own Laravel projects as well. In this blog post I'd like to tell you all about it.</p>
<h2>Using Flare's support bubble</h2>
<p>When you log into Flare, you'll see the support bubble on the bottom right of every page. Click the bubble to open the support form that can be used to submit a bug report or a suggestion. It is as simple as that.</p>
<p><img src="https://assets.flareapp.io/production/blog/17bce86a6758e6a3c396b79da62af19a/e4a7cfae-c2c2-45e0-8218-b464069f231b.gif" alt="Support bubble demo" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/17bce86a6758e6a3c396b79da62af19a/e4a7cfae-c2c2-45e0-8218-b464069f231b.gif" width="450" height="572"></p>
<p>When a form is submitted a mail is sent to our Freshdesk where one of our team members will pick it up.</p>
<h2>A package to display a support bubble</h2>
<p>Flare isn't the only place where our team at Spatie wants to use a support bubble. That's why we've extracted our solution to a reusable package called <a href="https://github.com/spatie/laravel-support-bubble">spatie/laravel-support-bubble</a>. You can make use of this package in your Laravel project too.</p>
<p>Using this package you can quickly add a chat bubble that opens a support form on any page. It comes with batteries included:</p>
<ul>
<li>
<p>TailwindCSS styling out of the box</p>
</li>
<li>
<p>Won't ask user information if there's a logged in user</p>
</li>
<li>
<p>Includes some meta data like URL and IP address</p>
</li>
<li>
<p>Easily extendable using custom views, language files and event listeners</p>
</li>
<li>
<p>Honeypot included and set-up to keep spammers at bay</p>
</li>
</ul>
<p>The package can be installed in only a couple of minutes and we've built in quite some customisation options! Want to display your bubble using another color that Flare's beautiful purple? Just change a config value. Want to change or translate some labels? Publish the localisation files and start making changes. Want to customise the looks of the form even further? You can publish the views and components and make any changes you like.</p>
<p>When a form is submitted, we'll fire an event called <code>Spatie\SupportBubble\Events\SupportBubbleSubmittedEvent</code> that contains all submitted form values, the user's information, some request context and the entire <code>Illuminate\Http\Request</code>. Out of the box, the package uses this event to send a mail notification to the mail address you specify in the config file.</p>
<p>Alternatively, you can listen for that event in your own project and react to it in any way you like. You could store the values in the database, totally ignore it (we don't recommend this) or send the values to a third party help desk service.</p>
<h2>In closing</h2>
<p>To know more about how to install the package, and customising it, head over to the readme of <a href="https://github.com/spatie/laravel-support-bubble">spatie/laravel-support-bubble</a> on GitHub.</p>
<p>In this stream on YouTube, Freek demonstrates how to use the package, and explains in great detail how it works under the hood.</p>
<p>If you see a bug on Flare, or want to make a feature request, fill in our support form. We're listening!</p>
]]>
            </summary>
                                    <updated>2021-09-02T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[How we're using static analysis to improve our codebase]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/how-were-using-static-analysis-to-improve-our-codebase" />
            <id>https://flareapp.io/how-were-using-static-analysis-to-improve-our-codebase</id>
            <author>
                <name><![CDATA[Ruben]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>The initial Flare codebase was written almost two years ago. Though that's not so long ago, but PHP landscape changed drastically in that timeframe. PHP version 7.4 and 8.0 were significant releases, and static analyzers became a popular topic in the PHP world.</p>
<p>A static analyzer helps you to find bugs in your code without even running it. Popular static analyzers for PHP are <a href="https://psalm.dev">Psalm</a> and <a href="https://phpstan.org">PHPStan</a>. In this post, we're going to look at what such a static analyzer can find in a two-year-old codebase.</p>
<p>Since Flare is a Laravel project, we decided to use <a href="https://github.com/nunomaduro/larastan">Larastan</a>, a Laravel flavoured extension of PHPStan.</p>
<h2>Installing Larastan</h2>
<p>Larastan is installed just like any other PHP dependency with Composer:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">composer require --dev nunomaduro/larastan
</span></span></code></pre>
<p>When Larastan is installed you create a <code>phpstan.neon</code> file in which you can configure PHPStan:</p>
<p>`includes:</p>
<ul>
<li>./vendor/nunomaduro/larastan/extension.neonparameters:
paths:</li>
<li>applevel: 1ignoreErrors:</li>
<li>'#Unsafe usage of new static#'checkMissingIterableValueType: false
`</li>
</ul>
<p>PHPStan has eight levels: one is the lowest level of error checking, and eight is the highest, most strict one. We're planning to raise the level in the coming months, so we know for sure that the Flare codebase is wholly checked.</p>
<p>Now you can run PHPStan like this:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">./vendor/bin/phpstan analyse
</span></span></code></pre>
<p>PHPStan will output a list of errors within your codebase like this one:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token"> ------ ----------------------------------------------------- 
</span></span><span class="line"><span class="token">  Line   Domain/Subscription/Listeners/CreateInvoice.php      
</span></span><span class="line"><span class="token"> ------ ----------------------------------------------------- 
</span></span><span class="line"><span class="token">  17     Variable $invoice in PHPDoc tag @var does not match  
</span></span><span class="line"><span class="token">         assigned variable $cashierInvoice.                   
</span></span><span class="line"><span class="token">  30     Access to an undefined property                      
</span></span><span class="line"><span class="token">         Laravel\Cashier\Invoice::$id.                        
</span></span><span class="line"><span class="token"> ------ ----------------------------------------------------- 
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token"> ------ ------------------------------------------------------ 
</span></span><span class="line"><span class="token">  Line   Domain/Subscription/Models/UsagePeriodDay.php         
</span></span><span class="line"><span class="token"> ------ ------------------------------------------------------ 
</span></span><span class="line"><span class="token">  19     Call to an undefined method                           
</span></span><span class="line"><span class="token">         App\Domain\Team\Models\Team::getApiUsageResetDate().  
</span></span><span class="line"><span class="token"> ------ ------------------------------------------------------ 
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token"> ------ ------------------------------------------------------ 
</span></span><span class="line"><span class="token">  Line   Domain/Subscription/Notifications/ConfirmPayment.php  
</span></span><span class="line"><span class="token"> ------ ------------------------------------------------------ 
</span></span><span class="line"><span class="token">  37     Access to an undefined property                       
</span></span><span class="line"><span class="token">         Laravel\Cashier\Payment::$id.                         
</span></span><span class="line"><span class="token"> ------ ------------------------------------------------------ 
</span></span></code></pre>
<h2>A look at some errors PHPStan found</h2>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">     protected function couponEligibleForBillable(
</span></span><span class="line"><span class="token">         string $coupon,
</span></span><span class="line"><span class="token">---      Team | Billable $billable,
</span></span><span class="line"><span class="token">+++      Team $billable,
</span></span><span class="line"><span class="token">         string $plan,
</span></span><span class="line"><span class="token">         array $options
</span></span><span class="line"><span class="token">     ): bool {
</span></span></code></pre>
<p>This piece of code checks if a coupon can be used for a Team, <code>Team|Billable</code> is a valid union type since we're using PHP 8. But <code>Billable</code> is a trait that cannot be type hinted as a method's parameter.</p>
<hr>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">     public function getTotalWithoutTax(): float
</span></span><span class="line"><span class="token">     {
</span></span><span class="line"><span class="token">+++      if ($this instanceof Refund) {
</span></span><span class="line"><span class="token">+++          return $this-&gt;total - $this-&gt;tax;
</span></span><span class="line"><span class="token">+++      }
</span></span><span class="line"><span class="token">+++
</span></span><span class="line"><span class="token">         return ($this-&gt;amount_due) - $this-&gt;tax;
</span></span><span class="line"><span class="token">     }
</span></span></code></pre>
<p>We have an <code>Invoice</code> and <code>Refund</code> models, both using a trait with this code to get the total amount without tax. On the <code>Invoice</code> model, we use the <code>amount_due</code> property for this calculation. The <code>Refund</code> model has no such property, so we should use the <code>total</code> property.</p>
<hr>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">--- public function didRecentlyUnsubscribe(): bool
</span></span><span class="line"><span class="token">--- {
</span></span><span class="line"><span class="token">---     if ($this-&gt;subscribed()) {
</span></span><span class="line"><span class="token">---         return false;
</span></span><span class="line"><span class="token">---     }
</span></span><span class="line"><span class="token">---  
</span></span><span class="line"><span class="token">---     if ($this-&gt;hasEverSubscribedTo()) {
</span></span><span class="line"><span class="token">---         return carbon($this-&gt;subscription()-&gt;ends_at)-&gt;isBetween(now(), now()-&gt;subDays(2));
</span></span><span class="line"><span class="token">---     }
</span></span><span class="line"><span class="token">---  
</span></span><span class="line"><span class="token">---     return carbon($this-&gt;trial_ends_at)-&gt;isBetween(now(), now()-&gt;subDays(2));
</span></span><span class="line"><span class="token">--- }
</span></span></code></pre>
<p>Recently we've updated the Laravel Spark version within Flare. The method above was being used with the previous version of Spark. The new version of Spark doesn't require it anymore, which means it could be deleted entirely.</p>
<hr>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">---      return $this-&gt;last_logged_in-&gt;addMonths(6)-&gt;isPast();
</span></span><span class="line"><span class="token">+++      return $this-&gt;last_logged_in_at-&gt;addMonths(6)-&gt;isPast();
</span></span></code></pre>
<p>This code in the <code>User</code> model checks if the user had logged in within the past six months. If not, it will mark the user to be deleted. The name <code>last_logged_in</code> was changed a few weeks ago to <code>last_logged_in_at</code> to follow the naming convention of other properties with a DateTime. We oversaw this one in our code. Luckily PHPStan found it!</p>
<hr>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">---  public function invitations()
</span></span><span class="line"><span class="token">+++  public function invitations(): HasMany
</span></span><span class="line"><span class="token">     {
</span></span><span class="line"><span class="token">         return $this-&gt;hasMany(Invitation::class);
</span></span><span class="line"><span class="token">     }
</span></span></code></pre>
<p>Some of the relations within our <code>Teams</code> model were missing some typing. Luckily this was a real quick fix.</p>
<h2>Closing thoughts</h2>
<p>The errors above were only a tiny subset of fixes we did on the Flare codebase using PHPStan. In the coming months, we will pump up the PHPStan checking level and make our codebase as bug-free as possible.</p>
<p>You can find more information about Larastan and PHPStan in its <a href="https://github.com/nunomaduro/larastan">GitHub repository</a>. It's worth running it on your codebase, and you'll probably find bugs you looked over. Flare can catch bugs that PHPStan cannot find, so you can fix them later, isn't that nice?</p>
]]>
            </summary>
                                    <updated>2021-08-16T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[How we use ElasticSearch, Kibana and Filebeat to handle our logs]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/how-we-use-elasticsearch-kibana-and-filebeat-to-handle-our-logs" />
            <id>https://flareapp.io/how-we-use-elasticsearch-kibana-and-filebeat-to-handle-our-logs</id>
            <author>
                <name><![CDATA[Ruben]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Flare runs on a few different servers and each one of them has its own purpose. We've got web servers that serve the Flare app and other public pages like this blog. Reporting servers will take dozens of error reports per second from our clients and store them for later processing. Finally, there are worker servers which will process these reports and run background tasks like sending notifications and so on.</p>
<p>Each one of these servers runs a Laravel installation that produce interesting metrics and logs. This is quite helpful when something goes wrong. The only problem is that, whenever something goes wrong, we need to manually log in to each server via SSH to check the logs.</p>
<p>In this blog post, we'll explain how we combine these logs in a single stream.</p>
<h2>Using ElasticSerach for logging</h2>
<p>There are a couple of services out there to which you can send all the logging output. They provide a UI for everything you send to them. We decided to not use these services because we already are using an ElasticSearch cluster to handle searching errors. It's rather straightforward use it too search our logging output too.</p>
<p>ElasticSearch provides an excellent web client called <a href="https://www.elastic.co/kibana/">Kibana</a>. This isn't only used to manage the ElasticSearch cluster and its contents. It can also show you logs that are sent to ElasticSearch as part of the ELK stack. It even has a real-time stream of logs.</p>
<p><img src="https://assets.flareapp.io/production/blog/76ca30a2da019eeacd1e9001406733be/f3432393-5888-40cc-b494-bd1e0ed7819b.png" alt="https://flareapp.io/images/blog/kibana.png" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/76ca30a2da019eeacd1e9001406733be/f3432393-5888-40cc-b494-bd1e0ed7819b.png" width="1913" height="962" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/76ca30a2da019eeacd1e9001406733be/f3432393-5888-40cc-b494-bd1e0ed7819b.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/76ca30a2da019eeacd1e9001406733be/f3432393-5888-40cc-b494-bd1e0ed7819b.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/76ca30a2da019eeacd1e9001406733be/f3432393-5888-40cc-b494-bd1e0ed7819b.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/76ca30a2da019eeacd1e9001406733be/f3432393-5888-40cc-b494-bd1e0ed7819b.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<h2>Improving performance using Filebeat</h2>
<p>When something is logged in our Flare API, we could immediately send that log message to ElasticSearch using the API. However, this synchronous API call would make the Flare API really slow. Every time something gets logged within Flare, we would need to send a separate request to our ElasticSearch cluster, which could happend hundreds of times per second.</p>
<p>Instead, we chose to use <a href="https://www.elastic.co/beats/filebeat">Filebeat</a>. It's a tool by ElasticSearch that runs on your servers and periodically sends log files to ElasticSearch. This happens in a separate process so it doesn't impact the Flare Laravel application. Using Filebeat, logs are getting send in bulk, and we don't have to sacrifice any resources in the Flare app, neat!</p>
<h2>Integration in Laravel</h2>
<p>Let's take a look at how this works. By default, the Laravel logging format looks like this:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">[2021-07-15 11:40:43] local.ERROR: something went wrong
</span></span></code></pre>
<p>Filebeat (and ElasticSearch's ingress) need a more structured logging format like this:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">    "@timestamp" : "2021-07-15T11:40:43.000000+00:00",
</span></span><span class="line"><span class="token">    "log" : {
</span></span><span class="line"><span class="token">        "level" : "ERROR",
</span></span><span class="line"><span class="token">        "logger" : "local"
</span></span><span class="line"><span class="token">    },
</span></span><span class="line"><span class="token">    "message" : "something went wrong"
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>As Laravel uses the monolog package for logging, we only need to create a new formatter for Filebeat that will output logs like the JSON format shown above:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">class ElasticSearchFormatter extends NormalizerFormatter
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">    public function format(array $record): string
</span></span><span class="line"><span class="token">    {
</span></span><span class="line"><span class="token">        $message = [
</span></span><span class="line"><span class="token">            '@timestamp' =&gt; $this-&gt;normalize($record['datetime']),
</span></span><span class="line"><span class="token">            'log' =&gt; [
</span></span><span class="line"><span class="token">                'level' =&gt; $record['level_name'],
</span></span><span class="line"><span class="token">                'logger' =&gt; $record['channel'],
</span></span><span class="line"><span class="token">            ],
</span></span><span class="line"><span class="token">        ];
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        if (isset($record['message'])) {
</span></span><span class="line"><span class="token">            $message['message'] = $record['message'];
</span></span><span class="line"><span class="token">        }
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        return $this-&gt;toJson($message) . "\n";
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>Now within the <code>logging.php</code> config file, we change the <code>daily</code> logger as such:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">'daily' =&gt; [
</span></span><span class="line"><span class="token">    'driver' =&gt; 'daily',
</span></span><span class="line"><span class="token">    'formatter' =&gt; App\Support\ElasticSearch\ElasticSearchFormatter::class,
</span></span><span class="line"><span class="token">    'path' =&gt; storage_path('logs/laravel.log'),
</span></span><span class="line"><span class="token">    'level' =&gt; env('LOG_LEVEL', 'debug'),
</span></span><span class="line"><span class="token">    'days' =&gt; 7,
</span></span><span class="line"><span class="token">],
</span></span></code></pre>
<p>We use the daily logger in Flare to ensure our server's hard drives are not getting overloaded with log files. This daily logger will only keep logs for the last seven days stored on the server.</p>
<p>Next, we need to <a href="https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-installation-configuration.html">install Filebeat</a> on all of our servers and configure it to send the context from the daily log files to the ElasticSearch cluster. The <code>filebeat.yml</code> config file to do so looks like this:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">filebeat:
</span></span><span class="line"><span class="token">  inputs:
</span></span><span class="line"><span class="token">  - enabled: true
</span></span><span class="line"><span class="token">    json.add_error_key: true
</span></span><span class="line"><span class="token">    json.expand_keys: true
</span></span><span class="line"><span class="token">    json.keys_under_root: true
</span></span><span class="line"><span class="token">    json.overwrite_keys: true
</span></span><span class="line"><span class="token">    paths:
</span></span><span class="line"><span class="token">    - {PATH_TO_FLARE}/storage/logs/laravel-*.log
</span></span><span class="line"><span class="token">    type: log
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">output:
</span></span><span class="line"><span class="token">  elasticsearch:
</span></span><span class="line"><span class="token">    hosts:
</span></span><span class="line"><span class="token">    - {ELASTIC_SEARCH_IP}:{ELASTIC_SEARCH_PORT}
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">logging:
</span></span><span class="line"><span class="token">  files:
</span></span><span class="line"><span class="token">    rotateeverybytes: 10485760
</span></span></code></pre>
<p>Finally, the last thing left to do is configuring Kibana to read the Filebeat logs. This can be configured from the Kibana UI by going to the settings panel in <code>Oberserveability</code> -&gt; <code>Logs</code>. Check that the log indices contain the <code>filebeat-*</code> wildcard. The indices that match this wildcard will be parsed for logs by Kibana.</p>
<p>In the log columns configuration we also added the <code>log.level</code> and <code>agent.hostname</code> columns. This way we can see how severe a log entry was and what server it originated from.</p>
]]>
            </summary>
                                    <updated>2021-08-09T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Refactoring our search capabilities using Elasticsearch and two new Spatie packages]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/refactoring-our-search-capabilities-using-elasticsearch-and-two-new-spatie-packages" />
            <id>https://flareapp.io/refactoring-our-search-capabilities-using-elasticsearch-and-two-new-spatie-packages</id>
            <author>
                <name><![CDATA[Ruben]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>At Flare, we're continously making improvements to our code base. A lot of the code has gone through multiple iterations and improvements. A part of our code base that was in need of some love was the errors &amp; occurrences search. The original code was a Frankenstein mix of a deprecated Eloquent search query package and ElasticSearch with some custom SQL sprinkled on top.</p>
<p>The past few weeks we've spent time refactoring this code. Recentely, we deployed our <code>improved-search</code> branch to production. Let's dive into the changes in this refactor and the new packages that were born from it.</p>
<h2>A re-usable strategy for search</h2>
<p>Flare features three search fields throughout the application: one for projects, one for errors and one for error occurrences. The biggest annoyance for us was that all three of these search solutions were built differently.</p>
<p>The project search is a simple <code>WHERE LIKE</code> SQL query to filter on project name. It's basically a one-liner, so we left it that way.</p>
<p>The errors and error occurences search on the other hand are both complex search fields with optional filters, grouping and fuzzy filtering on multiple fields. In true DRY (don't repeat yourself) spirit we decided to build a unified search solution for both of these using ElasticSearch.</p>
<p>Additionally, by open-sourcing this solution we've extracted a lot of the complicated search code from our codebase into a package. This isolated package code is easier to test and easier to re-use by other developers. In turn, we get valueable feedback and free bugfixes in the form of PRs.</p>
<h2>ElasticSearch using a custom query language</h2>
<p>When talking about search, it's hard to ignore ElasticSearch. Having used it before for error occurrences we quickly decided to start indexing errors in ElasticSearch as well and fully rely on it as our search driver. ElasticSearch can handle most complex search queries you can come up with, if you know how to formulate it in a JSON search request.</p>
<p>Sadly, having a search field that only takes JSON is bad UX. We still need a way to convert a query string like <code>class:QueryException status 404 group_by:user_id</code> to the JSON request that ElasticSearch can understand. This is where the <a href="https://github.com/spatie/elasticsearch-search-string-parser">spatie/elasticsearch-search-string-parser</a> package comes in handy.</p>
<h2>Introducing spatie/elasticsearch-search-string-parser</h2>
<p>Our new <a href="https://github.com/spatie/elasticsearch-search-string-parser">spatie/elasticsearch-search-string-parser</a> package can dissect a search string like the example above into different "directives" like <code>class=QueryException</code>, <code>grouping=user_id</code> and the remaining <code>status 404</code> for fuzzy filtering.</p>
<p>These directives can be dynamically defined and applied to a search string. Dissecting or parsing these directives works using regular expressions. This allows us to match basic directives like <code>/class:(.*)/</code> but also more complex syntax if we wanted too.</p>
<p>Finally, every directive also knows how to add itself to an <a href="https://github.com/spatie/elasticsearch-query-builder">ElasticSearch query builder</a>. For example, the class directive (<code>class=QueryException</code>) will add the following match query to the ElasticSearch builder:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">{ 
</span></span><span class="line"><span class="token">  query: {
</span></span><span class="line"><span class="token">	"match": {
</span></span><span class="line"><span class="token">	  "class_name": {
</span></span><span class="line"><span class="token">		"query": "QueryException"
</span></span><span class="line"><span class="token">      }
</span></span><span class="line"><span class="token">	}
</span></span><span class="line"><span class="token">  }
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<h2>Implementing search-string-parser in Flare</h2>
<p>Using the search-string-parser package, it suddenly becomes really easy to replace both search controllers in Flare with two <code>SearchQuery</code>s and a couple directives. We even managed to re-use a couple of directives as both the errors and occurrences have a <code>ClassNameDirective</code> to search by exception class and a <code>MessageDirective</code> to search by exception message.</p>
<p>Finally, the search-string-parser packages uses another Spatie package under the hood: the <a href="https://github.com/spatie/elasticsearch-query-builder">elasticsearch-query-builder</a>. It's features a fluent (Eloquent-like) API to to build ElasticSearch queries. It even comes with pagination support out of the box. This means it plugged right in to our custom paginator in Flare.</p>
<h2>Future plans</h2>
<p>Spoilers ahead! As we've now got access to the full power of ElasticSearch in both of our most important search fields, you'll see some cool search features coming up soon. One of the things we're most excited about is auto-completion on the directives in the search field. More on that soon™️!</p>
]]>
            </summary>
                                    <updated>2021-07-20T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Collecting metrics for Flare using event sourcing and laravel-stats]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/collecting-metrics-for-flare-using-event-sourcing-and-laravel-stats" />
            <id>https://flareapp.io/collecting-metrics-for-flare-using-event-sourcing-and-laravel-stats</id>
            <author>
                <name><![CDATA[Alex]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Like most SaaS companies, the Flare back-office features a neat dashboard showing some of our key metrics. Using Laravel Nova, it's pretty easy to aggregate data per model like the total number of errors or the active subscriptions per week. Behind the scenes, these dashboard tiles and graphs use simple database queries. For example, the number of active subscriptions is a simple <code>SELECT COUNT(*)</code> query on the <code>subscriptions</code> table. But how do we query new subscriptions per week?</p>
<h2>Counting new subscriptions per week</h2>
<p>Let's take a look at a possible solution for the "new subscriptions per week" metric. Using <a href="https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html#function_yearweek"><code>YEARWEEK()</code></a> we can easily count the active subscriptions grouped by week of the year. The underlying Eloquent query now looks like this:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">$numberOfActiveSubs = Subscriptions::query()
</span></span><span class="line"><span class="token">  -&gt;selectRaw('COUNT(*) as number')
</span></span><span class="line"><span class="token">  -&gt;where('status', 'active')
</span></span><span class="line"><span class="token">  -&gt;groupByRaw('YEARWEEK(created_at)')
</span></span><span class="line"><span class="token">  -&gt;get();
</span></span></code></pre>
<p>This works pretty well. With the correct indexes it's pretty fast too. However, there's one fatal flaw: once a subscription ends, it's no longer counted as a new subscription in the past weeks because it's no longer <code>active</code>. You can probably think of a couple more examples where aggregating metrics from a traditional active record database goes wrong. Here are a couple examples of our own:</p>
<ul>
<li>
<p>the number of active subscriptions per month</p>
</li>
<li>
<p>the number of deleted users</p>
</li>
<li>
<p>the total amount of errors received by a team, including errors outside of the retention period</p>
</li>
</ul>
<p>All of these metrics are based of a certain state in time that's no longer active or data that was only in the database temporarily. If we want to query these metrics at any time, we'll need to keep all relevant data and states forever. In practice, this is less daunting than it sounds. Let's look at three possible solutions (including our very own <a href="https://github.com/spatie/laravel-stats">laravel-stats</a> package) to solve this issue.</p>
<h2>Solution 1: snapshotting metrics</h2>
<p>As explained above, the query for the <em>current</em> number of active subscriptions works pretty well because it relies on the current state of the data. Using a CRON job we can query the current number of active subscriptions every day and save that in a separate <code>active_subscriptions_per_day</code> tabel. This way we're keeping a record of daily active subscriptions by snapshotting the data every day.</p>
<p>However, this method doesn't scale well if you need metrics from different time periods. If you're only storing new sign-ups per day, there's simply no way to query new sign-ups per hour. In other cases you might also miss data that happens in between two snapshots. For example an error that's created an deleted on the same day will never count as a "reported error" for that day. We cannot snapshot data if it's already deleted or modified at the end of the day.</p>
<h2>Solution 2: event sourcing everything</h2>
<p>If we simply store <em>everything</em> that happens in our application, we'll never run into a situation where there's not enough data to calculate a metric. The <a href="https://event-sourcing-laravel.com">event sourcing pattern</a> does exactly that: every change in the applications state is stored and processed as an event. Using these events we can even rebuild the application's state at any point in time and thus calculate metrics at any point in time.</p>
<p>Event sourcing is a pretty powerfull pattern but it can't easily be implemented in existing applications without some overhead. That's why we've opted for a hybrid solution: event sourced metrics without event sourcing.</p>
<h2>Solution 3: event sourced statistics using laravel-stats</h2>
<p>Using the <a href="https://github.com/spatie/laravel-stats">laravel-stats package</a> we can track changes to certain metrics based of the events that cause these changes. For the previous subscriptions example, we can easily created a <code>SubscriptionStats</code> class that can be used to track changes to subscriptions:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">use Spatie\Stats\BaseStats;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">class SubscriptionStats extends BaseStats {}
</span></span></code></pre>
<p>When a new subscription is activated, we can call <code>SubscriptionStats::increase()</code> to increase the number of active subscriptions. Likewise, when a subscription expires or gets cancelled, we can call the <code>SubscriptionStats::decrease()</code> method to update the number of active subscriptions. Behind the scenes, the package isn't just storing a fixed number of active subscription. Instead, it's storing the <code>increase</code> and <code>decrease</code> events that cause this number to go up or down. You can see this happen in the <code>stats_events</code>:</p>
<p><img src="https://assets.flareapp.io/production/blog/125ee856d52c55c9ebddd8cc6ad36af8/8459068f-0527-4651-aa92-44a231a3fbd2.png" alt="Laravel stats inner workings" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/125ee856d52c55c9ebddd8cc6ad36af8/8459068f-0527-4651-aa92-44a231a3fbd2.png" width="2038" height="1158" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/125ee856d52c55c9ebddd8cc6ad36af8/8459068f-0527-4651-aa92-44a231a3fbd2.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/125ee856d52c55c9ebddd8cc6ad36af8/8459068f-0527-4651-aa92-44a231a3fbd2.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/125ee856d52c55c9ebddd8cc6ad36af8/8459068f-0527-4651-aa92-44a231a3fbd2.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/125ee856d52c55c9ebddd8cc6ad36af8/8459068f-0527-4651-aa92-44a231a3fbd2.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>As you can see, we're also storing timestamps for these events. Using these timestamps we can aggregate events and metrics for every timeframe you can think of. For the subscriptions per week metric, this looks a little something like this:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">use Spatie\Stats\StatsQuery;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">$stats = StatsQuery::for(SubscriptionStats::class)
</span></span><span class="line"><span class="token">    -&gt;start(now()-&gt;subWeeks(2))
</span></span><span class="line"><span class="token">    -&gt;end(now()-&gt;subSecond())
</span></span><span class="line"><span class="token">    -&gt;groupByWeek()
</span></span><span class="line"><span class="token">    -&gt;get();
</span></span></code></pre>
<p>We're also specifying a <code>start</code> and <code>end</code> date. These boundaries are especially usefull when creating graphs. Finally, the returned data looks like this:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">[
</span></span><span class="line"><span class="token">    [
</span></span><span class="line"><span class="token">        'start' =&gt; '2020-01-01',
</span></span><span class="line"><span class="token">        'end' =&gt; '2020-01-08',
</span></span><span class="line"><span class="token">        'value' =&gt; 102,
</span></span><span class="line"><span class="token">        'increments' =&gt; 32,
</span></span><span class="line"><span class="token">        'decrements' =&gt; 20,
</span></span><span class="line"><span class="token">        'difference' =&gt; 12,
</span></span><span class="line"><span class="token">    ],
</span></span><span class="line"><span class="token">    [
</span></span><span class="line"><span class="token">        'start' =&gt; '2020-01-08',
</span></span><span class="line"><span class="token">        'end' =&gt; '2020-01-15',
</span></span><span class="line"><span class="token">        'value' =&gt; 114,
</span></span><span class="line"><span class="token">        'increments' =&gt; 63,
</span></span><span class="line"><span class="token">        'decrements' =&gt; 30,
</span></span><span class="line"><span class="token">        'difference' =&gt; 33,
</span></span><span class="line"><span class="token">    ],
</span></span><span class="line"><span class="token">]
</span></span></code></pre>
<p>The returned data doesn't only include the summed increments and decrements but it also keeps track of the total number of subscriptions and the difference between each group! This way you can metrics and graphs for, e.g. new teams, deleted teams and the total number of teams, all from one statistics class.</p>
<p>Intrigued? Take a look at the package's <a href="https://github.com/spatie/laravel-stats">readme on GitHub</a> or read more about it on <a href="https://freek.dev/1957-a-lightweight-laravel-package-to-track-changes-over-time">Freek's blog</a>.</p>
]]>
            </summary>
                                    <updated>2021-07-13T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Why and how we remove inactive users and teams]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/why-and-how-we-remove-inactive-users-and-teams" />
            <id>https://flareapp.io/why-and-how-we-remove-inactive-users-and-teams</id>
            <author>
                <name><![CDATA[Spatie]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Like many others SaaS applications, you can try out Flare  using a trial period. Of course, not everybody that tries out Flare will convert. After the trial period is over, some people will not use the service anymore.</p>
<p>If nothing is being done about it, email addresses and other pieces of (personal) data remain in our database. We've recently added clean up procedure to Flare, which will delete all inactive users and teams.</p>
<p>In this blog post we'd like to tell you more about it. We'll also share some actual code from our code base that performs the cleanup.</p>
<h2>Why we remove inactive users and teams</h2>
<p>There are two reasons why we want to delete old users and teams. First, we want to keep your database as small as possible. This has many small benefits. Our queries might execute faster. Our backup process will be shorter. Less disk space is needed... Granted, these things might be a micro-optimizations, as databases are very optimized in performing queries in even large datasets, and the disk space used is probably not too much. Still, the less work our server needs to do, the better.</p>
<p>The second reason is more important: we only want to keep as little personal data as needed for privacy reasons. We only collect and keep the absolute minimum of data needed to run our application. This keeps our user's privacy safe and minimizes the risks for us as a company if a security breach happens.</p>
<p>If people, a few months after a trial period, didn't subscribe, then it's unlikely that they'll subscribe to Flare. We don't need their data anymore.</p>
<h2>How we take care of deleting users and teams in Flare</h2>
<p>We didn't want the cleanup process to be a one time action but a continuous process. The process is implemented as a couple of Artisan commands which are scheduled to run every day.</p>
<h3>Deleting unverified users</h3>
<p>Let's start with the users. Before new users can use Flare, they should verify their email address. This verification is <a href="https://laravel.com/docs/master/verification#introduction">handled by Laravel</a>. There are a lot of users that never verify their email address. I'm assuming that most of them are bots, together with a couple of people that changed their mind about using our service.</p>
<p>Those unverified users have no way of using the application, so it's safe to delete them.</p>
<p>Here's the command that will delete all unverified users ten days after they have been created.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">namespace App\Domain\Team\Commands\Cleanup;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">use App\Domain\Team\Models\User;
</span></span><span class="line"><span class="token">use Illuminate\Console\Command;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">class DeleteOldUnverifiedUsers extends Command
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">    protected $signature = 'flare:delete-old-unverified-users';
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    public function handle(): void
</span></span><span class="line"><span class="token">    {
</span></span><span class="line"><span class="token">        $this-&gt;info('Deleting old unverified users...');
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        $count = User::query()
</span></span><span class="line"><span class="token">            -&gt;whereNull('email_verified_at')
</span></span><span class="line"><span class="token">            -&gt;where('created_at', '&lt;', now()-&gt;subDays(10))
</span></span><span class="line"><span class="token">            -&gt;delete();
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        $this-&gt;comment("Deleted {$count} unverified users.");
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        $this-&gt;info('All done!');
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>The most straightforward way to test the command above would be to</p>
<ul>
<li>
<p>seed a user that should be deleted,</p>
</li>
<li>
<p>seed another one that shouldn't be deleted</p>
</li>
<li>
<p>call the command above</p>
</li>
<li>
<p>verify if the command only deleted the right user</p>
</li>
</ul>
<p>Instead of that approach, I prefer scenario-based tests that more closely mimic what happens in real life. In the test below, a user is seeded, and by using <a href="https://github.com/spatie/test-time">the spatie/test-time package</a>, we modify the time.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">namespace Tests\Feature\Domain\Team\Cleanup;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">use App\Domain\Team\Commands\Cleanup\DeleteOldUnverifiedUsers;
</span></span><span class="line"><span class="token">use App\Domain\Team\Models\User;
</span></span><span class="line"><span class="token">use Spatie\TestTime\TestTime;
</span></span><span class="line"><span class="token">use Tests\TestCase;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">class DeleteOldUnverifiedUsersTest extends TestCase
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">    public function setUp(): void
</span></span><span class="line"><span class="token">    {
</span></span><span class="line"><span class="token">        parent::setUp();
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        TestTime::freeze('Y-m-d H:i:s', '2021-01-01 00:00:01');
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    /** @test */
</span></span><span class="line"><span class="token">    public function it_will_delete_unverified_users_after_some_days()
</span></span><span class="line"><span class="token">    {
</span></span><span class="line"><span class="token">        $user = User::factory()-&gt;create([
</span></span><span class="line"><span class="token">            'email_verified_at' =&gt; null,
</span></span><span class="line"><span class="token">        ]);
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        TestTime::addDays(10);
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        $this-&gt;artisan(DeleteOldUnverifiedUsers::class);
</span></span><span class="line"><span class="token">        $this-&gt;assertTrue($user-&gt;exists());
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        TestTime::addSecond();
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        $this-&gt;artisan(DeleteOldUnverifiedUsers::class);
</span></span><span class="line"><span class="token">        $this-&gt;assertFalse($user-&gt;exists());
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    /** @test */
</span></span><span class="line"><span class="token">    public function it_will_not_delete_verified_users()
</span></span><span class="line"><span class="token">    {
</span></span><span class="line"><span class="token">        $user = User::factory()-&gt;create([
</span></span><span class="line"><span class="token">            'email_verified_at' =&gt; now(),
</span></span><span class="line"><span class="token">        ]);
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        TestTime::addDays(20);
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        $this-&gt;artisan(DeleteOldUnverifiedUsers::class);
</span></span><span class="line"><span class="token">        $this-&gt;assertTrue($user-&gt;exists());
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<h3>Deleting inactive teams</h3>
<p>We currently consider a team inactive when it has never subscribed, and the trial period ended more than three months ago. Deleting inactive teams is slightly more complicated. If a team exists, that means that a verified user, the team owner, has created and configured it.</p>
<p>Because there is a chance that the team owner might want to reactivate the team at some point in the future, we don't want to delete the team immediately. Instead, we're going to mail the team owners and give them a chance to cancel the automatic deletion. If no response comes in after a week, we'll delete the team.</p>
<p>Let's take a look at the code. On our <code>teams</code> table, we added two columns.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">use Illuminate\Database\Migrations\Migration;
</span></span><span class="line"><span class="token">use Illuminate\Database\Schema\Blueprint;
</span></span><span class="line"><span class="token">use Illuminate\Support\Facades\Schema;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">class AddDeletionMarkerColumnsToTeamsTable extends Migration
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">    public function up()
</span></span><span class="line"><span class="token">    {
</span></span><span class="line"><span class="token">        Schema::table('teams', function (Blueprint $table) {
</span></span><span class="line"><span class="token">            $table-&gt;timestamp('deleting_soon_mail_sent_at')-&gt;nullable();
</span></span><span class="line"><span class="token">            $table-&gt;timestamp('automatically_delete_at')-&gt;nullable();
</span></span><span class="line"><span class="token">        });
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p><code>deleting_soon_mail_sent_at</code> will contain the datetime when we mailed the automatic deletion notice to the team. <code>automatically_delete_at</code> will contain the date on which the team is scheduled to be deleted.</p>
<p>Here's the <code>shouldBeMarkedForDeletion</code> function that was added to the <code>Team</code> model.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">public function shouldBeMarkedForDeletion(): bool
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">   if ($this-&gt;hasActiveSubscription()) {
</span></span><span class="line"><span class="token">     return false;
</span></span><span class="line"><span class="token">   }
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">   return $this-&gt;created_at-&gt;addMonths(3)-&gt;isPast();
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>Here's the command that sends out the automatic deletion notices.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">namespace App\Domain\Team\Commands\Cleanup;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">use App\Domain\Team\Actions\Cleanup\MarkInactiveTeamForDeletionSoonAction;
</span></span><span class="line"><span class="token">use App\Domain\Team\Models\Team;
</span></span><span class="line"><span class="token">use Illuminate\Console\Command;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">class MarkInactiveTeamsForDeletionCommand extends Command
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">    protected $signature = 'flare:mark-inactive-teams-for-deletion';
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    public function handle()
</span></span><span class="line"><span class="token">    {
</span></span><span class="line"><span class="token">        $this-&gt;info('Starting marking inactive teams for deletion');
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        $markedAsDeletionCount = 0;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        Team::each(function (Team $team) use (&amp;$markedAsDeletionCount) {
</span></span><span class="line"><span class="token">            if (! $team-&gt;shouldBeMarkedForDeletion()) {
</span></span><span class="line"><span class="token">                return;
</span></span><span class="line"><span class="token">            }
</span></span><span class="line"><span class="token">            $this-&gt;comment("Marking team {$team-&gt;name} ({$team-&gt;id}) for deletion");
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">            (new MarkInactiveTeamForDeletionSoonAction())-&gt;execute($team);
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">            $markedAsDeletionCount++;
</span></span><span class="line"><span class="token">        });
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        $this-&gt;info("Marked {$markedAsDeletionCount} teams for deletion!");
</span></span><span class="line"><span class="token">        $this-&gt;info('All done!');
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>Here's the code of the <code>shouldBeMarkedForDeletion</code>method  that will determine whether a team should be mailed an automatic deletion notice.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">// on the Team model
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">public function shouldBeMarkedForDeletion(): bool
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">   // if we've already sent the mail we don't want to resend it
</span></span><span class="line"><span class="token">    if ($this-&gt;deleting_soon_mail_sent_at !== null) {
</span></span><span class="line"><span class="token">        return false;
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    if ($this-&gt;hasActiveSubscription()) {
</span></span><span class="line"><span class="token">        return false;
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    if ($this-&gt;wasSubScribedAtSomePointInTime()) {
</span></span><span class="line"><span class="token">        return false;
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    return $this-&gt;created_at-&gt;addMonths(6)-&gt;isPast();
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>Here's the code of the <code>MarkInactiveTeamForDeletionSoonAction</code> used in the command. If you want to know more about Action classes in general, consider picking up <a href="https://laravel-beyond-crud.com">Laravel Beyond CRUD</a> course where this pattern is explained.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">namespace App\Domain\Team\Actions\Cleanup;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">use App\Domain\Team\Mails\TeamMarkedForDeletionMail;
</span></span><span class="line"><span class="token">use App\Domain\Team\Models\Team;
</span></span><span class="line"><span class="token">use Illuminate\Support\Facades\Mail;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">class MarkInactiveTeamForDeletionSoonAction
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">    public function execute(Team $team)
</span></span><span class="line"><span class="token">    {
</span></span><span class="line"><span class="token">        $team-&gt;update([
</span></span><span class="line"><span class="token">            'deleting_soon_mail_sent_at' =&gt; now(),
</span></span><span class="line"><span class="token">            'automatically_delete_at' =&gt; now()-&gt;addDays(7),
</span></span><span class="line"><span class="token">        ]);
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        Mail::to($team-&gt;owner-&gt;email)-&gt;queue(new TeamMarkedForDeletionMail($team));
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>Here's the mail that gets sent to inactive teams.</p>
<p><img src="https://assets.flareapp.io/production/blog/cc587b11b47641ac2272ded1a206b01a/5238593e-8baf-4566-98a3-7c799f38f765.png" alt="Mail" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/cc587b11b47641ac2272ded1a206b01a/5238593e-8baf-4566-98a3-7c799f38f765.png" width="1232" height="1282" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/cc587b11b47641ac2272ded1a206b01a/5238593e-8baf-4566-98a3-7c799f38f765.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/cc587b11b47641ac2272ded1a206b01a/5238593e-8baf-4566-98a3-7c799f38f765.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/cc587b11b47641ac2272ded1a206b01a/5238593e-8baf-4566-98a3-7c799f38f765.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1232/https://assets.flareapp.io/production/blog/cc587b11b47641ac2272ded1a206b01a/5238593e-8baf-4566-98a3-7c799f38f765.png 1232w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>To cancel the deletion, team owners should subscribe. Let's take a look at that delete cancellation code.</p>
<p>We have an event listener that executes when a team subscribes. It will set <code>deleting_soon_mail_sent_at</code> and <code>automatically_delete_at</code> to null so that the automatic deletion is effectively cancelled.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">class CancelAutomaticDeletion
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">    public function handle(SubscriptionCreated $event)
</span></span><span class="line"><span class="token">    {
</span></span><span class="line"><span class="token">        /** @var \App\Domain\Team\Models\Team $team */
</span></span><span class="line"><span class="token">        $team = $event-&gt;billable;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        $team-&gt;update([
</span></span><span class="line"><span class="token">           'deleting_soon_mail_sent_at' =&gt; null,
</span></span><span class="line"><span class="token">           'automatically_delete_at' =&gt; null,
</span></span><span class="line"><span class="token">        ]);
</span></span></code></pre>
<p>Now let's take a look at the command that does the actual deletion if team owners don't subscribe. It's pretty simple; it only has to consider the value of <code>automatically_delete_at</code> on a team.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">namespace App\Domain\Team\Commands\Cleanup;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">use App\Domain\Team\Actions\Cleanup\DeleteInactiveTeamAction;
</span></span><span class="line"><span class="token">use App\Domain\Team\Models\Team;
</span></span><span class="line"><span class="token">use Illuminate\Console\Command;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">class DeleteInactiveTeamsCommand extends Command
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">    protected $signature = 'flare:delete-inactive-teams';
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    public function handle()
</span></span><span class="line"><span class="token">    {
</span></span><span class="line"><span class="token">        $this-&gt;info('Start deleting old teams...');
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        Team::query()
</span></span><span class="line"><span class="token">            -&gt;where('automatically_delete_at', '&lt;', now())
</span></span><span class="line"><span class="token">            -&gt;each(function (Team $team) {
</span></span><span class="line"><span class="token">                $this-&gt;comment("Deleting team {$team-&gt;id}");
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">                (new DeleteInactiveTeamAction())-&gt;execute($team);
</span></span><span class="line"><span class="token">            });
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        $this-&gt;info('All done!');
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>Here's the code of the <code>DeleteInactiveTeamAction</code> class used in the command above. In the action, we'll delete the team. We'll also delete the owner if it has no other teams.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">namespace App\Domain\Team\Actions\Cleanup;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">use App\Domain\Team\Models\Team;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">class DeleteInactiveTeamAction
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">    public function execute(Team $team)
</span></span><span class="line"><span class="token">    {
</span></span><span class="line"><span class="token">        $teamOwner = $team-&gt;owner;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        $team-&gt;delete();
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        /** @var \App\Domain\Team\Models\User $teamOwner */
</span></span><span class="line"><span class="token">        $teamOwner = $teamOwner-&gt;refresh();
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        if ($teamOwner-&gt;allTeams()-&gt;count() === 0) {
</span></span><span class="line"><span class="token">            $teamOwner-&gt;delete();
</span></span><span class="line"><span class="token">        }
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>Let's now test that all the code above is working as intended. Again, we're going to use the <a href="https://github.com/spatie/test-time">TestTime</a> class to create a "scenario" test where we move forward in time.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">namespace Tests\Feature\Domain\Team\Cleanup;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">use App\Domain\Team\Commands\Cleanup\DeleteInactiveTeamsCommand;
</span></span><span class="line"><span class="token">use App\Domain\Team\Commands\Cleanup\MarkInactiveTeamsForDeletionCommand;
</span></span><span class="line"><span class="token">use App\Domain\Team\Mails\TeamMarkedForDeletionMail;
</span></span><span class="line"><span class="token">use App\Domain\Team\Models\Team;
</span></span><span class="line"><span class="token">use Illuminate\Support\Facades\Mail;
</span></span><span class="line"><span class="token">use Spatie\TestTime\TestTime;
</span></span><span class="line"><span class="token">use Tests\Factories\TeamFactory;
</span></span><span class="line"><span class="token">use Tests\TestCase;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">class AutomaticTeamDeletionTest extends TestCase
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">    public function setUp(): void
</span></span><span class="line"><span class="token">    {
</span></span><span class="line"><span class="token">        parent::setUp();
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        TestTime::freeze('Y-m-d H:i:s', '2020-01-01 00:00:00');
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        Mail::fake();
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    /** @test */
</span></span><span class="line"><span class="token">    public function it_will_delete_a_team_when_it_has_never_subscribed_and_is_older_than_6_months()
</span></span><span class="line"><span class="token">    {
</span></span><span class="line"><span class="token">        /** @var \App\Domain\Team\Models\Team $teamWithoutSubscription */
</span></span><span class="line"><span class="token">        $teamWithoutSubscription = Team::factory()-&gt;create();
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        TestTime::addMonths(6);
</span></span><span class="line"><span class="token">        $this-&gt;artisan(MarkInactiveTeamsForDeletionCommand::class);
</span></span><span class="line"><span class="token">        $this-&gt;assertFalse($teamWithoutSubscription-&gt;markedForDeletion());
</span></span><span class="line"><span class="token">        Mail::assertNothingQueued();
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        TestTime::addSecond();
</span></span><span class="line"><span class="token">        $this-&gt;artisan(MarkInactiveTeamsForDeletionCommand::class);
</span></span><span class="line"><span class="token">        $this-&gt;assertTrue($teamWithoutSubscription-&gt;refresh()-&gt;markedForDeletion());
</span></span><span class="line"><span class="token">        Mail::assertQueued(TeamMarkedForDeletionMail::class);
</span></span><span class="line"><span class="token">        $this-&gt;assertEquals(now()-&gt;addDays(7), $teamWithoutSubscription-&gt;refresh()-&gt;automatically_delete_at);
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        TestTime::addDays(7);
</span></span><span class="line"><span class="token">        $this-&gt;artisan(DeleteInactiveTeamsCommand::class);
</span></span><span class="line"><span class="token">        $this-&gt;assertTrue($teamWithoutSubscription-&gt;exists());
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        TestTime::addSecond();
</span></span><span class="line"><span class="token">        $this-&gt;artisan(DeleteInactiveTeamsCommand::class);
</span></span><span class="line"><span class="token">        $this-&gt;assertFalse($teamWithoutSubscription-&gt;exists());
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    /** @test */
</span></span><span class="line"><span class="token">    public function it_will_not_mark_teams_that_have_subscription_for_deletion()
</span></span><span class="line"><span class="token">    {
</span></span><span class="line"><span class="token">        $teamWithSubscription = TeamFactory::createSubscribedTeam();
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        TestTime::addMonths(6)-&gt;addSecond();
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        $this-&gt;artisan(MarkInactiveTeamsForDeletionCommand::class);
</span></span><span class="line"><span class="token">        $this-&gt;assertFalse($teamWithSubscription-&gt;refresh()-&gt;markedForDeletion());
</span></span><span class="line"><span class="token">        Mail::assertNothingQueued();
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        TestTime::addDays(7)-&gt;addSecond();
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        $this-&gt;artisan(DeleteInactiveTeamsCommand::class);
</span></span><span class="line"><span class="token">        $this-&gt;assertTrue($teamWithSubscription-&gt;exists());
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<h2>In closing</h2>
<p>We hope you like this little tour of how we delete inactive users and teams. If you work on a similar application, We highly encourage you to make sure the old user data is being deleted.</p>
]]>
            </summary>
                                    <updated>2021-06-14T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Meet the new projects overview with error trends and favourites]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/meet-the-new-projects-overview-with-error-trends-and-favourites" />
            <id>https://flareapp.io/meet-the-new-projects-overview-with-error-trends-and-favourites</id>
            <author>
                <name><![CDATA[Rias]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Up until a few days ago our projects overview  only showed you the name of the project, some basic error counts and when the latest error came.</p>
<p>A few days ago we launched a vastly improved version of the project overview. You'll immediately notice that for each project we now display the trend of incoming error occurrences.</p>
<p>Here's what it looks like (screenshot taken from our own account, I've blurred out client projects).</p>
<p><img src="https://assets.flareapp.io/production/blog/d58cdf30005b70bfae824404da9f7129/043bf0c3-59ab-448b-86d9-769cb905ad98.png" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/d58cdf30005b70bfae824404da9f7129/043bf0c3-59ab-448b-86d9-769cb905ad98.png" width="2564" height="2006" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/d58cdf30005b70bfae824404da9f7129/043bf0c3-59ab-448b-86d9-769cb905ad98.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/d58cdf30005b70bfae824404da9f7129/043bf0c3-59ab-448b-86d9-769cb905ad98.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/d58cdf30005b70bfae824404da9f7129/043bf0c3-59ab-448b-86d9-769cb905ad98.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/d58cdf30005b70bfae824404da9f7129/043bf0c3-59ab-448b-86d9-769cb905ad98.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>Using the graph you can quickly see which projects are currently sending more errors than usual.</p>
<p>A second new feature we've introduced is the ability to mark a project as favourite. You can do that by clicking the little star icon at the end of a row in the list. When you favourite a project, it will appear at the top of the list in a separate card.</p>
<p>Here's what that looks like:</p>
<p><img src="https://assets.flareapp.io/production/blog/ec6490b229256d774d7b8ff57ed40664/5c43aa98-c6b4-439c-88e1-23b6e11721b1.png" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/ec6490b229256d774d7b8ff57ed40664/5c43aa98-c6b4-439c-88e1-23b6e11721b1.png" width="2528" height="1812" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/ec6490b229256d774d7b8ff57ed40664/5c43aa98-c6b4-439c-88e1-23b6e11721b1.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/ec6490b229256d774d7b8ff57ed40664/5c43aa98-c6b4-439c-88e1-23b6e11721b1.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/ec6490b229256d774d7b8ff57ed40664/5c43aa98-c6b4-439c-88e1-23b6e11721b1.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/ec6490b229256d774d7b8ff57ed40664/5c43aa98-c6b4-439c-88e1-23b6e11721b1.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>Of course, you can favourite as many projects as you like.</p>
<p>You can see other improvements we recently made <a href="https://flareapp.io/changelog">on our changelog</a>. Do you have an idea to improve Flare? <a href="https://github.com/spatie/flareapp.io-roadmap">Let us know</a>!</p>
]]>
            </summary>
                                    <updated>2021-06-07T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[How our GitHub integration works under the hood]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/how-our-github-integration-works-under-the-hood" />
            <id>https://flareapp.io/how-our-github-integration-works-under-the-hood</id>
            <author>
                <name><![CDATA[Ruben]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>A few weeks ago, we introduced <a href="https://flareapp.io/blog/23-introducing-our-github-integration">a new integration with GitHub</a> that makes it possible to:</p>
<ul>
<li>
<p>create a GitHub issue directly on a Flare error</p>
</li>
<li>
<p>associate a GitHub issue with a Flare error by mentioning a Flare URL in the GitHub issue</p>
</li>
<li>
<p>automatically resolve an error on Flare when you close the GitHub issue</p>
</li>
<li>
<p>automatically close a GitHub issue when you resolve an error in Flare</p>
</li>
</ul>
<p>In the stream below, our developers <a href="https://twitter.com/rubenvassche">Ruben</a> and <a href="https://twitter.com/freekmurze">Freek</a> will show you how you can use this integration and how it works under the hood.</p>
]]>
            </summary>
                                    <updated>2021-05-10T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Mail notifications now allow to snooze and resolve errors]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/mail-notifications-now-allow-to-snooze-and-resolve-errors" />
            <id>https://flareapp.io/mail-notifications-now-allow-to-snooze-and-resolve-errors</id>
            <author>
                <name><![CDATA[Freek]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>When an error occurs, Flare can notify you via <a href="https://flareapp.io/docs/notifications/configuring-notifications">many different notification channels</a>. Some of these notifications destinations have rich APIs that allow us to add actions to snooze and resolve errors right on the notification. Here's how a Slack notification looks like; notice the action buttons below.</p>
<p><img src="https://assets.flareapp.io/production/blog/bda3a03c4bc2158d6ea430dd34560dfa/1169e565-2bd3-4630-a4a3-c2bffc2489f9.png" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/bda3a03c4bc2158d6ea430dd34560dfa/1169e565-2bd3-4630-a4a3-c2bffc2489f9.png" width="1344" height="1066" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/bda3a03c4bc2158d6ea430dd34560dfa/1169e565-2bd3-4630-a4a3-c2bffc2489f9.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/bda3a03c4bc2158d6ea430dd34560dfa/1169e565-2bd3-4630-a4a3-c2bffc2489f9.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/bda3a03c4bc2158d6ea430dd34560dfa/1169e565-2bd3-4630-a4a3-c2bffc2489f9.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1344/https://assets.flareapp.io/production/blog/bda3a03c4bc2158d6ea430dd34560dfa/1169e565-2bd3-4630-a4a3-c2bffc2489f9.png 1344w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>When we look at which notification channels our users use, we see that mail is the most popular channel. This motivated us to provide snoozing and resolving abilities to mail notifications. We deployed our changes a couple of days ago. In all mail notifications, you'll now see new snooze and resolve links.</p>
<p><img src="https://assets.flareapp.io/production/blog/72a25e5685226dd99178a9016055b282/7b00ef97-bba0-4d9d-9729-8760e8b093e4.png" alt="screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/72a25e5685226dd99178a9016055b282/7b00ef97-bba0-4d9d-9729-8760e8b093e4.png" width="1312" height="1844" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/72a25e5685226dd99178a9016055b282/7b00ef97-bba0-4d9d-9729-8760e8b093e4.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/72a25e5685226dd99178a9016055b282/7b00ef97-bba0-4d9d-9729-8760e8b093e4.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/72a25e5685226dd99178a9016055b282/7b00ef97-bba0-4d9d-9729-8760e8b093e4.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1312/https://assets.flareapp.io/production/blog/72a25e5685226dd99178a9016055b282/7b00ef97-bba0-4d9d-9729-8760e8b093e4.png 1312w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>These links will work even if you are not signed in at Flare. This functionality is implemented securely. These action links only have a limited lifetime. Behind the scenes, the links leverage Laravel's <a href="https://opendor.me/@freekmurze">https://laravel.com/docs/master/urls#signed-urls</a>, so they cannot be tampered with.</p>
<p>We think these action links provide a convenient way to snooze/resolve errors quickly. You can see other improvements we recently made <a href="https://flareapp.io/changelog">on our changelog</a>. Do you have an idea to improve Flare? <a href="https://github.com/spatie/flareapp.io-roadmap">Let us know</a>!</p>
]]>
            </summary>
                                    <updated>2021-04-19T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Snooze notifications per application version]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/snooze-notifications-per-application-version" />
            <id>https://flareapp.io/snooze-notifications-per-application-version</id>
            <author>
                <name><![CDATA[Freek]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>In addition to snoozing notifications for a number of occurrences or a fixed period, we have introduced a new way of snoozing errors. You can now snooze errors per application version. In this blog post, you'll learn all about it.</p>
<h2>Snoozing options in Flare</h2>
<p>Flare has powerful options to notify you when something goes wrong in your app. We provide notifications via email, Slack, SMS (through Nexmo), Discord, Microsoft Teams and webhooks.</p>
<p>For each of these channels, you can choose which notifications should be sent through them. These are the events we can notify you of</p>
<ul>
<li>
<p>when an error occurs for the very first time</p>
</li>
<li>
<p>when an error occurs for the 1st, 10th, 100th, 1000th, 2000th, ... time</p>
</li>
<li>
<p>when an error that was marked as resolved, occurs again</p>
</li>
<li>
<p>...</p>
</li>
</ul>
<h2>Snoozing errors per application version</h2>
<p>If you are aware of an error in an application, you might temporary not want to receive notifications concerning a particular error anymore. You do want to get notified again after you've deployed a fix for the error.</p>
<p>You can achieve this by sending the version number of your app whenever an error occurs. In Laravel projects, you can do this by calling <code>determineVersionUsing</code> on the <code>Flare</code> facade.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">// in a service provider
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">use Facade\Ignition\Facades\Flare;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">Flare::determineVersionUsing(function() {
</span></span><span class="line"><span class="token">   return '1.0'; // return your version number here.
</span></span><span class="line"><span class="token">});
</span></span></code></pre>
<p>With Flare being aware of your application version, you can now snooze any errors sent to Flare for a particular version.</p>
<p><img src="https://assets.flareapp.io/production/blog/b6fc7d14a3731c730bfc4c3c9cbc020b/553d6ff0-63ac-4ad8-8533-5b3f3215cee3.png" alt="Snooze version" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/b6fc7d14a3731c730bfc4c3c9cbc020b/553d6ff0-63ac-4ad8-8533-5b3f3215cee3.png" width="2534" height="642" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/b6fc7d14a3731c730bfc4c3c9cbc020b/553d6ff0-63ac-4ad8-8533-5b3f3215cee3.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/b6fc7d14a3731c730bfc4c3c9cbc020b/553d6ff0-63ac-4ad8-8533-5b3f3215cee3.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/b6fc7d14a3731c730bfc4c3c9cbc020b/553d6ff0-63ac-4ad8-8533-5b3f3215cee3.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/b6fc7d14a3731c730bfc4c3c9cbc020b/553d6ff0-63ac-4ad8-8533-5b3f3215cee3.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>You will only get notified when the error comes in with another version number (probably after you've redeployed your app).</p>
<h2>Automatically determining a version number</h2>
<p>If you don't set a version number explicitly using <code>determineVersionUsing</code>, we can use a short commit hash if your application is served via a git repo.</p>
<p>For this to work, you'll need to set the <code>reporting.collect_git_information</code> to value <code>true</code> in the <code>flare</code> config file, so git information is sent to Flare.</p>
]]>
            </summary>
                                    <updated>2021-04-05T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Improving Ignition's security]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/improving-ignitions-security" />
            <id>https://flareapp.io/improving-ignitions-security</id>
            <author>
                <name><![CDATA[Alex]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>A few days ago, you might have received a Dependabot security warning on Ignition. This warning concerns <a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-3129">CVE-2021-3129</a>, which warns that <a href="https://github.com/facade/ignition">Ignition</a>, Laravel's default error page, allows unauthenticated, remote attackers to execute arbitrary code.</p>
<p><img src="https://assets.flareapp.io/production/blog/15a7cd4f6c73e0dc3b7f3ab1e11112a9/ed5e8394-b6c6-4d88-8627-7dd4ec20f2db.png" alt="Dependabot warning" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/15a7cd4f6c73e0dc3b7f3ab1e11112a9/ed5e8394-b6c6-4d88-8627-7dd4ec20f2db.png" width="1082" height="810" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/15a7cd4f6c73e0dc3b7f3ab1e11112a9/ed5e8394-b6c6-4d88-8627-7dd4ec20f2db.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/15a7cd4f6c73e0dc3b7f3ab1e11112a9/ed5e8394-b6c6-4d88-8627-7dd4ec20f2db.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/15a7cd4f6c73e0dc3b7f3ab1e11112a9/ed5e8394-b6c6-4d88-8627-7dd4ec20f2db.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1082/https://assets.flareapp.io/production/blog/15a7cd4f6c73e0dc3b7f3ab1e11112a9/ed5e8394-b6c6-4d88-8627-7dd4ec20f2db.png 1082w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>In this blog post, we'd like to explain why that security warning isn't an issue for most and how we further improved Ignition's security.</p>
<h2>This exploit only works when debug mode is turned on</h2>
<p>When looking at the description of <a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-3129">CVE-2021-3129</a>, you might think that Ignition has a severe security problem. What is not mentioned in the description is that this exploit only worked when debug mode was turned on. The underlying RCE in Ignition has since been fixed (<a href="https://github.com/facade/ignition/pull/334">PR #334</a> and version 2.5.2) and additional security measures were taken to protect people from unknowingly exposing their Laravel apps due to misconfiguration.</p>
<p>Ignition has the ability to <a href="https://flareapp.io/docs/ignition/solutions/runnable-solutions">run executable solutions</a>. These solutions can make your life better by running migrations when you forgot to run them, generating an <code>APP_KEY</code> if you set none, fixing variable names in your code, ... These runnable solutions are only available when Laravel is in <a href="https://laravel.com/docs/8.x/configuration#debug-mode">debug mode</a>.</p>
<p><strong>We highly recommend never to turn on debug mode in a non-local environment. If you do so, then you risk exposing sensitive information and potentially allow outsiders to execute solutions.</strong></p>
<p>To know how Ignition could be exploited when debug mode is on, take a look at <a href="https://www.ambionics.io/blog/laravel-debug-rce">this research report by Ambionics security</a>.</p>
<p>We want to stress that you should never turn on debug mode in a non-local environment.</p>
<h2>How we have improved Ignition in response to this warning</h2>
<p>We think the vast majority of Laravel developers are aware that they should never enable debug mode on a non-local site.</p>
<p>To warn those who don't, Ignition will now display a warning when an error is rendered with debug mode in a non-local environment.</p>
<p><img src="https://assets.flareapp.io/production/blog/010659b5be0a7d91133899768c10cffd/96e44584-1a8e-4ded-86f5-c90d310975fe.png" alt="Ignition warning screenshot" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/010659b5be0a7d91133899768c10cffd/96e44584-1a8e-4ded-86f5-c90d310975fe.png" width="2862" height="1634" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/010659b5be0a7d91133899768c10cffd/96e44584-1a8e-4ded-86f5-c90d310975fe.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/010659b5be0a7d91133899768c10cffd/96e44584-1a8e-4ded-86f5-c90d310975fe.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1024/https://assets.flareapp.io/production/blog/010659b5be0a7d91133899768c10cffd/96e44584-1a8e-4ded-86f5-c90d310975fe.png 1024w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1536/https://assets.flareapp.io/production/blog/010659b5be0a7d91133899768c10cffd/96e44584-1a8e-4ded-86f5-c90d310975fe.png 1536w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>We think that most people do not ever want to run solutions in a production environment.  To protect Ignition users even further, we've downright removed the ability to run solutions in non-local environments and from non-local IP addresses. You can see the changes in <a href="https://github.com/facade/ignition/pull/364">this PR</a>, which is included in <a href="https://github.com/facade/ignition/blob/master/CHANGELOG.md#261---2021-03-30">v2.6.1</a>.</p>
<p>Finally, we've added <a href="https://flareapp.io/docs/ignition/introducing-ignition/security-recommendations">a page on security</a> to the Ignition docs that mention our recommendations.</p>
]]>
            </summary>
                                    <updated>2021-03-31T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Installing Blackfire on Laravel Vapor]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/installing-blackfire-on-laravel-vapor" />
            <id>https://flareapp.io/installing-blackfire-on-laravel-vapor</id>
            <author>
                <name><![CDATA[Freek]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Over at Flare we're dealing with a lot of traffic on just one endpoint backed by AWS Lambda. Our base approach here is "store first, process later" (later meaning just seconds later on a async queue). However, <a href="https://aws.amazon.com/blogs/aws/new-for-aws-lambda-1ms-billing-granularity-adds-cost-savings/">now more than ever</a> it's important to keep the reporting endpoint performant.</p>
<p>There are a couple of interesting PHP profilers out there that can help us to to so. Xdebug is probably one of the most wide-spread solutions but for Flare we chose to use Blackfire as it features an easy solution to get production profiling data without being too disruptive.</p>
<p>In this blogpost we'll take a quick look at how Blackfire is set-up and how Lambda layers works. In the second part of this post we'll add the two together to get production profiling data from Lambda into Blackfire.</p>
<h2>Blackfire basics</h2>
<p>If you've ever installed Blackfire on a more traditional server (or even locally) you'll know that there are two important moving parts:</p>
<ol>
<li>
<p>the Blackfire PHP extension or "PHP probe"</p>
</li>
<li>
<p>and the Blackfire agent.</p>
</li>
</ol>
<p>I like to think of the agent as a proxy service to Blackfire's APIs. It reads profiling data from a unix socket or TCP address, does some processing and then sends the data off to our Blackfire account using the configured server ID and token.</p>
<p>The PHP probe is nothing more than a PHP extension. It will read profiling data from the PHP process and send it to the Blackfire agent. That's why its most important configuration option is the <code>BLACKFIRE_AGENT_SOCKET</code> value. This configures how the PHP probe can communicate with the Blackfire agent. This is important because the agent doesn't necessarily run on the same machine as the PHP probe. This will come in handy for getting Blackfire to work on Lambda.</p>
<h2>Intro to Lambda layers</h2>
<p>It's like Shrek once said: "Lambdas are like onions. Onions have layers. Lambdas have layers". Every layer adds some additional code, libraries or runtimes on top of your Lambda's source code.</p>
<p>Compared to a more traditional Lambda that uses the built-in NodeJS runtime, PHP isn't one of the <a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html">default Lambda runtimes</a>. That's why every Laravel Vapor Lambda deploys with a PHP layer that contains the necessary binaries and configuration options to execute your PHP code.</p>
<p>These layers may sound like magic binary files containing some low-level unix voodoo but they're actually just ZIP archives containing files. If you unzip the PHP layer you should see some familiar looking directories containing a PHP binary and various extensions and CLI tools.</p>
<p>Lambda layers are typically used to add extensions to the base runtime. The <code>vapor.yml</code> file can be configured to include up to 4 layers in the application's deployment. A typical use-case for adding layers to Vapor's Lambdas is to <a href="https://blog.laravel.com/vapor-adding-imagick-as-a-separate-lambda-layer">install extensions like Imagick</a>. We'll use it to install the Blackfire probe PHP extension.</p>
<h2>Putting one and two together</h2>
<p>Now we've got the theory out of the way, here's a breakdown of the steps we'll take to install Blackfire on Laravel Vapor:</p>
<ol>
<li>
<p>Get a Blackfire account (this one's obvious)</p>
</li>
<li>
<p>Install the Blackfire agent on a separate EC2 instance</p>
</li>
<li>
<p>Build the Blackfire probe as a Lambda layer</p>
</li>
<li>
<p>Add the Lambda layer to the app's deployment configuration</p>
</li>
<li>
<p>Configure the Blackfire probe layer</p>
</li>
<li>
<p>...Profit (or debug)</p>
</li>
</ol>
<h3>1. Installing the Blackfire agent</h3>
<p>If you skipped the intro to Blackfire you might not know that Blackfire needs more than just a PHP extension to run. It also needs the Blackfire agent to function. Sadly, we can't simply install the agent on a Lambda function. We'll need a separate EC2 instance in the same security group to host the agent.</p>
<p>Luckily, if you're using Laravel Vapor, you might already have a "jump box" set-up to use as a SSH proxy into various services. This jump box is nothing more than a pre-configured EC2 instance that's already set-up to communicate with other services your app might need, including AWS Lambda. This means it's perfect for hosting the Blackfire agent. If you don't already have a jump box, you can create one using the <code>vapor jump</code> command.</p>
<p>For installing the Blackfire agent on this EC2 instance you can follow <a href="https://blackfire.io/docs/up-and-running/installation?action=install&amp;mode=full&amp;location=server&amp;os=debian&amp;language=php">the instructions provided by Blackfire</a>. They go over adding a package repository, installing and configuring the agent and finally starting it. You can skip the part about "Installing the PHP probe" as we'll take a different approach for Lambda.</p>
<p>The next step is to configure the agent to listen to a TCP address instead of the default unix socket. This allows us to connect to the agent from outside of the local machine. You can do this by editing the <code>/etc/default/blackfire-agent</code> config file using <code>nano</code> or <code>vim</code> and changing the <code>SOURCE</code> value to <code>"tcp://0.0.0.0:8307"</code>.</p>
<p>Don't forget to restart the agent afterwards using the following command:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">sudo /etc/init.d/blackfire-agent restart
</span></span></code></pre>
<p>Finally we need to configure the EC2 instance to accept incoming connections from any IP on port <code>8307</code>. This can be changed in the section on "inbound rules" for the EC2 instance's security group
(EC2 instance configuration &gt; security tab &gt; clicking through to the security group &gt; edit inbound rules). The resulting rules should include something like this:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">|-Type-------|-Protocol-|-Port-|-Source----|
</span></span><span class="line"><span class="token">|------------|----------|------|-----------|
</span></span><span class="line"><span class="token">| Custom TCP | TCP      | 8307 | 0.0.0.0/0 |
</span></span><span class="line"><span class="token">| Custom TCP | TCP      | 8307 | ::/0      |
</span></span></code></pre>
<h3>2. Building the Blackfire probe Lambda layer</h3>
<p>Neither Blackfire nor Laravel provide a pre-built Blackfire probe extension layer for Vapor. This means we'll have to build our own. Luckily the people behind Bref (another serverless PHP framework) made this rather easy for us.</p>
<p>Start by cloning the <a href="https://github.com/brefphp/extra-php-extensions">brefphp/extra-php-extensions</a> repository from GitHub. Make sure you have Docker running locally and execute the following command to build the Blackfire layer for PHP 7.4:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">make docker-images layer=blackfire php_version=7.4
</span></span></code></pre>
<p>After a couple of minutes you can thank the Bref team and take the <code>layer-blackfire-php-74.zip</code> file from the export directory.</p>
<h3>3. Deploying a Vapor app with the Blackfire probe layer</h3>
<p>To start using our freshly built layer, we'll have to upload it to AWS first. You can do this in the AWS Lambda service section under "Layers". Make sure to give it a descriptive name like <code>blackfire-php-74</code> and add the layer's ZIP archive we built in the previous step. You don't need to select any runtimes, description or license info.</p>
<p>Once AWS finishes processing your new layer you can copy its "Version ARN". This is the identifier we'll have to use in your application's <code>vapor.yml</code> file. Following the Vapor docs we can add our new layer after the default PHP 7.4 layer. Your <code>vapor.yml</code> file should look a little something like this:</p>
<p>`id: 1337
name: flareapp
environments:
production:
runtime: php-7.4
layers:</p>
<ul>
<li>vapor:php-7.4</li>
<li>arn:aws:lambda:eu-central-1:683058659124:layer:blackfire-php74:1</li>
</ul>
<h1>rest of the file...</h1>
<p>`</p>
<p>As you can see, we also included Vapor's own PHP as this is still required to run your codebase.</p>
<h3>4. Configuring the Blackfire probe extension</h3>
<p>At this point you should have the Blackfire agent running on an EC2 instance and the Blackfire probe layer built and added to your Vapor app. The final step is to configure the Blackfire probe extension to connect to the agent on that separate EC2 instance.</p>
<p>Vapor's PHP layer will check the <code>php/conf.d/</code> directory in your app's directory for any additional PHP config files. You can add the following Blackfire config file in <code>php/conf.d/blackfire.ini</code> to configure the probe extension to contact the agent on that external server:</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">extension=/opt/bref-extra/blackfire.so
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">; Replace the following value with your agent's EC2 domain address
</span></span><span class="line"><span class="token">blackfire.agent_socket=tcp://ip-10-0-0-61.eu-central-1.compute.internal:8307
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">blackfire.agent_timeout=0.25
</span></span></code></pre>
<p>Finally, that's everything. You're one deploy removed from profiling your production Vapor app! Check out the <a href="https://blackfire.io/docs/profiling-cookbooks/index">official docs on profiling your application</a> to get started doing some actual work.</p>
<h2>Conclusion</h2>
<p>Laravel Vapor could probably do with an official Blackfire layer and some documentation on how to configure it. Luckily for you we've already done some of the research to make your job easier. We can also make your job easier by reporting and solving errors in your Lambda app for you: <a href="https://flareapp.io/features">take a look at Flare's features</a>!</p>
]]>
            </summary>
                                    <updated>2020-12-17T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[How to safely delete records in massive tables on AWS using Laravel]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/how-to-safely-delete-records-in-massive-tables-on-aws-using-laravel" />
            <id>https://flareapp.io/how-to-safely-delete-records-in-massive-tables-on-aws-using-laravel</id>
            <author>
                <name><![CDATA[Freek]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>When reviewing the contents of the database of <a href="https://flareapp.io">Flare</a>, we encountered a table with 1 billion records. Technically that isn't a problem. Flare runs on Vapor and uses an Aurora database, so it can handle that scale. But of course, there's a cost for storing that many records.</p>
<p>We dove in and concluded that we could safely delete about 900 million records. They all were created before a specific date.</p>
<h2>Attempt 1: running a single delete query</h2>
<p>A first naive approach would be to run a simple delete query.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">DELETE FROM table_with_too_many_records 
</span></span><span class="line"><span class="token">WHERE created_at &lt; '2020 -01-01 00:00:00'
</span></span></code></pre>
<p>Performing a delete query like this on a small table will probably work. But when your table contains a billion items, it will likely cause problems.</p>
<p>In most cases, a database will try to perform all queries atomically, so no other queries see intermediary results. When a deletion query using a <code>where</code> clause starts, it will lock that table. All other queries that are run against the table will have to wait.</p>
<p>Because of the massive size of the table, the deletion query would probably run for hours. The lock would be in place for hours, essentially halting the entire app.</p>
<p>So, just running a single query to clean up the table isn't a good idea.</p>
<h2>Attempt 2: using a limit</h2>
<p>To make the query complete faster, you can add a <code>limit</code> clause. Because the query completes faster, the table lock will be released much faster as well. Other queries wont be halted as long.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">DELETE FROM table_with_too_many_records 
</span></span><span class="line"><span class="token">WHERE created_at &lt; '2020 -01-01 00:00:00` 
</span></span><span class="line"><span class="token">LIMIT 1000;
</span></span></code></pre>
<p>Here's an artisan command that performs that query.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">namespace App\Console\Commands;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">use App\Domain\Error\Models\ErrorOccurrence;
</span></span><span class="line"><span class="token">use Illuminate\Console\Command;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">class CleanTableCommand extends Command
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">    protected $signature = 'clean-table';
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    protected $description = 'Clean up the table';
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    public function handle()
</span></span><span class="line"><span class="token">    {
</span></span><span class="line"><span class="token">        $this-&gt;info('Cleaning table...')
</span></span><span class="line"><span class="token">    
</span></span><span class="line"><span class="token">        MyModel::query()
</span></span><span class="line"><span class="token">            -&gt;where('created_at', '&lt;', now()-&gt;subMonth())
</span></span><span class="line"><span class="token">            -&gt;limit(1000)
</span></span><span class="line"><span class="token">            -&gt;delete();
</span></span><span class="line"><span class="token">            
</span></span><span class="line"><span class="token">        $this-&gt;info('All done!');
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>In the code above, only the 1000 first selected are deleted. Our tables contain many more records that should be deleted.</p>
<p>A deletion query in Laravel returns the number of deleted records. We can use that to determine whether if it's worthwhile to rerun the deletion query.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">public function handle()
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">    $this-&gt;info('Cleaning table...')
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    do {
</span></span><span class="line"><span class="token">        $numberOfRecordsDeleted = MyModel::query()
</span></span><span class="line"><span class="token">            -&gt;where('created_at', '&lt;', now()-&gt;subMonth())
</span></span><span class="line"><span class="token">	        -&gt;limit(1000)
</span></span><span class="line"><span class="token">	        -&gt;delete();
</span></span><span class="line"><span class="token">	    
</span></span><span class="line"><span class="token">        $this-&gt;comment("Deleted {$numberOfRecordsDeleted} records");
</span></span><span class="line"><span class="token">    } while($numberOfRecordsDeleted);
</span></span><span class="line"><span class="token">        
</span></span><span class="line"><span class="token">    $this-&gt;info('All done!');
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>This strategy works quite well on traditional systems where there's no limit on how long a process can run. If you use a conventional server instead of AWS Lambda and want to use the cleanup strategy above, look at <a href="https://github.com/spatie/laravel-model-cleanup">our model cleanup package</a>.</p>
<p>The problem with this strategy on AWS Lambda is that there is a hard execution time limit of 15 minutes. When cleaning up a huge table, the process would likely take multiple hours, so doing the cleanup in a single artisan command isn't possible.</p>
<h2>Attempt 3: using jobs</h2>
<p>To overcome that execution time limit of 15 minutes, the deletion logic can be moved to a job. The job can dispatch itself to delete more records.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">namespace App\Jobs;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">use Illuminate\Bus\Queueable;
</span></span><span class="line"><span class="token">use Illuminate\Queue\SerializesModels;
</span></span><span class="line"><span class="token">use Illuminate\Queue\InteractsWithQueue;
</span></span><span class="line"><span class="token">use Illuminate\Contracts\Queue\ShouldQueue;
</span></span><span class="line"><span class="token">use Illuminate\Foundation\Bus\Dispatchable;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">class CleanTableJob implements ShouldQueue
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    public function handle()
</span></span><span class="line"><span class="token">    {
</span></span><span class="line"><span class="token">        $numberOfRecordsDeleted = MyModel::query()
</span></span><span class="line"><span class="token">            -&gt;where('created_at', '&lt;', now()-&gt;subMonth())
</span></span><span class="line"><span class="token">            -&gt;limit(1000)
</span></span><span class="line"><span class="token">            -&gt;delete();
</span></span><span class="line"><span class="token">        
</span></span><span class="line"><span class="token">        if ($numberOfRecordsDeleted &gt; 0) {
</span></span><span class="line"><span class="token">            self::dispatch(static::class);
</span></span><span class="line"><span class="token">        }
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>This strategy works great, the query executes fast, and the deletion process can keep running as long as needed.</p>
<h2>Scheduling the cleanup process</h2>
<p>In Laravel's console kernel, you can, in addition to artisan commands, schedule jobs.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">// in app/Console/Kernel.php
</span></span><span class="line"><span class="token">protected function schedule(Schedule $schedule)
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">   // other scheduled tasks
</span></span><span class="line"><span class="token">   $schedule-&gt;job(\App\Jobs\CleanTableJob::class)-&gt;hourly();
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>If <code>CleanTableJob</code> implements <code>ShouldQueue</code> (and in our case it does), the scheduler will dispatch the job on the queue, and it will not stall next entries in the schedule.</p>
<h2>Preventing overlaps</h2>
<p>There is one more caveat. In the example above, the <code>CleanTableJob</code> is scheduled to be dispatched hourly. The job will re-dispatch itself if more records need to be deleted.</p>
<p>Now imagine that the <code>CleanTableJob</code> will re-dispatch itself for more than an hour. While there is still a <code>CleanTableJob</code> running, the <code>CleanTableCommand</code> will dispatch another <code>CleanTableJob</code>. That will cause multiple <code>CleanTableJob</code> jobs to run at the same time.</p>
<p>In our specific situation, where we are cleaning up a table with hundreds of millions of records, it likely takes hours or even days to clean up the table. If left unchecked, there would be a great many <code>CleanTableJob</code> running. Ideally, we want one <code>CleanTableJob</code> running at any given time.</p>
<p>We can solve this problem by using an atomic lock. Laravel has support for this <a href="https://laravel.com/docs/master/cache#atomic-locks">out of the box</a>. Here's how using the lock could look like.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">// in `CleanTableJob`
</span></span><span class="line"><span class="token">public function handle()
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">    $lock = Cache::store('redis')-&gt;lock(DeleteOldErrorsJob::class.'_lock', 10 * 60)
</span></span><span class="line"><span class="token">		
</span></span><span class="line"><span class="token">    if (! $lock-&gt;get()) {
</span></span><span class="line"><span class="token">        // couldn't acquire lock, other job is probably running
</span></span><span class="line"><span class="token">        return;
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">    $numberOfRecordsDeleted = MyModel::query()
</span></span><span class="line"><span class="token">        -&gt;where('created_at', '&lt;', now()-&gt;subMonth())
</span></span><span class="line"><span class="token">        -&gt;limit(1000)
</span></span><span class="line"><span class="token">        -&gt;delete();
</span></span><span class="line"><span class="token">    
</span></span><span class="line"><span class="token">    // release to lock, so a next job can get it
</span></span><span class="line"><span class="token">    $lock-&gt;release();
</span></span><span class="line"><span class="token">    
</span></span><span class="line"><span class="token">    if ($numberOfRecordsDeleted &gt; 0) {
</span></span><span class="line"><span class="token">        self::dispatch(static::class);
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>This code looks good, but it can be improved. That locking code makes it hard to see the essence of the job, which is deleting rows.</p>
<p>The locking code can be moved to <a href="https://laravel.com/docs/8.x/queues#job-middleware">a job middleware</a>.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">namespace App\Jobs\Middleware;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">use Illuminate\Support\Facades\Cache;
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">class AtomicJobMiddleware
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">    /**
</span></span><span class="line"><span class="token">     * Process the queued job.
</span></span><span class="line"><span class="token">     *
</span></span><span class="line"><span class="token">     * @param  \Illuminate\Contracts\Queue\Job  $job
</span></span><span class="line"><span class="token">     * @param  callable  $next
</span></span><span class="line"><span class="token">     */
</span></span><span class="line"><span class="token">    public function handle($job, $next)
</span></span><span class="line"><span class="token">    {
</span></span><span class="line"><span class="token">        /** @var \Illuminate\Cache\RedisLock $lock */
</span></span><span class="line"><span class="token">        $lock = Cache::store('redis')-&gt;lock("{$job-&gt;resolveName()}_lock", 10 * 60);
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        if (! $lock-&gt;get()) {
</span></span><span class="line"><span class="token">            $job-&gt;delete();
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">            return;
</span></span><span class="line"><span class="token">        }
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        $next($job);
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">        $lock-&gt;release();
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>Here's the rewritten <code>CleanTableJob</code> that makes use of the <code>AtomicJobMiddleware</code>.</p>
<pre class="phiki nord" style="background-color: #2e3440;color: #d8dee9;"><code><span class="line"><span class="token">// in `CleanTableJob`
</span></span><span class="line"><span class="token">public function handle()
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">    $numberOfRecordsDeleted = MyModel::query()
</span></span><span class="line"><span class="token">        -&gt;where('created_at', '&lt;', now()-&gt;subMonth())
</span></span><span class="line"><span class="token">        -&gt;limit(1000)
</span></span><span class="line"><span class="token">        -&gt;delete();
</span></span><span class="line"><span class="token">    
</span></span><span class="line"><span class="token">    if ($numberOfRecordsDeleted &gt; 0) {
</span></span><span class="line"><span class="token">        self::dispatch(static::class);
</span></span><span class="line"><span class="token">    }
</span></span><span class="line"><span class="token">}
</span></span><span class="line"><span class="token">
</span></span><span class="line"><span class="token">public function middleware()
</span></span><span class="line"><span class="token">{
</span></span><span class="line"><span class="token">    return [new \App\Jobs\Middleware\AtomicJobMiddleware::class];
</span></span><span class="line"><span class="token">}
</span></span></code></pre>
<p>And with all this in place, we now have a pretty reliable way of cleaning up big tables.</p>
<h2>In closing</h2>
<p>Cleaning big tables isn't that difficult, but many caveats need to be taken care of. I hope this post helps you in cleaning up your tables.</p>
<p>If you're using an Aurora cluster on AWS, you should be aware that deleting a lot of records won't make your bill smaller as space used by the deleted records <a href="https://aws.amazon.com/premiumsupport/knowledge-center/view-storage-aurora-cluster/">will still be allocated</a>.  Here's <a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Aurora.Overview.StorageReliability.html#aurora-storage-data-billing">how to reclaim the allocated space</a>.</p>
<p>If you're not on AWS and want to clean up big tables in a safe way, take a look at <a href="https://github.com/spatie/laravel-model-cleanup">our model clean up package</a>.</p>
<p>We are currenlty optimizing <a href="https://flareapp.io">Flare</a> some more, and will soon release a first batch of new features. Stay tuned for that!</p>
]]>
            </summary>
                                    <updated>2020-09-21T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Introducing JavaScript error tracking]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/introducing-javascript-error-tracking" />
            <id>https://flareapp.io/introducing-javascript-error-tracking</id>
            <author>
                <name><![CDATA[Sebastian]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>This week, we are bringing delightful JavaScript error tracking to Flare.</p>
<p>The integration comes with libraries for vanilla JavaScript, Vue, and React. You can upload your JavaScript source maps so that Flare can un-minify your compiled JavaScript files and point you directly to the error.</p>
<p>Watch the introduction video, where we showcase some of the features:</p>
<h2>Everything in one place</h2>
<p>Flare groups all your tracked Laravel and Javascript errors in one project and makes it easy to understand what is going wrong. If you have a growing team and want to send notifications to frontend or backend developers separately, create additional projects in your Flare account. All plans come with an unlimited amount of projects.</p>
<p><img src="https://beyondco.de/flare/js_1.png" alt="JavaScript Error Tracking Screenshot" loading="lazy" decoding="async" data-original-src="https://beyondco.de/flare/js_1.png"></p>
<p>With this release, Flare supports error tracking beyond Laravel and plain PHP applications and makes it easy for you to monitor your full stack from backend to frontend code.</p>
<p>If you are not sure if Flare is the right fit for you, start a free trial and see how things are going.</p>
]]>
            </summary>
                                    <updated>2020-01-28T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Creating business opportunities with error tracking]]></title>
            <link rel="alternate" href="https://flareapp.io/blog/creating-business-opportunities-with-error-tracking" />
            <id>https://flareapp.io/creating-business-opportunities-with-error-tracking</id>
            <author>
                <name><![CDATA[Spatie]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>When running production applications for your clients, it can take days until a bug is reported to your team. This reporting-delay might not be critical to the business of your client if it is a low traffic site, but it might be a direct financial loss if it’s a high-traffic application. If the site doesn’t have a high amount of traffic, it’s still a bad experience for the user, and it will have a negative impact on the business in the long-term–a negative impact that can be avoided easily.</p>
<p>This is the point where an error tracker comes into place. The error tracker takes care if something breaks, and you instantly get a notification. With this notification, you receive much additional information about the incident so that tracking down the issue becomes much more comfortable than relying on screenshots provided by a user who contacts the customer support of your client. With the error tracker, you stay in the loop and be aware of the problem before the client calls and raises the issue. In Flare, the notification also includes the user who got the error, other useful context information, and if it is a common issue–a solution how to fix it.</p>
<p>When working with clients, they usually require some service-level-agreement when an application goes live. The easiest way to handle this on a basic level is by adding a monitoring service like Flare to the application and dedicate some weekly time to handle errors. This formalizes responsibilities but also creates business opportunities along the way. While the SLA covers the costs for the error tracker multiple times, you create new touch-points with your clients. These touch-points can be used to offer new services or to stay connected with your contacts.</p>
<h2>This is how a happy client looks like</h2>
<p><img src="https://assets.flareapp.io/production/blog/5159689d9436f28d08cf99c76369b351/e440e9f3-148c-4ce2-b16d-b086cb385963.png" alt="No Errors" loading="lazy" decoding="async" data-original-src="https://assets.flareapp.io/production/blog/5159689d9436f28d08cf99c76369b351/e440e9f3-148c-4ce2-b16d-b086cb385963.png" width="1000" height="226" srcset="https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=480/https://assets.flareapp.io/production/blog/5159689d9436f28d08cf99c76369b351/e440e9f3-148c-4ce2-b16d-b086cb385963.png 480w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=768/https://assets.flareapp.io/production/blog/5159689d9436f28d08cf99c76369b351/e440e9f3-148c-4ce2-b16d-b086cb385963.png 768w, https://assets.flareapp.io/cdn-cgi/image/format=auto,fit=contain,width=1000/https://assets.flareapp.io/production/blog/5159689d9436f28d08cf99c76369b351/e440e9f3-148c-4ce2-b16d-b086cb385963.png 1000w" sizes="(max-width: 832px) calc(100vw - 64px), 768px"></p>
<p>Staying in touch via errors might sound like an odd way for a new opportunity. Still, if you can solve an issue before the client event notices the problem themselves and notify them that you’ve already handled it, they know that they are in good hands.</p>
<p>Being recognized as a partner that cares, usually brings in a steady stream for project requests and makes it easier for you to run your business.</p>
<p>With Flare, you can track unlimited projects, so if the first SLA already pays the bill, Flare is free for all other applications, and you can add your internal applications at no additional cost.</p>
]]>
            </summary>
                                    <updated>2019-11-18T00:00:00+00:00</updated>
        </entry>
    </feed>
