Reading List
The most recent articles from a list of feeds I subscribe to.
Some business graphs for 2022
Hello! I like looking at other independent authors’ business graphs, so I thought I’d share some percentages and graphs of my own this year. Hopefully some of this is useful to other writers who run internet businesses.
All of the graphs are about Wizard Zines’ business selling zines – I don’t do sponsorships or consulting or commissions or anything.
print vs digital sales
This year 58% of sales were digital and 42% were print. I’m really happy with this: when I decided to start printing and shipping zines in 2021, I was a bit worried that nobody would buy them. But we’ve sold out our first two print runs and it’s been going great.
Note: PDF means “PDF you can read on your computer or print at home”, print means “print version that’s physically shipped to you”
corporate vs personal sales
I sell both corporate licenses (“buy this zine for your team!”) and personal copies. I think having corporate sales is great, but it feels like it would be risky to be too dependent on a small number of corporate customers. So I keep an eye on the percentage of corporate sales.
We’re at about 16% corporate sales for 2022 (as a percentage of revenue), which doesn’t seem too high. It looks like this number has been pretty flat over time.

PPP sales
I run a PPP (“purchasing power parity”) program where I give a discount to folks from countries with a weaker currency relative to the US. We’ll look at a couple of things here:
- How many different countries are represented?
- Which countries are the biggest users of PPP?
- What percentage of revenue is from these sales?
Here are some graphs:

People from 81 different countries used the program (so many!!), and the biggest two users are India and Brazil. This has been the case as long as I can remember, presumably because India and Brazil are two of the biggest countries in the world.
About 5% of revenue comes from sales with PPP discounts.
bundles vs individual zines
It used to be that you could only buy individual zines on the store – if you wanted to buy a bundle, you had to buy each one individually. But then one day someone suggested I add a way to buy all of the zines at once.
I did, and this turned out to be a very important business decision – here’s a graph of the percentage of revenue from zine bundles (“packs”) over time:

In 2022 it was 65% of revenue (up from 3% in 2018, which I guess is when I added zine packs for the first time).
platforms
I used to sell everything on Gumroad, now I sell everything on Shopify.
I could include some graphs here, but I think it’s not that interesting – it’s mostly a slow migration from 2020 to 2023, where 2020 was 100% Gumroad and 2023 is 100% Shopify.
Some reasons I switched to Shopify:
- I need to sell print zines, and Shopify has inventory management. Also the amazing fulfillment company I use (White Squirrel) uses a Shopify plugin.
- Since I needed to be using Shopify anyway for print zines, it felt easier to have everything on one platform.
- Right now Gumroad charges 10% (7% + processing fees), and Shopify charges $38/month plus processing fees. I actually pay more like $150/month including various Shopify plugins, but for me $150/month is a lot less than 7% of revenue.
Moving from Gumroad to Shopify took probably a month of energy in 2022 but it was worth it to have everything in one place.
revenue graph for 2020-2022
Here’s a graph of (relative) revenue. The blue and the purple are Shopify / Gumroad percentages, because of the Gumroad -> Shopify migration I mentioned in 2021 and 2022.
You can see that revenue for 2022 was less than 2020, but quite a bit more than 2021.
This is related to the number of zines released each year:
- 2 zines in 2022 (The Pocket Guide to Debugging and How DNS Works), but only 1
- 1 zine in 2021 (Bite Size Bash)
- 3 zines in 2020 (Become a SELECT Star, How Containers Work, Hell Yes! CSS!)
I put this towards the end because this is not something I try not to worry about too much – every year so far the business has made comfortably enough to pay everyone. If that looks like it might change, I’ll worry about it.
revenue by month
Here’s a graph of revenue by month for 2022 (and the first month of 2023 so far).

You can see two giant spikes: in April and December when we released new zines. (The Pocket Guide to Debugging has done super well so far, thanks to all of you who bought it! Print copies are on track to ship around the end of January.).
You can also see the Shopify / Gumroad migration happening in this graph – in May I stopped using Gumroad completely.
revenue by country
I got curious about which countries most sales come from, so I drew a graph. I combined the EU into one big group because there are a lot of European countries and I do a lot of special case stuff for Europe (for tax issues etc). Here’s the graph:

