April 29, 2019
I had a bad churn this past week.
Churn isn’t something I take that seriously; it’s sad, for sure, but I’m at the point where any individual person churning off of Buttondown feels less acute. I’ve had two accounts do what I’d describe as “positive churn”, which was I suggested they move off of the platform because they were not worth the revenue they were bringing to the table.
This was a bad churn because it was a perfect use case (dev-inclined web shop that wanted minimal branding and an easy subscription widget, willing to pay and be low maintenance) that I completely dropped the ball on due to an aggressive GMail filter, causing me to not respond to them for two weeks and they (rightfully!) asking for a refund and churn. It stings because the thing I try and pride myself on is good customer service, and as that’s been slipping with Buttondown’s scale this feels like a particularly symbolic failure.
So, that was a whammy. Not the biggest whammy in the world, but still.
I had one of the most productive days in a month on Saturday. I pushed out publicly configurable email domains; I knocked through a half-dozen tiny bugs; I got down to inbox zero. This felt really nice, and is reflective of how rare it is for me to just be able to dedicate a full chunk of time to Buttondown, and how productive that effort can be. (My plan is to do a similar thing on Wednesday, which I hope to have similarly positive effects.)
One of the things I used during Saturday’s code-hole was a half-baked abstraction for building out a Serializer based on a Python data-class. The implementation is as follows:
class DataclassSerializer(serializers.Serializer): class Meta: computed_fields = () def get_fields(self): fields = {} for name, type in list(self.Meta.dataclass.__annotations__.items()) + list( self.Meta.computed_fields ): field_arguments = {} if ( hasattr(type, "__origin__") and type.__origin__ == list and type not in DATACLASS_SERIALIZATION_REGISTRY ): field = DATACLASS_SERIALIZATION_REGISTRY[type.__args__[0]] field_arguments["many"] = True else: field = DATACLASS_SERIALIZATION_REGISTRY[type] fields[name] = field(**field_arguments) return fields
This code is extremely useful and nigh-incomprehensible. Buttondown is riddled with little tricky things like this that are actually pretty great, but when I come back to them after six months I have to rediscover how they work. (Even finding this in the codebase was a challenge; I googled for a third-party solution before stumbling into the fact that I had already written my own!)
(I want to get better about surfacing some of these things — open sourcing some of the cool stuff I’ve done in this codebase — but the idea of expanding my surface area of upkeep seems daunting.)
Getting the intense sense of “drowning in bugs”. GitHub is up to 340 open issues; it feels like going from 200 to 300 took two months, tops.
I had three things I wanted to do last week and I’m happy to say I did all of them! (And some additional stuff, too, as aforementioned.)
Well…kinda. I hooked up weeknotes to Connect, and mentioned it in my personal newsletter, but nothing else. I’ll throw it into the monthly updates bucket and see what happens.
My three things this week:
- Do the May updates shenanigans. (I still need to decide on a roadmap for May, which certainly might be “continue to try and pay down debt”, but we’ll find out.)
- DMARC-based warning system.
- Hmm… let’s go with “make non-trivial progress on migrating
Subscriber.user
toSubscriber.newsletter
. (This refactor — a denormalization of the assumption that subscribers correspond with users — is going to be heavy, and will unblock adding multiple newsletters! [God, that UX is going to be awful though.])