Tattoo A Species-Scale Timepiece

September 29, 2019 2:00 PM Chicago, IL

Apple Watch Series 5 went on sale last week. While I wear the Series 3 daily, I wanted to upgrade for the "Always-On" display so that I could see the exact time at all times without triggering the screen to wake up. How incredibly human-scale of me.

On my walk from our newly-old office to Apple Michigan Avenue to try it on, I changed my mind. The last thing that I needed was more attention on my relatively insignificant minute-by-minute trials and tribulations.

Instead, I invested the $200 it would have cost to upgrade into a permanent reminder of what's really at stake.

I tattooed the extinction symbol on my right wrist in the size and position of a watch face as a reminder of the critical moment we're in right now. I know more than I need to know about climate change and its implications, but I'm not doing enough.


I was introduced to this symbol by a kottke.org post earlier this spring. As Jason and the official website explained:

No extinction symbol merchandise exists, and it never will do.

I understand why that decision was made, for better or worse.


This was only my second tattoo.

My first was my wedding "ring" 8+ years ago. I passed out with very little notice right in the middle, just as the outline reached the inner-most edge of my finger. I woke up confused with a lollipop in my mouth and a mini orange juice box in my hand. The folded & furrowed brow of the tattoo artist did not spell sympathy.

If you've never seen my face blanch at the mention of blood, it would be easy to underestimate reflex syncope. Words, thoughts, and pictures trigger me as easily as pain. Its been that way since I was a kid and isn't letting up.

Which is all to say that I have to care a lot

About ⅓rd of the way through the extinction symbol tattoo, I was feeling cocky. I felt fine! So fine that I chose to mention to my tattoo artist (Sho from 9 Mag) that the last time I was tattooed I passed out even though I felt totally ok this time. Those words left my mouth and swirled around my head in a dizzying blur until 1 minute later I mumbled that I might have spoken a… bit… too… soon…

Ironically, I lost all understanding of time between then and when I was handed a cup of Pedialite and a cold paper towel for my forehead. Two for two. Ten minutes later, and my bearings were back.

Sho returned, pulled closed the curtain in front of his booth, and asked if I wanted to continue. I said "yes, definitely" — I actually felt pretty good. Then he said, looking down, "It's just that you had a bit of an accident." I followed his eyes until I understood why I felt so sweaty.

This time, I pissed my pants too.

Ten Things I Hate About WeWork

August 13, 2019 9:00 PM Chicago, IL

First, a brief explanation of how we ended up here.

In the early morning of April 4th, 2018, the front door and window of XBE's first office space in Chicago was literally shot up. 6 bullets in the wall, and one in the planter. We weren't in a particularly rough neighborhood (Old Irving Park), and it's never been entirely clear who was sending what message, but needless to say, we moved out immediately.

Given the lack of notice and nascent stage of the business, we needed something affordable and flexible. Through a colleague, I was put in contact with the team at BuiltWorlds, a construction industry media and conference company. At some point, they had planned on running a construction-themed coworking space out of the former Threadless headquarters on the Near West Side, and while that never came to pass, they still had some free space in the back of that office. And so, we agreed on a modest rent, put up a divider using a combination of their conference signage and some fake plants, and moved in.

Three months later, they decided not to renew their lease! I suppose it was just as well — that wasn't an ideal setup for any of us. There's never a great time to move, but Summer 2018 certainly wasn't it for XBE. Nevertheless, we needed something flexible quickly, and so in late July, we moved into the WeWork at 20 W Kinzie St.

Which is just to say that we all have our reasons, but even if your business is rendered homeless and you just-don't-have-time-to-deal, save your soul and find something better.


  1. Garbage Printing. You'll need to install ancient junk software on your computer and login with a username and password on the gross built-in keyboard (keycard support not provided) every time you print anything.
  2. Decorative Books. Treasures on the shelves such as the "Flat Belly Diet" are chosen exclusively based on how they color coordinate with the other books.
  3. Appropriated Lyrics. The theme of the interior design of this WeWork is "music", and so they thought it wise to appropriate Kanye West lyrics for the wallpaper of one of our floor's conference rooms. The didn't even get it right! "The City Raised My Crazy" instead of "Chi-Town, raised me, crazy". Unless they got permision from Yeezy…
  4. Cloying Announcements. I won't argue with a free snack at the end of the day now and then (with craft cocktails and seasonal beers served up by CRAFTY™ LOL bleh), but the promotional announcements are fuel for self loathing.
  5. Oppressive Echo. Perhaps the little fishbowls make the spaces look bigger, but the echo is debilitating. When they tell you that it'll be better when furniture is moved in, yodel into any furnished office and see how it sounds.
  6. Lousy Coffee. It's consistently inconsistently bad, and only available from 8AM - 4PM from Monday to Friday; all the hours when us "makers" need it.
  7. Bankers Hours. Speaking of… For all the talk about makers, it's mostly bankers; little hustle and zero vibe.
  8. Artless Walls. Decorations as far as you can see; not a thing to look at.
  9. Zero Privacy. Offices aren't private. Conferences rooms aren't private. Phone booths aren't private. Nothing is private.
  10. Enthusiastic Visitors. All of this makes it hard to deal with the enthusiasm of visitors about the space when they come to visit. I wish they'd hate it too.

Are you ready to sign up?!

Use this referral link and you'll receive a free month (and I'll get up to $5,000).

How To Order A Short Haircut

August 11, 2019 10:00 AM Chicago, IL

While washing my hands in our office building's public bathroom, a friendly and nerdy white guy in his late 20s who was at the adjacent sink looked over and said:

Hey, I noticed that you have a buzz cut, and I've wanted one myself for a long time. In fact, I just made an appointment for this weekend at a hair salon in my neighborhood. But, I'm not exactly sure how to order it so it looks right. What's the right way?

Who knew I was waiting for this moment! I had more to say than I expected. In case you're in the same boat, here's my answer:

  1. If you think you want a "buzz cut" but want to look like an adult with some style, you really want a fade.
  2. The quality of your fade has everything to do with where you get your cut, and little to do with how much you spend. Go to a barbershop that primarily services black and latino clientele.
  3. Specify your fade with two numbers, where the first number is the clipper setting of the side, and the second number in the clipper setting of the top; e.g. "zero one fade", or "one three fade". If you order a zero one fade (my usual style), you may hear it referred to as a "bald fade".
  4. You may be asked if you want it low or high. This refers to where on the side of your head they'll start fading. Unless you have a strong opinion, say "mid". If it's too low, you might end up looking like a mushroom, and if it's too high, it could look like a flattop.
  5. After they've finished the main part of the haircut, you may be asked if you want a "line in the front" or a "lineup". This is where the hairline is shaved to make a neat and clean hairline. There's no "right" answer on this one (either can look good), but unless you want to get your haircut frequently (once every 1-2 weeks), you should probably say "no" since a non-lineup grows out a bit more gracefully.
  6. There's a 50/50 chance that they'll clean up any stray hairs in your ears, nose, or eyebrows without even asking, but if they ask, the answer is "yes".
  7. If you have a beard, let them neaten it up. There's a good chance that they'll trim it back more than you do currently, and they'll also do a better job of finding the right outline. Then you can follow their lead at home.

One more thing…

Once you've adopted the fade as your look, be prepared to have bald white guys treat you differently because most assume that other white guys that have intentionally short haircuts are doing so out of necessity. Before I realized what was happening in these conversations, I would explain that my hair length was actually a choice (not a necessity) because I was proactively reducing the total volume of my head (which sans hair is big enough). That was wrong approach! It's much better to receive their hospitality graciously without making mention of any differences in hair growth capacity. It's hard enough to make new friends as an adult — go with the pitch.

This was originally written by me as an internal Slack post at XBE to help us better navigate the trade off between innovation and quality. I'm posting publicly to help others that struggle with the same tension and might benefit from reading these strategies, or from simply knowing they aren't alone!


Development Team,

We’re at a bit of a crossroads. Our customers need for us to innovate in order to help them increase production, reduce waste, and reduce risk AND they also depend heavily on our software for their daily operations and need for our quality to be rock solid.

These goals are at odds. The more we innovate, the more change we introduce, and the larger the surface area of our solution. That causes quality problems, even if short lived. But, if we don’t innovate, we fail to live up to the promised future for which our customers have signed up.

I think that we’re struggling to strike the right balance. And so, I wanted to share some of the techniques that I’ve used successfully to ship a lot of brand new functionality of relatively high quality. Now, I don’t always get it right!, but when I do…

1. Stay focused on a single thing until it’s shipped.

Yes, there are distractions to deal with, some our fault, some not. But, do your best to keep your head down on a single thing until it’s done. The cost of loading a feature into your head is non-trivial, and so do that as few times as possible.

2. Don’t work on something that’s not ready to be worked on.

Yes, sometimes we have no choice but to get going on a feature that we’re unsure of because external demands say so. But, even in those cases, don’t start programming until you’re ready and it’s important. Stare into space, take a walk, talk it out, etc. Or, if part of the feature is ready to be worked on, start there and mull the rest while you’re working. But avoid the avoidable missteps.

3. Take full responsibility for your quality.

It’s a luxury to have others test your code. Don’t be lazy and depend on others to find quality gaps that you could have found. For me, that usually means developing functionality in a few waves. First, get the happy path working. Then, defend against the potential problems and handle the corner cases. Then, refine the usability. Then, clean up the clarify the code. There’s no substitute for careful programming in the first place. Feel like you’re on a high wire without a net.

4. Keep things as simple as possible, but not more so.

Keep things straight forward. Feel where there is tension between your code and our app’s conventions, your code and the framework, and your code and the language. Where there is tension, your instinct should be to adjust your code. Of course, there are places where improving the frameworks is right, but in general, that’s the wrong decision, especially in the moment. Go with the pitch, attack asymmetrically, and make the smallest changes you can.

5. Assume you won’t come back to the code for a while.

Get it as right as possible the first time. Create affordances for what might be required in the future so that extensions can be built instead of planning on large refactors. Tie off loose ends. Now, there is a time and place for spiking something, and it’s reasonable to ship MVPs that are iterated on immediately. But, at any point, if you didn’t come back to the feature, it should be in good shape.

6. Be a self-sufficient generalist.

I’ll take a pretty good generalist any day over a collection of great specialists. Notice where you’re weak, and get stronger through purposeful education and practice. None of us can be great at everything, but we can be good enough in most cases. As you plug holes and even out the peaks and valleys, you’ll be able to tackle bigger problem more quickly, and you’ll have more fun.


I’ve shipped nearly every line of code that I’ve written for our apps. In part, that’s because I merge my PRs!, but mostly because I follow the above rules.

I want each of you to strive to do better to balance innovation and quality. Our customers and team members are depending on us to get that right, we’re all extremely committed to our shared goals, and I know we can level up!

Thanks as always,

Sean

I like my Git branch names to start with the GitHub issue number that I'm working on and then have a sensible description (usually based on the GitHub issue name).

For example:

First material transaction in time card pre-approval form is incorrect #4045

becomes…

4045-first-material-transaction-in-incorrect

And so I made an Alfred workflow (right-click to download) that takes the GitHub issue name and number and creates a Git branch name that's usually good enough.

