<?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: Alejandro Garcia</title>
    <description>The latest articles on DEV Community by Alejandro Garcia (@devale).</description>
    <link>https://dev.to/devale</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F535712%2F14d0e382-419a-46f9-9b0e-c2d2e6f00d64.jpeg</url>
      <title>DEV Community: Alejandro Garcia</title>
      <link>https://dev.to/devale</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/devale"/>
    <language>en</language>
    <item>
      <title>I Built an Over-Engineered Analytics Dashboard for My Indie iOS App — Here’s How</title>
      <dc:creator>Alejandro Garcia</dc:creator>
      <pubDate>Thu, 19 Mar 2026 06:51:02 +0000</pubDate>
      <link>https://dev.to/devale/i-built-an-over-engineered-analytics-dashboard-for-my-indie-ios-app-heres-how-260o</link>
      <guid>https://dev.to/devale/i-built-an-over-engineered-analytics-dashboard-for-my-indie-ios-app-heres-how-260o</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdveukghq7ay5r8ao97ml.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdveukghq7ay5r8ao97ml.png" alt=" " width="800" height="443"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;PostHog is great. But staring at your own Grafana dashboard at 11pm feels different.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I launched rCoon a few days ago - a photo-cleaning app for iOS that helps you delete blurry shots, duplicates, and short throwaway videos. It uses on-device ML so nothing ever leaves your phone. Classic indie side project.&lt;br&gt;
PostHog is my analytics backend of choice. It's powerful, the HogQL query language is surprisingly fun, and the free tier is generous. But at some point I wanted a dashboard I actually enjoyed opening. Something that felt like a mission control, not a SaaS admin panel.&lt;br&gt;
So I set up Grafana locally via Docker, wired it to PostHog and Sentry, and now I have a dashboard that shows me exactly what I care about - paying subscribers front and center, and everything else below.&lt;br&gt;
Here's exactly how I did it.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;The Stack&lt;/strong&gt;&lt;br&gt;
Grafana - running locally via Docker&lt;br&gt;
Infinity datasource plugin - queries any HTTP/JSON API, perfect for PostHog&lt;br&gt;
PostHog Query API - HogQL queries over REST&lt;br&gt;
Sentry - for crash/health metrics&lt;br&gt;
docker-compose - one command to spin it all up&lt;/p&gt;

&lt;p&gt;No cloud hosting needed. This runs on my MacBook, I open it when I want it, done.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff7srjxsba3qf6urv6gb1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff7srjxsba3qf6urv6gb1.png" alt=" " width="800" height="442"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Project Structure&lt;/strong&gt;&lt;br&gt;
After some iteration, this is the file structure I landed on:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4k60qbq1s0f931zuug5y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4k60qbq1s0f931zuug5y.png" alt=" " width="800" height="334"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The key insight: Grafana supports provisioning - you can define datasources and dashboards as YAML/JSON files, so everything is version-controlled and reproducible. No clicking through the UI to configure things. Just docker compose up and it's all there.&lt;br&gt;
[SCREENSHOT - file structure in VS Code]&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Step 1 - docker-compose.yml&lt;/strong&gt;&lt;br&gt;
yaml&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fliu2a9184gm4qiggmu63.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fliu2a9184gm4qiggmu63.png" alt=" " width="800" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh6sp06c2fiwxu2w0pswo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh6sp06c2fiwxu2w0pswo.png" alt=" " width="800" height="113"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The GF_INSTALL_PLUGINS line auto-installs the Infinity plugin on first boot. No manual plugin installation needed.&lt;br&gt;
Create a .env file (never commit this):&lt;br&gt;
env&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fczf6qvlv9fh8fppf94a0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fczf6qvlv9fh8fppf94a0.png" alt=" " width="800" height="140"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Step 2 - Configure the PostHog Datasource&lt;/strong&gt;&lt;br&gt;
provisioning/datasources/posthog.yml:&lt;br&gt;
yaml&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftxjazwyeeo96ilm0zirg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftxjazwyeeo96ilm0zirg.png" alt=" " width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Grafana reads this on startup and the datasource is ready without touching the UI.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Step 3 - Provision the Dashboard&lt;/strong&gt;&lt;br&gt;
provisioning/dashboards/dashboard.yml:&lt;br&gt;
yaml&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi0il34tyjt6y15pqygwa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi0il34tyjt6y15pqygwa.png" alt=" " width="800" height="367"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This tells Grafana to watch the /dashboards folder and auto-load any .json files as dashboards. You edit the JSON, save, and Grafana hot-reloads it.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Step 4 - Querying PostHog with HogQL&lt;/strong&gt;&lt;br&gt;
Every panel hits PostHog's query endpoint:&lt;br&gt;
POST &lt;a href="https://us.posthog.com/api/projects/%7Bproject_id%7D/query" rel="noopener noreferrer"&gt;https://us.posthog.com/api/projects/{project_id}/query&lt;/a&gt;&lt;br&gt;
Content-Type: application/json&lt;br&gt;
Authorization: Bearer &lt;br&gt;
With a body like:&lt;br&gt;
json&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;"query"&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="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HogQLQuery"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SELECT count(DISTINCT person_id) FROM events WHERE event = 'Application Opened' AND timestamp &amp;gt; now() - INTERVAL 7 DAY"&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;In Grafana's Infinity panel, set:&lt;br&gt;
Type: JSON&lt;br&gt;
Method: POST&lt;br&gt;
URL: &lt;a href="https://us.posthog.com/api/projects/YOUR_PROJECT_ID/query" rel="noopener noreferrer"&gt;https://us.posthog.com/api/projects/YOUR_PROJECT_ID/query&lt;/a&gt;&lt;br&gt;
Body: your HogQL query JSON&lt;/p&gt;

