<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jura</title>
    <description>The latest articles on DEV Community by Jura (@__c500e8ac9bc2).</description>
    <link>https://dev.to/__c500e8ac9bc2</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1728609%2F0a2482f3-5a07-49de-b6f7-d76f4624fa51.jpg</url>
      <title>DEV Community: Jura</title>
      <link>https://dev.to/__c500e8ac9bc2</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/__c500e8ac9bc2"/>
    <language>en</language>
    <item>
      <title>We put a real, live dashboard on our homepage with no signup wall. Here's how it survives strangers.</title>
      <dc:creator>Jura</dc:creator>
      <pubDate>Tue, 01 Sep 2026 11:51:25 +0000</pubDate>
      <link>https://dev.to/__c500e8ac9bc2/we-put-a-real-live-dashboard-on-our-homepage-with-no-signup-wall-heres-how-it-survives-strangers-1ghe</link>
      <guid>https://dev.to/__c500e8ac9bc2/we-put-a-real-live-dashboard-on-our-homepage-with-no-signup-wall-heres-how-it-survives-strangers-1ghe</guid>
      <description>&lt;p&gt;You land on a SaaS product's homepage. It looks good. You want to see the actual dashboard before you decide whether five more minutes of your life are worth a signup form. There's no way to. "Book a demo." "Start free trial" (email required, sometimes a card). A carousel of static screenshots that were true eighteen months ago. So you close the tab, and the product loses you before you ever saw the product.&lt;/p&gt;

&lt;p&gt;We didn't want to be that tab-close. So we put a real, live, populated workspace behind a "Try live demo" button on the homepage - no email, no account, one click, straight into the actual dashboard with actual-looking data: usage charts, dead-feature detection, an events feed. Not screenshots. The real app.&lt;/p&gt;

&lt;p&gt;The problem with that idea is the second half of the sentence: &lt;em&gt;the real app&lt;/em&gt;, open to anyone. A dashboard is normally only as trustworthy as its access control, and we were about to hand out a session to whoever clicked a button, pointed at data other visitors are looking at too. Here's what actually had to be true before that button could ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Hide the buttons" is not a security model
&lt;/h2&gt;

&lt;p&gt;The lazy version of a public demo is a normal account with the delete/edit buttons hidden in the UI. It's also worthless as protection, because nothing stops a visitor from opening devtools, finding the real API call behind that hidden button, and firing it directly. UI-level restrictions are a UX feature, not an access-control boundary - they describe what a well-behaved client does, not what the server allows.&lt;/p&gt;

&lt;p&gt;So the read-only guarantee for our demo account isn't a frontend concern at all. It's a single interceptor sitting in front of every request the backend handles, checked after auth and before any handler runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SAFE_METHODS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HEAD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OPTIONS&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DemoReadOnlyInterceptor&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;NestInterceptor&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;intercept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ExecutionContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CallHandler&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Observable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;switchToHttp&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;getRequest&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AppRequest&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;isDemoAccount&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;SAFE_METHODS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ForbiddenException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;The demo account is read-only.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's registered globally, once, next to the app's other cross-cutting interceptor. It doesn't know or care what the request was trying to do - create a project, delete a workspace, rotate an API key, invite a teammate. If the session belongs to the demo account and the method isn't GET/HEAD/OPTIONS, it's rejected before it reaches the controller. That's the whole surface. Every mutation endpoint in the app - workspaces, projects, invites, API keys, dashboard layout, even the admin routes - is covered by one check, not by remembering to add a guard to each one individually. The alternative (annotate every mutating endpoint by hand) is exactly the kind of thing that's correct on the day you write it and silently wrong the first time someone adds a new endpoint and forgets.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;isDemoAccount&lt;/code&gt; itself is about as small as a security check gets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isDemoAccount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DEMO_ACCOUNT_EMAIL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One env var, one comparison, computed once per request in the auth guard and stamped onto &lt;code&gt;req.user&lt;/code&gt;. No table to keep in sync, no role to accidentally grant to the wrong person later.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap that wasn't in the interceptor - it was next to it
&lt;/h2&gt;

&lt;p&gt;Here's the failure mode that actually worried us once the read-only guard existed: what if the demo account somehow &lt;em&gt;also&lt;/em&gt; ended up with platform-admin rights? Those are two completely independent booleans, computed from two completely independent env vars (&lt;code&gt;DEMO_ACCOUNT_EMAIL&lt;/code&gt;, &lt;code&gt;PLATFORM_ADMIN_EMAIL&lt;/code&gt;), by two completely independent checks. Nothing links them - which is fine, until someone misconfigures one operator's environment and sets both to the same address. The read-only interceptor only blocks &lt;em&gt;mutations&lt;/em&gt;. It says nothing about &lt;em&gt;reads&lt;/em&gt;, and platform-admin routes are gated purely by &lt;code&gt;isPlatformAdmin&lt;/code&gt;, not by &lt;code&gt;isDemoAccount&lt;/code&gt;. A misconfiguration like that would hand a public, no-login-required visitor read access to internal operator tooling - workspace-wide billing internals, every customer's account list, whatever else "admin" means in a growing app.&lt;/p&gt;

&lt;p&gt;That's not a bug you find by testing the happy path. It's a bug you find by asking "what has to be independently true for this to be safe, and is any of it actually coupled to anything else." The fix is one line, in the same place both flags get computed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;isPlatformAdmin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;demoAccount&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;isPlatformAdmin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Demo unconditionally wins. Even if an operator someday points both env vars at the same address, the account that resolves as "demo" can never also resolve as "platform admin" - it's not a policy documented in a runbook that says "don't do that," it's a line of code that makes the mistake structurally impossible instead of merely inadvisable.&lt;/p&gt;