Here's the good-enough JavaScript that does the conversion.

function run(argv) {
  const SEP = '-';
  const MAX_LENGTH = 50;
  const NUMBER_PATTERN = /\d+$/;
  const SUFFIX_PATTERN = /\s*#\d+$/;
  const WORD_SEPARATOR_PATTERN = /[\s\(\)]+/g;

  let [query] = argv;
  let numberMatches = query.match(NUMBER_PATTERN);
  if (numberMatches) {
    let [number] = numberMatches;
    let parts = [number];
    let name = query.replace(SUFFIX_PATTERN, '');
    let words = name.split(WORD_SEPARATOR_PATTERN);
    parts.push(words.pop());
    while (words.length > 0) {
      let word = words.shift().toLowerCase();
      if (parts.join(SEP).length + word.length + number.length < MAX_LENGTH) {
        parts.splice(-1, 0, word);
      } else {
        break;
      }
    }
    return parts.join(SEP);
  }
}

Updates:

  • v1.1 updated the WORD_SEPARATOR_PATTERN to remove additional characters

Takeaways from EmberConf 2019

March 20, 2019 Portland, OR

I wrote these takeaways to share with my team at XBE based on what I learned at EmberConf 2019.

  1. Use native classes with decorators.
    The wind has been blowing this way for quite a while, and the time seems right to switch. The conversion doesn't appear too difficult, can be done gradually, and the benefits (standard JS, cleaner code, better tooling, slimmer framework) are clear. This blog post by Chris Garrett is a nice introduction to the topic.

  2. Use tracked properties as soon as they're available.
    Ooooh do I like this one! The idea of tracked properties is to explicitly mark the changeable state so that anything that depends on it will know when it must be rerendered. Think computed properties, but in reverse. In addition to removing the need to use set (which is nice), it is more explicit and fixes the "flow" of change propagation. The before-and-after code examples look excellent, and I love that it can be adopted incrementally (one field at a time). This blog post by Chris Garrett is a nice introduction.

  3. Use Glimmer components.
    Adopting tagName: '' in our classic Ember components made them way easier to reason about by putting the entire template in the template. Glimmer components simplify things in a first class way, and when combined with element modifiers, they provide what looks to be the right separation of concerns. This blog post by Chris Garrett provides some examples.

  4. Transition to modifiers.
    If you want to get excited about modifiers (which oddly weren't talked about much at the conference except at lunch which turned out to be an excellent impromptu talk by Matthew Beale), check out the Ember Functional Modifiers video from EmberMap. Moving behavior into the template at the element level is both more clear, and is more flexible (you don't need component hooks for the kind of functionality that have historically relied on them). There is some work left to do before this feature is finalized, but some nice add-ons are available that allow for exploration right now. This blog post by Chris Garrett provides some additional detail.

  5. Use EmberData Storefront to attack data loading bugs.
    I had a conversation with someone that attended the pre-conference data loading workshop focused on EmberData Storefront. We haven't tried the library yet, but I expect that we'll adopt it right away. One of our largest categories of bugs relate to being either overly optimistic or overly pessimistic regarding data loading. I love that there's an add-on built to attack this problem that goes with the EmberData flow. I haven't watched it yet, but there's a video on YouTube with Sam Selikoff and Ryan Toronto from EmberMap that I'll watch next.

  6. Maybe extract primitive components and styles into a styleguide add-on.
    In his Building a UI Styleguide in Ember talk (which I saw in a version of at EmberCamp Chicago), Frédéric Soumaré demonstrated an interesting approach that they use at qonto for bundling reusable presentation components and styles into an add-on. The cost seemed pretty low save some initial setup, though I imagine it could be a bit difficult if there was significant churn that had to be synced across a medium-to-large team. I'm interested in exploring the option value of having the reusable bits be portable across apps, and making the choice about where to put a given component seems valuable by itself.

  7. Try ember-service-worker.
    I was happy that I learned FastBoot when I figured out how to deploy a static-first version of this blog using prember, and I could tell from the great Anatomy of an Addon Ecosystem talk by Lisa Backer that I'd be happy if I learned about how to better use service workers too.

  8. Build something real with ember-animated.
    After watching Ed Faulkner's great ember-animated talk last year, we built a fun proof of concept using it. But since then, we haven't done too much. I heard some good things about the pre-conference animation workshop over lunch, and that encouraged me to have us build something a bit more substantial with the library this year. I think my main problem is that I don't have a lot of confidence about what and when to animate! I could use a book like Refactoring UI but for animation. For now, I feel most comfortable copying things that I see elsewhere.

  9. Try orbit.
    I first tried Ember because I believed in the sensibilities of Yehuda Katz, and that bet has paid off. I also believe in the sensibilities of Dan Gebhardt, and so it's time to try orbit, his composable data framework.

  10. Learn TypeScript.
    When I see Ember apps written in TypeScript, I'm jealous. They look excellent. I've only ever written a toy glimmer app in TypeScript, and I think it's time to do something more substantial (but probably with a limited surface area to start as I learn the language and tooling).

I've adopted the pattern of writing websites like this one and the marketing website for my company (XBE) as Ember apps that are pre-rendered using FastBoot and prember. Once familiar with that development pattern, it's quite straight forward and provides the benefits of a static site with the development and interactive benefits of a client-rendered app.

This post explains the pattern that I've used to ensure that the hydration of the Ember app's data store doesn't cause re-rendering issues while still getting the immediate route transition behavior that I prefer in all of my apps.

Here's a description of the specific problem:

When the application is pre-rendered, I'd like it to wait until the model is resolved before rendering so that the pre-rendered page includes all content. However, when I'm handling a transition to that page within the app (non-FastBoot) I'd like for the transition to happen immediately (before the model is resolved) to avoid a lag between the click and the transition. After the app is subsequently loaded, I'd like it to avoid displaying the "loading" state it would show during a route transition if the corresponding template was pre-rendered.

