<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
 
 <title>Simon Weber</title>
 <link href="http://www.simonmweber.com/atom.xml" rel="self"/>
 <link href="http://www.simonmweber.com/"/>
 <updated>2024-07-20T13:02:55-04:00</updated>
 <id>http://www.simonmweber.com/</id>
 <author>
   <name>Simon Weber</name>
   <email>simon@simonmweber.com</email>
 </author>

 
 <entry>
   <title>Opsgrid</title>
   <link href="http://www.simonmweber.com/2020/09/20/opsgrid.html"/>
   <updated>2020-09-20T00:00:00-04:00</updated>
   <id>http://www.simonmweber.com/2020/09/20/opsgrid</id>
   <content type="html">&lt;h2 id=&quot;opsgrid&quot;&gt;Opsgrid&lt;/h2&gt;

&lt;p class=&quot;meta&quot;&gt;September 20 2020&lt;/p&gt;

&lt;p&gt;I’ve built up a number of &lt;a href=&quot;https://simon.codes/2020/01/16/side-project-income-2019.html&quot;&gt;side projects&lt;/a&gt; over the years.
The ones that require a backend are hosted on individual vms from &lt;a href=&quot;https://my.frantech.ca/aff.php?aff=3397&quot;&gt;BuyVM&lt;/a&gt;, which means I need to monitor about a half-dozen servers.&lt;/p&gt;

&lt;p&gt;While there’s plenty of free Pingdom-style uptime monitoring options out there, cheap options to monitor metrics like cpu, ram, and disk are more rare.
The big names usually have a free tier allowing one or two hosts with minimal data retention, then a big jump to a $10+/month per host paid tier.
I can’t justify spending that when I only pay $2/month for the vms themselves!
There’s also some smaller independent options out there, but they tend to require custom server agents and aren’t all that much cheaper.
So, I decided to build my own.&lt;/p&gt;

&lt;p&gt;I didn’t need anything fancy, just support for the usual metrics, user-defined alerting, and, ideally, reasonable retention of past data so I could identify trends.
Like my other projects, it also needed to be operationally simple.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.opsgrid.net/&quot;&gt;Opsgrid&lt;/a&gt; is the result.
It allows free server/application alerting with about 1 year of data retention per host.
The trick is that Opsgrid doesn’t actually store historical metrics: instead, users connect a Google account, and it pushes data as rows to a Google Sheet per host.
It also makes use of &lt;a href=&quot;https://github.com/influxdata/telegraf&quot;&gt;Telegraf&lt;/a&gt;, an open-source monitoring agent, to avoid dealing with metric collection code.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.opsgrid.net/&quot;&gt;Check it out&lt;/a&gt; if you also run your own servers!
I plan to keep it free unless it attracts a significant number of users, but even then I should be able to price it about 5-10x lower than offerings like New Relic or Datadog.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Side project income 2019</title>
   <link href="http://www.simonmweber.com/2020/01/16/side-project-income-2019.html"/>
   <updated>2020-01-16T00:00:00-05:00</updated>
   <id>http://www.simonmweber.com/2020/01/16/side-project-income-2019</id>
   <content type="html">&lt;h2 id=&quot;side-project-income-2019&quot;&gt;Side project income 2019&lt;/h2&gt;

&lt;p class=&quot;meta&quot;&gt;January 15 2020&lt;/p&gt;

&lt;p&gt;2019 was my fourth year running my own small software business.
I also &lt;a href=&quot;/2019/04/26/leaving-venmo-after-five-years.html&quot;&gt;quit my job&lt;/a&gt; in the spring!
I’m now self-employed but spend most of my time consulting, so I still consider the business to be “on the side”.
Here’s my active projects as of the end of the year:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://autoplaylists.simon.codes/&quot;&gt;Autoplaylists for Google Music&lt;/a&gt;: iTunes Smart Playlists for Google Music.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://gchat.simon.codes/&quot;&gt;Autoresponder for Google Hangouts&lt;/a&gt;: vacation/out-of-office replies for Google Chat.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.kleroteria.org/&quot;&gt;Kleroteria&lt;/a&gt;: an email lottery and spiritual successor to The Listserve.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.repominder.com/&quot;&gt;Repominder&lt;/a&gt;: release reminders for open source projects.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://parks.simon.codes/&quot;&gt;NYC Park Alerts&lt;/a&gt;:  closure notifications about NYC Parks locations.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.plugserv.com/&quot;&gt;Plugserv&lt;/a&gt;: an ad server for my own projects.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;financials&quot;&gt;Financials&lt;/h3&gt;

&lt;p&gt;This was my first down year:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;annual revenue decreased from $3600 to $3330&lt;/li&gt;
  &lt;li&gt;annual expenses grew slightly from $240 to $280&lt;/li&gt;
  &lt;li&gt;monthly recurring revenue dropped substantially from $350 to $250&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Autoplaylists and Autoresponder - which make up nearly all my revenue - both extend Google products that are being shut down, so I had actually expected more of a decline.
While I &lt;a href=&quot;/2019/01/07/side-project-income-2018.html&quot;&gt;began diversifying in 2018&lt;/a&gt;, those efforts haven’t yet impacted the bottom line.&lt;/p&gt;

&lt;h3 id=&quot;new-projects&quot;&gt;New Projects&lt;/h3&gt;

&lt;p&gt;I added two smaller projects last year.
NYC Park Alerts was inspired by my gym habits, of all things.
I work out at a local rec center and showed up a few times to find it unexpectedly closed.
Looking into it, I found that the city posts updates online but with no way to subscribe to them.
So, I set up a scraper that lets people receive emails when locations of their choice are affected.
It’s got one user (besides me) and is more of a community service, so I don’t expect to ever make money off it.&lt;/p&gt;

&lt;p&gt;
My second new project was Plugserv.
It&#39;s a tiny ad server that helps me shamelessly plug my projects across my own sites.
Here&#39;s a live example of what it looks like: &quot;&lt;span id=&quot;example-plug&quot;&gt;&lt;/span&gt;&quot;
Behind the scenes that copy is being retrieved from the Plugserv api, which handles rotation and tracking.
It&#39;s performed better than I expected: in the six months it&#39;s been active it&#39;s served ~25k ads with a click through rate of over 1%, which beats most display ad benchmarks.
Despite working well for me it hasn&#39;t picked up any other users; go &lt;a href=&quot;https://www.plugserv.com/&quot;&gt;check it out&lt;/a&gt; if you&#39;d like to run your own ads on your own sites!
&lt;/p&gt;

&lt;h3 id=&quot;existing-projects&quot;&gt;Existing Projects&lt;/h3&gt;

&lt;p&gt;2019 was also the first year I stopped running any projects.
I’m usually content to let projects run despite low usage because my expenses are so low.
But, I made an exception for Analytics for Google Payments:
despite being free to run and having potential with business customers, it was painful to maintain and targeted a much smaller market than I expected.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.minmaxmeals.com/&quot;&gt;MinMaxMeals&lt;/a&gt;, my cooking site, was more of a case of lost interest.
Its recipes were heavily optimized for time, which isn’t as important to me now that I’m self-employed and work from home.
It’s still a fun talking point, though, and I might return to it someday.&lt;/p&gt;

&lt;p&gt;My remaining projects both grew slowly without my involvement.
Repominder picked up a notable open source power user and Kleroteria added about 500 subscribers.&lt;/p&gt;

&lt;p&gt;Though I didn’t do much marketing or feature work, I did spend some time on operational improvements.
I consolidated my sundry virtual servers into a fleet of $2/month instances at &lt;a href=&quot;https://my.frantech.ca/aff.php?aff=3397&quot;&gt;BuyVM&lt;/a&gt;.
Each runs a new NixOS+docker setup that’s generic enough to be cloned for new projects.
I also upgraded everything to Python 3 and worked out a better way to route my custom-domain emails into Gmail for cheap (which I’ll write up eventually).&lt;/p&gt;

&lt;h3 id=&quot;reflection&quot;&gt;Reflection&lt;/h3&gt;

&lt;p&gt;2019 had its ups and downs, but now that I’m settled into my new lifestyle I’m feeling ready for 2020.
I don’t expect it to be easy, though: Autoplaylists and Autoresponder are likely to shut down completely, and there’s a real chance I’ll find myself in the red.&lt;/p&gt;

&lt;p&gt;So, hopefully my upcoming projects can prevent this.
One of these - hosted server/application monitoring with unusually low operating costs - should launch soon, so sign up below if you’d like to get notified about it!&lt;/p&gt;

&lt;p&gt;As always, thanks for following along and feel free to reach out with questions.
If you’d like to read my previous annual summaries, they’re available &lt;a href=&quot;/annual-summaries&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;script&gt;
  window.plugserv_config = {
    elementId: &#39;example-plug&#39;,
    endpoint: &#39;https://www.plugserv.com/serve/eb7e777e-9ec5-4323-acba-4e05f881cf42&#39;
  };
&lt;/script&gt;

&lt;script async=&quot;&quot; src=&quot;https://www.plugserv.com/js/v1/plugserv.js&quot; integrity=&quot;sha384-Ngv41QqyGqgFyjzQseAmANPgTafxpqZ3fRcQXsShP02KwdzUy9VzIrp/ARgmFEql&quot; crossorigin=&quot;anonymous&quot;&gt;
&lt;/script&gt;

</content>
 </entry>
 
 <entry>
   <title>Plugserv</title>
   <link href="http://www.simonmweber.com/2019/08/03/plugserv.html"/>
   <updated>2019-08-03T00:00:00-04:00</updated>
   <id>http://www.simonmweber.com/2019/08/03/plugserv</id>
   <content type="html">&lt;h2 id=&quot;plugserv&quot;&gt;Plugserv&lt;/h2&gt;

&lt;p class=&quot;meta&quot;&gt;August 3 2019&lt;/p&gt;

&lt;script&gt;
// empty script block so GA doesn&#39;t show up right next to Plugserv.
&lt;/script&gt;

&lt;p&gt;I’ve been creating websites for a while now.
Some of these get consistent visitors, while some are nearly unknown.
At some point I realized there was an opportunity here: my popular sites could advertise my other sites.
So, I started manually adding small text ads to each.&lt;/p&gt;

&lt;p&gt;This quickly became unwieldy.
I’m closing in on ten active projects, and I got sick of updating all of them whenever I launched something new.
I decided my next project would solve this problem.&lt;/p&gt;

&lt;p&gt;I call it &lt;a href=&quot;https://www.plugserv.com/&quot;&gt;Plugserv&lt;/a&gt;, since it lets me shamelessly plug my websites.
It’s an open source ad server: I configure it with all my ads and add a javascript snippet to each site.
Plugserv then rotates my ads automatically and collects basic performance metrics.
Here’s a live example of what this looks like:&lt;/p&gt;

&lt;div id=&quot;plugserv-example&quot;&gt;
&lt;p style=&quot;text-align: center&quot;&gt;
    &lt;span id=&quot;example-plug&quot;&gt;&lt;/span&gt;
&lt;/p&gt;

&lt;script&gt;
  window.plugserv_config = {
      elementId: &#39;example-plug&#39;,
      endpoint: &#39;https://www.plugserv.com/serve/eb7e777e-9ec5-4323-acba-4e05f881cf42&#39;
  };
&lt;/script&gt;

&lt;script async=&quot;&quot; src=&quot;https://www.plugserv.com/js/v1/plugserv.js&quot; integrity=&quot;sha384-Ngv41QqyGqgFyjzQseAmANPgTafxpqZ3fRcQXsShP02KwdzUy9VzIrp/ARgmFEql&quot; crossorigin=&quot;anonymous&quot;&gt;
&lt;/script&gt;
&lt;/div&gt;

&lt;p&gt;Sign up at &lt;a href=&quot;https://www.plugserv.com/&quot;&gt;plugserv.com&lt;/a&gt; to run ads for your own sites!
It’s free to use and &lt;a href=&quot;https://github.com/simon-weber/plugserv&quot;&gt;open source&lt;/a&gt;.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Leaving Venmo after five years</title>
   <link href="http://www.simonmweber.com/2019/04/26/leaving-venmo-after-five-years.html"/>
   <updated>2019-04-26T00:00:00-04:00</updated>
   <id>http://www.simonmweber.com/2019/04/26/leaving-venmo-after-five-years</id>
   <content type="html">&lt;h2 id=&quot;leaving-venmo-after-five-years&quot;&gt;Leaving Venmo after five years&lt;/h2&gt;

&lt;p class=&quot;meta&quot;&gt;April 26 2019&lt;/p&gt;

&lt;p&gt;I left Venmo recently, after an eventful five and a half year stay.
Over that time we increased quarterly payment volume by over 100x, grew from dozens to hundreds of employees, and were both acquired and spun off.
I’d grown too: with the fourth longest tenure at Venmo, I was now leading a team and part of engineering-wide discussions.&lt;/p&gt;

&lt;p&gt;Unsurprisingly, with increased responsibilities came increased stress.
Upper management turnover was especially tough, as I found myself fighting the same political battles under each regime.
My enthusiasm turned to jadedness, then cynicism, then anxiety.
Eventually, despite a supportive manager and a sabbatical, I became concerned for my health and decided to make a change.&lt;/p&gt;

&lt;p&gt;I’m now officially working for myself.
After taking some time to recover, I plan to focus mostly on &lt;a href=&quot;/2019/01/07/side-project-income-2018.html&quot;&gt;my own software&lt;/a&gt; - which I suppose aren’t “side projects” anymore - and occasionally pick up consulting work.
I also look forward to having time for &lt;a href=&quot;https://www.minmaxmeals.com/&quot;&gt;cooking&lt;/a&gt;, &lt;a href=&quot;https://www.twitch.tv/videos/416687233&quot;&gt;music&lt;/a&gt;, and engaging with the ever-growing &lt;a href=&quot;https://www.recurse.com/&quot;&gt;Recurse Center&lt;/a&gt; community.&lt;/p&gt;

&lt;p&gt;Sign up below if you’d like to follow what I’m up to.
Or, shoot me an email if you’re interested in part-time help, particularly around backend scaling, CI/CD, or python.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>NYC Park Alerts</title>
   <link href="http://www.simonmweber.com/2019/03/30/nyc-park-alerts.html"/>
   <updated>2019-03-30T00:00:00-04:00</updated>
   <id>http://www.simonmweber.com/2019/03/30/nyc-park-alerts</id>
   <content type="html">&lt;h2 id=&quot;nyc-park-alerts&quot;&gt;NYC Park Alerts&lt;/h2&gt;

&lt;p class=&quot;meta&quot;&gt;March 30 2019&lt;/p&gt;

&lt;p&gt;I work out at a city &lt;a href=&quot;https://www.nycgovparks.org/facilities/recreationcenters&quot;&gt;rec center&lt;/a&gt;, largely because the price - about $10/month - is hard to beat.&lt;/p&gt;

&lt;p&gt;Schedule consistency is a downside, however.
Due to events and maintenance I’ve sometimes shown up to find my location unexpectedly closed.&lt;/p&gt;

&lt;p&gt;After some research, I found that the city does a pretty good job of &lt;a href=&quot;https://www.nycgovparks.org/news/notices&quot;&gt;posting schedule changes online&lt;/a&gt;.
But, since there’s no way to subscribe to them, you need to check every time before visiting.&lt;/p&gt;

&lt;p&gt;So, I built &lt;a href=&quot;https://parks.simon.codes/&quot;&gt;NYC Park Alerts&lt;/a&gt;.
It lets you subscribe to locations of your choice, and will email you when updates are posted.
Go sign up at &lt;a href=&quot;https://parks.simon.codes/&quot;&gt;parks.simon.codes&lt;/a&gt; if you’re also a rec center fan!&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Side project income 2018</title>
   <link href="http://www.simonmweber.com/2019/01/07/side-project-income-2018.html"/>
   <updated>2019-01-07T00:00:00-05:00</updated>
   <id>http://www.simonmweber.com/2019/01/07/side-project-income-2018</id>
   <content type="html">&lt;h2 id=&quot;side-project-income-2018&quot;&gt;Side project income 2018&lt;/h2&gt;

&lt;p class=&quot;meta&quot;&gt;January 7 2019&lt;/p&gt;

&lt;p&gt;2018 marked my third year running a software business.
While &lt;a href=&quot;/2018/01/09/side-project-income-2017.html&quot;&gt;last year&lt;/a&gt; was focused on growing existing products, this year was more about diversification.
I’m now running six separate projects:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://analytics.simon.codes/&quot;&gt;Analytics for Google Payments&lt;/a&gt;: business metrics for Google merchants.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://autoplaylists.simon.codes/&quot;&gt;Autoplaylists for Google Music&lt;/a&gt;: iTunes Smart Playlists for Google Music.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://gchat.simon.codes/&quot;&gt;Autoresponder for Google Chat and Hangouts&lt;/a&gt;: vacation/out-of-office replies for Google Chat.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.kleroteria.org/&quot;&gt;Kleroteria&lt;/a&gt;: an email lottery and spiritual successor to The Listserve.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.minmaxmeals.com/&quot;&gt;MinMaxMeals&lt;/a&gt;: cheap, healthy, and fast cooking with no regard for convention.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.repominder.com/&quot;&gt;Repominder&lt;/a&gt;: release reminders for open source projects.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;financials&quot;&gt;Financials&lt;/h3&gt;
&lt;p&gt;Autoplaylists and Autoresponder continued to be my only revenue sources.
Here’s the numbers:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;annual revenue increased from $2000 to $3600&lt;/li&gt;
  &lt;li&gt;expenses dropped slightly from $260 to $240&lt;/li&gt;
  &lt;li&gt;revenue run rate grew from $250 to $350, a slight slowdown in growth&lt;/li&gt;
  &lt;li&gt;monthly net subscribers grew from 10 to 15, then dropped under 5&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The subscriber numbers were mostly due to increased Autoplaylists churn once &lt;a href=&quot;https://arstechnica.com/gadgets/2018/05/youtube-music-will-replace-google-play-music-but-wont-kill-user-uploads/&quot;&gt;Google Music’s shutdown&lt;/a&gt; was announced.
Autoresponder growth was steady, but not enough to compensate.&lt;/p&gt;

&lt;p&gt;To reduce my exposure to future Google changes, I worked on two new independent projects.&lt;/p&gt;

&lt;h3 id=&quot;new-projects&quot;&gt;New Projects&lt;/h3&gt;
&lt;p&gt;Over the summer I launched Kleroteria in response to the shutdown of &lt;a href=&quot;https://thelistservearchive.com/&quot;&gt;The Listserve&lt;/a&gt;.
Both work the same way: people subscribe to an email list, and periodically one person is chosen to write to everyone else.
My take mostly differs behind the scenes, with improved automation and free hosting (it &lt;a href=&quot;/2018/07/09/running-kleroteria-for-free-by-abusing-free-tiers.html&quot;&gt;runs on 13 free tiers&lt;/a&gt;, which was a fun technical exercise).&lt;/p&gt;