In numbers, that’s:
- US: 59.63%
- Europe: 14.71%
- Canada: 6.62%
- UK: 5.40%
- Australia: 3.35%
- Other: 10.29% (there are 117 countries in there)
Mostly what this tells me is that 40% of revenue comes from outside of the US – that’s a lot! I already offer free international shipping for large orders, but it makes me want to figure out if it’s possible to improve the international shipping situation for small orders too.
If we look at copies by country instead of revenue, this looks a bit different – India and Brazil are both above Australia.
more country data
Here’s a more detailed list of revenue percentages per country, if you want to break that “Europe” category above down more.
- US: 59.65%
- Canada: 6.57%
- United Kingdom: 5.43%
- Germany: 4.81%
- Australia: 3.34%
- France: 1.85%
- Netherlands: 1.58%
- India: 1.41%
- South Africa: 1.3%
- Switzerland: 1.15%
- Spain: 0.88%
- Sweden: 0.87%
- Brazil: 0.85%
- New Zealand: 0.82%
- Ireland: 0.7%
- Singapore: 0.64%
- Poland: 0.62%
- Norway: 0.61%
- Belgium: 0.5%
- Italy: 0.43%
- Finland: 0.42%
- Austria: 0.39%
- Denmark: 0.35%
- Portugal: 0.34%
- Mexico: 0.32%
And the full list of 126 countries where customers came from this year is: Canada, United States, Australia, Netherlands, United Kingdom, Denmark, Austria, Belgium, Spain, Mexico, Italy, Greece, France, Sweden, Finland, Germany, New Zealand, Switzerland, Malaysia, Norway, Israel, India, Iceland, Ireland, Portugal, China, Brazil, Argentina, Colombia, Czechia, Poland, Chile, Saudi Arabia, Bangladesh, Thailand, Belarus, Georgia, South Africa, Serbia, Kenya, Slovenia, Taiwan, Indonesia, Russia, Bhutan, Lithuania, Nigeria, Singapore, Maldives, Morocco, Japan, Sri Lanka, Romania, Egypt, Turkey, El Salvador, Estonia, United Arab Emirates, Malta, Vietnam, Ukraine, Macao, Croatia, Latvia, Jamaica, Iraq, Slovakia, Algeria, Philippines, Ecuador, Cyprus, Bulgaria, Jordan, Montenegro, Pakistan, Hungary, South Korea, Kyrgyzstan, Costa Rica, Ghana, Armenia, Peru, Hong Kong, Kazakhstan, Mongolia, Tunisia, Uruguay, Madagascar, Guatemala, Afghanistan, Angola, Bolivia, Uganda, Tanzania, Venezuela, Dominican Republic, Nepal, Réunion, Antigua and Barbuda, Qatar, Gibraltar, Azerbaijan, Guinea, Moldova, Sudan, Bahamas, Sierra Leone, Seychelles, Pitcairn, Uzbekistan, Bosnia and Herzegovina, Cambodia, Rwanda, Honduras, Benin, Kuwait, Cameroon, Puerto Rico, Turks and Caicos, Trinidad and Tobago, Senegal, North Macedonia, Botswana, Mauritius, Nicaragua.
It’s fun to see so many countries on the list!
how were the graphs generated?
One person asked how these graphs were generated. Basically I have:
- a “data warehouse” (a small 100MB SQLite database)
- a few scripts that use sqlite-utils to import data from the Shopify / Gumroad APIs into the “warehouse”, that run in a cron job on my laptop
- one view that combines my Gumroad sales and Shopify sales into a single table called
all_sales - a Metabase installation on my laptop where I can write SQL to make dashboards and graphs. Metabase is an open source BI tool.
This setup has worked great, I really like Metabase. All of the graphs are screenshots from Metabase.
people
Finally, I want to thank everyone who worked with me on the zines this year. I couldn’t do it without them. We had:
- Marie Claire LeBlanc Flanagan (we worked together on writing pretty much every weekday in 2022)
- Dolly Lanuza (editing)
- Gersande La Flèche (copy editing)
- Vladimir Kašiković (cover illustration)
- All of the beta readers who read and commented on early versions of the zines
- The team at White Squirrel (fulfillment/shipping)
- The team at Girlie Press (printing)
Also my partner Kamal reads every zine before it’s published and he always gives me great advice.
And of course I couldn’t do any of this without the readers who support all of this work ❤.
New zine: The Pocket Guide to Debugging
Hello! On Monday, we released a new zine: The Pocket Guide to Debugging! It has 47 of my favourite strategies for solving your sneakiest bugs.
You can get it for $12 here: https://wizardzines.com/zines/debugging-guide, or get an 12-pack of all my zines here.
Here’s the cover:
the table of contents
Here’s the table of contents!
A few people mentioned that they were printing it out, so I made a PDF poster version if you want to print it:
I love that the table of contents is already kind of useful as a collection of strategies on its own.
why debugging?
I wrote this zine because debugging is a huge part of how we spend our time as programmers, but nobody teaches us how to do it! If you’re lucky, you get to pair program with someone who’s good at debugging and explaining their thought processes, and they can show you. But not all of us have that person, so we end up just struggling through it ourselves and learning strategies the hard way.
So I wanted to write a zine where beginners can learn some of these strategies the easy way, and which more experienced programmers can use as a reference to get ideas when you’re in the middle of a tricky bug.
it comes with some debugging mysteries!
This zine comes with a few choose-your-own-adventure debugging mysteries (like “The Case of the Connection Timeout”), at https://mysteries.wizardzines.com.
These mysteries show you how to apply some of the tricks in the zine to a specific kind of bug: computer networking issues! It also demos some of my favourite networking spy tools – it’ll show you some tips for interpreting their output.
You can read some notes on designing those puzzles here: Notes on building debugging puzzles. (You might notice that post is from a year and half – that’s because I’ve been trying to write this zine on and off for 3 and a half years and a lot of things happened along the way :))
it’s actually been helping me debug!
I’ve actually been shocked by how useful this zine has been for helping me debug – after all, I know all these strategies! I like to think I’m pretty good at debugging!
But when I’m in the middle of a tricky bug and I’m frustrated, I’ve actually been finding it incredibly helpful to reach for the table of contents and get an idea for something to try.
It’s also been fun to reflect on what strategies I’m using when debugging. For example, yesterday I had a CSS bug, and I was super frustrated. But it turned that I just needed to:
- come up with one small question
- write a tiny program
- start writing a message asking for help
- quickly read the docs
- delete the message I started writing without sending it, since I’d figured it out :)
some blog posts I wrote along the way
Here are a few blog posts I wrote while thinking about how to write this zine:
- a debugging manifesto
- some ways to get better at debugging
- reasons why bugs might feel “impossible”
- when debugging, your attitude matters
- what does debugging a program look like?
you can get a print copy shipped to you!
There’s always been the option to print the zines yourself on your home printer.
But this time there’s a new option too: you can get a print copy shipped to you! (just click on the “print version” link on this page)
The only caveat is print orders will ship around the end of January – I need to wait for orders to come in to get an idea of how many I should print before sending it to the printer.
the home printing directions are a little bit different!
This zine is twice the length of other zines, but half the height! This makes it extremely pocket sized, and it means you have to cut the print version in half. But don’t worry – there’s a dotted line and a video :)
The video with the print directions is at https://wizardzines.com/print/
the hardest part of writing this zine: making it specific
It’s relatively easy to give high-level debugging advice. Reproduce the bug! Be rigorous! Try to divide the problem space in half! Print stuff out! And this zine started out as pretty general high-level advice. (you can read a old table of contents here from an earlier draft)
Turning those high-level guidelines into specific things that you can actually do was a lot harder. I sat down with my amazing friend Marie Claire LeBlanc Flanagan every weekday at 10am for 6 months, and every day we made the zine a little more specific and concrete and useful.
I’m really proud of how it turned out.
beta readers are amazing
Also, I want to thank the beta readers – 40 of you read the zine and left comments about what was confusing, what was working, and ideas for how to make it better. It made the end product so much better.
thank you
As always: if you’ve bought zines in the past, thank you for all your support over the years. I couldn’t do this without you. Happy holidays.
A debugging manifesto
Hello! I’ve been working on a zine about debugging for the last 6 months with my friend Marie, and one of the problems we ran into was figuring out how to explain the right attitude to take when debugging.
We ended up writing a short debugging manifesto to start the zine with, and I’m pretty happy with how it came out. Here it is as an image, and as text (with some extra explanations)

