DEV Community

Jura
Jura

Posted on

Your product has features nobody uses. Here's how to find them automatically.

Eight months ago I was in a product review meeting where someone asked: "does anyone actually use the bulk export?"

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.

Two weeks later, the same thing happened with a different feature. Then again.

That's when I understood the real problem — and it wasn't "we need better analytics."


The actual problem isn't the data. It's the catalog.

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

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?

You can't find dead features if you don't have a reliable catalog of what features exist.

That's the gap I kept running into. And that's what I tried to fix with Eventra.


The insight: build the catalog from the code

Instead of asking teams to manually define what they track, Eventra reads it from the TypeScript codebase itself.

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

Let me show you what that means in practice.

Say you have this in your codebase:

// lib/analytics.ts
import { Eventra } from "@eventra_dev/eventra-sdk";

const sdk = new Eventra({ apiKey: process.env.EVENTRA_KEY! });

export function trackFeature(name: string, userId?: string) {
  sdk.track(name, { userId });
}
Enter fullscreen mode Exit fullscreen mode
// features/export.ts
import { trackFeature } from "@/lib/analytics";

export function handleExport() {
  trackFeature("bulk_export", currentUser.id);
}
Enter fullscreen mode Exit fullscreen mode

Most analytics tools would never know trackFeature wraps the SDK. You'd have to manually register it. Eventra finds the chain automatically:

npx @eventra_dev/eventra-cli sync
# Scanning...
# Found 14 events, 1 function wrapper (trackFeature → eventra.track)
Enter fullscreen mode Exit fullscreen mode

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


Dead features fall out naturally

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?

The dashboard surfaces these in a dedicated view. No configuration. No manually defining what "dead" means. The data tells you.

What's more useful is the three-state classification:

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

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.


Keeping the catalog in sync: CI integration

The catalog drifts. You add a feature, forget to run sync, 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.

The check command is the CI gate:

npx @eventra_dev/eventra-cli check
Enter fullscreen mode Exit fullscreen mode

It compares the current state of the codebase against eventra.json (the committed catalog snapshot) and exits 1 if they differ:

Checking events and function wrappers...

Events:
+ new_feature_name   ← in code, not in catalog
- old_feature_name   ← in catalog, removed from code
Enter fullscreen mode Exit fullscreen mode

Add this to your CI pipeline and the catalog never drifts without someone noticing.


The SDK side

The CLI handles catalog management. The SDK handles runtime events.

import { Eventra } from "@eventra_dev/eventra-sdk";

const eventra = new Eventra({ apiKey: "your-key" });

eventra.track("bulk_export", { userId: currentUser.id });
Enter fullscreen mode Exit fullscreen mode

A few things worth mentioning about the SDK implementation that I'm happy with:

Zero dependencies. 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.

Circuit breaker. 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.

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

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


The ingest pipeline

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.

The solution is PostgreSQL's COPY FROM STDIN with pg-copy-streams. Instead of individual INSERT 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:

INSERT INTO analytics."FeatureEvent" (id, "projectId", ...)
SELECT t.id, t."projectId", ...
FROM tmp_events t
INNER JOIN registry_insert r
  ON r."projectId" = t."projectId"
  AND r."idempotencyKey" = t."idempotencyKey"
Enter fullscreen mode Exit fullscreen mode

The IdempotencyRegistry 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.

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


What I learned building this after work for 6 months

The CLI was the hardest part. 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 ts.Symbol as the canonical identity and using WeakMaps keyed on symbols for all caches. Once I had that foundation, incremental watch mode fell into place.

Dead features was always the core idea. 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.

Ship less 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.


Try it

If you have a TypeScript project and want to see what's dead:

# Install the SDK
npm install @eventra_dev/eventra-sdk

# Initialize the CLI config
npx @eventra_dev/eventra-cli init

# Scan and sync
npx @eventra_dev/eventra-cli sync
Enter fullscreen mode Exit fullscreen mode

Free tier is 100k events/month, no credit card.

eventra.dev

The SDK and CLI are MIT-licensed:

Examples:
-github.com/and-1991/eventra-examples

Happy to answer questions in the comments — especially about the static analysis approach or the ingest pipeline design.

Top comments (2)

Collapse
 
alexshev profile image
Alex Shev

Feature usage detection is valuable because unused features are not just product clutter; they are maintenance surface. The hard part is separating "nobody uses this" from "the right user needs it rarely but critically."

Collapse
 
__c500e8ac9bc2 profile image
Jura

You're right — that's exactly the core challenge. Eventra isn't an automatic "delete this" button; it's a signal generator. The dashboard shows usage history, not just a badge. You see the pattern: regular but infrequent spikes (a rarely-used but critical feature) look different from a single hit months ago that never repeats. The final call is still yours.