DEV Community

Cover image for Track your side project's funnel (visits signups sales) with 12 spreadsheet formulas
Revenue Operator
Revenue Operator

Posted on

Track your side project's funnel (visits signups sales) with 12 spreadsheet formulas

When a side project launches and nothing happens, the usual reaction is to change everything at once: new landing page, new price, new subreddit, new logo. A week later you still don't know what was wrong, because you never measured which step was leaking.

A funnel for a small product has three steps, so there are only three places it can leak:

visits ──► signups (free thing) ──► sales
   │             │                    │
 traffic      the offer /          the paid offer /
 problem      landing page         sales page
Enter fullscreen mode Exit fullscreen mode

You don't need an analytics stack to see which step is broken. One spreadsheet with a daily log and a dozen formulas will do it. This post gives you all of them, plus a small Python script that builds the workbook so you can start from a file instead of a blank grid.

Everything below works in Google Sheets and Excel. Where a formula is Sheets-only, it's labeled.

Step 1: a log you'll actually keep

One row per channel per day (or per post, if you prefer). Keep it boring:

A B C D E F G H I
1 Date Channel Visits Signups Sales Revenue Refunds Refund $ Week of
2 2026-10-01 reddit 120 9 1 27 0 0 formula
  • Visits: from your analytics, filtered by referrer or UTM source.
  • Signups: from your email tool or form submissions.
  • Sales and Revenue: from your checkout platform's export.
  • Channel: use a fixed list (reddit, search, pinterest, newsletter, directories, other). Free-typed channel names ("Reddit", "r/SideProject", "reddit ") will quietly break every SUMIFS below. A data validation dropdown on column B fixes this.

Column I gives you the Monday of each row's week, for weekly rollups:

=IF(A2="","",A2-WEEKDAY(A2,3))
Enter fullscreen mode Exit fullscreen mode

WEEKDAY(date, 3) returns 0 for Monday through 6 for Sunday, so subtracting it snaps any date to its week's Monday. Format the column as a date.

Step 2: pick a date window

On a Dashboard sheet, add two input cells:

A B
1 Start date 2026-10-01
2 End date 2026-10-31
3 Revenue goal 1000
4 Target signup rate 5%
5 Target visit-to-sale rate 1%

Rows 3–5 are your targets. Pick numbers that make sense for your product and price. There's no universal "good" conversion rate, so the dashboard compares you against what you decided, not against an internet benchmark.

Step 3: the 12 formulas

Every total uses the same date-window condition. Here's the first one in full:

B7  Visits         =SUMIFS(Log!$C:$C, Log!$A:$A,">="&$B$1, Log!$A:$A,"<="&$B$2)
Enter fullscreen mode Exit fullscreen mode

The next five are identical except for the column being summed:

B8  Signups        =SUMIFS(Log!$D:$D, Log!$A:$A,">="&$B$1, Log!$A:$A,"<="&$B$2)
B9  Sales          =SUMIFS(Log!$E:$E, Log!$A:$A,">="&$B$1, Log!$A:$A,"<="&$B$2)
B10 Gross revenue  =SUMIFS(Log!$F:$F, Log!$A:$A,">="&$B$1, Log!$A:$A,"<="&$B$2)
B11 Refunds        =SUMIFS(Log!$G:$G, Log!$A:$A,">="&$B$1, Log!$A:$A,"<="&$B$2)
B12 Net revenue    =B10-SUMIFS(Log!$H:$H, Log!$A:$A,">="&$B$1, Log!$A:$A,"<="&$B$2)
Enter fullscreen mode Exit fullscreen mode

Then the ratios. Wrap each one in IFERROR, because on day one every denominator is zero:

B13 Signup rate          =IFERROR(B8/B7,0)
B14 Visit-to-sale rate   =IFERROR(B9/B7,0)
B15 Sales per signup     =IFERROR(B9/B8,0)
B16 Average order value  =IFERROR(B10/B9,0)
B17 Refund rate          =IFERROR(B11/B9,0)
B18 Revenue per visit    =IFERROR(B12/B7,0)
Enter fullscreen mode Exit fullscreen mode

And the two that make the sheet useful rather than just descriptive.

Visits needed to hit your goal at your current rates:

B19 =IFERROR(ROUNDUP(B3/B16/B14,0),"need 1+ sale")
Enter fullscreen mode Exit fullscreen mode

This is goal ÷ average order value ÷ visit-to-sale rate. It's the most sobering cell in the sheet. If you need 1,000 in revenue, sell at 27 and convert 1% of visitors, you need about 3,700 visits. That number tells you whether your current channels can get there at all.