1. Inspect, don’t squash
When you run into a bug, the natural instinct is to try to fix it as fast as possible. And of course, sometimes that’s what you have to do – if the bug is causing a huge production incident, you have to mitigate it quickly before diving into figuring out the root cause.
But in my day to day debugging, I find that it’s generally more effective (and faster!) to leave the bug in place, figure out exactly what’s gone wrong, and then fix it after I’ve understood what happened.
Trying to fix it or add workarounds without fully understanding what happened usually ends up just leaving me more confused.
2. Being stuck is temporary
Sometimes I get really demoralized when debugging and it feels like I’ll NEVER make progress.
I have to remind myself that I’ve fixed a lot of bugs before, and I’ll probably fix this one too :)
3. Trust nobody and nothing
Sometimes bugs come from surprising sources! For example, in I think I found a Mac kernel bug? I describe how, the first time I tried to write a program for Mac OS, I had a bug in my program that was caused by a Mac OS kernel bug.
This was really surprising (usually the operating system is not at fault!!), but sometimes even normally-trustworthy sources are wrong. Even it’s a popular library, your operating system, the official documentation, or an extremely smart and competent coworker!
4. It’s probably your code
That said, almost all of the time the problem is not “there’s a bug in Mac OS”. I can only speak for myself, but 95% of the time something is going wrong with my program, it’s because I did something silly.
So it’s important to look for the problem in your own code first before trying to blame some external source.
5. Don’t go it alone
I’ve learned SO much by asking coworkers or friends for help with debugging. I think it’s one of the most fun ways to collaborate because you have a specific goal, and there are tons of opportunities to share information like:
- how to use a specific debugging tool (“here’s how to use GDB to inspect the memory here….”)
- how a computer thing works (“hey, can you explain CORS?”)
- similar past bugs (“I’ve seen this break in X way in the past, maybe it’s that?”)
6. There’s always a reason
This one kind of speaks for itself: sometimes it feels like things are just randomly breaking for no reason, but that’s never true.
Even if something truly weird is happening (like a hardware problem), that’s still a reason.
7. Build your toolkit
I’ve written a LOT about my love for debugging tools like tcpdump, strace, and more on this blog.
To fix bugs you need information about what your program is doing, and to get that information sometimes you need to learn a new tool.
Also, sometimes you need to build your own better tools, like by improving your test suite, pretty printing, etc.
8. It can be an adventure
As you probably know if you’re a regular reader of this blog, I love debugging and I’ve learned a lot from doing it. You get to learn something new! Sometimes you get a great war story to tell! What could be more fun?
I really think of debugging as an investment in my future knowledge – if something is breaking, it’s often because there’s something wrong in my mental model, and that’s an opportunity to learn and make sure that I know it for next time.
Of course, not all bugs are adventures (that off-by-one error I was debugging today certainly did not feel like a fun adventure). But I think it’s important to (as much as you can) reflect on your bugs and see what you can learn from them.
Tips for analyzing logs
Hello! I’ve been working on writing a zine about debugging for a while now (we’re getting close to finishing it!!!!), and one of the pages is about analyzing logs. I asked for some tips on Mastodon and got WAY more tips than could fit on the page, so I thought I’d write a quick blog post.
I’m going to talk about log analysis in the context of distributed systems debugging (you have a bunch of servers with different log files and you need to work out what happened) since that’s what I’m most familiar with.
search for the request’s ID
Often log lines will include a request ID. So searching for the request ID of a failed reques will show all the log lines for that request.
This is a GREAT way to cut things down, and it’s one of the first helpful tips I got about distributed systems debugging – I was staring at a bunch of graphs on a dashboard fruitlessly trying to find patterns, and a coworker gave me the advice (“julia, try looking at the logs for a failed request instead!”). That turned out to be WAY more effective in that case.
correlate between different systems
Sometimes one set of logs doesn’t have the information you need, but you can get that information from a different service’s logs about the same request.
If you’re lucky, they’ll both share a request ID.
More often, you’ll need to manually piece together context from clues and the timestamps of the request.
This is really annoying but I’ve found that often it’s worth it and gets me a key piece of information.
beware of time issues
If you’re trying to correlate events based on time, there are a couple of things to be aware of:
- sometimes the time in a logging system is based on the time the log was ingested, not the time that the event actually happened. Sometimes you have to write a date parser to get the actual time the event happened.
- different machines can have slightly skewed clocks
log lines for the same request can be very far apart
Especially if a request takes a long time (maybe it took 5 minutes because of a long timeout!), the log lines for the request might be much more spread out than you expected. You can accumulate many thousands of log lines in 5 minutes!
Searching for the request ID really helps with this – it makes it harder to accidentally miss a log entry with an important clue.
Also, log lines can occasionally get completely lost if a server dies.
build a timeline
Keeping all of the information straight in your head can get VERY confusing, so I find it helpful to keep a debugging document where I copy and paste bits of information.
This might include:
- key error messages
- links to relevant dashboards / log system searches
- pager alerts
- graphs
- human actions that were taken (“right before this message, we restarted the load balancer…”)
- my interpretation of various messages (“I think this was caused by…”)
reformat them into a table
Sometimes I’ll reformat the log lines to just print out the information I’m
interested in, to make it easier to scan. I’ve done this on the command line
with a simple awk command:
cat ... | awk '{print $5 - $8}'
but also with fancy log analysis tools (like Splunk) that let you make a table on the web
check that a “suspicious” error is actually new
Sometimes I’ll notice a suspicious error in the logs and think “OH THERE’S THE CULPRIT!!!“. But when I search for that message to make sure that it’s actually new, I’ll find out that this error actually happens constantly during normal operation, and that it’s completely unrelated to the (new) situation that I’m dealing with.
use the logs to make a graph
Some log analysis tools will let you turn your log lines into a graph to detect patterns.
You can also make a quick histogram with grep and sort. For example I’ve
often done something like:
grep -o (some regex) | sort | uniq -c | sort -n
to count how many of each line matching my regular expression there are
filter out irrelevant lines
You can remove irrelevant lines with grep like this:
cat file | grep -v THING1 | grep -v THING2 | grep -v THING3 | grep -v THING4
for the reply guys: yes, we all know you don’t need to use cat here :)
Or if your log system has some kind of query language, you can search for NOT THING1 AND NOT THING2 ...
find the first error
Often an error causes a huge cascade of related errors. Digging into the later errors can waste a lot of your time – you need to start by finding the first thing that triggered the error. Often you don’t need to understand the exact deals of why the 15th thing in the error cascade failed, you can just fix the original problem and move on.
scroll through the log really fast
If you already have an intuition for what log lines for this service should normally look like, sometimes scrolling through them really fast will reveal something that looks off.
turn the log level up (or down)
Sometimes turning up the log level will give you a key error message that explains everything.
But other times, you’ll get overwhelmed by a million irrelevant messages
because the log level is set to INFO, and you need to turn the log level down.
put it in a spreadsheet/database
I’ve never tried this myself, but a couple of people suggested copying parts of the logs into a spreadsheet (with the timestamp in a different column) to make it easier to filter / sort.
You could also put the data into SQLite or something (maybe with sqlite-utils?) if you want to be able to run SQL queries on your logs.
on generating good logs
A bunch of people also had thoughts on how to output easier-to-analyze logs. This is a bigger topic than a few bullet points but here are a few quick things:
- use a standard schema/format to make them easier to parse
- include a transaction ID/request ID, to make it easier to filter for all lines related to a single transaction/request
- include relevant information. For example, “ERROR: Invalid msg size” is less helpful than “ERROR: Invalid msg size. Msg-id 234, expected size 54, received size 0”.
- avoid logging personally identifiable information
- use a logging framework instead of using
printstatements (this helps you have things like log levels and a standard structure)
that’s all!
Let me know on Twitter/Mastodon if there’s anything I missed! I might edit this to add a couple more things.
A couple of Rust error messages
Hello!
I’ve been doing Advent of Code in Rust for the past couple of days, because I’ve never really gotten comfortable with the language and I thought doing some Advent of Code problems might help.
My solutions aren’t anything special, but because I’m trying to learn, I’ve been trying to take a slightly more rigorous approach than usual to compiler errors. Instead of just fixing the error and moving on, I’m trying to make sure that I actually understand what the error message means and what it’s telling me about how the language works.
My steps to do that are:
- fix the bug
- make a tiny standalone program reproducing the same compiler error
- think about it and try to explain it to myself to make sure I actually understand why that error happened
- ask for help if I still don’t understand
So here are a couple of compiler errors and my explanations to myself of why the error is happening.
Both of them are pretty basic Rust errors, but I had fun thinking about them today. I wrote this for an imagined audience of “people who know some Rust basics but are still pretty bad at Rust”, if there are any of you out there.
error 1: a borrowing error
Here’s some code (rust playground link):
fn inputs() -> Vec<(i32, i32)> {
return vec![(0, 0)];
}
fn main() {
let scores = inputs().iter().map(|(a, b)| {
a + b
});
println!("{}", scores.sum::<i32>());
}
And here’s the compiler error:
5 | let scores = inputs().iter().map(|(a, b)| {
| ^^^^^^^^ creates a temporary which is freed while still in use
6 | a + b
7 | });
| - temporary value is freed at the end of this statement
8 | println!("{}", scores.sum::<i32>());
| ------ borrow later used here
help: consider using a `let` binding to create a longer lived value
|
5 ~ let binding = inputs();
6 ~ let scores = binding.iter().map(|(a, b)| {
|
For more information about this error, try `rustc --explain E0716`.
This is a pretty basic Rust error message about borrowing, but I’ve forgotten everything about Rust so I didn’t understand it.
There are 2 things I didn’t know / wasn’t thinking about here:
thing 1: Variables are semantically meaningful in Rust.
What I mean by that is that this code:
let scores = inputs().iter().map(|(a, b)| { ... };
does not do the same thing as if we factor out inputs() into a variable, in this code:
let input = inputs();
let scores = input.iter().map(|(a, b)| { ... };
If some memory is allocated and it isn’t in its own variable, then it’s freed
at the end of the expression (though there are some exceptions to this
apparently, see rustc --explain E0716 for more). But it does have its own
variable, then it’s kept around until the end of the block.
In the error message the Rust compiler actually suggests an explanation to read
(rustc --explain E0716), which explains all of this and more. I didn’t notice
it right away, but once I read it (and Googled a little), it really helped me.
thing 2:. Computations with iter() don’t happen right away.
This is something that I theoretically knew, but wasn’t thinking about how it might relate to compiler errors.
When I call .map(...), that doesn’t actually do the map right away – it
just sets up an iterator that can do actual calculation later, when we call
.sum().
This means that I need to keep around the memory from inputs(), because none
of the calculation has even happened yet!
error 2: summing an Iterator<()>
Here’s some code (rust playground link) (This isn’t the actual code I was debugging, but it’s the fastest way to demonstrate the error message)
fn main() {
vec![(), ()].iter().sum::<i32>();
}
This has a pretty obvious bug: You can’t sum a bunch of () (the empty type)
and get an i32 as a result. Here’s the compiler error, though:
2 | vec![(), ()].iter().sum::<i32>();
| ^^^^^^^^^^^^^^^^^^^ --- required by a bound introduced by this call
| |
| the trait `Sum<&()>` is not implemented for `i32`
This was very confusing to me – I’d expect to see an error saying something
like Sum is not implemented for Iterator<()>. But instead it says that Sum is
not implemented for i32. But I’m not trying to sum i32s! What’s going on?
What’s actually going on here is (thanks to some lovely people who helped me out!):
i32has a static method calledsum(iter: Iterator<i32>), that comes from theSumtrait. (defined here for integers)Iteratorhas asum()method that calls this static method oni32(defined here)- when I run
my_iter.sum(), it tries to calli32::sum(my_iter) - But
i32::sumisn’t defined forIterator<&()>! - The type parameter in
Sum(eg)Sum<&()>refers to the type of the iterator that’s being passed toi32::sum() - as a result, we get the error message
the trait Sum<&()> is not implemented for i32
I might not have gotten all the types/terms exactly right here, but I think that’s the gist of it.
This was a good reminder that sometimes methods (like sum() on Iterator are
defined in slightly indirect/counterintuitive ways and that you have to hunt
down the details of how it’s implemented to understand the compiler errors.
(my actual bug here was actually that I’d accidentally added an extra ; in my
code, which meant that I accidentally created an Iterator<()> instead of an
Iterator<i32>, and the confusing error message made it harder to figure out that
out)
Rust error messages are cool
I found these error messages pretty helpful, I especially really appreciated the --explain output on the borrowing error.

