Monday, August 2, 2010

Debugging your app remotely via Twilio SMS

Over the last few days, I've been camped out at a local pool for Madison's annual All City Swimming Championships. Twelve teams and 1,703 swimmers. A fun but exhausting couple of days. Managing your kids is a two-part challenge. The first part is actually getting them to the block on time for their race. The second part is trying to sort through the 100+ swimmers in the event to determine how well they did.

So I set out to solve the latter problem. I created an SMS notification system that sent text messages to parents letting them know what their swimmer's time was and more importantly, overall rank (note there are as many as 20+ heats for some events so you have no idea if they've qualified for the finals by simply watching). If a swimmer had a rank in the top sixteen, they'd have the opportunity to swim in the finals on Saturday.

As soon as the scorer's table has scored an event, they post the results to the web which was the trigger for the app to notify parents. It turned out to be hugely popular. So much so that I think there's a great opportunity to monetize it for next year's event. Parents at the meet and at work loved the little notes about their swimmers. Here's what one of my notifications looked like...

Tracy, Anna finished event 21 in 40.26, ranked 142

As much fun and useful as the app was, the most geeky and interesting element was actually the debugging part. The problem I faced was that I had no good way to test the app ahead of the meet. I had a pretty good idea how the meet scorers would format the data posted to the web, but there was no certainty. I also didn't have a lot of confidence that the app could actually follow the flow of the meet since the events don't go in order during the preliminary heats. 

Since I didn't have a laptop or access to the codebase, there wouldn't be a way to triage issues during the meet using traditional debug tools. The solution was to create SMS hooks that let me tweak the app as the meet unfolded. I combined this with the implementation of multiple regular expressions when parsing the results. I built the app on App Engine so I created multiple memcache'd variables that could be controlled via text messages from my phone. 

I coded in the following hooks...
  • The ability to set/reset the swim event being polled so I can re-run (or skip) events if there was a mistake

  • The ability to add new swimmers and phone numbers when parents wanted to be added to the app

  • The ability to disable the entire app. I was paranoid that I'd made a hideous mistake that continuously sent text messages out to users and I wanted a way to shunt the entire app.

  • The ability to query the app to determine the current event being monitored

  • The ability to modify the URL base variable used to find the results on the web

In addition to these hooks being controlled with inbound SMS, I also had a few app events that triggered outbound SMS to my phone so I knew it was behaving correctly. 

The hooks turned out to be invaluable on the first day. Both for keeping the app on the right event as well as adding parents to the app. I didn't actually have to use the emergency kill switch although it was nice to text 'disable' at the end of each day to make sure something didn't happen overnight.

The only downside to these hooks was actually a bug in the Google Voice app on my Droid. I was using a Google Voice number for the inbound events and one out of three texts I sent actually resulted in duplicate messages. The downside was when I added a new user, it added them twice (resulting in duplicate notifications when that swimmer swam!) and when I reset the event number, it reset it twice. 

Here's a look at the main handler for the inbound Twilio messages...

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


class MainHandler(webapp.RequestHandler):
  def post(self):
      
      # this handler is intended for admin use only
      # only accept calls from my own phone
      caller = self.request.get('From')
      if caller != '608XXXXXXX':
          logging.error('illegal caller %s with message %s' % (caller,self.request.get('Body')))
          return
      
      # validate it is in fact coming from twilio
      if ACCOUNT_SID == self.request.get('AccountSid'):
        logging.info("was confirmed to have come from Twilio (%s)." % caller)
      else:
        logging.info("was NOT VALID. It might have been spoofed (%s)!" % caller)
        return
      
      # determine the command from the message
      body = self.request.get('Body')
      if body is None:
          logging.error('empty command!?')
          return
      
      command = body.split()
      logging.info('processing new command %s from message %s' % (command[0],body))
      if command[0].lower() == 'help':
          # setup the response SMS
          smsBody = "disable, enable, event <#>, get, add <name> <name> <number>"
      elif command[0].lower() == 'disable':
          setService(False)
          smsBody = "turned service off!"
      elif command[0].lower() == 'enable':
          setService(True)
          smsBody = "turned service on!"
      elif command[0].lower() == 'add':
          addUser(body)
          smsBody = "added athlete %s" % command[1]
      elif command[0].lower() == 'event':
          setEvent(command[1])
          smsBody = "set event to %s" % command[1]
      elif command[0].lower() == 'get':
          smsBody = "current event is %s" % memcache.get('eventNumber')
      else:
          smsBody = "error... unsupported command [%s]" % command[0]
      logging.debug("responding to query with %s" % smsBody)
      r = twilio.Response()
      r.append(twilio.Sms(smsBody))
      self.response.out.write(r)


I forgot to take a picture in front of the results board at the meet. There's only one results board (for 1,700 swimmers), and each event is printed with a 10 point font. Most parents received their notification messages thirty minutes before the results board was updated!