Which step to fix first:

B20 =IF(B7=0,"Traffic: no visits logged yet",
     IF(B13<B4,"Signup: fix the free offer / page",
     IF(B14<B5,"Sale: fix the offer / sales page",
     "On target: get more traffic")))
Enter fullscreen mode Exit fullscreen mode

It checks the funnel top-down and stops at the first step below your target. That's the step to work on this week, and only that one. Changing two things at once means you can't tell which one helped.

Step 4: per-channel conversion

Totals hide the useful bit. A channel can send lots of visits that never convert, while a quiet one converts well. Put a channel list in D2:D7 and add one condition to the same SUMIFS:

E2  Visits         =SUMIFS(Log!$C:$C, Log!$B:$B,$D2, Log!$A:$A,">="&$B$1, Log!$A:$A,"<="&$B$2)
F2  Signups        =SUMIFS(Log!$D:$D, Log!$B:$B,$D2, Log!$A:$A,">="&$B$1, Log!$A:$A,"<="&$B$2)
G2  Sales          =SUMIFS(Log!$E:$E, Log!$B:$B,$D2, Log!$A:$A,">="&$B$1, Log!$A:$A,"<="&$B$2)
H2  Signup rate    =IFERROR(F2/E2,0)
I2  Visit-to-sale  =IFERROR(G2/E2,0)
Enter fullscreen mode Exit fullscreen mode

Fill down. If you'd rather not maintain the channel list by hand, =SORT(UNIQUE(FILTER(Log!B2:B,Log!B2:B<>""))) builds it automatically (Google Sheets and Excel 365).

Google Sheets only: one QUERY produces a weekly rollup by channel, using the "Week of" column:

=QUERY(Log!A1:I, "select I, B, sum(C), sum(D), sum(E)
  where A is not null group by I, B order by I
  label I 'Week of', sum(C) 'Visits', sum(D) 'Signups', sum(E) 'Sales'", 1)
Enter fullscreen mode Exit fullscreen mode

Two weeks of that table usually answers "where should I post next?" better than any growth thread.

Step 5: generate the workbook with Python

If you'd rather start from a file, this script builds the whole thing: log, dashboard, channel table, yellow input cells and number formats. It includes six example rows so the dashboard isn't empty when you open it. They're made-up numbers for demonstration, so delete them before logging your own.

# make_funnel_tracker.py: builds funnel-tracker.xlsx (works in Excel and Google Sheets)
# pip install openpyxl
from datetime import date, timedelta
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill

wb = Workbook()
log = wb.active
log.title = "Log"
log.append(["Date", "Channel", "Visits", "Signups", "Sales", "Revenue",
            "Refunds", "Refund $", "Week of"])

# Example rows so the dashboard isn't empty. Delete them and log your own.
sample = [("reddit", 120, 9, 1, 27), ("search", 40, 4, 0, 0), ("pinterest", 65, 3, 1, 27),
          ("newsletter", 210, 22, 3, 81), ("reddit", 80, 5, 0, 0), ("search", 55, 6, 1, 27)]
start = date(2026, 10, 1)
for i, (ch, v, s, n, r) in enumerate(sample):
    row = log.max_row + 1
    log.append([start + timedelta(days=i * 3), ch, v, s, n, r, 0, 0,
                f"=IF(A{row}=\"\",\"\",A{row}-WEEKDAY(A{row},3))"])  # Monday of that week
for col in ("A", "I"):
    for cell in log[col][1:]:
        cell.number_format = "yyyy-mm-dd"

d = wb.create_sheet("Dashboard")
yellow = PatternFill("solid", fgColor="FFF59D")
bold = Font(bold=True)

# Inputs (yellow cells)
inputs = [("Start date", date(2026, 10, 1)), ("End date", date(2026, 10, 31)),
          ("Revenue goal", 1000), ("Target signup rate", 0.05), ("Target visit-to-sale rate", 0.01)]
for i, (label, val) in enumerate(inputs, start=1):
    d[f"A{i}"], d[f"B{i}"] = label, val
    d[f"B{i}"].fill = yellow
d["B1"].number_format = d["B2"].number_format = "yyyy-mm-dd"
d["B4"].number_format = d["B5"].number_format = "0.0%"

# Date-window condition reused by every SUMIFS
W = 'Log!$A:$A,">="&$B$1,Log!$A:$A,"<="&$B$2'
metrics = [
    ("Visits",              f"=SUMIFS(Log!$C:$C,{W})", "0"),
    ("Signups",             f"=SUMIFS(Log!$D:$D,{W})", "0"),
    ("Sales",               f"=SUMIFS(Log!$E:$E,{W})", "0"),
    ("Gross revenue",       f"=SUMIFS(Log!$F:$F,{W})", "$#,##0.00"),
    ("Refunds",             f"=SUMIFS(Log!$G:$G,{W})", "0"),
    ("Net revenue",         f"=B10-SUMIFS(Log!$H:$H,{W})", "$#,##0.00"),
    ("Signup rate",         "=IFERROR(B8/B7,0)", "0.0%"),
    ("Visit-to-sale rate",  "=IFERROR(B9/B7,0)", "0.00%"),
    ("Sales per signup",    "=IFERROR(B9/B8,0)", "0.0%"),
    ("Average order value", "=IFERROR(B10/B9,0)", "$#,##0.00"),
    ("Refund rate",         "=IFERROR(B11/B9,0)", "0.0%"),
    ("Revenue per visit",   "=IFERROR(B12/B7,0)", "$0.000"),
    ("Visits needed for goal", '=IFERROR(ROUNDUP(B3/B16/B14,0),"need 1+ sale")', "#,##0"),
    ("Weakest step",        '=IF(B7=0,"Traffic: no visits logged yet",IF(B13<B4,"Signup: fix the free offer / page",IF(B14<B5,"Sale: fix the offer / sales page","On target: get more traffic")))', "@"),
]
for i, (label, formula, fmt) in enumerate(metrics, start=7):
    d[f"A{i}"], d[f"B{i}"] = label, formula
    d[f"B{i}"].number_format = fmt
    d[f"A{i}"].font = bold

# By-channel table, columns D:I
d["D1"], d["E1"], d["F1"], d["G1"], d["H1"], d["I1"] = ("Channel", "Visits", "Signups", "Sales",
                                                       "Signup rate", "Visit-to-sale")
for c in "DEFGHI":
    d[f"{c}1"].font = bold
channels = ["reddit", "search", "pinterest", "newsletter", "directories", "other"]
for r, ch in enumerate(channels, start=2):
    d[f"D{r}"] = ch
    for col, src in (("E", "C"), ("F", "D"), ("G", "E")):
        d[f"{col}{r}"] = f"=SUMIFS(Log!${src}:${src},Log!$B:$B,$D{r},{W})"
    d[f"H{r}"] = f"=IFERROR(F{r}/E{r},0)"
    d[f"I{r}"] = f"=IFERROR(G{r}/E{r},0)"
    d[f"H{r}"].number_format = "0.0%"
    d[f"I{r}"].number_format = "0.00%"

d.column_dimensions["A"].width = 26
d.column_dimensions["B"].width = 34
wb.save("funnel-tracker.xlsx")
print("saved funnel-tracker.xlsx")
Enter fullscreen mode Exit fullscreen mode

Run python make_funnel_tracker.py, then open the file in Excel, or upload it to Google Drive and open it with Sheets. openpyxl writes formulas without calculating them, so the values appear when a spreadsheet app opens the file. We recalculated it in LibreOffice as a check. With the sample rows, the dashboard shows 570 visits, 49 signups, 6 sales, an 8.6% signup rate, a 1.05% visit-to-sale rate, 3,519 visits needed for the 1,000 goal, and "On target: get more traffic" as the weakest step.

How to use it without fooling yourself

  • Log daily, even zeros. A missing day and a zero day look the same in a chart, but they mean very different things.
  • Wait for a minimum sample before judging a channel. A conversion rate from 30 visits is mostly noise. Pick a threshold before you look (say, 200 visits) and don't judge a channel until it reaches it.
  • Change one thing per week. Use the "Weakest step" cell to pick it, write down what you changed and the date, and compare the next window against the previous one.
  • Keep refunds in. Net revenue and refund rate tell you whether the product delivers what the page promised. A high refund rate is a product or messaging problem, not a traffic problem.
  • Don't compare yourself with screenshots. Other people's dashboards come from different products, prices and audiences. Compare this week with your own last week.

Wrapping up

The whole point of the sheet is to replace "nothing's working, change everything" with a single sentence: "Search converts but sends little traffic, Reddit sends traffic but signups are low, so this week I'm fixing the free offer." That gives you a decision you can act on and check.

If you build on the script (say, pulling visits from an analytics CSV automatically), share it in the comments. We'd like to see it.


Disclosure: this article is from Revenue Operator. We make the 30-Day Revenue Operator Pack, a $27 playbook whose templates include a metrics dashboard, a distribution log and a diagnostic tree for this exact process. Everything above works without it, and there's a free 6-page guide to the method here: getrevenueoperator.com/free.

Top comments (0)