&lt;p&gt;I was hoping to take over The Listserve’s 10k subscribers, but they had already deleted their email list by the time I reached out.
Instead I settled for cold-emailing previous winners who’d provided their emails.
This yielded about 500 subscribers from 2000 prospects.
While this is a decent start, there’s no real revenue potential without much more growth.
So, I hope to do some marketing in 2019, then look into options like Patreon or merchandise.&lt;/p&gt;

&lt;p&gt;My second project was a bit of a departure: MinMaxMeals is my first content site.
It launched a month ago, and has picked up a few dozen email subscribers and twitter followers so far.
I plan to put consistent effort into writing and marketing over the next year.
There’s a variety of revenue options if it gets traction, like affiliate links, sponsorships, or a cookbook.&lt;/p&gt;

&lt;h3 id=&quot;existing-projects&quot;&gt;Existing Projects&lt;/h3&gt;
&lt;p&gt;Autoplaylists and Autoresponder each received maintenance and some minor feature work.
Notably, this included handling &lt;a href=&quot;https://us14.campaign-archive.com/?u=cd59835a4c0a0f502ac0bc803&amp;amp;id=2645857509&quot;&gt;a small crisis&lt;/a&gt; while at a friend’s wedding.
I also attempted a few Facebook ad campaigns to little success.&lt;/p&gt;

&lt;p&gt;On the positive side, I made good progress on Analytics for Google Payments.
A reverse engineering breakthrough led to support for new data and a public beta.
It’s now got its first 10 users, largely from a cold-email campaign I ran against owners of paid Chrome extensions.
That also had the side effect of sizing the market: as it turns out, there a very few paid chrome extensions.
So, I may look into supporting other merchant types (like Android or YouTube) to expand it.
I’m a bit hesitant given its coupling to Google, however.&lt;/p&gt;

&lt;p&gt;Finally, poor Repominder got little attention aside from a move to free hosting.
I’m not sure it’s worth investing in now that GitHub seems to have stepped up development of similar features.&lt;/p&gt;

&lt;h3 id=&quot;reflection&quot;&gt;Reflection&lt;/h3&gt;
&lt;p&gt;I’m pleased to have diversified in 2018, even if my new projects aren’t yet making money.
I’m also proud of how much I got done – especially as my responsibilities at my day job increased.&lt;/p&gt;

&lt;p&gt;Here’s hoping 2019 is similarly successful!
Follow along by signing up below, and feel free to reach out with questions.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>MinMaxMeals: cheap, healthy, and fast cooking</title>
   <link href="http://www.simonmweber.com/2018/12/03/minmaxmeals.html"/>
   <updated>2018-12-03T00:00:00-05:00</updated>
   <id>http://www.simonmweber.com/2018/12/03/minmaxmeals</id>
   <content type="html">&lt;h2 id=&quot;minmaxmeals-cheap-healthy-and-fast-cooking&quot;&gt;MinMaxMeals: cheap, healthy, and fast cooking&lt;/h2&gt;

&lt;p class=&quot;meta&quot;&gt;December 3 2018&lt;/p&gt;

&lt;p&gt;In a departure from my typical projects, I’ve launched a cooking site!
&lt;a href=&quot;https://www.minmaxmeals.com/&quot;&gt;MinMaxMeals&lt;/a&gt; disregards culinary convention in favor of cost, health, and speed
– typical meals call for $1 of ingredients and a few minutes in a microwave.&lt;/p&gt;

&lt;p&gt;Here’s a bit on its origins from &lt;a href=&quot;https://www.minmaxmeals.com/about/&quot;&gt;the about page&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;While today I cook this way because it’s practical, it was originally out of necessity.
I had suddenly become ill, and was put on a restricted diet to figure out what foods I could tolerate.
Prepping all my own meals lead to my focus on speed, cost, and nutrition, while the limited ingredients ruled out most familiar dishes.
As my health returned I added more ingredients, and later - after my cooking became a conversation starter - I decided to share what I’d learned.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If this sounds intriguing, check out my recipe for &lt;a href=&quot;https://www.minmaxmeals.com/recipes/garlic-oatmeal/&quot;&gt;oatmeal that tastes like garlic bread&lt;/a&gt;.
Or, read about &lt;a href=&quot;https://www.minmaxmeals.com/ingredients/lentils/&quot;&gt;the different types of lentils&lt;/a&gt; and how they’re often cheaper under different names.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Running Kleroteria for free by (ab)using free tiers</title>
   <link href="http://www.simonmweber.com/2018/07/09/running-kleroteria-for-free-by-abusing-free-tiers.html"/>
   <updated>2018-07-09T00:00:00-04:00</updated>
   <id>http://www.simonmweber.com/2018/07/09/running-kleroteria-for-free-by-abusing-free-tiers</id>
   <content type="html">&lt;h2 id=&quot;running-kleroteria-for-free-by-abusing-free-tiers&quot;&gt;Running Kleroteria for free by (ab)using free tiers&lt;/h2&gt;

&lt;p class=&quot;meta&quot;&gt;July 9 2018&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.kleroteria.org/&quot;&gt;Kleroteria&lt;/a&gt; is an email lottery: periodically, a random subscriber is chosen to write to everyone else.
I built it to replace The Listserve, which recently shut down after providing years of advice, hundreds of personal stories, and one impromptu picnic.
It was a fun and personal corner of the internet that I missed immediately.&lt;/p&gt;

&lt;p&gt;I suspect the creators of The Listserve just got tired of running it.
First, it was high maintenance: the lottery and post sending was done by hand.
Second, it was expensive: it was powered by MailChimp, which would have cost hundreds of dollars per month given their tens of thousands of subscribers.&lt;/p&gt;

&lt;p&gt;I’d need to address these in my replacement.
I figured I could easily run it for little more than the cost of sending emails.
But could I host the entire thing for free?
I wasn’t sure, and figuring it out quickly became a personal challenge.&lt;/p&gt;

&lt;p&gt;After far too long digging through pricing docs, I came up with a serverless setup run on AWS’s indefinitely free tier.
It’s illustrative of just how much Amazon gives away, but also how quickly a serverless architecture can get out of hand.&lt;/p&gt;

&lt;p&gt;This post lays out my ridiculous thirteen-service setup to inspire future free tier shenanigans.&lt;/p&gt;

&lt;h3 id=&quot;infrastructure-shopping&quot;&gt;Infrastructure shopping&lt;/h3&gt;

&lt;p&gt;My other side projects are run unceremoniously on cheap virtual private servers.
&lt;a href=&quot;https://gchat.simon.codes/&quot;&gt;Autoresponder&lt;/a&gt; and &lt;a href=&quot;https://www.repominder.com/&quot;&gt;Repominder&lt;/a&gt;, for example, run on &amp;lt;$1/month deals from lowendbox.
A comparable free offering is Google’s f1-micro instance, though I was concerned about bottlenecking on vcpu during signup spikes.&lt;/p&gt;

&lt;p&gt;So, I started looking at serverless options since the free tiers seemed generous.
GCP looked promising, but Google’s free email offering was limited to a few thousand sends/month.
I considered Azure as well, but was turned off by the lack of a free hosted database.
AWS ended up providing roughly the same pieces as Google, but with the benefit of 62k free sends/month through SES.&lt;/p&gt;

&lt;p&gt;Things were starting to take shape.
I could use Lambda for the backend, DynamoDB for storage (RDS is only free for a year), and SES for email.
Most of the service limits weren’t an issue - I’ll never need 25GB of storage - but Dynamo throughput was a concern.
It’s measured in “capacity units” which boil down to about 25 writes/second and 50 eventually-consistent reads/second.
I needed to make sure I could stay under these limits.&lt;/p&gt;

&lt;h3 id=&quot;dynamo-math&quot;&gt;Dynamo math&lt;/h3&gt;

&lt;p&gt;Kleroteria stores two things: posts and subscribers.
I decided to use random ids to prevent hot partitions.
The full schemas ended up looking like this:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;pending_posts table:
    &lt;ul&gt;
      &lt;li&gt;key: randomly-generated id&lt;/li&gt;
      &lt;li&gt;values: contents and status&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;subscriber table:
    &lt;ul&gt;
      &lt;li&gt;key: randomly-generated id&lt;/li&gt;
      &lt;li&gt;values: email address&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Post contents are capped such that their rows are less than 4kb.
Subscriber rows, on the other hand, are only ~32b on average.
These row sizes are important, since capacity units are measured in 1kb chunks for writes and 4kb chunks for reads.&lt;/p&gt;

&lt;p&gt;For example, consider sending out a post.
This requires a full scan of the subscriber table.
With a table of 10k rows, each under 32b, read in perfect 4kb chunks, a sustained scan at one read capacity unit would require about 40 seconds.
Using 5 rcus, it’d take ~8 seconds.
Similarly, 100k subscribers could be handled in ~20 seconds with 20 rcus.&lt;/p&gt;

&lt;p&gt;Selecting a lottery winner is roughly the same thing: a full scan, then random selection.
Originally, I avoided the scan by using a composite primary key.
This involved a two-level index - partitioned by the first character of the id and sorted by the full id - and could select a random item in under two queries.
It turned out not to be worth the trouble, though, since its distribution was biased and the AWS limit of 25 rcus allows for fast enough scans.&lt;/p&gt;

&lt;p&gt;The other operations are more straightforward.
Subscribing and unsubscribing, for example, cost 1 wcu each.
Processed synchronously, though, this could cause an issue: beyond 25 signups/second I’d start getting throttled (or worse, charged).
I needed a way to spread out operations during spikes.&lt;/p&gt;

&lt;h3 id=&quot;throttling-under-service-limits&quot;&gt;Throttling under service limits&lt;/h3&gt;

&lt;p&gt;Kleroteria has the luxury of being entirely asynchronous, so I connected everything with SQS queues.
This allows precise throttling by adjusting the queue polling rate.
For example, if subscriptions were sent through a queue consumed at 1 message/second, it’d never exceed 1 wcu.&lt;/p&gt;

&lt;p&gt;Dynamo’s capacity units aren’t the only limit in play, though.
SQS and Lambda both allow only one million free requests/month.
Lambda also constrains runtime and memory usage, and SES has a sending rate limit.
To fit within all of these, I’m currently running:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;1 scheduled lambda execution / minute&lt;/li&gt;
  &lt;li&gt;30 seconds / lambda execution&lt;/li&gt;
  &lt;li&gt;10 messages / SQS poll&lt;/li&gt;
  &lt;li&gt;10 second wait / SQS poll&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This puts me at ~40k executions/month, ~120k polls/month, ~1 wcu, and ~1 email/second, which is pretty conservative.
I plan to increase the message limit once it’s been live for a while.&lt;/p&gt;

&lt;p&gt;Putting it all together, the pieces involved in subscription handling look like this:&lt;/p&gt;

&lt;p&gt;
&lt;pre style=&quot;font-family: monospace&quot;&gt;
      browser (AWS sdk + Cognito) --&amp;gt; SQS                     
                                       |                      
                                       v                      
      CloudWatch events -----------&amp;gt; Lambda --&amp;gt; Dynamo and SES
&lt;/pre&gt;
&lt;/p&gt;

&lt;p&gt;Note that the frontend enqueues directly to SQS.
This avoids API Gateway (which isn’t free) and per-request Lambda executions.
It comes with a cost, though: javascript must be enabled to sign up, and I can’t give users feedback from serverside validation.
I’m hoping this doesn’t matter too much in practice.&lt;/p&gt;

&lt;h3 id=&quot;everything-else&quot;&gt;Everything else&lt;/h3&gt;

&lt;p&gt;Astute readers will count six services so far, while I promised thirteen.
The seventh is IAM, which is used for internal access control.
It’s always free since it’s an essential part of AWS.&lt;/p&gt;

&lt;p&gt;The remaining six services aren’t from AWS.
One is netlify, used to host the frontend assets.
I’ve found it comparable to GitHub Pages - my usual choice - though I’ve noticed surprisingly flaky uptime according to New Relic Synthetics, my external monitoring tool.&lt;/p&gt;

&lt;p&gt;The remaining services play supporting roles and don’t require much commentary:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Sentry: error reporting (frontend + lambda)&lt;/li&gt;
  &lt;li&gt;Google Analytics: frontend analytics&lt;/li&gt;
  &lt;li&gt;BitBucket: private git repo&lt;/li&gt;
  &lt;li&gt;CloudFlare: dns (with cdn disabled, since netlify provides one)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;whew&quot;&gt;Whew&lt;/h3&gt;

&lt;p&gt;So, there you have it: thirteen services, tens of thousands of subscribers, $0 per month.
Was it worth it?
Probably not, at least in the context of a side project.
Reading pricing docs, planning Dynamo capacity, and setting up a local environment added days to what should have been a weekend project.
That said, it was a fun challenge and the result is more robust than my usual vps setups.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://www.kleroteria.org/&quot;&gt;Go join Kleroteria&lt;/a&gt; so I can justify my effort!&lt;/strong&gt;
If you’re interested in hearing more about it - or getting notified if I open source it - consider subscribing with the links below.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Analytics for Google Payments: now in public beta</title>
   <link href="http://www.simonmweber.com/2018/03/21/analytics-for-google-payments-public-beta.html"/>
   <updated>2018-03-21T00:00:00-04:00</updated>
   <id>http://www.simonmweber.com/2018/03/21/analytics-for-google-payments-public-beta</id>
   <content type="html">&lt;h2 id=&quot;analytics-for-google-payments-now-in-public-beta&quot;&gt;Analytics for Google Payments: now in public beta&lt;/h2&gt;

&lt;p class=&quot;meta&quot;&gt;March 21 2018&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://analytics.simon.codes&quot;&gt;Analytics for Google Payments&lt;/a&gt;,
a Chrome Extension to help Google merchants make sense of their business metrics,
is now in public beta.&lt;/p&gt;

&lt;p&gt;To find out more and try it out, head to the new product site at &lt;a href=&quot;https://analytics.simon.codes&quot;&gt;analytics.simon.codes&lt;/a&gt;!&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Autoplaylists 2017 user survey results</title>
   <link href="http://www.simonmweber.com/2018/03/11/autoplaylists-user-survey-results.html"/>
   <updated>2018-03-11T00:00:00-05:00</updated>
   <id>http://www.simonmweber.com/2018/03/11/autoplaylists-user-survey-results</id>
   <content type="html">&lt;h2 id=&quot;autoplaylists-2017-user-survey-results&quot;&gt;Autoplaylists 2017 user survey results&lt;/h2&gt;

&lt;p class=&quot;meta&quot;&gt;March 11 2018&lt;/p&gt;

&lt;p&gt;In early 2018 I sent out a user survey for &lt;a href=&quot;https://autoplaylists.simon.codes&quot;&gt;Autoplaylists for Google Music&lt;/a&gt;.
My goal was to get a general idea of user satisfaction, as well as figure out what I should focus on next.
Here’s a quick summary of the results:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;~30% response rate from mailing list, representing ~5% of daily actives&lt;/li&gt;
  &lt;li&gt;~6:4 representation of free vs paying users&lt;/li&gt;
  &lt;li&gt;on average, users were likely to recommend it (NPS of 15)&lt;/li&gt;
  &lt;li&gt;on average, users found it reliable (4+ on 5-point scale of “never works” to “almost always works”)&lt;/li&gt;
  &lt;li&gt;common themes in feedback were:
    &lt;ul&gt;
      &lt;li&gt;syncing is opaque&lt;/li&gt;
      &lt;li&gt;complex autoplaylist definitions are difficult to manage&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I don’t want to read too much into this - especially with a low response rate and over-representation of paying users - but in general I’m pleased with the results.
The reliability data is particularly relieving, since I had my doubts going by my user support work.&lt;/p&gt;

&lt;p&gt;I’m hoping to address both the common pain points this year by:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;adding a syncing dashboard with past syncs and their results&lt;/li&gt;
  &lt;li&gt;adding a way to share common parts of playlists definitions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, here’s to a successful 2018!
I’m excited to run a similar survey next year now that I have results to compare against.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Analytics for the Google Payments Center: accounting data now supported</title>
   <link href="http://www.simonmweber.com/2018/01/26/analytics-google-payments-center-accounting-upgrade.html"/>
   <updated>2018-01-26T00:00:00-05:00</updated>
   <id>http://www.simonmweber.com/2018/01/26/analytics-google-payments-center-accounting-upgrade</id>
   <content type="html">&lt;h2 id=&quot;analytics-for-the-google-payments-center-accounting-data-now-supported&quot;&gt;Analytics for the Google Payments Center: accounting data now supported&lt;/h2&gt;

&lt;p class=&quot;meta&quot;&gt;January 26 2018&lt;/p&gt;

&lt;p&gt;&lt;em&gt;tl;dr &lt;a href=&quot;https://analytics.simon.codes&quot;&gt;install the extension here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Last year I launched the beta of &lt;a href=&quot;https://analytics.simon.codes&quot;&gt;Analytics for the Google Payments&lt;/a&gt;,
a Chrome Extension to help Google merchants make sense of their business metrics.
Today I’m announcing the first major change in a while: full support for accounting data.&lt;/p&gt;

&lt;p&gt;Basically, Analytics used to only support order data.
This was useful for tracking subscription metrics like net new subscribers or churn, but didn’t help with accounting.
Now, Analytics has access to every transaction, earnings report and VAT invoice, meaning it can answer questions like:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;what’s my growth in profit over time?&lt;/li&gt;
  &lt;li&gt;how much sales tax did I collect in the last reporting period?&lt;/li&gt;
  &lt;li&gt;how much am I paying in Google fees?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s an example graph:&lt;/p&gt;

&lt;div class=&quot;figure&quot;&gt;
&lt;p&gt;&lt;img src=&quot;/images/gpc_earnings.png&quot; alt=&quot;the new earnings graph&quot; /&gt;&lt;/p&gt;
&lt;p&gt;A screenshot of the new earnings graph. Each bar is a monthly breakdown with the trend line showing net growth.&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;I’m also looking into supporting bulk exports of this new data.
This would combine monthly documents like earnings reports for download all at once, rather than one at a time.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Side project income 2017</title>
   <link href="http://www.simonmweber.com/2018/01/09/side-project-income-2017.html"/>
   <updated>2018-01-09T00:00:00-05:00</updated>
   <id>http://www.simonmweber.com/2018/01/09/side-project-income-2017</id>
   <content type="html">&lt;h2 id=&quot;side-project-income-2017&quot;&gt;Side project income 2017&lt;/h2&gt;

&lt;p class=&quot;meta&quot;&gt;January 9 2018&lt;/p&gt;

