DEV Community

Zenovay
Zenovay

Posted on

Building a terminal interface for my analytics SaaS with Cloudflare Durable Objects

Every analytics dashboard lives in a browser tab. My terminal is always open. So I built @zenovay/cli, a terminal interface for my analytics SaaS.

The killer command is zenovay events tail: a live stream of pageviews and events, filterable, pipeable, scriptable. Think stripe listen but for your web traffic.

This post covers how the live tail actually works.

The product surface

$ zenovay events tail --site mysite.com --path /pricing
[14:32:18] mysite.com  pageview  /pricing               US  Chrome
[14:32:21] mysite.com  pageview  /pricing               DE  Firefox
[14:32:24] mysite.com  click     /pricing#cta-pro       US  Chrome
[14:32:28] mysite.com  pageview  /pricing               GB  Safari
Enter fullscreen mode Exit fullscreen mode

You can pipe it:

$ zenovay events tail --site mysite.com | jq .country | sort | uniq -c
Enter fullscreen mode Exit fullscreen mode

The architecture

Three components:

  1. CLI binary (TypeScript, distributed via npm)
  2. WebSocket gateway (Cloudflare Worker with Durable Objects)
  3. Event broadcast (Workers pushing to subscribed Durable Objects)

The Durable Object as fanout point

Each tracked site gets a Durable Object that holds the list of active WebSocket subscribers.

export class SiteStream {
  constructor(private state: DurableObjectState) {
    this.sockets = new Set<WebSocket>()
  }

  async fetch(request: Request) {
    if (request.headers.get('Upgrade') === 'websocket') {
      const pair = new WebSocketPair()
      this.handleSocket(pair[1])
      return new Response(null, { 
        status: 101, 
        webSocket: pair[0] 
      })
    }

    if (request.method === 'POST') {
      const event = await request.json()
      this.broadcast(event)
      return new Response('ok')
    }
  }

  broadcast(event: unknown) {
    const data = JSON.stringify(event)
    for (const ws of this.sockets) {
      try { ws.send(data) }
      catch { this.sockets.delete(ws) }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The CLI side

Built with Commander for argument parsing and just plain WebSocket for the stream. Catppuccin Mocha palette via chalk hex codes.

Install

npm install -g @zenovay/cli
zenovay auth login
zenovay events tail --site yoursite.com
Enter fullscreen mode Exit fullscreen mode

Free tier of Zenovay includes the CLI. Site is zenovay.com.

Anyone else building TUIs for SaaS products? Curious what patterns you have found.

Valerio

Top comments (0)