&lt;h2&gt;
  
  
  A demo that doesn't visibly rot
&lt;/h2&gt;

&lt;p&gt;A demo account that never gets new data is a different kind of broken - it's harder to notice, but "last used: 4 months ago" on every feature is its own way of telling a visitor the product is dead. So the demo workspace reseeds itself every hour, on a cron, wiping and regenerating its own analytics tables (aggregates, daily/hourly stats, the raw event feed - synthesized in memory as one internally-consistent event stream, not generated table by table) so "last used" always looks like today, whatever time zone the visitor is in.&lt;/p&gt;

&lt;p&gt;The part worth calling out isn't the cron - it's what happens if two requests hit it at once, which will happen the moment you run more than one instance of the API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;lockClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT pg_try_advisory_lock($1)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;LOCK_ID&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;pg_try_advisory_lock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A Postgres session-level advisory lock, held for the entire multi-transaction refresh. If a second instance's cron fires while the first is still mid-reseed, it just checks the lock, sees it's held, and returns immediately - no queueing, no duplicate writes, no two processes racing to regenerate the same rows. And if the reseed itself throws partway through, it's caught and logged, never rethrown - a failed tick just leaves last hour's data in place instead of leaving the demo workspace half-rewritten, and the next tick tries again.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "done" required before this shipped
&lt;/h2&gt;