&lt;p&gt;2017 was my second year of running a small software business.
Unlike my first year - which &lt;a href=&quot;/2017/01/09/side-project-income-2016-0-to-100.html&quot;&gt;you should read about first&lt;/a&gt; if you haven’t already - I didn’t launch any new paid products.
Instead, I spent most of my time experimenting with my existing products: &lt;a href=&quot;https://autoplaylists.simon.codes&quot;&gt;Autoplaylists for Google Music&lt;/a&gt; and &lt;a href=&quot;https://gchat.simon.codes&quot;&gt;Autoresponder for Google Chat&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In this post I’ll go over what I worked on and how it turned out.&lt;/p&gt;

&lt;h3 id=&quot;financials&quot;&gt;Financials&lt;/h3&gt;
&lt;p&gt;Here’s an overview of the business on paper:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;annual revenue increased from $600 to $2000&lt;/li&gt;
  &lt;li&gt;annual expenses decreased slightly from $300 to $260&lt;/li&gt;
  &lt;li&gt;monthly revenue run rate increased from $100 to $250&lt;/li&gt;
  &lt;li&gt;monthly net subscribers increased from 5 to 10&lt;/li&gt;
  &lt;li&gt;monthly cancelled/churned subscriptions also increased from 5 to 10&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Out of these, the churn rate stands out.
I’ve already started collecting feedback from cancelled Autoresponder users, but have yet to put similar effort into Autoplaylists.&lt;/p&gt;

&lt;h3 id=&quot;product-development&quot;&gt;Product Development&lt;/h3&gt;

&lt;p&gt;Part of my coding effort went into two new yet-to-be-monetized projects.&lt;/p&gt;

&lt;p&gt;The first is &lt;a href=&quot;https://analytics.simon.codes&quot;&gt;Analytics for Google Payments&lt;/a&gt;, which helps merchants calculate business metrics like the ones I presented earlier.
Motivated by my frustration getting that data for Autoplaylists, it’s also similar behind the scenes:
both are Chrome Extensions using reverse-engineered apis.
While the market is crowded - Baremetrics and ChartMogul come to mind - nobody has bothered with Google Payments because of the lack of official apis.
Hopefully that continues to be the case.
If Google launches an api for that data, I’ll probably just let the big players fight it out.&lt;/p&gt;

&lt;p&gt;My second new project is &lt;a href=&quot;https://www.repominder.com&quot;&gt;Repominder&lt;/a&gt;.
It’s a SaaS that notifies forgetful open source maintainers (like me) when a release is needed.
This has a less obvious path to profit than Analytics, but may have potential if marketed towards software businesses.&lt;/p&gt;

&lt;p&gt;Outside of my new projects my programming time went towards maintenance.
Based on my analytics data and customer support interactions I chose to focus on features for Autoresponder and reliability for Autoplaylists.&lt;/p&gt;

&lt;h3 id=&quot;growth-and-other-work&quot;&gt;Growth and Other Work&lt;/h3&gt;

&lt;p&gt;One of my major growth goals was to set up mailing lists.
I’m pleased to have achieved that: I now have one for each product (and this blog!) combining for a reach of 500+ and 1+ per day growth.
They’re used for announcements right now, but I may experiment with drip campaigns in the future.&lt;/p&gt;

&lt;p&gt;Along with a mailing list, Autoplaylists got a payment plan tweak late in the year.
It now includes a one-week free trial in addition to the existing one-autoplaylist freemium model.
My goal was to give users a chance to try features that only work with multiple autoplaylists.
While I haven’t noticed an effect on the bottom line yet, new user engagement has gone up since.&lt;/p&gt;

&lt;p&gt;Autoplaylists also saw my first experiment with paid advertising, after an ad blocker mishap led me to realize that Google Music serves desktop text ads.
This was about as targeted as possible, so I had high hopes.
One month and plenty of AdWords fiddling later I had generated about 20k impressions, 40 clicks, and 4 installs.
With the average cost per click at $2.31, this meant the cost of a new user was over $20.
Even assuming 100% conversion this was already above my customer lifetime value, so I stopped the campaign.&lt;/p&gt;

&lt;p&gt;In another marketing attempt I tried for earned media coverage.
Unfortunately, unlike in 2016 I didn’t hear back from anyone I reached out to.
I don’t blame them: with no launch announcements I lacked a solid pitch.&lt;/p&gt;

&lt;p&gt;My last bit of notable business work was when I noticed an idea of mine had already been built.
It seemed abandoned, so I contacted the owner to ask about buying it.
This didn’t pan out either, as they had plans to revitalize it.
Still, I feel good about the time spent researching and getting advice, since I expect to be in this situation again someday (maybe even as a seller).&lt;/p&gt;

&lt;h3 id=&quot;looking-forward&quot;&gt;Looking Forward&lt;/h3&gt;

&lt;p&gt;Overall, I’m pleased with what I got done in 2017.
My biggest disappointment was the lack of a new paid product.
However, I’m set up to address that next year with my untapped projects and new product ideas.&lt;/p&gt;

&lt;p&gt;I’m also eager to pursue businesses as customers next year.
Autoresponder is particularly ripe for this, since I’ve noticed a user pattern of a) small business owners and b) employees of larger companies.
I haven’t reached out to them yet, but I suspect they’d be willing to pay more for added features.&lt;/p&gt;

&lt;p&gt;So, here’s to a successful 2018!
Follow along by signing up below, and feel free to reach out with questions.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Repominder</title>
   <link href="http://www.simonmweber.com/2017/12/09/repominder.html"/>
   <updated>2017-12-09T00:00:00-05:00</updated>
   <id>http://www.simonmweber.com/2017/12/09/repominder</id>
   <content type="html">&lt;h2 id=&quot;repominder&quot;&gt;Repominder&lt;/h2&gt;

&lt;p class=&quot;meta&quot;&gt;December 12 2017&lt;/p&gt;

&lt;p&gt;I maintain a number of open source projects.
Too often, something like this would happen:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;I merge a pull request&lt;/li&gt;
  &lt;li&gt;I promptly forget about it&lt;/li&gt;
  &lt;li&gt;weeks later, the changes haven’t reached a package registry&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, I made &lt;a href=&quot;https://www.repominder.com&quot;&gt;Repominder&lt;/a&gt; to help OSS maintainers avoid this situation.