Let's start with the route and work backwards. I'd like to write an ember-concurrency task that is used to fetch the route's model, and then have everything just work™ across the scenarios listed above. The example below shows a minimal example of the idea.

// routes/example.js
import Route from '@ember/routing/route';
import RouteModelTask from 'marketing/mixins/route-model-task';
import { task } from 'ember-concurrency';

export default Route.extend(RouteModelTask, {
  modelTask: task(function * (){
    return yield this.store.query('example', {});
  }),
});

And then when we render the template, we'll show a loading indicator of some sort only if the modelTask hasn't resolved yet. Again, this is simplified and doesn't include any sugar to make dealing with the task that is bound to the model feel cleaner — I just didn't want to confuse the ideas with additional abstraction.

// templates/example.hbs
{{#if this.model.lastSuccessful}}
  {{#each this.model.lastSuccessful.value}}
    ✂️
  {{/each}}
{{else}}
  Loading...
{{/if}}

Whether or not to defer rendering in various contexts is handled by the mixin, and therefore none of that logic clutters up any routes that implement this pattern.

Let's take a look at a stripped-down implementation of the mixin. I removed some of the logic that's needed to handle all types of routes to make the key initial ideas more obvious.

// mixins/route-model-task.js
import Mixin from '@ember/object/mixin';

import {
  task,
  waitForProperty,
 } from 'ember-concurrency';

import { inject as service } from '@ember/service';
import { assert } from '@ember/debug';
import { isPresent } from '@ember/utils';

export default Mixin.create({
  fastboot: service(),

  router: service(),

  model(params) {
    this._super(...arguments);

    const {
      fastboot,
      modelTask
    } = this;

    assert('modelTask must be present in the route', isPresent(modelTask));

    const instance = modelTask.perform(...arguments);

    if (fastboot.isFastBoot) {
      // defer rendering until the data is fetched from the server
      // but handle it this way to enable the route to render immediately
      // when not in fastboot to avoid that s l u g g i s h feel
      fastboot.deferRendering(instance);
    }

    return this.modelTask;
  },

  afterModel() {
    const { _super, afterModelTask } = this;
    _super(...arguments);
    return afterModelTask.perform(...arguments);
  },

  afterModelTask: task(function * (model) {
    const {
      fastboot,
      fastboot: { shoebox },
      router,
      fullRouteName,
    } = this;

    if (fastboot.isFastBoot) {
      return;
    } else {
      let path = router.urlFor(fullRouteName);

      if (shoebox.retrieve('requestPath') === path) {
        // wait for the model task instance to idle before
        // rendering to avoid the *blink* of the model not being defined
        // as the data is hydrated
        return yield waitForProperty(model, 'isIdle', (v) => v);
      }
    }
  }),
});

This all gives us the behavior that we want in all situations without clouding the route-specific logic.

On Your 16th Birthday

January 7, 2019 Chicago, IL

Because you can doesn't mean you should.

And so my kids receive bikes instead of cars from me on their 16th birthdays.

Happy Birthday Mac!

Async Computed Properties in Ember

January 2, 2019 7:00 PM Chicago, IL

Computed properties are flexible, efficient, and easy to understand. When I was first learning Ember, they reminded me of what I enjoyed so much about Excel - my first programming 🥰.

...and then I met ember-concurrency.

I didn't want to choose between them (computed properties and ember concurrency) — I wanted to define computed properties with generator functions and have the backing ember-concurrency task built seamlessly.

Like this:

// ✂️

tempForecast: computedTask('selectedDate', function * () {
  const {
    date,
    buildForecastUrl,
  } = this;
  const url = buildForecastUrl(date);
  const response = yield fetch(url);
  if (response.status === 200) {
    const json = yield response.json();
    return json['temperature'];
  }
})

// ✂️
Forecast Date
Temperature

With computed tasks, there are no promises to deal with in the template, and only a single computed task explicitly declared in the component.

// ✂️

{{#if this.tempForecastTask.isRunning}}
  {{fa-icon "sun" spin=true}}
{{else}}
  {{this.tempForecast}}
  <span class="text-grey">
    &#8457;
  </span>
{{/if}}

// ✂️

How does it work? I added this function in my utils that builds the task and computed property.

// manifesto/utils/computed-task.js

import {
  defineProperty
} from '@ember/object';

import {
  task
} from 'ember-concurrency';

import ComputedProperty from '@ember/object/computed';

import {
  expandProperties
} from '@ember/object/computed';

const _CP = ComputedProperty;

function parseArgs(args) {
  return {
    dks: args.slice(0, -1),
    gf: args[args.length - 1],
  };
}

const ComputedTaskProperty = function (...args) {
  const { dks, gf } = parseArgs(args);

  return _CP.call(this, function(pn) {
    const tn = `${pn}Task`;

    const isInitKn = [
      'isCtInit',
      pn
    ].join('-');

    const isInit = this.get(isInitKn);

    const vkn = [
      tn,
      'lastSuccessful',
      'value'
    ].join('.');

    if (!isInit) {
      defineProperty(
        this,
        tn,
        task(gf).restartable()
      );

      this.addObserver(vkn, () => {
        this.notifyPropertyChange(pn);
      });

      this.get(tn).perform();

      const eks = [];

      dks.forEach((dk) => {
        expandProperties(dk, (p) => {
          eks.push(p);
        });
      });

      eks.forEach((ek) => {
        this.addObserver(ek, () => {
          this.get(tn).perform();
        });
      });

      this.set(isInitKn, true);
    }

    return this.get(vkn);
  });
}

ComputedTaskProperty.prototype = Object.create(
  ComputedProperty.prototype
);

export default function computedTask(...args) {
  return new ComputedTaskProperty(...args);
}

It's not perfect, but I've used it for a while in our applications, and its been fine™.

A few final notes:

  • Why didn't I release it as an add on?
    Because I'm a lousy maintainer.
  • Why aren't there more options?
    Because we haven't needed them.
  • Why do you have to "get" the computed task in init?
    Because it's generated lazily and the task wouldn't have been created if I hadn't "gotten" it.
  • Should something like this be provided by ember-concurrency?
    Maybe? I'd like it, but given that I'm haven't even released it as an add on, who am I to say.

There Is No Irony in India

January 1, 2019 8:30 PM Chicago, IL

As we waited for the wedding ceremony to start, my colleague asked me to compare and contrast Indian and US culture, and if my time in India had changed my perspective on anything.

I told him that there were some things about India that seemed objectively worse - air quality, traffic, and cleanliness. And there were some things that seemed objectively better - hospitality, family connections, and fruit! While the reasons for these differences were worth exploring, I didn't feel much uncertainty about the experience of them.

I was far less sure about how I felt about another difference - one that I had a difficult time getting used to.

There is no irony in India.

"I don't know the word 'irony', can you explain it?", he said, unironically.


My best definition: "successfully communicating an idea by saying its opposite".

As for why American communication is laced with so much irony, my best guess was part indulgence and part defensiveness. It's a good time! It makes communicating more game-like for both the sender and receiver. It's exclusive - a sorting hat to identify common sensibilities and weed out those that can't "see". It's interesting, affording plenty of room to build a unique style that is still honest. And it's revealing, making clear who shares what context.

And so which communication style is better?

As far as protecting one's downside, irony seems essential. It's expensive to be the person that doesn't understand what others mean, and since we can't control how others communicate, fluency in irony is critical.

But to maximize one's upside, it might be best to abandon irony when you're in control. Take all of the time and energy and lost fidelity required to mean what you don't say and reinvest it in clarity and directness.


Try-hards play well in India. Earnestness is preferred to ambition. Coldplay, Michael Bublé, and the early Beatles sound better.

And that's interesting, literally.

This website is an Ember application, rendered into static HTML files by prember, deployed to AWS S3 by ember-cli-deploy, and served by AWS CloudFront.

The static files are rendered as **/index.html as explained in the prember README.

However, in order to prevent S3 from returning 302 Moved Temporarily when CloudFront forwards a request without a trailing slash we need to modify the URI of the request using Lambda@Edge.

The following very simple Lambda function adds a trailing slash if the URI doesn't contain an extension or a trailing slash.

const path = require('path');
exports.handler = async (event) => {
  const { request } = event.Records[0].cf;
  const { uri } = request;
  const extension = path.extname(uri);
  if (extension && extension.length > 0) {
    return request;
  }
  const last_character = uri.slice(-1);
  if (last_character === "/") {
    return request;
  }
  const newUri = `${uri}/`;
  console.log(`Rewriting ${uri} to ${newUri}...`);
  request.uri = newUri;
  return request;
};

That's it! Trigger the Lambda function with a CloudFront viewer request event, and the request's URI will have the trailing slash added which S3 will handle without a redirect. That'll save 50ms (give or take) and be more search engine friendly.

The function needs to be written in Node (sorry Ruby) since it will be deployed via Lambda@Edge.

The Paradox of Indian Consumption

December 29, 2018 5:00 PM Chicago, IL

I recently spent two weeks in India; mostly in Goa, and a bit in Mumbai.

There was a conspicuous amount of visible garbage (litter, air pollution, debris, etc), and very little consumption!

I don't have any real statistics to back this up, but in comparison to the US, I'd estimate that the per capita consumption of disposable goods was 95% lower, but that the per capita visibility of the byproducts of disposable consumption was 20X higher. In other words, while each person was leaving far less of an impact on the environment, the local impact felt disproportionate, inescapable, and unnecessary.

The reasons are undoubtably many and complicated, but lack of trash processing infrastructure seemed high on the list. I'd never noticed just how efficient and pervasive the machine of waste management is in the US until I noticed its absence in India. There were few "dust bins" and infrequent trash pickup (if any at all). I asked around a bit and learned that many smaller towns still either don't have regular trash pickup, or have only recently added rudimentary capabilities.

Consumer packaged goods from global brands have certainly found their way to India — exhibit A being the canister of cheddar cheese Pringles™ that I ate on the way back from Calangute Beach when I was feeling less adventurous than usual and in "need" of some convenient energy. But the business of dealing with refuse is mostly local, and from what I saw, the infrastructure for handling remnants of cheap imported consumables is woefully inadequate, even at very low levels of consumption.

And so, the "solution" to this problem is probably to dramatically reduce the "cost" of consumption by making its byproducts literally less visible. On the plus side, this would almost certainly increase the "standard of living" and lessen the apparent impact of consumerism on the environment. However, the cost of this "advancement" would almost certainly be the erosion of Indian culture, and the long-term net per capita (global) increase of the environmental impact of the Indian economy.

In other words, the "problem" is a firewall against what may be the irreversible destruction of the invaluable. And in the context of globalization, perhaps the destruction is unavoidable anyhow, and so perhaps its best to engineer a "soft landing".

Swim in a kiddie pool of trash, or fill the oceans…

India Travelogue: Dispatch 1

December 13, 2018 9:00 AM Goa, India

To ensure that I take note of all that seemed notable, I'll leave the development of a narrative to the reader or future me. My goal for now is just to capture as many of the small moments as possible so that I may reflect on them more easily when my memory fades.

Through Britain

I travelled in business class from Chicago to London to Mumbai on British Airways. The layover in London was only 3 hours, and the entire journey took approximately 20 hours. But, the service on the first leg made it feel like I spent a 1/2 day in the UK to start the trip. My seat was on the second floor of the aircraft (more on floor numbering to come), and there were only ~24 passengers in that section. We were serviced by a team of two middle-aged white male flight attendants straight out of of a Downton Abbey casting call. The service was almost impossibly polite, but with a bit of British humour. Though most passengers in my section were American, it was fascinating to see everyone audition for the part of the aristocracy. And while the trouble with this sort of class hierarchy is clear, I saw dignity in the work that I might typically underestimate. In exchange for good-natured and attentive service, passengers showed the staff uncommon respect. During those first 2 hours of flight, I felt an understanding of the positive role that ones profession can play in identity construction, even if the job could be seen as demeaning. And, perhaps I was looking to start my journey straight away, but all of this made me feel quite far from the USA before we left the tarmac at O'Hare.

Green Within Me

I don't typically drink on airplanes, and I didn't say no to the glass of champagne upon boarding, or the glass of wine with dinner, or the port after dessert. The meal was really quite lovely and left me feeling self-satisfied with this audible until I reclined my seat into the sleeping position and laid down to get some rest. One teeny bump of turbulence later and I was nauseated. I'd rather forget the next 12 hours, and so I won't record any of the details. But my general rule against airborne alcohol is now a strict prohibition. Thankfully, I finally recovered my bearings about 90 minutes before touching down in Mumbai.

Uber Blackish

It was 2AM by the time I made it through immigration and customs. I made my way to the Uber pickup area (located one level up from the arrivals exit, oddly only accessible from two very crowded elevators). Given my large suitcase and uncertainty about what to expect, I ordered an Uber Black for my 20 minute ride to where I was staying for the night. I'm not sure what I thought that meant, but the little white Nissan Sunny that picked me up wasn't it! That said, as long as the luggage fit and it had doors, it was good enough for me. The driver started up the car (it had been parked), disappeared inexplicably for 10 minutes, and then we were off.

Hilarious that I found it notable to take a photo of this rickshaw on the highway. I had no idea what was to come.

The Other First Floor

I arrived at my destination in the Bandra West neighborhood of Mumbai at 2:45AM. The cousin of a good friend and colleague had offered to put me up for the night, and I warmly accepted the hospitality. It took me an awkwardly long time to figure out in which building they lived. Addresses are commonly referred to by name instead of number, but with some phone-based guidance from my friend, I made it inside the gate. I was told to go to the first floor and ring the bell of the flat on the right. I did so timidly though repetitively until I heard voices one floor up and realized that the "first floor" meant the "second floor" and that I had just woken up not only my hosts, but also their neighbors.

I even took note in Heathrow of the fantastic floor numbers (-2, -1, 0, 1, 2) in the elevators of my terminal. Perhaps the airsickness took its toll.

Welcome Party

I was greeted at 3AM by my friend's cousin, her two adult sons, and her uncle whom they were also hosting on his way to Goa. They were gracious, made polite small talk, provided me with some water (I can only imagine how dehydrated I looked), and showed me to the bed that they had kindly setup in the living room. I slept until 7:30AM when the sun rose.

Breakfast Included

I woke up around the same time as my host's husband and his wife's Uncle, a printing industry retiree that lives in Toronto, Canada and had travelled back to India for the winter (through March) to attend his nephew's wedding (that I'm also attending). We shared pictures of our families, he showed me a television advertisement for laundry detergent that he randomly starred in last year (over 1M YouTube views and a matching billboard campaign), and he gave some touristing recommendations for various destinations in India.

My host's husband was friendly and gracious and insisted on making me coffee and breakfast. We chatted a bit about the cities (Miami, New York, Washington DC, Vancouver, Seattle, San Diego, etc) that he had visited over a 12 year stretch as a chef on cruise ships (Royal Carribean and Carnival). He cooked eggs (sunny side up) with toast and that Indian tortilla-like food, the name of which I can't recall. The butter dish was room temperature (makes all the difference), and he provided a small piece of cheese and peanut butter on the side (though I stuck to the butter).

That I was being hosted by the family of a chef that may have worked on a cruise ship that I travelled with my family on years ago felt important, even in the moment. If ever I have second thought about hosting anyone in our home, it will take only a moment of reflection on their generosity to get my act together.

Three Hour Tour

After showering and some more conversation, I headed out for a walking tour of the area shortly before 10AM. My only plan was to make it down to Mount Mary Church and back, about 2 miles each way. I kept my phone out to take photos and video along the way, and kept my AirPods in my pocket so that I could soak in the sights and sounds of the city.

Walking by a small "commodity store" at the start of the walk.

It should go without saying, but the density of everything in Mumbai is staggering. Some of the photos will make the point for that moment, but it's hard to capture how consistent it is. The best way I came up with to communicate just how dense is to say that in the 4 miles that I walked through a relatively nice section of the city and then the hour+ that I drove from there to the dock from which the boat left to Goa, I never saw anywhere where one could run for more than 20 yards. There are people, and buildings, and vehicles, and everything else imaginable everywhere.

St. Joseph's Convent Primary School

Most of the nicest buildings on the walk were related to the Catholic Church in one way or another. I would see many more schools that were more interesting than this one, but it was the first to catch my eye. The Catholic schools mostly looked great and were in good shape, and the children's uniforms looked sharp.

Fence guarding convent playground

Nearly all buildings, including those related to the Church, are surrounded by walls, fences, gates, and other barriers. I loved the contrasting elements like the weathered-and-graffiti'd red paint with the ornate ironwork above. Peek through the metal to see the playground, huge convent, and uniformed children.

Men in sandals repair roadside wall in front of high school

The purpose of the work above wasn't very clear, but I believe that they were chiseling away the outer layer of building materials to get down to the underlying brick. As it stood, the wall had a good look to it, but it seemed to be the property of the high school in back. The men worked in sandals and with simple tools and slowly chipped away at the wall as the rubble piled up on their feet.


Sign outside St Andrew's Catholic Church

Brilliant copywriting, an odd translation, or both? I love the earnestness. And the name of the church isn't even on the sign! That makes more sense when you see the next picture (the church is gorgeous and hard to miss).

Saint Andrew's Catholic Church with graves everywhere

The area in front of Saint Andrew's was all graves. And somehow, even though the church is hundreds of years old, some of the graves were days old. For example, look at the right foreground of this photo. I believe that's a grave that doesn't even have its top stone yet. There were a few other new graves, and even a burial going on when I was there! Also, in addition to these graves underfoot, there was a wraparound wall on the other side of the church with stacked boxes with placards on the front that I believe contain the ashes of the deceased. The entire grounds were utilized - for buildings or to bury the dead.

Saint Andrew's Catholic Church interior

Saint Andrew's Catholic Church burial

I stopped for some time at Saint Andrew's to appreciate it as a place of worship and congregation, and to just absorb its beauty. It seemed to be faithfully serving its intended purpose, and providing huge value to the surrounding community. I think it was the highlight of the walk.


Just after leaving Saint Andrew's, I came upon a paving project that was just beginning. Given that XBE is involved in construction logistics (primarily related to paving), and that my trip is to work with our team in India on our software platform for construction logistics, I took a close look!

Lintech Paver

Apollo Paver

Roadwork sign for related paving project

Unfortunately, there wasn't an active paving job to watch. I can't imagine what that would do to traffic. Where would everyone go? How would the paving crew keep things safe? To say the least, it did not seem like an easy task to manage.

Somewhere near the roadwork I saw the tinyist dump truck that I've ever seen! I tried to get a photo, but was a bit preoccupied by crossing 4-6 lanes of traffic (the lane count was fluid). It looked like it was moving road debris, and couldn't have hauled over 4 tons.


From there, I headed up to Mount Mary Church, the ostensible destination.

Sitting on mosaic steps on the way to Mount Mary Church

I love mosaics, and these were great. They were inlaid on the stone steps on a walkway that was separated from the road which veered off to the right as it "switch backed" up the hill. There were relatively few places along the walk where the road and walkway were separate, and so I found it quite nice that they made the most out of the rarity.

Saint Stephen's Church (said) vs (sung)

Given all of the competing church options, I'm not sure how Saint Stephens could differentiate, but I liked the clarity of their "said" vs "sung" signage. Even their address refers to their competitor!

"No honking" sign in front of convent

The crazy part about this sign is that it worked! I don't think that there was another 50 yard stretch of road during the entire walk where there wasn't near constant honking. It's a wonder that those signs are everywhere.

Mount Mary Church adjacent monument under construction

This monument was across the road from Mount Mary Church and had an incredible web of hand-tied scaffolding surrounding it. Somehow, everything seemed under construction, very little seemed complete, and the construction and use carried on in spite of each other.

I didn't actually take any photos of Mount Mary! There was a sign asking that personal photos not be taken inside, and the next thing I knew, I was headed out of the church and down the hill.

Only Dry Garbage vs Only Wet Garbage

Where we might be used to seeing dueling garbage cans for trash and recyclables, it was common to see these "only wet" vs "only dry" containers. I'm not sure how, but "wet garbage" is finding a way into my vocabulary.

Mount March Church donation box

Something seemed conspicuous about this donation box! It was at the bottom of the hill, down the long steps from the church. Maybe it just reminded me of the great "box" that Colin's wife gave him in the second-to-last episode of Homecoming, but I liked it.

Walking down a busy market street in a slum by the water

I took a wrong turn in an attempt to get down near the water, and walked down a long and winding street full of vendors and all sorts of activity surrounding a large slum. It was one of the most dizzying places I've ever been to.

Barbed-wire surrounded cemetery

Just up the hill was this amazing little cemetery surrounded by dull concrete walls and barbed wire. It was across the street from a nice small Jewish cemetery which seemed unconnected to a place of worship and was pretty unexpected to me.

Dominos Pizza delivery motorcycles

I'm not even sure where this Dominos Pizza store was! Maybe that's why they made the choice to brand the motorcycles.

Bicycle knife sharpener

I loved everything about this bicycle knife sharpener! If you look close, I think that he's sharpening a pizza cutter, perhaps from the unseen Dominos.

Avoid using plastic carry bags sign

This is an interesting alternative to the $0.07/bag tax on plastic bags in Chicago. And it seems to be working! I didn't see very many plastic bags being carried, though I did see a number that had been previously "discarded". Progress…

I didn't have as many photos from the last mile of the trip because I bought a rather/unintentionally large flower display for my hosts and it turned out to be a bit of a project to carry it on the street without knocking into someone or destroying it! As if I wasn't conspicuous enough.


All of that by 1PM!

I had a quick lunch, said my goodbyes (though I'll see my hosts at the wedding next week), and hustled off to make it to the docks on time for the overnight boat trip down to Goa.

Manifesto Destiny

November 25, 2018 12:00 PM Chicago, IL

I'm 40 years old; exactly two weeks from 41.

A blog feels, how do you say, unbecoming.

…but a manifesto!

The former is the want of my late 20s, whereas the latter embraces mortality in the way that middle age should.

This is a public declaration, née plea, wherein I state my opinions for the record.

Wherefore‽

Visions and Verbs

January 2, 2017 Simsbury, CT

Inspired by Letters From a Self-Made Merchant to His Son, I started writing letters to my 20-year old daughter. This is the first.


Dear Kayla,

I’ve been thinking more about your struggle to find purpose. In case it might be helpful, I decided to share some more thoughts on the topic. It’s hard for your vision for your life not to be constrained by your resources. In other words, most people can only imagine a future that can be accomplished using what they already possess (skills, relationships, money, time, etc). As you know, this creates a chicken-or-egg problem. It’s like the person that wants a job that requires experience, but can only get experience by having the job. How do you “seed” your life when it feels like the seed that you need would be an outcome of the life that you want? While it might be fun to fantasize about being given the seeds for a great life (i.e. inherited wealth, random fame, model looks), that’s not how life works. The exception proves the rule.

Instead, spend your time imagining the things that you want to accomplish, however big or small. Count on being able to figure out how to achieve your vision. You already have plenty of raw material to do amazing things, but it won’t be clear how to mix those ingredients if you’re not working in the kitchen.

And so, once you have a goal in mind, start cooking. It’s like the TV show Chopped. It may seem like a hopeless situation to look down and see ketchup, cabbage, and an octopus as your three ingredients for the entree, but people get resourceful. You will too.

That’s the thing about resourcefulness. It’s a misnomer. Being resourceful means that you have a clear purpose, an optimistic perspective, and relatively few resources! But once you have a goal in mind, you’ll be shocked to discover the usefulness of the seemingly useless things you already have at your disposal.

Transform your inner voice to speak in visions and verbs. What do you see for your future? What can you do to make that happen?

When your self-talk veers into nouns and adjectives, don’t listen. Those are excuses that will just get in your way. No more “I wish I had this”, or “I wish I was that”.

Start with something small this week. Close your eyes and imagine something that you want to accomplish. Write it down and refer to the goal regularly. Let it stew a bit. Then start taking action. Do the next thing that comes to mind that would get you closer to your vision. Rinse and repeat.

You’ll accomplish more than you thought you could. And once you start getting more confident, your vision will naturally get more audacious.

I’m already confident in you, and so is your Mom. You’re going to make a great life.

Love you lots,

Dad


Originally posted on Medium

Oh the Bugs You'll Write

August 10, 2015 Simsbury, CT

Gifts of Blank Middle Names

August 1, 2015 Simsbury, CT

Dear Violet and Ivy,

You'll be born any day now. Or any hour.

Our first gifts are your blank middle names. We hope you love them as much as we loved the process of choosing.

Without middle names during your childhood, you may occasionally feel like you're missing out. But give it until 25.

By then you'll learn that when you plant mature landscaping you leave no room for life to grow. And at that point you can pick the middle names that suit you best.

Between now and then, every time a parent at the playground turns their head in horror when I holler "VIOLET BLANK DEVINE" or "IVY BLANK DEVINE", you'll learn a bit more about what it means to be funny.

Love you lots,

Sean Michael Devine

Trapped in the Car

November 26, 2014 Albany, NY

The trip to my parents' house for Thanksgiving usually takes about two hours. Today there was a snowstorm and it took four.

Our oldest is home from college for a few days. It's her freshman year. We keep in touch daily by txt message. It just isn't the same.

But we had four hours, three kids, two parents, one dog, and nothing else to do. Thanksgiving.

First Comes Power

January 3, 2009 Ann Arbor, MI

There is a frenzy of conversation on the Internet about pricing at the iPhone App Store (examples here, there and everywhere). While I'm interested in all of the different perspectives that are shared from users, developers and observers, I don't think that enough attention is paid to Apple's objective. They certainly don't want poor applications, or for developers of iPhone applications to have unsustainable business models. But, while they want to make short-term profit from iPhone applications, they're more interested in maximizing long-term profit for the entire iPhone franchise. The KEY to maximizing iPhone profit is to create very high switching costs for users, just as they did for the iPod via the iTunes Music Store. Apple is using the App Store to create switching costs, and they know that if all of their users have "invested" in many little applications that will only work on the iPhone (a la songs from the iTunes Music Store), they will eventually have users locked in to a long-term investment in the iPhone franchise. The profit from the successful execution of the iPhone franchise strategy will dwarf any amount of profit that they could suboptimize if they focused on what was best for the iPhone application development community.

There is no killer APP for the iPhone, but there is a killer APP STORE. Until they believe that their users have hit the point-of-no-return, they will continue to optimize the app store around volume, not quality or price. Volume = switching costs = long-term domination.

Can't Apple have its cake and eat it too?

Sure. There's no reason why Apple couldn't modify the app store to balance volume with quality and price. And they will. But, they know how close they are to becoming THE dominant mobile phone company in the world and they're not going to take any chances. Once they've locked users in, they'll shift focus to mine as much profit as possible from each of those users each year. That will mean having them purchase more expensive applications (in addition to the little knick knacks that dominate the store now), and driving increasingly tough bargains with the cell phone networks.

What's the lesson for developers?

Don't waste your time hoping that Apple will change its strategy to make it easier for you to sell your expensive applications and more difficult for other developers to sell their inexpensive applications. And, don't count on ever getting any more free advertising from Apple than you get now. One way or another, you'll have to pay for your exposure (marketing $s or pricing $s).

Do take note of their strategy and focus on switching costs. If you take the long view and build a defensible competitive advantage by creating value for users and significant barriers to entry, you will do well.

To sum up Apple's strategy (and Tony Montana's):

Step #1: Get Power

Step #2: Use Power

It's a good strategy.


Originally posted here, and linked to by Daring Fireball.