&lt;p&gt;The response comes back as { "results": [[value]] } - you point Infinity at results[0][0] and you've got your stat panel.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu3fjwv20fritzww5lbdi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu3fjwv20fritzww5lbdi.png" alt=" " width="800" height="619"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Step 5 - The Dashboard Layout&lt;/strong&gt;&lt;br&gt;
Here's how I structured the panels, top to bottom:&lt;/p&gt;

&lt;p&gt;Row 1 - North Star&lt;br&gt;
One giant stat panel: Paying Subscribers (total active). This is the number I actually care about. Everything else is context.&lt;br&gt;
[SCREENSHOT - north star panel]&lt;/p&gt;

&lt;p&gt;Row 2 - Revenue&lt;br&gt;
MRR (current month)&lt;br&gt;
Trial starts this week&lt;br&gt;
Trial → paid conversion %&lt;br&gt;
Monthly vs yearly split (pie chart)&lt;/p&gt;

&lt;p&gt;Row 3 - Engagement&lt;br&gt;
DAU / WAU / MAU time series&lt;br&gt;
DAU/MAU ratio (stickiness %)&lt;br&gt;
Feature usage breakdown - Blur vs Duplicate vs Short Videos&lt;/p&gt;

&lt;p&gt;[SCREENSHOT - engagement row]&lt;br&gt;
Row 4 - Funnel&lt;br&gt;
Waterfall panel: Install → Onboarding complete → First scan → Paywall seen → Trial started → Paid&lt;br&gt;
This is the most useful view. You can immediately see where people drop off.&lt;br&gt;
[SCREENSHOT - funnel panel]&lt;/p&gt;

&lt;p&gt;Row 5 - Health&lt;br&gt;
Crash-free session rate (from Sentry)&lt;br&gt;
App version distribution&lt;br&gt;
Events with anomalies&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Key HogQL Queries&lt;/strong&gt;&lt;br&gt;
DAU (last 30 days):&lt;br&gt;
sql&lt;br&gt;
&lt;code&gt;SELECT &lt;br&gt;
  toDate(timestamp) as day,&lt;br&gt;
  count(DISTINCT person_id) as dau&lt;br&gt;
FROM events&lt;br&gt;
WHERE event = 'Application Opened'&lt;br&gt;
  AND timestamp &amp;gt; now() - INTERVAL 30 DAY&lt;br&gt;
GROUP BY day&lt;br&gt;
ORDER BY day ASC&lt;br&gt;
Trial starts this week:&lt;br&gt;
sql&lt;br&gt;
SELECT count(DISTINCT person_id)&lt;br&gt;
FROM events&lt;br&gt;
WHERE event = 'trial_started'&lt;br&gt;
  AND timestamp &amp;gt; now() - INTERVAL 7 DAY&lt;br&gt;
Feature usage breakdown:&lt;br&gt;
sql&lt;br&gt;
SELECT &lt;br&gt;
  properties.$screen_name as feature,&lt;br&gt;
  count() as sessions&lt;br&gt;
FROM events&lt;br&gt;
WHERE event = 'screen_viewed'&lt;br&gt;
  AND timestamp &amp;gt; now() - INTERVAL 30 DAY&lt;br&gt;
GROUP BY feature&lt;br&gt;
ORDER BY sessions DESC&lt;/code&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What I Learned&lt;/strong&gt;&lt;br&gt;
PostHog's native dashboards are genuinely good. I'm not replacing them - I still use them for ad-hoc exploration. This Grafana setup is for the "mission control" view I want at a glance.&lt;br&gt;
Provisioning as code is worth the setup. Being able to git clone and docker compose up and have the full dashboard running is satisfying. It also makes writing this article much easier.&lt;br&gt;
The North Star metric changes how you feel about the data. When paying subscribers is the first thing you see, everything else becomes "what's driving or blocking that number." It focuses the analysis.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What rCoon Actually Does&lt;/strong&gt;&lt;br&gt;
Since you read this far - rCoon is an iOS app that cleans up your photo library. It finds blurry shots using on-device ML (Apple's Vision framework), finds near-duplicates, and surfaces short throwaway videos. Everything runs on-device, nothing is uploaded. It's $2.99/month or $9.99/year with a 3-day free trial.&lt;br&gt;
If your camera roll is a disaster like mine was, give it a try.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsqjlxkm8k8xz7dbnjvft.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsqjlxkm8k8xz7dbnjvft.png" alt=" " width="800" height="490"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Get the Code&lt;/strong&gt;&lt;br&gt;
I'll be open-sourcing the Grafana config (minus the API keys, obviously) on my Github. Follow me here or check rcoon.app for updates.&lt;br&gt;
If you found this useful or have questions about the setup, drop a comment - happy to go deeper on any part of it.&lt;/p&gt;




&lt;p&gt;Built with Grafana, PostHog, Docker, and too much coffee.&lt;/p&gt;

</description>
      <category>grafana</category>
      <category>ios</category>
      <category>mobile</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