Now when I forget about changes, I get a weekly email with diffs of affected projects.
It also provides a badge showing the status of my projects (&lt;a href=&quot;https://github.com/simon-weber/Autoplaylists-for-Google-Music/#autoplaylists-for-google-music&quot;&gt;eg&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Feel free to &lt;a href=&quot;https://www.repominder.com&quot;&gt;sign up and monitor your own projects&lt;/a&gt;!
It’s &lt;a href=&quot;https://github.com/simon-weber/repominder&quot;&gt;open source&lt;/a&gt;, and likely to remain free unless hosting becomes annoying.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>tabs.executeScript as a content script alternative in Chrome Extensions</title>
   <link href="http://www.simonmweber.com/2017/07/17/executescript-content-script-alternative-chrome-extension.html"/>
   <updated>2017-07-17T00:00:00-04:00</updated>
   <id>http://www.simonmweber.com/2017/07/17/executescript-content-script-alternative-chrome-extension</id>
   <content type="html">&lt;h2 id=&quot;tabsexecutescript-as-a-content-script-alternative-in-chrome-extensions&quot;&gt;tabs.executeScript as a content script alternative in Chrome Extensions&lt;/h2&gt;

&lt;p class=&quot;meta&quot;&gt;July 17 2017&lt;/p&gt;
&lt;p&gt;I just updated &lt;a href=&quot;https://autoplaylists.simon.codes&quot;&gt;Autoplaylists for Google Music&lt;/a&gt; so it uses &lt;code class=&quot;highlighter-rouge&quot;&gt;chrome.tabs.executeScript&lt;/code&gt; instead of a manifest content script.
It’s only been a few days, but so far it’s halved sync problems.
Since I didn’t find similar recommendations elsewhere, this post explains what motivated the change and how I went about it.&lt;/p&gt;

&lt;p&gt;First, some context on Autoplaylists.
It’s a Chrome Extension that adds iTunes-style “smart playlists” to Google Music.
It runs entirely in the browser and makes money from a freemium subscription model (there’s more details in &lt;a href=&quot;/2016/07/11/launching-a-chrome-extension-part-1-taxes-and-legal.html&quot;&gt;my&lt;/a&gt; &lt;a href=&quot;/2016/07/18/launching-a-chrome-extension-part-2-analytics-and-error-reporting.html&quot;&gt;business-focused&lt;/a&gt; &lt;a href=&quot;/2017/01/09/side-project-income-2016-0-to-100.html&quot;&gt;posts&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Since it uses unofficial apis, Autoplaylists depends on communication with a running Google Music tab.
Specifically, it needs to:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;retrieve the Google Music user id at startup&lt;/li&gt;
  &lt;li&gt;retrieve the cached library from Google’s IndexedDB at startup&lt;/li&gt;
  &lt;li&gt;retrieve Google’s xsrf cookie at startup, and on-demand (if it expires)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To do this, Autoplaylists previously used a single long-running content script which sent its tab id to the background script after loading.
The background script would use that tab id for any on-demand messages.&lt;/p&gt;

&lt;p&gt;This caused a few problems.
First, a tab refresh was required after an install, upgrade, or reload, since the background script would lose the tab id and message passing channel.
This was the worst issue: it’d interrupt syncing for no apparent reason, causing symptoms like &lt;a href=&quot;https://github.com/simon-weber/Autoplaylists-for-Google-Music/issues/157&quot;&gt;these&lt;/a&gt;.
Second, the background script had no way of knowing when tabs closed without messaging them.
On-demand messages often ended up going into the void.&lt;/p&gt;

&lt;p&gt;My new setup fixes these problems.
The notable changes are:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;the background script has &lt;code class=&quot;highlighter-rouge&quot;&gt;tabs&lt;/code&gt; permission&lt;/li&gt;
  &lt;li&gt;at startup or when detecting a tab opening, the background script will:
    &lt;ul&gt;
      &lt;li&gt;create a unique script id&lt;/li&gt;
      &lt;li&gt;add a new message listener that detaches itself after receiving a response with the script id&lt;/li&gt;
      &lt;li&gt;executeScript the former content script code (adapted for one-time use)&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;the content script code sends responses as before, but includes the script id&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This moves the responsibility of running page code from Chrome into the background script.
The added flexibility allows startup-time detection of tabs (after installs/upgrades/reloads), garbage collection of page code after one use, and easier handling of closed tabs for on-demand messages.&lt;/p&gt;

&lt;p&gt;It’s not a perfect solution.
Aside from the added &lt;a href=&quot;https://github.com/simon-weber/Autoplaylists-for-Google-Music/blob/master/src/js/page.js&quot;&gt;complexity of script ids and dynamic event listeners&lt;/a&gt;, adding the tabs permission turned out to be costly.
Unexpectedly, it’s presented to users as allowing the extension to “view your browsing activity”, which is easily misinterpreted as access to history.
Furthermore, Chrome prompts users for all the extension’s permissions when just adding one.
The result was a lot of scary prompts and a few negative reviews.
I’m working on setting up a mailing list so I can get in front of changes like these next time.&lt;/p&gt;

&lt;p&gt;Overall, though, the costs have been worth it.
I’d recommend executeScript as a manifest content script alternative for extensions with long-running background scripts and on-demand page communication requirements.
In case it’s helpful, you can find most of my relevant code in &lt;a href=&quot;https://github.com/simon-weber/Autoplaylists-for-Google-Music/blob/master/src/js/page.js&quot;&gt;page.js&lt;/a&gt;, and the injected code in &lt;a href=&quot;https://github.com/simon-weber/Autoplaylists-for-Google-Music/blob/master/src/js/querypage.js&quot;&gt;querypage.js&lt;/a&gt;.
As always, feel free to reach out on GitHub or via email with questions.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Analytics for the Google Payments Center</title>
   <link href="http://www.simonmweber.com/2017/03/18/google-payments-center-analytics-extension.html"/>
   <updated>2017-03-18T00:00:00-04:00</updated>
   <id>http://www.simonmweber.com/2017/03/18/google-payments-center-analytics-extension</id>
   <content type="html">&lt;h2 id=&quot;analytics-for-the-google-payments-center&quot;&gt;Analytics for the Google Payments Center&lt;/h2&gt;

&lt;p class=&quot;meta&quot;&gt;March 18 2017&lt;/p&gt;

&lt;p&gt;&lt;em&gt;tl;dr &lt;a href=&quot;https://analytics.simon.codes&quot;&gt;install the extension here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you receive money from Google you’re surely familiar with the Google Payments Center.
I deal with it in the context of my subscription Chrome Extension &lt;a href=&quot;https://autoplaylists.simon.codes&quot;&gt;Autoplaylists for Google Music&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Its main interface is a list of orders:&lt;/p&gt;
&lt;div class=&quot;figure&quot;&gt;
&lt;p&gt;&lt;img src=&quot;/images/gpc_orders_redacted.png&quot; alt=&quot;example Google Payments Center orders&quot; /&gt;&lt;/p&gt;
&lt;p&gt;A screenshot from my account with full order numbers removed.&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;This is largely useless for answering business reporting questions such as:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;how many new users purchased this month?&lt;/li&gt;
  &lt;li&gt;how many subscriptions churned this month?&lt;/li&gt;
  &lt;li&gt;what’s the net growth in subscriptions over time?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, I’ve created an analytics Chrome Extension to calculate these numbers.
It lets me answer these questions at a glance, like Baremetrics or ChartMogul do for other processors.
Here’s the graph it generates for me:&lt;/p&gt;
&lt;div class=&quot;figure&quot;&gt;
&lt;p&gt;&lt;img src=&quot;/images/gpc_graph.png&quot; alt=&quot;the graph generated from my orders&quot; /&gt;&lt;/p&gt;
&lt;p&gt;A screenshot of the extension. Each bar is a monthly breakdown with the trend line showing net growth.&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;While it’s still rough around the edges, it’s already my favorite way to look at order trends.
So, I’m opening it up for a free private beta.
&lt;strong&gt;If you’d like to try it out, &lt;a href=&quot;https://analytics.simon.codes&quot;&gt;install it here&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here’s a few more details:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;your order information never leaves your own computer&lt;/li&gt;
  &lt;li&gt;subscription products will generate the best graph, but one-time purchases, in-app purchases, Android Apps, Youtube Fan Funding, ad payments, etc will also work&lt;/li&gt;
  &lt;li&gt;more metrics are in the works, such as monthly recurring revenue (MRR), failed charges, and life time value (LTV)&lt;/li&gt;
  &lt;li&gt;I’m interesting in addressing other pain points as well, such as bulk exporting of statements&lt;/li&gt;
  &lt;li&gt;I plan to charge after leaving beta&lt;/li&gt;
&lt;/ul&gt;
</content>
 </entry>
 
 <entry>
   <title>Side project income 2016: 0 to $100/month</title>
   <link href="http://www.simonmweber.com/2017/01/09/side-project-income-2016-0-to-100.html"/>
   <updated>2017-01-09T00:00:00-05:00</updated>
   <id>http://www.simonmweber.com/2017/01/09/side-project-income-2016-0-to-100</id>
   <content type="html">&lt;h2 id=&quot;side-project-income-2016-0-to-100month&quot;&gt;Side project income 2016: 0 to $100/month&lt;/h2&gt;

&lt;p class=&quot;meta&quot;&gt;January 9 2017, updated March 18 2017&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you’re selling an extension, check out my project &lt;a href=&quot;https://analytics.simon.codes&quot;&gt;Analytics for Google Payments&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In 2016, I set a goal to earn money from software I built.
Being an employee or contractor wouldn’t count.
I wanted the product and business experience of making something from the ground up.&lt;/p&gt;

&lt;p&gt;I’m pleased to have achieved this: as of December, my projects net about $100/month of (mostly) passive income.
This post will lay out some of what I’ve learned and hopefully help others with similar goals.&lt;/p&gt;

&lt;h3 id=&quot;the-products&quot;&gt;The Products&lt;/h3&gt;

&lt;p&gt;I launched two consumer subscription products last year.
They are:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://autoplaylists.simon.codes&quot;&gt;Autoplaylists for Google Music&lt;/a&gt;: iTunes-style smart playlists for Google Music, a $2/month Chrome Extension&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://gchat.simon.codes&quot;&gt;Autoresponder for Google Chat&lt;/a&gt;: hosted auto-replies for Google Chat/Hangouts, a $2/month SaaS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why did I pick these particular products?
Three reasons:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;they solve a problem I had, so I identify easily with users&lt;/li&gt;
  &lt;li&gt;they let me use my expertise, so I’m well-positioned to execute&lt;/li&gt;
  &lt;li&gt;they target a niche audience, so there’s little competition&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first two points make these projects work well, while the last one provides flexibility.
I’ll use Autoplaylists as an example.
Custom playlists like “my most-played metal not heard recently” were common in the days of local media players, and people like me missed them when migrating to hosted services.
Other interested developers were turned off by the lack of an official api, but through my earlier work on &lt;a href=&quot;https://github.com/simon-weber/gmusicapi&quot;&gt;gmusicapi&lt;/a&gt;, a reverse-engineered client library, I knew it could be done.
And since only a small percent of Google Music users actually want this feature, the stakes are low and I don’t worry about competitors (including Google).&lt;/p&gt;

&lt;p&gt;It’s worth noting that both projects are also open source.
This is partly for philosophical reasons, but also because a public Github repo is a productivity boost.
For example, Autoplaylists uses Github’s hosting for the product site and wiki for the support site, in addition to the obvious issue tracking and version control offerings.
The added complexity around things like contributions and secret management has been straightforward to manage so far.&lt;/p&gt;

&lt;h3 id=&quot;the-business&quot;&gt;The Business&lt;/h3&gt;

&lt;p&gt;Running a small business seemed daunting at first, but it turned out to be pretty simple in this case.
I run both projects as a sole proprietorship, mostly because I’m not worried about getting sued and have no intention of raising money.
This is the simplest option: there was no extra paperwork besides the Employee Identification Number I got (which I can give out instead of my SSN).
Bookkeeping just takes a few minutes of spreadsheet work per month, and my taxes remain straightforward.
Even PCI compliance was a breeze since my SaaS uses Braintree’s drop-in iframe;
I just had to click through a set of checkboxes to affirm I wasn’t doing anything obviously insecure.
The trickiest business puzzle was figuring out how New York’s sales tax rules applied to Autoplaylists.
For the details, check out my post on the &lt;a href=&quot;https://www.simonmweber.com/2016/07/11/launching-a-chrome-extension-part-1-taxes-and-legal.html&quot;&gt;tax and legal implication of launching a Chrome Extension&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Marketing, on the other hand, wasn’t so simple for me.
I started the year out strong with the Autoplaylists launch.
Here’s what I did, all of which I’d do again:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;before launch, found passionate users for a beta and got them on a mailing list&lt;/li&gt;
  &lt;li&gt;at launch, reached out to tech journalists who’d written about Google Music. &lt;a href=&quot;http://fieldguide.gizmodo.com/how-to-use-one-of-itunes-best-features-in-google-play-m-1766789374&quot;&gt;Gizmodo’s article&lt;/a&gt; landed tens of thousands of views for the cost of a few emails.&lt;/li&gt;
  &lt;li&gt;after launch, shared good autoplaylists to music-sharing communities&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unfortunately, I fizzled out after that.
Not continuing to push Autoplaylists users onto a mailing list was a mistake, and I didn’t put much effort at all into the Autoresponder launch.
While organic search and word of mouth have provided some growth, I’m planning to double down on content marketing and media outreach this year.&lt;/p&gt;

&lt;h3 id=&quot;whats-next&quot;&gt;What’s Next?&lt;/h3&gt;

&lt;p&gt;2016 was a great start, and I’m proud to be a small business owner with happy customers.
More importantly, I’ve now got the confidence for bigger ventures in 2017.
Two things I’m considering are launching a product for small businesses and buying an existing product, each of which would present some interesting new challenges.&lt;/p&gt;

&lt;p&gt;Hopefully next year I’ll be writing a similar post!
If you’d like to follow along, be sure to subscribe with the links below.
Also, feel free to send an email if I can be of assistance; my inbox is always open.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Publishing your first Chrome Extension, part 2: Analytics and Error Reporting</title>
   <link href="http://www.simonmweber.com/2016/07/18/launching-a-chrome-extension-part-2-analytics-and-error-reporting.html"/>
   <updated>2016-07-18T00:00:00-04:00</updated>
   <id>http://www.simonmweber.com/2016/07/18/launching-a-chrome-extension-part-2-analytics-and-error-reporting</id>
   <content type="html">&lt;h2 id=&quot;publishing-your-first-chrome-extension-part-2-analytics-and-error-reporting&quot;&gt;Publishing your first Chrome Extension, part 2: Analytics and Error Reporting&lt;/h2&gt;

&lt;p class=&quot;meta&quot;&gt;July 18 2016, updated March 18 2017&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you’re selling an extension, check out my project &lt;a href=&quot;https://analytics.simon.codes&quot;&gt;Analytics for Google Payments&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Earlier this year, I launched my first paid Chrome extension: &lt;a href=&quot;https://autoplaylists.simon.codes/&quot;&gt;Autoplaylists for Google Music&lt;/a&gt;.
The non-software work involved - like organizing a beta, wrangling paperwork, and finding users - was a bit daunting to handle solo.
So, to help other first-timers publish their side projects, I’m writing up my experiences and making recommendations.&lt;/p&gt;

&lt;p&gt;This post focuses on analytics and error reporting.
For the first post on the tax and legal requirements of selling an extension, see &lt;a href=&quot;/2016/07/11/launching-a-chrome-extension-part-1-taxes-and-legal.html&quot;&gt;part 1&lt;/a&gt;.
If you’re interested in reading future posts, consider &lt;a href=&quot;http://feeds.feedburner.com/SimonWeber&quot;&gt;subscribing to my RSS feed&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=&quot;analytics-and-error-reporting&quot;&gt;Analytics and Error Reporting&lt;/h3&gt;

&lt;p&gt;Analytics lets you track how your extension is being used and found, while error reporting lets you find bugs users may not otherwise report.
Without them, you’re sticking your head in the sand.&lt;/p&gt;

&lt;p&gt;I recommend the free tiers of Google Analytics and Sentry.
If you’ve used them before, know that there’s a bit of extra work involved to make them effective in an extension.&lt;/p&gt;

&lt;h4 id=&quot;google-analytics&quot;&gt;Google Analytics&lt;/h4&gt;

&lt;p&gt;I use &lt;a href=&quot;https://analytics.google.com/&quot;&gt;Google Analytics&lt;/a&gt; heavily.
It helps me answer questions like:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;how many playlists are users creating?&lt;/li&gt;
  &lt;li&gt;how many visits, installs, and subscriptions did a writeup generate?&lt;/li&gt;
  &lt;li&gt;how does usage differ between free and paid users?&lt;/li&gt;
  &lt;li&gt;how is user growth changing over time?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To handle all this, Analytics runs on my web store page, the extension itself, and &lt;a href=&quot;https://autoplaylists.simon.codes&quot;&gt;autoplaylists.simon.codes&lt;/a&gt;.
They all report into one account as separate properties.&lt;/p&gt;

&lt;p&gt;Hooking up the web store and homepage are trivial.
Adding reporting to the Chrome extension itself is a bit more involved: you’ll need to use the &lt;a href=&quot;https://github.com/GoogleChrome/chrome-platform-analytics/wiki&quot;&gt;Chrome Platform Analytics&lt;/a&gt; library.
In addition, be sure to:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/simon-weber/Autoplaylists-for-Google-Music/blob/3f9c5d02e8b34f540171b856e18e1a5180b8afef/src/js/storage.js#L17&quot;&gt;generate a per-user uuid&lt;/a&gt;, &lt;a href=&quot;https://github.com/simon-weber/Autoplaylists-for-Google-Music/blob/3f9c5d02e8b34f540171b856e18e1a5180b8afef/src/js/reporting.js#L58&quot;&gt;send it with requests&lt;/a&gt;, and &lt;a href=&quot;https://github.com/simon-weber/Autoplaylists-for-Google-Music/blob/3f9c5d02e8b34f540171b856e18e1a5180b8afef/src/js/storage.js#L89&quot;&gt;keep it in sync storage&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;create &lt;a href=&quot;https://github.com/simon-weber/Autoplaylists-for-Google-Music/wiki/Frequently-Asked-Questions#privacy-and-security&quot;&gt;a privacy policy&lt;/a&gt; and clearly link it in your extension&lt;/li&gt;
  &lt;li&gt;implement &lt;a href=&quot;https://github.com/simon-weber/Autoplaylists-for-Google-Music/blob/master/src/js/privacy.js&quot;&gt;opt-out&lt;/a&gt; and an &lt;a href=&quot;https://github.com/simon-weber/Autoplaylists-for-Google-Music/blob/master/src/html/privacy.html&quot;&gt;opt-out ui&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’ve linked my code above as examples.&lt;/p&gt;

&lt;p&gt;Now that Analytics is running, you should customize it a bit.
The web store is simplest: track installs by setting up a destination goal for “begins with /track_install”.
I plot goal conversions and completions on the same chart and have it emailed to me weekly.
I haven’t figured out a good way to track other web store events like reviews and comments.&lt;/p&gt;

&lt;p&gt;Customizing reporting for your extension will of course vary based on what you’re doing.
I’d recommend at least adding &lt;a href=&quot;https://github.com/GoogleChrome/chrome-platform-analytics/wiki#track-app-view-changes&quot;&gt;app views&lt;/a&gt; for your UI and an event for loading the extension.
To give you some ideas on what else to track, here are some examples from Autoplaylists:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;to segment paid vs free users: a custom dimension for paid status; custom segments for paid and unpaid.&lt;/li&gt;
  &lt;li&gt;to track sync success: events for sync success, failure, and retry; custom segments for users without syncs, without successful syncs, and with at least one successful sync.&lt;/li&gt;
  &lt;li&gt;to track onboarding success: a destination goal for playlist creation and required funnel from a zero-playlists event.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;sentry&quot;&gt;Sentry&lt;/h4&gt;

&lt;p&gt;&lt;a href=&quot;https://getsentry.com&quot;&gt;Sentry&lt;/a&gt; tracks errors coming from your extension.
Hooking up Raven, their client library, is simple: I don’t have much to add to &lt;a href=&quot;https://docs.getsentry.com/hosted/clients/javascript/&quot;&gt;their docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Once basic error reporting is working, there are two extra steps you should do every time you publish a new version: create a &lt;a href=&quot;https://docs.getsentry.com/hosted/api/releases/post-project-releases/&quot;&gt;Sentry release&lt;/a&gt; and upload &lt;a href=&quot;https://docs.getsentry.com/hosted/clients/javascript/sourcemaps/&quot;&gt;source maps&lt;/a&gt;.
These will help you track down exactly which changes introduced an error.
I’ve written &lt;a href=&quot;https://github.com/simon-weber/Autoplaylists-for-Google-Music/blob/3f9c5d02e8b34f540171b856e18e1a5180b8afef/release.sh#L39&quot;&gt;a script that automates both steps&lt;/a&gt; (it also publishes my extension via &lt;a href=&quot;https://developer.chrome.com/webstore/using_webstore_api&quot;&gt;Google’s web store api&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Keep in mind that Raven will only automatically collect exceptions, which Chrome apis don’t raise (they use lastError instead).
I’ve written  &lt;a href=&quot;https://github.com/simon-weber/Autoplaylists-for-Google-Music/blob/3f9c5d02e8b34f540171b856e18e1a5180b8afef/src/js/chrometools.js#L7&quot;&gt;a decorator&lt;/a&gt; that will automatically send these errors to Sentry as well.&lt;/p&gt;

&lt;p&gt;Finally, here are some parting tips:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;if you use custom dimensions with Analytics, you’ll want to add that information to Raven’s &lt;a href=&quot;https://docs.getsentry.com/hosted/learn/context/&quot;&gt;user context&lt;/a&gt; as well.&lt;/li&gt;
  &lt;li&gt;consider &lt;a href=&quot;https://github.com/simon-weber/Autoplaylists-for-Google-Music/blob/master/src/js/reporting.js&quot;&gt;wrapping Raven&lt;/a&gt; to make it accessible across background, UI, and context scripts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Feel free to shoot me an email if I can help with anything: &lt;a href=&quot;mailto:simon@simonmweber.com&quot;&gt;simon@simonmweber.com&lt;/a&gt;.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Publishing your first Chrome Extension, part 1: Taxes and Legal</title>
   <link href="http://www.simonmweber.com/2016/07/11/launching-a-chrome-extension-part-1-taxes-and-legal.html"/>
   <updated>2016-07-11T00:00:00-04:00</updated>
   <id>http://www.simonmweber.com/2016/07/11/launching-a-chrome-extension-part-1-taxes-and-legal</id>
   <content type="html">&lt;h2 id=&quot;publishing-your-first-chrome-extension-part-1-taxes-and-legal&quot;&gt;Publishing your first Chrome Extension, part 1: Taxes and Legal&lt;/h2&gt;

&lt;p class=&quot;meta&quot;&gt;July 11 2016, updated March 18 2017&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you’re selling an extension, check out my project &lt;a href=&quot;https://analytics.simon.codes&quot;&gt;Analytics for Google Payments&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Earlier this year, I launched my first paid Chrome extension: &lt;a href=&quot;https://autoplaylists.simon.codes/&quot;&gt;Autoplaylists for Google Music&lt;/a&gt;.
The non-software work involved - like organizing a beta, wrangling paperwork, and finding users - was a bit daunting to handle solo.
So, to help other first-timers publish their side projects, I’m writing up my experiences and making recommendations.&lt;/p&gt;

&lt;p&gt;There’s too much to cover in one post, so I’ll start with the part that was scariest for me: dealing with the tax and legal implications of selling software.
I’m effectively a small business owner that sells a product, so I’ve got to follow the rules or risk getting shut down (or worse).
If yours is free, consider &lt;a href=&quot;http://feeds.feedburner.com/SimonWeber&quot;&gt;subscribing to my RSS feed&lt;/a&gt;.
I’ll have advice for you in the coming weeks.&lt;/p&gt;

&lt;p&gt;Finally, I want to give thanks to &lt;a href=&quot;http://www.kalzumeus.com/&quot;&gt;Patrick McKenzie&lt;/a&gt;.
He generously gave me some pointers on the topics below.
If you find yourself wrestling with something not covered here, do consider his &lt;a href=&quot;http://www.kalzumeus.com/standing-invitation/&quot;&gt;standing invitation to reach out&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=&quot;tax-and-legal-considerations&quot;&gt;Tax and Legal Considerations&lt;/h3&gt;

&lt;p&gt;Disclaimer time!&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;I am not a lawyer nor an accountant, so this post cannot be considered professional advice. Think of it as a starting point for your own research or conversations with professionals.&lt;/li&gt;
  &lt;li&gt;this is largely specific to the US, though the themes may apply elsewhere.&lt;/li&gt;
  &lt;li&gt;if you’re making real money, just pay someone to do all this for you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With that out of the way, here are the main things I did before allowing sales:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;chose a business structure&lt;/li&gt;
  &lt;li&gt;prepared to pay income tax&lt;/li&gt;
  &lt;li&gt;prepared to pay sales tax&lt;/li&gt;
  &lt;li&gt;handled Google’s merchant requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’ll cover each separately.&lt;/p&gt;

&lt;h4 id=&quot;business-structures&quot;&gt;business structures&lt;/h4&gt;

&lt;p&gt;I’m running as a sole proprietorship, which is free and simple.
Basically, it means there’s no legal distinction between me and the business.
The only additional paperwork was applying for an Employer Identification Number, which I got just to avoid giving out my SSN.
Despite the name, it’s applicable even though I have no employees.&lt;/p&gt;

&lt;p&gt;Reasons to not use a sole prop include things like desire to raise investor money or concern about getting sued.
In these cases, a C Corp or LLC are likely more appropriate, but you should definitely talk to a lawyer before deciding – you’re in territory my side project likely won’t see.&lt;/p&gt;

&lt;h4 id=&quot;income-tax&quot;&gt;income tax&lt;/h4&gt;

&lt;p&gt;I report income from my extension and pay taxes on that income.
I include the income with my estimated tax, and next year I’ll file a Schedule C with the business’s total income and expenses.&lt;/p&gt;

&lt;p&gt;If your extension makes little money and you typically receive a tax refund, estimated tax might be overkill.&lt;/p&gt;

&lt;p&gt;Be sure to keep an excellent record of all your sales and business expenses.
I’d recommend exporting both your earnings report and orders every month, as Google puts slightly different information in each.&lt;/p&gt;

&lt;h4 id=&quot;sales-tax&quot;&gt;sales tax&lt;/h4&gt;

&lt;p&gt;Unlike Apple’s App Store, Google’s Web Store makes you the merchant of record.
This is both good and bad: it means Google’s cut is less than Apple’s, but it means you need to handle sales tax.
How this works varies by state, but this is what I did for New York:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;determined that my extension qualified as “pre-made software” for sales tax purposes&lt;/li&gt;
  &lt;li&gt;registered for a &lt;a href=&quot;https://www.tax.ny.gov/pubs_and_bulls/tg_bulletins/st/how_to_register_for_nys_sales_tax.htm&quot;&gt;Certificate of Authority&lt;/a&gt; a month before my first sale&lt;/li&gt;
  &lt;li&gt;turned on automatic NY sales tax collection in the Payments Merchant Center&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When it’s time to file sales tax, I check for sales to NY residents, look up the tax rate based on their address, and file my return online.
This is tedious, but don’t let it discourage you: I’m willing to bet New York’s requirements are among the worst to deal with.
For example, these sales wouldn’t qualify for sales tax in California, and many other states have reporting minimums I’m not likely to hit.&lt;/p&gt;

&lt;p&gt;If you publish internationally, you may have to deal with sales tax on a country-by-country basis as well.
For example, there’s VAT for Europe (which Google mostly handles for you) and JCT for Japan.
With the amount of money I’m making, it doesn’t make sense for me to put much effort into this.&lt;/p&gt;

&lt;h4 id=&quot;googles-requirements&quot;&gt;Google’s requirements&lt;/h4&gt;

&lt;p&gt;Compared to the government, appeasing Google is pretty straightforward. You’ll need:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;your EIN for tax purposes&lt;/li&gt;
  &lt;li&gt;$5 for a one-time signup fee&lt;/li&gt;
  &lt;li&gt;a business address and email you’re comfortable posting publicly&lt;/li&gt;
  &lt;li&gt;your bank information for payouts&lt;/li&gt;
  &lt;li&gt;a privacy policy hosted somewhere (I use a Github wiki page)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With this you’ll create two accounts - a Payments merchant account and a Chrome developer account - and then link them together.
Be sure to go through all the options in your merchant account: they include important settings like the name on users’ credit card statements.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;This all may seem like a lot if you’re only making a few bucks a month.
Still, I’d encourage you to go through with it: it took me only a few days in total, and the business experience has been super valuable.
If you find yourself feeling overwhelmed, feel free to shoot me an email.
While I’m far from an expert, I’m happy to help: &lt;a href=&quot;mailto:simon@simonmweber.com&quot;&gt;simon@simonmweber.com&lt;/a&gt;.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Autoplaylists (iTunes smart playlists) for Google Music</title>
   <link href="http://www.simonmweber.com/2016/03/01/autoplaylists-itunes-smart-playlists-for-google-music.html"/>
   <updated>2016-03-01T00:00:00-05:00</updated>
   <id>http://www.simonmweber.com/2016/03/01/autoplaylists-itunes-smart-playlists-for-google-music</id>
   <content type="html">&lt;h2 id=&quot;autoplaylists-itunes-smart-playlists-for-google-music&quot;&gt;Autoplaylists (iTunes smart playlists) for Google Music&lt;/h2&gt;

&lt;p class=&quot;meta&quot;&gt;March 1, 2016&lt;/p&gt;

&lt;p&gt;Autoplaylists - called “smart playlists” in iTunes - allow you to create playlists that always contain songs matching some set of rules.
For example, you could create a playlist that contains only music you haven’t listened to recently, or your most-played songs of a certain genre.&lt;/p&gt;

&lt;p&gt;They’re one of my favorite ways to listen to music, but have never been available in &lt;a href=&quot;https://play.google.com/music/listen&quot;&gt;Google Music&lt;/a&gt;.
So, I’ve created a Chrome Extension to add them.
Here’s an example of one of my autoplaylists:&lt;/p&gt;

&lt;div class=&quot;figure&quot;&gt;
&lt;p&gt;
&lt;a href=&quot;https://autoplaylists.simon.codes&quot;&gt;
&lt;img src=&quot;/images/autoplaylists-screenshot.jpg&quot; alt=&quot;A screenshot of Autoplaylists for Google Music showing declaration of an autoplaylist.&quot; /&gt;
&lt;/a&gt;
&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;If this is something you’ve also found Google Music lacking, you can download it at &lt;a href=&quot;https://autoplaylists.simon.codes&quot;&gt;https://autoplaylists.simon.codes&lt;/a&gt;.
There’s a free version limited to one playlist, and a paid version supporting unlimited playlists.&lt;/p&gt;

&lt;p&gt;The code is also open source, and can be found at &lt;a href=&quot;https://github.com/simon-weber/Autoplaylists-for-Google-Music&quot;&gt;https://github.com/simon-weber/Autoplaylists-for-Google-Music&lt;/a&gt;.
Check it out and let me know what you think!&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Automatic Responses for Google Chat and Hangouts</title>
   <link href="http://www.simonmweber.com/2015/10/18/automatic-responses-for-google-chat-and-hangouts.html"/>
   <updated>2015-10-18T00:00:00-04:00</updated>
   <id>http://www.simonmweber.com/2015/10/18/automatic-responses-for-google-chat-and-hangouts</id>
   <content type="html">&lt;h2 id=&quot;automatic-responses-for-google-chat-and-hangouts&quot;&gt;Automatic Responses for Google Chat and Hangouts&lt;/h2&gt;

&lt;p class=&quot;meta&quot;&gt;October 18, 2015&lt;/p&gt;

&lt;p&gt;Do you have Google accounts that forward email?
If you never log into them, you might be missing Chat/Talk/Hangouts messages.&lt;/p&gt;

&lt;p&gt;After missing an important message, I made &lt;a href=&quot;https://gchat.simon.codes&quot;&gt;https://gchat.simon.codes&lt;/a&gt;.
It automatically replies to - and optionally forwards - the messages you’d otherwise miss.
Check it out and let me know what you think; it’s free and open source!&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Python linting at Venmo</title>
   <link href="http://www.simonmweber.com/2015/08/28/python-linting-at-venmo.html"/>
   <updated>2015-08-28T00:00:00-04:00</updated>
   <id>http://www.simonmweber.com/2015/08/28/python-linting-at-venmo</id>
   <content type="html">&lt;h2 id=&quot;python-linting-at-venmo&quot;&gt;Python linting at Venmo&lt;/h2&gt;

&lt;p class=&quot;meta&quot;&gt;August 28, 2015&lt;br /&gt;
cross-posted at the &lt;a href=&quot;https://venmo-blog.squarespace.com/2015/8/26/python-linting-at-venmo&quot;&gt;Venmo blog&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;Quick! What’s wrong with this (contrived) Python 2 code?&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;sys&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NotFoundError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;Exception&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
   &lt;span class=&quot;k&quot;&gt;pass&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;enforce_presence&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entries&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
   &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;Raise NotFoundError if key is not in entries.&quot;&quot;&quot;&lt;/span&gt;

   &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entry&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entries&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
       &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entry&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
           &lt;span class=&quot;k&quot;&gt;break&lt;/span&gt;
   &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
       &lt;span class=&quot;n&quot;&gt;NotFoundError&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;If you said the unused import and missing &lt;code class=&quot;highlighter-rouge&quot;&gt;raise&lt;/code&gt; keyword, you’re right!
But, if you took longer than a quarter of a second to answer, sorry: you were outperformed by my linting tools.&lt;/p&gt;

&lt;p&gt;Don’t feel bad! Linting is designed to detect these problems more quickly and consistently than a human.
There are two ways to make use of it: manually or automatically.
The former is flexible but not robust, while the latter risks getting in the way.
We lint automatically at Venmo; here’s how we strike a balance between flexibility and enforcement.&lt;/p&gt;

&lt;p&gt;We use a collection of linting tools.
Currently we use &lt;a href=&quot;https://flake8.readthedocs.org&quot;&gt;flake8&lt;/a&gt;, &lt;a href=&quot;http://www.pylint.org/&quot;&gt;pylint&lt;/a&gt; and a custom internal tool.
They each address different needs: flake8 quickly catches simple errors (like the unused import), pylint slowly catches complex errors (like the missing &lt;code class=&quot;highlighter-rouge&quot;&gt;raise&lt;/code&gt;), and our internal tool catches errors that are only relevant to Venmo.
For easy use from the shell, we combine their output with &lt;a href=&quot;https://github.com/sk-/git-lint&quot;&gt;git-lint&lt;/a&gt; and a &lt;a href=&quot;https://gist.github.com/simon-weber/cfb1dcb3118135714abc&quot;&gt;short script&lt;/a&gt;.
This setup catches a wide variety of errors and can easily accommodate new linters.
Here’s what it looks like when run on the code from this post:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;gp&quot;&gt;$ &lt;/span&gt;./lint example.py
Linting file: example.py FAILURE
line 1, col 1: &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;F401]: &lt;span class=&quot;s1&quot;&gt;&#39;sys&#39;&lt;/span&gt; imported but unused
line 15, col 8: Warning: &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;W0104]: Statement seems to have no effect&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Linting happens in three places during our workflow: in-editor, pre-commit, and during builds.
The first step varies for each of us since we don’t all use the same editor (though vim with &lt;a href=&quot;https://github.com/scrooloose/syntastic&quot;&gt;syntastic&lt;/a&gt; is a common choice).
This is the step with the fastest feedback loop: if you don’t currently use linting, start with this.&lt;/p&gt;

&lt;p&gt;The second step is implemented with a git &lt;a href=&quot;https://gist.github.com/simon-weber/b056db8cfa81e08ac67d&quot;&gt;pre-commit hook&lt;/a&gt;.
It lints all the files about to be committed and aborts the commit if there are problems.
Sometimes we opt out of this check - maybe we know about the problems and plan to address them later - by using git’s built in &lt;code class=&quot;highlighter-rouge&quot;&gt;--no-verify&lt;/code&gt; flag.&lt;/p&gt;

&lt;p&gt;Finally, any errors that survive to a pull request will be caught during build linting on Jenkins.
It’s similar to the pre-commit check, but runs on all files that have been changed in the feature branch.
However, unlike the pre-commit check, our build script uses GitHub Enterprise’s &lt;a href=&quot;https://developer.github.com/v3/repos/commits/#compare-two-commits&quot;&gt;comparison api&lt;/a&gt; to find these files.
This eliminates the need to download the repository’s history, allowing us to save bandwidth and disk space with a git shallow clone.&lt;/p&gt;

&lt;p&gt;No matter when linting is run, we always operate it at the granularity of an entire file.
This is necessary to catch problems such as unused imports or dead code; these aren’t localized to only modified lines.
It also means that any file that’s been touched recently is free of problems, so it’s rare that we need to fix problems unrelated to our changes.&lt;/p&gt;

&lt;p&gt;All of our configuration is checked into git, pinning our desired checks to a specific version of the codebase.
Checks that we want to enable are whitelisted, allowing us to safely update our linters without worrying about accidentally enabling new, unwanted checks.&lt;/p&gt;

&lt;p&gt;When enabling a new check, we also fix any existing violations.
This avoids chilling effects: we don’t want to discourage small changes through fear of cleaning up lots of linting violations.
It also incentivizes automated fixes, which saves engineering time compared to distributed manual editing.&lt;/p&gt;

&lt;p&gt;Hopefully, sharing our linting workflow helps save you some time as well!&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>python-libfaketime</title>
   <link href="http://www.simonmweber.com/2015/03/29/libfaketime.html"/>
   <updated>2015-03-29T00:00:00-04:00</updated>
   <id>http://www.simonmweber.com/2015/03/29/libfaketime</id>
   <content type="html">&lt;h2 id=&quot;python-libfaketime&quot;&gt;python-libfaketime&lt;/h2&gt;

&lt;p class=&quot;meta&quot;&gt;March 29, 2015&lt;/p&gt;

&lt;p&gt;I recently released &lt;a href=&quot;https://github.com/simon-weber/python-libfaketime&quot;&gt;python-libfaketime&lt;/a&gt;, which makes it easy to mock the current time during tests.
It’s also two orders of magnitude faster than &lt;a href=&quot;https://github.com/spulec/freezegun&quot;&gt;freezegun&lt;/a&gt; on my laptop.
This is because instead of searching for and monkeypatching imported python stdlib modules, python-libfaketime uses &lt;a href=&quot;https://github.com/wolfcw/libfaketime&quot;&gt;libfaketime&lt;/a&gt; to intercept c standard library calls.&lt;/p&gt;

&lt;p&gt;Here’s how it works:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;the user sets an environment variable requesting that the dynamic linker prioritize libfaketime over the c standard library (eg LD_PRELOAD on linux)&lt;/li&gt;
  &lt;li&gt;python-libfaketime communicates the datetime to libfaketime through another environment variable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The second step is seamless to end users, but the first step can be annoying.
To make this easier, python-libfaketime provides a function to modify the environment and re-exec the current process.&lt;/p&gt;

&lt;p&gt;Give it a try if you notice slow tests while using freezegun – it made the Venmo test suite about 20% faster.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Python Logging Traps</title>
   <link href="http://www.simonmweber.com/2014/11/24/python-logging-traps.html"/>
   <updated>2014-11-24T00:00:00-05:00</updated>
   <id>http://www.simonmweber.com/2014/11/24/python-logging-traps</id>
   <content type="html">&lt;h2 id=&quot;python-logging-traps&quot;&gt;Python Logging Traps&lt;/h2&gt;

&lt;p class=&quot;meta&quot;&gt;November 24 2014 &lt;br /&gt;
cross-posted at the &lt;a href=&quot;http://engineering.venmo.com/blog/2014/11/24/python-logging-traps&quot;&gt;Venmo Engineering blog&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;Python’s logging framework is powerful, but gives you plenty of ways to shoot yourself in the foot.
To make matters worse, logging mistakes are subtle and slip through code review easily.&lt;/p&gt;

&lt;p&gt;In my time at Venmo, I’ve seen logging mistakes cause everything from unneeded debugging to application crashes.
Here are the most common culprits:&lt;/p&gt;

&lt;h3 id=&quot;manual-interpolation&quot;&gt;Manual Interpolation&lt;/h3&gt;

&lt;h4 id=&quot;bad&quot;&gt;Bad:&lt;/h4&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span class=&quot;n&quot;&gt;logger&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;info&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;My data: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;s&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;some_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# or, equivalently&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;logger&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;info&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;My data: {}&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;some_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h4 id=&quot;good&quot;&gt;Good:&lt;/h4&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span class=&quot;n&quot;&gt;logger&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;info&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;My data: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;s&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;some_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;This pushes the interpolation off to the logging framework:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;interpolation overhead is skipped if info level is not enabled&lt;/li&gt;
  &lt;li&gt;encoding problems will not raise an exception (they’ll be logged instead)&lt;/li&gt;
  &lt;li&gt;tools like &lt;a href=&quot;https://pypi.python.org/pypi/sentry&quot;&gt;Sentry&lt;/a&gt; will be able to aggregate log messages intelligently&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pylint will detect this antipattern as &lt;a href=&quot;https://github.com/PyCQA/pylint/blob/master/pylint/checkers/logging.py&quot;&gt;W1201 and W1202&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=&quot;loggererrore-in-exception-handlers&quot;&gt;logger.error(…e) in exception handlers&lt;/h3&gt;

&lt;h4 id=&quot;bad-1&quot;&gt;Bad:&lt;/h4&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span class=&quot;k&quot;&gt;try&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;except&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;FooException&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;logger&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Uh oh! Error: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;s&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h4 id=&quot;good-1&quot;&gt;Good:&lt;/h4&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span class=&quot;k&quot;&gt;try&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;except&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;FooException&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;logger&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;exception&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Uh oh!&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Formatting an exception throws away the traceback, which you usually want for debugging.
&lt;code class=&quot;highlighter-rouge&quot;&gt;logger.exception&lt;/code&gt; is a helper that calls &lt;code class=&quot;highlighter-rouge&quot;&gt;logger.error&lt;/code&gt; but also logs the traceback.&lt;/p&gt;

&lt;h3 id=&quot;implicitly-encoding-unicode-data&quot;&gt;Implicitly encoding unicode data&lt;/h3&gt;

&lt;h4 id=&quot;bad-2&quot;&gt;Bad:&lt;/h4&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span class=&quot;n&quot;&gt;checkmark&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&#39;√&#39;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;decode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&#39;utf-8&#39;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;logger&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;info&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;My data: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;s&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;checkmark&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h4 id=&quot;good-2&quot;&gt;Good:&lt;/h4&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span class=&quot;n&quot;&gt;logger&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;info&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;My data: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;r&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;checkmark&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Using &lt;code class=&quot;highlighter-rouge&quot;&gt;%s&lt;/code&gt; with a unicode string will cause it to be encoded, which will cause an &lt;code class=&quot;highlighter-rouge&quot;&gt;EncodingError&lt;/code&gt; unless your default encoding is utf-8.
Using &lt;code class=&quot;highlighter-rouge&quot;&gt;%r&lt;/code&gt; will format the unicode in \u-escaped repr format instead.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Adding a vcs dependency to requirements.txt</title>
   <link href="http://www.simonmweber.com/2014/11/23/git-pip-requirements.html"/>
   <updated>2014-11-23T00:00:00-05:00</updated>
   <id>http://www.simonmweber.com/2014/11/23/git-pip-requirements</id>
   <content type="html">&lt;h2 id=&quot;adding-a-vcs-dependency-to-requirementstxt&quot;&gt;Adding a vcs dependency to requirements.txt&lt;/h2&gt;

&lt;p class=&quot;meta&quot;&gt;November 23 2014, updated August 27 2016&lt;/p&gt;

&lt;p&gt;It’s sometimes useful to point your requirements.txt at code not yet on pypi.
For example, imagine you’ve just sent a bugfix PR to one of the libraries you depend on.
Instead of waiting for the PR to be merged and packaged, you can temporarily change your dependency to point at your fork.&lt;/p&gt;

&lt;p&gt;When adding the dependency line, it’s best to provide every field.
Leaving some out can have unexpected effects, like pip installing a different version of the code or recloning it on every run.&lt;/p&gt;

&lt;p&gt;Here’s an example of a fully-specified requirement line pointing to &lt;a href=&quot;https://github.com/simon-weber/gmusicapi&quot;&gt;gmusicapi&lt;/a&gt; 4.0.0 on Github:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;git+https://github.com/simon-weber/gmusicapi.git@4.0.0#egg&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;gmusicapi&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;==&lt;/span&gt;4.0.0&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Here’s an explanation of each field:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;git+https://...git&lt;/code&gt;: the vcs type with the repo url appended. https (rather than ssh) is usually how you want to install public code, since it doesn’t require keys to be set up on the machine you’re running on.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;@4.0.0&lt;/code&gt;: the git ref to clone. This example points to &lt;a href=&quot;https://github.com/simon-weber/gmusicapi/releases/tag/4.0.0&quot;&gt;the 4.0.0 tag&lt;/a&gt;. You don’t need to use a tag - pip will happily use anything that can be checked out, such as a feature branch - but I recommend it to avoid using a ref that will point somewhere else later (like &lt;code class=&quot;highlighter-rouge&quot;&gt;master&lt;/code&gt; likely would).&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;egg=gmusicapi&lt;/code&gt;: the name of the package. This is the name you’d give to &lt;code class=&quot;highlighter-rouge&quot;&gt;pip install&lt;/code&gt; (which isn’t always the same name as the repo).&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;==4.0.0&lt;/code&gt;: the version of the package. Without this field pip can’t tell what version to expect at the repo and will be forced to clone on every run (even if the package is up to date).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the maintainer doesn’t change package versions between releases, you’ll want to change it on your branch so pip can tell the difference between your temporary release and the last release. For example, say you contribute a fix to version 1.2.3 of a library. To create your new version, you could:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;branch from your feature branch&lt;/li&gt;
  &lt;li&gt;change the version to 1.2.4-rc.1, since it’s a release candidate of the bugfixed 1.2.3 release&lt;/li&gt;
  &lt;li&gt;use a requirements line like &lt;code class=&quot;highlighter-rouge&quot;&gt;git+https://github.com/me/lib.git@new_branch#egg=lib==1.2.4-rc.1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</content>
 </entry>
 
 <entry>
   <title>VCR.py at Venmo</title>
   <link href="http://www.simonmweber.com/2014/05/01/vcrpy-at-venmo.html"/>
   <updated>2014-05-01T00:00:00-04:00</updated>
   <id>http://www.simonmweber.com/2014/05/01/vcrpy-at-venmo</id>
   <content type="html">&lt;h2 id=&quot;vcrpy-at-venmo&quot;&gt;VCR.py at Venmo&lt;/h2&gt;

&lt;p class=&quot;meta&quot;&gt;May 1 2014&lt;/p&gt;

&lt;p&gt;Recently at Venmo, I spent some time improving our Python test suite.
It was in pretty bad shape when I arrived: builds took 45 minutes and tests were flaky.
Now, builds take 6 minutes and are much more dependable.&lt;/p&gt;

&lt;p&gt;I was asked to write up some blog posts describing our changes, and I decided to start with mocking external interactions.
The full post is at the &lt;a href=&quot;http://venmo.github.io/blog/2014/04/30/vcrpy-at-venmo/&quot;&gt;Venmo engineering blog&lt;/a&gt;.
Hopefully it’s helpful! Feel free to email me with questions.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Staying Busy</title>
   <link href="http://www.simonmweber.com/2014/04/17/staying-busy.html"/>
   <updated>2014-04-17T00:00:00-04:00</updated>
   <id>http://www.simonmweber.com/2014/04/17/staying-busy</id>
   <content type="html">&lt;h2 id=&quot;staying-busy&quot;&gt;Staying Busy&lt;/h2&gt;

&lt;p class=&quot;meta&quot;&gt;April 17 2014&lt;/p&gt;

&lt;p&gt;I’ve been with Venmo for about six months now, and everything has been great!
I’ve worked on all sorts of things - security fixes, features and performance improvements among them -
but my main concern has been our test suite.&lt;/p&gt;

&lt;p&gt;When I first showed up, our builds were flaky and took nearly an hour to run.
Now they’re much more stable and run in five minutes.
I’ve got some Venmo blog posts in the works about how we acheived this
(which I’ll be sure to cross-post or link here).&lt;/p&gt;

&lt;p&gt;This work has been fulfilling, so my personal projects have been a bit neglected.
However, I’ve been open-sourcing a lot of Venmo code lately:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;http://www.thomasboyt.com&quot;&gt;Thomas Boyt&lt;/a&gt; and I &lt;a href=&quot;https://github.com/gittip/www.gittip.com/pull/1857&quot;&gt;added Venmo to Gittip&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;A few of us wrote &lt;a href=&quot;https://github.com/venmo/btnamespace&quot;&gt;btnamespace&lt;/a&gt;,
a library to make Braintree integration testing easier&lt;/li&gt;
  &lt;li&gt;I’m about to release &lt;a href=&quot;https://gist.github.com/simon-weber/b0683e27285dc9ec8efa&quot;&gt;a nose plugin to detect unmocked integration tests&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’ve got some more detailed updates in the works – stay tuned!&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Google Helpouts first impressions</title>
   <link href="http://www.simonmweber.com/2013/11/07/google-helpouts-first-impressions.html"/>
   <updated>2013-11-07T00:00:00-05:00</updated>
   <id>http://www.simonmweber.com/2013/11/07/google-helpouts-first-impressions</id>
   <content type="html">&lt;h2 id=&quot;google-helpouts-first-impressions&quot;&gt;Google Helpouts first impressions&lt;/h2&gt;

&lt;p class=&quot;meta&quot;&gt;November 7 2013&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://helpouts.google.com&quot;&gt;Google Helpouts&lt;/a&gt; launched on Tuesday.
If you haven’t heard of it, it’s a peer-to-peer platform for getting expert help.
Naturally, Google Hangouts are used for the actual sessions.&lt;/p&gt;

&lt;p&gt;I used to provide free computer science tutoring when I was in school, so I figured this might be a nice way to keep giving back.
After getting into the private beta a few weeks ago, I whipped up a free CS help listing – you can see the new version of it &lt;a href=&quot;https://helpouts.google.com/103350848301234480355&quot;&gt;here&lt;/a&gt;.
The Helpouts team looked it over and scheduled me for a one-on-one chat, which lead to my listing being accepted for the launch.
I set my availability for 7:45pm - 10pm, thinking that I’d be lucky to get one or two hits.&lt;/p&gt;

&lt;p&gt;To my complete surprise, I ended up being booked for nearly the entire night!
Here’s how my sessions went:&lt;/p&gt;

&lt;h3 id=&quot;call-one-ideal&quot;&gt;call one: ideal&lt;/h3&gt;

&lt;p&gt;I joined the first Hangout 5 minutes early.
After an awkward 10 minute wait, my first user showed up.
We exchanging greetings, then paired on a Python program.
Setting up a reasonable way to share code wasn’t super easy, but we eventually made good use of the Hangout’s screen sharing.
The allotted 15 minutes gave us just enough time to work out a solution before he signed off.
Success!&lt;/p&gt;

&lt;p&gt;Honestly, my first Helpout couldn’t have gone much better.
I was able to be immediately helpful, the Hangout worked perfectly, and I even got a nice review.&lt;/p&gt;

&lt;h3 id=&quot;call-two-no-help-needed&quot;&gt;call two: no help needed&lt;/h3&gt;

&lt;p&gt;My second user was also about five minutes late.
To my surprise, he didn’t need any help; he was just a developer who wanted to chat.
So, we talked shop.
While this wasn’t what the platform was intended for, it was still fun.&lt;/p&gt;

&lt;h3 id=&quot;calls-three-through-seven-no-shows&quot;&gt;calls three through seven: no shows&lt;/h3&gt;

&lt;p&gt;Unfortunately, I soon found that my first experiences may have been flukes.
My next five users either didn’t show or cancelled at the last minute.&lt;/p&gt;

&lt;h3 id=&quot;call-eight-off-topic&quot;&gt;call eight: off-topic&lt;/h3&gt;

&lt;p&gt;A user showed up for my final slot of the night.
However, English wasn’t his first language, and we had some trouble communicating.
I eventually discovered that he was interested in programming for the job opportunities, so I provided some resources off the top of my head: Cousera, Udacity, Codecademy, etc.
I also spent some time explaining CS as a discipline and attempting to separate it from software engineering.&lt;/p&gt;

&lt;p&gt;I disconnected with the feeling that my time wasn’t well spent.
But, hey, at least someone showed up.&lt;/p&gt;

&lt;h2 id=&quot;impressions&quot;&gt;Impressions&lt;/h2&gt;

&lt;p&gt;Obviously, the no-shows were a huge problem.
Not only did it waste my time, but it may have prevented me from helping actual users.
To mitigate this, my listing now charges a dollar upfront, but promises to refund it if the user shows.
So far this hasn’t gotten any takers, so it’s hard to tell if it’s working or just scaring everyone off.&lt;/p&gt;

&lt;p&gt;I got in touch with the Helpouts team about my problem - over a Helpout - and they told me there wasn’t anything in the pipeline to fix it.
However, they did seem aware of the problem.
I do hope it’s addressed; it’s absolutely sapped some of my enthusiasm for the platform.&lt;/p&gt;

&lt;p&gt;No-shows aside, my first Helpout really showed the potential of the platform: fifteen minutes of my time made a real difference!
I’ll definitely keep some timeslots open on the platform, and hope to have better experiences to share in the future.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Strangers, Go, and Openshift</title>
   <link href="http://www.simonmweber.com/2013/07/31/go-openshift-and-strangers.html"/>
   <updated>2013-07-31T00:00:00-04:00</updated>
   <id>http://www.simonmweber.com/2013/07/31/go-openshift-and-strangers</id>
   <content type="html">&lt;h2 id=&quot;strangers-go-and-openshift&quot;&gt;Strangers, Go, and Openshift&lt;/h2&gt;

&lt;p class=&quot;meta&quot;&gt;July 31 2013&lt;/p&gt;

&lt;p&gt;I’ve always liked the idea of &lt;a href=&quot;http://omegle.com&quot;&gt;Omegle&lt;/a&gt;: one-on-one text chats with strangers.
I also hang out in irc a lot, and thought it’d be fun to combine the two.
This wasn’t a new idea, but since the Recurse Center is all about learning by exploring,
I built an &lt;a href=&quot;https://github.com/simon-weber/omegle-to-irc&quot;&gt;Omegle to irc bridge&lt;/a&gt; with Twisted.&lt;/p&gt;

&lt;p&gt;When I hooked it up in &lt;a href=&quot;http://rochack.org/&quot;&gt;#rochack&lt;/a&gt;,
someone had the idea to have the stranger talk to our resident markov chain bot.
The stranger had no way to know they were talking to a bot, and hilarity ensued.&lt;/p&gt;

&lt;p&gt;The thing was, only folks in our channel could watch.
To share this, I built &lt;a href=&quot;http://strangerbotting-simonmweber.rhcloud.com&quot;&gt;Strangerbotting&lt;/a&gt;.
It’s written in Go, and runs on Openshift, Red Hat’s newish open source PaaS.
There are a couple of pieces to the app:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/simon-weber/gomegle&quot;&gt;gomegle&lt;/a&gt;: a client library for Omegle&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/simon-weber/gomarkov&quot;&gt;gomarkov&lt;/a&gt;: simple, fast, in-memory markov chains for text generation&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/simon-weber/strangerbotting-backend&quot;&gt;strangerbotting-backend&lt;/a&gt;: manages one bot-to-stranger conversation and streams it out over websockets. Runs on Openshift.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/simon-weber/strangerbotting-frontend&quot;&gt;strangerbotting-frontend&lt;/a&gt;: a simple webclient to display conversations. Also runs on Openshift.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There’s nothing too magical going on in the code, so I won’t bother talking about it.
However, I have some new opinions on Go and Openshift that I wanted to write down.&lt;/p&gt;

&lt;h3 id=&quot;golang&quot;&gt;Golang&lt;/h3&gt;

&lt;p&gt;Before getting started with Go, I’d seen a lot of folks complain about the more pedantic parts of the language.
To me, this indicates some combination of:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;disagreement with Google’s engineering culture&lt;/li&gt;
  &lt;li&gt;unfamiliarity with Go’s design goals&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, unused imports in Go raise a compile error.
This reflects both Google’s conservative engineering tendencies (they wouldn’t want such code checked in),
and the goal of fast compilation (see &lt;a href=&quot;http://talks.golang.org/2012/splash.article#TOC_7.&quot;&gt;here&lt;/a&gt; for details).&lt;/p&gt;

&lt;p&gt;Maybe it’s last summer’s Google indoctrination speaking, but I’m fine with these parts of the language.
That said, I still find Go’s error handling - which eschews exceptions for multi-value returns - a bit wonky.
For those unfamiliar with the language, here’s the relevant part of the Go faq:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;
Why does Go not have exceptions?
&lt;/p&gt;

&lt;p&gt;
We believe that coupling exceptions to a control structure, as in the try-catch-finally idiom, results in convoluted code.
It also tends to encourage programmers to label too many ordinary errors, such as failing to open a file, as exceptional.
&lt;/p&gt;

&lt;p&gt;
Go takes a different approach.
For plain error handling, Go&#39;s multi-value returns make it easy to report an error without overloading the return value.
A canonical error type, coupled with Go&#39;s other features, makes error handling pleasant but quite different from that in other languages.
&lt;/p&gt;

&lt;p&gt;
Go also has a couple of built-in functions to signal and recover from truly exceptional conditions.
The recovery mechanism is executed only as part of a function&#39;s state being torn down after an error, which is sufficient to handle catastrophe but requires no extra control structures and, when used well, can result in clean error-handling code.
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This means that the majority of function calls get wrapped in a conditional check on a returned error,
which can seem verbose and tedious.
While this certainly required some getting used to, it doesn’t bother me anymore.
After all, I end up using a similar number of try/except blocks when writing robust Python code.&lt;/p&gt;

&lt;p&gt;What does bother me is the loss of debugging information.
For example, here’s an error I received from Go’s http client: &lt;code class=&quot;highlighter-rouge&quot;&gt;Post http://front2.omegle.com/events: EOF&lt;/code&gt;.
The unclear message and lack of a stacktrace left me guessing: is my code broken?
Is Go’s?
Maybe it was the server?
I ended up reluctantly grepping the stdlib source, since I didn’t even have a line number to go off of.&lt;/p&gt;

&lt;p&gt;Thankfully, there’s enough to like in Go that I’ll put up with some debugging annoyances.
First off, goroutines and selection over channels make for really easy concurrent code.
As somebody who was new to both Twisted- and CSP-style concurrency a month ago, I now greatly prefer the latter.&lt;/p&gt;

&lt;p&gt;The stdlib is also pretty decent, even if it can feel somewhat inconsistent.
For example, there’s no deque implementation, but there is a one for &lt;a href=&quot;http://golang.org/pkg/index/suffixarray/&quot;&gt;suffix arrays&lt;/a&gt;.
I’m willing to write this off as a combination of Python spoiling me and Go being relatively new.&lt;/p&gt;

&lt;p&gt;However, despite its youth, Go’s community is fantastic.
The &lt;a href=&quot;http://irc.lc/freenode/go-nuts&quot;&gt;#go-nuts&lt;/a&gt; channel and &lt;a href=&quot;http://blog.golang.org/&quot;&gt;official blog&lt;/a&gt; are informative and active,
and &lt;a href=&quot;http://play.golang.org&quot;&gt;play.golang.org&lt;/a&gt; makes sharing code a breeze.&lt;/p&gt;

&lt;p&gt;Overall, I came away pleased with Go, and I’ll definitely consider it for backend work in the future.&lt;/p&gt;

&lt;h3 id=&quot;openshift&quot;&gt;Openshift&lt;/h3&gt;

&lt;p&gt;Openshift, however, left me with mixed feelings.
On one hand, I love the ability to ssh into a PaaS box and have a persistent filesystem.
Rather than futz around with custom buildpacks,
I just downloaded and compiled Nginx to host the &lt;a href=&quot;http://github.com/simon-weber/strangerbotting-frontend&quot;&gt;Strangerbotting frontend&lt;/a&gt;.
Their free tier is generous, too;
I wouldn’t have to pay for a three node host/reverse-proxy to redundant backend setup.&lt;/p&gt;

&lt;p&gt;However, there’s not much else to like.
In particular, their bizarre naming choices are a constant source of confusion.
Apps are the top-level construct.
They are made up of gears (VMs), which run cartridges (buildpacks).&lt;/p&gt;

&lt;p&gt;To run the three node architecture I referred to earlier, you don’t make an application with three gears, though;
that’d be too easy.
Instead, you need to make three applications, each with one gear.
You see, you only get one “web gear” per application, which the other gears then support
(by eg hosting a database or build system).&lt;/p&gt;

&lt;p&gt;Since their docs don’t make any of this clear, 
expect to dig around the forums for answers.
Be warned, they’ve got a very “googling for .NET answers on SO” kind of vibe to them.&lt;/p&gt;

&lt;p&gt;I ran into all sorts of other minor difficulties.
For one, there’s no official Go cartridge.
There is a Red Hat community catridge, but that’s been broken for months now, apparently.
After spending a half a day trying to fix it and cursing at their slow cartridge development system, I gave up.
I’m currently deploying a binary over git, which I’m not too happy about.&lt;/p&gt;

&lt;p&gt;These kinds of problems left me with the impression that the service isn’t quite there yet.
So, while I won’t be paying for Openshift any time soon, I will be rooting for them.
An open source PaaS is a noble goal, and more I always welcome competition for my dollars.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Python protobuf on Google App Engine</title>
   <link href="http://www.simonmweber.com/2013/06/18/python-protobuf-on-app-engine.html"/>
   <updated>2013-06-18T00:00:00-04:00</updated>
   <id>http://www.simonmweber.com/2013/06/18/python-protobuf-on-app-engine</id>
   <content type="html">&lt;h2 id=&quot;python-protobuf-on-google-app-engine&quot;&gt;Python protobuf on Google App Engine&lt;/h2&gt;

&lt;p class=&quot;meta&quot;&gt;June 18 2013&lt;/p&gt;

&lt;p&gt;Today, if you try to use Google Protocol Buffers on App Engine, you’ll run into
an error like this one:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;google.protobuf&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;descriptor&lt;/span&gt; 
&lt;span class=&quot;nb&quot;&gt;ImportError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;No&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;named&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;protobuf&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;The problem is that Google’s own App Engine apis also use the &lt;code class=&quot;highlighter-rouge&quot;&gt;google&lt;/code&gt; package namespace,
and they don’t include the protobuf package.&lt;/p&gt;

&lt;p&gt;Thankfully, there’s a simple way to fix this.
First, vendorize the library as you normally would.
I just ripped the &lt;code class=&quot;highlighter-rouge&quot;&gt;site-packages&lt;/code&gt; folder from a virtualenv into the application root:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;app.py
app.yaml
vendor
├── google
│   └── protobuf...
└── httplib2...&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Then, just add your google directory to the google namespace, and add your
vendor directory to the system path.
I used this bit of code:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;os&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;sys&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;google&lt;/span&gt;  &lt;span class=&quot;c&quot;&gt;# provided by GAE&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# add vendorized protobuf to google namespace package&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;vendor_dir&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;os&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;os&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dirname&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;__file__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&#39;vendor&#39;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;google&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;__path__&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;os&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;vendor_dir&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&#39;google&#39;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# add vendor path&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;sys&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;insert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vendor_dir&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;That’s it!
There’s no need for &lt;a href=&quot;https://code.google.com/p/protobuf-gae-hack&quot;&gt;weird hacks&lt;/a&gt;.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Chrome extension hacks: Google Music to turntable</title>
   <link href="http://www.simonmweber.com/2013/06/05/chrome-extension-hacks.html"/>
   <updated>2013-06-05T00:00:00-04:00</updated>
   <id>http://www.simonmweber.com/2013/06/05/chrome-extension-hacks</id>
   <content type="html">&lt;h2 id=&quot;chrome-extension-hacks-google-music-to-turntable&quot;&gt;Chrome extension hacks: Google Music to turntable&lt;/h2&gt;

&lt;p class=&quot;meta&quot;&gt;June 5 2013, updated March 18 2017&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you’re selling an extension, check out my project &lt;a href=&quot;https://analytics.simon.codes&quot;&gt;Analytics for Google Payments&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I just shipped my first Recurse Center project: a Chrome extension that scratches a personal itch.
I use &lt;a href=&quot;http://turntable.fm&quot;&gt;turntable.fm&lt;/a&gt; every now and then, which lets me dj music with friends.
There, I can search for a song, and if someone has previously uploaded it, queue it up for everyone to hear.
If turntable doesn’t have a song, I can upload a file from my computer.&lt;/p&gt;

&lt;p&gt;However, I don’t normally have music files on my laptop; my library is kept in Google Music.
So, if turntable was missing a song, I used to:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;open Google Music and sign in&lt;/li&gt;
  &lt;li&gt;find and download the song&lt;/li&gt;
  &lt;li&gt;switch to turntable and upload it&lt;/li&gt;
  &lt;li&gt;delete the file&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My extension lets me do this right in turntable.
Now, all I do is click “Upload from Google Music”, find the song and hit upload.
The extension does the same thing behind the scenes.&lt;/p&gt;

&lt;p&gt;If this sounds useful, you can get it here: &lt;a href=&quot;https://chrome.google.com/webstore/detail/turntable-uploader-for-go/akchbpaepakjnaihbgkdgjjgpdcckapb&quot;&gt;Turntable Uploader for Google Music™&lt;/a&gt;.
But, this blog post isn’t about advertising: I wanted to document all the weird hacks that went into making this work.
If you’re already a Chrome extension guru, you may already know this stuff;
it’s intended as a post I would have wanted before writing a line of code.&lt;/p&gt;

&lt;p&gt;First, a brief intro to Chrome extensions.
If you’ve written one before, you can probably skip this part.&lt;/p&gt;

&lt;h3 id=&quot;intro-to-chrome-extensions&quot;&gt;Intro to Chrome extensions&lt;/h3&gt;

&lt;p&gt;Chrome extensions are just normal javascript and html, but with optional extra permissions.
For example, cross domain requests and cookie access are kosher – you just need the user to approve them.&lt;/p&gt;

&lt;p&gt;Your extension can be made up of two different kinds of scripts.
The first is the &lt;em&gt;background script&lt;/em&gt; (or &lt;em&gt;event script&lt;/em&gt;; the same thing but not always running).
This does the heavy lifting, since it gets access to special &lt;code class=&quot;highlighter-rouge&quot;&gt;chrome.*&lt;/code&gt; apis.
These enable stuff like the cookie access I mentioned earlier.&lt;/p&gt;

&lt;p&gt;You can also run a bunch of &lt;em&gt;content scripts&lt;/em&gt;.
Unlike the background script, these are specific to a certain page.
They get triggered when a tab matches a url pattern you specify (eg &lt;code class=&quot;highlighter-rouge&quot;&gt;http://turntable.com/*&lt;/code&gt;).
They don’t get &lt;code class=&quot;highlighter-rouge&quot;&gt;chrome.*&lt;/code&gt; access.
They can access the DOM, but run in an &lt;em&gt;isolated world&lt;/em&gt; –
basically, they can modify the DOM, but can’t mess with other code running on the page.
For example, my content script can’t remove an event handler that a turntable script has set up.&lt;/p&gt;

&lt;p&gt;At a first glance, content scripts sound pretty limited.
However, you can get around all of the restrictions above with a bit of hackery.
To access &lt;code class=&quot;highlighter-rouge&quot;&gt;chrome.*&lt;/code&gt; apis, there’s a two-way messaging interface to the background script:
you just offload the work there.
Anything json-encodable is fair game for transport.
Even the isolated world isn’t bulletproof: with DOM access, you can inject a script tag to get at the global namespace.&lt;/p&gt;

&lt;p&gt;My extension uses all three pieces mentioned above: background, content, and injected scripts.
All together, they communicate like this:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;our background script
     ^
     |
     |
   &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;chrome message passing&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
     |
     v
our content script
     ^
     |
     |
   &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;the dom&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
     |
     v
our injected code
     |
     |
   &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;global namespace&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
     |
     v
other code on the page&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Now that we know what we have to work with, I’ll go over how I addressed each of the big pieces of my solution.&lt;/p&gt;

&lt;h3 id=&quot;getting-the-google-music-library-and-downloading-songs&quot;&gt;Getting the Google Music library and downloading songs&lt;/h3&gt;

&lt;p&gt;I’ve spent far too much time with the Google Music protocol from my work on &lt;a href=&quot;http://github.com/simon-weber/Unofficial-Google-Music-API&quot;&gt;gmusicapi&lt;/a&gt;, so I already knew which endpoints to hit.&lt;/p&gt;

&lt;p&gt;Auth presented a hurdle, though, since it requires either plaintext credentials (yuck) or OAuth (annoying).
I got around this with my extra Chrome host and cookies permissions:
I just have the user open Google Music in another tab, then piggyback on that session.
My requests will automatically send Google cookies, and I just have to grab an xsrf cookie for use in the url.&lt;/p&gt;

&lt;h3 id=&quot;uploading-to-turntable&quot;&gt;Uploading to turntable&lt;/h3&gt;

&lt;p&gt;This was the toughest nut to crack.
I considered reverse engineering the endpoints that turntable’s client page used for uploading, but this had a number of disadvantages:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;the user wouldn’t see the upload in the turntable interface&lt;/li&gt;
  &lt;li&gt;I don’t have the javascript chops to implement a complicated upload protocol&lt;/li&gt;
  &lt;li&gt;future protocol changes would break stuff&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A better approach: tricking turntable’s own clientside code into believing the user had initiated a normal upload.
This makes turntable do all the work, and is business as usual for the user.&lt;/p&gt;

&lt;p&gt;After some quality time in the DevTools debugger, a friend of mine (thanks, &lt;a href=&quot;https://github.com/clehner&quot;&gt;Charlie&lt;/a&gt;!) figured out a way to do this.
turntable uses a third party library called &lt;a href=&quot;http://plupload.com&quot;&gt;plupload&lt;/a&gt;.
Unfortunately, a high level &lt;code class=&quot;highlighter-rouge&quot;&gt;window.plupload.upload(File)&lt;/code&gt; function isn’t accessible; it’s hidden inside turntable closures.
However, a similar function is stored directly as a handler on the main file input, meaning that we can spoof an upload with something like &lt;code class=&quot;highlighter-rouge&quot;&gt;$(&#39;..input[file]..&#39;).onchange.call(our_File)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Since injected code gets around the isolated world restriction, this is totally possible:
we just need to also inject an html5 File containing the desired mp3.&lt;/p&gt;

&lt;h3 id=&quot;getting-a-file-from-a-blob&quot;&gt;Getting a File from a Blob&lt;/h3&gt;

&lt;p&gt;Html5 Blobs are easy to create, and just represent binary data.
Html5 Files, though, can only be created when a user interacts with a file input (here’s the &lt;a href=&quot;http://www.w3.org/TR/FileAPI/&quot;&gt;File api docs&lt;/a&gt;, if you want the details).&lt;/p&gt;

&lt;p&gt;Luckily, Files aren’t much different from Blobs: they just add a filename and date of modification.
Duck typing to the rescue!&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;c1&quot;&gt;//b is our Blob&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&#39;myfile.mp3&#39;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;lastModifiedDate&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// tada!&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h3 id=&quot;putting-it-all-together&quot;&gt;Putting it all together&lt;/h3&gt;

&lt;p&gt;Those are the main hacks. Combining everything, this is about what happens when an upload is requested:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;content script messages the background script with a file id to upload&lt;/li&gt;
  &lt;li&gt;background script performs a cross domain request to Google and retrieves a Blob&lt;/li&gt;
  &lt;li&gt;background script encodes the Blob as a base64 dataurl&lt;/li&gt;
  &lt;li&gt;Blob dataurl (now json-compatible) is messaged back to the content script&lt;/li&gt;
  &lt;li&gt;content script injects the entire dataurl, along with code to do the Blob to File spoofing, trigger the plupload code, and clean up when done&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From there, I just added a bunch of messaging to get the ui working and a third party library to display the Google Music library.
You can grab my ugly code on &lt;a href=&quot;https://github.com/simon-weber/Google-Music-Turntable-Uploader&quot;&gt;my GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I plan to write continue writing posts like these during my time at the Recurse Center, so if you dug this, Twitter or RSS are the best ways to get more.&lt;/p&gt;

&lt;p&gt;Many thanks to my fellow Recursers who read over this post: &lt;a href=&quot;http://lfranchi.com&quot;&gt;Leo Franchi&lt;/a&gt; and &lt;a href=&quot;https://github.com/eriktaubeneck&quot;&gt;Erik Taubeneck&lt;/a&gt;.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>gmusicapi retrospective</title>
   <link href="http://www.simonmweber.com/2013/05/14/gmusicapi-retrospective.html"/>
   <updated>2013-05-14T00:00:00-04:00</updated>
   <id>http://www.simonmweber.com/2013/05/14/gmusicapi-retrospective</id>
   <content type="html">&lt;h2 id=&quot;gmusicapi-retrospective&quot;&gt;gmusicapi retrospective&lt;/h2&gt;

&lt;p class=&quot;meta&quot;&gt;May 14 2013&lt;/p&gt;

&lt;p&gt;gmusicapi has come a long way since I pushed my &lt;a href=&quot;https://github.com/simon-weber/Unofficial-Google-Music-API/commit/880fcb3063e5ed3ba7179f1a9c8fda8c41f1dd6a&quot;&gt;first commit&lt;/a&gt; back in January 2012.&lt;/p&gt;

&lt;p&gt;The project has grown more than usual during this semester, since – for the first time – I worked on it for school credit.
Here’s what I spent my time on:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;a number of big features: scan-and-match support, album art operations, Music Manager OAuth support, python 2.6 compatibility. . .&lt;/li&gt;
  &lt;li&gt;a new dynamic testing system for my end-to-end tests&lt;/li&gt;
  &lt;li&gt;a huge rewrite to allow for more flexible protocol declarations&lt;/li&gt;
  &lt;li&gt;continuous integration that also catches protocol changes&lt;/li&gt;
  &lt;li&gt;documentation improvements and a new irc support channel&lt;/li&gt;
  &lt;li&gt;version 1.0!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/simon-weber/Unofficial-Google-Music-API/graphs&quot;&gt;Github’s graphs&lt;/a&gt; provide a quick overview of what I’ve been up to.
Here are a few other metrics:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;documentation visits x5, bounce rate halved&lt;/li&gt;
  &lt;li&gt;PyPi download rate x2&lt;/li&gt;
  &lt;li&gt;use in a couple of real-world products&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I figure this is a good chance to reflect on some of the decisions I’ve made, both for my sake and for those who might come this way in the future.&lt;/p&gt;

&lt;h3 id=&quot;dont-count-out-end-to-end-testing&quot;&gt;Don’t count out end-to-end testing&lt;/h3&gt;

&lt;p&gt;I have a few unit tests, but the majority of my tests are end-to-end black-box tests that exercise my clients against actual Google servers.
Naturally, they violate core unit test principles: they’re stateful, cannot be run offline, and run slowly.&lt;/p&gt;

&lt;p&gt;One thing I’ve learned while coming to this solution: there’s a lot of dogma surrounding test strategies.&lt;/p&gt;

&lt;p&gt;It’s not that I’m against unit testing.
I’d love to have more unit tests!
Unfortunately, writing them isn’t free, and in my case, end-to-end tests offer more bang for my buck.
I get more coverage with less code and get to test my expectations about Google’s servers.
gmusicapi is also pretty small, so the loss of granularity when bug-hunting isn’t a huge deal.&lt;/p&gt;

&lt;p&gt;Of course, there’s some extra work required to make your end-to-end tests robust.
Backend flakiness can be addressed with retry and backoff.
Test dependence and state management can be handled with TestNG-like frameworks (I use &lt;a href=&quot;http://pythonhosted.org/proboscis/&quot;&gt;Proboscis&lt;/a&gt;).
Protocol changes can be caught by validating responses against a schema.&lt;/p&gt;

&lt;p&gt;Currently, I have a few Google accounts I use exclusively for testing, and I keep their credentials in TravisCI encrypted variables.
There’s one big downside to this: encrypted variables can’t be used with other folks’ pull requests.
In the future, I hope to figure out a way to safely manage a public test account (I think it’s possible with 2-factor auth).&lt;/p&gt;

&lt;h3 id=&quot;invest-in-support&quot;&gt;Invest in support&lt;/h3&gt;

&lt;p&gt;I have trouble saying no to requests for help.
Interestingly, gmusicapi also seems to attract many users who are new to Python.
Because of these factors, I used to spend a fair amount of time answering support emails.
I didn’t mind this, but after receiving a few emails on the same topics, I figured it’d be better to flesh out the &lt;a href=&quot;http://unofficial-google-music-api.readthedocs.org&quot;&gt;gmusicapi documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Basic questions were addressed with installation and getting starting sections.
These were no-brainers, and I should have added them earlier.&lt;/p&gt;

&lt;p&gt;I also added a section for a less obvious topic: people want the reverse-engineered protocols I’m using.
So far, I’ve thrown together a hack that introspects my protocol declaration code, but I’ve got plenty of room to improve this.&lt;/p&gt;

&lt;p&gt;The &lt;a href=&quot;http://webchat.freenode.net/?channels=gmusicapi&quot;&gt;#gmusicapi irc channel on Freenode&lt;/a&gt; is another recent support addition.
I wish I’d set it up earlier!
Recently, devs building on gmusicapi have started hanging out and answering questions, which has been a huge help.&lt;/p&gt;

&lt;p&gt;I still go the extra mile when I do direct support.
Helping users out is its own reward, but I’m also flattered to have received thank-you emails after irc chats, money, and – once – even a gift in the mail!&lt;/p&gt;

&lt;h3 id=&quot;looking-forward&quot;&gt;Looking forward&lt;/h3&gt;

&lt;p&gt;I’m very pleased with what I’ve learned and accomplished so far.
My next step is to get the project into a self-sustaining state.
I figure this means knocking out todos, beefing up internal documentation, and refactoring all of the wonky bits that have accumulated over time.&lt;/p&gt;

&lt;p&gt;Thankfully, I’ll have plenty of time this summer at the &lt;a href=&quot;https://www.recurse.com&quot;&gt;Recurse Center&lt;/a&gt;!&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Development on a Chromebook: an opinionated guide</title>
   <link href="http://www.simonmweber.com/2013/04/20/development-on-a-chromebook-an-opinionated-guide.html"/>
   <updated>2013-04-20T00:00:00-04:00</updated>
   <id>http://www.simonmweber.com/2013/04/20/development-on-a-chromebook-an-opinionated-guide</id>
   <content type="html">&lt;h2 id=&quot;development-on-a-chromebook-an-opinionated-guide&quot;&gt;Development on a Chromebook: an opinionated guide&lt;/h2&gt;

&lt;p class=&quot;meta&quot;&gt;April 20 2013, last updated January 6 2017&lt;/p&gt;

&lt;p&gt;I started using a
&lt;a href=&quot;http://www.google.com/intl/en/chrome/devices/chromebook-samsung-550.html&quot;&gt;Samsung 550 Chromebook&lt;/a&gt;
as my on-the-go machine two semesters ago. It worked nicely for
taking notes, but I remained a skeptic: how could I ever write code
from a glorified web browser?&lt;/p&gt;

&lt;p&gt;Fast forward 6 months: today, I love hacking on my Chromebook, and
I have no problems working offline. It took some effort to get
everything set up, so I’ve put together my recommendations to get
other folks up to speed.&lt;/p&gt;

&lt;p&gt;First, a disclaimer: the device was given to me as part of the
&lt;a href=&quot;http://www.google.com/intl/en/jobs/students/proscho/programs/uscanada/ambassador/&quot;&gt;Google Student Ambassador Program&lt;/a&gt;,
and Google pays me for brand advocacy at my school. That said, I’m
&lt;em&gt;not&lt;/em&gt; getting paid to write this, and this is my advice, not
Google’s. I’m a hacker, not a shill (and I’ll even save hn the work
of linking
&lt;a href=&quot;http://www.paulgraham.com/submarine.html&quot;&gt;pg’s submarine essay&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Anyway: let’s get started. It’s easiest to work over ssh, so I’ll
cover this first. Later, I’ll get to working offline. I won’t talk
about cloud development webapps (maybe check out &lt;a href=&quot;https://www.nitrous.io&quot;&gt;Nitrous.IO&lt;/a&gt;?).
Also, I don’t have any advice if you prefer heavy IDEs; my usual
tools are a terminal and web browser.&lt;/p&gt;

&lt;h3 id=&quot;for-when-you-have-a-good-connection&quot;&gt;For when you have a good connection&lt;/h3&gt;

&lt;h4 id=&quot;ssh&quot;&gt;ssh&lt;/h4&gt;

&lt;p&gt;You want
&lt;a href=&quot;https://chrome.google.com/webstore/detail/secure-shell/pnhechapfaindjhompbnflcldabbghjo&quot;&gt;Secure Shell&lt;/a&gt;
as your ssh client. It’s basically openssh wrapped for
&lt;a href=&quot;https://developers.google.com/native-client/&quot;&gt;NaCl&lt;/a&gt;, with
&lt;a href=&quot;https://groups.google.com/a/chromium.org/group/chromium-hterm&quot;&gt;hterm&lt;/a&gt;
powering the ui.&lt;/p&gt;

&lt;p&gt;It supports everything you’d expect, like key authentication,
per-connection profiles, port forwarding, custom color schemes, and
even your &lt;code class=&quot;highlighter-rouge&quot;&gt;~/.ssh/config&lt;/code&gt; file (well, probably; mine is simple).&lt;/p&gt;

&lt;p&gt;The &lt;a href=&quot;http://goo.gl/RYHiK&quot;&gt;chromium-hterm mailing list&lt;/a&gt; is where
updates are posted. If you need an upcoming feature &lt;em&gt;now&lt;/em&gt;, there’s
also a hidden &lt;a href=&quot;http://goo.gl/cFZlv&quot;&gt;dev channel&lt;/a&gt; (you need to be
logged into an account that’s on the mailing list for this link to
work).&lt;/p&gt;

&lt;p&gt;A quick tip: set Secure Shell to ‘Open as Window’, otherwise Chrome
will intercept keyboard shortcuts (which e.g. makes Control-W close
your terminal). You’ll probably also want to set TERM to xterm. The
FAQ (linked below) has the details and is worth reading through:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;http://goo.gl/3i5AJ&quot;&gt;Secure Shell FAQ&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://gist.github.com/johnbender/5018685&quot;&gt;Solarized colors&lt;/a&gt;
(run the commands in a DevTools javascript console)&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;vpn&quot;&gt;vpn&lt;/h4&gt;

&lt;p&gt;&lt;a href=&quot;http://support.google.com/chromeos/bin/answer.py?hl=en&amp;amp;answer=1282338&quot;&gt;Legend has it&lt;/a&gt;
that CrOS supports OpenVPN and L2TP/IPsec. However, they’re notoriously difficult
to get working in some configurations; I never got mine working.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;edit&lt;/em&gt;: I haven’t tried this for about a year, and it seems like 
&lt;a href=&quot;https://code.google.com/p/chromium/issues/detail?id=217624&quot;&gt;the team is making progress&lt;/a&gt;.&lt;/p&gt;

&lt;h4 id=&quot;remote-desktop&quot;&gt;remote desktop&lt;/h4&gt;

&lt;p&gt;If you need a graphical environment, you can use
&lt;a href=&quot;https://chrome.google.com/webstore/detail/chrome-remote-desktop/gbchcmhmhahfdphkhkmpfmihenigjmpp&quot;&gt;Chrome Remote Desktop&lt;/a&gt;.
This provides vnc-like functionality across Windows, Mac and Linux,
and can be set up for repeated or one-off access. Note that you
cannot currently remote &lt;em&gt;into&lt;/em&gt; a Chromebook.&lt;/p&gt;

&lt;p&gt;I rarely need this.&lt;/p&gt;

&lt;h4 id=&quot;crosh-window&quot;&gt;Crosh Window&lt;/h4&gt;

&lt;p&gt;Crosh, the built-in CrOS shell, can (and should) usually be
avoided. If you find yourself using it,
&lt;a href=&quot;https://chrome.google.com/webstore/detail/crosh-window/nhbmpbdladcchdhkemlojfjdknjadhmh?hl=en-US&quot;&gt;Crosh Window&lt;/a&gt;
takes away some of the pain; it fixes the Control-W problem
mentioned earlier and gets you an up-to-date version of hterm.&lt;/p&gt;

&lt;h3 id=&quot;for-when-youre-offline-or-on-a-terrible-connection&quot;&gt;For when you’re offline or on a terrible connection&lt;/h3&gt;

&lt;h4 id=&quot;galliumos&quot;&gt;GalliumOS&lt;/h4&gt;

&lt;p&gt;&lt;em&gt;edit&lt;/em&gt;: &lt;a href=&quot;https://galliumos.org/&quot;&gt;GalliumOS&lt;/a&gt; is a linux distribution that’s designed for Chromebooks.
For example, it supported my Pixel’s keyboard layout and touchscreen out of the box.
If you’re comfortable maintaining your own linux install - or regularly work with VMs - I’d suggest it over crouton.&lt;/p&gt;

&lt;p&gt;My setup is a dual-boot installed with chrx, which you can find instructions for &lt;a href=&quot;https://wiki.galliumos.org/Installing&quot;&gt;here&lt;/a&gt;.
If you run into problems, &lt;a href=&quot;https://wiki.galliumos.org/Community&quot;&gt;the subreddit and irc channel &lt;/a&gt; are quite active.&lt;/p&gt;

&lt;h4 id=&quot;crouton&quot;&gt;crouton&lt;/h4&gt;

&lt;p&gt;For offline work, you’ll want root access to a local Linux install.
&lt;a href=&quot;https://github.com/dnschneid/crouton&quot;&gt;crouton&lt;/a&gt; is &lt;em&gt;by far&lt;/em&gt; the
best way to go about this: it runs Ubuntu in a chroot. This has
security implications (check the README), but you avoid the
performance hit of virtualization, and keep all the CrOS
functionality.&lt;/p&gt;

&lt;p&gt;You’ll need to have your Chromebook in developer mode (i.e. rooted)
to use it, which is easy: I just flipped a hardware switch.
The specifics for going about this vary by model, so just
search for instructions. Once in dev mode, you’ll want to hit
Control-D on each boot to skip the “you’re in developer mode”
warning (there’s a 30-second wait otherwise). Have faith: this
isn’t nearly as annoying as it sounds.&lt;/p&gt;

&lt;p&gt;The crouton README has all the information you need to get started.
Note that you can run a normal graphical environment (e.g. Xfce)
alongside CrOS. I prefer using Secure Shell to ssh into localhost
so I can keep my terminal customizations and stay in CrOS. If this
sounds appealing, here’s what I did:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;used the crouton cli-extra target (eg
&lt;code class=&quot;highlighter-rouge&quot;&gt;crouton -t cli-extra ...&lt;/code&gt;).&lt;/li&gt;
  &lt;li&gt;installed openssh in my chroot&lt;/li&gt;
  &lt;li&gt;start sshd, then use Secure Shell to connect to
my-user@localhost&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To make life a bit easier, I stuck &lt;code class=&quot;highlighter-rouge&quot;&gt;/etc/init.d/ssh start&lt;/code&gt; into my
chroot’s &lt;code class=&quot;highlighter-rouge&quot;&gt;/etc/rc.local&lt;/code&gt; (which crouton runs upon mounting). Now,
when I want to work locally, I just Control-Alt-Forward to get my
local shell, &lt;code class=&quot;highlighter-rouge&quot;&gt;$ sudo enter-chroot&lt;/code&gt;, Control-Alt-Back to CrOS and
then run Secure Shell. You could probably get your chroot to mount
and run sshd on boot if you use it all the time.&lt;/p&gt;

&lt;h4 id=&quot;mosh&quot;&gt;mosh&lt;/h4&gt;

&lt;p&gt;As an alternative to ssh on flaky connections, you can use &lt;a href=&quot;http://mosh.mit.edu/&quot;&gt;mosh&lt;/a&gt;.
It’ll need to be installed on your server to use it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;edit&lt;/em&gt;: I used to recommend running it inside crouton, but
there’s now a proper
&lt;a href=&quot;https://chrome.google.com/webstore/detail/mosh/ooiklbnjmhbcgemelgfhaeaocllobloj?hl=en&quot;&gt;mosh Chrome packaged app&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=&quot;parting-words&quot;&gt;Parting words&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;edit&lt;/em&gt;: &lt;a href=&quot;http://gigaom.com/2013/04/05/running-out-of-memory-on-a-chromebook-heres-a-30-second-solution/&quot;&gt;zRAM&lt;/a&gt;
is now enabled by default, so you don’t need to worry about turning it on yourself.&lt;/p&gt;

&lt;p&gt;If you’re a fellow Chromebook hacker and think I missed something,
definitely let me know. I’ll do my best to keep this guide updated
as better tools arrive.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;edit&lt;/em&gt;: here’s a link to the &lt;a href=&quot;https://news.ycombinator.com/item?id=5582531&quot;&gt;hn discussion&lt;/a&gt;.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Tough cookies: a debugging story</title>
   <link href="http://www.simonmweber.com/2013/02/14/tough-cookies-a-debugging-story.html"/>
   <updated>2013-02-14T00:00:00-05:00</updated>
   <id>http://www.simonmweber.com/2013/02/14/tough-cookies-a-debugging-story</id>
   <content type="html">&lt;h2 id=&quot;tough-cookies-a-debugging-story&quot;&gt;Tough cookies: a debugging story&lt;/h2&gt;

&lt;p class=&quot;meta&quot;&gt;February 14 2012&lt;/p&gt;

&lt;p&gt;I used to think of (web) cookies as simple key/value pairs.
That was before I spent an hour tracking down a bug in &lt;a href=&quot;http://github.com/simon-weber/Unofficial-Google-Music-API&quot;&gt;gmusicapi&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The symptoms were simple.
The user would successfully log in.
Then, on their first request to a web service endpoint (eg music.google.com/loadalltracks), Google would reject their request and redirect them to a login page.&lt;/p&gt;

&lt;p&gt;The auth code was immediately suspect. However, there were a few complications:
- I could not recreate the issue
- that code hadn’t changed much since last known-good version&lt;/p&gt;

&lt;p&gt;The user who reported the bug stopped by #gmusicapi on Freenode (thanks, Leonardo!) and humored my requests for debug information.
Here’s what we found out:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;the login was successful; calls to Music Manager endpoints succeeded&lt;/li&gt;
  &lt;li&gt;the user was outside of the US&lt;/li&gt;
  &lt;li&gt;normal login to music.google.com through a browser was fine&lt;/li&gt;
  &lt;li&gt;the bug did not depend on multiple login status, locale assumptions or current login status&lt;/li&gt;
  &lt;li&gt;the user had all the proper session cookies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Throughout all this, I still could not recreate the issue.
In a lucky guess, I used a test account from outside the US: bingo!
This only affected non-US logins!&lt;/p&gt;

&lt;p&gt;Now that I had a way to recreate the bug, I fired up git bisect to track down the commit that introduced it.
Inside a function to take a &lt;a href=&quot;http://docs.python-requests.org/en/latest/&quot;&gt;requests.Request&lt;/a&gt; and send it off to Google, here’s the relevant code before:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;send_xt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&#39;u&#39;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&#39;xt&#39;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get_web_cookie&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&#39;xt&#39;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# web_cookies is a CookieJar&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cookies&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;web_cookies&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;prep_request&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;prepare&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;requests&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;res&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;send&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;prep_request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;res&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;and after:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cookies&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cookies&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;#Attach auth.&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;send_xt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&#39;u&#39;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&#39;xt&#39;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get_web_cookie&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&#39;xt&#39;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;send_clientlogin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cookies&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&#39;SID&#39;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get_sid_token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;send_sso&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;c&quot;&gt;#dict &amp;lt;- CookieJar&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;web_cookies&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;web_cookies&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cookies&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;web_cookies&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;prepped&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;prepare&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;requests&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;res&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;send&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;prepped&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;session_options&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;res&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;The commit was some refactoring from two separate request-sending functions (eg send_web_request and send_musicmanager_request) to one.
Do you see the bug?
Here’s a hint, changing:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span class=&quot;n&quot;&gt;web_cookies&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;web_cookies&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cookies&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;web_cookies&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;to:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cookies&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;web_cookies&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;solves the problem (hackily, since then clientlogin and sso auth can’t be sent together, but that never happens anyway - and this will all be rewritten soon).&lt;/p&gt;

&lt;p&gt;If you didn’t figure it out, here’s the issue: auth was stored in Python &lt;a href=&quot;http://docs.python.org/2/library/cookie.html#module-Cookie&quot;&gt;Cookie objects&lt;/a&gt;, but I had attached them to the request as a name/value dictionary.
In doing so, there was information loss:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;in the case of cookies with identical names, only one would be sent&lt;/li&gt;
  &lt;li&gt;Cookie-specific fields like secure and domain were dropped&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In their Python form, Cookies are not simple name/value pairs!
Attaching Cookies directly to the request kept all the relevant information and solved the problem.&lt;/p&gt;

&lt;p&gt;I’d like to thank Lukasa and SigmaVirus24 on #python-requests for pointing me to the relevant Requests internals, and for generally putting up with my mad raving.
Lukasa also had what I thought was some sharp insight into the situation:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;simon_weber&lt;/strong&gt;: I suppose it would be nice to be able to set both simple and Cookie cookies using Requests&lt;/p&gt;
&lt;/blockquote&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Lukasa&lt;/strong&gt;: I think more than that, we want to discourage it.
Cookies are complicated and easy to get wrong
(as this entire discussion shows)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Lukasa&lt;/strong&gt;: And so we’d rather that people use the known-good code in Requests&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;He’s exactly right, of course, and I already have an entry on my todo list for this (I’ve just got &lt;a href=&quot;https://github.com/simon-weber/Unofficial-Google-Music-API/issues&quot;&gt;a lot on my plate&lt;/a&gt; at the moment).&lt;/p&gt;

&lt;p&gt;At least this story has a happy ending: despite the confusion, gmusicapi soldiers on with &lt;a href=&quot;https://twitter.com/thiloleibelt/status/302159032922296322&quot;&gt;happy international users&lt;/a&gt;.
As for me, having learned from this bug hunt, hopefully I’ll never be suckered into disrespecting the surprising complexity of the cookie - nor their internet cousins of the same name.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>What's in a genre?</title>
   <link href="http://www.simonmweber.com/2012/09/07/whats-in-a-genre.html"/>
   <updated>2012-09-07T00:00:00-04:00</updated>
   <id>http://www.simonmweber.com/2012/09/07/whats-in-a-genre</id>
   <content type="html">&lt;script src=&quot;//ajax.googleapis.com/ajax/libs/jquery/1.8.1/jquery.min.js&quot;&gt;
&lt;/script&gt;

&lt;script type=&quot;text/javascript&quot; src=&quot;https://www.google.com/jsapi&quot;&gt;
&lt;/script&gt;

&lt;script type=&quot;text/javascript&quot;&gt;
$(document).ready(function(){
  google.load(&#39;visualization&#39;, &#39;1.0&#39;, {
    &#39;packages&#39;:[&#39;corechart&#39;],
    &#39;callback&#39;: drawOccurrenceChart
  });

  function drawOccurrenceChart(){
    $.getJSON(&#39;/data/occurrences.json&#39;, function(data) {
      data.unshift([&#39;Word&#39;, &#39;Occurrences&#39;]);
      var table = google.visualization.arrayToDataTable(data);
      var options = { 
        title: &#39;Frequency of words in genres&#39;,
        backgroundColor: {fill:&#39;transparent&#39;}
      };

      var chart = new google.visualization.PieChart(document.getElementById(&#39;words_chart&#39;));
      chart.draw(table, options);
    });
  }
});
&lt;/script&gt;

&lt;h2 id=&quot;whats-in-a-genre&quot;&gt;What’s in a genre?&lt;/h2&gt;

&lt;p class=&quot;meta&quot;&gt;September 07 2012&lt;/p&gt;

&lt;p&gt;I’m taking a course on data mining this semester. Our first assignment: mine some data. The dataset and techniques don’t matter; the point is to extract meaning in any way possible. I’m greenhorn data miner; hopefully I’ll be able to look back at this post and laugh at my own naivete.&lt;/p&gt;

&lt;p&gt;For my dataset I chose my own Google Music library. It’s unique, big enough (7600+ songs), and well organized. Plus, it’s a cinch to access with my &lt;a href=&quot;https://github.com/simon-weber/Unofficial-Google-Music-API&quot;&gt;Google Music api&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;My analysis was simple: I investigated the occurrences of words in genres. I figured the most frequent words would be genres themselves (eg ‘metal’ in ‘power metal’), but there was also the chance of exposing common adjectives (eg ‘post’ in ‘post-rock’ and ‘post-metal’).&lt;/p&gt;

&lt;p&gt;A few lines of Python later, and I had my results. The first thing I noticed: I listen to a lot of metal. A third of my songs are some kind of metal. If you put all the genre words into a hat, you’d pick ‘metal’ almost a quarter of the time. Next up: ‘rock’ and ‘jazz’. Rounding out the top six are two adjectives - ‘alternative’ and ‘progressive’ - as well as ‘accompaniment’ (as in &lt;a href=&quot;http://en.wikipedia.org/wiki/Jamey_Aebersold&quot;&gt;Jamey Aebersold&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Metal bands also claim the longest genres in my library. &lt;a href=&quot;http://en.wikipedia.org/wiki/Novembre_(band)&quot;&gt;Novembre&lt;/a&gt; is the champion, boasting this mouthful: ‘Progressive atmospheric doom metal’.&lt;/p&gt;

&lt;p&gt;Now that we’ve figured out I’m a jazz-playing metalhead, let’s take a look at the least common words. ‘Country’ appears only once (and at the risk of sounding one-sided, it labels the fantastic &lt;a href=&quot;http://en.wikipedia.org/wiki/Slaughter_of_the_bluegrass&quot;&gt;Slaughter of the Bluegrass&lt;/a&gt;). There’s a bunch of mispellings, too, like ‘reggaer’ and ‘sountrack’.&lt;/p&gt;

&lt;p&gt;Here’s a quick chart of all the words I found:&lt;/p&gt;

&lt;div id=&quot;words_chart&quot; class=&quot;eleven columns&quot; style=&quot;height: 400px;&quot;&gt; &lt;/div&gt;

&lt;p&gt;This assignment turned out to be a surprising amount of fun. For any other music lovers who want to take a dive into their libraries, I’ve got the &lt;a href=&quot;https://github.com/simon-weber/Google-Music-genre-analysis&quot;&gt;source on GitHub&lt;/a&gt;.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Returning to the east</title>
   <link href="http://www.simonmweber.com/2012/08/29/returning-to-the-east.html"/>
   <updated>2012-08-29T00:00:00-04:00</updated>
   <id>http://www.simonmweber.com/2012/08/29/returning-to-the-east</id>
   <content type="html">&lt;h2 id=&quot;returning-to-the-east&quot;&gt;Returning to the east&lt;/h2&gt;

&lt;p class=&quot;meta&quot;&gt;August 29 2012&lt;/p&gt;

&lt;p&gt;I spent the summer as a Google intern at their Mountain View headquarters. Having recently returned to the east, the California sun has now melted into Pennsylvania rain; it’s an anticlimactic end to an otherwise fantastic summer.&lt;/p&gt;

&lt;p&gt;Google exceeded my expectations. I arrived skeptical, expecting to see their world-famous perks masking a typical BigCo™ culture - where engineers are codemonkeys, bureaucracy reigns, and nobody cares about users. I’m happy to report that this is far from the truth. While Google as a whole is definitely nothing like a startup - don’t let anyone tell you otherwise - the culture felt healthy. Googlers constantly stick up for their users, openly asking the hard questions of management.&lt;/p&gt;

&lt;p&gt;Google’s also got an incredible amount of talent, which was reflected in those I worked with. My team was responsible for making Google’s call centers more efficient (yes, Google does pick up the phone, but only for paying customers). I was tasked with a webapp to provide real-time visualization of our call centers’ statuses. This allows managers to intelligently shuffle agents around to different call queues. The entire project was my responsibility: project management, frontend, backend, monitoring, deployment. . .the works. I got to play with all kinds of internal secret-sauce, and despite ballooning requirements, my team and I were really pleased with what I shipped.&lt;/p&gt;

&lt;p&gt;When I wasn’t banging out code, the Google perks didn’t disappoint. Tech talks and events: held daily (one of which &lt;a href=&quot;https://docs.google.com/presentation/d/1grp9hLaYnegWN0rfkZHSfqtYmBme9vtaAoinZmIIqKI/edit&quot;&gt;I presented&lt;/a&gt;). The food: delicious. The booze: classy. Googlers are encouraged (expected?) to work hard and play hard. This seems to align with what I experienced at visits to other bay area companies: GitHub, Quora, LinkedIn, Stripe, Facebook, etc.&lt;/p&gt;

&lt;p&gt;Silicon Valley boasts perfect weather, but the best part was the people. I hung out with a posse of interns from various tech companies. I danced in Google’s SF Pride contingent. I watched classic demoscene while discussing phreaker war stories at &lt;a href=&quot;http://www.hackerdojo.com&quot;&gt;Hacker Dojo&lt;/a&gt;. I ate donut burgers with Zuck, &lt;a href=&quot;http://www.youtube.com/watch?v=SNdcgVTTaS8&quot;&gt;partied&lt;/a&gt; with PJ Hyett, and boogied with Alan Eustace.&lt;/p&gt;

&lt;div class=&quot;figure&quot;&gt;
&lt;p&gt;&lt;img src=&quot;/images/alan_eustace_dancing.jpg&quot; alt=&quot;Alan Eustace and Simon Weber dancing&quot; /&gt;&lt;/p&gt;
&lt;p&gt;No, seriously: this is Alan and me on the dance floor.&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;All these people had a common feature: real passion for their work. Being around this energy has inspired me, so - with my Google legal restrictions lifted - I’m ready to jump into my side projects again. I’ll be maintaining my &lt;a href=&quot;https://github.com/simon-weber/Unofficial-Google-Music-API&quot;&gt;Google Music api&lt;/a&gt;, but probably won’t implement any huge new features. Instead, my time will likely be spent dogfooding it in a project to sync 3rd party media players to Google Music. I’ve already finished a &lt;a href=&quot;https://github.com/simon-weber/Mirror-MediaMonkey-to-Google-Music&quot;&gt;proof of concept&lt;/a&gt;; now it’s time to buckle down and ship something. That is, if this unacceptable (read: not absolutely perfect) east coast weather doesn’t kill me first.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Cold-email a VP, publish in Phrack</title>
   <link href="http://www.simonmweber.com/2012/04/14/email-vps-and-publish-in-phrack.html"/>
   <updated>2012-04-14T00:00:00-04:00</updated>
   <id>http://www.simonmweber.com/2012/04/14/email-vps-and-publish-in-phrack</id>
   <content type="html">&lt;h2 id=&quot;cold-email-a-vp-publish-in-phrack&quot;&gt;Cold-email a VP, publish in Phrack&lt;/h2&gt;

&lt;p class=&quot;meta&quot;&gt;April 14 2012&lt;/p&gt;

&lt;p&gt;In the popular sense of the word, I’m no super-hacker. While coding, my sunglasses and fingerless gloves stay on the desk.&lt;/p&gt;

&lt;div class=&quot;figure&quot;&gt;
&lt;p&gt;&lt;img src=&quot;/images/hackers-trenchcoat.jpg&quot; alt=&quot;Acid Burn (Hackers 1995) in a trench coat&quot; /&gt;&lt;/p&gt;
&lt;p&gt;My collection of sexy trenchcoats is also lacking. (&lt;a href=&quot;http://www.news.com.au/technology/gallery-e6frflwi-1225878429415?page=3&quot;&gt;credit&lt;/a&gt;)&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;Despite these disadvantages, back in September I discovered a &lt;a href=&quot;https://github.com/simon-weber/XSS-over-NBNS&quot;&gt;vulnerability&lt;/a&gt; in the web interface of a Netgear wireless router. Now &lt;a href=&quot;http://www.phrack.com/issues.html?issue=68&amp;amp;id=4#article&quot;&gt;published in Phrack&lt;/a&gt;, my exploit code allowed for nasty things like stealing admin credentials and hiding network devices. I wanted to let Netgear know, so I wrote my first disclosure: a friendly email briefly describing myself, the flaw, and my intentions of publishing.&lt;/p&gt;

&lt;p&gt;Unfortunately, that was the easy part. A Netgear security email was nowhere to be found. In fact, I couldn’t even find a way to submit a support ticket (this has since changed).&lt;/p&gt;

&lt;p&gt;If I could just get my message to a human, I figured it would end up in the right place. After all, who wants to be responsible for blowing off a security flaw? On Netgear’s contact page, I found a press relations email. No response. Investor relations channel? Nope (I must not be rich enough). Support emails found by Googling? Nothing.&lt;/p&gt;

&lt;p&gt;This obviously wasn’t going to work. It was time to pivot.&lt;/p&gt;

&lt;p&gt;Ten more minutes of searching got me everything I needed: the direct emails of five random support staff, plus the most executive position I could muster: the VP of engineering. Brazenly dumping them all in the recipient box, I tried again.&lt;/p&gt;

&lt;p&gt;Within 10 minutes, one of the support staff got back to me, eagerly CC’ing his boss’s boss. I apologized, and my ensuing communication with Netgear was pleasant and to the point.&lt;/p&gt;

&lt;p&gt;My takeaway: it’s easier to beg for forgiveness than to ask for permission. Well, that, and don’t let a lack of fingerless gloves keep you from submitting to Phrack.&lt;/p&gt;
</content>
 </entry>
 
 
</feed>
