Jekyll2023-08-11T08:42:05+00:00https://www.julo.ch/atom.xmljuloThree frequent Mixpanel funnel gotchas2022-07-12T00:00:00+00:002022-07-12T00:00:00+00:00https://www.julo.ch/blog/mixpanel-funnel-gotchas<p>The ones among you that know me well, also know that (a) I like data and (b) I like Mixpanel. Mixpanel is a pretty cool analytics tool<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup> and one of the wonderful features it adds are funnels. Funnels, essentially, help you to figure out to see, what the conversion rates are in a sequence of events and where users drop off, so you can identify improvement potentials.</p>
<p>They seem pretty intuitive to use, which they are, but there is some things which can be pretty confusing and you need to pay attention to, otherwise you might end up with some results that is <em>very</em> different from what you’d expect.</p>
<p>One of the most common issues that I see with funnels is someone doing something along the lines of the following. Let’s say you want to see how many people look at a specific post and then actually end up sharing that post. Your typical PM will build a funnel with something like the following events:</p>
<ol>
<li>Viewed post</li>
<li>Clicked “share post”</li>
<li>Shared post</li>
</ol>
<p>Sounds reasonable, right? User looks at the post, initiates sharing and then actually performs the activity.</p>
<p>The problem here is, that if you do this, you don’t actually know that the user performed all of these actions on the same post. Something like the following might happen without you knowing:</p>
<ol>
<li>User views post A</li>
<li>User clicks on “share post” on post A</li>
<li>User abandons sharing</li>
<li>User views post B</li>
<li>User clicks on “share post” on post B</li>
<li>User share post B</li>
</ol>
<p>With the above setup, if you only look at the single user, your conversion rate is now going to be 100%. Does that sound right to you? Seems inaccurate to me.</p>
<p>The thing here is, Mixpanel can solve for this, you just need to do it. There is multiple things you need to do about the defaults here to address this.</p>
<h2 id="1-counting-users-not-attempts">#1: Counting users, not attempts</h2>
<p>By default Mixpanel will not report the funnel based on how frequently it’s started, but on a per user base. Mixpanel will throw a cookie at the user and if the user performs the funnel start event (view post) multiple times, it will only count as 1.</p>
<div class="image-wrapper">
<img src="/img/mixpanel-entries.png" alt="A mixpanel funnel displaying the option to customise entry counting" />
<p class="image-caption">A mixpanel funnel displaying the option to customise entry counting</p>
</div>
<p>Luckily this is super easy to change (if you know about it) and might give you some interesting insights into how frequently people attempt things multiple times before they actually finish them.</p>
<p>With this change done, you will now see the correct conversion rate of 50%.</p>
<h2 id="2-correct-event-grouping">#2: Correct event grouping</h2>
<p>However, there is still something wrong. Both the time to conversion might be wrong as well as the ability to understand which posts are working well and which ones are not. Why? Because mixpanel will mix events together that don’t belong together. The conversion Mixpanel will see is from “View post A”, to “Click ‘share post’ on post A” to “User shares post B”, instead of looking at A and B separately.</p>
<p>This is probably one of <strong>the most frequent</strong> mistakes I see being made with funnels.</p>
<p>Luckily this is also super easy to fix, presuming you have the right setup going already. Mixpanel allows you to group events together based on a shared property. So if your engineering team did a good job in setting up the tracking, all of the above events will have an event property, that might be called <code class="language-plaintext highlighter-rouge">Post_Id</code> and contains the ID of the post that the event is performed on.</p>
<p>If that’s not the case, you should pop off and tell them to do just that right now. 🙂</p>
<p>If it is already the case, you just need to find the option to set this up, which is hidden away in the “Advanced” menu and called “Holding property constant”. Here you can then select the event property with the post id. If it has not been implemented correctly by your team, you will see your funnel do super weird stuff (as in conversion will drop to 0).</p>
<p>With that now correctly set up, you can then see whether there is patterns to the posts that convert better and you will also get the correct times to completion as well as the correct conversion rates.</p>
<div class="image-wrapper">
<img src="/img/mixpanel-holding-property-constant.png" alt="How to set the constant property" />
<p class="image-caption">How to set the constant property</p>
</div>
<h2 id="3-attribution-touchpoints">#3: Attribution touchpoints</h2>
<p>The last thing that I see frequently go wrong is attribution. A lot of people will build their funnel and then go “okay, let’s throw some UTM breakdowns at this and see where people come from”. However, they don’t notice a rather important flag, which is the tiny, grey “last touch” above it.</p>
<div class="image-wrapper">
<img src="/img/mixpanel-changing-attribution-logic.png" alt="Changing the attribution logic" />
<p class="image-caption">Changing the attribution logic</p>
</div>
<p>Basically, by default, Mixpanel will base breakdowns based on event properties (vs. profile properties) on the last touch. Unfortunately, this is not always what you want. But what does that even mean? If you don’t change anything, Mixpanel will take the UTM parameter from the last event in the funnel (in our example the UTM parameter that was supplied when finishing the signup).</p>
<p>This might be interesting if you’re i.e. looking at figuring out what event pushed a user over the line to complete their purchases (if you are sending emails to remind people about abandoned baskets), but if you are looking at which ad campaigns are converting the best in your signup, it’s likely going to give you wrong results, worse: if you have a multi page signup with full page reloads it might not give you any at all.</p>
<p>Luckily, this one also is really easy to fix! You can just change the attribution using the dropdown. Just think about what you want to measure and you can make it happen.</p>
<div class="footnotes" role="doc-endnotes">
<ol>
<li id="fn:1" role="doc-endnote">
<p>Which also has <strong>great</strong> product direction. Kudos to the PMs at Mixpanel, they discontinued a bunch of features that did not align super well with the core focus of the product (e.g. around messaging) and refined a bunch of the core features (e.g. allowing to customise the periods for retention and much more). They also added a bunch of really cool stuff, like Signal, which is basically impossible to do yourself without a bunch of data scientists. I only wish they paid a little more attention to the JQL feature, which is SUPER useful when you really need to do something a bit more crazy. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
</ol>
</div>The ones among you that know me well, also know that (a) I like data and (b) I like Mixpanel. Mixpanel is a pretty cool analytics tool1 and one of the wonderful features it adds are funnels. Funnels, essentially, help you to figure out to see, what the conversion rates are in a sequence of events and where users drop off, so you can identify improvement potentials. Which also has great product direction. Kudos to the PMs at Mixpanel, they discontinued a bunch of features that did not align super well with the core focus of the product (e.g. around messaging) and refined a bunch of the core features (e.g. allowing to customise the periods for retention and much more). They also added a bunch of really cool stuff, like Signal, which is basically impossible to do yourself without a bunch of data scientists. I only wish they paid a little more attention to the JQL feature, which is SUPER useful when you really need to do something a bit more crazy. ↩A skeleton for product management interview cases2022-06-23T00:00:00+00:002022-06-23T00:00:00+00:00https://www.julo.ch/blog/product-management-case-skeleton<p>So recently I’ve been thinking a lot about product management interview processes, both the ones that I have put together myself as well as the ones that I went through in the past. A popular element of most of these interview processes is a case of some sort.</p>
<p>Usually the interviewing company will try to provide a case that is somewhat related to the business, e.g. they might give you a problem that they have solved recently in the past or something that they are considering to solve for in the future.<sup id="fnref:is-this-just-free-labour" role="doc-noteref"><a href="#fn:is-this-just-free-labour" class="footnote" rel="footnote">1</a></sup> This is usually beneficial and insightful for both sides:</p>
<ul>
<li>The company gets to see how well you adapt to their problem space and how much you already know, can extrapolate or transfer from past experiences</li>
<li>You get a really good look into what they’d consider interesting problems. The difficulty of the case (if you are certain you can read that), can tell you a lot about their expectations.</li>
</ul>
<p>Interestingly enough I think after having gone through a bunch and also having seen a bunch (both successful and unsuccessful on both ends), I do see some kind of a skeleton for good cases. Every good solution (no matter whether it’s a presentation or a ) will cover roughly the following topics:</p>
<ul>
<li>A <strong>quick summary of the case</strong> sticking to the facts, no interpretation or anything. This makes sure the whole audience knows the deal, no matter whether they read the case beforehand or not, and also demonstrates that you understood the case.</li>
<li>
<p>The “<strong>things I’d do if this was real</strong>“-section: This is the section where you basically unpack your methodology kit and show all the things that you skipped because it’s an interview without saying that the case is not a real product management situation but purely hypothetical.</p>
<p>Mention all the validation, discovery, opportunity sizing and other things you would do in this place. You can even throw in that you would base the scope of discovery work on how quick a solution could be implemented and how important this problem is for the company.</p>
</li>
<li>
<p><strong>Assumptions</strong>: The previous section is methodology, this one is content. You’re basically saying “here are all the things that I am taking for granted that I’d usually validate one way or another”.</p>
<p>This might be the most important section, since that’s often the difference between a successful and an unsuccessful interview. What frequently happens if you don’t do this, is that the interviewer disagrees with your conclusions and thinks you are wrong. When you make your assumptions explicit and your analysis is logical, nothing can really go wrong. The interview can call you on your assumptions, but when they do that, they will have to tell you what to assume instead and you can change up your answers based on the new assumptions.
This section is also fun for the interviewer, what I frequently like to do with candidates, is throw some of the assumptions over and see how they react. Can they think on the spot and come up with alternatives or are they stuck with what they come up with? Tells a lot about the candidate.</p>
</li>
<li><strong>Analysis</strong>: You analyse the information in the case and prepare an analysis of what you think the problem is, basing things on your assumptions. Shows you took in all the information and managed to interpret it.</li>
<li>
<p><strong>Solution(s)</strong>: You propose one or more solutions that work well given the analysis you just performed. Make sure to link these back to your assumptions also to make them solid.</p>
<p>The scope of the solution is often pretty different, sometimes you’ll be expected to make some wireframes or some kind of simple prototype, sometimes they’ll just expect some specs.
What I’d recommend in any situation is to keep an eye on data here. Mostly people will expect you to explain how you will know whether this solution is actually doing the job it’s supposed to, ideally by the way of KPIs: Define some measurements of how you know if it succeeds or fails, e.g. a retention, conversion or adoption metric. This is also a good point in time to show off your knowledge about analytics, you might throw in a balancing KPI or talk about leading or lagging metrics.
Generally throwing in a comment a la “this would be an initial approach that we’d plan on shipping fast, but based on the data we’d like to iterate quickly”. You might want to adapt that comment though depending on your audience, it might not work so well in more corporate, risk-averse or hardware product companies.</p>
</li>
<li>
<p><strong>Implementation</strong>: Not always, but sometimes, someone will ask you for a roadmap or an implementation plan or similar, maybe even an estimate of how long you think it will take to implement. Some might disagree with me on that, but I’d generally advise to NOT include any dates or specific timelines. It will be really hard to not set yourself up for failure by doing that. The likelihood you end up where they think is reasonable, is incredibly low and it’s generally not something you should do as a PM.</p>
<p>If someone pushes you on this, don’t let them put you in a corner and stick your ground on that (you’ll definitely win over the engineers in the audience this way). Instead I recommend talking about phases, stages or iterations, give a general sense that the speed can be impacted in many ways, but you’d really want to talk to the engineering team first and dig a bit deeper into the requirements before giving an estimate.<sup id="fnref:no-estimate-required" role="doc-noteref"><a href="#fn:no-estimate-required" class="footnote" rel="footnote">2</a></sup></p>
</li>
</ul>
<p>There will be some variance depending on the case, sometimes there will be specific aspects empathised or things requested to be included specifically (or excluded specifically), but generally if you cover all the above points you should hit a lot of the requirements of any of the cases. In any case: best of luck!</p>
<div class="footnotes" role="doc-endnotes">
<ol>
<li id="fn:is-this-just-free-labour" role="doc-endnote">
<p>In the past I’ve received questions from candidates that seemed concerned that I am just using them for free labour, which definitely isn’t the case. If you can solve for the problem better in just a few hours, than the company could in a long time, something’s off. I’m not going to say what it is, but you definitely should think about it. <a href="#fnref:is-this-just-free-labour" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:no-estimate-required" role="doc-endnote">
<p>Unless of course your solution doesn’t involve any engineering in the first iteration, or you proposed to make more discovery first (likely a good idea), in which case you should have a rough idea ready. <a href="#fnref:no-estimate-required" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
</ol>
</div>So recently I’ve been thinking a lot about product management interview processes, both the ones that I have put together myself as well as the ones that I went through in the past. A popular element of most of these interview processes is a case of some sort.Ruby Gems and IPv62022-06-22T00:00:00+00:002022-06-22T00:00:00+00:00https://www.julo.ch/blog/gem-and-ipv6<p>Can’t believe this is real, apparently since 2018 you can’t install gems on a machine that has working IPv6 support? There is a trillion fixes and workaround around on the internet, but the only one I could get to work was turn off IPv6 support on my mac, when the actual fix would really have to be done by <a href="rubygems.org">rubygems.org</a>: either remove the <code class="language-plaintext highlighter-rouge">AAAA</code> records on your domain or actual set up IPv6 (preferably the latter of course).</p>
<p>Truly shocked that such a popular tool hasn’t been fixed within 4 years..</p>Can’t believe this is real, apparently since 2018 you can’t install gems on a machine that has working IPv6 support? There is a trillion fixes and workaround around on the internet, but the only one I could get to work was turn off IPv6 support on my mac, when the actual fix would really have to be done by rubygems.org: either remove the AAAA records on your domain or actual set up IPv6 (preferably the latter of course).The new puritans2021-09-16T00:00:00+00:002021-09-16T00:00:00+00:00https://www.julo.ch/blog/mob-justice<p>An excellent article from my point of view. It’s a bit long, so you might want to skip certain parts, but it hits home the part that is really important: these days no one is safe from judgement, independent of whether they actually did the deed and whether it even was a deed at all (at least in the criminal/legal sense).</p>
<p>On top of that the consequences are as far-reaching as they ever have been, you can lose your job for something that has nothing to do with your work and much more. Standing up for people that have been falsely accused and wrongly punished essentially leads to you being accused of being guilty by assocation.</p>
<p>It’s a scary time we live in, indeed.</p>An excellent article from my point of view. It’s a bit long, so you might want to skip certain parts, but it hits home the part that is really important: these days no one is safe from judgement, independent of whether they actually did the deed and whether it even was a deed at all (at least in the criminal/legal sense).A look inside apple’s way of thinking about products2021-05-09T00:00:00+00:002021-05-09T00:00:00+00:00https://www.julo.ch/blog/apple-product-thinking<p>I think one of the most interesting things about the whole epic vs. apple trial is that it affords us a quite significant insight on discussions that are happening within Apple, that usually are not at all visible to us.</p>
<p>What makes these insights extra fun is that it’s likely they are actually authentic, since we’re seeing emails that were written in the past in a different context. While we have to be aware of the fact, that obviously we are dealing with professionals here, so they are careful what they write in emails, exactly in preparation for this kind of situation, given that the discussions are internal, we actually can learn a lot about the thought process and decision making process at Apple.</p>
<p>All that being said, there are some fun things that I noticed while reading some of the press on the case, specifically when it comes to advertising on the App Store. There are a couple of things that I’d like to quote here, that really struck a chord with me.</p>
<blockquote>
<p>Yes, the ability to pay for promotion would be awesome. We’ve floated it several times as the way to end chart gaming: if people are willing to pay “marketing companies” (bot nets) to gain position, why don’t we just let them pay us to gain position?</p>
</blockquote>
<p>So this one is wow. Friedman<sup id="fnref:friedman-background" role="doc-noteref"><a href="#fn:friedman-background" class="footnote" rel="footnote">1</a></sup> wrote that he’s interested in Ads for one reason and the reason is not that it would make them money, the reason is that it would stop people from gaming the charts. That way they’d actually help people find the things they are looking for more easily.</p>
<p>Quite interestingly, the only reason they had not done it apparently, is appearances:</p>
<blockquote>
<p>The devs would love it. The problem is that Tim is telling the world that we make great products without monetizing users. Ads would be weirdly at odds with that.</p>
</blockquote>
<p>Super funny, how here their PR was standing in the way of their fixing an issue by introducing Ads.</p>
<p>However, what interests me even more is the question of discovery, that is being raised in the same email:</p>
<blockquote>
<p>But in the App Store I don’t only want to know what is popular. I want apps that are high quality, well looked after by engaged developers, and retained (because useful) by other users. Being popular within a category is a nice to have and should mostly correlate with the other values I described.</p>
</blockquote>
<p>I actually think they found part of the picture here: A high quality app that is well looked after is definitely something that I care about, however, it seems more like a secondary criteria. Usually an app would have to solve a problem or give me something that I want, even if it’s one I am not even aware of yet. What good is it to me, that the app will be well looked after, if I don’t need it in the first place.</p>
<p>However, I am not really sure how either ads nor a list of popular apps will solve for this discovery problem. Interestingly enough, personalization might actually solve for this problem, just that it’s something else that Apple cannot offer, since it conflicts with their values.</p>
<p>If a lot of people that are similar to me (e.g. by having similar apps), have an app that I don’t have, that will mean there’s a good chance I will want that app as well. I am not sure how this could be solved for technically without ending up like Google’s FLoC, but if it can be solved in a way that does not compromise privcay, it would surely make the App Store significantly more valuable than the current overview of featured apps & games does.</p>
<div class="footnotes" role="doc-endnotes">
<ol>
<li id="fn:friedman-background" role="doc-endnote">
<p>Who’s head of Apple’s Fraud Engineering Algorithms and Risk unit. <a href="#fnref:friedman-background" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
</ol>
</div>I think one of the most interesting things about the whole epic vs. apple trial is that it affords us a quite significant insight on discussions that are happening within Apple, that usually are not at all visible to us.macOS: wifi jitter2021-03-06T00:00:00+00:002021-03-06T00:00:00+00:00https://www.julo.ch/blog/wifi-jitter<p>Since I’ve been spending a good amount of the pandemic playing games using the cloud gaming offering, I have been somewhat dependent on a good internet connection. While pure bandwidth is important (both down- and upstream), the latency and stability of your connection are even more important.</p>
<p>This is mostly due to the fact, that with high latency your actions and the results are going to feel disjointed, which makes playing super frustrating and a lack of stability just means that you can’t even adjust to this, since sometimes it’ll happen or not.</p>
<p>Often this phenomenon of rapidly changing transmission speed on your connection is referred to as <strong>jitter</strong>.</p>
<p>For reasons, a tethered connection, which is definitely the best solution to get a highly reliable connection, was not an option, so I’ve been stuck with wifi.</p>
<p>On my mac I’ve seen two very interesting patterns, in terms of latency, that I’d like to share so that other people know how to deal with them:</p>
<h2 id="generally-low-latency-with-very-regular-spikes-of-high-latency">Generally low latency with very regular spikes of high latency</h2>
<p>When you have a healthy baseline latency but in regular intervals (e.g. every 15 minutes), this is very likely caused by location services on your mac. The mac does some magic in the background to determine your location based on the wifis available around you and for that to work it will temporarily suspend your connection.</p>
<p>To verify this, you can turn off location services completely (you can always turn them back on after). Find them under <strong>System Preferences → Security & Privacy → Privacy → Location Services</strong>. Just use the master switch.</p>
<div class="image-wrapper">
<img src="/img/location-services-preferences.png" alt="Location services preferences on macOS" />
<p class="image-caption">Location services preferences on macOS</p>
</div>
<h2 id="your-latency-is-normally-fine-but-completely-nuts-sometimes">Your latency is normally fine, but completely nuts sometimes</h2>
<p>This happened to me a couple of times. Sometimes I had no jitter whatsoever, with a nice 20ms response times, sometimes I had high jitter and 100ms response times, which is unfortunately completely unplayable. I did a lot of research on this and by digging into various log files, there seemed to be a correlation with airdrop activity happen. There is no UI to turn this off easily in the preferences, however you can verify this easily using the command line. Simply run</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>ifconfig awdl0 down
</pre></td></tr></tbody></table></code></pre></div></div>
<p>This should, if the underlying cause is airdrop, instantly fix this issue. While this is disabled, airdrop, icloud copy&paste and similar things will not work however. You can always turn this back on using the following command:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>ifconfig awdl0 up
</pre></td></tr></tbody></table></code></pre></div></div>Since I’ve been spending a good amount of the pandemic playing games using the cloud gaming offering, I have been somewhat dependent on a good internet connection. While pure bandwidth is important (both down- and upstream), the latency and stability of your connection are even more important.Cloud gaming: an overview2020-04-19T00:00:00+00:002020-04-19T00:00:00+00:00https://www.julo.ch/blog/cloud-gaming<p>I have recently, once more, become more interested in playing some games. While I really enjoy playing on my Nintendo Switch (especially while travelling), I realized I really miss playing a bit more strategically involved games (e.g. Civilization). However, I also noticed that playing them on my Macbook is just not as much fun, given that the performance is not really there and a lot of the games don’t work super well.</p>
<p>Accordingly I evaluated my options, of which there mostly seemed to be two:</p>
<ul>
<li>Buying some degree of a gaming computer</li>
<li>Trying out cloud gaming</li>
</ul>
<p>I don’t really play enough that I felt I could justify a gaming computer, however the idea of cloud gaming had intrigued me before. I had in the past already tested a setup that would allow me to play using AWS spot instances for little money, but somehow the offering didn’t really do it for me at the time. I did however decide to give it another shot.</p>
<p>So I started looking around and evaluating a couple of players and their offerings out there. I evaluated the following options in some more depth and want to give you the ups and downs as I have perceived them so far.</p>
<p>The options I tested myself are these:</p>
<ul>
<li><a href="https://parsecgaming.com/">Parsec</a> & <a href="https://aws.amazon.com/">AWS</a></li>
<li><a href="https://parsecgaming.com/">Parsec</a> & <a href="https://www.paperspace.com/">Paperspace</a></li>
<li><a href="https://www.nvidia.com/en-eu/geforce-now/">Geforce NOW</a></li>
</ul>
<p>I also pre-ordered a <a href="https://shadow.tech/int/discover">Shadow Boost</a>, however they have not managed to fulfill my order yet and they seem to struggle significantly under the current corona virus caused demand and I am not sure they will fulfill my order any time soon.</p>
<p>There were two other players that I looked at but already discarded before it got to the testing phase, due to some concerns:</p>
<ul>
<li><a href="https://welcome.playkey.net">PlayKey</a>: Is a cloud gaming service that somehow failed to convince me that I wouldn’t regret using it by having my credentials stolen. Googling for it also returned a bunch of negative feedback so I decided against testing it.</li>
<li><a href="https://stadia.google.com/">Google Stadia</a>: Didn’t have any of the games I was interested in, so I didn’t even consider it seriously.</li>
</ul>
<h2 id="criterias-to-consider--scale">Criterias to consider & scale</h2>
<p>So while trying out and evaluating the various offerings out there, I came up with the following, hopefully useful criteria, that should help everyone to find the solution that works best for them, independently of whether it is the one I prefer the most.</p>
<table>
<thead>
<tr>
<th>Criteria</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>Setup</td>
<td>When you want to get started initially, how much time/work is it going to be?</td>
</tr>
<tr>
<td>Storage</td>
<td>Do you have storage restrictions? If so, what kind?</td>
</tr>
<tr>
<td>Maintenance</td>
<td>Is there any maintenance work necessary? If so, what kind?</td>
</tr>
<tr>
<td>Resolutions</td>
<td>What resolutions can you play in? Are there restrictions?</td>
</tr>
<tr>
<td>Performance</td>
<td>What is the performance of the games itself, any issues?</td>
</tr>
<tr>
<td>Streaming</td>
<td>How well does the streaming work, any lag? Quality?</td>
</tr>
<tr>
<td>Usability</td>
<td>What is it like interacting with games? Any issues? How quickly can you start your game?</td>
</tr>
<tr>
<td>Pricing</td>
<td>How much does it cost?</td>
</tr>
<tr>
<td>Availability</td>
<td>Can you even get started at all?</td>
</tr>
<tr>
<td>Compatibility</td>
<td>Which platforms can you use this on?</td>
</tr>
<tr>
<td>Games</td>
<td>Can you play whatever you want? Any game restrictions?</td>
</tr>
</tbody>
</table>
<p>I used the following, really rough scale when evaluating the above criteria:</p>
<table>
<thead>
<tr>
<th> </th>
<th>Rating</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>⭐️</td>
<td>Great</td>
<td>When there is no issues and something is in line with my expectations.</td>
</tr>
<tr>
<td>🤷♂️</td>
<td>Okay</td>
<td>It’s not great, but I could live with this.</td>
</tr>
<tr>
<td>🚨</td>
<td>Beware</td>
<td>This was a showstopper for me, so you might want to consider it.</td>
</tr>
<tr>
<td>❔</td>
<td>Could not evalulate</td>
<td>Some stuff I could not test/find info on, in which case I marked it accordingly.</td>
</tr>
</tbody>
</table>
<h2 id="ratings-overview">Ratings overview</h2>
<p>For your convenience I included an overview table of the results of my tests & research here. If you are interested in the reasoning behind the evaluation, you can find more details in the respective section.</p>
<table>
<thead>
<tr>
<th>Criteria</th>
<th>Geforce NOW</th>
<th>Parsec & AWS</th>
<th>Parsec & Paperspace</th>
<th>Shadow</th>
</tr>
</thead>
<tbody>
<tr>
<td>Setup</td>
<td>⭐️</td>
<td>🚨</td>
<td>🤷♂️</td>
<td>⭐️</td>
</tr>
<tr>
<td>Storage</td>
<td>⭐️</td>
<td>🤷♂️</td>
<td>🤷♂️</td>
<td>🤷♂️</td>
</tr>
<tr>
<td>Maintenance</td>
<td>⭐️</td>
<td>🤷♂️</td>
<td>🤷♂️</td>
<td>🤷♂️</td>
</tr>
<tr>
<td>Resolutions</td>
<td>🤷♂️</td>
<td>⭐️</td>
<td>⭐️</td>
<td>🤷♂️</td>
</tr>
<tr>
<td>Performance</td>
<td>⭐️</td>
<td>⭐️</td>
<td>⭐️</td>
<td>❔</td>
</tr>
<tr>
<td>Streaming</td>
<td>⭐️</td>
<td>❔</td>
<td>⭐️</td>
<td>❔</td>
</tr>
<tr>
<td>Usability</td>
<td>⭐️</td>
<td>🤷♂️</td>
<td>🤷♂️</td>
<td>⭐️</td>
</tr>
<tr>
<td>Pricing</td>
<td>⭐️</td>
<td>🤷♂️</td>
<td>🤷♂️</td>
<td>🤷♂️</td>
</tr>
<tr>
<td>Availability</td>
<td>🚨</td>
<td>⭐️</td>
<td>⭐️</td>
<td>🚨</td>
</tr>
<tr>
<td>Compatibility</td>
<td>🤷♂️</td>
<td>🤷♂️</td>
<td>🤷♂️</td>
<td>🤷♂️</td>
</tr>
<tr>
<td>Games</td>
<td>🚨</td>
<td>⭐️</td>
<td>⭐️</td>
<td>⭐️</td>
</tr>
</tbody>
</table>
<h3 id="geforce-now"><a href="https://www.nvidia.com/en-eu/geforce-now/">Geforce NOW</a></h3>
<p>So this was my favorite contender for a while. I get on it really quickly with no issues whatsoever, could just launch any game pretty much instantly, didn’t have to worry about anything really. But then they pulled all the games I wanted to play, so it become a no-go for me.</p>
<table>
<thead>
<tr>
<th>Criteria</th>
<th> </th>
<th>Explanation</th>
</tr>
</thead>
<tbody>
<tr>
<td>Setup</td>
<td>⭐️</td>
<td>Just install the app and you’re good to go!</td>
</tr>
<tr>
<td>Storage</td>
<td>⭐️</td>
<td>Nope, just play whatever you want!</td>
</tr>
<tr>
<td>Maintenance</td>
<td>⭐️</td>
<td>None, just works.</td>
</tr>
<tr>
<td>Resolutions</td>
<td>🤷♂️</td>
<td>Up to FullHD, none more than that.</td>
</tr>
<tr>
<td>Performance</td>
<td>⭐️</td>
<td>Seemed to work perfectly fine based on what I can tell.</td>
</tr>
<tr>
<td>Streaming</td>
<td>⭐️</td>
<td>Worked flawlessly for me, no lag, no issues.</td>
</tr>
<tr>
<td>Usability</td>
<td>⭐️</td>
<td>Just launch the app, select your game and go, it doesn’t get much better than this!</td>
</tr>
<tr>
<td>Pricing</td>
<td>⭐️</td>
<td>There is a free plan available, with restrictions. Founders edition is 5.49€/month, pretty cheap in comparison to most.</td>
</tr>
<tr>
<td>Availability</td>
<td>🚨</td>
<td>As of right now, you can only get into the free plan, nothing else.</td>
</tr>
<tr>
<td>Compatibility</td>
<td>⭐️</td>
<td>They have apps for all the usual platforms, no issues here.</td>
</tr>
<tr>
<td>Games</td>
<td>🚨</td>
<td>This is where it fell apart for me. Recently <a href="https://www.theverge.com/2020/3/2/21161469/nvidia-geforce-now-cloud-gaming-service-developers-controversy-licensing">a bunch of studios have pulled their games from Geforce NOW</a>, so make sure that the games you want to play are available first.</td>
</tr>
</tbody>
</table>
<h3 id="parsec">Parsec</h3>
<p>So in general, <a href="https://parsecgaming.com/">Parsec</a> was not super intuitive to get ready. Their website doesn’t clearly say that it is going to offer you functionality to cloud game, it looks more like a tool that you can use to play together with friends. Even in their helpcenter most of the documentation on this functionality is pretty hidden, however it works super well. The just offer the software to do remote gaming, streaming, controls and stuff, the infrastructure you have to get from somewhere else. I tried two providers and I will evaluate them separately below.</p>
<h4 id="parsec--aws">Parsec & AWS</h4>
<p>I tried to set up a VM on AWS for Parsec and I failed to do it properly. I would probably not recommend doing this and instead suggest you try paperspace instead.</p>
<table>
<thead>
<tr>
<th>Criteria</th>
<th> </th>
<th>Explanation</th>
</tr>
</thead>
<tbody>
<tr>
<td>Setup</td>
<td>🚨</td>
<td>I failed here. I tried to use <a href="https://support.parsecgaming.com/hc/en-us/articles/115003642812-Using-The-Parsec-AMI-On-Amazon">the guide referencing an AMI for AWS</a>, which didn’t exist anymore and I didn’t manage to pull it together manually. Feel free to try it with the more up-to-date documentation, but it seems like a pain.</td>
</tr>
<tr>
<td>Storage</td>
<td>🤷♂️</td>
<td>You’ll have to pay for however much you need, you can provision as much as you want, but it will cost you also.</td>
</tr>
<tr>
<td>Maintenance</td>
<td>🤷♂️</td>
<td>You basically have a VM, so you will have to do whatever you’d do for a real PC as well.</td>
</tr>
<tr>
<td>Resolutions</td>
<td>⭐️</td>
<td>You can configure whatever the underlying machine can handle.</td>
</tr>
<tr>
<td>Performance</td>
<td>⭐️</td>
<td>Just pick the machine that you need!</td>
</tr>
<tr>
<td>Streaming</td>
<td>❔</td>
<td>Didn’t test this, so I cannot tell you what it will be like.</td>
</tr>
<tr>
<td>Usability</td>
<td>🤷♂️</td>
<td>You will have to launch your VM first and then open it in Parsec and then launch your game (and do the reverse when you’re done). It could be worse, but it could be better also.</td>
</tr>
<tr>
<td>Pricing</td>
<td>🤷♂️</td>
<td>You will have to pay a fixed fee for your storage (or re-provision your machine every time you play) plus an hourly fee for playing. Which means you pay as much as you play.</td>
</tr>
<tr>
<td>Availability</td>
<td>⭐️</td>
<td>You’re good to go whenever you want to, no issues.</td>
</tr>
<tr>
<td>Compatibility</td>
<td>🤷♂️</td>
<td>Parsec on the desktop is no issues, couldn’t find any clients for mobile or the likes.</td>
</tr>
<tr>
<td>Games</td>
<td>⭐️</td>
<td>It’s a full VM, so you can install whatever you like.</td>
</tr>
</tbody>
</table>
<h4 id="parsec--paperspace">Parsec & Paperspace</h4>
<p>I alternatively tried Paperspace and it worked fairly well, I would recommend doing this! This is also the setup I use at the moment with very little issues.</p>
<table>
<thead>
<tr>
<th>Criteria</th>
<th> </th>
<th>Explanation</th>
</tr>
</thead>
<tbody>
<tr>
<td>Setup</td>
<td>🤷♂️</td>
<td>It’s not super intuitive, but it works. There is <a href="https://support.parsecgaming.com/hc/en-us/articles/115002698512-Using-Your-Paperspace-Cloud-Machine-With-Parsec">a guide available</a>, that I’d recommend to follow. However you need to file a support ticket with the Paperspace team for them to allow you to get the right kind of instance.</td>
</tr>
<tr>
<td>Storage</td>
<td>🤷♂️</td>
<td>You’ll have to pay for however much you need, you can provision as much as you want, but it will cost you also.</td>
</tr>
<tr>
<td>Maintenance</td>
<td>🤷♂️</td>
<td>You basically have a VM, so you will have to do whatever you’d do for a real PC as well.</td>
</tr>
<tr>
<td>Resolutions</td>
<td>⭐️</td>
<td>You can configure whatever the underlying machine can handle.</td>
</tr>
<tr>
<td>Performance</td>
<td>⭐️</td>
<td>Just pick the machine that you need!</td>
</tr>
<tr>
<td>Streaming</td>
<td>⭐️</td>
<td>For me this worked super well, however your mileage may vary. It will mostly be dependent on how far you are away from the datacenter where your VM is.</td>
</tr>
<tr>
<td>Usability</td>
<td>🤷♂️</td>
<td>You will have to launch your VM first and then open it in Parsec and then launch your game (and do the reverse when you’re done). It could be worse, but it could be better also.</td>
</tr>
<tr>
<td>Pricing</td>
<td>🤷♂️</td>
<td>You will have to pay a fixed fee for your storage (or re-provision your machine every time you play) plus an hourly fee for playing. Which means you pay as much as you play.</td>
</tr>
<tr>
<td>Availability</td>
<td>⭐️</td>
<td>You’re good to go whenever you want to, no issues.</td>
</tr>
<tr>
<td>Compatibility</td>
<td>🤷♂️</td>
<td>Parsec on the desktop is no issues, couldn’t find any clients for mobile or the likes.</td>
</tr>
<tr>
<td>Games</td>
<td>⭐️</td>
<td>It’s a full VM, so you can install whatever you like.</td>
</tr>
</tbody>
</table>
<h3 id="shadow">Shadow</h3>
<p><a href="https://shadow.tech/int/discover">Shadow</a> is a dedicated cloud gaming provider. I have not been able to test this yet, since my pre-order that was supposed to be ready in March is still not ready yet. So all of the information below is based on information I pulled together. I will update this article if I ever get access.</p>
<table>
<thead>
<tr>
<th>Criteria</th>
<th> </th>
<th>Explanation</th>
</tr>
</thead>
<tbody>
<tr>
<td>Setup</td>
<td>⭐️</td>
<td>Just install the app and you’re good to go!</td>
</tr>
<tr>
<td>Storage</td>
<td>🤷♂️</td>
<td>The base plan comes with 256GB of storage, which means you will likely need more, however availability seems limited at the moment.</td>
</tr>
<tr>
<td>Maintenance</td>
<td>🤷♂️</td>
<td>You basically have a VM, so you will have to do whatever you’d do for a real PC as well.</td>
</tr>
<tr>
<td>Resolutions</td>
<td>🤷♂️</td>
<td>Boost (the base plan) is limited to FullHD, you can get better resolution with higher plans, however they are very expensive.</td>
</tr>
<tr>
<td>Performance</td>
<td>❔</td>
<td>Didn’t test this, so I cannot tell you what it will be like.</td>
</tr>
<tr>
<td>Streaming</td>
<td>❔</td>
<td>Didn’t test this, so I cannot tell you what it will be like.</td>
</tr>
<tr>
<td>Usability</td>
<td>⭐️</td>
<td>Just launch into your VM using the app and you can do whatever you want, pretty easy!</td>
</tr>
<tr>
<td>Pricing</td>
<td>🤷♂️</td>
<td>So the base plan is pretty reasonable, at 17CHF (where I am), however the higher plans with better resolutions are fairly pricey. On the other hand, at least your costs are fixed, so you can play as much as you want!</td>
</tr>
<tr>
<td>Availability</td>
<td>🚨</td>
<td>This is a major issue at the moment, depending on where you are. Make sure to check out <a href="https://www.reddit.com/r/ShadowPC/">the subreddit</a> as well as <a href="https://community.shadow.tech/gben/blog/update/covid-19-impacts-on-shadow-regular-updates">their blog</a> to see how long you might wait, some people seem to have been waiting since months.</td>
</tr>
<tr>
<td>Compatibility</td>
<td>🤷♂️</td>
<td>This would have been a star, but it looks like Apple pulled their iOS app, so no more Shadow on your iPhone/iPad. If they manage to bring it back, I will upgrade this to a star.</td>
</tr>
<tr>
<td>Games</td>
<td>⭐️</td>
<td>It’s a full VM, so you can install whatever you like.</td>
</tr>
</tbody>
</table>
<h2 id="summary">Summary</h2>
<p>So in summary: If you are relatively technically versatile, I’d recommend you to go for a setup using Parsec & Paperspace right now, if you are not versatile, check out the other options to see what fits you the best, based on the available games and how quickly you need your setup.</p>I have recently, once more, become more interested in playing some games. While I really enjoy playing on my Nintendo Switch (especially while travelling), I realized I really miss playing a bit more strategically involved games (e.g. Civilization). However, I also noticed that playing them on my Macbook is just not as much fun, given that the performance is not really there and a lot of the games don’t work super well.Magic caddy proxy for docker containers2016-05-21T00:00:00+00:002016-05-21T00:00:00+00:00https://www.julo.ch/blog/docker-caddy-proxy<p>Recently I have played a lot more with docker then I might have let on in my blog. It’s pretty much one of the main things I have done recently, at least technology wise. Among other stuff I have pretty much moved everything I do or deploy to docker containers, and preferably combined webservices with caddy (and thus letsencrypt) to be handled pretty much automatically.</p>
<p>This might also be guessable from some of the docker images I have recently created, among which are the following:</p>
<ul>
<li><a href="https://hub.docker.com/r/alexanderjulo/caddy/">alexanderjulo/caddy</a>: Just another caddy docker image, based on alpine linux, so that debugging can be done in the container, if necessary (busybox included)</li>
<li><a href="https://hub.docker.com/r/alexanderjulo/caddy-gen/">alexanderjulo/caddy-gen</a>: a docker image basing on docker-gen that automatically creates a caddy configuration file given containers that fulfill certain criteria</li>
<li><a href="https://hub.docker.com/r/alexanderjulo/caddy-proxy-flex/">alexanderjulo/caddy-proxy-flex</a>: a not so automatic caddy proxy that can be used as an intermediate in certain cases</li>
</ul>
<p>Just on their own these images probably do not sound like they make a lot of sense, but whenever you will start to deploy your containers to production you will probably run into some questions. One of the main problems is that if you run a lot of webservices, you will probably want all of them running on the default ports (80/443) to be accessible to other people, but only one container can listed on either one of these ports.</p>
<p>An easy solution would obviously be to have a manually setup web server (either in a container) or directly on the host. This means adapting the configuration of this webserver whenever you decide to change a container.</p>
<p>Another solution would be <a href="https://github.com/jwilder/nginx-proxy">nginx-gen</a>, which runs an nginx and automatically updates it with a new configuration whenever a new relevant container comes on or goes off. The problem with this setup is, that the setup does not support automatic SSL certificate generation & renewal with letsencrypt. This can be solved by using <a href="https://github.com/JrCs/docker-letsencrypt-nginx-proxy-companion">a companion</a>, which unfortunately is rather complicated to setup and run.</p>
<p>The third, and my personal favorite option, is to make this whole thing very much easier by using docker-gen and caddy.</p>
<h2 id="setup-caddy-proxy">Setup caddy-proxy</h2>
<p>In the most basic setup, you need 3 containers:</p>
<ol>
<li>A container running your service that is expecting traffic on a port of your choosing (let’s call it <code class="language-plaintext highlighter-rouge">X</code>). It needs two environmental variables set, <code class="language-plaintext highlighter-rouge">VIRTUAL_HOST</code>, which should contain the host you want the app to be available at, i.e. <code class="language-plaintext highlighter-rouge">www.julo.ch</code> and <code class="language-plaintext highlighter-rouge">SERVER_PORT</code>, which is the port <code class="language-plaintext highlighter-rouge">X</code> where your service is available.</li>
<li>A container running caddy that has listening on port 80 and 443 on the host, it needs a volume shared to the place where caddy expects the configuration file, if you are using my <code class="language-plaintext highlighter-rouge">alexanderjulo/caddy</code> that would be <code class="language-plaintext highlighter-rouge">/srv</code>, to get it’s configuration. And optionally a second volume share to <code class="language-plaintext highlighter-rouge">/root/.caddy/letsencrypt</code> to back up the configuration to the host.</li>
<li>A container that will automatically generate a caddy configuration, i.e. <code class="language-plaintext highlighter-rouge">alexanderjulo/caddy-gen</code>, which has a volume share with the docker socket (read only) to be able to listen to the docker events and know about starting and stopping containers and a volume share to the caddy configuration directory from #2 shared to <code class="language-plaintext highlighter-rouge">/etc/caddy</code>. Additionally an environmental variable <code class="language-plaintext highlighter-rouge">LETSENCRYPT_EMAIL</code> that contains your email address for the SSL certificates.</li>
</ol>
<p>While this might sound very complicated, if we put it into a docker-compose file i.e., it becomes rather simple:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
</pre></td><td class="rouge-code"><pre><span class="na">version</span><span class="pi">:</span> <span class="s2">"</span><span class="s">2"</span>
<span class="na">services</span><span class="pi">:</span>
<span class="na">app</span><span class="pi">:</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">tutum/hello-world</span>
<span class="na">environment</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">VIRTUAL_HOST="app.example.com"</span>
<span class="pi">-</span> <span class="s">SERVER_PORT=80</span>
<span class="na">caddy-gen</span><span class="pi">:</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">alexanderjulo/caddy-gen:latest</span>
<span class="na">volumes</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">/var/run/docker.sock:/var/run/docker.sock:ro</span>
<span class="pi">-</span> <span class="s">config:/etc/caddy</span>
<span class="na">caddy</span><span class="pi">:</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">alexanderjulo/caddy</span>
<span class="na">command</span><span class="pi">:</span> <span class="s">caddy -restart=inproc -http2=false</span>
<span class="na">ports</span><span class="pi">:</span>
<span class="pi">-</span> <span class="m">80</span>
<span class="pi">-</span> <span class="m">443</span>
<span class="na">volumes</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">config:/srv</span>
<span class="pi">-</span> <span class="s">./ssl:/root/.caddy/letsencrypt</span>
<span class="na">volumes</span><span class="pi">:</span>
<span class="na">config</span><span class="pi">:</span>
<span class="na">driver</span><span class="pi">:</span> <span class="s">local</span>
</pre></td></tr></tbody></table></code></pre></div></div>
<p>You just have to make sure that the DNS entry for <code class="language-plaintext highlighter-rouge">VIRTUAL_HOST</code> points at your caddy so that it can get an SSL certificate, otherwise it will crash.</p>
<p>I also adapted the command setting for caddy with two options:</p>
<ol>
<li><code class="language-plaintext highlighter-rouge">-restart=inproc</code>: If you do not add this option, caddy will restart which will kill the container and lead to downtime upon every configuration change. With <code class="language-plaintext highlighter-rouge">inproc</code> the downtime is way smaller</li>
<li><code class="language-plaintext highlighter-rouge">-http2=false</code>: This is due to a <a href="https://github.com/mholt/caddy/issues/809">bug in caddy</a> I discovered. As soon as it is fixed this option can be removed.</li>
</ol>
<p>While the <code class="language-plaintext highlighter-rouge">app</code> can run on any host, <code class="language-plaintext highlighter-rouge">caddy</code> and <code class="language-plaintext highlighter-rouge">caddy-gen</code> have to run on the same host due to the configuration sharing. This can potentially be mitigated by using another storage driver</p>
<h2 id="upcoming">Upcoming</h2>
<p>Now this is a very simple setup which might not scale very well when you are running a lot of hosts. So for the future I have two more posts planned, which I will link here as soon as available:</p>
<ol>
<li>Scaling web services with caddy-proxy setups over many hosts</li>
<li>Managing dockers hosts and web services on a larger scale</li>
</ol>
<p>If you are interested in either one of these posts or other related things, please let me know!</p>Recently I have played a lot more with docker then I might have let on in my blog. It’s pretty much one of the main things I have done recently, at least technology wise. Among other stuff I have pretty much moved everything I do or deploy to docker containers, and preferably combined webservices with caddy (and thus letsencrypt) to be handled pretty much automatically.Todo(ist) upgrade2015-09-29T00:00:00+00:002015-09-29T00:00:00+00:00https://www.julo.ch/blog/todoist-upgrade<p>After having written <a href="/blog/todo-apps/">a rather long rant</a> about todo apps I wrote over two years ago, I have switched away from <a href="http://culturedcode.com/things/">Things</a> about two weeks ago.</p>
<p>The one or the other might be wondering why I did that, and it is mainly for two reasons:</p>
<ol>
<li>Platform support</li>
<li>Karma</li>
<li>Progress</li>
</ol>
<h2 id="platform-support">Platform support?</h2>
<p>Well, I recently had to switch to an android due to hardware failure twice and both time I had to painfully realize that Things does not support anything that has no apple on the back, which was rather annoying because I did not want to afford an intermediary apple device when mine was only waiting for it’s repair. Thus I had to look for alternatives, at least until I could go back to Things.</p>
<p>I tried Wunderlist (again) which works well but just does not offer me the kind of power user options that I want to have, so noes. Then I tried <a href="https://todoist.com">Todoist</a>, which I had looked at a while ago already, but which was not really ready then. But it turns out by now the apps are pretty alright.</p>
<p>Neither of them (iPhone, Android, Mac or Web) are nearly as good as the Things app was, but I know that I will be able to still use it in case I switch away from Apple one day.</p>
<h2 id="karma">Karma</h2>
<p>But that was only the necessary condition to use an app, it was not really sufficient. But throughout using Todoist I realized that the app offered me a feature called <em>Karma</em>. So what is Karma about? Essentially it is kind of a gamification thing. You get points for using the app, completing tasks and achieving your daily tasks. And then there are certain levels for a certain amount of points.</p>
<div class="image-wrapper">
<img src="/img/todoist-karma.png" alt="A screenshot of my Todoist Karma" />
<p class="image-caption">A screenshot of my Todoist Karma</p>
</div>
<p>This had me hooked pretty fast. Before Todoist I was simply trying to get my todo list to zero, which involved a lot of moving todos around instead of actually doing that. Now I actually try to complete my todos, because I will only reach my goals and complete my todos, when I complete them, not when I have an empty todo list.</p>
<p>Furthermore with Todoist - for the first time - I have the feeling that I actually know what I got done. While all the other apps I tried tried to hide from me what I had done, Todoist gives me an overview of my work over the last few days and last few weeks, which gives me the feeling that things I did do not just disappear but actually make me happy.</p>
<h2 id="progress">Progress</h2>
<p>And that was it. Now I have my iPhone back but I am still using the inferior Todoist app, because I don’t want to live without karma anymore. And well, from what I can see, the timeline I predicted for Things 3 was wrong: it was promised for iOS 8, I predicted it for iOS 9, but it looks like it is gonna be iOS 11 until they finally release anything, while the Todoist apps seem to be under heavy development all the time, which means Todoist gets better all the time.</p>
<p>And I did not even speak about the fact, that Todoist supports collaborating with other users on todos and project, which Things doesnt. This will definitely come in handy at some point.</p>After having written a rather long rant about todo apps I wrote over two years ago, I have switched away from Things about two weeks ago.WTForms ChosenSelect2015-02-08T00:00:00+00:002015-02-08T00:00:00+00:00https://www.julo.ch/blog/wtforms-chosen<p>Recently I often had to build huge selects or even multiple selects and as you might know, especially multi selects can look quite ugly.</p>
<div class="image-wrapper">
<img src="/img/chosen-without.png" alt="Multiple select without styling" />
<p class="image-caption">Multiple select without styling</p>
</div>
<p>But they do not only look ugly, they are also very unnatural to handle, if not for pros at least for casual users, as you have to use shift+alt to select multiple entries, which is not clear to every user. Of course you can add a small description to explain that, but that does not really improve the usability itself.</p>
<p>So what is the alternative? The guys at harvest wrote a very nice javascript plugin that is very much downward compatible and will make your selects and multiple select much more beautiful: <a href="https://harvesthq.github.io/chosen/">chosen</a>. At some point I realized I did not want to manually add a script tag after every field in my templates and decided to write a custom widget to take care of that for me:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
</pre></td><td class="rouge-code"><pre><span class="kn">import</span> <span class="nn">json</span>
<span class="kn">from</span> <span class="nn">wtforms.widgets</span> <span class="kn">import</span> <span class="n">Select</span><span class="p">,</span> <span class="n">HTMLString</span>
<span class="k">class</span> <span class="nc">ChosenSelect</span><span class="p">(</span><span class="n">Select</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">multiple</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span> <span class="n">renderer</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span> <span class="n">options</span><span class="o">=</span><span class="p">{}):</span>
<span class="s">"""
Initiate the widget. This offers you two general options.
First off it allows you to configure the ChosenSelect to
allow multiple options and it allows you to pass options
to the chosen select (this will produce a json object)
that chosen will get passed as configuration.
:param multiple: whether this is a multiple select
(default to `False`)
:param renderer: If you do not want to use the default
select renderer, you can pass a function that will
get the field and options as arguments so that
you can customize the rendering.
:param options: a dictionary of options that will
influence the chosen behavior. If no options are
given `width: 100%` will be set.
"""</span>
<span class="nb">super</span><span class="p">(</span><span class="n">ChosenSelect</span><span class="p">,</span> <span class="bp">self</span><span class="p">).</span><span class="n">__init__</span><span class="p">(</span><span class="n">multiple</span><span class="o">=</span><span class="n">multiple</span><span class="p">)</span>
<span class="bp">self</span><span class="p">.</span><span class="n">renderer</span> <span class="o">=</span> <span class="n">renderer</span>
<span class="n">options</span><span class="p">.</span><span class="n">setdefault</span><span class="p">(</span><span class="s">'width'</span><span class="p">,</span> <span class="s">'100%'</span><span class="p">)</span>
<span class="bp">self</span><span class="p">.</span><span class="n">options</span> <span class="o">=</span> <span class="n">options</span>
<span class="k">def</span> <span class="nf">__call__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">field</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="s">"""
Render the actual select.
:param field: the field to render
:param **kwargs: options to pass to the rendering
(i.e. class, data-* and so on)
This will render the select as is and attach a chosen
initiator script for the given id afterwards considering
the options set up in the beginning.
"""</span>
<span class="n">kwargs</span><span class="p">.</span><span class="n">setdefault</span><span class="p">(</span><span class="s">'id'</span><span class="p">,</span> <span class="n">field</span><span class="p">.</span><span class="nb">id</span><span class="p">)</span>
<span class="c1"># currently chosen does not reflect the readonly attribute
</span> <span class="c1"># we compensate for that by automatically setting disabled,
</span> <span class="c1"># if readonly if given
</span> <span class="c1"># https://github.com/harvesthq/chosen/issues/67
</span> <span class="k">if</span> <span class="n">kwargs</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"readonly"</span><span class="p">):</span>
<span class="n">kwargs</span><span class="p">[</span><span class="s">'disabled'</span><span class="p">]</span> <span class="o">=</span> <span class="s">'disabled'</span>
<span class="n">html</span> <span class="o">=</span> <span class="p">[]</span>
<span class="c1"># render the select
</span> <span class="k">if</span> <span class="bp">self</span><span class="p">.</span><span class="n">renderer</span><span class="p">:</span>
<span class="n">html</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">renderer</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">field</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">))</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">html</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="nb">super</span><span class="p">(</span><span class="n">ChosenSelect</span><span class="p">,</span> <span class="bp">self</span><span class="p">).</span><span class="n">__call__</span><span class="p">(</span><span class="n">field</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">))</span>
<span class="c1"># attach the chosen initiation with options
</span> <span class="n">html</span><span class="p">.</span><span class="n">append</span><span class="p">(</span>
<span class="s">'<script>$("#%s").chosen(%s);</script></span><span class="se">\n</span><span class="s">'</span>
<span class="o">%</span> <span class="p">(</span><span class="n">kwargs</span><span class="p">[</span><span class="s">'id'</span><span class="p">],</span> <span class="n">json</span><span class="p">.</span><span class="n">dumps</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">options</span><span class="p">))</span>
<span class="p">)</span>
<span class="c1"># return the HTML (as safe markup)
</span> <span class="k">return</span> <span class="n">HTMLString</span><span class="p">(</span><span class="s">'</span><span class="se">\n</span><span class="s">'</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">html</span><span class="p">))</span>
</pre></td></tr></tbody></table></code></pre></div></div>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="kn">from</span> <span class="nn">wtforms</span> <span class="kn">import</span> <span class="n">Form</span>
<span class="kn">from</span> <span class="nn">wtforms.fields</span> <span class="kn">import</span> <span class="n">Select</span>
<span class="k">class</span> <span class="nc">ExampleForm</span><span class="p">(</span><span class="n">Form</span><span class="p">):</span>
<span class="n">example</span> <span class="o">=</span> <span class="n">Select</span><span class="p">(</span><span class="s">"Example"</span><span class="p">,</span> <span class="n">choices</span><span class="o">=</span><span class="p">[(</span><span class="s">"1"</span><span class="p">,</span> <span class="s">"1"</span><span class="p">),</span> <span class="p">(</span><span class="s">"2"</span><span class="p">,</span> <span class="s">"2"</span><span class="p">)],</span> <span class="n">widget</span><span class="o">=</span><span class="n">ChosenSelect</span><span class="p">())</span>
</pre></td></tr></tbody></table></code></pre></div></div>
<p>Now to use that you can simply pass <code class="language-plaintext highlighter-rouge">widget=ChosenSelect()</code> or if you want a multi select <code class="language-plaintext highlighter-rouge">widget=ChosenSelect(multiple=True)</code> to the field setup. As long <strong>as you include the chosen.js in your template</strong> your select will automatically be converted to a chosen select when your add <code class="language-plaintext highlighter-rouge">{{ form.example }}</code> to your template.</p>
<p>And then it might just look like this:</p>
<div class="image-wrapper">
<img src="/img/chosen-with.png" alt="Multiple select with chosen" />
<p class="image-caption">Multiple select with chosen</p>
</div>Recently I often had to build huge selects or even multiple selects and as you might know, especially multi selects can look quite ugly.