<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Proworkbech Blog]]></title><description><![CDATA[Proworkbech Blog]]></description><link>https://proworkbench.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/699f6844c9015c37f6c1b0c3/8ea13c1d-69cb-4413-9848-37d3293dd410.png</url><title>Proworkbech Blog</title><link>https://proworkbench.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Fri, 11 Sep 2026 14:44:46 GMT</lastBuildDate><atom:link href="https://proworkbench.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Why I Shipped a Linux Desktop App as an AppImage (and Skipped Snap/Flatpak)]]></title><description><![CDATA[Linux packaging is where good desktop apps go to die. If you’ve shipped a Linux desktop application, you already know the pain: you can build something solid, fast, and stable… and then lose days (or ]]></description><link>https://proworkbench.hashnode.dev/why-i-shipped-a-linux-desktop-app-as-an-appimage-and-skipped-snap-flatpak</link><guid isPermaLink="true">https://proworkbench.hashnode.dev/why-i-shipped-a-linux-desktop-app-as-an-appimage-and-skipped-snap-flatpak</guid><dc:creator><![CDATA[Jamie Folsom]]></dc:creator><pubDate>Sat, 21 Mar 2026 21:15:03 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/699f6844c9015c37f6c1b0c3/14bb4e36-24b9-433c-85f4-86ef633aee4b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Linux packaging is where good desktop apps go to die. If you’ve shipped a Linux desktop application, you already know the pain: you can build something solid, fast, and stable… and then lose days (or weeks) fighting packaging quirks across distros, runtimes, sandboxes, portals, GLIBC mismatches, and “works on my machine” dependency landmines. I’m building OpenChat for Linux, a lightweight ChatGPT desktop client built with Tauri (Rust). Early on, I decided to focus on one distribution format that would actually let Linux users install it easily without creating a maintenance nightmare for me: AppImage. This post is the reasoning behind that decision, what went wrong with other formats, and what I learned. The goal: one build that works across distros When you’re distributing a desktop app to Linux users, you’re really targeting: different distros (Ubuntu, Fedora, Arch, etc.) different package managers different libc/runtime realities different desktop environments and portal behaviors different expectations about sandboxing and filesystem access For a small team (or one person), the biggest risk is not that you can’t build packages—it’s that you spend more time keeping packages alive than improving the app. So the goal became: Make installation stupid-simple across distros and minimize maintenance. Why AppImage “just works” (most of the time) AppImage has a few properties that make it extremely pragmatic: No distro-specific repo required Runs on most distros with minimal friction One artifact to ship (great for fast iteration) Users can download, mark executable, and run Updates are straightforward: ship a new file (and optionally use AppImageUpdate later) In practice, AppImage is the least “ecosystem heavy” option. It doesn’t ask you to become an expert in every distro’s packaging norms. For my use case—getting a stable Linux desktop client into people’s hands quickly—this mattered more than ideological purity. Why Snap became a time sink for me Snap isn’t “bad,” but it can become expensive in time. The big issues I ran into were runtime and sandbox friction—things like: portal behaviors sandboxing assumptions that don’t match how your app needs to access resources dependency/runtime differences that are hard to predict the “it launches, but key features fail” class of bugs Even when you get it packaged, you can burn hours chasing down problems that don’t exist in your core app at all—they only exist in the packaging environment. At some point you have to ask: Is this helping users, or just dragging development velocity down? For me, Snap was pulling too much time away from improving the product. Why Flatpak failed for a more “boring” reason: GLIBC mismatch Flatpak has a lot going for it: consistent runtimes, sandboxing, and a strong ecosystem. But the reality is: if your build target and runtime don’t line up perfectly, you can hit hard failures. In my case, I hit a GLIBC mismatch issue (e.g., runtime missing a required GLIBC version). That’s the kind of problem that isn’t solved by “try harder”—it’s solved by rebuilding under the correct runtime constraints and aligning everything. If you’re a larger project with time to invest, that’s totally doable. But if your priority is shipping and reducing maintenance, it’s easy for Flatpak to turn into an engineering project on its own. So I paused Flatpak and kept the focus where it had the best ROI. A practical distribution strategy that actually scales Here’s the strategy I landed on: AppImage as the primary distribution Provide .deb / tarball for users who prefer them Only invest in Snap/Flatpak again when: there’s clear demand the ecosystem value outweighs the maintenance the build pipeline is ready to support it sustainably That’s not anti-Snap or anti-Flatpak. It’s pro-shipping. Bonus: performance matters as much as packaging Packaging isn’t the only Linux pain point. A lot of chat clients (and some desktop wrappers) can bog down during long sessions. OpenChat uses a technique I call message windowing: keep an active slice of the chat rendered load older messages only as you scroll avoid long-thread memory growth and UI slowdown It’s a simple idea that makes a big difference: long conversations stay responsive instead of turning into a laggy mess. What I’m hoping to learn from the Linux community If you’ve shipped desktop apps on Linux, I’d genuinely like feedback on: whether AppImage is still the “lowest friction” path for most users where Snap/Flatpak provide real value today (not just in theory) what install experience you personally prefer what breaks most often for you across distros Shipping on Linux is worth it—but the packaging ecosystem can punish you if you try to do everything at once. Link (if you want to check it out) OpenChat for Linux: <a href="https://snippetsupply.com/product/openchat-for-linux-openai-chat-by-snippetsupply-com-2?utm%5C_source=lemmy&amp;utm%5C_medium=community&amp;utm%5C_campaign=openchat%5C_launch&amp;utm%5C_content=appimage%5C_article">https://snippetsupply.com/product/openchat-for-linux-openai-chat-by-snippetsupply-com-2?utm\_source=lemmy&amp;utm\_medium=community&amp;utm\_campaign=openchat\_launch&amp;utm\_content=appimage\_article</a></p>
]]></content:encoded></item><item><title><![CDATA[Building a Hotjar-Class Behavior Analytics Platform in PHP (Heatmaps, Replay, Funnels, Forms, Surveys, A/B Tests)]]></title><description><![CDATA[Most analytics tools tell you what happened.
Behavior analytics tells you why:

where users clicked and hesitated

how far they scrolled

where they rage-clicked

where forms broke down

where funnels]]></description><link>https://proworkbench.hashnode.dev/Building-a-Hotjar-Class Behavior-Analytics-Platform-in-PHP-Heatmaps-Replay-Funnels-Forms-Surveys-A-B Tests</link><guid isPermaLink="true">https://proworkbench.hashnode.dev/Building-a-Hotjar-Class Behavior-Analytics-Platform-in-PHP-Heatmaps-Replay-Funnels-Forms-Surveys-A-B Tests</guid><category><![CDATA[PHP]]></category><category><![CDATA[PHP7]]></category><category><![CDATA[PHP development]]></category><category><![CDATA[php frameworks]]></category><category><![CDATA[MySQL]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[SaaS]]></category><category><![CDATA[saas development ]]></category><category><![CDATA[webdev]]></category><category><![CDATA[webdevelopment]]></category><dc:creator><![CDATA[Jamie Folsom]]></dc:creator><pubDate>Thu, 05 Mar 2026 09:32:52 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/699f6844c9015c37f6c1b0c3/7946f9b5-8aab-4259-a3da-37460647fa56.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Most analytics tools tell you <strong>what happened</strong>.</p>
<p>Behavior analytics tells you <strong>why</strong>:</p>
<ul>
<li><p>where users clicked and hesitated</p>
</li>
<li><p>how far they scrolled</p>
</li>
<li><p>where they rage-clicked</p>
</li>
<li><p>where forms broke down</p>
</li>
<li><p>where funnels leaked</p>
</li>
<li><p>what users <em>said</em> (surveys/feedback) alongside what they <em>did</em></p>
</li>
</ul>
<p>I built <strong>Spyglass360</strong>, a full Hotjar-class behavior analytics + CRO platform, using:</p>
<ul>
<li><p><strong>PHP</strong></p>
</li>
<li><p><strong>MySQL</strong></p>
</li>
<li><p><strong>Vanilla JavaScript</strong></p>
</li>
<li><p><strong>Bootstrap</strong></p>
</li>
<li><p><strong>Square</strong> for subscriptions</p>
</li>
</ul>
<p>This post breaks down the engineering architecture: event design, storage strategy, and the “sharp edges” you hit when you ship a real tracker.</p>
<hr />
<h2>1) The install contract: one snippet, stable endpoint</h2>
<p>If install isn’t simple, you lose users instantly.</p>
<p>Your tracker needs one snippet and a <strong>stable public URL</strong> (do not expose internal asset paths like <code>/assets/js/...</code> in customer-facing snippets).</p>
<pre><code class="language-plaintext">&lt;!-- Spyglass360 Tracker --&gt;
&lt;script&gt;
(function(){
  var s = document.createElement("script");
  s.src = "https://spyglass360.com/track.js?api=YOUR_API_KEY";
  s.async = true;
  document.head.appendChild(s);
})();
&lt;/script&gt;
&lt;!-- End Spyglass360 Tracker --&gt;
</code></pre>
<p>What is this?</p>
<p><strong>Rule:</strong> <code>/track.js</code> should remain stable forever—even if you refactor your internal structure later.</p>
<hr />
<h2>2) What the tracker collects (minimum data, maximum value)</h2>
<p>The easiest way to lose trust in this space is to collect “too much” (PII risk) or “the wrong kind” (not actionable).</p>
<p>A practical base set of events:</p>
<ul>
<li><p><code>pageview</code> (url, referrer, viewport)</p>
</li>
<li><p><code>click</code> (normalized x/y + small element hint)</p>
</li>
<li><p><code>scroll</code> (depth %, viewport)</p>
</li>
<li><p><code>form_focus</code>, <code>form_blur</code>, <code>form_submit</code> (never raw values)</p>
</li>
<li><p><code>custom_event</code> (for funnels/goals)</p>
</li>
<li><p>session/visitor identifiers (to correlate journeys)</p>
</li>
</ul>
<p>Example payload (simplified):</p>
<pre><code class="language-plaintext">{
  "apiKey": "…",
  "siteId": 123,
  "visitorId": "v_abc",
  "sessionId": "s_xyz",
  "type": "click",
  "url": "https://example.com/pricing",
  "ts": 1710000000000,
  "viewport": { "w": 1440, "h": 900 },
  "event": { "x": 0.62, "y": 0.41, "selector": "button#start" }
}
</code></pre>
<h3>Privacy principle (non-negotiable)</h3>
<p>For forms, you log <strong>interaction</strong>, not secrets:</p>
<ul>
<li><p>field focus/blur</p>
</li>
<li><p>time spent</p>
</li>
<li><p>submit attempts</p>
</li>
<li><p>optional “validation error happened” flags</p>
</li>
</ul>
<p>Not the typed values.</p>
<hr />
<h2>3) PHP isn’t the bottleneck — query shape is</h2>
<p>The “hard” part of analytics isn’t receiving requests. It’s turning a flood of events into fast queries.</p>
<p>So the backend pattern is:</p>
<ol>
<li><p>accept event batch</p>
</li>
<li><p>validate + normalize</p>
</li>
<li><p>write raw events append-only</p>
</li>
<li><p>maintain aggregated tables for dashboards</p>
</li>
</ol>
<p><strong>Raw events</strong> give you replay and deep drill-down.  </p>
<p><strong>Aggregates</strong> give you fast dashboards.</p>
<hr />
<h2>4) MySQL storage strategy: raw + aggregates</h2>
<p>A sane starting point in MySQL:</p>
<h3>Raw events table (append-only)</h3>
<ul>
<li><p><code>site_id</code></p>
</li>
<li><p><code>session_id</code></p>
</li>
<li><p><code>visitor_id</code></p>
</li>
<li><p><code>event_type</code></p>
</li>
<li><p><code>url</code></p>
</li>
<li><p><code>ts</code></p>
</li>
<li><p><code>payload_json</code></p>
</li>
</ul>
<h3>Session index table</h3>
<ul>
<li><p>session start/end</p>
</li>
<li><p>landing page</p>
</li>
<li><p>referrer</p>
</li>
<li><p>device info (lightweight)</p>
</li>
<li><p>geo coarse (optional)</p>
</li>
<li><p>UTM fields (if you collect them)</p>
</li>
</ul>
<h3>Aggregates</h3>
<ul>
<li><p>daily totals per site</p>
</li>
<li><p>per-page click bins</p>
</li>
<li><p>per-page scroll depth bins</p>
</li>
<li><p>funnel step completions</p>
</li>
<li><p>form field drop-off</p>
</li>
</ul>
<p>This avoids the “every page load runs a full scan of raw JSON blobs” trap.</p>
<hr />
<h2>5) Heatmaps: normalize → bin → aggregate → render</h2>
<p>Heatmaps are basically “count points and draw an overlay,” but normalization is the difference between “useful” and “garbage.”</p>
<p>Store click coordinates as <strong>percentages</strong>:</p>
<ul>
<li><p><code>x_norm = x / viewport_width</code></p>
</li>
<li><p><code>y_norm = y / viewport_height</code></p>
</li>
</ul>
<p>Then aggregate into bins (example: 100×100 grid).  </p>
<p>Rendering becomes resolution-independent.</p>
<p>This is why heatmaps can be a “cheap win”: once the event model is correct, the rest is math + UI.</p>
<hr />
<h2>6) Session replay: it’s not video</h2>
<p>Replay looks like video, but it’s actually:</p>
<ul>
<li><p>an initial snapshot</p>
</li>
<li><p>incremental events (scroll/click/mutations)</p>
</li>
<li><p>a player that reconstructs the timeline</p>
</li>
</ul>
<p>An MVP replay can be staged:</p>
<p><strong>Stage 1:</strong> click trail + scroll + navigation timeline  </p>
<p><strong>Stage 2:</strong> DOM snapshot + simple masking  </p>
<p><strong>Stage 3:</strong> mutations + higher fidelity playback</p>
<p>The core challenge is balancing:</p>
<ul>
<li><p>fidelity vs storage</p>
</li>
<li><p>masking vs usefulness</p>
</li>
<li><p>performance vs detail</p>
</li>
</ul>
<hr />
<h2>7) Funnels: the “this makes money” feature</h2>
<p>Heatmaps impress. Funnels sell.</p>
<p>Funnels answer:</p>
<ul>
<li><p>where people drop off</p>
</li>
<li><p>how long steps take</p>
</li>
<li><p>which traffic sources convert</p>
</li>
</ul>
<p>Implementation idea:</p>
<ul>
<li><p>define funnel steps by URL patterns and/or custom events</p>
</li>
<li><p>compute completion by <strong>session correlation</strong> (not just counts)</p>
</li>
<li><p>aggregate drop-offs and time-to-step</p>
</li>
</ul>
<p>If you get funnels right, you can ship value even before replay is perfect.</p>
<hr />
<h2>8) Form analytics: friction without creepiness</h2>
<p>Forms are where conversions die.</p>
<p>Track:</p>
<ul>
<li><p>focus/blur per field</p>
</li>
<li><p>time in field</p>
</li>
<li><p>abandon point</p>
</li>
<li><p>submit attempts</p>
</li>
<li><p>optional “error happened” flags</p>
</li>
</ul>
<p>Then show:</p>
<ul>
<li><p>“This field causes 60% abandonment”</p>
</li>
<li><p>“Users spend 12s here and quit”</p>
</li>
<li><p>“Mobile users fail on step 2”</p>
</li>
</ul>
<p>That’s actionable.</p>
<hr />
<h2>9) Feedback widgets + surveys: the “why layer”</h2>
<p>Behavior analytics tells you what. Feedback tells you why.</p>
<p>Once you combine:</p>
<ul>
<li><p>funnels + dropoffs  </p>
<p>with:</p>
</li>
<li><p>micro surveys at the right moments</p>
</li>
</ul>
<p>…you get the full loop:  </p>
<p><strong>observe → ask → fix → verify</strong>.</p>
<p>The key is targeting (rules):</p>
<ul>
<li><p>pages</p>
</li>
<li><p>behaviors</p>
</li>
<li><p>segments</p>
</li>
<li><p>exit intent (carefully)</p>
</li>
<li><p>post-conversion survey</p>
</li>
</ul>
<hr />
<h2>10) A/B testing is easier when analytics is unified</h2>
<p>Once you already track:</p>
<ul>
<li><p>visitors/sessions</p>
</li>
<li><p>goal events</p>
</li>
<li><p>segments</p>
</li>
</ul>
<p>Experiments become manageable:</p>
<ul>
<li><p>assign variant consistently per visitor</p>
</li>
<li><p>track goals</p>
</li>
<li><p>analyze results by segment</p>
</li>
</ul>
<p>This is where “all-in-one” becomes a real advantage: your experiments don’t live in a separate tool.</p>
<hr />
<h2>11) Billing gotcha: Square plan variations</h2>
<p>One real launch-day landmine:</p>
<p>Square subscriptions require the <strong>SUBSCRIPTION_PLAN_VARIATION</strong> ID for enrollment, not just the plan ID.</p>
<p>If you pass a plan ID where a variation ID is required, you get an error like:</p>
<blockquote>
<p>object does not have type SUBSCRIPTION_PLAN_VARIATION</p>
</blockquote>
<p>Fix pattern:</p>
<ul>
<li><p>when creating a plan in admin, create the plan <strong>and</strong> its variation</p>
</li>
<li><p>store both IDs in MySQL</p>
</li>
<li><p>enroll subscriptions using the variation ID</p>
</li>
</ul>
<hr />
<h2>12) Another gotcha: never grant access before payment success</h2>
<p>The classic SaaS mistake:</p>
<ul>
<li><p>user picks paid plan</p>
</li>
<li><p>you create account and log them in immediately</p>
</li>
<li><p>payment fails or is abandoned</p>
</li>
<li><p>they still get access</p>
</li>
</ul>
<p>Fix it with a state machine:</p>
<ul>
<li><p><code>pending_payment</code></p>
</li>
<li><p><code>active</code></p>
</li>
<li><p><code>failed/canceled</code></p>
</li>
</ul>
<p>Gate your dashboard/API by <code>active</code> (except for legit free plans).</p>
<hr />
<h2>13) What I’d do differently if starting again</h2>
<ol>
<li><p>Make <code>/track.js</code> the stable endpoint from day one</p>
</li>
<li><p>Treat privacy/masking as a <strong>first-class UX feature</strong></p>
</li>
<li><p>Build funnels + forms early (they pay the bills)</p>
</li>
<li><p>Add an install verification screen (“we’re receiving events”)</p>
</li>
<li><p>Make replay incremental so you can ship value early</p>
</li>
</ol>
<hr />
<h2>Closing</h2>
<p>Building behavior analytics isn’t about the tracker alone. It’s about:</p>
<ul>
<li><p>event design</p>
</li>
<li><p>storage strategy</p>
</li>
<li><p>privacy discipline</p>
</li>
<li><p>turning data into actions</p>
</li>
</ul>
<p>If you’ve built anything in replay/analytics/CRO tooling, I’d love to hear:</p>
<p><strong>What’s the hardest part for you?</strong>  </p>
<p>Replay fidelity? masking? storage scale? funnels that don’t lie?</p>
]]></content:encoded></item></channel></rss>