&lt;p&gt;None of the above shipped on the strength of "looks right in a code review." Before we called it done:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ran the actual seed script twice in a row against a local database and confirmed it doesn't duplicate the workspace, the projects, or double-count the current month's usage - a reseed job that isn't idempotent turns "every hour" into "growing forever."&lt;/li&gt;
&lt;li&gt;Hit the real endpoints with a manually issued demo session cookie: &lt;code&gt;GET /api/v1/auth/me&lt;/code&gt; (200 - reads work), &lt;code&gt;POST /api/v1/workspaces&lt;/code&gt; (403 - mutation blocked), &lt;code&gt;POST&lt;/code&gt; on the event-ingestion path (403 - can't be used to inject arbitrary events into the shared dataset either), &lt;code&gt;GET&lt;/code&gt; on the analytics endpoints (200, real numbers back).&lt;/li&gt;
&lt;li&gt;Wrote a dedicated collision test: set the demo-account env var and the platform-admin env var to the same address on purpose, and asserted the resulting session still comes back &lt;code&gt;isPlatformAdmin: false&lt;/code&gt;. Not just "the normal case works" - the specific misconfiguration scenario, provoked deliberately.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The demo account also can't be used to bootstrap a live API key and start pushing real events into the shared workspace - there's deliberately no &lt;code&gt;ProjectApiKey&lt;/code&gt; row created for the demo project during seeding, so there's no key for a visitor to find and copy. That one's a narrower guarantee than the interceptor (it's "we never created the door," not "the door is locked"), which is worth being honest about rather than overstating: the day a demo project would need a live key issued for some other reason, that assumption needs revisiting, not just re-trusting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;If you want to see what any of this looks like from the visitor's side rather than the request-log side: &lt;a href="https://eventra.dev" rel="noopener noreferrer"&gt;eventra.dev&lt;/a&gt; has a "Try live demo" link on the homepage and the sign-in page. One click, no account, into a populated workspace that's never more than an hour stale. Poke at it, try to break something - it's read-only by construction, not by convention, so go ahead.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>typescript</category>
      <category>saas</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Your feature-usage scanner doesn't know Vue, Svelte, or Astro exist. Here's how we fixed that without touching its core.</title>
      <dc:creator>Jura</dc:creator>
      <pubDate>Fri, 21 Aug 2026 09:32:28 +0000</pubDate>
      <link>https://dev.to/__c500e8ac9bc2/your-feature-usage-scanner-doesnt-know-vue-svelte-or-astro-exist-heres-how-we-fixed-that-1eif</link>
      <guid>https://dev.to/__c500e8ac9bc2/your-feature-usage-scanner-doesnt-know-vue-svelte-or-astro-exist-heres-how-we-fixed-that-1eif</guid>
      <description>&lt;p&gt;If a static analyzer only walks &lt;code&gt;.ts&lt;/code&gt;/&lt;code&gt;.tsx&lt;/code&gt;/&lt;code&gt;.js&lt;/code&gt;/&lt;code&gt;.jsx&lt;/code&gt;, every other file type isn't scanned badly - it's not scanned at all. A &lt;code&gt;.vue&lt;/code&gt; component, a &lt;code&gt;.svelte&lt;/code&gt; widget, an &lt;code&gt;.astro&lt;/code&gt; page: none of them exist to the tool. Not "low confidence." Not "partial support." Invisible, the same way an empty search result looks identical whether there's genuinely nothing to find or the search just never looked in the right place.&lt;/p&gt;

&lt;p&gt;That's exactly the gap Eventra's CLI had. It scans a codebase and tells you which tracked features are actually used - the whole pitch is "stop guessing which code is dead." Except if your team ships a Vue admin panel, a Svelte checkout widget, and an Astro marketing site around the same core app (which, if you've worked on more than one team, you've probably seen - nobody plans a multi-framework stack, it just accretes), the CLI would silently skip all three, report a clean scan, and never mention that it hadn't actually looked. The exact failure mode the product exists to prevent, happening inside the product itself.&lt;/p&gt;

&lt;p&gt;We'd already closed this gap once, for Vue. This month we closed it for Svelte and Astro too, and the interesting part isn't the frameworks - it's that adding two more meant touching exactly zero lines of the CLI's core analysis engine.&lt;/p&gt;




&lt;h2&gt;
  
  
  The trick: don't teach the core anything
&lt;/h2&gt;

&lt;p&gt;The CLI's core is a TypeScript-compiler-API engine: it builds a real program, walks real ASTs, resolves real symbols across files, and figures out which &lt;code&gt;.track("event_name")&lt;/code&gt; calls are statically reachable. It is, deliberately, framework-agnostic - it doesn't know what Vue is, and it shouldn't have to.&lt;/p&gt;

&lt;p&gt;So instead of teaching the core about &lt;code&gt;.vue&lt;/code&gt;/&lt;code&gt;.svelte&lt;/code&gt;/&lt;code&gt;.astro&lt;/code&gt;, each framework gets a small, separate plugin whose only job is a translation: take the framework file, hand back one virtual TypeScript module. A Vue &lt;code&gt;Checkout.vue&lt;/code&gt; becomes &lt;code&gt;Checkout.vue.ts&lt;/code&gt;. A Svelte &lt;code&gt;Cart.svelte&lt;/code&gt; becomes &lt;code&gt;Cart.svelte.ts&lt;/code&gt;. The core never sees the original file - it sees TypeScript, because by the time it looks, that's what's actually there.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;CliPluginTransformResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;modules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}[];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's most of the contract. A plugin declares which globs it wants (&lt;code&gt;**/*.svelte&lt;/code&gt;), a &lt;code&gt;match()&lt;/code&gt; to claim files, and a &lt;code&gt;transform()&lt;/code&gt; that returns virtual modules. The core registers the glob, calls &lt;code&gt;transform()&lt;/code&gt; on matching files, and feeds the result into the exact same incremental TypeScript program every &lt;code&gt;.ts&lt;/code&gt; file goes through. No &lt;code&gt;if (isVueFile)&lt;/code&gt; branch anywhere in the resolver, the propagation engine, or the wrapper detector. They don't know these files were ever anything but TypeScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that has to be a real compiler, not a regex
&lt;/h2&gt;

&lt;p&gt;The first version of the Vue plugin - before this month - parsed &lt;code&gt;.vue&lt;/code&gt; files with two regexes: one to grab &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; content, one to grab &lt;code&gt;event="..."&lt;/code&gt; attributes out of &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt;. It worked on every fixture, because every fixture was written to be exactly what that regex expected. It fell over on real code: dynamic bindings vanished silently, HTML comments got matched as if they were live markup, and &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; + &lt;code&gt;&amp;lt;script setup&amp;gt;&lt;/code&gt; got concatenated with zero understanding of how Vue actually merges them.&lt;/p&gt;

&lt;p&gt;The fix - and the standard we held Svelte and Astro to from the start - is to parse with the framework's own real compiler. Not a library that looks similar. The actual thing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Vue: &lt;code&gt;@vue/compiler-sfc&lt;/code&gt;, the same package Vite and Nuxt build on.&lt;/li&gt;
&lt;li&gt;Svelte: &lt;code&gt;svelte/compiler&lt;/code&gt;'s &lt;code&gt;parse()&lt;/code&gt;, in its documented legacy-AST mode (&lt;code&gt;modern: false&lt;/code&gt;) - stable, tooling-oriented, and it gives you &lt;code&gt;IfBlock&lt;/code&gt;/&lt;code&gt;EachBlock&lt;/code&gt;/&lt;code&gt;AwaitBlock&lt;/code&gt; as real distinct node types instead of text to pattern-match.&lt;/li&gt;
&lt;li&gt;Astro: &lt;code&gt;@astrojs/compiler&lt;/code&gt;, the WASM parser the Astro toolchain itself uses, which hands back a typed AST with &lt;code&gt;element&lt;/code&gt;/&lt;code&gt;component&lt;/code&gt;/&lt;code&gt;expression&lt;/code&gt; nodes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three different compilers means three different AST shapes, and the differences turned out to matter more than expected. Svelte's legacy &lt;code&gt;&amp;lt;script context="module"&amp;gt;&lt;/code&gt; node has no &lt;code&gt;attributes&lt;/code&gt; field at all - recovering &lt;code&gt;lang="ts"&lt;/code&gt; meant regex-matching the raw opening-tag text, not asking the AST for it. Astro's attribute nodes carry a &lt;code&gt;kind&lt;/code&gt; (&lt;code&gt;quoted&lt;/code&gt; / &lt;code&gt;expression&lt;/code&gt; / &lt;code&gt;shorthand&lt;/code&gt; / &lt;code&gt;template-literal&lt;/code&gt; / …) and hand you the already-extracted expression source directly; Svelte makes you slice the original source string yourself using a &lt;code&gt;MustacheTag&lt;/code&gt;'s &lt;code&gt;expression.start&lt;/code&gt;/&lt;code&gt;.end&lt;/code&gt; offsets. And Astro's JSX shorthand - &lt;code&gt;&amp;lt;Button {event} /&amp;gt;&lt;/code&gt;, sugar for &lt;code&gt;event={event}&lt;/code&gt; - leaves the attribute's &lt;code&gt;.value&lt;/code&gt; empty for that specific &lt;code&gt;kind&lt;/code&gt;, so the plugin has to notice the shorthand case and substitute the attribute name back in as the expression. None of that is guessable from documentation; it's the kind of thing you only find by parsing real snippets and printing the actual AST.&lt;/p&gt;

&lt;h2&gt;
  
  
  One convention, three parsers
&lt;/h2&gt;

&lt;p&gt;Eventra's templates use a plain attribute, &lt;code&gt;event="checkout.cta"&lt;/code&gt;, to mark a tracked interaction directly in markup - no wrapper function needed for the simple case. It needed to mean the same thing in all three frameworks, expressed in whatever each one's own binding syntax actually is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Vue --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;event=&lt;/span&gt;&lt;span class="s"&gt;"checkout.cta"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;:event=&lt;/span&gt;&lt;span class="s"&gt;"computedName"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight svelte"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Svelte --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;event=&lt;/span&gt;&lt;span class="s"&gt;"checkout.cta"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;event=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;computedName&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;!-- Astro --&amp;gt;
&amp;lt;button event="checkout.cta" /&amp;gt;
&amp;lt;button event={computedName} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Literal or dynamic, each plugin turns every one of these into a synthetic function call inside the virtual module - &lt;code&gt;__eventra_svelte_template_event__("checkout.cta")&lt;/code&gt; for a literal, &lt;code&gt;__eventra_svelte_template_event__(computedName)&lt;/code&gt; for a dynamic one, unquoted. Because it's a real call sitting in the same module scope as the actual &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;/frontmatter, a dynamic binding that happens to reference a real constant defined a few lines up gets resolved through precisely the same symbol-resolution path a plain &lt;code&gt;tracker.track(someVariable)&lt;/code&gt; would use in any &lt;code&gt;.ts&lt;/code&gt; file. If it can't be resolved - say, an &lt;code&gt;{#each}&lt;/code&gt; loop variable - it's reported as a dynamic occurrence instead of silently vanishing, which was the entire bug that made the first Vue regex implementation dangerous.&lt;/p&gt;

&lt;p&gt;The plugin declares this callee as a &lt;code&gt;staticSink&lt;/code&gt; - &lt;code&gt;{ id, callee, eventNameArgumentIndex }&lt;/code&gt; - and the core converts that into its own internal sink detector. The plugin never imports anything from the CLI. The CLI never imports anything framework-specific. They meet in the middle through a plain data contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trusting a plugin without trusting arbitrary code
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;eventra.json&lt;/code&gt; lists which plugins to load, and it's a file that gets committed to git - which means it's PR-editable by anyone who can open a pull request. If the loader did &lt;code&gt;import(anyStringFromThatFile)&lt;/code&gt;, a one-line JSON edit in a PR would be arbitrary code execution on every machine that ever runs &lt;code&gt;eventra sync&lt;/code&gt;. The fix already existed for the Vue plugin and needed zero changes to cover the two new ones: only packages matching &lt;code&gt;@eventra_dev/cli-plugin-*&lt;/code&gt; - an npm scope only the maintainers can publish to - are ever dynamically imported. Anything else gets skipped with a warning. Adding Svelte and Astro meant the allowlist regex, written generically the first time, just worked; the only thing that proved it was a test asserting &lt;code&gt;cli-plugin-svelte&lt;/code&gt; matched it, written before the Svelte plugin itself existed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proving it actually works, not just that the unit tests pass
&lt;/h2&gt;

&lt;p&gt;Unit tests for a plugin are cheap to fool - you write the fixture, you write the assertion, and if you got the mental model wrong in the same way twice, they both pass anyway. So after the plugins were built and green, we did the thing a real npm consumer would do: packed all four packages (SDK, CLI, and both new plugins) into actual &lt;code&gt;.tgz&lt;/code&gt; tarballs with &lt;code&gt;npm pack&lt;/code&gt;, installed them into a throwaway project exactly the way &lt;code&gt;npm install&lt;/code&gt; would from the registry, and ran the real compiled &lt;code&gt;eventra&lt;/code&gt; binary against a &lt;code&gt;.svelte&lt;/code&gt; and an &lt;code&gt;.astro&lt;/code&gt; fixture, each with one direct SDK call and one template &lt;code&gt;event="..."&lt;/code&gt; binding.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"events"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"astro.checkout.cta"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"astro.checkout.started"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"svelte.checkout.cta"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"svelte.checkout.started"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four events in, four events out, through the actual dynamic-import loading path, not a mocked one. That's the difference between "the code that implements the feature is correct" and "the feature works when a stranger installs it," and it's cheap enough to do that there's no excuse not to.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "done" actually required
&lt;/h2&gt;

&lt;p&gt;Each plugin ended up with roughly twenty tests and a hard 100% statement/branch/function/line coverage gate, and the coverage gate is what surfaced most of the interesting edge cases, not the other way around. A branch that never gets hit is either a real gap or a genuinely impossible case - and it's worth knowing which before shipping either way. A few turned out to be the second kind: &lt;code&gt;svelte/compiler&lt;/code&gt;'s attribute grammar guarantees a single-value attribute is either plain text or one expression, never a third thing, so the "neither" branch in that check is unreachable by construction, not untested by neglect. Those got a one-line comment explaining why, instead of a fabricated test pretending to exercise something the parser's own grammar rules out. The rest - a boolean-shorthand &lt;code&gt;event&lt;/code&gt; attribute with no value, an interpolated &lt;code&gt;event="a-{b}"&lt;/code&gt; that's neither a clean literal nor a clean expression, a &lt;code&gt;context="module"&lt;/code&gt; script block that's present but empty - were real gaps, and each one is a case someone's actual codebase would eventually hit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add &lt;span class="nt"&gt;-D&lt;/span&gt; @eventra_dev/cli-plugin-svelte @eventra_dev/eventra-cli
&lt;span class="c"&gt;# or&lt;/span&gt;
pnpm add &lt;span class="nt"&gt;-D&lt;/span&gt; @eventra_dev/cli-plugin-astro @eventra_dev/eventra-cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"plugins"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"@eventra_dev/cli-plugin-svelte"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@eventra_dev/cli-plugin-astro"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same config shape as the Vue plugin, and you can list all three at once if your codebase actually is that mixed. &lt;a href="https://www.npmjs.com/package/@eventra_dev/cli-plugin-svelte" rel="noopener noreferrer"&gt;Svelte plugin on npm&lt;/a&gt; · &lt;a href="https://www.npmjs.com/package/@eventra_dev/cli-plugin-astro" rel="noopener noreferrer"&gt;Astro plugin on npm&lt;/a&gt; · &lt;a href="https://www.npmjs.com/package/@eventra_dev/cli-plugin-vue" rel="noopener noreferrer"&gt;Vue plugin on npm&lt;/a&gt; · &lt;a href="https://eventra.dev/docs" rel="noopener noreferrer"&gt;docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Angular's next. If your own tool has a hard-coded list of "supported" things, it's worth asking whether the boundary is actually load-bearing, or whether it's just the first three cases someone happened to write down.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>typescript</category>
      <category>svelte</category>
      <category>astro</category>
    </item>
    <item>
      <title>Your product has features nobody uses. Here's how to find them automatically.</title>
      <dc:creator>Jura</dc:creator>
      <pubDate>Tue, 16 Jun 2026 14:56:50 +0000</pubDate>
      <link>https://dev.to/__c500e8ac9bc2/your-product-has-features-nobody-uses-heres-how-to-find-them-automatically-3baa</link>
      <guid>https://dev.to/__c500e8ac9bc2/your-product-has-features-nobody-uses-heres-how-to-find-them-automatically-3baa</guid>
      <description>&lt;p&gt;Eight months ago I was in a product review meeting where someone asked: "does anyone actually use the bulk export?"&lt;/p&gt;

&lt;p&gt;Nobody knew. The PM checked Mixpanel. The event wasn't there. Someone said it might be tracked under a different name. We ended up skipping the question and moving on.&lt;/p&gt;

&lt;p&gt;Two weeks later, the same thing happened with a different feature. Then again.&lt;/p&gt;

&lt;p&gt;That's when I understood the real problem — and it wasn't "we need better analytics."&lt;/p&gt;




&lt;h2&gt;
  
  
  The actual problem isn't the data. It's the catalog.
&lt;/h2&gt;

&lt;p&gt;Every analytics tool I've used works the same way: you decide what to track, you add &lt;code&gt;.track()&lt;/code&gt; calls, data starts flowing. The tool shows you what events exist.&lt;/p&gt;

&lt;p&gt;But what about features that were tracked under the wrong name? What about the helper function someone added six months ago that wraps the SDK — did it ever get registered in Mixpanel? What about the feature that was renamed in code but the old event name is still in the dashboard, looking alive?&lt;/p&gt;

&lt;p&gt;You can't find dead features if you don't have a reliable catalog of what features exist.&lt;/p&gt;

&lt;p&gt;That's the gap I kept running into. And that's what I tried to fix with &lt;a href="https://eventra.dev" rel="noopener noreferrer"&gt;Eventra&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The insight: build the catalog from the code
&lt;/h2&gt;

&lt;p&gt;Instead of asking teams to manually define what they track, Eventra reads it from the TypeScript codebase itself.&lt;/p&gt;

&lt;p&gt;The CLI uses the TypeScript compiler API to scan your project and find every &lt;code&gt;eventra.track()&lt;/code&gt; call — including through wrapper functions, re-exports, and barrel files.&lt;/p&gt;

&lt;p&gt;Let me show you what that means in practice.&lt;/p&gt;

&lt;p&gt;Say you have this in your codebase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/analytics.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Eventra&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@eventra_dev/eventra-sdk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sdk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Eventra&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;EVENTRA_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;trackFeature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// features/export.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;trackFeature&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/analytics&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleExport&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;trackFeature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bulk_export&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most analytics tools would never know &lt;code&gt;trackFeature&lt;/code&gt; wraps the SDK. You'd have to manually register it. Eventra finds the chain automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @eventra_dev/eventra-cli &lt;span class="nb"&gt;sync&lt;/span&gt;
&lt;span class="c"&gt;# Scanning...&lt;/span&gt;
&lt;span class="c"&gt;# Found 14 events, 1 function wrapper (trackFeature → eventra.track)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It does this by building a semantic model of your code — resolving imports, following the call graph, extracting what string each &lt;code&gt;name&lt;/code&gt; argument resolves to. Not a regex pass. The actual TypeScript compiler API.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dead features fall out naturally
&lt;/h2&gt;

&lt;p&gt;Once you have a complete, code-derived catalog, dead feature detection is trivial: which catalog entries have had no events in the last N days?&lt;/p&gt;

&lt;p&gt;The dashboard surfaces these in a dedicated view. No configuration. No manually defining what "dead" means. The data tells you.&lt;/p&gt;

&lt;p&gt;What's more useful is the three-state classification:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Active&lt;/strong&gt; — at least one event in the last 14 days&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dead&lt;/strong&gt; — had events before, nothing recently. This is your deletion list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never seen&lt;/strong&gt; — in your code, never reached the platform. Misconfigured, unreachable, or behind a flag that never fires.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That third state was the one that surprised me most when I built this. Features that exist in code but have never, in the history of the product, produced a single event. Either they're behind a toggle that was never turned on, or the tracking is broken, or the feature is so buried nobody found it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Keeping the catalog in sync: CI integration
&lt;/h2&gt;

&lt;p&gt;The catalog drifts. You add a feature, forget to run &lt;code&gt;sync&lt;/code&gt;, and now the dead feature list is incomplete. Or you remove a feature but the old name stays in the catalog, giving false confidence that something is being tracked when the code is gone.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;check&lt;/code&gt; command is the CI gate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @eventra_dev/eventra-cli check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It compares the current state of the codebase against &lt;code&gt;eventra.json&lt;/code&gt; (the committed catalog snapshot) and exits 1 if they differ:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Checking events and function wrappers...

Events:
+ new_feature_name   ← in code, not in catalog
- old_feature_name   ← in catalog, removed from code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add this to your CI pipeline and the catalog never drifts without someone noticing.&lt;/p&gt;




&lt;h2&gt;
  
  
  The SDK side
&lt;/h2&gt;

&lt;p&gt;The CLI handles catalog management. The SDK handles runtime events.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Eventra&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@eventra_dev/eventra-sdk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;eventra&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Eventra&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;your-key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;eventra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bulk_export&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things worth mentioning about the SDK implementation that I'm happy with:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zero dependencies.&lt;/strong&gt; The whole thing — batching, retry with exponential backoff, circuit breaker, browser localStorage persistence, multi-tab leader election — is implemented with no runtime dependencies. Works in browser, Node.js, edge runtimes, and serverless.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Circuit breaker.&lt;/strong&gt; After 5 consecutive delivery failures, the SDK stops sending for 5 seconds. This prevents hammering a struggling API endpoint and burning through retry budgets. When the cooldown expires, one request goes through in half-open mode. If it succeeds, the circuit closes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Browser multi-tab coordination.&lt;/strong&gt; If you have multiple tabs open, only one tab (the "leader") flushes events to the server. The queue is shared via &lt;code&gt;localStorage&lt;/code&gt; and &lt;code&gt;BroadcastChannel&lt;/code&gt;. This prevents duplicate events from tab-heavy users and reduces server load.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Graceful shutdown.&lt;/strong&gt; In Node.js, the SDK registers &lt;code&gt;SIGINT&lt;/code&gt;/&lt;code&gt;SIGTERM&lt;/code&gt; handlers and flushes the queue before the process exits. In the browser, it listens to &lt;code&gt;visibilitychange&lt;/code&gt; and &lt;code&gt;pagehide&lt;/code&gt;. Events sent just before the user closes the tab actually arrive.&lt;/p&gt;




&lt;h2&gt;
  
  
  The ingest pipeline
&lt;/h2&gt;

&lt;p&gt;On the server side, the bottleneck I was most worried about was write throughput. Feature events can arrive in bursts — a deploy goes out, users start using a new feature, thousands of events arrive in seconds.&lt;/p&gt;

&lt;p&gt;The solution is PostgreSQL's &lt;code&gt;COPY FROM STDIN&lt;/code&gt; with &lt;code&gt;pg-copy-streams&lt;/code&gt;. Instead of individual &lt;code&gt;INSERT&lt;/code&gt; statements, events are written to an in-memory buffer, drained in batches of up to 5,000 rows, streamed into a temp table, and atomically upserted with deduplication:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"FeatureEvent"&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;"projectId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"projectId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;tmp_events&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;registry_insert&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"projectId"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"projectId"&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"idempotencyKey"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"idempotencyKey"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;IdempotencyRegistry&lt;/code&gt; table handles deduplication: if the SDK sends the same event twice (retry after a timeout), the second insert is a no-op. Clients use UUID v4 as the idempotency key, generated client-side.&lt;/p&gt;

&lt;p&gt;If the database goes down, batches spill to disk (atomic tmp-rename write) and are recovered on restart. The API returns &lt;code&gt;{ success: true }&lt;/code&gt; as soon as events hit the in-memory buffer, so ingest latency stays low regardless of what the database is doing.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I learned building this after work for 6 months
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The CLI was the hardest part.&lt;/strong&gt; The TypeScript compiler API is powerful but the documentation is thin. The wrapper propagation system — tracking which function parameter maps to the event name, across files, through re-exports — took multiple rewrites to get right. The breakthrough was treating &lt;code&gt;ts.Symbol&lt;/code&gt; as the canonical identity and using WeakMaps keyed on symbols for all caches. Once I had that foundation, incremental watch mode fell into place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dead features was always the core idea.&lt;/strong&gt; Everything else — the SDK, the dashboard, the billing, the workspaces — exists to support the dead feature detection. The catalog-from-code approach only works if there's a reliable way to discover what's in the code. The CLI is that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ship less&lt;/strong&gt; is underrated as a product strategy. Every feature you remove is maintenance you never have to do, bugs that can't exist, and cognitive load you save your users. Eventra is a tool for shipping less. I find that more interesting than another tool for shipping more.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;If you have a TypeScript project and want to see what's dead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install the SDK&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; @eventra_dev/eventra-sdk

&lt;span class="c"&gt;# Initialize the CLI config&lt;/span&gt;
npx @eventra_dev/eventra-cli init

&lt;span class="c"&gt;# Scan and sync&lt;/span&gt;
npx @eventra_dev/eventra-cli &lt;span class="nb"&gt;sync&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Free tier is 100k events/month, no credit card.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://eventra.dev" rel="noopener noreferrer"&gt;eventra.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The SDK and CLI are MIT-licensed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/and-1991/eventra-sdk" rel="noopener noreferrer"&gt;github.com/and-1991/eventra-sdk&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/and-1991/eventra-cli" rel="noopener noreferrer"&gt;github.com/and-1991/eventra-cli&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Examples:&lt;br&gt;
-&lt;a href="https://github.com/and-1991/eventra-examples" rel="noopener noreferrer"&gt;github.com/and-1991/eventra-examples&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Happy to answer questions in the comments — especially about the static analysis approach or the ingest pipeline design.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>typescript</category>
      <category>analytics</category>
    </item>
    <item>
      <title>I Built Eventra in 6 Months (After My Day Job). Here's How.</title>
      <dc:creator>Jura</dc:creator>
      <pubDate>Fri, 10 Apr 2026 19:17:25 +0000</pubDate>
      <link>https://dev.to/__c500e8ac9bc2/i-built-eventra-in-6-months-after-my-day-job-heres-how-2j9</link>
      <guid>https://dev.to/__c500e8ac9bc2/i-built-eventra-in-6-months-after-my-day-job-heres-how-2j9</guid>
      <description>&lt;h1&gt;
  
  
  Building Eventra After Work: A 6-Month Solo Developer Journey
&lt;/h1&gt;

&lt;p&gt;For the past six months, I've been building &lt;strong&gt;Eventra&lt;/strong&gt; after work.&lt;/p&gt;

&lt;p&gt;I have a full-time job that pays the bills.&lt;br&gt;
Eventra is something I build in the evenings and on weekends.&lt;/p&gt;

&lt;p&gt;I didn’t start with a startup idea.&lt;br&gt;
I didn’t start with validation.&lt;br&gt;
I just wanted to build something properly.&lt;/p&gt;

&lt;p&gt;Not a weekend project.&lt;br&gt;
Not a demo.&lt;br&gt;
Something real.&lt;/p&gt;

&lt;p&gt;That eventually became &lt;strong&gt;Eventra&lt;/strong&gt; — a feature analytics platform focused on understanding which features users actually use.&lt;/p&gt;


&lt;h1&gt;
  
  
  Why I Started Building Eventra
&lt;/h1&gt;

&lt;p&gt;When you build products long enough, you accumulate features.&lt;/p&gt;

&lt;p&gt;Some are used every day.&lt;br&gt;
Some occasionally.&lt;br&gt;
Some… never.&lt;/p&gt;

&lt;p&gt;Over time, you lose visibility.&lt;/p&gt;

&lt;p&gt;You don't know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which features matter&lt;/li&gt;
&lt;li&gt;Which ones nobody uses&lt;/li&gt;
&lt;li&gt;What can be removed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Analytics helps — but only partially.&lt;/p&gt;

&lt;p&gt;Analytics shows what happened.&lt;br&gt;
But it doesn't show what &lt;strong&gt;exists but never happens&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That idea stuck with me.&lt;/p&gt;


&lt;h1&gt;
  
  
  Building After Work
&lt;/h1&gt;

&lt;p&gt;Building Eventra while working full-time wasn't easy.&lt;/p&gt;

&lt;p&gt;Some days I wrote code for 8 hours at work, then another 2–3 hours at night.&lt;/p&gt;

&lt;p&gt;Some weekends were spent debugging rollups instead of going outside.&lt;/p&gt;

&lt;p&gt;Progress wasn't fast — but it was steady.&lt;/p&gt;

&lt;p&gt;Over six months, Eventra slowly took shape.&lt;/p&gt;


&lt;h1&gt;
  
  
  Step 1 — Building the Core Platform
&lt;/h1&gt;

&lt;p&gt;I started with the core platform:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Workspaces&lt;/li&gt;
&lt;li&gt;Projects&lt;/li&gt;
&lt;li&gt;Events&lt;/li&gt;
&lt;li&gt;Feature adoption&lt;/li&gt;
&lt;li&gt;Dead feature detection&lt;/li&gt;
&lt;li&gt;Alerts&lt;/li&gt;
&lt;li&gt;API keys&lt;/li&gt;
&lt;li&gt;Operations dashboard&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal was to build something complete — not just analytics, but a system focused on feature usage.&lt;/p&gt;


&lt;h1&gt;
  
  
  What the Dashboard Shows
&lt;/h1&gt;

&lt;p&gt;The main dashboard gives a quick overview of product usage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Total events&lt;/li&gt;
&lt;li&gt;Unique users&lt;/li&gt;
&lt;li&gt;Active features&lt;/li&gt;
&lt;li&gt;Inactive features&lt;/li&gt;
&lt;li&gt;Usage over time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This helps quickly understand whether your product is actually being used and how feature adoption changes over time.&lt;/p&gt;


&lt;h1&gt;
  
  
  Feature Tracking
&lt;/h1&gt;

&lt;p&gt;Eventra tracks individual features and shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Total usage&lt;/li&gt;
&lt;li&gt;Unique users&lt;/li&gt;
&lt;li&gt;First used&lt;/li&gt;
&lt;li&gt;Last used&lt;/li&gt;
&lt;li&gt;Usage timeline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can open any feature and see its detailed analytics and adoption patterns.&lt;/p&gt;

&lt;p&gt;This makes it easier to understand which parts of your product matter.&lt;/p&gt;


&lt;h1&gt;
  
  
  Dead Feature Detection
&lt;/h1&gt;

&lt;p&gt;One of the core ideas behind Eventra is detecting unused functionality.&lt;/p&gt;

&lt;p&gt;Eventra automatically detects features that haven't been used recently and marks them as inactive.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Feature not used in 14 days&lt;/li&gt;
&lt;li&gt;Feature used once but never again&lt;/li&gt;
&lt;li&gt;Feature abandoned after release&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This helps identify functionality that might be safe to remove.&lt;/p&gt;


&lt;h1&gt;
  
  
  Events Explorer
&lt;/h1&gt;

&lt;p&gt;You can also inspect raw events.&lt;/p&gt;

&lt;p&gt;This helps when debugging or understanding how users interact with your application.&lt;/p&gt;

&lt;p&gt;You can see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Event name&lt;/li&gt;
&lt;li&gt;User&lt;/li&gt;
&lt;li&gt;Timestamp&lt;/li&gt;
&lt;li&gt;Usage patterns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes it easier to understand what users actually do.&lt;/p&gt;


&lt;h1&gt;
  
  
  Workspaces and Projects
&lt;/h1&gt;

&lt;p&gt;Eventra supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple workspaces&lt;/li&gt;
&lt;li&gt;Multiple projects&lt;/li&gt;
&lt;li&gt;Team members&lt;/li&gt;
&lt;li&gt;Permissions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This allows you to organize analytics across different products.&lt;/p&gt;


&lt;h1&gt;
  
  
  API Keys and SDK Integration
&lt;/h1&gt;

&lt;p&gt;Each project has API keys for event ingestion.&lt;/p&gt;

&lt;p&gt;You can rotate keys, create multiple keys, and track usage.&lt;/p&gt;

&lt;p&gt;This allows safe integration with SDKs and backend systems.&lt;/p&gt;


&lt;h1&gt;
  
  
  Operations Dashboard
&lt;/h1&gt;

&lt;p&gt;I also built an internal operations dashboard.&lt;/p&gt;

&lt;p&gt;It shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ingest throughput&lt;/li&gt;
&lt;li&gt;Buffer size&lt;/li&gt;
&lt;li&gt;Rollup lag&lt;/li&gt;
&lt;li&gt;Processing delay&lt;/li&gt;
&lt;li&gt;System health&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This helps monitor the analytics pipeline.&lt;/p&gt;


&lt;h1&gt;
  
  
  Interactive Demo
&lt;/h1&gt;

&lt;p&gt;There's an &lt;strong&gt;interactive demo&lt;/strong&gt; on the homepage:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://eventra.dev" rel="noopener noreferrer"&gt;https://eventra.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can explore the UI, navigate between pages, and see how everything works.&lt;/p&gt;

&lt;p&gt;Some data is intentionally masked to avoid exposing real information.&lt;/p&gt;

&lt;p&gt;This makes it possible to explore the product without signing up.&lt;/p&gt;


&lt;h1&gt;
  
  
  Step 2 — The SDK
&lt;/h1&gt;

&lt;p&gt;Then I built the SDK.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Eventra&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@eventra_dev/eventra-sdk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tracker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Eventra&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;YOUR_PROJECT_API_KEY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;tracker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;feature_used&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user_123&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;feature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dashboard&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SDK supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Batch sending&lt;/li&gt;
&lt;li&gt;Retry logic&lt;/li&gt;
&lt;li&gt;Browser / Node / Edge runtimes&lt;/li&gt;
&lt;li&gt;TypeScript-first API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This worked well — but something was missing.&lt;/p&gt;

&lt;p&gt;The SDK only tracks events that actually happen.&lt;/p&gt;

&lt;p&gt;Which meant I still couldn't detect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Features that exist but aren't used&lt;/li&gt;
&lt;li&gt;Dead UI&lt;/li&gt;
&lt;li&gt;Forgotten code&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Step 3 — The CLI
&lt;/h1&gt;

&lt;p&gt;That's when I built the CLI.&lt;/p&gt;

&lt;p&gt;The CLI scans your codebase and finds all tracked events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @eventra_dev/eventra-cli init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scans your project&lt;/li&gt;
&lt;li&gt;Finds track() calls&lt;/li&gt;
&lt;li&gt;Detects wrapper components&lt;/li&gt;
&lt;li&gt;Saves configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now Eventra can compare:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Events in code&lt;/li&gt;
&lt;li&gt;Events actually triggered&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And detect unused features.&lt;/p&gt;

&lt;p&gt;That's when Eventra became a complete product.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Hardest Parts
&lt;/h1&gt;

&lt;p&gt;Each part had its own challenges:&lt;/p&gt;

&lt;p&gt;Backend — rollup engine&lt;br&gt;
Frontend — workspace/project architecture&lt;br&gt;
CLI — AST parsing and wrapper detection&lt;/p&gt;

&lt;p&gt;The CLI ended up being one of the hardest parts technically.&lt;/p&gt;




&lt;h1&gt;
  
  
  Current Status
&lt;/h1&gt;

&lt;p&gt;After 6 months:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dashboard — production-ready&lt;/li&gt;
&lt;li&gt;SDK — stable&lt;/li&gt;
&lt;li&gt;CLI — early version (0.0.4)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm still building this solo, after work.&lt;/p&gt;




&lt;h1&gt;
  
  
  Pricing
&lt;/h1&gt;

&lt;p&gt;I'm not trying to maximize revenue right now.&lt;/p&gt;

&lt;p&gt;The goal is validation and feedback.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://eventra.dev/pricing" rel="noopener noreferrer"&gt;https://eventra.dev/pricing&lt;/a&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  Try It
&lt;/h1&gt;

&lt;p&gt;Website&lt;br&gt;
&lt;a href="https://eventra.dev" rel="noopener noreferrer"&gt;https://eventra.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Docs&lt;br&gt;
&lt;a href="https://eventra.dev/docs" rel="noopener noreferrer"&gt;https://eventra.dev/docs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;SDK&lt;br&gt;
&lt;a href="https://www.npmjs.com/package/@eventra_dev/eventra-sdk" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/@eventra_dev/eventra-sdk&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;CLI&lt;br&gt;
&lt;a href="https://www.npmjs.com/package/@eventra_dev/eventra-cli" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/@eventra_dev/eventra-cli&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;GitHub&lt;br&gt;
&lt;a href="https://github.com/and-1991/eventra-sdk" rel="noopener noreferrer"&gt;https://github.com/and-1991/eventra-sdk&lt;/a&gt;&lt;br&gt;
&lt;a href="https://github.com/and-1991/eventra-cli" rel="noopener noreferrer"&gt;https://github.com/and-1991/eventra-cli&lt;/a&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  Why I'm Sharing This
&lt;/h1&gt;

&lt;p&gt;I'm not trying to sell anything.&lt;/p&gt;

&lt;p&gt;I'm just building something useful after work.&lt;/p&gt;

&lt;p&gt;If you're also building something on the side — I'd love to hear about it.&lt;/p&gt;

&lt;p&gt;And if Eventra looks useful, I'd love your feedback.&lt;/p&gt;

</description>
      <category>node</category>
      <category>react</category>
      <category>typescript</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
